mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Merge main
This commit is contained in:
@@ -773,17 +773,22 @@ jobs:
|
||||
run: >-
|
||||
pnpm exec vitest run tests/harness/worktree-files.test.ts
|
||||
--testNamePattern "workspace reset keeps Linux Sandbox paths POSIX"
|
||||
# The Node host checks with tsc-ts, the default, over the named pipe it
|
||||
# serves on Windows, and with tsgo.
|
||||
- name: Windows scriptc run smoke
|
||||
shell: pwsh
|
||||
run: |
|
||||
zig version
|
||||
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$actual = ($output -join "`n").Trim()
|
||||
if ($actual -ne "hello world") {
|
||||
throw "unexpected scriptc output: $actual"
|
||||
foreach ($checker in @("tsc-ts", "tsgo")) {
|
||||
$env:SCRIPTC_CHECKER = $checker
|
||||
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$actual = ($output -join "`n").Trim()
|
||||
if ($actual -ne "hello world") {
|
||||
throw "unexpected scriptc output with ${checker}: $actual"
|
||||
}
|
||||
}
|
||||
- name: Windows PE subsystem selection
|
||||
env:
|
||||
|
||||
@@ -12,6 +12,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-methods.mjs",
|
||||
"args": [
|
||||
"300000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Untyped JavaScript calling builtin methods on values it never declares
|
||||
// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array
|
||||
// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number
|
||||
// toFixed, and calls of an object's own methods. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "300000");
|
||||
const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"];
|
||||
const counts = new Map();
|
||||
const seen = new Set();
|
||||
const stack = [];
|
||||
const recent = [];
|
||||
const tally = {
|
||||
total: 0,
|
||||
add(value) {
|
||||
this.total += value;
|
||||
return this.total;
|
||||
},
|
||||
};
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const word = words[i % words.length];
|
||||
const head = word.slice(0, 3).toUpperCase();
|
||||
const key = head.padStart(4, "-");
|
||||
if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1;
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
seen.add(i % 97);
|
||||
if (seen.has(i % 89)) checksum += 2;
|
||||
stack.push(i);
|
||||
if (stack.length > 32) checksum += stack.pop() % 3;
|
||||
recent.push(word);
|
||||
if (recent.length > 4) recent.shift();
|
||||
if (recent.includes("gamma")) checksum += recent.indexOf("gamma");
|
||||
checksum += tally.add(i % 5) % 11;
|
||||
if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0;
|
||||
}
|
||||
console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total);
|
||||
@@ -206,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
<dd>A runtime pack directory to use instead of an installed <code>@scriptc/runtime-<target></code> package.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER</code></dt>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is <code>tsgo</code>, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.</dd>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" },
|
||||
runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v33");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v35");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#ifndef _WIN32
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
/* flock and LOCK_EX/LOCK_UN, which Darwin hides under _POSIX_C_SOURCE. */
|
||||
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
||||
#define _DARWIN_C_SOURCE 1
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/runtime": "workspace:*",
|
||||
"tsc-ts": "0.0.7",
|
||||
"tsc-ts": "0.0.8",
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
|
||||
@@ -153,6 +153,8 @@ export interface DynHost extends WalkerHost {
|
||||
/** See moduleUsesDynBuiltinStream. */
|
||||
usesDynBuiltinStream?(): boolean;
|
||||
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
|
||||
/** The module has declaration-typed boundary casts (IrModule.declBoundary). */
|
||||
declBoundary?(): boolean;
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
classSubtypes(className: string): readonly string[];
|
||||
@@ -4526,6 +4528,31 @@ export class LlDyn {
|
||||
/** The dyn argument spelling of one static value: dyn passes through
|
||||
* (+1), funcs box anonymously, everything else rides toDyn. `expr` is
|
||||
* BORROWED in every arm. Emits into the given builder. */
|
||||
/** A live capsule of a record or array value (+1; the value is
|
||||
* borrowed), as `dynFrom` with `liveRef` builds it; null for other
|
||||
* types. Closures boxed in a declaration-typed boundary program return
|
||||
* their records and arrays this way. */
|
||||
private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null {
|
||||
const host = this.host;
|
||||
if (t.kind === "union") {
|
||||
const arms = host.unionsById.get(t.unionId)?.arms ?? [];
|
||||
if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null;
|
||||
const boxed = B.tmp();
|
||||
B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`);
|
||||
return boxed;
|
||||
}
|
||||
if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null;
|
||||
const key = typeKey(t);
|
||||
const adapter = host.liveDynRefAdapter(t);
|
||||
const rc = vAdapters(host, t);
|
||||
const boxed = B.tmp();
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line);
|
||||
return boxed;
|
||||
}
|
||||
|
||||
private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string {
|
||||
if (t.kind === "dyn") return this.retainDyn(B, expr);
|
||||
if (t.kind === "func") {
|
||||
@@ -4657,6 +4684,26 @@ export class LlDyn {
|
||||
},
|
||||
"ptr null",
|
||||
);
|
||||
// Declaration-typed boundary programs: an argument package code
|
||||
// passes keeps its identity (scr_dyn_decl_adopt).
|
||||
const capsule = this.host.declBoundary?.() === true ? this.liveToDyn(B, p, a) : null;
|
||||
if (capsule !== null) {
|
||||
host.declare(`declare void @scr_dyn_decl_adopt(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
B.line(`call void @scr_dyn_decl_adopt(ptr ${ad}, ptr ${capsule})`);
|
||||
B.line(`call void @scr_dyn_release(ptr ${capsule})`);
|
||||
this.pendingBail(
|
||||
B,
|
||||
"dfk.adopt",
|
||||
() => {
|
||||
t.params.slice(0, i + 1).forEach((q, j) => {
|
||||
const name = j === i ? a : argNames[j];
|
||||
if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${name})`);
|
||||
});
|
||||
},
|
||||
"ptr null",
|
||||
);
|
||||
}
|
||||
argNames.push(a);
|
||||
}
|
||||
});
|
||||
@@ -4689,7 +4736,10 @@ export class LlDyn {
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ${retTy} ${fn}(${callArgs})`);
|
||||
this.pendingBail(B, "dfkr", () => {}, "ptr null");
|
||||
const out = this.toDynExpr(B, t.ret, r);
|
||||
const out =
|
||||
this.host.declBoundary?.() === true
|
||||
? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r))
|
||||
: this.toDynExpr(B, t.ret, r);
|
||||
if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`);
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
}
|
||||
|
||||
@@ -873,6 +873,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`);
|
||||
}
|
||||
if (emitter.mapOpsConsts.size > 0) out.push(``);
|
||||
for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`);
|
||||
if (emitter.methodIdSlots.size > 0) out.push(``);
|
||||
|
||||
for (const line of ffiCallbacks.globals) out.push(line);
|
||||
if (ffiCallbacks.globals.length > 0) out.push(``);
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
type IntegerRanges,
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
@@ -527,7 +528,9 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1076,6 +1079,10 @@ export class LlEmitter {
|
||||
readonly cstrs = new Map<string, { sym: string; len: number }>();
|
||||
/** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */
|
||||
readonly mapOpsConsts = new Map<string, string>();
|
||||
/** Method-id slots of checked-dynamic method calls
|
||||
* (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the
|
||||
* runtime fills with the name's id on the first call. */
|
||||
readonly methodIdSlots = new Map<string, string>();
|
||||
/** Type-directed walker functions (JSON serializers, the indent
|
||||
* rewriter, union ToString/join) — interned per typeKey/unionId, defs
|
||||
* flushed with the shape helpers. */
|
||||
@@ -1844,6 +1851,7 @@ export class LlEmitter {
|
||||
liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
|
||||
usesDynBuiltinStream: () => this.usesDynBuiltinStream,
|
||||
liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
|
||||
declBoundary: () => this.mod.declBoundary === true,
|
||||
dynPromiseAdapter: (type) => this.dynPromiseAdapter(type),
|
||||
isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error",
|
||||
classRootOf: (name) => this.classMeta.get(name)?.root.def.name,
|
||||
@@ -2736,6 +2744,17 @@ export class LlEmitter {
|
||||
return `@${c.sym}`;
|
||||
}
|
||||
|
||||
/** The method-id slot for a constant method name (methodIdSlots). */
|
||||
methodIdSlot(name: string): string {
|
||||
let sym = this.methodIdSlots.get(name);
|
||||
if (!sym) {
|
||||
sym = this.contentSymbol("sc_mid_", name);
|
||||
this.methodIdSlots.set(name, sym);
|
||||
}
|
||||
this.journal?.entry("methodIdSlot", name, sym);
|
||||
return `@${sym}`;
|
||||
}
|
||||
|
||||
needBadTag(): void {
|
||||
this.needsBadTag = true;
|
||||
}
|
||||
@@ -4853,6 +4872,15 @@ export class LlEmitter {
|
||||
if (own !== value) names.set(value as string, own);
|
||||
return;
|
||||
}
|
||||
case "methodIdSlot": {
|
||||
let own = this.methodIdSlots.get(key);
|
||||
if (own === undefined) {
|
||||
own = this.contentSymbol("sc_mid_", key);
|
||||
this.methodIdSlots.set(key, own);
|
||||
}
|
||||
if (own !== value) names.set(value as string, own);
|
||||
return;
|
||||
}
|
||||
case "classObj":
|
||||
addEntry(this.classObjs, key, value as { nameSym: string });
|
||||
return;
|
||||
|
||||
@@ -250,8 +250,10 @@ export function emitDynamicExpr(
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
@@ -260,7 +262,7 @@ export function emitDynamicExpr(
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
|
||||
@@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
|
||||
/** dynInvoke's method read on an ordinary object through the site's
|
||||
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
|
||||
* which scr_dyn_invoke_prepared answers with the full dispatch). */
|
||||
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
|
||||
export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string {
|
||||
const B = host.B;
|
||||
const method = e.method;
|
||||
|
||||
@@ -973,6 +973,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
|
||||
"dyn.classIs": "scr_dyn_class_is_key",
|
||||
"dyn.iterPack": "scr_dyn_iter_pack",
|
||||
"dyn.mathFold": "scr_dyn_math_fold",
|
||||
"dyn.destructureRest": "scr_dyn_destructure_rest",
|
||||
"dyn.iterator": "scr_dyn_iterator",
|
||||
"dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open",
|
||||
@@ -1587,6 +1588,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.objValueList": "scr_dyn_obj_value_list",
|
||||
"dyn.objEntries": "scr_dyn_obj_entries",
|
||||
"dyn.structuredClone": "scr_structured_clone",
|
||||
"dyn.declAdopt": "scr_dyn_decl_adopt",
|
||||
"dyn.cloneMissing": "scr_structured_clone_missing",
|
||||
"dyn.cloneTransferFail": "scr_structured_clone_transfer_fail",
|
||||
"dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with",
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface NativeLinkFeatures {
|
||||
/** Own properties JavaScript adds to children: scr_dyn_handle_props.c.
|
||||
* Absent (false) for programs without them. */
|
||||
handleProps?: boolean;
|
||||
declAdopt?: boolean;
|
||||
/** for await over checked-dynamic values: scr_dyn_for_await.c. Absent
|
||||
* (false) for programs without it. */
|
||||
dynForAwait?: boolean;
|
||||
|
||||
@@ -122,6 +122,7 @@ export interface CcOptions {
|
||||
streamDyn?: boolean;
|
||||
/** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */
|
||||
handleProps?: boolean;
|
||||
declAdopt?: boolean;
|
||||
/** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */
|
||||
dynForAwait?: boolean;
|
||||
/** events.once with options (events.onceSignal): compiles
|
||||
|
||||
@@ -279,6 +279,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"),
|
||||
// Own properties JavaScript adds to children.
|
||||
unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"),
|
||||
// The declaration-typed boundary's adoption.
|
||||
unit((u) => u.opts.declAdopt === true, "scr_decl_adopt.c"),
|
||||
// for await over checked-dynamic values.
|
||||
unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"),
|
||||
// for await over checked-dynamic values that may hold native streams.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 33 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const;
|
||||
export const RUNTIME_ABI_VERSION = 35 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v35" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -86,6 +86,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
dynTier: boolean;
|
||||
streamDyn: boolean;
|
||||
handleProps: boolean;
|
||||
declAdopt: boolean;
|
||||
dynForAwait: boolean;
|
||||
eventsOnceSignal: boolean;
|
||||
dynBuffer: boolean;
|
||||
@@ -283,6 +284,7 @@ export function effectiveRuntimeFeatures(
|
||||
dynTier: features.dynTier === true,
|
||||
streamDyn: features.streamDyn === true,
|
||||
handleProps: features.handleProps === true,
|
||||
declAdopt: features.declAdopt === true,
|
||||
dynForAwait: features.dynForAwait === true,
|
||||
eventsOnceSignal: features.eventsOnceSignal === true,
|
||||
dynBuffer: features.dynBuffer === true,
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" },
|
||||
runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" },
|
||||
runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dynTier?: boolean;
|
||||
streamDyn?: boolean;
|
||||
handleProps?: boolean;
|
||||
declAdopt?: boolean;
|
||||
dynForAwait?: boolean;
|
||||
eventsOnceSignal?: boolean;
|
||||
dynBuffer?: boolean;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { applyEdits, isDeclBoundaryFile, isDeclBoundaryTypeName } from "./decl-projection.js";
|
||||
|
||||
describe("decl-projection", () => {
|
||||
test("applyEdits closes the wrap that opened last first", () => {
|
||||
// `x.y` wrapped from 0 (outer) and from 2 (inner), both closing at 5:
|
||||
// closing parts rank by their wrap's negated start.
|
||||
const out = applyEdits("x = a.b;", [
|
||||
{ start: 4, end: 4, text: "(" },
|
||||
{ start: 7, end: 7, text: " as Outer)", rank: -4 },
|
||||
{ start: 6, end: 6, text: "(" },
|
||||
{ start: 7, end: 7, text: " as Inner)", rank: -6 },
|
||||
]);
|
||||
expect(out).toBe("x = (a.(b as Inner) as Outer);");
|
||||
});
|
||||
|
||||
test("applyEdits keeps the first of overlapping replacements and every line", () => {
|
||||
const text = "type A = z.infer<typeof S>;\nconst x = 1;";
|
||||
const out = applyEdits(text, [
|
||||
{ start: 9, end: 26, text: "__scriptc_decl_0" },
|
||||
{ start: 11, end: 16, text: "nope" },
|
||||
]);
|
||||
expect(out).toBe("type A = __scriptc_decl_0;\nconst x = 1;");
|
||||
});
|
||||
|
||||
test("names", () => {
|
||||
expect(isDeclBoundaryTypeName("__scriptc_decl_3")).toBe(true);
|
||||
expect(isDeclBoundaryTypeName("Product")).toBe(false);
|
||||
expect(isDeclBoundaryFile("/app/src/__scriptc_decl_boundary__.d.ts")).toBe(true);
|
||||
expect(isDeclBoundaryFile("/app/src/types.d.ts")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,793 @@
|
||||
/* The declaration-typed boundary's projection pass.
|
||||
*
|
||||
* An opted-in npm package compiles from its JavaScript, and the program the
|
||||
* lowering compiles types it from that JavaScript: where the package's
|
||||
* published declarations are richer (zod's `z.infer`), user code sees `any`.
|
||||
* This pass asks the DECLARATION world (the same entry checked against the
|
||||
* packages' declarations) for the types of those user-code positions. Where
|
||||
* the declared type is pure data, it is printed as structural TypeScript into
|
||||
* a generated global declaration file, and the user file is rewritten in the
|
||||
* lowering world:
|
||||
*
|
||||
* - type positions that name package types (`z.infer<typeof S>`) name the
|
||||
* projected alias instead;
|
||||
* - package-produced expressions (`S.parse(x)`, `r.data`) are wrapped in
|
||||
* `(<expr> as __scriptc_decl_N)`, the boundary cast;
|
||||
* - untyped parameters of user callbacks the package calls gain the alias.
|
||||
*
|
||||
* The lowering world is then rebuilt from the rewritten text, so the
|
||||
* checker's own inference carries the projected types through the rest of
|
||||
* the user code, and the lowering sees an ordinary checked cast at each
|
||||
* crossing (lowered with `adopt`, see narrowing.ts). No edit adds or removes
|
||||
* a line break, so positions keep their lines. IR records are structural,
|
||||
* so a printed type interns to the same shape as any equal type.
|
||||
*
|
||||
* Only programs whose user code has such positions are rewritten; every
|
||||
* other program is untouched (the zero-cost rule). See
|
||||
* scriptc-perf/results/design-decl-boundary/DESIGN.md. */
|
||||
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
|
||||
/** Alias names of projected types start with this prefix; the lowering
|
||||
* recognizes boundary casts and parameters by it. */
|
||||
export const DECL_BOUNDARY_PREFIX = "__scriptc_decl_";
|
||||
|
||||
/** The generated declaration file's base name. */
|
||||
export const DECL_BOUNDARY_FILE = "__scriptc_decl_boundary__.d.ts";
|
||||
|
||||
export function isDeclBoundaryFile(fileName: string): boolean {
|
||||
return fileName.split("\\").join("/").endsWith(`/${DECL_BOUNDARY_FILE}`);
|
||||
}
|
||||
|
||||
export function isDeclBoundaryTypeName(name: string): boolean {
|
||||
return name.startsWith(DECL_BOUNDARY_PREFIX);
|
||||
}
|
||||
|
||||
/** True for `T` in `x as T` / `(p: T) =>` when T names a projected alias. */
|
||||
export function isDeclBoundaryTypeNode(node: ts.TypeNode | undefined): boolean {
|
||||
return (
|
||||
node !== undefined &&
|
||||
ts.isTypeReferenceNode(node) &&
|
||||
ts.isIdentifier(node.typeName) &&
|
||||
isDeclBoundaryTypeName(node.typeName.text) &&
|
||||
node.typeArguments === undefined
|
||||
);
|
||||
}
|
||||
|
||||
interface Edit {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
/** Tie-break among edits at one position (lower first). */
|
||||
rank?: number;
|
||||
}
|
||||
|
||||
export interface DeclProjection {
|
||||
/** Rewritten text per user file. */
|
||||
files: Map<string, string>;
|
||||
/** The generated declaration file (path and text). */
|
||||
declFile: string;
|
||||
declText: string;
|
||||
/** Human-readable plan (SCRIPTC_DEBUG_DECL_BOUNDARY). */
|
||||
plan: string[];
|
||||
}
|
||||
|
||||
export interface DeclProjectionInput {
|
||||
jsProgram: ts.Program;
|
||||
declProgram: ts.Program;
|
||||
/** User source files to consider (TypeScript, outside node_modules). */
|
||||
userFiles: readonly string[];
|
||||
/** Directory of the generated declaration file. */
|
||||
outDir: string;
|
||||
}
|
||||
|
||||
function isPackageDeclarationFile(sf: ts.SourceFile | undefined): boolean {
|
||||
if (sf === undefined || !sf.isDeclarationFile) return false;
|
||||
const f = sf.fileName.split("\\").join("/");
|
||||
// The TypeScript standard library ships inside node_modules too.
|
||||
if (/(^|\/)lib(\.[\w.-]+)?\.d\.ts$/.test(f) || f.startsWith("bundled:")) return false;
|
||||
return (
|
||||
f.includes("/node_modules/") &&
|
||||
!f.includes("/node_modules/@types/node/") &&
|
||||
!f.includes("/node_modules/typescript/") &&
|
||||
!f.includes("/node_modules/@typescript/")
|
||||
);
|
||||
}
|
||||
|
||||
function declaredInPackage(symbol: ts.Symbol | undefined): boolean {
|
||||
if (symbol === undefined) return false;
|
||||
return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d)));
|
||||
}
|
||||
|
||||
function declaredInUserFile(symbol: ts.Symbol | undefined): boolean {
|
||||
if (symbol === undefined) return false;
|
||||
const decls = symbol.declarations;
|
||||
return (
|
||||
decls.length > 0 &&
|
||||
decls.every((d) => {
|
||||
const sf = ts.getSourceFileOfNode(d);
|
||||
return !sf.isDeclarationFile && !sf.fileName.split("\\").join("/").includes("/node_modules/");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const ANYISH = ts.TypeFlags.Any | ts.TypeFlags.Unknown;
|
||||
const READONLY_CHECK_FLAG = 8;
|
||||
|
||||
/** Prints declaration-world types as structural TypeScript, or answers null
|
||||
* when a type is not pure data (DESIGN.md §4.3). */
|
||||
class TypePrinter {
|
||||
readonly aliases = new Map<string, string>();
|
||||
readonly declarations: string[] = [];
|
||||
private readonly inProgress = new Map<number, { name: string | null }>();
|
||||
|
||||
constructor(private readonly checker: ts.TypeChecker) {}
|
||||
|
||||
/** The alias naming a projectable type, or null. Top-level any/unknown
|
||||
* never projects. */
|
||||
project(type: ts.Type): string | null {
|
||||
if (type.flags & ANYISH) return null;
|
||||
const text = this.print(type, 0);
|
||||
if (text === null || text === "unknown") return null;
|
||||
let name = this.aliases.get(text);
|
||||
if (name === undefined) {
|
||||
name = `${DECL_BOUNDARY_PREFIX}${this.aliases.size}`;
|
||||
this.aliases.set(text, name);
|
||||
this.declarations.push(`type ${name} = ${text};`);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private print(type: ts.Type, depth: number): string | null {
|
||||
if (depth > 24) return null;
|
||||
const f = type.flags;
|
||||
if (f & ANYISH) return "unknown";
|
||||
if (f & ts.TypeFlags.String) return "string";
|
||||
if (f & ts.TypeFlags.Number) return "number";
|
||||
if (f & ts.TypeFlags.Boolean) return "boolean";
|
||||
if (f & ts.TypeFlags.Null) return "null";
|
||||
if (f & ts.TypeFlags.Undefined) return "undefined";
|
||||
if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null;
|
||||
if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false";
|
||||
if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value));
|
||||
if (f & ts.TypeFlags.NumberLiteral) {
|
||||
const v = Number(type.value);
|
||||
return Number.isFinite(v) ? String(v) : null;
|
||||
}
|
||||
if (type.isUnionType()) {
|
||||
const parts: string[] = [];
|
||||
for (const arm of type.getTypes()) {
|
||||
const p = this.print(arm, depth + 1);
|
||||
if (p === null || p === "unknown") return null;
|
||||
if (!parts.includes(p)) parts.push(p);
|
||||
}
|
||||
// `true | false` reads better as boolean, and maps the same.
|
||||
const t = parts.indexOf("true");
|
||||
const fl = parts.indexOf("false");
|
||||
if (t >= 0 && fl >= 0) {
|
||||
parts.splice(Math.max(t, fl), 1);
|
||||
parts.splice(Math.min(t, fl), 1, "boolean");
|
||||
}
|
||||
return parts.map((p) => (/=>/.test(p) ? `(${p})` : p)).join(" | ");
|
||||
}
|
||||
if (type.isIntersectionType()) {
|
||||
// Brands: drop members that only carry unique-symbol keys.
|
||||
const kept = type.getTypes().filter((m) => !this.isBrandOnly(m));
|
||||
if (kept.length === 1) return this.print(kept[0]!, depth + 1);
|
||||
if (kept.length === 0) return null;
|
||||
if (!kept.every((m) => (m.flags & ts.TypeFlags.Object) !== 0)) return null;
|
||||
return this.printObject(type, depth);
|
||||
}
|
||||
if (f & ts.TypeFlags.Object) return this.printObject(type, depth);
|
||||
return null;
|
||||
}
|
||||
|
||||
private isBrandOnly(type: ts.Type): boolean {
|
||||
if (!(type.flags & ts.TypeFlags.Object) || type.isIntersectionType()) return false;
|
||||
const props = this.checker.getPropertiesOfType(type);
|
||||
return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@"));
|
||||
}
|
||||
|
||||
private printObject(type: ts.Type, depth: number): string | null {
|
||||
const checker = this.checker;
|
||||
if (checker.isArrayType(type)) {
|
||||
const [elem] = checker.getTypeArguments(type);
|
||||
if (elem === undefined) return null;
|
||||
const e = this.print(elem, depth + 1);
|
||||
if (e === null) return null;
|
||||
const ro = type.getSymbol()?.escapedName === "ReadonlyArray" ? "readonly " : "";
|
||||
return `${ro}(${e})[]`;
|
||||
}
|
||||
if (checker.isTupleType(type)) {
|
||||
const target = type.getTarget() ?? type;
|
||||
const flags = target.elementFlags ?? [];
|
||||
if (flags.some((fl) => fl !== ts.ElementFlags.Required)) return null;
|
||||
const elems = checker.getTypeArguments(type).slice(0, flags.length);
|
||||
const out: string[] = [];
|
||||
for (const e of elems) {
|
||||
const p = this.print(e, depth + 1);
|
||||
if (p === null) return null;
|
||||
out.push(p);
|
||||
}
|
||||
return `${target.readonly === true ? "readonly " : ""}[${out.join(", ")}]`;
|
||||
}
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol !== undefined) {
|
||||
if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method))
|
||||
return null;
|
||||
// Interfaces: only user-declared ones describe plain data reliably
|
||||
// (package and lib interfaces often describe class instances).
|
||||
if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null;
|
||||
}
|
||||
if (
|
||||
checker.getCallSignatures(type).length > 0 ||
|
||||
checker.getConstructSignatures(type).length > 0
|
||||
)
|
||||
return null;
|
||||
const memo = this.inProgress.get(type.id);
|
||||
if (memo !== undefined) {
|
||||
memo.name ??= `${DECL_BOUNDARY_PREFIX}r${type.id}`;
|
||||
return memo.name;
|
||||
}
|
||||
const frame: { name: string | null } = { name: null };
|
||||
this.inProgress.set(type.id, frame);
|
||||
try {
|
||||
const members: string[] = [];
|
||||
let readonlyCount = 0;
|
||||
const props = checker.getPropertiesOfType(type);
|
||||
for (const prop of props) {
|
||||
const key = String(prop.escapedName);
|
||||
if (key.startsWith("__@")) continue; // symbol-keyed (brands)
|
||||
if (
|
||||
prop.flags &
|
||||
(ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)
|
||||
)
|
||||
return null;
|
||||
const propType = checker.getTypeOfSymbol(prop);
|
||||
const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
|
||||
let p = this.print(propType, depth + 1);
|
||||
if (p === null) return null;
|
||||
const readonly = (prop.checkFlags & READONLY_CHECK_FLAG) !== 0 || hasReadonlyModifier(prop);
|
||||
if (readonly) readonlyCount++;
|
||||
const name = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
|
||||
if (optional && !/\bundefined\b/.test(p)) p = `${p} | undefined`;
|
||||
members.push(`${readonly ? "readonly " : ""}${name}${optional ? "?" : ""}: ${p}`);
|
||||
}
|
||||
for (const info of checker.getIndexInfosOfType(type)) {
|
||||
if (!(info.keyType.flags & ts.TypeFlags.String)) return null;
|
||||
const v = this.print(info.type, depth + 1);
|
||||
if (v === null) return null;
|
||||
members.push(`[key: string]: ${v}`);
|
||||
}
|
||||
if (members.length === 0) return null; // `{}` admits any non-nullish value
|
||||
// All-readonly object types may describe frozen values (.readonly()).
|
||||
if (props.length > 0 && readonlyCount === props.length) return null;
|
||||
const text = `{ ${members.join("; ")} }`;
|
||||
if (frame.name !== null) {
|
||||
this.declarations.push(`type ${frame.name} = ${text};`);
|
||||
return frame.name;
|
||||
}
|
||||
return text;
|
||||
} finally {
|
||||
this.inProgress.delete(type.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasReadonlyModifier(prop: ts.Symbol): boolean {
|
||||
return prop.declarations.some(
|
||||
(d) =>
|
||||
(ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) &&
|
||||
(d.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword),
|
||||
);
|
||||
}
|
||||
|
||||
/** Children of a node in source order (forEachChild order). */
|
||||
function childrenOf(node: ts.Node): ts.Node[] {
|
||||
const out: ts.Node[] = [];
|
||||
ts.forEachChild(
|
||||
node,
|
||||
(c) => {
|
||||
out.push(c);
|
||||
return undefined;
|
||||
},
|
||||
(list) => {
|
||||
for (const c of list.nodes) out.push(c);
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
function isWriteTarget(node: ts.Node): boolean {
|
||||
let child = node;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined) return false;
|
||||
if (ts.isBinaryExpression(parent) && parent.left === child) {
|
||||
const k = parent.operatorToken.kind;
|
||||
return k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment;
|
||||
}
|
||||
if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) {
|
||||
const op = parent.operator;
|
||||
return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken;
|
||||
}
|
||||
if (ts.isDeleteExpression(parent)) return true;
|
||||
if (ts.isForOfStatement(parent) || ts.isForInStatement(parent))
|
||||
return parent.initializer === child;
|
||||
if (ts.isShorthandPropertyAssignment(parent)) return true;
|
||||
if (
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isSpreadElement(parent)
|
||||
) {
|
||||
// Destructuring assignment targets: conservative, skip.
|
||||
let up: ts.Node | undefined = parent;
|
||||
while (
|
||||
up !== undefined &&
|
||||
(ts.isArrayLiteralExpression(up) ||
|
||||
ts.isObjectLiteralExpression(up) ||
|
||||
ts.isPropertyAssignment(up) ||
|
||||
ts.isSpreadElement(up))
|
||||
)
|
||||
up = up.parent;
|
||||
if (
|
||||
up !== undefined &&
|
||||
ts.isBinaryExpression(up) &&
|
||||
up.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function inOptionalChain(node: ts.Node): boolean {
|
||||
const parent = node.parent;
|
||||
if (parent === undefined) return false;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(parent) ||
|
||||
ts.isElementAccessExpression(parent) ||
|
||||
ts.isCallExpression(parent)) &&
|
||||
parent.expression === node &&
|
||||
(parent.questionDotToken !== undefined || (parent.flags & ts.NodeFlags.OptionalChain) !== 0)
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
(ts.isPropertyAccessExpression(node) ||
|
||||
ts.isElementAccessExpression(node) ||
|
||||
ts.isCallExpression(node)) &&
|
||||
(node.flags & ts.NodeFlags.OptionalChain) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
function isCallee(node: ts.Node): boolean {
|
||||
const p = node.parent;
|
||||
return (
|
||||
p !== undefined &&
|
||||
(ts.isCallExpression(p) || ts.isNewExpression(p) || ts.isTaggedTemplateExpression(p)) &&
|
||||
(ts.isTaggedTemplateExpression(p) ? p.tag === node : p.expression === node)
|
||||
);
|
||||
}
|
||||
|
||||
function inTypeContext(node: ts.Node): boolean {
|
||||
for (let p = node.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isTypeNode(p) || ts.isTypeQueryNode(p)) return true;
|
||||
if (ts.isExpressionWithTypeArguments(p) && p.parent && ts.isHeritageClause(p.parent))
|
||||
return true;
|
||||
if (ts.isStatement(p)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function unwrapTransparent(node: ts.Expression): ts.Expression {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e))
|
||||
e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Plans the projection for one program pair. Answers null when nothing
|
||||
* in the user files projects (the program stays as it is). */
|
||||
export function planDeclProjection(input: DeclProjectionInput): DeclProjection | null {
|
||||
const jsChecker = input.jsProgram.getTypeChecker();
|
||||
const declChecker = input.declProgram.getTypeChecker();
|
||||
const printer = new TypePrinter(declChecker);
|
||||
const plan: string[] = [];
|
||||
const edits = new Map<string, Edit[]>();
|
||||
|
||||
const typeAt = (checker: ts.TypeChecker, node: ts.Node): ts.Type | null => {
|
||||
try {
|
||||
return checker.getTypeAtLocation(node);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const isAnyish = (t: ts.Type | null): boolean => t !== null && (t.flags & ANYISH) !== 0;
|
||||
const resolveAlias = (s: ts.Symbol | undefined): ts.Symbol | undefined => {
|
||||
if (s === undefined || !(s.flags & ts.SymbolFlags.Alias)) return s;
|
||||
try {
|
||||
return declChecker.getAliasedSymbol(s);
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
const declSymbol = (n: ts.Node): ts.Symbol | undefined => {
|
||||
try {
|
||||
return resolveAlias(declChecker.getSymbolAtLocation(n));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const namesPackageSymbol = (node: ts.Node): boolean => {
|
||||
let found = false;
|
||||
const visit = (n: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (ts.isIdentifier(n) && declaredInPackage(declSymbol(n))) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
for (const c of childrenOf(n)) visit(c);
|
||||
};
|
||||
visit(node);
|
||||
return found;
|
||||
};
|
||||
|
||||
for (const file of input.userFiles) {
|
||||
const jsSf = input.jsProgram.getSourceFile(file);
|
||||
const declSf = input.declProgram.getSourceFile(file);
|
||||
if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue;
|
||||
const fileEdits: Edit[] = [];
|
||||
/** Candidate expressions (by js node) with their alias. */
|
||||
const exprCandidates = new Map<ts.Node, string>();
|
||||
const exprOrder: ts.Node[] = [];
|
||||
|
||||
const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => {
|
||||
if (
|
||||
!ts.isTypeReferenceNode(jsNode) &&
|
||||
!ts.isIndexedAccessTypeNode(jsNode) &&
|
||||
!ts.isExpressionWithTypeArguments(jsNode)
|
||||
)
|
||||
return false;
|
||||
if (ts.isExpressionWithTypeArguments(jsNode)) {
|
||||
if (!jsNode.parent || !ts.isHeritageClause(jsNode.parent)) return false;
|
||||
if (!jsNode.parent.parent || !ts.isInterfaceDeclaration(jsNode.parent.parent)) return false;
|
||||
}
|
||||
let jsType: ts.Type | null;
|
||||
let declType: ts.Type | null;
|
||||
try {
|
||||
jsType = ts.isExpressionWithTypeArguments(jsNode)
|
||||
? jsChecker.getTypeAtLocation(jsNode)
|
||||
: jsChecker.getTypeFromTypeNode(jsNode as ts.TypeNode);
|
||||
declType = ts.isExpressionWithTypeArguments(declNode)
|
||||
? declChecker.getTypeAtLocation(declNode)
|
||||
: declChecker.getTypeFromTypeNode(declNode as ts.TypeNode);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!isAnyish(jsType) || declType === null) return false;
|
||||
if (!namesPackageSymbol(declNode)) return false;
|
||||
const alias = printer.project(declType);
|
||||
if (alias === null) return false;
|
||||
const start = ts.tokenStartOfNode(jsNode);
|
||||
fileEdits.push({ start, end: jsNode.end, text: alias });
|
||||
plan.push(`${file}:${start}: type ${jsSf.text.slice(start, jsNode.end)} -> ${alias}`);
|
||||
return true;
|
||||
};
|
||||
|
||||
const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => {
|
||||
if (!ts.isExpression(jsNode)) return;
|
||||
const isCall =
|
||||
ts.isCallExpression(jsNode) ||
|
||||
ts.isNewExpression(jsNode) ||
|
||||
ts.isTaggedTemplateExpression(jsNode);
|
||||
const isAccess =
|
||||
ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode);
|
||||
const isAwait = ts.isAwaitExpression(jsNode);
|
||||
if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return;
|
||||
if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return;
|
||||
if (inOptionalChain(jsNode)) return;
|
||||
const parent = jsNode.parent;
|
||||
if (parent !== undefined && ts.isExpressionStatement(parent)) return;
|
||||
if (
|
||||
parent !== undefined &&
|
||||
(ts.isImportSpecifier(parent) ||
|
||||
ts.isImportClause(parent) ||
|
||||
ts.isNamespaceImport(parent) ||
|
||||
ts.isExportSpecifier(parent))
|
||||
)
|
||||
return;
|
||||
if (ts.isIdentifier(jsNode)) {
|
||||
if (parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.name === jsNode)
|
||||
return;
|
||||
}
|
||||
const jsType = typeAt(jsChecker, jsNode);
|
||||
if (!isAnyish(jsType)) return;
|
||||
// Package-produced?
|
||||
const d = declNode as ts.Expression;
|
||||
let produced = false;
|
||||
if (ts.isCallExpression(d) || ts.isNewExpression(d)) {
|
||||
const callee = d.expression;
|
||||
const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
|
||||
produced = declaredInPackage(declSymbol(target));
|
||||
if (!produced) {
|
||||
try {
|
||||
const sig = declChecker.getResolvedSignature(d);
|
||||
const decl = sig?.declaration;
|
||||
produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl));
|
||||
} catch {
|
||||
produced = false;
|
||||
}
|
||||
}
|
||||
} else if (ts.isTaggedTemplateExpression(d)) {
|
||||
produced = declaredInPackage(declSymbol(d.tag));
|
||||
} else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) {
|
||||
const recvDecl = typeAt(declChecker, d.expression);
|
||||
const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression);
|
||||
const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null;
|
||||
produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs));
|
||||
} else if (ts.isAwaitExpression(d)) {
|
||||
const inner = unwrapTransparent(d.expression);
|
||||
produced =
|
||||
(ts.isCallExpression(inner) || ts.isPropertyAccessExpression(inner)) &&
|
||||
(declaredInPackage(
|
||||
declSymbol(
|
||||
ts.isCallExpression(inner)
|
||||
? ts.isPropertyAccessExpression(inner.expression)
|
||||
? inner.expression.name
|
||||
: inner.expression
|
||||
: inner.name,
|
||||
),
|
||||
) ||
|
||||
isAnyish(typeAt(jsChecker, inner)));
|
||||
} else if (ts.isIdentifier(d)) {
|
||||
const raw = (() => {
|
||||
try {
|
||||
return declChecker.getSymbolAtLocation(d);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
produced =
|
||||
raw !== undefined &&
|
||||
(raw.flags & ts.SymbolFlags.Alias) !== 0 &&
|
||||
declaredInPackage(resolveAlias(raw));
|
||||
}
|
||||
if (!produced) return;
|
||||
// A consumer that takes anything needs no conversion.
|
||||
try {
|
||||
const ctx = declChecker.getContextualType(d);
|
||||
if (ctx !== undefined && ctx.flags & ANYISH) return;
|
||||
} catch {
|
||||
// no contextual type
|
||||
}
|
||||
const declType = typeAt(declChecker, d);
|
||||
if (declType === null) return;
|
||||
const alias = printer.project(declType);
|
||||
if (alias === null) return;
|
||||
exprCandidates.set(jsNode, alias);
|
||||
exprOrder.push(jsNode);
|
||||
};
|
||||
|
||||
const annotated = new Set<ts.Node>();
|
||||
const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => {
|
||||
if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name))
|
||||
return;
|
||||
const fn = jsNode.parent;
|
||||
if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return;
|
||||
if (jsNode.dotDotDotToken !== undefined) return;
|
||||
const jsType = typeAt(jsChecker, jsNode.name);
|
||||
if (!isAnyish(jsType)) return;
|
||||
const declType = typeAt(declChecker, (declNode as ts.ParameterDeclaration).name);
|
||||
if (declType === null) return;
|
||||
// Only parameters the package's declarations type (a callback the
|
||||
// package calls), not ones the user typed elsewhere.
|
||||
try {
|
||||
const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression);
|
||||
if (ctxFn === undefined) return;
|
||||
const sigs = declChecker.getCallSignatures(ctxFn);
|
||||
if (
|
||||
!sigs.some(
|
||||
(s) =>
|
||||
s.declaration !== undefined &&
|
||||
isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)),
|
||||
)
|
||||
)
|
||||
return;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const alias = printer.project(declType);
|
||||
if (alias === null) return;
|
||||
annotated.add(jsNode);
|
||||
const name = jsNode.name;
|
||||
const nameStart = ts.tokenStartOfNode(name);
|
||||
const text = jsSf.text;
|
||||
// `x => ...` or `async x => ...`: nothing but the modifiers comes
|
||||
// before the parameter, which then needs parentheses for its type.
|
||||
const head = ts.isArrowFunction(fn)
|
||||
? ((ts.getModifiers(fn) ?? []).at(-1)?.end ?? ts.tokenStartOfNode(fn))
|
||||
: nameStart;
|
||||
const bareArrow =
|
||||
ts.isArrowFunction(fn) &&
|
||||
fn.parameters.nodes.length === 1 &&
|
||||
text.slice(head, nameStart).trim() === "";
|
||||
const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end;
|
||||
if (bareArrow) {
|
||||
fileEdits.push({ start: nameStart, end: nameStart, text: "(" });
|
||||
fileEdits.push({ start: afterName, end: afterName, text: `: ${alias})` });
|
||||
} else {
|
||||
fileEdits.push({ start: afterName, end: afterName, text: `: ${alias}` });
|
||||
}
|
||||
plan.push(`${file}:${nameStart}: parameter ${name.text}: ${alias}`);
|
||||
};
|
||||
|
||||
const walk = (jsNode: ts.Node, declNode: ts.Node): void => {
|
||||
if (jsNode.kind !== declNode.kind) return;
|
||||
if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) {
|
||||
if (considerType(jsNode, declNode)) return;
|
||||
} else {
|
||||
considerParam(jsNode, declNode);
|
||||
considerExpr(jsNode, declNode);
|
||||
}
|
||||
const a = childrenOf(jsNode);
|
||||
const b = childrenOf(declNode);
|
||||
if (a.length !== b.length) return;
|
||||
for (let i = 0; i < a.length; i++) walk(a[i]!, b[i]!);
|
||||
};
|
||||
walk(jsSf, declSf);
|
||||
|
||||
// Suppress candidates whose receiver chain another candidate types.
|
||||
const symbolTyped = new Map<ts.Symbol, boolean>();
|
||||
const willBeTyped = (e: ts.Node, depth = 0): boolean => {
|
||||
if (depth > 32) return false;
|
||||
if (exprCandidates.has(e)) return true;
|
||||
if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e))
|
||||
return willBeTyped(e.expression, depth + 1);
|
||||
if (ts.isAsExpression(e)) return false;
|
||||
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))
|
||||
return willBeTyped(e.expression, depth + 1);
|
||||
if (ts.isIdentifier(e)) {
|
||||
let sym: ts.Symbol | undefined;
|
||||
try {
|
||||
sym = jsChecker.getSymbolAtLocation(e);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (sym === undefined) return false;
|
||||
const known = symbolTyped.get(sym);
|
||||
if (known !== undefined) return known;
|
||||
symbolTyped.set(sym, false);
|
||||
let result = false;
|
||||
const decls = sym.declarations;
|
||||
if (decls.length === 1) {
|
||||
const decl = decls[0]!;
|
||||
if (
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
decl.type === undefined
|
||||
)
|
||||
result = willBeTyped(decl.initializer, depth + 1);
|
||||
else if (ts.isParameter(decl) && annotated.has(decl)) result = true;
|
||||
else if (ts.isBindingElement(decl)) result = false;
|
||||
}
|
||||
symbolTyped.set(sym, result);
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const receiverOf = (e: ts.Node): ts.Node | null => {
|
||||
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression;
|
||||
if (ts.isIdentifier(e)) return null;
|
||||
return null;
|
||||
};
|
||||
const kept: ts.Node[] = [];
|
||||
for (const e of exprOrder) {
|
||||
const recv = receiverOf(e);
|
||||
if (recv !== null && willBeTyped(recv)) continue;
|
||||
// An identifier candidate that is a user binding never gets here
|
||||
// (only package imports are candidates).
|
||||
kept.push(e);
|
||||
}
|
||||
// Nested candidates: keep only the innermost (an outer one is typed by
|
||||
// the inner wrap).
|
||||
const keptSet = new Set(kept);
|
||||
const containsKept = (outer: ts.Node): boolean => {
|
||||
let found = false;
|
||||
const visit = (n: ts.Node): void => {
|
||||
if (found) return;
|
||||
for (const c of childrenOf(n)) {
|
||||
if (keptSet.has(c)) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
visit(c);
|
||||
}
|
||||
};
|
||||
visit(outer);
|
||||
return found;
|
||||
};
|
||||
for (const e of kept) {
|
||||
if (
|
||||
!ts.isAwaitExpression(e) &&
|
||||
containsKept(e) &&
|
||||
(ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))
|
||||
) {
|
||||
// `a.b.c` with `a.b` wrapped: the outer access reads typed data.
|
||||
const recv = receiverOf(e);
|
||||
if (
|
||||
recv !== null &&
|
||||
(keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv))
|
||||
)
|
||||
continue;
|
||||
}
|
||||
const alias = exprCandidates.get(e)!;
|
||||
const start = ts.tokenStartOfNode(e);
|
||||
const stmt = e.parent;
|
||||
let prefix = "(";
|
||||
// A statement that would start with `(` after a line without a
|
||||
// semicolon continues the previous expression: guard it.
|
||||
let up: ts.Node | undefined = e;
|
||||
while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent;
|
||||
// Only a statement in a statement list can follow such a line; an
|
||||
// unbraced body (`for (...) S.parse(x).run()`) follows its header,
|
||||
// and a `;` there would become the body.
|
||||
const list = up?.parent?.parent;
|
||||
if (
|
||||
up?.parent !== undefined &&
|
||||
ts.isExpressionStatement(up.parent) &&
|
||||
ts.tokenStartOfNode(up.parent) === start &&
|
||||
list !== undefined &&
|
||||
(ts.isBlock(list) ||
|
||||
ts.isSourceFile(list) ||
|
||||
ts.isModuleBlock(list) ||
|
||||
ts.isCaseClause(list) ||
|
||||
ts.isDefaultClause(list))
|
||||
)
|
||||
prefix = ";(";
|
||||
void stmt;
|
||||
fileEdits.push({ start, end: start, text: prefix });
|
||||
// Closing parts at one position close inner wraps first.
|
||||
fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start });
|
||||
plan.push(
|
||||
`${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (fileEdits.length === 0) continue;
|
||||
edits.set(file, fileEdits);
|
||||
}
|
||||
|
||||
if (edits.size === 0) return null;
|
||||
const files = new Map<string, string>();
|
||||
for (const [file, list] of edits) {
|
||||
const sf = input.jsProgram.getSourceFile(file)!;
|
||||
files.set(file, applyEdits(sf.text, list));
|
||||
}
|
||||
const declFile = `${input.outDir.split("\\").join("/")}/${DECL_BOUNDARY_FILE}`;
|
||||
const declText =
|
||||
"// Generated by scriptc: types projected from npm package declarations.\n" +
|
||||
printer.declarations.join("\n") +
|
||||
"\n";
|
||||
return { files, declFile, declText, plan };
|
||||
}
|
||||
|
||||
/** Applies insertions and replacements; overlapping replacements keep the
|
||||
* first. Insertions at one position keep their order. */
|
||||
export function applyEdits(text: string, edits: readonly Edit[]): string {
|
||||
const sorted = edits
|
||||
.map((e, i) => ({ ...e, i }))
|
||||
.sort(
|
||||
(a, b) => a.start - b.start || a.end - b.end || (a.rank ?? 0) - (b.rank ?? 0) || a.i - b.i,
|
||||
);
|
||||
let out = "";
|
||||
let at = 0;
|
||||
for (const e of sorted) {
|
||||
if (e.start < at) continue; // overlaps an earlier replacement
|
||||
out += text.slice(at, e.start) + e.text;
|
||||
at = e.end;
|
||||
}
|
||||
return out + text.slice(at);
|
||||
}
|
||||
@@ -34,6 +34,31 @@ export function packageShimRoot(): string {
|
||||
: join(installedDeclarationRoot, "shims");
|
||||
}
|
||||
|
||||
/** The directories of files scriptc itself ships, each with the prefix
|
||||
* that names its files independently of where scriptc is installed
|
||||
* (compilerOwnedFileName). IR artifacts spell source locations in them this
|
||||
* way (serializeModuleParts). */
|
||||
export function compilerOwnedFileRoots(): readonly { from: string; to: string }[] {
|
||||
return [{ from: tsgoPath(packageShimRoot()) + "/", to: "scriptc:shims/" }];
|
||||
}
|
||||
|
||||
/** The name of a file scriptc itself ships (a package shim or one of the
|
||||
* shipped declaration files) that does not depend on where scriptc is
|
||||
* installed: `scriptc:shims/<path below the shim root>` or
|
||||
* `scriptc:<declaration file name>`. Null for every other file. Names and
|
||||
* artifacts that would otherwise spell the installation's absolute path use
|
||||
* it, so two installations of one compiler emit the same output. */
|
||||
export function compilerOwnedFileName(file: string): string | null {
|
||||
const path = tsgoPath(file);
|
||||
for (const { from, to } of compilerOwnedFileRoots()) {
|
||||
if (path.startsWith(from)) return to + path.slice(from.length);
|
||||
}
|
||||
for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) {
|
||||
if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Path of the shipped ambient declarations — the always-shipped CORE
|
||||
* (comptime, setTimeout). Part of EVERY program scriptc
|
||||
* builds, the project-world preflight program included. */
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/* The string-keyed properties Node 24 exposes on its builtin constructor
|
||||
* and namespace globals, own and inherited, so `"key" in Error` (a feature
|
||||
* probe such as `"captureStackTrace" in Error`) answers statically. The
|
||||
* global must be the unshadowed builtin; compiled programs cannot add
|
||||
* properties to these objects, so the answer is fixed. */
|
||||
|
||||
const FUNCTION_PROTOTYPE = [
|
||||
"length",
|
||||
"name",
|
||||
"arguments",
|
||||
"caller",
|
||||
"constructor",
|
||||
"apply",
|
||||
"bind",
|
||||
"call",
|
||||
"toString",
|
||||
];
|
||||
const OBJECT_PROTOTYPE = [
|
||||
"constructor",
|
||||
"__defineGetter__",
|
||||
"__defineSetter__",
|
||||
"hasOwnProperty",
|
||||
"__lookupGetter__",
|
||||
"__lookupSetter__",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"__proto__",
|
||||
"toLocaleString",
|
||||
];
|
||||
const CONSTRUCTOR_OWN = ["length", "name", "prototype"];
|
||||
const ERROR_OWN = [
|
||||
...CONSTRUCTOR_OWN,
|
||||
"captureStackTrace",
|
||||
"prepareStackTrace",
|
||||
"isError",
|
||||
"stackTraceLimit",
|
||||
];
|
||||
|
||||
/** Own string keys per global, beyond what the prototype chain adds. */
|
||||
const CONSTRUCTORS: Readonly<Record<string, readonly string[]>> = {
|
||||
Error: ERROR_OWN,
|
||||
// The native error constructors inherit from Error.
|
||||
TypeError: ERROR_OWN,
|
||||
RangeError: ERROR_OWN,
|
||||
SyntaxError: ERROR_OWN,
|
||||
ReferenceError: ERROR_OWN,
|
||||
EvalError: ERROR_OWN,
|
||||
URIError: ERROR_OWN,
|
||||
Object: [
|
||||
...CONSTRUCTOR_OWN,
|
||||
"assign",
|
||||
"getOwnPropertyDescriptor",
|
||||
"getOwnPropertyDescriptors",
|
||||
"getOwnPropertyNames",
|
||||
"getOwnPropertySymbols",
|
||||
"hasOwn",
|
||||
"is",
|
||||
"preventExtensions",
|
||||
"seal",
|
||||
"create",
|
||||
"defineProperties",
|
||||
"defineProperty",
|
||||
"freeze",
|
||||
"getPrototypeOf",
|
||||
"setPrototypeOf",
|
||||
"isExtensible",
|
||||
"isFrozen",
|
||||
"isSealed",
|
||||
"keys",
|
||||
"entries",
|
||||
"fromEntries",
|
||||
"values",
|
||||
"groupBy",
|
||||
],
|
||||
Array: [...CONSTRUCTOR_OWN, "isArray", "from", "fromAsync", "of"],
|
||||
Promise: [
|
||||
...CONSTRUCTOR_OWN,
|
||||
"all",
|
||||
"allSettled",
|
||||
"any",
|
||||
"race",
|
||||
"resolve",
|
||||
"reject",
|
||||
"withResolvers",
|
||||
"try",
|
||||
],
|
||||
Number: [
|
||||
...CONSTRUCTOR_OWN,
|
||||
"isFinite",
|
||||
"isInteger",
|
||||
"isNaN",
|
||||
"isSafeInteger",
|
||||
"parseFloat",
|
||||
"parseInt",
|
||||
"MAX_VALUE",
|
||||
"MIN_VALUE",
|
||||
"NaN",
|
||||
"NEGATIVE_INFINITY",
|
||||
"POSITIVE_INFINITY",
|
||||
"MAX_SAFE_INTEGER",
|
||||
"MIN_SAFE_INTEGER",
|
||||
"EPSILON",
|
||||
],
|
||||
String: [...CONSTRUCTOR_OWN, "fromCharCode", "fromCodePoint", "raw"],
|
||||
Map: [...CONSTRUCTOR_OWN, "groupBy"],
|
||||
Set: CONSTRUCTOR_OWN,
|
||||
WeakMap: CONSTRUCTOR_OWN,
|
||||
WeakSet: CONSTRUCTOR_OWN,
|
||||
Boolean: CONSTRUCTOR_OWN,
|
||||
Function: CONSTRUCTOR_OWN,
|
||||
Date: [...CONSTRUCTOR_OWN, "now", "parse", "UTC"],
|
||||
BigInt: [...CONSTRUCTOR_OWN, "asUintN", "asIntN"],
|
||||
};
|
||||
|
||||
/** Whether Node answers `key in <name>` with true, or null when `name` is
|
||||
* not a tabled global. */
|
||||
export function builtinGlobalHasKey(name: string, key: string): boolean | null {
|
||||
const own = CONSTRUCTORS[name];
|
||||
if (own === undefined) return null;
|
||||
return own.includes(key) || FUNCTION_PROTOTYPE.includes(key) || OBJECT_PROTOTYPE.includes(key);
|
||||
}
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
STRING,
|
||||
VOID,
|
||||
isClassOwnEnumerableFieldName,
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { PoisonError, type Lowerer } from "./lowerer.js";
|
||||
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { observedPrototypeMethod } from "./class-prototypes.js";
|
||||
@@ -162,6 +164,7 @@ export function classCallbackCall(
|
||||
fallback: IrExpr,
|
||||
loc: SrcLoc,
|
||||
calleeName = name,
|
||||
original?: { originalId: () => string | undefined; directCall: () => IrExpr },
|
||||
): IrExpr {
|
||||
const bag = lowerer.declareHiddenLocal("%callbackBag", DYN);
|
||||
const present = lowerer.declareHiddenLocal("%callbackPresent", BOOL);
|
||||
@@ -186,27 +189,91 @@ export function classCallbackCall(
|
||||
loc,
|
||||
};
|
||||
const observed = observedSlot(lowerer, receiver, name, loc);
|
||||
const result: IrExpr = observed
|
||||
? call
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: varRef(present.id, BOOL, loc),
|
||||
then: call,
|
||||
else_:
|
||||
fallback.type.kind === "void"
|
||||
? {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: fallback, loc }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: lowerer.coerceToExpected(fallback, DYN),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const answer =
|
||||
fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type);
|
||||
const boxed =
|
||||
observed || fallback.type.kind === "void" ? null : lowerer.coerceToExpected(fallback, DYN);
|
||||
let answer: IrExpr;
|
||||
if (boxed !== null && boxed.type.kind !== "dyn") {
|
||||
// A result with no checked-dynamic form (an Iterable<T>, whose
|
||||
// generator arm does not box) keeps its native type: the unchanged
|
||||
// method's result stays as it is, and a replacement's dynamic result
|
||||
// takes the checked conversion into that type.
|
||||
const converted = lowerer.coerceToExpected(call, fallback.type);
|
||||
if (!typeEquals(converted.type, fallback.type)) {
|
||||
lowerer.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
loc,
|
||||
`calling replaceable method '${calleeName}' with a '${lowerer.fmt(fallback.type)}' result`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
answer = {
|
||||
kind: "ternary",
|
||||
cond: varRef(present.id, BOOL, loc),
|
||||
then: converted,
|
||||
else_: fallback,
|
||||
type: fallback.type,
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
const result: IrExpr = observed
|
||||
? call
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: varRef(present.id, BOOL, loc),
|
||||
then: call,
|
||||
else_: boxed ?? {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: fallback, loc }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
answer =
|
||||
fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type);
|
||||
}
|
||||
// Read after observedSlot built the prototype data and its originals.
|
||||
const originalId = observed ? original?.originalId() : undefined;
|
||||
if (original && originalId !== undefined) {
|
||||
// The slot still holds the method's original value: calling it is
|
||||
// calling the method, so the call stays native (no boxed arguments,
|
||||
// dynamic call or checked result). A replacement takes the dynamic call.
|
||||
const unchanged: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.sameValue",
|
||||
args: [varRef(callback.id, DYN, loc), varRef(originalId, DYN, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
answer =
|
||||
fallback.type.kind === "void"
|
||||
? {
|
||||
kind: "ternary",
|
||||
cond: unchanged,
|
||||
then: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: original.directCall(), loc }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: call,
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: unchanged,
|
||||
then: original.directCall(),
|
||||
else_: answer,
|
||||
type: answer.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
|
||||
@@ -908,6 +908,11 @@ export class ClassDynamicDispatch {
|
||||
private readonly prototypeClasses = new Set<string>();
|
||||
private prototypeIdentityHelper: IrFunction | null = null;
|
||||
private readonly prototypeIdentityClasses = new Set<string>();
|
||||
/** `value instanceof Error` through Error.prototype: boxed instances of
|
||||
* classes with a heritage evaluated at run time test their prototype
|
||||
* data's chain. */
|
||||
private errorPrototypeHelper: IrFunction | null = null;
|
||||
private readonly errorPrototypeClasses = new Set<string>();
|
||||
private readonly instancePrototypes = new Map<string, { fn: IrFunction; classes: Set<string> }>();
|
||||
private readonly checkedCasts = new Map<string, { fn: IrFunction; classes: Set<string> }>();
|
||||
/** The for-in key helpers, by libCall (dyn and native string[] keys). */
|
||||
@@ -1114,6 +1119,49 @@ export class ClassDynamicDispatch {
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
this.errorPrototypeHelper &&
|
||||
!this.errorPrototypeClasses.has(className) &&
|
||||
hasRuntimeHeritage(info)
|
||||
) {
|
||||
this.errorPrototypeClasses.add(className);
|
||||
const loc = this.errorPrototypeHelper.loc;
|
||||
const type: IrType = { kind: "object", className };
|
||||
const value = varRef("value", DYN, loc);
|
||||
const prototype = classPrototypeData(lowerer, info, loc, {
|
||||
kind: "dynCheck",
|
||||
value,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
if (prototype)
|
||||
this.errorPrototypeHelper.body.unshift({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.inheritsErrorPrototype",
|
||||
args: [prototype],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
this.serializesClasses &&
|
||||
!this.jsonPrototypes.has(className) &&
|
||||
@@ -1516,6 +1564,31 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
return { kind: "call", callee: cast.fn.name, args: [expr.value], type, loc: expr.loc };
|
||||
}
|
||||
if (expr.kind === "libCall" && expr.fn === "dyn.inheritsErrorPrototype") {
|
||||
if (!this.errorPrototypeHelper) {
|
||||
const loc = expr.loc;
|
||||
this.errorPrototypeHelper = {
|
||||
name: "%dyn.class.inheritsErrorPrototype",
|
||||
params: [{ localId: "value", name: "value", type: DYN }],
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
body: [
|
||||
{ kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc },
|
||||
],
|
||||
};
|
||||
lowerer.liftedFns.push(this.errorPrototypeHelper);
|
||||
this.generated.add(this.errorPrototypeHelper);
|
||||
changed = true;
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: this.errorPrototypeHelper.name,
|
||||
args: expr.args,
|
||||
type: BOOL,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
expr.kind === "libCall" &&
|
||||
expr.fn === "dyn.getPrototype" &&
|
||||
@@ -5942,3 +6015,11 @@ const INHERITED_OBJECT_METHODS: ReadonlySet<string> = new Set([
|
||||
"hasOwnProperty",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
/** Whether a class or one of its bases has a heritage evaluated at run
|
||||
* time, whose prototype chain may reach Error.prototype. */
|
||||
function hasRuntimeHeritage(info: ClassInfo): boolean {
|
||||
for (let current: ClassInfo | null = info; current; current = current.base)
|
||||
if (current.callableBase) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -380,6 +380,13 @@ export function collectClassMethodMutations(
|
||||
markTarget(node.initializer, node);
|
||||
if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node))
|
||||
lowerer.errorPrototypeValues = true;
|
||||
// A heritage evaluated at run time may be Error, which links the
|
||||
// class's instances to Error.prototype.
|
||||
if (
|
||||
(ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
|
||||
runtimeHeritage(lowerer, node)
|
||||
)
|
||||
lowerer.errorPrototypeValues = true;
|
||||
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
||||
const base = unwrap(node.expression);
|
||||
const prototype =
|
||||
@@ -621,3 +628,22 @@ export function collectClassMethodMutations(
|
||||
if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a class's `extends` expression is a value computed at run time:
|
||||
* anything other than a name of a class declaration or of a builtin
|
||||
* constructor (`extends Parent` over `const Parent = params.Parent ?? Object`,
|
||||
* `extends make()`). */
|
||||
function runtimeHeritage(
|
||||
lowerer: Lowerer,
|
||||
node: ts.ClassDeclaration | ts.ClassExpression,
|
||||
): boolean {
|
||||
const heritage = node.heritageClauses?.nodes.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (heritage === undefined) return false;
|
||||
if (!ts.isIdentifier(heritage) && !ts.isPropertyAccessExpression(heritage)) return true;
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(heritage);
|
||||
if (symbol === undefined) return true;
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
return !(symbol.flags & ts.SymbolFlags.Class) && !lowerer.isStdlibSymbol(symbol);
|
||||
}
|
||||
|
||||
@@ -207,12 +207,21 @@ export function classPrototypeData(
|
||||
// promise-returning signature) gets no data slot.
|
||||
const boxedValue = lowerer.coerceToExpected(compiled, DYN);
|
||||
if (boxedValue.type.kind !== "dyn") continue;
|
||||
const originalId = `%g.${info.def.prototypeDataHelper}.original.${method}`;
|
||||
lowerer.globalsList.push({
|
||||
id: originalId,
|
||||
name: `${info.def.jsName ?? info.def.name}.prototype.${method}.original`,
|
||||
type: DYN,
|
||||
mutable: true,
|
||||
});
|
||||
info.prototypeOriginals ??= new Map();
|
||||
info.prototypeOriginals.set(method, originalId);
|
||||
const descriptor: IrExpr = {
|
||||
kind: "dynObjLit",
|
||||
fields: [
|
||||
{
|
||||
key: { kind: "strLit", value: "value", type: STRING, loc },
|
||||
value: boxedValue,
|
||||
value: varRef(originalId, DYN, loc),
|
||||
},
|
||||
{
|
||||
key: { kind: "strLit", value: "writable", type: STRING, loc },
|
||||
@@ -227,6 +236,8 @@ export function classPrototypeData(
|
||||
loc,
|
||||
};
|
||||
const init = helper.body[0]!;
|
||||
if (init.kind === "if")
|
||||
init.then.push({ kind: "assign", localId: originalId, value: boxedValue, loc });
|
||||
if (init.kind === "if")
|
||||
init.then.push({
|
||||
kind: "exprStmt",
|
||||
|
||||
@@ -288,6 +288,14 @@ export function applyWidthLift(
|
||||
case "dynIn": {
|
||||
if (dst.kind !== "dyn")
|
||||
throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot");
|
||||
// The declaration-typed boundary keeps user values' identity when
|
||||
// they cross into package code (decl-projection.ts).
|
||||
if (
|
||||
lowerer.declBoundary &&
|
||||
!/[\\/]node_modules[\\/]/.test(loc.file) &&
|
||||
liveBoxable(lowerer, value.type)
|
||||
)
|
||||
return { kind: "dynFrom", value, liveRef: true, type: DYN, loc };
|
||||
return { kind: "dynFrom", value, type: DYN, loc };
|
||||
}
|
||||
case "dynOut":
|
||||
@@ -775,3 +783,18 @@ function buildRecordProjection(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Records and arrays (and unions holding them), whose identity a live
|
||||
* capsule keeps. */
|
||||
function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set<string>()): boolean {
|
||||
if (t.kind === "record") {
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
return shape !== undefined && !shape.tuple;
|
||||
}
|
||||
if (t.kind === "array") return true;
|
||||
if (t.kind !== "union" || seen.has(t.unionId)) return false;
|
||||
seen.add(t.unionId);
|
||||
return (
|
||||
lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -361,6 +361,21 @@ export function lowerArrayMethodCall(
|
||||
if (probe.type.kind === "array") {
|
||||
receiverIr = probe.type;
|
||||
probedUntyped = true;
|
||||
} else if (
|
||||
probe.type.kind === "dyn" &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(access)) &&
|
||||
lowerer.isStdlibMember(access)
|
||||
) {
|
||||
// An `unknown[]` (or `any[]`) array stored checked-dynamic — a
|
||||
// generic body instantiated at `unknown`, a spread of unknown
|
||||
// elements: the method runs on the runtime array (dynInvoke, JS
|
||||
// exact), and a typed result is checked like any member read.
|
||||
const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, probe, true);
|
||||
if (!dispatched) return null;
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
return result && result.kind !== "void" && result.kind !== "dyn"
|
||||
? lowerer.coerceInto(call, dispatched, result)
|
||||
: dispatched;
|
||||
}
|
||||
}
|
||||
if (receiverIr?.kind === "union") {
|
||||
|
||||
@@ -12,10 +12,11 @@ import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
|
||||
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
|
||||
|
||||
/** Runtime entry points that step a checked-dynamic iterable's iterator:
|
||||
* destructuring rest, and the collection constructors' and
|
||||
* Object.fromEntries' seeds. */
|
||||
* destructuring rest, the collection constructors' and Object.fromEntries'
|
||||
* seeds, and Math.min/Math.max over a spread. */
|
||||
const RUNTIME_STEP_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"dyn.destructureRest",
|
||||
"dyn.mathFold",
|
||||
"dyn.nativeSetNew",
|
||||
"dyn.mapSeedEntries",
|
||||
"dyn.fromEntries",
|
||||
@@ -80,6 +81,7 @@ const DISPATCH_LIB_CALLS = new Set<string>([
|
||||
"dyn.propertyIsEnumerableComputed",
|
||||
"dyn.defineProperty",
|
||||
"dyn.getPrototype",
|
||||
"dyn.inheritsErrorPrototype",
|
||||
"dyn.reflectGet",
|
||||
"dyn.reflectSet",
|
||||
]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
|
||||
import { predeclareForwardClass } from "../lower-stmts.js";
|
||||
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
|
||||
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
@@ -860,6 +861,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
{
|
||||
const sym = lowerer.resolveValueSymbol(expr);
|
||||
const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined;
|
||||
// A local class referenced from a function hoisted above its statement:
|
||||
// its TDZ box, holding the class as a checked-dynamic constructor.
|
||||
if (!classInfo && sym && predeclareForwardClass(lowerer, sym)) {
|
||||
const local = lowerer.resolveLocal(expr);
|
||||
if (local) return { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
}
|
||||
// A GENERIC class name whose CONTEXTUAL type pins one instantiation
|
||||
// (`const B: new (v: string) => Counter<string> = Counter`): the
|
||||
// value is that instantiation's class object — the generic-fn
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
canAdaptDynFuncTo,
|
||||
canDynCheckTo,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js";
|
||||
import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js";
|
||||
import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js";
|
||||
import { isDeclBoundaryTypeNode } from "../../decl-projection.js";
|
||||
|
||||
/** `c ? a : b`. An explicit destination constructs each branch at that
|
||||
* layout before joining it. Without one, retain the expression's inferred
|
||||
@@ -1516,6 +1518,12 @@ export function lowerAsExpression(
|
||||
const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs);
|
||||
if (!target) lowerer.badType(expr.type, targetTs);
|
||||
if (target.kind === "dyn") return inner; // `as unknown`: erasure
|
||||
// The declaration-typed boundary (decl-projection.ts): an identity-
|
||||
// preserving checked conversion of a value package code produced.
|
||||
if (isDeclBoundaryTypeNode(expr.type)) {
|
||||
const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr));
|
||||
if (adopted) return adopted;
|
||||
}
|
||||
// An all-`unknown`-fields record target (`err as { code?: unknown }`
|
||||
// — the errno-probing idiom): there is nothing to validate (every
|
||||
// field is unknown, exactly what the dyn value already answers) and
|
||||
@@ -1603,3 +1611,83 @@ export function lowerAsExpression(
|
||||
}
|
||||
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
|
||||
}
|
||||
|
||||
/** True when a type holds a record or an array somewhere (values whose
|
||||
* identity can be observed). */
|
||||
function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set<string>()): boolean {
|
||||
if (t.kind === "record" || t.kind === "array") return true;
|
||||
if (t.kind !== "union" || seen.has(t.unionId)) return false;
|
||||
seen.add(t.unionId);
|
||||
return (
|
||||
lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
/** `(<package expr> as __scriptc_decl_N)`: check the dynamic value against
|
||||
* the projected type and build the native value (dynCheck), then adopt:
|
||||
* every source object or array that something else still references
|
||||
* becomes a live capsule of the value built from it
|
||||
* (scr_dyn_decl_adopt), so identity and mutation visibility hold on both
|
||||
* sides. Scalar targets are ordinary checked casts. */
|
||||
function declBoundaryCast(
|
||||
lowerer: Lowerer,
|
||||
inner: IrExpr,
|
||||
target: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (
|
||||
!canDynCheckTo(
|
||||
target,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
) &&
|
||||
!(target.kind === "union" && lowerer.dynConvertible(target))
|
||||
)
|
||||
return null;
|
||||
if (!holdsReference(lowerer, target)) {
|
||||
return { kind: "dynCheck", value: inner, type: target, loc };
|
||||
}
|
||||
const source = lowerer.declareHiddenLocal("%boundarySource", DYN);
|
||||
const value = lowerer.declareHiddenLocal("%boundaryValue", target);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: source.id, init: inner, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: value.id,
|
||||
init: {
|
||||
kind: "dynCheck",
|
||||
value: varRef(source.id, DYN, loc),
|
||||
adopt: true,
|
||||
type: target,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.declAdopt",
|
||||
args: [
|
||||
varRef(source.id, DYN, loc),
|
||||
{
|
||||
kind: "dynFrom",
|
||||
value: varRef(value.id, target, loc),
|
||||
liveRef: true,
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: varRef(value.id, target, loc),
|
||||
type: target,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isNavigatorGlobal } from "../builtins/navigator.js";
|
||||
import { isSafeToDiscard } from "./evaluation-safety.js";
|
||||
import { completeFuncValueArgs } from "../call-arguments.js";
|
||||
import { dynUndefinedExpr } from "../../../ir/build.js";
|
||||
@@ -223,6 +224,16 @@ export function lowerOptionalChain(
|
||||
// Native handle methods have no first-class value to guard; when the
|
||||
// method read itself has no lowering, re-dispatch the plain call (which
|
||||
// reports its own diagnostics) instead of failing on the value read.
|
||||
// `navigator?.userAgent`: Node's navigator global is never nullish and
|
||||
// has no value form, so the guarded step is the plain member read.
|
||||
if (isNavigatorGlobal(lowerer, recvNode)) {
|
||||
lowerer.chainHandled.add(dotNode);
|
||||
try {
|
||||
return lowerer.lowerExpr(expr);
|
||||
} finally {
|
||||
lowerer.chainHandled.delete(dotNode);
|
||||
}
|
||||
}
|
||||
let loweredReceiver: IrExpr | null;
|
||||
if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) {
|
||||
loweredReceiver = tryLowerExpression(lowerer, recvNode);
|
||||
|
||||
@@ -307,10 +307,10 @@ export function lowerPropertyAccessExpr(
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!expr.questionDotToken) {
|
||||
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
|
||||
if (navigator) return navigator;
|
||||
}
|
||||
// Node's navigator is never nullish, so `navigator?.userAgent` (zod's
|
||||
// Cloudflare probe) folds like `navigator.userAgent`.
|
||||
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
|
||||
if (navigator) return navigator;
|
||||
// util.inspect.custom: Symbol.for('nodejs.util.inspect.custom').
|
||||
if (isInspectCustomAccess(lowerer, expr)) {
|
||||
return {
|
||||
@@ -564,6 +564,19 @@ export function lowerPropertyAccessExpr(
|
||||
expr.name.text,
|
||||
)
|
||||
) {
|
||||
// A name the program stores on the global object
|
||||
// (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from
|
||||
// the global object's own properties.
|
||||
const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression));
|
||||
if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) {
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return dynUndefinedExpr(loc);
|
||||
}
|
||||
// `Uint8Array.prototype.toBase64` and its siblings, the base64/hex
|
||||
@@ -4502,3 +4515,29 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null {
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** The property names the program assigns on the global object, through
|
||||
* `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */
|
||||
function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set<string> {
|
||||
const known = lowerer.globalThisWrittenNames;
|
||||
if (known) return known;
|
||||
const names = new Set<string>();
|
||||
lowerer.globalThisWrittenNames = names;
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
(node.operatorToken.kind === ts.SyntaxKind.EqualsToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
lowerer.typeOf(node.left.expression) === globalType
|
||||
)
|
||||
names.add(node.left.name.text);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { checkedClassInstanceOf } from "../class-construction.js";
|
||||
import { unsupportedDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { globalSymbolKey } from "./global-symbols.js";
|
||||
import { builtinGlobalHasKey } from "../builtins/global-namespace-keys.js";
|
||||
import { foldedStringKeyOf } from "./object-literals.js";
|
||||
import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
|
||||
@@ -1430,6 +1431,16 @@ export function lowerInExpression(
|
||||
"'in' with computed (non-literal) keys (a const whose type is one string literal folds; a string-typed key answers over index-signature record receivers)",
|
||||
);
|
||||
}
|
||||
// A feature probe on a builtin constructor global (`"captureStackTrace"
|
||||
// in Error`): the global is a fixed object, so the answer is static.
|
||||
{
|
||||
let recvNode = expr.right;
|
||||
while (ts.isParenthesizedExpression(recvNode)) recvNode = recvNode.expression;
|
||||
if (ts.isIdentifier(recvNode) && lowerer.isStdlibGlobal(recvNode, recvNode.text)) {
|
||||
const has = builtinGlobalHasKey(recvNode.text, key);
|
||||
if (has !== null) return { kind: "boolLit", value: has, type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
// A receiver re-read (the static folds below) is safe for plain reads
|
||||
// AND for side-effect-free expressions (an object literal of literals
|
||||
// — the `"a" in { a: true }` shape).
|
||||
|
||||
@@ -227,6 +227,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
const opaqueSeen = new Set<string>();
|
||||
const runtimeShapes = new Set<string>();
|
||||
const runtimeSeen = new Set<string>();
|
||||
/** Shapes the declaration-typed boundary converts into (adopting casts):
|
||||
* their values come from package code that builds them in the
|
||||
* declaration's order, the order their table holds. */
|
||||
const declaredShapes = new Set<string>();
|
||||
const declaredSeen = new Set<string>();
|
||||
const producedBy = (type: IrType, opaque: boolean): void => {
|
||||
if (opaque) shapesIn(type, opaqueShapes, opaqueSeen);
|
||||
else shapesIn(type, runtimeShapes, runtimeSeen);
|
||||
@@ -332,7 +337,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
producedBy(e.type, true);
|
||||
return;
|
||||
case "dynCheck":
|
||||
if (!orderPreservingClone(e)) producedBy(e.type, false);
|
||||
if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen);
|
||||
else if (!orderPreservingClone(e)) producedBy(e.type, false);
|
||||
return;
|
||||
case "dynDestrCheck":
|
||||
case "caughtCheck":
|
||||
@@ -392,6 +398,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
f.opaque = true;
|
||||
}
|
||||
for (const id of runtimeShapes) factsOf(id).runtime = true;
|
||||
for (const id of declaredShapes) if (!runtimeShapes.has(id)) factsOf(id).pinned = true;
|
||||
|
||||
// ── scope ───────────────────────────────────────────────────────────
|
||||
// Only plain declared-field shapes with a table take part.
|
||||
|
||||
@@ -2430,6 +2430,20 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc)
|
||||
return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc };
|
||||
if (arg.type.kind === "f64")
|
||||
return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc };
|
||||
// Byte storage converts through ToPrimitive, which BigInt then parses: a
|
||||
// typed array's text is its elements joined with "," (one element reads
|
||||
// as that element, none as 0n, several throw the SyntaxError for the
|
||||
// joined text), a Buffer's is its UTF-8 decoding and a DataView's is
|
||||
// "[object DataView]". The runtime value tells them apart, as for String.
|
||||
if (arg.type.kind === "bytes") {
|
||||
const text: IrExpr = {
|
||||
kind: "toString",
|
||||
operand: { kind: "dynFrom", value: arg, type: DYN, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "bigint.parse", args: [text], type: BIGINT_T, loc };
|
||||
}
|
||||
if (arg.type.kind === "bool") {
|
||||
return {
|
||||
kind: "ternary",
|
||||
@@ -2470,6 +2484,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc)
|
||||
arm.kind === "string" ||
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "bool" ||
|
||||
arm.kind === "bytes" ||
|
||||
isUnitType(arm),
|
||||
)
|
||||
)
|
||||
@@ -3827,18 +3842,22 @@ function lowerDynReceiverMethodCall(
|
||||
}
|
||||
if (access.name.text === "flatMap") return lowerDynArrayFlatMapCall(lowerer, call, access, recv);
|
||||
// String methods claim only names NO other dyn-representable kind's
|
||||
// prototype declares (Array carries includes/indexOf/slice too): for
|
||||
// these, "the receiver is a string, or the call throws V8's TypeError"
|
||||
// IS Node's semantics for every possible dyn value. Shared names would
|
||||
// need a receiver-kind dispatch — they keep the fence.
|
||||
// prototype declares (Array carries includes/indexOf/slice too), so no
|
||||
// other kind inherits them. Shared names would need a receiver-kind
|
||||
// dispatch — they keep the fence.
|
||||
if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) {
|
||||
// An object-typed receiver the member is missing from (a JavaScript
|
||||
// module object whose members are assigned in a function body,
|
||||
// `common.repeat(" ", n)`) holds a string or an object with that
|
||||
// member: dispatch on the receiver's runtime kind.
|
||||
// A checked-dynamic object can still carry its OWN member of that
|
||||
// name: zod's schemas assign `trim`, `toLowerCase` and `startsWith`
|
||||
// in their constructors (`z.string().trim().min(1)`), an object's
|
||||
// prototype can define `trim`, and a JavaScript module object can
|
||||
// assign `repeat` in a function body (`common.repeat(" ", n)`, an
|
||||
// object-typed receiver the member is missing from). An untyped
|
||||
// receiver, or a member the checker cannot type, dispatches on the
|
||||
// receiver's runtime kind: a string runs the string lowering, anything
|
||||
// else calls its member as Node does.
|
||||
if (
|
||||
!checkerUntyped &&
|
||||
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 &&
|
||||
(checkerUntyped ||
|
||||
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) &&
|
||||
!call.questionDotToken &&
|
||||
!access.questionDotToken &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement)
|
||||
@@ -4290,6 +4309,7 @@ const DYN_DISPATCH_METHODS = new Set([
|
||||
"shift",
|
||||
"unshift",
|
||||
"slice",
|
||||
"splice",
|
||||
"at",
|
||||
"indexOf",
|
||||
"lastIndexOf",
|
||||
@@ -8297,6 +8317,12 @@ function lowerObjectStaticCall(
|
||||
...(fnName !== null ? { fnName } : {}),
|
||||
};
|
||||
}
|
||||
// A class value (`Object.defineProperty(Definition, "name", { value })`)
|
||||
// boxes to its constructor function, whose own-property table holds the
|
||||
// definition for every alias of the class.
|
||||
if (target?.type.kind === "classval" && lowerer.dynConvertible(target.type)) {
|
||||
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) };
|
||||
}
|
||||
const descriptorPrimitive =
|
||||
member === "getOwnPropertyDescriptor" &&
|
||||
target &&
|
||||
@@ -10214,6 +10240,23 @@ export function lowerObjectMethodCall(
|
||||
type: found.sig.ret,
|
||||
loc,
|
||||
};
|
||||
// The declaring class's original value, built with the observed slot.
|
||||
// An override below (a virtual call) keeps the dynamic call: a slot
|
||||
// reassigned to an ancestor's method must not reach the override. So do
|
||||
// methods declared in unchecked JavaScript, whose results are inferred,
|
||||
// and calls with a spread argument: the dynamic call drains the spread
|
||||
// through the iterator's current `next`, which may be a replacement.
|
||||
const declarerFile = found.declarer.decl ? ts.getSourceFileOfNode(found.declarer.decl) : null;
|
||||
const original =
|
||||
virtual ||
|
||||
call.arguments.nodes.some(ts.isSpreadElement) ||
|
||||
!declarerFile ||
|
||||
(isJsSourceFile(declarerFile) && !lowerer.checkedJsFile(declarerFile))
|
||||
? undefined
|
||||
: {
|
||||
originalId: (): string | undefined => found.declarer.prototypeOriginals?.get(method),
|
||||
directCall: (): IrExpr => fallback,
|
||||
};
|
||||
const result = classCallbackCall(
|
||||
lowerer,
|
||||
value,
|
||||
@@ -10222,6 +10265,7 @@ export function lowerObjectMethodCall(
|
||||
fallback,
|
||||
loc,
|
||||
ts.getTextOfNode(access),
|
||||
original,
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isDatePrototypeMethod,
|
||||
lowerDateSuperMethodCall,
|
||||
} from "./builtins/date-classes.js";
|
||||
import { predeclareForwardClass } from "./lower-stmts.js";
|
||||
import { lowerDateStaticCall } from "./builtins/dates.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
|
||||
@@ -478,6 +479,10 @@ export interface ClassInfo {
|
||||
prototypeMethodValues?: Map<string, IrExpr>;
|
||||
prototypeReflectionReady?: true;
|
||||
prototypeObservedMethodsReady?: true;
|
||||
/** The globals holding each observed method's original prototype value
|
||||
* (set when the prototype data is built), by method name. A typed call
|
||||
* whose slot still holds that value calls the method natively. */
|
||||
prototypeOriginals?: Map<string, string>;
|
||||
prototypeReflectionRefusal?: string;
|
||||
localClass?: {
|
||||
context: FnCtx | null;
|
||||
@@ -2356,6 +2361,7 @@ export function collectClassShapeInner(
|
||||
const checkedStatics =
|
||||
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
((base && hasRuntimeStatics(base)) ||
|
||||
definedThroughReflection(lowerer, decl) ||
|
||||
decl.members.nodes.some(
|
||||
(member) =>
|
||||
ts.isAccessor(member) &&
|
||||
@@ -3209,15 +3215,6 @@ export function collectClassShapeInner(
|
||||
symbolMethods.set(key.identity, mName);
|
||||
}
|
||||
}
|
||||
// Error values held as untyped values render through their error
|
||||
// encoding, which keeps no compiled methods to call.
|
||||
if (mName === INSPECT_CUSTOM_MEMBER && errorRooted) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
member.name,
|
||||
"[util.inspect.custom] methods on Error subclasses",
|
||||
);
|
||||
}
|
||||
// Generator calls use their spawn wrapper. Public methods are
|
||||
// direct calls only when no subclass overrides the method;
|
||||
// override chains involving generators fence below.
|
||||
@@ -5489,7 +5486,12 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node)
|
||||
const display = info.def.name.replace(/^%|^%m\d+\./, "");
|
||||
fenceDecorationThrows(lowerer, info, blame);
|
||||
if (info.localClass && ts.isIdentifier(blame)) {
|
||||
const stored = lowerer.resolveLocal(blame);
|
||||
let stored = lowerer.resolveLocal(blame);
|
||||
// A reference from a function hoisted above the class statement.
|
||||
if (!stored) {
|
||||
const symbol = lowerer.resolveValueSymbol(blame);
|
||||
if (symbol && predeclareForwardClass(lowerer, symbol)) stored = lowerer.resolveLocal(blame);
|
||||
}
|
||||
if (stored)
|
||||
return lowerer.coerceToExpected(
|
||||
{ kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) },
|
||||
@@ -11285,3 +11287,37 @@ function genericFieldFnNodeOf(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a JavaScript class's own binding is the target of
|
||||
* `Object.defineProperty`/`defineProperties` in its file
|
||||
* (`Object.defineProperty(Definition, "name", { value })`): its constructor
|
||||
* then needs a live own-property table. */
|
||||
function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean {
|
||||
const name = decl.name;
|
||||
const sf = ts.getSourceFileOfNode(decl);
|
||||
if (name === undefined || !sf.text.includes("definePropert")) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) return false;
|
||||
let found = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (found) return "stop";
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
(node.expression.name.text === "defineProperty" ||
|
||||
node.expression.name.text === "defineProperties") &&
|
||||
lowerer.stdlibGlobalMember(node.expression, "Object") !== null
|
||||
) {
|
||||
const target = node.arguments.nodes[0];
|
||||
if (
|
||||
target !== undefined &&
|
||||
ts.isIdentifier(target) &&
|
||||
target.text === name.text &&
|
||||
lowerer.checker.getSymbolAtLocation(target) === symbol
|
||||
)
|
||||
found = true;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -1375,8 +1375,15 @@ function customInspectDynHelper(
|
||||
export function customInspectRegistrations(lowerer: Lowerer, loc: SrcLoc): IrStmt[] {
|
||||
const stmts: IrStmt[] = [];
|
||||
for (const info of lowerer.classes.values()) {
|
||||
if (info.def.runtime || info.def.abstract || isErrorClass(lowerer, info.def.name)) continue;
|
||||
if (info.def.runtime || info.def.abstract) continue;
|
||||
// Error classes register when their own properties render statically
|
||||
// (fieldedErrorReason): the others keep no method to call.
|
||||
if (isErrorClass(lowerer, info.def.name) && fieldedErrorReason(lowerer, info) !== null)
|
||||
continue;
|
||||
if (!lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER)) continue;
|
||||
// Only constructed classes have instances, and only their methods lower
|
||||
// (the constructor edge reaches the method).
|
||||
if (!lowerer.classCanBeConstructed(info)) continue;
|
||||
if (customInspectReason(lowerer, info) !== null) continue;
|
||||
const t = { kind: "object" as const, className: info.def.name };
|
||||
const fn = customInspectDynHelper(lowerer, t, loc);
|
||||
|
||||
@@ -2496,6 +2496,25 @@ export function lowerStdlibMethodCall(
|
||||
// method, drains into the number[] the fold reads.
|
||||
src = classGeneratorIterable(lowerer, src) ?? src;
|
||||
if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src;
|
||||
// An untyped iterable (`Math.min(...caps.map(f))` in JavaScript): the
|
||||
// runtime packs its values and folds their ToNumber like Node. V8 names
|
||||
// a null or undefined source by its text, calls as `f(...)`.
|
||||
if (src.type.kind === "dyn") {
|
||||
const what = ts.isCallExpression(spread.expression)
|
||||
? `${ts.getTextOfNode(spread.expression.expression)}(...)`
|
||||
: ts.getTextOfNode(spread.expression);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.mathFold",
|
||||
args: [
|
||||
src,
|
||||
{ kind: "boolLit", value: name === "max", type: BOOL, loc },
|
||||
{ kind: "strLit", value: what, type: STRING, loc },
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (src.type.kind !== "array" || src.type.elem.kind !== "f64") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1010,6 +1010,44 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The CLASS-DECLARATION twin of predeclareForwardCapture: a function
|
||||
* hoisted above a local `class D` (`function make() { return new D(); }`
|
||||
* reached through an earlier function) captures the class binding, which
|
||||
* is in its temporal dead zone until the class statement runs. The class
|
||||
* cannot be collected yet (its heritage may name bindings declared between
|
||||
* here and the statement), so the binding is pre-declared as a TDZ box
|
||||
* holding the class as a checked-dynamic constructor; the class statement
|
||||
* later initializes it, and every reference reads it from there. */
|
||||
export function predeclareForwardClass(lowerer: Lowerer, symbol: ts.Symbol): boolean {
|
||||
const decl = symbol.valueDeclaration;
|
||||
if (!decl || !ts.isClassDeclaration(decl) || decl.name === undefined) return false;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return false;
|
||||
for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) {
|
||||
const entry = lowerer.activeStmtLists[i]!;
|
||||
const idx = entry.stmts.indexOf(decl);
|
||||
if (idx < 0) continue;
|
||||
if (idx < entry.index || entry.ctx === lowerer.ctx || entry.ctx.tdzPredeclared.has(symbol))
|
||||
return false;
|
||||
const name = decl.name.text;
|
||||
const count = entry.ctx.localCounters.get(name) ?? 0;
|
||||
entry.ctx.localCounters.set(name, count + 1);
|
||||
const local: IrLocal = {
|
||||
id: `${name}.${count}`,
|
||||
name,
|
||||
type: DYN,
|
||||
mutable: false,
|
||||
boxed: true,
|
||||
tdz: true,
|
||||
};
|
||||
entry.ctx.locals.push(local);
|
||||
entry.frame.set(symbol, local);
|
||||
entry.out.push({ kind: "varDecl", localId: local.id, init: null, loc: locOf(decl) });
|
||||
entry.ctx.tdzPredeclared.set(symbol, local);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The FUNCTION-DECLARATION twin of predeclareForwardCapture: JS hoists a
|
||||
* nested `function f() {}` to scope entry — the binding is LIVE from the
|
||||
* first statement, so a reference lexically above the declaration in the
|
||||
@@ -1579,6 +1617,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
|
||||
const info = lowerer.lowerClassExpressionInfo(stmt);
|
||||
// An earlier hoisted function captured the class: its TDZ box exists.
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(stmt.name);
|
||||
const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined;
|
||||
if (pre) {
|
||||
lowerer.ctx.tdzPredeclared.delete(symbol!);
|
||||
const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN);
|
||||
return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) };
|
||||
}
|
||||
const type: IrType = { kind: "classval", className: info.def.name };
|
||||
const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true);
|
||||
// Static members of a function-local class run while the class value is
|
||||
|
||||
@@ -5683,7 +5683,10 @@ export function lowerStreamMethodCall(
|
||||
// argument reaches an object-mode side's _write as passed, null when
|
||||
// the callback took its place (a byte-mode side encodes a string
|
||||
// chunk with it).
|
||||
const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => {
|
||||
const writeEnc = (
|
||||
encArg: ts.Expression | undefined,
|
||||
cbArg: ts.Expression | undefined,
|
||||
): IrExpr => {
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
|
||||
const tail: IrExpr[] = [];
|
||||
@@ -5696,7 +5699,7 @@ export function lowerStreamMethodCall(
|
||||
args: [receiver, numLit(flags, loc), chunk, ...tail],
|
||||
type: BOOL,
|
||||
loc,
|
||||
} satisfies IrExpr;
|
||||
};
|
||||
};
|
||||
if (kind === "dyn") {
|
||||
if (args.length === 3) return writeEnc(args[1], args[2]);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { isDeclBoundaryFile } from "../decl-projection.js";
|
||||
import { relieveHeapPressure } from "../../heap-pressure.js";
|
||||
import { moduleNamespaceObject } from "./module-namespace-elements.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { positionTag, StructuralPositions } from "./structural-positions.js";
|
||||
import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js";
|
||||
@@ -164,7 +166,7 @@ import {
|
||||
import { DeferredModuleInitializers } from "./deferred-module-initializers.js";
|
||||
import { finalizeClassMethodValues } from "./class-method-values.js";
|
||||
import { finalizePrivateBrands } from "./private-brands.js";
|
||||
import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js";
|
||||
import { type ClassSymbolKey, INSPECT_CUSTOM_MEMBER, tableKeyFieldType } from "./symbol-fields.js";
|
||||
import type { HttpClientFnBinding } from "./lower-server.js";
|
||||
export type { WidthLift } from "./width-lift.js";
|
||||
export { newFnCtx, type FnCtx } from "./function-context.js";
|
||||
@@ -184,7 +186,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js";
|
||||
*/
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import { tsgoPath } from "../dts-paths.js";
|
||||
import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isBundledShimForwarder } from "../package-shims.js";
|
||||
import { erasedTypeOnlyImport } from "../type-only-imports.js";
|
||||
@@ -2412,6 +2414,51 @@ export class Lowerer {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
/** A fresh TypeScript data literal (`{ message: "m" }`) passed to a
|
||||
* checked-dynamic parameter of a callee implemented in JavaScript, whose
|
||||
* contract lets it reshape the object: a call through an untyped value, an
|
||||
* untyped parameter of a JavaScript function, or a package-declared
|
||||
* parameter type (a non-library `.d.ts`) that makes one of the literal's
|
||||
* properties optional (zod's `normalizeParams` deletes `params.message`).
|
||||
* No typed alias of a fresh literal exists, so it is built as the plain
|
||||
* object the callee receives instead of a view whose record layout refuses
|
||||
* that delete. Standard-library and TypeScript callees, and literals with
|
||||
* methods or accessors, keep their native storage. */
|
||||
private reshapeableContextualLiteral(
|
||||
literal: ts.ObjectLiteralExpression,
|
||||
contextual: readonly ts.Type[],
|
||||
): boolean {
|
||||
const call = literal.parent;
|
||||
if (
|
||||
call === undefined ||
|
||||
!(ts.isCallExpression(call) || ts.isNewExpression(call)) ||
|
||||
!call.arguments?.nodes.includes(literal) ||
|
||||
literal.properties.nodes.length === 0 ||
|
||||
!literal.properties.nodes.every(
|
||||
(prop) =>
|
||||
(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
|
||||
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
const declaration = this.checker.getResolvedSignature(call)?.declaration;
|
||||
const file = declaration ? ts.getSourceFileOfNode(declaration) : undefined;
|
||||
const untyped = contextual.some((part) => part.flags & ts.TypeFlags.Any);
|
||||
if (file === undefined) return untyped;
|
||||
if (isJsSourceFile(file)) return untyped;
|
||||
if (!file.isDeclarationFile || this.isStdlibFile(file)) return false;
|
||||
if (untyped) return true;
|
||||
const objects = contextual.filter((part) => part.flags & ts.TypeFlags.Object);
|
||||
if (objects.length === 0) return false;
|
||||
return literal.properties.nodes.some((prop) => {
|
||||
const name = (prop.name as ts.Identifier | ts.StringLiteral).text;
|
||||
return objects.every((part) => {
|
||||
const symbol = this.checker.getPropertyOfType(part, name);
|
||||
return symbol !== undefined && (symbol.flags & ts.SymbolFlags.Optional) !== 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** A TypeScript object literal handed to a JavaScript checked-value
|
||||
* parameter whose inferred type is open (`constructor({ factory, ...options
|
||||
* } = {})` — the rest element admits any property): the literal names a
|
||||
@@ -2975,9 +3022,14 @@ export class Lowerer {
|
||||
* ABI and shared prototype descriptors instead of call-site specialization. */
|
||||
readonly prototypeMethodAccesses = new Map<string, ts.Node>();
|
||||
/** Whether the program reads `Error.prototype`, the checked-dynamic
|
||||
* object an ES5 constructor function can inherit from: only then can a
|
||||
* value that is not an Error instance be `instanceof Error`. */
|
||||
* object an ES5 constructor function can inherit from, or has a class
|
||||
* whose heritage is evaluated at run time (`extends (Base ?? Error)`):
|
||||
* only then can a value that is not an Error instance be
|
||||
* `instanceof Error`. */
|
||||
errorPrototypeValues = false;
|
||||
/** The property names the program assigns on the global object
|
||||
* (property-access.ts), collected on first use. */
|
||||
globalThisWrittenNames: Set<string> | null = null;
|
||||
/** Well-known symbol methods (`sym:iterator`) the program may replace
|
||||
* through a symbol-keyed write: an instance or prototype property, or a
|
||||
* reflection call. */
|
||||
@@ -3204,13 +3256,14 @@ export class Lowerer {
|
||||
/** Structural source positions (structural-positions.ts) for names that
|
||||
* stay the same when unrelated code moves. */
|
||||
/** Files are named by their path relative to the entry's directory, so
|
||||
* names do not depend on where the program sits, and default library
|
||||
* files by their base name: two checkers install them in different
|
||||
* directories. */
|
||||
* names do not depend on where the program sits, default library files
|
||||
* by their base name (two checkers install them in different
|
||||
* directories), and the files scriptc ships by their name inside scriptc,
|
||||
* so names do not depend on where scriptc is installed either. */
|
||||
readonly positions = new StructuralPositions((sf) =>
|
||||
this.program.isSourceFileDefaultLibrary(sf)
|
||||
? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}`
|
||||
: relative(dirname(this.entry.fileName), sf.fileName),
|
||||
: (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)),
|
||||
);
|
||||
|
||||
/** The outermost node whose token span is exactly `loc`, or null. */
|
||||
@@ -3242,7 +3295,10 @@ export class Lowerer {
|
||||
* before it. */
|
||||
closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string {
|
||||
const node = this.nodeAtLoc(loc);
|
||||
const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node);
|
||||
const where =
|
||||
node === null
|
||||
? `${compilerOwnedFileName(loc.file) ?? loc.file}:${loc.start}:${loc.end}`
|
||||
: this.positions.of(node);
|
||||
const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`;
|
||||
const first = keyedHelperName(this, prefix, key);
|
||||
const seen = this.closureNameCounts.get(first) ?? 0;
|
||||
@@ -3383,8 +3439,18 @@ export class Lowerer {
|
||||
if (interned.has(fn.name)) this.sharedHelpers.add(fn);
|
||||
if (this.sharedHelpers.size === 0) return;
|
||||
const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn]));
|
||||
const earlier = (left: SrcLoc, right: SrcLoc): boolean =>
|
||||
left.file < right.file || (left.file === right.file && left.start < right.start);
|
||||
// Files scriptc ships compare by their name inside scriptc, so the
|
||||
// choice does not depend on where scriptc is installed.
|
||||
const fileKeys = new Map<string, string>();
|
||||
const fileKey = (file: string): string => {
|
||||
let key = fileKeys.get(file);
|
||||
if (key === undefined) fileKeys.set(file, (key = compilerOwnedFileName(file) ?? file));
|
||||
return key;
|
||||
};
|
||||
const earlier = (left: SrcLoc, right: SrcLoc): boolean => {
|
||||
if (left.file === right.file) return left.start < right.start;
|
||||
return fileKey(left.file) < fileKey(right.file);
|
||||
};
|
||||
// The earliest use of each shared helper outside the shared helpers, and
|
||||
// which shared helpers each shared helper uses.
|
||||
const uses = new Map<string, SrcLoc>();
|
||||
@@ -3569,6 +3635,18 @@ export class Lowerer {
|
||||
else if (kind === "failed") s.failed += 1;
|
||||
}
|
||||
|
||||
/** The program holds the declaration-typed boundary's projection
|
||||
* (decl-projection.ts): user-file record and array values crossing into
|
||||
* dynamic slots (package parameters) box as live capsules, so a value
|
||||
* that comes back through a boundary cast keeps its identity. */
|
||||
private declBoundaryMemo: boolean | null = null;
|
||||
get declBoundary(): boolean {
|
||||
this.declBoundaryMemo ??= this.program
|
||||
.getSourceFiles()
|
||||
.some((sf) => isDeclBoundaryFile(sf.fileName));
|
||||
return this.declBoundaryMemo;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly program: ts.Program,
|
||||
readonly entry: ts.SourceFile,
|
||||
@@ -3691,8 +3769,11 @@ export class Lowerer {
|
||||
classImplementedProtocol: (type) => this.classImplementedProtocol(type),
|
||||
isStdlibFile: this.isStdlibFile,
|
||||
isNpmFile: this.isNpmFile,
|
||||
// The declaration-typed boundary's generated types (decl-projection.ts)
|
||||
// describe data, like an explicitly mapped external type surface.
|
||||
isExternalTypeFile: (sf) =>
|
||||
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))),
|
||||
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))) ||
|
||||
isDeclBoundaryFile(sf.fileName),
|
||||
typeMemo: this.typeMemo,
|
||||
...(this.generalNodeStreams ? { generalNodeStreams: true } : {}),
|
||||
...(this.dynNodeStreams ? { dynNodeStreams: true } : {}),
|
||||
@@ -7712,6 +7793,7 @@ export class Lowerer {
|
||||
entry: ENTRY_NAME,
|
||||
...(this.usesWorkers ? { workers: true } : {}),
|
||||
...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}),
|
||||
...(this.declBoundary ? { declBoundary: true as const } : {}),
|
||||
...(this.threadSlotKeys.size > 0
|
||||
? { threadSlotKeys: [...this.threadSlotKeys].sort() }
|
||||
: {}),
|
||||
@@ -8061,6 +8143,19 @@ export class Lowerer {
|
||||
this.constructedClasses.add(name.slice(1, -".constructor".length));
|
||||
for (const [owner, methods] of this.virtualEdges)
|
||||
for (const method of methods) this.noteVirtualEdge(owner, method);
|
||||
// An instance's [util.inspect.custom] method runs from the runtime
|
||||
// inspect walk (customInspectRegistrations), which no IR edge shows.
|
||||
for (
|
||||
let c = this.classes.get(name.slice(1, -".constructor".length)) ?? null;
|
||||
c;
|
||||
c = c.base
|
||||
) {
|
||||
const custom = `%${c.def.name}.${INSPECT_CUSTOM_MEMBER}`;
|
||||
if (units.has(custom)) {
|
||||
this.onEdge?.(custom);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Class EXPRESSIONS collect while init bodies lower (below): their
|
||||
@@ -9452,6 +9547,22 @@ export class Lowerer {
|
||||
}
|
||||
if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr))
|
||||
return this.divergentValue(expr, expected);
|
||||
// A builtin error constructor global stored in a slot of its class value
|
||||
// type (`{ Parent: Error }`): the class value itself, which a checked
|
||||
// cast of the dynamic constructor value could not produce.
|
||||
if (
|
||||
expected.kind === "classval" &&
|
||||
expr.kind === "libCall" &&
|
||||
expr.fn === "dyn.globalConstructor" &&
|
||||
expr.args[0]?.kind === "strLit" &&
|
||||
`%${expr.args[0].value}` === expected.className
|
||||
) {
|
||||
const info = this.classes.get(expected.className);
|
||||
if (info?.builtinError && info.valueCtorParams !== undefined) {
|
||||
this.defineClassValueConstruct(info);
|
||||
return { kind: "classRef", className: info.def.name, type: expected, loc: expr.loc };
|
||||
}
|
||||
}
|
||||
// process.stdout/stderr into a general Writable slot (general-streams.ts).
|
||||
const stdioStream = stdioIntoGeneralStream(this, expr, expected);
|
||||
if (stdioStream) return this.coerceToExpected(stdioStream, expected);
|
||||
@@ -9558,6 +9669,13 @@ export class Lowerer {
|
||||
// constructor. `require("events")` is the EventEmitter class itself,
|
||||
// so its module binding is too (`ReleaseEmitter = EventEmitter` over
|
||||
// `const EventEmitter = require("events")`).
|
||||
if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) {
|
||||
const object = moduleNamespaceObject(
|
||||
this,
|
||||
expr as IrExpr & { type: IrType & { kind: "moduleNs" } },
|
||||
);
|
||||
if (object) return object;
|
||||
}
|
||||
if (
|
||||
expr.type.kind === "moduleNs" &&
|
||||
expr.type.moduleId === "builtin:events" &&
|
||||
@@ -10564,6 +10682,46 @@ export class Lowerer {
|
||||
return this.coerceInto(node, value, expected);
|
||||
}
|
||||
|
||||
/** The right side of a write to a member of a typed view of the global
|
||||
* object (`(globalThis as typeof globalThis & { k?: T }).k = value`, also
|
||||
* through `??=` and `||=`) that is `value` itself or wraps only it in
|
||||
* parentheses or `satisfies`; null when `value` is not in such a write. */
|
||||
private globalViewSlot(value: ts.Expression): ts.Expression | null {
|
||||
let operand: ts.Expression = value;
|
||||
while (
|
||||
operand.parent !== undefined &&
|
||||
(ts.isParenthesizedExpression(operand.parent) || ts.isSatisfiesExpression(operand.parent))
|
||||
)
|
||||
operand = operand.parent;
|
||||
const write = operand.parent;
|
||||
if (
|
||||
write === undefined ||
|
||||
!ts.isBinaryExpression(write) ||
|
||||
write.right !== operand ||
|
||||
(write.operatorToken.kind !== ts.SyntaxKind.EqualsToken &&
|
||||
write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken &&
|
||||
write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken)
|
||||
)
|
||||
return null;
|
||||
let target = write.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target))
|
||||
return null;
|
||||
const receiver = this.typeOf(target.expression);
|
||||
const isGlobal = (type: ts.Type): boolean => {
|
||||
const symbol = type.getSymbol();
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
ts.symbolName(symbol) === "globalThis" &&
|
||||
symbol.declarations.length === 0
|
||||
);
|
||||
};
|
||||
return isGlobal(receiver) ||
|
||||
(receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal))
|
||||
? operand
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Lower for a known destination, then apply ordinary value coercion.
|
||||
* Fresh arrays and records use the destination layout directly. */
|
||||
lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr {
|
||||
@@ -10631,6 +10789,21 @@ export class Lowerer {
|
||||
(element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element),
|
||||
)
|
||||
) {
|
||||
// A checked slot that the program declares as a native array (a
|
||||
// member of a typed view of the global object, `typeof globalThis &
|
||||
// { cache?: number[] }`) is read back through that declaration. Build
|
||||
// the native array and store it boxed: every typed read then extracts
|
||||
// that same array (a dynamic literal would be copied on each read,
|
||||
// losing writes such as push).
|
||||
const slot = this.globalViewSlot(x);
|
||||
const slotType = slot ? this.checker.getContextualType(slot) : undefined;
|
||||
const declared = slotType && this.checker.getNonNullableType(slotType);
|
||||
const native =
|
||||
declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
|
||||
? this.mapTypeOf(declared)
|
||||
: null;
|
||||
if (native?.kind === "array")
|
||||
return this.coerceInto(node, this.lowerArrayLiteral(x, native), DYN);
|
||||
return {
|
||||
kind: "dynArrLit",
|
||||
elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)),
|
||||
@@ -10660,6 +10833,7 @@ export class Lowerer {
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x)) ||
|
||||
this.excessJsContextualProperty(x, parts) ||
|
||||
this.reshapeableContextualLiteral(x, parts) ||
|
||||
holdsEvolvingArray(this, x)
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { DYN, STRING, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
DYN,
|
||||
STRING,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { newFnCtx, type Lowerer } from "./lowerer.js";
|
||||
|
||||
@@ -25,12 +32,13 @@ export function lowerModuleNamespaceElement(
|
||||
try {
|
||||
const body: IrStmt[] = [];
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(source);
|
||||
symbol?.getExports().forEach((exported, key) => {
|
||||
for (const exported of symbol ? lowerer.checker.getExportsOfModule(symbol) : []) {
|
||||
const key = ts.symbolName(exported);
|
||||
const target =
|
||||
exported.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(exported)
|
||||
: exported;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) return;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) continue;
|
||||
const declaration = target.valueDeclaration;
|
||||
const identifier = declaration?.name as ts.Node | undefined;
|
||||
if (!identifier || !ts.isIdentifier(identifier)) {
|
||||
@@ -43,7 +51,7 @@ export function lowerModuleNamespaceElement(
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: varRef("key", STRING, loc),
|
||||
right: strLit(String(key), loc),
|
||||
right: strLit(key, loc),
|
||||
type: { kind: "bool" },
|
||||
loc,
|
||||
},
|
||||
@@ -51,7 +59,7 @@ export function lowerModuleNamespaceElement(
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
});
|
||||
}
|
||||
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
|
||||
const fn: IrFunction = {
|
||||
name,
|
||||
@@ -88,3 +96,78 @@ export function lowerModuleNamespaceElement(
|
||||
};
|
||||
return { kind: "call", callee: name, args: [receiver, text], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** A module namespace taken as an untyped value (`{ ...ns }`, `{ iso: ns }`
|
||||
* in JavaScript): an object holding each value export, keyed in the
|
||||
* namespace's sorted key order. The copy is exact while every export is a
|
||||
* binding that never changes after initialization (const, function or
|
||||
* class); null for other namespaces, which keep their fence. */
|
||||
export function moduleNamespaceObject(
|
||||
lowerer: Lowerer,
|
||||
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
|
||||
): IrExpr | null {
|
||||
const source = lowerer.sourceFileOfModuleNamespace(ns.type);
|
||||
if (!source) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(source);
|
||||
if (!symbol) return null;
|
||||
const exports: { key: string; identifier: ts.Identifier }[] = [];
|
||||
let exact = true;
|
||||
// The module's resolved exports, which include `export *` members.
|
||||
for (const exported of lowerer.checker.getExportsOfModule(symbol)) {
|
||||
const key = ts.symbolName(exported);
|
||||
const target =
|
||||
exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
|
||||
if (!(target.flags & ts.SymbolFlags.Value)) continue;
|
||||
const declaration = target.valueDeclaration;
|
||||
const identifier = declaration?.name as ts.Node | undefined;
|
||||
const immutable =
|
||||
declaration !== undefined &&
|
||||
(ts.isFunctionDeclaration(declaration) ||
|
||||
ts.isClassDeclaration(declaration) ||
|
||||
(ts.isVariableDeclaration(declaration) &&
|
||||
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0));
|
||||
if (!identifier || !ts.isIdentifier(identifier) || !immutable) {
|
||||
exact = false;
|
||||
break;
|
||||
}
|
||||
exports.push({ key, identifier });
|
||||
}
|
||||
if (!exact) return null;
|
||||
exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
||||
const name = `%module.object:${ns.type.moduleId}`;
|
||||
const loc = ns.loc;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
const previousClass = lowerer.currentClass;
|
||||
lowerer.currentClass = null;
|
||||
lowerer.fnStack.push(context);
|
||||
try {
|
||||
const fields = exports.map(({ key, identifier }) => ({
|
||||
key: strLit(key, loc),
|
||||
value: lowerer.lowerExprExpecting(identifier, DYN),
|
||||
}));
|
||||
if (fields.some((field) => field.value.type.kind !== "dyn")) return null;
|
||||
const fn: IrFunction = {
|
||||
name,
|
||||
params: [],
|
||||
returnType: DYN,
|
||||
locals: [...context.locals],
|
||||
body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push(fn);
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
lowerer.currentClass = previousClass;
|
||||
}
|
||||
}
|
||||
const call: IrExpr = { kind: "call", callee: name, args: [], type: DYN, loc };
|
||||
if (ns.kind === "moduleNsRef") return call;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: ns, loc }],
|
||||
result: call,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -685,8 +685,8 @@ function loadFrontend(
|
||||
...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile),
|
||||
].map((sf) => [sf.fileName, sf.text]),
|
||||
),
|
||||
lower: (opts) =>
|
||||
lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, {
|
||||
lower: (opts) => {
|
||||
const lowered = lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, {
|
||||
...opts,
|
||||
frontendServices: finalLoad.services,
|
||||
startupCrash: finalLoad.startupCrash ?? null,
|
||||
@@ -695,7 +695,13 @@ function loadFrontend(
|
||||
projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false,
|
||||
externalTypes: finalLoad.externalTypes,
|
||||
externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile,
|
||||
}),
|
||||
});
|
||||
// The declaration-typed boundary's notes (SC6102) join the
|
||||
// performance warnings; they never fail a build.
|
||||
const notes = finalLoad.declBoundaryNotes;
|
||||
if (notes === undefined) return lowered;
|
||||
return { ...lowered, performance: [...notes, ...(lowered.performance ?? [])] };
|
||||
},
|
||||
npmStatic: statuses,
|
||||
npmImportSites: npmSites,
|
||||
dispose: finalLoad.dispose,
|
||||
|
||||
@@ -139,6 +139,7 @@ import {
|
||||
typeOnlyReachedFiles,
|
||||
} from "./type-only-imports.js";
|
||||
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
import { planDeclProjection, type DeclProjection } from "./decl-projection.js";
|
||||
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
|
||||
import {
|
||||
containsJsx,
|
||||
@@ -386,6 +387,9 @@ export interface LoadResult {
|
||||
* checkPreflight. */
|
||||
startupCrash?: StartupCrash | null;
|
||||
configDiags: ScrDiagnostic[];
|
||||
/** SC6102 performance notes of the declaration-typed boundary
|
||||
* (decl-projection.ts): files whose projection was dropped. */
|
||||
declBoundaryNotes?: ScrDiagnostic[];
|
||||
/** The files whose JSX the program lowers (jsx-source.ts), by file name.
|
||||
* The program holds their rewritten text; positions in diagnostics refer
|
||||
* to it. */
|
||||
@@ -397,6 +401,9 @@ export interface LoadResult {
|
||||
/** The program's pre-emit diagnostics, computed once. */
|
||||
diagnostics: readonly ts.Diagnostic[];
|
||||
projectWorld: ts.Program | null;
|
||||
/** The declaration-typed boundary rewrote user files: errors fall
|
||||
* back to the declaration world (load.projectWorld()), as without it. */
|
||||
declWorldFallback?: true;
|
||||
};
|
||||
/** How scriptc's type world differs from the project's own tsc setup —
|
||||
* used only to label the TypeScript errors those differences cause. */
|
||||
@@ -846,6 +853,9 @@ function loadProgram7(
|
||||
externalTypes: ReadonlyMap<string, string> = new Map(),
|
||||
/** The --npm-static lowering world: root opted-in packages' deep edges. */
|
||||
rootNpmStaticEdges = false,
|
||||
/** The declaration-typed boundary's projection (decl-projection.ts):
|
||||
* answers rewritten user files for the lowering world, or null. */
|
||||
projector?: (program: ts.Program) => DeclProjection | null,
|
||||
): LoadResult & { disposeAll: () => void } {
|
||||
const config = adoptProjectConfig7(host, entryPath);
|
||||
const configuredPaths = config.options["paths"];
|
||||
@@ -925,6 +935,7 @@ function loadProgram7(
|
||||
// lowering compiles holds them with their JSX lowered.
|
||||
const jsx = lowerProgramJsx7(program, options);
|
||||
let tscWorld: LoadResult["tscWorld"];
|
||||
const declBoundaryNotes: ScrDiagnostic[] = [];
|
||||
if (jsx.refusal !== null) {
|
||||
config.diags.push(jsx.refusal);
|
||||
} else if (jsx.sources.size > 0) {
|
||||
@@ -940,6 +951,51 @@ function loadProgram7(
|
||||
};
|
||||
for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text);
|
||||
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
||||
} else if (projector !== undefined) {
|
||||
// The declaration-typed boundary: user files typed from package
|
||||
// declarations (decl-projection.ts). The tsc gate reads the original
|
||||
// text; the lowering compiles the rewritten files.
|
||||
const projection = projector(program);
|
||||
if (projection !== null) {
|
||||
const original = program;
|
||||
const baseline = new Map<string, number>();
|
||||
for (const file of projection.files.keys()) {
|
||||
const sf = original.getSourceFile(file);
|
||||
if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length);
|
||||
}
|
||||
for (const [file, text] of projection.files) host.addVirtualFile(file, text);
|
||||
host.addVirtualFile(projection.declFile, projection.declText);
|
||||
programRoots.push(projection.declFile);
|
||||
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
||||
// A rewrite the lowering world disagrees with (new errors) is
|
||||
// dropped for that file: the file lowers as it would without it.
|
||||
const dropped: string[] = [];
|
||||
for (const file of projection.files.keys()) {
|
||||
const sf = program.getSourceFile(file);
|
||||
if (sf === undefined) continue;
|
||||
if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) {
|
||||
dropped.push(file);
|
||||
host.addVirtualFile(file, original.getSourceFile(file)!.text);
|
||||
}
|
||||
}
|
||||
if (dropped.length > 0) {
|
||||
program.dispose();
|
||||
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
||||
for (const file of dropped) declBoundaryNotes.push(declBoundaryDroppedDiag(file));
|
||||
}
|
||||
if (dropped.length === projection.files.size) {
|
||||
program.dispose();
|
||||
programRoots.pop();
|
||||
program = original;
|
||||
} else {
|
||||
tscWorld = {
|
||||
program: original,
|
||||
diagnostics: ts.getPreEmitDiagnostics(original),
|
||||
projectWorld: null,
|
||||
declWorldFallback: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const entry = program.getSourceFile(entryPath);
|
||||
if (!entry) throw new Error(`could not load ${entryPath}`);
|
||||
@@ -950,6 +1006,7 @@ function loadProgram7(
|
||||
entry,
|
||||
moduleOrder: [],
|
||||
configDiags: config.diags,
|
||||
...(declBoundaryNotes.length > 0 ? { declBoundaryNotes } : {}),
|
||||
typeWorld: {
|
||||
nodeFallback: nodeTypes === null,
|
||||
projectLib: config.projectLib ?? null,
|
||||
@@ -1156,31 +1213,69 @@ export function loadProgram(
|
||||
(npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null,
|
||||
};
|
||||
const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow });
|
||||
let authoredHost: ts.Ts7Host | null = null;
|
||||
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
|
||||
const authored = (): ReturnType<typeof loadProgram7> => {
|
||||
if (!authoredLoad) {
|
||||
authoredHost = services.createProgramHost({ cwd: dirname(entryPath) });
|
||||
authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes);
|
||||
}
|
||||
return authoredLoad;
|
||||
};
|
||||
const declPackages = npmStaticPackages.filter((pkg) => {
|
||||
const resolved = resolveBareModule(entryPath, pkg, "types-only");
|
||||
return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile);
|
||||
});
|
||||
const projector =
|
||||
declPackages.length === 0
|
||||
? undefined
|
||||
: (program: ts.Program): DeclProjection | null => {
|
||||
const userFiles = declBoundaryUserFiles(program, declPackages);
|
||||
if (userFiles.length === 0) return null;
|
||||
let projection: DeclProjection | null;
|
||||
try {
|
||||
projection = planDeclProjection({
|
||||
jsProgram: program,
|
||||
declProgram: authored().program,
|
||||
userFiles,
|
||||
outDir: dirname(entryPath),
|
||||
});
|
||||
} catch (error) {
|
||||
if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error;
|
||||
return null;
|
||||
}
|
||||
if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") {
|
||||
process.stderr.write(
|
||||
`[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`,
|
||||
);
|
||||
if (projection !== null) process.stderr.write(projection.declText);
|
||||
}
|
||||
return projection;
|
||||
};
|
||||
try {
|
||||
const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0);
|
||||
let authoredHost: ts.Ts7Host | null = null;
|
||||
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
|
||||
const load = loadProgram7(
|
||||
host,
|
||||
entryPath,
|
||||
externalTypes,
|
||||
npmStaticPackages.length > 0,
|
||||
projector,
|
||||
);
|
||||
return {
|
||||
...load,
|
||||
services,
|
||||
projectWorld:
|
||||
npmStaticPackages.length === 0
|
||||
? load.projectWorld
|
||||
: () => {
|
||||
: () =>
|
||||
// Check authored code against the package's published declarations.
|
||||
// Runtime bodies still lower from JavaScript in the original world;
|
||||
// inference there must not manufacture errors in valid user code.
|
||||
if (!authoredLoad) {
|
||||
authoredHost = services.createProgramHost({ cwd: dirname(entryPath) });
|
||||
authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes);
|
||||
}
|
||||
return authoredLoad.projectWorld();
|
||||
},
|
||||
authored().projectWorld(),
|
||||
dispose: () => {
|
||||
try {
|
||||
authoredLoad?.disposeAll();
|
||||
(authoredLoad as ReturnType<typeof loadProgram7> | null)?.disposeAll();
|
||||
} finally {
|
||||
authoredHost?.close();
|
||||
(authoredHost as ts.Ts7Host | null)?.close();
|
||||
try {
|
||||
load.disposeAll();
|
||||
} finally {
|
||||
@@ -1190,11 +1285,47 @@ export function loadProgram(
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
(authoredLoad as ReturnType<typeof loadProgram7> | null)?.disposeAll();
|
||||
(authoredHost as ts.Ts7Host | null)?.close();
|
||||
host.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** User TypeScript files (outside node_modules) that import one of the
|
||||
* packages whose declarations the boundary projects from. */
|
||||
function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] {
|
||||
const out: string[] = [];
|
||||
for (const sf of program.getSourceFiles()) {
|
||||
const name = sf.fileName.split("\\").join("/");
|
||||
if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name))
|
||||
continue;
|
||||
if (name.endsWith(".tsx")) continue; // JSX files: not projected yet
|
||||
const text = sf.text;
|
||||
if (
|
||||
packages.some(
|
||||
(pkg) =>
|
||||
text.includes(`"${pkg}"`) ||
|
||||
text.includes(`'${pkg}'`) ||
|
||||
text.includes(`"${pkg}/`) ||
|
||||
text.includes(`'${pkg}/`),
|
||||
)
|
||||
)
|
||||
out.push(sf.fileName);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** SC6102 — a projected file the lowering world disagreed with. */
|
||||
function declBoundaryDroppedDiag(file: string): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC6102",
|
||||
message:
|
||||
"values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript",
|
||||
loc: { file, start: 0, end: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/** tsc diagnostics (syntax + types), the supported-import fence, and the
|
||||
* module evaluation order: fills load.moduleOrder (the SourceFiles
|
||||
* themselves — the lowering consumes them directly) and returns the
|
||||
@@ -4801,7 +4932,9 @@ function preflight7(load: LoadResult): {
|
||||
tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics);
|
||||
if (tscErrors.length > 0) {
|
||||
const world =
|
||||
tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program);
|
||||
tscWorld === undefined || tscWorld.declWorldFallback === true
|
||||
? load.projectWorld()
|
||||
: (tscWorld.projectWorld ?? tscWorld.program);
|
||||
for (const d of errorsOf(world)) {
|
||||
const diag = toPassthrough(d);
|
||||
const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file);
|
||||
|
||||
@@ -24,15 +24,11 @@ function writePackage(directory: string, name: string, executable?: string): voi
|
||||
writeFileSync(join(directory, "lib", executable), "");
|
||||
}
|
||||
|
||||
test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => {
|
||||
expect(checkerBackend({}, "linux")).toBe("tsc-ts");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts");
|
||||
expect(checkerBackend({}, "darwin")).toBe("tsc-ts");
|
||||
expect(checkerBackend({}, "win32")).toBe("tsgo");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo");
|
||||
test("tsc-ts is the default checker, and SCRIPTC_CHECKER selects one", () => {
|
||||
expect(checkerBackend({})).toBe("tsc-ts");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "" })).toBe("tsc-ts");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" })).toBe("tsc-ts");
|
||||
expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" })).toBe("tsgo");
|
||||
expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow(
|
||||
'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"',
|
||||
);
|
||||
|
||||
@@ -4,17 +4,13 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
/** The TypeScript 7 API servers a compile can check with: tsc-ts, the default,
|
||||
* and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`.
|
||||
* Both serve the same API and give the same results. On Windows the default is
|
||||
* tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's
|
||||
* API server has no named-pipe transport yet. */
|
||||
* Both serve the same API and give the same results. On Windows the Node host
|
||||
* connects to either over a named pipe the server creates (rpc-process.ts). */
|
||||
export type CheckerBackend = "tsgo" | "tsc-ts";
|
||||
|
||||
export function checkerBackend(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: string = process.platform,
|
||||
): CheckerBackend {
|
||||
export function checkerBackend(env: NodeJS.ProcessEnv = process.env): CheckerBackend {
|
||||
const value = env["SCRIPTC_CHECKER"];
|
||||
if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts";
|
||||
if (value === undefined || value === "") return "tsc-ts";
|
||||
if (value === "tsc-ts") return "tsc-ts";
|
||||
if (value === "tsgo") return "tsgo";
|
||||
throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`);
|
||||
|
||||
@@ -631,6 +631,14 @@ export class CheckerFacade {
|
||||
return aliased;
|
||||
}
|
||||
|
||||
/** A module symbol's exports, with the members of its `export *`
|
||||
* declarations resolved (a symbol's own export table lists only the
|
||||
* module's own declarations). */
|
||||
getExportsOfModule(symbol: Ts7Symbol): readonly Ts7Symbol[] {
|
||||
this.ensureActive();
|
||||
return this.raw.getExportsOfModule(symbol);
|
||||
}
|
||||
|
||||
getDeclaredTypeOfSymbol(symbol: Ts7Symbol): Type {
|
||||
this.ensureActive();
|
||||
let type = this.cache.declaredTypeOfSymbol.get(symbol);
|
||||
|
||||
@@ -126,6 +126,7 @@ import { moduleRuntimeFeatures } from "./ir/runtime-features.js";
|
||||
import { serializeModuleParts } from "./ir/serialize.js";
|
||||
import { readTextParts, writeTextParts } from "./text-parts.js";
|
||||
import { validateModule } from "./ir/validate.js";
|
||||
import { compilerOwnedFileRoots } from "./frontend/dts-paths.js";
|
||||
import { loadProgram } from "./frontend/program-node.js";
|
||||
import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js";
|
||||
import {
|
||||
@@ -600,6 +601,7 @@ async function compileExecutableNative(
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
@@ -785,7 +787,7 @@ async function prepareExecutableInput(
|
||||
|
||||
if (outputKind === "ir") {
|
||||
await mkdir(dirname(opts.outPath), { recursive: true });
|
||||
await writeTextParts(opts.outPath, serializeModuleParts(mod));
|
||||
await writeTextParts(opts.outPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
|
||||
return { ok: true, artifact: { kind: "ir", path: opts.outPath } };
|
||||
}
|
||||
|
||||
@@ -885,7 +887,7 @@ async function prepareExecutableInput(
|
||||
if (opts.emitIr) {
|
||||
irPath = defaultSourcePaths.ir;
|
||||
irSourcePath = privateSiblingPath(irPath, "ir");
|
||||
await writeTextParts(irSourcePath, serializeModuleParts(mod));
|
||||
await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots()));
|
||||
}
|
||||
|
||||
const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release");
|
||||
@@ -1986,7 +1988,7 @@ async function emitSemanticLibraryHit(
|
||||
let irPath: string | undefined;
|
||||
if (opts.emitIr) {
|
||||
irPath = join(opts.outDir, `${stem}.lib.ir.json`);
|
||||
await writeTextParts(irPath, serializeModuleParts(mod));
|
||||
await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
|
||||
}
|
||||
await compileLibraryNative(profile, llvmPath, archivePath, opts.sanitize ?? false, hit.native);
|
||||
timing("native-archive");
|
||||
@@ -2272,7 +2274,7 @@ async function compileLibraryTracked(
|
||||
let irPath: string | undefined;
|
||||
if (opts.emitIr) {
|
||||
irPath = join(opts.outDir, `${stem}.lib.ir.json`);
|
||||
await writeTextParts(irPath, serializeModuleParts(mod));
|
||||
await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
|
||||
}
|
||||
|
||||
const nativeFeatures = libraryNativeFeatures(mod, profile.emission);
|
||||
|
||||
@@ -622,6 +622,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// fence; the zero-argument form always throws ERR_MISSING_ARGS. The
|
||||
// DOMException clone shares the option validation.
|
||||
"dyn.structuredClone",
|
||||
"dyn.declAdopt",
|
||||
"dyn.cloneMissing",
|
||||
"dyn.cloneTransferFail",
|
||||
"dyn.cloneTransferFailWith",
|
||||
@@ -683,6 +684,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.propertyIsEnumerableComputed",
|
||||
// the destructuring pack throws V8's TypeError on non-iterable dyn kinds
|
||||
"dyn.iterPack",
|
||||
// ToNumber of each element can run valueOf
|
||||
"dyn.mathFold",
|
||||
"dyn.destructureRest",
|
||||
"dyn.arrayFromIterator",
|
||||
"dyn.iterator",
|
||||
|
||||
@@ -219,6 +219,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* not iterable (cannot read property Symbol(Symbol.iterator))"). In the
|
||||
* may-throw seed set. */
|
||||
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 },
|
||||
"dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.arrayFromIterator": { argTypes: [DYN], result: DYN },
|
||||
"dyn.iterator": { argTypes: [DYN, STRING], result: DYN },
|
||||
@@ -3776,6 +3777,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's
|
||||
* own (verbatim, doubly-wrapped) message. */
|
||||
"dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN },
|
||||
/** The declaration-typed boundary's adoption (decl-projection.ts):
|
||||
* `source` is the dynamic value a boundary cast just converted, and
|
||||
* `capsule` a live capsule of the native value built from it. Every
|
||||
* object or array of `source` that something else still references
|
||||
* becomes, in place, a live capsule of the native value built from it.
|
||||
* Throws the path TypeError, changing nothing, when a converted object
|
||||
* is not plain data (a prototype, accessors, symbols, frozen state). */
|
||||
"dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID },
|
||||
"dyn.cloneMissing": { argTypes: [], result: DYN },
|
||||
/** structuredClone with a NON-EMPTY transfer array of static values:
|
||||
* nothing static is transferable, so the call always throws Node's
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
|
||||
type SwitchCase = Extract<IrStmt, { kind: "switch" }>["cases"][number];
|
||||
import { reparseExpr } from "./serialize.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
@@ -382,20 +384,26 @@ function rewriteCandidates(
|
||||
if (!call || simple(call.args[1]!)) return [s];
|
||||
const { decl, ref } = bind(call.args[1]!);
|
||||
const read: IrExpr = { ...call, args: [call.args[0]!, ref] };
|
||||
return [decl, s.kind === "varDecl" ? { ...s, init: read } : { ...s, value: read }];
|
||||
if (s.kind === "varDecl") return [decl, { ...s, init: read }];
|
||||
return [decl, { ...s, value: read }];
|
||||
});
|
||||
const hoistCase = (c: SwitchCase): SwitchCase => ({ ...c, body: hoist(c.body) });
|
||||
const lists = (s: IrStmt): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "if":
|
||||
return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) };
|
||||
case "block":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "while":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "doWhile":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "for":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "forOf":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "switch":
|
||||
return { ...s, cases: s.cases.map((c) => ({ ...c, body: hoist(c.body) })) };
|
||||
return { ...s, cases: s.cases.map((c) => hoistCase(c)) };
|
||||
case "tryCatch":
|
||||
return {
|
||||
...s,
|
||||
@@ -425,7 +433,10 @@ function rewriteCandidates(
|
||||
};
|
||||
}
|
||||
const call = readerCall(e);
|
||||
if (!call) return e.kind === "seqExpr" ? { ...e, stmts: hoist(e.stmts) } : e;
|
||||
if (!call) {
|
||||
if (e.kind === "seqExpr") return { ...e, stmts: hoist(e.stmts) };
|
||||
return e;
|
||||
}
|
||||
const helper = readers.get(call.callee)!;
|
||||
const array = call.args[0]!;
|
||||
const [arrayParam, indexParam] = helper.params;
|
||||
@@ -448,7 +459,9 @@ function rewriteCandidates(
|
||||
},
|
||||
stmt: (x) => x,
|
||||
});
|
||||
const typed: IrExpr = { ...body, loc: e.loc };
|
||||
// The ternary is a fresh copy (reparseExpr): it takes the call's location.
|
||||
const typed = body;
|
||||
typed.loc = e.loc;
|
||||
return stmts.length === 0
|
||||
? typed
|
||||
: { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc };
|
||||
|
||||
@@ -1143,7 +1143,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
const key = `l:${fn.name}\0${id}`;
|
||||
if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue;
|
||||
slots.dropInt32Elements(key);
|
||||
const checkable = (value: IrExpr): boolean => int32(value) || ranges.get(value) != null;
|
||||
const checkable = (value: IrExpr): boolean => {
|
||||
const range = ranges.get(value);
|
||||
return int32(value) || range != null;
|
||||
};
|
||||
if (values.every(checkable) && checkableInt32Allocations(fn, id))
|
||||
slots.checkedInt32Arrays.add(`${fn.name}\0${id}`);
|
||||
else reason(`array:${fn.name}\0${id}`, "an element store is not int32");
|
||||
|
||||
@@ -402,6 +402,16 @@ test("paired counters closing in on each other run as integers", () => {
|
||||
guarded: false,
|
||||
range: { min: 0, max: 9 },
|
||||
});
|
||||
// `hi > lo` and `hi >= lo` are `lo < hi` and `lo <= hi`.
|
||||
for (const op of [">", ">="] as const) {
|
||||
const reversed = paired(number(0), number(9));
|
||||
reversed.cond = { kind: "bin", op, left: ref("hi"), right: ref("lo"), type: BOOL, loc };
|
||||
expect(matchIntegerPairedForLoop(reversed, vars)).toMatchObject({
|
||||
low: { localId: "lo", step: 1 },
|
||||
high: { localId: "hi", step: -1 },
|
||||
inclusive: op === ">=",
|
||||
});
|
||||
}
|
||||
// A start without a known range is tested at run time.
|
||||
expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({
|
||||
high: { startGuarded: true },
|
||||
|
||||
@@ -382,9 +382,9 @@ export function matchIntegerCountedForLoop(
|
||||
*
|
||||
* The first counter only grows and the second only shrinks, by positive
|
||||
* literal steps, the body writes neither, and the loop runs while the first
|
||||
* is below (or at most) the second. Every body entry therefore holds both
|
||||
* between the first start and the second start: exact integers when the
|
||||
* starts are. A start without a proven integer range is tested at run time
|
||||
* is below (or at most) the second, written either way round (`hi > lo`).
|
||||
* Every body entry therefore holds both between the first start and the
|
||||
* second start: exact integers when the starts are. A start without a proven integer range is tested at run time
|
||||
* on a versioned loop, as a counted loop's is. */
|
||||
export interface IntegerPairedCounter {
|
||||
localId: string;
|
||||
@@ -412,10 +412,13 @@ export function matchIntegerPairedForLoop(
|
||||
cond = stmt.cond;
|
||||
if (init?.kind !== "block" || init.body.length !== 2) return null;
|
||||
if (update?.kind !== "block" || update.body.length !== 2) return null;
|
||||
if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null;
|
||||
if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null;
|
||||
const lowId = cond.left.localId,
|
||||
highId = cond.right.localId;
|
||||
if (cond?.kind !== "bin" || cond.left.kind !== "varRef" || cond.right.kind !== "varRef")
|
||||
return null;
|
||||
const below = cond.op === "<" || cond.op === "<=";
|
||||
if (!below && cond.op !== ">" && cond.op !== ">=") return null;
|
||||
// `hi > lo` is `lo < hi`.
|
||||
const lowId = below ? cond.left.localId : cond.right.localId,
|
||||
highId = below ? cond.right.localId : cond.left.localId;
|
||||
if (lowId === highId) return null;
|
||||
const counter = (id: string): IntegerPairedCounter | null => {
|
||||
const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id);
|
||||
@@ -485,7 +488,7 @@ export function matchIntegerPairedForLoop(
|
||||
return {
|
||||
low,
|
||||
high,
|
||||
inclusive: cond.op === "<=",
|
||||
inclusive: cond.op === "<=" || cond.op === ">=",
|
||||
guarded,
|
||||
// A loop whose starts already fail the condition never enters the body.
|
||||
range: {
|
||||
|
||||
@@ -1044,6 +1044,10 @@ export interface IrModule {
|
||||
entry: string;
|
||||
/** Executable roots run in isolated native worker contexts. */
|
||||
workers?: boolean;
|
||||
/** The program has declaration-typed boundary casts (decl-projection.ts):
|
||||
* closures boxed for package code return records and arrays as live
|
||||
* capsules, so a value that comes back keeps its identity. */
|
||||
declBoundary?: true;
|
||||
/** The program constructs LifetimeRegion (@scriptc/experimental): every
|
||||
* ProgramLifetime instance lives in a counted region (the region.*
|
||||
* intrinsics; backend/llvm/regions.ts). */
|
||||
@@ -3886,10 +3890,19 @@ export type IrExpr =
|
||||
/** `exact`: the value is known to be a capsule this program boxed from
|
||||
* exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the
|
||||
* expression), so the check is one load of the capsule's instance. */
|
||||
/** `adopt`: the declaration-typed boundary's cast (decl-projection.ts).
|
||||
* The conversion preserves identity: a source object or array that
|
||||
* something else still references (reference count above one) is
|
||||
* turned, after the whole check succeeded, into a live capsule of the
|
||||
* value built from it, so every holder sees the same native value; an
|
||||
* unshared source is simply consumed. Values must be plain data
|
||||
* (ordinary objects and arrays without accessors, prototypes, symbols
|
||||
* or frozen state), otherwise the cast throws the path TypeError. */
|
||||
| {
|
||||
kind: "dynCheck";
|
||||
value: IrExpr;
|
||||
preserveRefs?: true;
|
||||
adopt?: true;
|
||||
exact?: true;
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
arrayOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { runPairedWhileLoopsAsFor } from "./paired-while-loops.js";
|
||||
|
||||
const loc = { file: "paired-while-loops.ts", start: 0, end: 0 };
|
||||
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const array = { kind: "varRef", localId: "a", type: arrayOf(F64), loc } as const;
|
||||
const element = (index: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayGet", arr: array, index, type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const step = (id: string, op: "+" | "-"): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: id,
|
||||
value: { kind: "bin", op, left: ref(id), right: number(1), type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const declare = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
|
||||
const number_ = (id: string): IrLocal => ({
|
||||
id,
|
||||
name: id.split(".")[0]!,
|
||||
type: F64,
|
||||
mutable: true,
|
||||
});
|
||||
|
||||
function whileLoop(
|
||||
body: IrStmt[] = [element(ref("lo.0")), element(ref("hi.0"))],
|
||||
updates: IrStmt[] = [step("lo.0", "+"), step("hi.0", "-")],
|
||||
): IrStmt & { kind: "while" } {
|
||||
return {
|
||||
kind: "while",
|
||||
cond: { kind: "bin", op: "<", left: ref("lo.0"), right: ref("hi.0"), type: BOOL, loc },
|
||||
body: [...body, ...updates],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function program(body: IrStmt[], extra: Partial<IrFunction> = {}): IrModule {
|
||||
const fn: IrFunction = {
|
||||
name: "f",
|
||||
params: [{ localId: "n.0", name: "n", type: F64 }],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
number_("n.0"),
|
||||
number_("lo.0"),
|
||||
number_("hi.0"),
|
||||
{ id: "a", name: "a", type: arrayOf(F64), mutable: false },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
...extra,
|
||||
};
|
||||
return { functions: [fn], globals: [] } as unknown as IrModule;
|
||||
}
|
||||
|
||||
const rewritten = (mod: IrModule): IrStmt[] => runPairedWhileLoopsAsFor(mod).functions[0]!.body;
|
||||
|
||||
test("a two-counter while loop runs as a for loop over new counters", () => {
|
||||
const mod = program([declare("lo.0", number(0)), declare("hi.0", ref("n.0")), whileLoop()]);
|
||||
const out = runPairedWhileLoopsAsFor(mod).functions[0]!;
|
||||
// The declarations stay; the loop's counters are new locals that start
|
||||
// from them, and the trailing updates are the for loop's update.
|
||||
expect(out.body.slice(0, 2)).toEqual(mod.functions[0]!.body.slice(0, 2));
|
||||
expect(out.body[2]).toMatchObject({
|
||||
kind: "for",
|
||||
init: {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: "lo.1", init: { kind: "varRef", localId: "lo.0" } },
|
||||
{ kind: "varDecl", localId: "hi.1", init: { kind: "varRef", localId: "hi.0" } },
|
||||
],
|
||||
},
|
||||
cond: { op: "<", left: { localId: "lo.1" }, right: { localId: "hi.1" } },
|
||||
update: { kind: "block", body: [{ localId: "lo.1" }, { localId: "hi.1" }] },
|
||||
body: [
|
||||
{ expr: { kind: "arrayGet", index: { localId: "lo.1" } } },
|
||||
{ expr: { kind: "arrayGet", index: { localId: "hi.1" } } },
|
||||
],
|
||||
});
|
||||
expect(out.locals.map((l) => l.id)).toEqual(["n.0", "lo.0", "hi.0", "a", "lo.1", "hi.1"]);
|
||||
});
|
||||
|
||||
test("parameters, reversed comparisons, labels and either update order qualify", () => {
|
||||
const loop = whileLoop(undefined, [step("hi.0", "-"), step("lo.0", "+")]);
|
||||
loop.cond = { kind: "bin", op: ">=", left: ref("hi.0"), right: ref("lo.0"), type: BOOL, loc };
|
||||
loop.labels = ["outer"];
|
||||
const mod = program([declare("lo.0", number(0)), loop], {
|
||||
params: [{ localId: "hi.0", name: "hi", type: F64 }],
|
||||
});
|
||||
expect(rewritten(mod)[1]).toMatchObject({
|
||||
kind: "for",
|
||||
labels: ["outer"],
|
||||
cond: { op: ">=", left: { localId: "hi.1" }, right: { localId: "lo.1" } },
|
||||
update: { body: [{ localId: "hi.1" }, { localId: "lo.1" }] },
|
||||
});
|
||||
});
|
||||
|
||||
test("loops the for form would change, or whose counters are used elsewhere, stay", () => {
|
||||
const decls = [declare("lo.0", number(0)), declare("hi.0", ref("n.0"))];
|
||||
const kept = (body: IrStmt[], extra: Partial<IrFunction> = {}): void => {
|
||||
const mod = program(body, extra);
|
||||
expect(runPairedWhileLoopsAsFor(mod)).toBe(mod);
|
||||
};
|
||||
// A continue that binds to the loop skips the trailing updates.
|
||||
kept([...decls, whileLoop([{ kind: "continue", loc }, element(ref("lo.0"))])]);
|
||||
const labeled = whileLoop([{ kind: "continue", label: "outer", loc }, element(ref("lo.0"))]);
|
||||
labeled.labels = ["outer"];
|
||||
kept([...decls, labeled]);
|
||||
// A counter read after the loop, written before it, or declared twice.
|
||||
kept([...decls, whileLoop(), { kind: "return", value: ref("lo.0"), loc }]);
|
||||
kept([...decls, step("hi.0", "-"), whileLoop()]);
|
||||
kept([...decls, declare("lo.0", number(1)), whileLoop()]);
|
||||
// A declaration outside an enclosing loop: a later entry would resume
|
||||
// from the values the previous run left.
|
||||
kept([
|
||||
...decls,
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [whileLoop()],
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
// A parameter counter inside another loop, for the same reason.
|
||||
const outerLoop: IrStmt = {
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [whileLoop()],
|
||||
loc,
|
||||
};
|
||||
kept([outerLoop], {
|
||||
params: [
|
||||
{ localId: "lo.0", name: "lo", type: F64 },
|
||||
{ localId: "hi.0", name: "hi", type: F64 },
|
||||
],
|
||||
});
|
||||
// A counter that was never declared, or that this function captures.
|
||||
kept([declare("lo.0", number(0)), whileLoop()]);
|
||||
kept([...decls, whileLoop()], { captures: [{ localId: "hi.0", name: "hi", type: F64 }] });
|
||||
// The body writes a counter, a counter is boxed, or no counter is an index.
|
||||
kept([...decls, whileLoop([element(ref("lo.0")), step("hi.0", "-")])]);
|
||||
const boxed = program([...decls, whileLoop()]);
|
||||
boxed.functions[0]!.locals[2] = { ...number_("hi.0"), boxed: true };
|
||||
expect(runPairedWhileLoopsAsFor(boxed)).toBe(boxed);
|
||||
kept([...decls, whileLoop([])]);
|
||||
});
|
||||
|
||||
test("a continue in a nested loop keeps the outer loop eligible", () => {
|
||||
const inner: IrStmt = {
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
body: [{ kind: "continue", loc }],
|
||||
loc,
|
||||
};
|
||||
// Literal starts have known ranges, so the loop needs no start test and
|
||||
// may hold a nested loop.
|
||||
const mod = program([
|
||||
declare("lo.0", number(0)),
|
||||
declare("hi.0", number(9)),
|
||||
whileLoop([inner, element(ref("lo.0"))]),
|
||||
]);
|
||||
expect(rewritten(mod)[2]).toMatchObject({ kind: "for", body: [{ kind: "while" }, {}] });
|
||||
// A start without a known range is tested at run time, on a loop that
|
||||
// may not nest another.
|
||||
const guarded = program([
|
||||
declare("lo.0", number(0)),
|
||||
declare("hi.0", ref("n.0")),
|
||||
whileLoop([inner, element(ref("lo.0"))]),
|
||||
]);
|
||||
expect(runPairedWhileLoopsAsFor(guarded)).toBe(guarded);
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import { matchIntegerPairedForLoop } from "./integer-loops.js";
|
||||
import { analyzeIntegerRanges, type IntegerRange } from "./integer-ranges.js";
|
||||
import {
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
type WhileLoop = IrStmt & { kind: "while" };
|
||||
|
||||
/** Two-counter while loops run as two-counter for loops.
|
||||
*
|
||||
* let lo = a, hi = b;
|
||||
* while (lo < hi) { ...; lo++; hi--; }
|
||||
*
|
||||
* runs as `for (let l = lo, h = hi; l < h; l++, h--) { ... }`, whose
|
||||
* counters the backend stores as integers (matchIntegerPairedForLoop). The
|
||||
* loop qualifies when:
|
||||
* - its condition compares the two counters (`<`, `<=`, `>` or `>=`);
|
||||
* - its body ends with one update of each counter;
|
||||
* - no `continue` in the body binds to it (a for loop's `continue` runs the
|
||||
* update, a while loop's skips the body's last statements);
|
||||
* - the function mentions neither counter outside the loop, except in one
|
||||
* declaration or as a parameter;
|
||||
* - every entry starts from the declared or passed values: a declaration is
|
||||
* an earlier statement of the loop's own statement list, which runs before
|
||||
* each entry, and a loop over a parameter is in no other loop.
|
||||
*
|
||||
* The for loop's counters are new locals that start from the counters'
|
||||
* values on entry, so the declarations stay where they are and nothing
|
||||
* between them and the loop moves. The original counters are not read
|
||||
* after the loop, so they need no final value. A loop that the paired match
|
||||
* would refuse is left as it is. */
|
||||
export function runPairedWhileLoopsAsFor(mod: IrModule): IrModule {
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const next = rewriteFunction(fn);
|
||||
if (next !== fn) changed = true;
|
||||
return next;
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
|
||||
/** The local a statement steps: `x++`, `x--` or `x = ...`. */
|
||||
function updatedLocal(s: IrStmt): string | null {
|
||||
if (s.kind === "assign") return s.localId;
|
||||
if (s.kind === "exprStmt" && s.expr.kind === "incDec") return s.expr.localId;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The counters of a loop with the qualifying condition and trailing
|
||||
* updates, or null. */
|
||||
function loopCounters(loop: WhileLoop, locals: ReadonlyMap<string, IrLocal>): string[] | null {
|
||||
const cond = loop.cond;
|
||||
if (cond.kind !== "bin") return null;
|
||||
if (cond.op !== "<" && cond.op !== "<=" && cond.op !== ">" && cond.op !== ">=") return null;
|
||||
if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null;
|
||||
const counters = [cond.left.localId, cond.right.localId];
|
||||
if (counters[0] === counters[1]) return null;
|
||||
const n = loop.body.length;
|
||||
if (n < 2) return null;
|
||||
const updated = [updatedLocal(loop.body[n - 2]!), updatedLocal(loop.body[n - 1]!)];
|
||||
if (!counters.every((id) => updated.includes(id))) return null;
|
||||
for (const id of counters) {
|
||||
const local = locals.get(id);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
}
|
||||
return counters;
|
||||
}
|
||||
|
||||
function isLoop(s: IrStmt): boolean {
|
||||
return s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
|
||||
}
|
||||
|
||||
/** True when a `continue` in the body binds to the loop: an unlabeled one
|
||||
* outside nested loops, or one that names a label of the loop. */
|
||||
function continuesLoop(loop: WhileLoop): boolean {
|
||||
const labels = loop.labels ?? [];
|
||||
let depth = 0;
|
||||
let found = false;
|
||||
const expr = (e: IrExpr): boolean => !found && everyExprChild(e, expr, stmt);
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (found) return false;
|
||||
if (s.kind === "continue") {
|
||||
if (s.label === undefined ? depth === 0 : labels.includes(s.label)) found = true;
|
||||
return !found;
|
||||
}
|
||||
const nested = isLoop(s);
|
||||
if (nested) depth++;
|
||||
const ok = everyStmtChild(s, expr, stmt);
|
||||
if (nested) depth--;
|
||||
return ok;
|
||||
};
|
||||
loop.body.every(stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Mentions of each id in `node`, in any field (a string literal equal to
|
||||
* an id counts too, which only refuses more loops). */
|
||||
function countMentions(
|
||||
node: unknown,
|
||||
ids: ReadonlySet<string>,
|
||||
mentions: Map<string, number>,
|
||||
): void {
|
||||
if (typeof node === "string") {
|
||||
if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1);
|
||||
} else if (Array.isArray(node)) {
|
||||
for (const item of node) countMentions(item, ids, mentions);
|
||||
} else if (node !== null && typeof node === "object") {
|
||||
const record = node as Record<string, unknown>;
|
||||
for (const key in record)
|
||||
if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions);
|
||||
}
|
||||
}
|
||||
|
||||
/** A loop with the qualifying shape, the statements before it in its
|
||||
* statement list, and whether it is inside another loop. */
|
||||
interface Candidate {
|
||||
loop: WhileLoop;
|
||||
counters: string[];
|
||||
before: readonly IrStmt[];
|
||||
nested: boolean;
|
||||
}
|
||||
|
||||
function findCandidates(fn: IrFunction, locals: ReadonlyMap<string, IrLocal>): Candidate[] {
|
||||
const candidates: Candidate[] = [];
|
||||
let depth = 0;
|
||||
const list = (body: readonly IrStmt[]): void => {
|
||||
body.forEach((s, i) => {
|
||||
if (s.kind === "while") {
|
||||
const counters = loopCounters(s, locals);
|
||||
if (counters !== null && !continuesLoop(s))
|
||||
candidates.push({ loop: s, counters, before: body.slice(0, i), nested: depth > 0 });
|
||||
}
|
||||
visit(s);
|
||||
});
|
||||
};
|
||||
// Statement lists inside expressions are not searched: their loops stay.
|
||||
const visit = (s: IrStmt): void => {
|
||||
const loop = isLoop(s);
|
||||
if (loop) depth++;
|
||||
switch (s.kind) {
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
list(s.body);
|
||||
break;
|
||||
case "if":
|
||||
list(s.then);
|
||||
if (s.else_ !== null) list(s.else_);
|
||||
break;
|
||||
case "switch":
|
||||
for (const c of s.cases) list(c.body);
|
||||
break;
|
||||
case "tryCatch":
|
||||
list(s.tryBody);
|
||||
if (s.catchBody !== null) list(s.catchBody);
|
||||
if (s.finallyBody !== null) list(s.finallyBody);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (loop) depth--;
|
||||
};
|
||||
list(fn.body);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const candidates = findCandidates(fn, locals);
|
||||
if (candidates.length === 0) return fn;
|
||||
|
||||
const ids = new Set<string>();
|
||||
for (const c of candidates) for (const id of c.counters) ids.add(id);
|
||||
const total = new Map<string, number>();
|
||||
countMentions(fn.body, ids, total);
|
||||
const params = new Set(fn.params.map((p) => p.localId));
|
||||
// A counter this function reaches through its environment.
|
||||
const outer = new Map<string, number>();
|
||||
countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer);
|
||||
|
||||
let ranges: ReadonlyMap<IrExpr, IntegerRange | null> | null = null;
|
||||
const used = new Set(fn.locals.map((l) => l.id));
|
||||
const added: IrLocal[] = [];
|
||||
const rewritten = new Map<WhileLoop, Map<string, string>>();
|
||||
for (const { loop, counters, before, nested } of candidates) {
|
||||
const inside = new Map<string, number>();
|
||||
countMentions(loop, new Set(counters), inside);
|
||||
// Each counter's start: its declaration's initializer, or null for a
|
||||
// parameter.
|
||||
const starts = new Map<string, IrExpr | null>();
|
||||
for (const id of counters) {
|
||||
if ((outer.get(id) ?? 0) !== 0) break;
|
||||
const elsewhere = (total.get(id) ?? 0) - (inside.get(id) ?? 0);
|
||||
if (params.has(id)) {
|
||||
if (elsewhere === 0 && !nested) starts.set(id, null);
|
||||
continue;
|
||||
}
|
||||
const decl = before.find((s) => s.kind === "varDecl" && s.localId === id);
|
||||
if (elsewhere === 1 && decl?.kind === "varDecl" && decl.init !== null)
|
||||
starts.set(id, decl.init);
|
||||
}
|
||||
if (starts.size !== counters.length) continue;
|
||||
const renamed = new Map<string, string>();
|
||||
const fresh: IrLocal[] = [];
|
||||
for (const id of counters) {
|
||||
const local = locals.get(id)!;
|
||||
let k = 0;
|
||||
while (used.has(`${local.name}.${k}`)) k++;
|
||||
const next = `${local.name}.${k}`;
|
||||
used.add(next);
|
||||
renamed.set(id, next);
|
||||
fresh.push({ ...local, id: next });
|
||||
}
|
||||
const candidate = pairedForLoop(loop, renamed);
|
||||
const forLocals = new Map(locals);
|
||||
for (const local of fresh) forLocals.set(local.id, local);
|
||||
// A start reads its counter on entry, which holds the initializer's
|
||||
// value, with that value's range.
|
||||
ranges ??= analyzeIntegerRanges(fn);
|
||||
const startRanges = new Map<IrExpr, IntegerRange | null>();
|
||||
if (candidate.init?.kind === "block")
|
||||
for (const decl of candidate.init.body) {
|
||||
if (decl.kind !== "varDecl" || decl.init?.kind !== "varRef") continue;
|
||||
const init = starts.get(decl.init.localId);
|
||||
if (!init) continue;
|
||||
const literal =
|
||||
init.kind === "numLit" && Number.isSafeInteger(init.value) && !Object.is(init.value, -0);
|
||||
startRanges.set(
|
||||
decl.init,
|
||||
literal ? { min: init.value, max: init.value } : (ranges.get(init) ?? null),
|
||||
);
|
||||
}
|
||||
if (matchIntegerPairedForLoop(candidate, forLocals, startRanges) === null) {
|
||||
for (const local of fresh) used.delete(local.id);
|
||||
continue;
|
||||
}
|
||||
rewritten.set(loop, renamed);
|
||||
added.push(...fresh);
|
||||
}
|
||||
if (rewritten.size === 0) return fn;
|
||||
|
||||
const expr = (e: IrExpr): IrExpr => mapExprChildren(e, expr, stmt);
|
||||
const stmt = (s: IrStmt): IrStmt => {
|
||||
const next = mapStmtChildren(s, expr, stmt);
|
||||
const renamed = s.kind === "while" ? rewritten.get(s) : undefined;
|
||||
// The body may hold rewritten loops of its own.
|
||||
return renamed === undefined ? next : pairedForLoop(next as WhileLoop, renamed);
|
||||
};
|
||||
return { ...fn, body: fn.body.map(stmt), locals: [...fn.locals, ...added] };
|
||||
}
|
||||
|
||||
/** The for loop over the renamed counters: their starts read the original
|
||||
* counters, and the body's two trailing updates become the update. */
|
||||
function pairedForLoop(
|
||||
loop: WhileLoop,
|
||||
renamed: ReadonlyMap<string, string>,
|
||||
): IrStmt & { kind: "for" } {
|
||||
const expr = (e: IrExpr): IrExpr => {
|
||||
const next = mapExprChildren(e, expr, stmt);
|
||||
if (next.kind === "varRef" || next.kind === "incDec" || next.kind === "assignExpr") {
|
||||
const id = renamed.get(next.localId);
|
||||
if (id !== undefined) return { ...next, localId: id };
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const stmt = (s: IrStmt): IrStmt => {
|
||||
const next = mapStmtChildren(s, expr, stmt);
|
||||
if (
|
||||
next.kind === "varDecl" ||
|
||||
next.kind === "assign" ||
|
||||
next.kind === "forOf" ||
|
||||
next.kind === "rethrow"
|
||||
) {
|
||||
const id = renamed.get(next.localId);
|
||||
if (id !== undefined) return { ...next, localId: id };
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const loc = loop.loc;
|
||||
const n = loop.body.length;
|
||||
const declarations: IrStmt[] = [];
|
||||
for (const [id, next] of renamed)
|
||||
declarations.push({
|
||||
kind: "varDecl",
|
||||
localId: next,
|
||||
init: { kind: "varRef", localId: id, type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const result: IrStmt & { kind: "for" } = {
|
||||
kind: "for",
|
||||
init: { kind: "block", body: declarations, loc },
|
||||
cond: expr(loop.cond),
|
||||
update: { kind: "block", body: loop.body.slice(n - 2).map(stmt), loc },
|
||||
body: loop.body.slice(0, n - 2).map(stmt),
|
||||
loc,
|
||||
};
|
||||
if (loop.labels !== undefined) result.labels = loop.labels;
|
||||
return result;
|
||||
}
|
||||
@@ -858,6 +858,10 @@ const RUNTIME_FEATURE_RULES = {
|
||||
}),
|
||||
/** Own properties JavaScript adds to children (scr_dyn_handle_props.c). */
|
||||
handleProps: rule({ fns: ["child.propsInstall"] }),
|
||||
/** The declaration-typed boundary's adoption (scr_decl_adopt.c): its
|
||||
* casts, and closures boxed in a boundary program, which adopt their
|
||||
* arguments. */
|
||||
declAdopt: rule({ fns: ["dyn.declAdopt"], initial: (mod) => mod.declBoundary === true }),
|
||||
/** for await over checked-dynamic values (scr_dyn_for_await.c). */
|
||||
dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }),
|
||||
/** Buffer.prototype methods called on checked-dynamic values
|
||||
@@ -1081,6 +1085,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
stream: false,
|
||||
streamDyn: false,
|
||||
handleProps: false,
|
||||
declAdopt: false,
|
||||
dynForAwait: false,
|
||||
dynBuffer: false,
|
||||
builtinValues: false,
|
||||
|
||||
@@ -198,3 +198,24 @@ test("byte decoding keeps the whole-document errors and version refusal", () =>
|
||||
const sentinel = text.replace('"nan"', '"future"');
|
||||
expect(() => deserializeModuleBytes(Buffer.from(sentinel))).toThrow("Invalid IR number sentinel");
|
||||
});
|
||||
|
||||
test("file roots respell the source locations under them and nothing else", () => {
|
||||
const mod = numbers([1, 2]);
|
||||
const shim = { file: '/opt/scriptc "a"/shims/pkg/index.ts', start: 3, end: 4 };
|
||||
const other = { file: "/opt/elsewhere/shims/pkg/index.ts", start: 5, end: 6 };
|
||||
const body = mod.functions[0]!.body as IrStmt[];
|
||||
body.push(
|
||||
{ kind: "exprStmt", expr: { kind: "numLit", value: 3, type: F64, loc: shim }, loc: shim },
|
||||
{ kind: "exprStmt", expr: { kind: "numLit", value: 4, type: F64, loc: other }, loc: other },
|
||||
);
|
||||
const roots = [{ from: '/opt/scriptc "a"/shims/', to: "scriptc:shims/" }];
|
||||
const relocated = deserializeModule(serializeModuleParts(mod, roots).join(""));
|
||||
const files = (relocated.functions[0]!.body as IrStmt[]).map((stmt) => stmt.loc.file);
|
||||
expect(files).toEqual([
|
||||
"numbers.ts",
|
||||
"numbers.ts",
|
||||
"scriptc:shims/pkg/index.ts",
|
||||
"/opt/elsewhere/shims/pkg/index.ts",
|
||||
]);
|
||||
expect(serializeModuleParts(mod).join("")).toBe(serializeModule(mod, true));
|
||||
});
|
||||
|
||||
@@ -46,11 +46,41 @@ function encodeFunction(fn: IrFunction): string | undefined {
|
||||
return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber);
|
||||
}
|
||||
|
||||
/** A directory whose source location files an IR artifact names by a
|
||||
* prefix of its own: a location file `from + rest` serializes as
|
||||
* `to + rest`. */
|
||||
export interface SerializedFileRoot {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
/** Respells the source location files under `roots` in serialized parts.
|
||||
* A location's file is the first member of its JSON object, and the
|
||||
* escaped spelling of a path prefix is the prefix of the escaped path. */
|
||||
function relocateFiles(parts: string[], roots: readonly SerializedFileRoot[]): string[] {
|
||||
const needles = roots.map(({ from, to }) => ({
|
||||
from: `"file":${JSON.stringify(from).slice(0, -1)}`,
|
||||
to: `"file":${JSON.stringify(to).slice(0, -1)}`,
|
||||
}));
|
||||
return parts.map((part) =>
|
||||
needles.reduce(
|
||||
(text, { from, to }) => (text.includes(from) ? text.replaceAll(from, to) : text),
|
||||
part,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** The compact encoding as ordered parts: the module header up to the
|
||||
* function array, one part per function (with separate comma parts), and the
|
||||
* header's remainder. Concatenating the parts yields exactly
|
||||
* `serializeModule(mod, true)`; writers stream them to disk in batches. */
|
||||
export function serializeModuleParts(mod: IrModule): string[] {
|
||||
* `serializeModule(mod, true)`; writers stream them to disk in batches.
|
||||
* `fileRoots` respells the source locations in those directories, so
|
||||
* artifacts do not depend on where the compiler that wrote them is
|
||||
* installed. */
|
||||
export function serializeModuleParts(
|
||||
mod: IrModule,
|
||||
fileRoots: readonly SerializedFileRoot[] = [],
|
||||
): string[] {
|
||||
// A replacer reads through live native views. Serializing the complete
|
||||
// function array through one view refreshes every function capsule for
|
||||
// each element, making large compiler artifacts quadratic. The sentinel
|
||||
@@ -65,7 +95,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
|
||||
parts.push(encodeFunction(fn) ?? "null");
|
||||
}
|
||||
parts.push("]" + header.slice(offset + slot.length));
|
||||
return parts;
|
||||
return fileRoots.length === 0 ? parts : relocateFiles(parts, fileRoots);
|
||||
}
|
||||
|
||||
/** Compiler artifacts use compact JSON; API consumers can retain the
|
||||
|
||||
@@ -120,6 +120,8 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
|
||||
if (mod.workers !== undefined && typeof mod.workers !== "boolean")
|
||||
errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc });
|
||||
if (mod.declBoundary !== undefined && mod.declBoundary !== true)
|
||||
errors.push({ message: "declBoundary must be true when present", loc: moduleLoc });
|
||||
if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true)
|
||||
errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc });
|
||||
if (mod.workers && mod.lib !== undefined)
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type FrontendInputExclusions,
|
||||
trackedReadFile,
|
||||
} from "../frontend/input-tracker.js";
|
||||
import { compilerOwnedFileRoots } from "../frontend/dts-paths.js";
|
||||
import { provenanceSources } from "../frontend/provenance-registry.js";
|
||||
import { processFork } from "../frontend/process-fork.js";
|
||||
import { tokenEquivalentSource } from "../frontend/token-equivalence.js";
|
||||
@@ -377,7 +378,7 @@ export function prepareNativeExecutable(
|
||||
if (irInput !== "") timing("ir-input");
|
||||
const ir =
|
||||
outputKind === "ir" || options.emitIr
|
||||
? batchTextParts(serializeModuleParts(prepared.mod))
|
||||
? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots()))
|
||||
: null;
|
||||
timing("ir-serialize");
|
||||
const llvm =
|
||||
@@ -510,7 +511,9 @@ export function prepareNativeLibrary(
|
||||
const input: NativeExecutableInput = {
|
||||
ok: true,
|
||||
llvm,
|
||||
ir: options.emitIr ? batchTextParts(serializeModuleParts(prepared.mod)) : null,
|
||||
ir: options.emitIr
|
||||
? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots()))
|
||||
: null,
|
||||
sidecarJson: prepared.sidecarJson,
|
||||
features: executableLinkFeatures(prepared.mod),
|
||||
sources: [...prepared.sourceTexts],
|
||||
|
||||
@@ -79,6 +79,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
dynTier: false,
|
||||
streamDyn: false,
|
||||
handleProps: false,
|
||||
declAdopt: false,
|
||||
dynForAwait: false,
|
||||
eventsOnceSignal: false,
|
||||
dynBuffer: false,
|
||||
|
||||
@@ -99,11 +99,11 @@ test("native toolchain assets resolve from the physical manifest through package
|
||||
const direct = loadNativeToolchain(path);
|
||||
const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"));
|
||||
expect(viaLink).toEqual(direct);
|
||||
expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe"));
|
||||
expect(viaLink.ts7Executable).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe"));
|
||||
expect(
|
||||
loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" })
|
||||
loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsgo" })
|
||||
.ts7Executable,
|
||||
).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe"));
|
||||
).toBe(join(assets, "typescript/lib/tsc.exe"));
|
||||
expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe"));
|
||||
expect(
|
||||
loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), {
|
||||
|
||||
@@ -177,11 +177,11 @@ export function loadNativeToolchain(
|
||||
: [...target.defaultLinkerArgs];
|
||||
return {
|
||||
compilerVersion: manifest.compiler_version,
|
||||
// tsc-ts, the default checker except on Windows, comes with the distribution;
|
||||
// a development build takes it from the tsc-ts package installed with the
|
||||
// compiler or for the working directory. SCRIPTC_CHECKER selects a checker.
|
||||
// tsc-ts, the default checker, comes with the distribution; a development
|
||||
// build takes it from the tsc-ts package installed with the compiler or for
|
||||
// the working directory. SCRIPTC_CHECKER selects a checker.
|
||||
ts7Executable:
|
||||
checkerBackend(env, host.platform) === "tsgo"
|
||||
checkerBackend(env) === "tsgo"
|
||||
? pathFrom(root, manifest.ts7)
|
||||
: manifest.tsc_ts !== undefined
|
||||
? pathFrom(root, manifest.tsc_ts)
|
||||
|
||||
@@ -378,7 +378,7 @@ done:
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v35");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -712,7 +712,7 @@ done:
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v33");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v35");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
type IrRecord = Record<string, unknown>;
|
||||
|
||||
function recordsOf(value: unknown, out: IrRecord[] = []): IrRecord[] {
|
||||
if (value === null || typeof value !== "object") return out;
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) recordsOf(item, out);
|
||||
return out;
|
||||
}
|
||||
const record = value as IrRecord;
|
||||
out.push(record);
|
||||
for (const child of Object.values(record)) recordsOf(child, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// An untyped computed write puts every method name in the census, so typed
|
||||
// calls read the observable slot. While the slot holds the declared method,
|
||||
// the call is the native method call, not a dynamic call of the slot.
|
||||
test("typed calls of an observed method call it natively while its slot is unchanged", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-"));
|
||||
dirs.push(dir);
|
||||
const entry = join(dir, "main.ts");
|
||||
const outDir = join(dir, ".scriptc");
|
||||
const outPath = join(outDir, "main.ir.json");
|
||||
await writeFile(
|
||||
join(dir, "untyped.js"),
|
||||
"export function put(target, key, value) { target[key] = value; }\n",
|
||||
);
|
||||
await writeFile(
|
||||
entry,
|
||||
[
|
||||
'import { put } from "./untyped.js";',
|
||||
"class Repo {",
|
||||
" items: number[] = [];",
|
||||
" total(): number { let s = 0; for (const n of this.items) s += n; return s; }",
|
||||
"}",
|
||||
"const repo = new Repo();",
|
||||
"repo.items.push(1, 2);",
|
||||
"put({}, String(Math.random()), 1);",
|
||||
"export function sum(r: Repo): number { return r.total(); }",
|
||||
"console.log(sum(repo));",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const result = await compile(entry, { outDir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
const sum = module.functions.find((fn) => fn.name.endsWith("sum"));
|
||||
expect(sum).toBeDefined();
|
||||
const records = recordsOf(sum);
|
||||
const guards = records.filter(
|
||||
(record) =>
|
||||
record.kind === "ternary" &&
|
||||
(record.cond as IrRecord | undefined)?.kind === "libCall" &&
|
||||
(record.cond as IrRecord).fn === "dyn.sameValue",
|
||||
);
|
||||
expect(guards).toHaveLength(1);
|
||||
const then = guards[0]!.then as IrRecord;
|
||||
expect(then.kind).toBe("call");
|
||||
expect(String(then.callee)).toMatch(/Repo\.total$/);
|
||||
expect(module.globals?.some((global) => global.name === "Repo.prototype.total.original")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// A spread argument keeps the dynamic call, which drains the spread through
|
||||
// the iterator's current `next` (a replacement included).
|
||||
test("typed calls of an observed method with a spread argument keep the dynamic call", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-"));
|
||||
dirs.push(dir);
|
||||
const entry = join(dir, "main.ts");
|
||||
const outDir = join(dir, ".scriptc");
|
||||
const outPath = join(outDir, "main.ir.json");
|
||||
await writeFile(
|
||||
join(dir, "untyped.js"),
|
||||
"export function put(target, key, value) { target[key] = value; }\n",
|
||||
);
|
||||
await writeFile(
|
||||
entry,
|
||||
[
|
||||
'import { put } from "./untyped.js";',
|
||||
"class Bag {",
|
||||
" sum(...values: number[]): number { let s = 0; for (const n of values) s += n; return s; }",
|
||||
"}",
|
||||
"const bag = new Bag();",
|
||||
"put({}, String(Math.random()), 1);",
|
||||
"export function spread(b: Bag, values: number[]): number { return b.sum(...values); }",
|
||||
"console.log(spread(bag, [1, 2]));",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const result = await compile(entry, { outDir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
const spread = module.functions.find((fn) => fn.name.endsWith("spread"));
|
||||
expect(spread).toBeDefined();
|
||||
const guards = recordsOf(spread).filter(
|
||||
(record) =>
|
||||
record.kind === "ternary" &&
|
||||
(record.cond as IrRecord | undefined)?.kind === "libCall" &&
|
||||
(record.cond as IrRecord).fn === "dyn.sameValue",
|
||||
);
|
||||
expect(guards).toHaveLength(0);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7144-paired-while-counters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7262-fresh-literal-untyped-param/lib.js",
|
||||
"<repo>/tests/corpus/7262-fresh-literal-untyped-param/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js",
|
||||
"<repo>/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7366-typed-global-view-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7402-unknown-array-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7403-bigint-of-typed-arrays.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7441-untyped-receiver-own-string-method-names.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7480-builtin-constructor-in.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7481-class-runtime-heritage-object-error.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7482-define-property-function-class-values.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7483-constructor-factory-traits.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7484-hoisted-function-captures-later-class.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7485-global-object-own-properties.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7486-prototype-accessor-string-method-names.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/shapes.js",
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/extra.js",
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/all.js",
|
||||
"<repo>/tests/corpus/7487-namespace-spread-untyped/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7488-math-spread-untyped.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7489-math-spread-generator-only.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7536-inspect-custom-errors.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7537-inspect-custom-errors.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7538-inspect-custom-prototypes.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7565-iterable-replaceable-method-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7600-navigator-optional-chain.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7602-error-stack-limit-value.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7603-error-stack-limit-own.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7604-error-stack-limit-frozen.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/types-boundary/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/zod3-adoption/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user