Merge main

This commit is contained in:
Chris Tate
2026-10-25 21:22:05 -05:00
177 changed files with 5152 additions and 337 deletions
+1
View File
@@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `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 |
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `handler-tables` | cpu | dispatch tables of functions: `Record<string, fn>` read as `t[k]!(…)`, `t[k]?.(…)` and through a checked local, a `Map<string, fn>`, an array of handlers, and an object literal of methods; some handlers declare fewer parameters than the table type |
| `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 |
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
@@ -0,0 +1,6 @@
{
"entry": "workloads/handler-tables.ts",
"args": [
"4"
]
}
@@ -0,0 +1,108 @@
// Dispatch tables of functions: a `Record<string, fn>` of opcode handlers
// called as `ops[name]!(…)`, `ops[name]?.(…)` and through a checked local,
// a `Map<string, fn>` of the same handlers, an array of handlers indexed by
// opcode number, and an object literal of methods. Some handlers declare
// fewer parameters than the table's function type.
type Op = (acc: number, arg: number) => number;
const ops: Record<string, Op> = {
add: (acc, arg) => acc + arg,
sub: (acc, arg) => acc - arg,
mul: (acc, arg) => (acc * arg) % 1000003,
neg: (acc) => -acc,
half: (acc) => Math.floor(acc / 2),
mix: (acc, arg) => (acc ^ arg) & 0xffff,
};
const byName = new Map<string, Op>(Object.entries(ops));
const byCode: Op[] = Object.values(ops);
const names = Object.keys(ops);
const methods = {
add(acc: number, arg: number): number {
return acc + arg;
},
sub(acc: number, arg: number): number {
return acc - arg;
},
mul(acc: number, arg: number): number {
return (acc * arg) % 1000003;
},
};
interface Insn {
name: string;
code: number;
arg: number;
}
function program(length: number): Insn[] {
const out: Insn[] = [];
let seed = 12345;
for (let i = 0; i < length; i++) {
seed = (seed * 1103515245 + 12345) % 2147483648;
const code = seed % (names.length + 1);
// One in seven instructions names an opcode no table holds.
out.push({ name: code < names.length ? names[code]! : "nop", code, arg: (seed >> 8) % 97 });
}
return out;
}
function runAsserted(insns: Insn[]): number {
let acc = 1;
for (const insn of insns) if (insn.name !== "nop") acc = ops[insn.name]!(acc, insn.arg);
return acc;
}
function runOptional(insns: Insn[]): number {
let acc = 1;
for (const insn of insns) acc = ops[insn.name]?.(acc, insn.arg) ?? acc + 1;
return acc;
}
function runChecked(insns: Insn[]): number {
let acc = 1;
for (const insn of insns) {
const handler = ops[insn.name];
if (handler) acc = handler(acc, insn.arg);
}
return acc;
}
function runMap(insns: Insn[]): number {
let acc = 1;
for (const insn of insns) {
const handler = byName.get(insn.name);
if (handler !== undefined) acc = handler(acc, insn.arg);
}
return acc;
}
function runArray(insns: Insn[]): number {
let acc = 1;
for (const insn of insns) if (insn.code < byCode.length) acc = byCode[insn.code]!(acc, insn.arg);
return acc;
}
function runMethods(insns: Insn[]): number {
let acc = 1;
for (const insn of insns) {
if (insn.code === 0) acc = methods.add(acc, insn.arg);
else if (insn.code === 1) acc = methods.sub(acc, insn.arg);
else if (insn.code === 2) acc = methods["mul"](acc, insn.arg);
}
return acc;
}
const rounds = Number(process.argv[2] ?? "4");
const insns = program(200_000);
const totals = [0, 0, 0, 0, 0, 0];
for (let round = 0; round < rounds * 10; round++) {
totals[0] = (totals[0]! + runAsserted(insns)) % 1e9;
totals[1] = (totals[1]! + runOptional(insns)) % 1e9;
totals[2] = (totals[2]! + runChecked(insns)) % 1e9;
totals[3] = (totals[3]! + runMap(insns)) % 1e9;
totals[4] = (totals[4]! + runArray(insns)) % 1e9;
totals[5] = (totals[5]! + runMethods(insns)) % 1e9;
}
console.log(totals.join(" "));
@@ -4,7 +4,13 @@
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does.
Later `pipeline` stages and `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
It may also be a source function, which the pipeline calls with `{ signal }` and whose iterable result it pumps, or a `{ readable, writable }` pair, whose readable side it pumps.
Later `pipeline` stages that arrive as untyped values may be native streams, `Cipheriv` and `Decipheriv` values, functions, and iterables.
A function is called with the previous stage and `{ signal }` as a function stage: before the last stage it must return an async iterable, and as the last stage it may return a promise, whose value resolves the pipeline, or an async iterable, which feeds the PassThrough that `pipeline()` returns.
An untyped source function, function stage, or destination that returns a stream is unsupported.
An iterable becomes `Duplex.from` of it, which the next stage reads from while the stages before it are not read, as in Node.
A pipeline branches at run time on whether each of its first three untyped stages after the first is a function; a function in a later untyped stage, an untyped web stream, and a `{ readable, writable }` pair after the first stage throw a not-supported error.
`for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
+12 -6
View File
@@ -2,12 +2,18 @@ Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled pro
Direct `createRequire` calls to local addons are rejected at compile time.
A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI.
A package that looks for an optional addon at run time compiles with `--npm-static` when the lookup is inside a `try` block.
There, a `createRequire` require call with a computed specifier, and a `require.resolve` or `require.resolve.paths` call with a computed request, evaluate their arguments and then throw an error.
Its `require.resolve` read as a value is a function that throws when called.
Coverage lists these as deferred to runtime.
The package's `catch` clause then runs as it does in Node.js when the addon is missing.
Outside a `try` block, the same uses keep the package refused.
A package that looks for an optional addon at run time compiles with `--npm-static`.
In JavaScript dependencies, a `createRequire` require call with a computed specifier, and `require.resolve` called with a computed request or read as a value, resolve the specifier with the Node.js algorithm when the program runs.
`require.resolve` returns the path Node.js returns.
When nothing resolves, both throw the `MODULE_NOT_FOUND` error that Node.js throws, so a package that probes for a missing addon takes the same branch as in Node.js.
When the specifier of a require call resolves to a native addon, a builtin, or a file that is not part of the compiled program, the program prints an SC2020 error that names it and exits with status 1, because Node.js would load that module.
A `catch` clause around the call does not run in that case.
Resolution steps that the runtime does not reproduce, such as package `imports` specifiers, `require.resolve` options, and subpaths that a package does not export, end the program the same way.
Coverage lists these calls as deferred to runtime.
Other computed uses compile when they are inside a `try` block: a require call with spread arguments, a `require.resolve` call with another argument count, and `require.resolve.paths` called with a computed request evaluate their arguments and then throw an error, and `require.resolve.paths` read as a value is a function that throws when called.
Coverage lists these as deferred to runtime as well, and the package's `catch` clause then runs as it does in Node.js when the addon is missing.
Outside a `try` block, these uses keep the package refused.
This applies until compiled programs can load addons through Node.js.
Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive.
The guide includes a replacement for a small Node-API helper.
+1 -1
View File
@@ -37,7 +37,7 @@ Repeating `--output` divides the program into that many partitions, which are co
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect. Partitions drop the names of their struct types before the thin link, which would otherwise number same-named types from different partitions in an order that any edit can shift, changing the module after importing in partitions the edit did not touch.
`--import-bitcode <unit.bc>`, given once per runtime-pack unit that ships bitcode, imports runtime functions for inlining; scriptc's program builds do not pass it. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
+18
View File
@@ -173,6 +173,23 @@ void limitProfileImports() {
(void)Limited;
}
// Drops the names of a partition's struct types before its summary is
// written. Every partition declares the program's types under the same
// names, and a thin link loads the importing partition and the partitions
// it imports from into one context, which tells same-named types apart by
// appending a counter that runs across the whole link. An edit that adds a
// type anywhere shifts the counter, so the module after importing, the
// object cache key, changed in partitions whose code had not. Unnamed types
// carry no such suffix; importing still matches them by their bodies.
struct UnnameStructTypes : PassInfoMixin<UnnameStructTypes> {
PreservedAnalyses run(Module &M, ModuleAnalysisManager &) {
for (StructType *Type : M.getIdentifiedStructTypes())
Type->setName("");
return PreservedAnalyses::all();
}
static bool isRequired() { return true; }
};
// How the --profile-use profile matched the functions it annotated, summed
// over partitions: functions that received its counts, functions whose
// control flow changed since it was collected (hash mismatch), and functions
@@ -733,6 +750,7 @@ compileOptimizedPartitions(const EmitOptions &Options,
if (Annotating)
P.countAnnotated(PartitionMatch.Annotated);
ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level);
MPM.addPass(UnnameStructTypes());
MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false,
/*EmitSummaryIndex=*/true,
/*EmitModuleHash=*/true));
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
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_v45");
expect(error?.stderr).toContain("scr_runtime_abi_v46");
});
});
@@ -37,6 +37,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.utilTiming ? { utilTiming: true } : {}),
...(features.dynBuffer ? { dynBuffer: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
...(features.computedRequire ? { computedRequire: true } : {}),
events: features.processEvents,
emitter: features.emitter,
symbol: features.symbol,
@@ -101,6 +101,13 @@ export class CheckedNarrows {
}
}
/** A call to a checked extraction whose helper borrows its argument, so
* emitCheckedNarrowReceiver can lend the payload instead of calling it. */
export function borrowsCheckedNarrow(host: LlvmEmitterContext, e: IrExpr): boolean {
if (e.kind !== "call" || host.checkedNarrows.get(e) === null) return false;
return host.callLifetimes.borrowed.get(e.callee)?.has(0) === true && host.mayThrow.has(e.callee);
}
/** A checked extraction consumed at once by a field or tag load. The
* successful arm borrows the payload from the union, which itself borrows
* from its source: a dense array read keeps the element in the array slot,
@@ -113,7 +120,7 @@ export function emitCheckedNarrowReceiver(
narrow: CheckedNarrowCall,
): LlValue | null {
const callee = narrow.call.callee;
if (!host.callLifetimes.borrowed.get(callee)?.has(0) || !host.mayThrow.has(callee)) return null;
if (!borrowsCheckedNarrow(host, narrow.call)) return null;
const B = host.B;
const read = host.optionalArrayReads.get(narrow.value);
const union = read
@@ -277,6 +277,10 @@ export function emitFieldAbsentTest(B: BlockBuilder, value: string, undefinedTag
return absent;
}
/** ScrClosure.function_kind's SCR_FN_RECEIVER_FREE bit (scr_closure.h):
* the body never observes the ambient receiver. */
export const CLOSURE_RECEIVER_FREE = 16;
/** The right-hand side of a JS function-identity comparison: two closures
* are one function when their identity roots match (a signature adapter
* stands for the function it adapts). Both operands must be closures. */
@@ -299,6 +299,47 @@ test("receiver readers propagate backwards through direct calls and recursion",
).toBe(false);
});
test("signature adapters that only forward the receiver ignore it with their original", () => {
const forward = (receiver: IrExpr): IrStmt => ({
kind: "return",
value: {
kind: "callValue",
callee: ref("f.0"),
receiver,
args: [{ kind: "varRef", localId: "a.0", type: F64, loc }],
type: BOOL,
loc,
},
loc,
});
const ambient: IrExpr = { kind: "libCall", fn: "dyn.this", args: [], type: { kind: "dyn" }, loc };
const adapter: IrFunction = {
name: "%fn.adapt.x.impl",
params: [{ localId: "a.0", name: "a", type: F64 }],
captures: [{ localId: "f.0", name: "f", type: callback }],
locals: [
{ id: "f.0", name: "f", type: callback, mutable: false, boxed: true },
{ id: "a.0", name: "a", type: F64, mutable: false },
],
returnType: BOOL,
body: [forward(ambient)],
loc,
};
const readers = (): AmbientReceiverReaders =>
new AmbientReceiverReaders(new Map([[adapter.name, adapter]]));
// The adapter itself reads the receiver, only to forward it.
expect(readers().ignores(adapter)).toBe(false);
expect(readers().forwardsOnly(adapter)).toBe(true);
// Any other read of the receiver, or forwarding something else, does not.
adapter.body = [thisRead, forward(ambient)];
expect(readers().forwardsOnly(adapter)).toBe(false);
adapter.body = [forward(ref("a.0", F64))];
expect(readers().forwardsOnly(adapter)).toBe(false);
adapter.body = [forward(ambient)];
adapter.captures = [];
expect(readers().forwardsOnly(adapter)).toBe(false);
});
test("virtual calls admit every same-named method", () => {
const call: IrStmt = {
kind: "exprStmt",
+72 -19
View File
@@ -183,14 +183,14 @@ import {
unionBoxBits,
} from "./inline-unions.js";
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
import { CheckedNarrows } from "./checked-narrows.js";
import { borrowsCheckedNarrow, CheckedNarrows } from "./checked-narrows.js";
import {
emitDenseArraySet,
emitDenseSlotUnlink,
SCR_ELEM_I32,
type ArrayAccess,
} from "./dense-array-access.js";
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
import { matchMapRead, pointerMapGet, type MapReadLifetimes } from "./map-read-lifetimes.js";
import { type MapEntryFusion } from "./map-entry-fusion.js";
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
import { emitBorrowedInputs } from "./borrowed-inputs.js";
@@ -213,7 +213,13 @@ import {
} from "./string-slices.js";
import { LazyCaptures } from "./lazy-captures.js";
import { StackCaptures } from "./stack-captures.js";
import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD, SCR_ARR_FROZEN } from "./common.js";
import {
CLOSURE_RECEIVER_FREE,
emitFieldAbsentTest,
f64Lit,
SCR_ARR_FIELD,
SCR_ARR_FROZEN,
} from "./common.js";
import { ffiExtendsNarrowIntegers } from "../targets.js";
import {
emitLiteralExpr,
@@ -285,7 +291,12 @@ import {
emitBytesSet,
emitBytesIntrinsic,
} from "./expr-bytes.js";
import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
import {
emitRecordKeyGet,
emitRegexIntrinsic,
keyedRecordReadInto,
pointerRecordKeyGet,
} from "./expr-records.js";
import {
dynPromiseAdapter,
streamTypedRefCommitAdapter,
@@ -2560,7 +2571,7 @@ export class LlEmitter {
ret === "void" ? ` ${call}` : ` %r = ${call}`,
ret === "void" ? ` ret void` : ` ret ${ret} %r`,
`}`,
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${this.closureFunctionKind(fn)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
``,
);
if (lending)
@@ -3112,12 +3123,13 @@ export class LlEmitter {
// ── plumbing (frame and scope ownership) ──────
/** A symbol derived from the constant's text rather than its first-use
* index, so an edit that interns one more string leaves every other
* constant's symbol, and the code referencing it, unchanged. This lets
* native builds reuse the compiled partitions an edit does not touch. A
* collision retries with a counter, still in first-use order. */
private contentSymbol(prefix: string, text: string): string {
/** A symbol derived from a constant's text, or a helper's key, rather
* than its first-use index, so an edit that interns one more string or
* helper leaves every other symbol, and the code referencing it,
* unchanged. This lets native builds reuse the compiled partitions an
* edit does not touch. A collision retries with a counter, still in
* first-use order. */
contentSymbol(prefix: string, text: string): string {
for (let attempt = 0; ; attempt++) {
const digest = createHash("sha256")
.update(attempt === 0 ? text : `${attempt}\0${text}`)
@@ -3243,7 +3255,7 @@ export class LlEmitter {
const key = `${unionId}:${tag}`;
let sym = this.unitInstances.get(key);
if (!sym) {
sym = `sc_unit_${this.unitInstances.size}`;
sym = this.contentSymbol("sc_unit_", key);
this.unitInstances.set(key, sym);
}
this.journal?.entry("unit", key, sym);
@@ -5449,8 +5461,15 @@ export class LlEmitter {
case "retainWrapper":
addEntry(this.nullableUnions.retainWrappers, key, value as string);
return;
case "unit":
return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names);
case "unit": {
let own = this.unitInstances.get(key);
if (own === undefined) {
own = this.contentSymbol("sc_unit_", key);
this.unitInstances.set(key, own);
}
if (own !== value) names.set(value as string, own);
return;
}
case "absent":
return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names);
case "retype":
@@ -5458,9 +5477,9 @@ export class LlEmitter {
case "slots":
return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names);
case "regex":
return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names);
return this.addContentEntry(this.regexInstances, key, value as never, "sc_re_", names);
case "regexReplacer":
return addNumberedSymbol(this.regexReplacers, key, value as never, "sc_rr_", names);
return this.addContentEntry(this.regexReplacers, key, value as never, "sc_rr_", names);
case "templateStrings":
return addNumberedSymbol(
this.templateStringsInstances,
@@ -5737,7 +5756,10 @@ export class LlEmitter {
unwrittenRead(e: IrExpr, writes: Writes): boolean {
const rw = this.referenceWrites;
if (rw === null) return false;
const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
// A checked extraction reads nothing besides the union it narrows.
const keys = rw.readKeys(e, (call) =>
borrowsCheckedNarrow(this, call) ? [] : this.borrowedReturnKeys(call.callee),
);
return keys !== null && !rw.conflicts(keys, writes);
}
@@ -6014,7 +6036,21 @@ export class LlEmitter {
}
case "fieldGet":
case "recordGet":
case "recordKeyGet":
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
case "mapIntrinsic": {
// A Map binding's own pointer (maps are not plainReference types).
if (!this.borrowsWithoutOwning(e) || e.receiver.kind !== "varRef") return false;
const binding = this.binding(e.receiver.localId);
return (
binding.kind === "global" ||
(binding.kind === "local" &&
binding.local !== undefined &&
!binding.local.boxed &&
!binding.local.tdz &&
!this.localUnionStorage.has(e.receiver.localId))
);
}
case "unionNarrow":
case "downcast":
case "upcast":
@@ -7272,6 +7308,11 @@ export class LlEmitter {
return this.nullableFieldGet(e) === null;
case "recordGet":
return true;
// A keyed read whose hits are all the result's own pointer.
case "recordKeyGet":
return pointerRecordKeyGet(this, e);
case "mapIntrinsic":
return pointerMapGet(this, e);
// emitReadReceiver lends a required element of a borrowed array when
// computing the index removes no edge.
case "arrayGet":
@@ -7417,8 +7458,20 @@ export class LlEmitter {
return wrapEmitterListener(this, target, adapterFn);
}
unwrapNullableClosure(u: string, funcTag: number): string {
return unwrapNullableClosure(this, u, funcTag);
/** ScrClosure.function_kind for a closure over `fn`: generator 1, async
* 2, own prototype 4, and SCR_FN_RECEIVER_FREE (16) when its body never
* observes the ambient receiver, so indirect callers skip the bracket. */
closureFunctionKind(fn: IrFunction): number {
return (
(fn.generator ? 1 : 0) +
(fn.async ? 2 : 0) +
(fn.ownsPrototype ? 4 : 0) +
(this.receiverReaders.ignores(fn) ? CLOSURE_RECEIVER_FREE : 0)
);
}
unwrapNullableClosure(u: string, unionId: string, funcTag: number): string {
return unwrapNullableClosure(this, u, unionId, funcTag);
}
closeBindThunkFor(cbUnion: IrType, retServer: boolean): string {
@@ -8,7 +8,7 @@ import {
typeEquals,
typeKey,
} from "../../ir/ir.js";
import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
import { FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext } from "./expr-context.js";
@@ -54,7 +54,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
const key = `race:${typeKey(from)}=>${typeKey(to)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_race_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_race_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "raceAdapterFor", [from, to]);
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
@@ -217,7 +217,7 @@ export function genResultThunkFor(
const key = `gr:${typeKey(genT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = mangleGenResThunk(host.resolveThunks.size);
sym = host.contentSymbol("sc_gres_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]);
const shape = host.recordsById.get(recT.shapeId);
@@ -474,7 +474,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
const key = `cx:${param.unionId}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_cx_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_cx_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childExitThunkFor", [param]);
const def = host.unionsById.get(param.unionId);
@@ -519,7 +519,7 @@ export function childExitSignalThunkFor(
const key = `cx2:${codeParam.unionId}+${sigParam.unionId}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_cx_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_cx_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]);
const codeDef = host.unionsById.get(codeParam.unionId);
@@ -580,7 +580,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
const key = `exec:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_exec_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_exec_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "execFileThunkFor", [cbT]);
const errorParam = cbT.params[0];
@@ -652,7 +652,7 @@ export function ipcMessageThunkFor(
const key = `ipcmsg:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_ipc_msg_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ipc_msg_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]);
const param = cbT.params[0];
@@ -703,7 +703,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
const key = `ipcsend:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_ipc_send_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ipc_send_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]);
const param = cbT.params[0];
@@ -764,7 +764,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
const key = `cd:${param.unionId}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_cd_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_cd_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childDataThunkFor", [param]);
const def = host.unionsById.get(param.unionId);
@@ -799,7 +799,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
const key = `cdd:${chunk}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_cdd_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_cdd_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]);
const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
@@ -834,7 +834,7 @@ export function emitterFixedAdapter(
const key = `ee:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return { fn: sym, shim };
sym = `sc_ee_ad_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ee_ad_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
@@ -905,7 +905,7 @@ export function emitterOnceAdapter(
: `eeOnce:${fire}:${typeKey(boxerT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return { fn: sym, shim };
sym = `sc_ee_once_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ee_once_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
@@ -974,10 +974,20 @@ export function wrapEmitterListener(
export function unwrapNullableClosure(
host: LlvmEmitterContext,
u: string,
unionId: string,
funcTag: number,
): string {
const B = host.B;
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
const nullable = host.nullableUnions.get(unionId);
if (nullable !== null) {
// The union is the closure pointer itself (NULL for the unit arm).
if (nullable.repr !== "single" || nullable.nullTag >= 0 || nullable.refTag !== funcTag)
throw new InternalCompilerError("llvm emitter bug: nullable callback union shape");
const r = B.tmp();
B.line(`${r} = call ptr ${retainSym(host.shapeHost, { kind: "union", unionId })}(ptr ${u})`);
return r;
}
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ptr`);
B.line(`store ptr null, ptr ${slot}`);
@@ -1015,7 +1025,7 @@ export function closeBindThunkFor(
const key = `ncb:${cbUnion.unionId}:${retServer ? "srv" : "void"}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_ncb_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ncb_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]);
const def = host.unionsById.get(cbUnion.unionId);
@@ -1026,9 +1036,9 @@ export function closeBindThunkFor(
"llvm emitter bug: bound-close callback union lacks its func arm",
);
const oneParam = funcArm.params.length === 1;
const cbNullable = host.nullableUnions.get(cbUnion.unionId);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
host.declare(`declare void @scr_union_release(ptr)`);
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
host.declare(`declare void @scr_net_server_close_direct(ptr, ptr)`);
let trampoline: string | null = null;
@@ -1072,13 +1082,11 @@ export function closeBindThunkFor(
` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`,
` %bx = load ptr, ptr %capsp`,
` %srv = call ptr @scr_box_get_ref(ptr %bx) ; +1`,
` %tagp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 1`,
` %tag = load i32, ptr %tagp`,
...unionTagLines(cbNullable, "%cbu", "%tag"),
` %isfn = icmp eq i32 %tag, ${funcTag}`,
` br i1 %isfn, label %fn, label %go`,
`fn:`,
` %pp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 5`,
` %pv = load ptr, ptr %pp`,
...unionPeekLines(cbNullable, "%cbu", "%pv"),
];
if (oneParam) {
d.push(
@@ -1097,7 +1105,7 @@ export function closeBindThunkFor(
` br label %go`,
`go:`,
` %regv = load ptr, ptr %regslot`,
` call void @scr_union_release(ptr %cbu) ; the callee owns its +1 param`,
` call void ${releaseSym(host.shapeHost, cbUnion)}(ptr %cbu) ; the callee owns its +1 param`,
` call void @scr_net_server_close_direct(ptr %srv, ptr %regv) ; reg moves`,
);
if (retServer) {
@@ -1121,7 +1129,7 @@ export function closeOverrideWrapFor(
const key = `ncw:${cbUnion.unionId}:${retServer ? "srv" : "void"}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_ncw_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ncw_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]);
const def = host.unionsById.get(cbUnion.unionId);
@@ -1169,7 +1177,7 @@ export function settledStoreFor(
const key = `settled:${typeKey(from)}=>${typeKey(to)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_settled_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_settled_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "settledStoreFor", [from, to]);
const def = host.unionsById.get(to.unionId);
+102 -13
View File
@@ -42,7 +42,13 @@ import {
} from "./classes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js";
import {
CLOSURE_RECEIVER_FREE,
f64Lit,
ffiNativeTypeLl,
ffiNativeParamLl,
ffiNativeReturnLl,
} from "./common.js";
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
import { emitCallArrayRead } from "./local-array-reads.js";
@@ -761,8 +767,7 @@ export function emitCallExpr(
`${c} = call ptr @scr_closure_new(ptr @${host.callTarget(e.fnName)}, ${host.sizeType} ${e.captures.length})`,
);
const out = host.own({ name: c, type: e.type });
const functionKind =
(target.generator ? 1 : 0) + (target.async ? 2 : 0) + (target.ownsPrototype ? 4 : 0);
const functionKind = host.closureFunctionKind(target);
if (functionKind) {
const kindPtr = B.tmp();
B.line(`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
@@ -800,6 +805,25 @@ export function emitCallExpr(
: 0;
host.declare(`declare void @scr_closure_adopt_identity(ptr, ptr, i32)`);
B.line(`call void @scr_closure_adopt_identity(ptr ${c}, ptr ${original}, i32 ${length})`);
if (host.receiverReaders.forwardsOnly(target)) {
// The adapter forwards the ambient receiver to its original,
// so it ignores the receiver exactly when the original does.
const fromPtr = B.tmp(),
from = B.tmp(),
bit = B.tmp(),
toPtr = B.tmp(),
to = B.tmp(),
kind = B.tmp();
B.line(
`${fromPtr} = getelementptr inbounds %ScrClosure, ptr ${original}, i64 0, i32 4`,
);
B.line(`${from} = load i32, ptr ${fromPtr}`);
B.line(`${bit} = and i32 ${from}, ${CLOSURE_RECEIVER_FREE}`);
B.line(`${toPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
B.line(`${to} = load i32, ptr ${toPtr}`);
B.line(`${kind} = or i32 ${to}, ${bit}`);
B.line(`store i32 ${kind}, ptr ${toPtr}`);
}
}
});
return out;
@@ -813,7 +837,18 @@ export function emitCallExpr(
const ft = e.callee.type;
if (e.args.length !== ft.params.length)
throw new LlvmUnsupportedError("callValue:arity", e.loc);
const callee = host.stackCallbacks.emit(host, e.callee) ?? emitBorrowedInput(host, e.callee);
// A receiver that reads the ambient `this` binds what the callee
// would see without a binding: the call forwards it unchanged and
// needs no receiver bracket (signature adapters forward this way).
const forwardsReceiver =
e.receiver?.kind === "libCall" &&
e.receiver.fn === "dyn.this" &&
e.receiver.args.length === 0;
const callee =
host.stackCallbacks.emit(host, e.callee) ??
(lendsCallee(host, e, forwardsReceiver)
? host.emitReadReceiver(e.callee)
: emitBorrowedInput(host, e.callee));
// A class instance bound as the callee's `this` (`h.fn(x)`) is boxed
// only if the callee reads it: the receiver window records the
// instance (owned by this frame for the whole call) with its typed-ref
@@ -825,7 +860,10 @@ export function emitCallExpr(
!host.shapeHost.isErrorClass(e.receiver.value.type.className)
? host.emitExpr(e.receiver.value)
: null;
const receiver = e.receiver === undefined || lazyReceiver ? null : host.emitExpr(e.receiver);
const receiver =
e.receiver === undefined || lazyReceiver || forwardsReceiver
? null
: host.emitExpr(e.receiver);
const pushReceiver = (): void => {
if (lazyReceiver) {
const t = lazyReceiver.type;
@@ -865,7 +903,7 @@ export function emitCallExpr(
? borrowableInputs(host, e.args, false, host.referenceWrites?.summary(known!.name))
: [];
let dynamic = false;
if (!direct && e.receiver === undefined) {
if (!direct && (e.receiver === undefined || forwardsReceiver)) {
const references = new Set<number>();
ft.params.forEach((param, index) => {
if (isRefCounted(param)) references.add(index);
@@ -898,16 +936,51 @@ export function emitCallExpr(
lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a,
)
: args;
const invoke = (target: string, needsReceiver: boolean, values = args): string => {
// An entry through the closure's own pointer brackets the call only
// when the closure may observe the receiver (SCR_FN_RECEIVER_FREE).
const invoke = (
target: string,
bracket: boolean,
values = args,
indirect = false,
): string => {
const needsReceiver = bracket && !forwardsReceiver;
let observes: string | null = null;
if (needsReceiver) {
host.declare("declare void @scr_dyn_this_pop()");
pushReceiver();
if (indirect) {
const kindPtr = B.tmp(),
kind = B.tmp(),
free = B.tmp();
observes = B.tmp();
B.line(
`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${callee.name}, i64 0, i32 4`,
);
B.line(`${kind} = load i32, ptr ${kindPtr}`);
B.line(`${free} = and i32 ${kind}, ${CLOSURE_RECEIVER_FREE}`);
B.line(`${observes} = icmp eq i32 ${free}, 0`);
const push = B.newLabel("receiver.push"),
pushed = B.newLabel("receiver.call");
B.condBr(observes, push, pushed);
B.startBlock(push);
pushReceiver();
B.br(pushed);
B.startBlock(pushed);
} else pushReceiver();
}
const result = e.type.kind === "void" ? "" : B.tmp();
B.line(
`${result ? `${result} = ` : ""}call ${host.llType(e.type)} ${target}(${argListOf(values)})`,
);
if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
if (observes !== null) {
const pop = B.newLabel("receiver.pop"),
popped = B.newLabel("receiver.done");
B.condBr(observes, pop, popped);
B.startBlock(pop);
B.line("call void @scr_dyn_this_pop()");
B.br(popped);
B.startBlock(popped);
} else if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
return result;
};
const lending =
@@ -953,11 +1026,11 @@ export function emitCallExpr(
B.line(`${has} = icmp ne ptr ${entry}, null`);
B.condBr(has, borrowing, owning);
B.startBlock(borrowing);
const lentResult = invoke(entry, true);
const lentResult = invoke(entry, true, args, true);
const borrowingEnd = B.currentLabel();
B.br(done);
B.startBlock(owning);
const ownedResult = invoke(fn, true, ownedArgs());
const ownedResult = invoke(fn, true, ownedArgs(), true);
const owningEnd = B.currentLabel();
B.br(done);
B.startBlock(done);
@@ -966,7 +1039,7 @@ export function emitCallExpr(
B.line(
`${t} = phi ${host.llType(e.type)} [ ${lentResult}, %${borrowingEnd} ], [ ${ownedResult}, %${owningEnd} ]`,
);
} else if (!direct) t = invoke(fn, true, ownedArgs());
} else if (!direct) t = invoke(fn, true, ownedArgs(), true);
else {
// A global initializer may not have run, or a callback may enter
// through a boundary the census cannot prove. Validate the actual
@@ -984,7 +1057,7 @@ export function emitCallExpr(
const fastEnd = B.currentLabel();
B.br(done);
B.startBlock(fallback);
const indirect = invoke(fn, true, ownedArgs());
const indirect = invoke(fn, true, ownedArgs(), true);
const fallbackEnd = B.currentLabel();
B.br(done);
B.startBlock(done);
@@ -1358,6 +1431,22 @@ function ctorArguments(
* walk consumes without owning it (LlEmitter.canBorrowReceiver proved every
* reference argument borrowable and every argument edge-preserving). The
* result stays reachable from the borrowed arguments until the consumer. */
/** A closure value read from a location the call cannot write (a keyed
* table entry, a field, a checked extraction of either) is entered without
* a reference of its own: the location keeps the closure alive until the
* call returns. Plain stable bindings already lend (emitBorrowedInput). */
function lendsCallee(
host: LlvmEmitterContext,
e: ExprOf<"callValue">,
forwardsReceiver: boolean,
): boolean {
const writes = host.referenceWrites;
if (writes === null || host.canBorrowCallArgument(e.callee) || !host.canBorrowReceiver(e.callee))
return false;
const later = e.receiver === undefined || forwardsReceiver ? e.args : [e.receiver, ...e.args];
return borrowableInputs(host, [e.callee, ...later], false, writes.ownWrites(e))[0] === true;
}
export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call">): LlValue {
const callee = host.fnByName.get(e.callee);
if (!callee || !host.borrowedReturns.has(e.callee))
@@ -4,7 +4,6 @@ import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
import { InternalCompilerError } from "../../errors.js";
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { mangleResolveThunk } from "../mangle.js";
import {
arrOps,
elemAccess,
@@ -76,7 +75,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
const key = typeKey(inner);
let sym = host.resolveThunks.get(key);
if (!sym) {
sym = mangleResolveThunk(host.resolveThunks.size);
sym = host.contentSymbol("sc_rslv_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "resolveThunkFor", [inner]);
const v = vAdapters(host.shapeHost, inner);
@@ -499,10 +499,7 @@ export function emitOptionalChainReceiver(
// needs no reference of its own while the body cannot remove an edge
// (so nothing can overwrite the projection before the body finishes).
const borrowReceiver =
!multiple &&
isRefCounted(narrowed) &&
host.canBorrowReceiver(e.receiver) &&
host.referenceEffects.preserves(e.body);
!multiple && isRefCounted(narrowed) && borrowsChainReceiver(host, e.receiver, e.body);
// An optional array read of a nullable-pointer union is the element
// pointer itself, read inline and owned by the statement frame until
// the body finishes; the bind slot borrows it like an emitExpr value.
@@ -562,6 +559,16 @@ export function emitOptionalChainReceiver(
};
}
/** An optional chain's receiver can lend its payload to the bind slot when
* the body leaves the receiver's locations alone: it removes no edge at
* all, or it writes none of the locations the receiver reads. */
function borrowsChainReceiver(host: LlvmEmitterContext, receiver: IrExpr, body: IrExpr): boolean {
if (!host.canBorrowReceiver(receiver)) return false;
if (host.referenceEffects.preserves(body)) return true;
const writes = host.referenceWrites;
return writes !== null && host.unwrittenRead(receiver, writes.expr(body));
}
/** `a?.b ?? d` narrowed to the chain body's present arm. The body wraps a
* value of the result type in a non-unit arm, so the default runs exactly
* when the receiver is nullish and the intermediate result box is never
@@ -588,16 +595,21 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
if (unitTags.length === 0 || present.length !== 1) return null;
const B = host.B;
const narrowed = receiverDef.arms[present[0]!]!;
const r = host.canStackReceiver(chain.receiver)
? host.emitReadReceiver(chain.receiver)
: host.emitExpr(chain.receiver);
// A receiver read from a location the body cannot write lends its
// payload to the bind slot for the whole body (`table[k]?.(…) ?? d`).
const borrowed =
isRefCounted(narrowed) && borrowsChainReceiver(host, chain.receiver, chain.body.value);
const r =
borrowed || host.canStackReceiver(chain.receiver)
? host.emitReadReceiver(chain.receiver)
: host.emitExpr(chain.receiver);
const bindTy = host.llType(narrowed);
const bind = B.slot();
B.entryAllocas.push(`${bind} = alloca ${bindTy}`);
B.line(
`store ${bindTy} ${bindTy === "ptr" ? "null" : bindTy === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
);
host.ownSlot(bind, narrowed);
if (!borrowed) host.ownSlot(bind, narrowed);
const isUnit = host.tagInSet(r.name, receiverDef.id, unitTags);
const ty = host.llType(e.type);
const slot = B.slot();
@@ -610,7 +622,10 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
host.emitBranchInto(slot, e.right);
B.br(lj);
B.startBlock(lb);
B.line(`store ${bindTy} ${host.unionExtract(r.name, receiverDef.id, narrowed)}, ptr ${bind}`);
const payload = borrowed
? host.unionPeek(r.name, receiverDef.id)
: host.unionExtract(r.name, receiverDef.id, narrowed);
B.line(`store ${bindTy} ${payload}, ptr ${bind}`);
host.chainSlots.set(chain.id, { name: bind, type: narrowed, slot: true });
host.emitBranchInto(slot, chain.body.value);
host.chainSlots.delete(chain.id);
@@ -11,7 +11,7 @@ import { bindLiveArrayOps } from "./expr-stream-bridges.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { emitNullableIsTag, emitNullablePresent } from "./union-repr.js";
import { isObjectArm } from "./nullable-unions.js";
import { isPointerIdentityArm } from "./nullable-unions.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import {
DYN,
@@ -908,13 +908,13 @@ export function emitDynamicExpr(
if (!def)
throw new InternalCompilerError(`llvm emitter bug: equality of unknown union ${e.unionId}`);
const nullable = host.nullableUnions.get(def.id);
if (nullable && isObjectArm(nullable)) {
if (nullable && isPointerIdentityArm(nullable)) {
// Object identity, and NULL === NULL for the unit arm: one compare.
const eq = B.tmp();
B.line(`${eq} = icmp ${e.negated ? "ne" : "eq"} ptr ${l.name}, ${r.name}`);
return { name: eq, type: e.type };
}
if (nullable) {
if (nullable && nullable.repr === "single" && nullable.arm.kind === "string") {
// String arm: identical pointers are equal (the same string or the
// same unit encoding); otherwise equal only when both hold strings
// with the same bytes.
@@ -649,7 +649,7 @@ export function regexLiteralTemplate(
let re = host.regexInstances.get(key);
if (!re) {
re = {
sym: `sc_re_${host.regexInstances.size}`,
sym: host.contentSymbol("sc_re_", key),
src: host.internLiteral(e.pattern),
fl: host.internLiteral(e.flags),
pattern: e.pattern,
@@ -673,7 +673,7 @@ export function regexLiteralReplacer(
let rr = host.regexReplacers.get(key);
if (!rr) {
rr = {
sym: `sc_rr_${host.regexReplacers.size}`,
sym: host.contentSymbol("sc_rr_", key),
pattern: e.pattern,
flags: e.flags,
replacement,
@@ -10,6 +10,7 @@ import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js
import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js";
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
import { preservesRegexInputs } from "./reference-effects.js";
import { emitNumberMapSlot, emitStringMapEntry } from "./map-number-lookup.js";
/** Widen one tagged read. The source is borrowed unless the overflow map
* supplied an owned value. Each selected reference payload is retained
@@ -417,8 +418,8 @@ export function emitRecordKeyGet(
e: IrExpr & { kind: "recordKeyGet" },
): LlValue {
const B = host.B;
const obj = host.emitExpr(e.obj);
const k = host.emitExpr(e.key);
// The read runs no user code: both inputs lend for its duration.
const [obj, k] = emitBorrowedInputs(host, [e.obj, e.key]) as [LlValue, LlValue];
const ty = host.llType(e.type);
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${ty}`);
@@ -440,6 +441,80 @@ export function emitRecordKeyGet(
return host.own({ name: t, type: e.type });
}
/** A keyed read whose every hit is the result's own pointer: the result
* is a nullable-pointer union (nullable-unions.ts) with an undefined arm,
* and each declared field and the overflow value hold exactly the result
* type or its reference arm in a plain (non-inline) slot. A miss is the
* immortal undefined encoding. Such a read can lend the stored pointer:
* borrowedRecordKeyGet reads it without a reference. */
export function pointerRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean {
if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false;
const resultType = e.type;
const nullable = host.nullableUnions.single(resultType.unionId);
if (nullable === null || undefinedArmTag(resultType, host.unionsById) < 0) return false;
const exact = (t: IrType): boolean => typeEquals(t, resultType) || typeEquals(t, nullable.arm);
const shape = host.recordShape(e.shapeId);
if (e.overflowOnly !== true) {
for (const f of shape.fields)
if (host.recordFieldInline(e.shapeId, f.name) || !exact(f.type)) return false;
}
return shape.indexValue === undefined || typeEquals(shape.indexValue, nullable.arm);
}
/** A pointerRecordKeyGet read without a reference: the declared field or
* overflow entry the key names, or the undefined encoding for a miss. The
* record (and through it the stored value) must stay unchanged and alive
* until the consumer is done with the pointer (canBorrowReceiver). */
export function emitBorrowedRecordKeyGet(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordKeyGet" },
): LlValue {
const B = host.B;
if (e.type.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: borrowed keyed read of a non-union");
const resultType = e.type;
const obj = host.emitReadReceiver(e.obj);
const key = host.canBorrowReceiver(e.key) ? host.emitReadReceiver(e.key) : host.emitExpr(e.key);
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ptr`);
const join = B.newLabel("rkb.j");
const shape = host.recordShape(e.shapeId);
for (const f of e.overflowOnly === true ? [] : shape.fields) {
const hit = B.tmp();
B.line(
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
);
const lh = B.newLabel("rkb.h");
const ln = B.newLabel("rkb.n");
B.condBr(hit, lh, ln);
B.startBlock(lh);
const { ptr, type } = host.recordFieldPtr(obj.name, e.shapeId, f.name);
B.line(`store ptr ${host.loadRecordField(ptr, type, false)}, ptr ${slot}`);
B.br(join);
B.startBlock(ln);
}
if (shape.indexValue) {
const ovf = host.recordOvfPtr(obj.name, e.shapeId);
const entry = emitStringMapEntry(host, ovf, key.name);
const found = B.tmp();
B.line(`${found} = icmp ne ptr ${entry}, null`);
const lh = B.newLabel("rkb.h");
const ln = B.newLabel("rkb.n");
B.condBr(found, lh, ln);
B.startBlock(lh);
B.line(`store ptr ${emitNumberMapSlot(host, entry, "ptr")}, ptr ${slot}`);
B.br(join);
B.startBlock(ln);
}
const undefTag = undefinedArmTag(resultType, host.unionsById);
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
B.br(join);
B.startBlock(join);
const t = B.tmp();
B.line(`${t} = load ptr, ptr ${slot}`);
return { name: t, type: e.type };
}
/** A keyed read whose result is a scalar union (`r[k]` on a
* `Record<string, number>`, at `number | undefined`) and whose every hit
* is a number or boolean arm of it, or an inline field of the same union.
@@ -735,27 +810,17 @@ export function keyedRecordReadInto(
B.br(join);
B.startBlock(ln);
} else {
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
// The entry lookup filters absent keys inline; a hit retains the
// stored value with its own type's retain (map-number-lookup.ts). A
// nullable value's NULL is a stored unit arm: presence decides.
const lh = B.newLabel("rkg.h");
const ln = B.newLabel("rkg.n");
let raw: string;
if (iv.kind === "union" && host.nullableUnions.has(iv.unionId)) {
// NULL is a stored unit arm of a nullable value: presence decides.
host.declare(`declare zeroext i1 @scr_map_has_str(ptr, ptr)`);
const has = B.tmp();
B.line(`${has} = call zeroext i1 @scr_map_has_str(ptr ${ovf}, ptr ${keyName})`);
B.condBr(has, lh, ln);
B.startBlock(lh);
raw = B.tmp();
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
} else {
raw = B.tmp();
const isnull = B.tmp();
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
B.condBr(isnull, ln, lh);
B.startBlock(lh);
}
const entry = emitStringMapEntry(host, ovf, keyName);
const found = B.tmp();
B.line(`${found} = icmp ne ptr ${entry}, null`);
B.condBr(found, lh, ln);
B.startBlock(lh);
const raw = host.retainHeap(emitNumberMapSlot(host, entry, "ptr"), iv);
B.line(`store ${ty} ${surface(iv, raw, true)}, ptr ${slot}`);
B.br(join);
B.startBlock(ln);
@@ -13,7 +13,7 @@ export function streamDataAdapter(
const key = `eed:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_ee_dad_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_ee_dad_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "streamDataAdapter", [cbT]);
const p = cbT.params[0];
@@ -221,7 +221,7 @@ export function streamDoneFnFor(
const key = `sd:${kind}:${typeKey(doneT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_sd_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_sd_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]);
const errT = doneT.params[0];
@@ -365,7 +365,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
const key = `fsren:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_fsren_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_fsren_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]);
const param = cbT.params[0]!;
@@ -454,7 +454,7 @@ export function cryptoIntThunkFor(
const key = `cryptoint:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_cryptoint_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_cryptoint_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]);
const d: string[] = [
@@ -516,7 +516,7 @@ export function zlibBytesThunkFor(
const key = `${family}bytes:${arrayBuffer ? "arraybuffer:" : ""}${mode}${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_${family}bytes_${host.resolveThunks.size}`;
sym = host.contentSymbol(`sc_${family}bytes_`, key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]);
const d: string[] = [
@@ -644,7 +644,7 @@ export function streamCbThunkFor(
const key = `scb:${kind}:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_scb_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_scb_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]);
const runtimeParams =
@@ -17,7 +17,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
const key = `connect:${h2}:${typeKey(type)}`;
const previous = host.resolveThunks.get(key);
if (previous !== undefined) return previous;
const symbol = `sc_connect_${host.resolveThunks.size}`;
const symbol = host.contentSymbol("sc_connect_", key);
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "connectAdapter", [cb, h2]);
const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v";
@@ -19,7 +19,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
const key = "dgram:" + type.shapeId;
const previous = host.resolveThunks.get(key);
if (previous !== undefined) return previous;
const symbol = `sc_dgram_message_${host.resolveThunks.size}`;
const symbol = host.contentSymbol("sc_dgram_message_", key);
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "messageAdapter", [type]);
const shape = host.recordsById.get(type.shapeId);
@@ -68,7 +68,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
const key = `dns:${error.unionId}:${arity}`;
const previous = host.resolveThunks.get(key);
if (previous !== undefined) return previous;
const symbol = `sc_dns_lookup_${host.resolveThunks.size}`;
const symbol = host.contentSymbol("sc_dns_lookup_", key);
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "lookupAdapter", [cb]);
host.declare("declare ptr @scr_error_new(i32, ptr)");
@@ -671,11 +671,13 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
// which also takes those streams.
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
? "scr_dyn_builtin_stream_unbox"
: // for await over a checked-dynamic value iterates a native
// stream behind it (scr_dyn_for_await_stream.c)
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
? "scr_stream_dyn_async_iterator_open"
: LIB_FN_SYMS[e.fn]);
: e.fn === "stream.unboxDynStage" && host.usesDynBuiltinStream
? "scr_dyn_builtin_stream_unbox_stage"
: // for await over a checked-dynamic value iterates a native
// stream behind it (scr_dyn_for_await_stream.c)
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
? "scr_stream_dyn_async_iterator_open"
: LIB_FN_SYMS[e.fn]);
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
// The runtime's Date functions take and answer time values: a Date
// operand passes its time value, read at once from the borrowed object,
@@ -1197,7 +1199,7 @@ function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrT
const key = `allTuple:${fnName}`;
let adapter = host.resolveThunks.get(key);
if (adapter !== undefined) return adapter;
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
adapter = host.contentSymbol("sc_all_tuple_", key);
host.resolveThunks.set(key, adapter);
host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]);
const rc = vAdapters(host.shapeHost, tuple);
@@ -22,7 +22,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
const key = "sni:" + typeKey(type);
const previous = host.resolveThunks.get(key);
if (previous !== undefined) return previous;
const symbol = `sc_sni_answer_${host.resolveThunks.size}`;
const symbol = host.contentSymbol("sc_sni_answer_", key);
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "sniAnswer", [type]);
const params = ["ptr %self"],
@@ -95,11 +95,12 @@ export function sniCallbackArgs(host: LlvmEmitterContext, cb: LlValue): AbiArgum
let type = cb.type,
value = cb.name;
if (type.kind === "union") {
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
const unionId = type.unionId;
const arms = host.unionsById.get(unionId)?.arms ?? [];
const tag = arms.findIndex((arm) => arm.kind === "func");
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
type = arms[tag]!;
value = host.unwrapNullableClosure(value, tag);
value = host.unwrapNullableClosure(value, unionId, tag);
} else host.toHeap(cb);
if (type.kind !== "func" || type.params[1] === undefined)
throw new InternalCompilerError("SNI callback lacks its answer function");
@@ -37,7 +37,7 @@ function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func"
const key = "lookup:" + typeKey(answer);
let symbol = host.resolveThunks.get(key);
if (symbol === undefined) {
symbol = `sc_lookup_answer_${host.resolveThunks.size}`;
symbol = host.contentSymbol("sc_lookup_answer_", key);
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "lookupAnswer", [answer]);
for (const declaration of [
@@ -165,7 +165,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
if (funcTag < 0)
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its func arm`);
cb = host.unwrapNullableClosure(args[cbIndex]!.name, funcTag);
cb = host.unwrapNullableClosure(args[cbIndex]!.name, cbT.unionId, funcTag);
}
}
const valueCount = reusePort ? 5 : 4;
@@ -497,7 +497,7 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun
const key = `alsbound:${typeKey(cbT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_alsbound_${host.resolveThunks.size}`;
sym = host.contentSymbol("sc_alsbound_", key);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "boundCallbackThunk", [cbT]);
host.declare("declare ptr @scr_als_bound_target(ptr)");
@@ -292,6 +292,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"module.id": "scr_module_id",
"module.path": "scr_module_path",
"module.paths": "scr_module_paths",
"module.requireComputed": "scr_require_computed",
"module.resolveComputed": "scr_require_resolve",
"module.children": "scr_module_children",
"module.parent": "scr_module_parent",
"module.loaded": "scr_module_loaded",
@@ -824,6 +826,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"stream.stdioWritable": "scr_stream_stdio_writable",
"stream.unboxDyn": "scr_stream_unbox_dyn",
"stream.dynIterableSource": "scr_stream_dyn_iterable_source",
"stream.dynSourceValue": "scr_stream_dyn_source_value",
"stream.unboxDynStage": "scr_stream_unbox_dyn_stage",
"stream.dynTransformResult": "scr_stream_dyn_transform_result",
"stream.handleStream": "scr_stream_handle",
"stream.pipeDyn": "scr_stream_pipe_dyn",
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
@@ -743,7 +743,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const helpers = packed(5 + n + k, helperCount);
let at = 5 + n + k + helperCount;
const take = (present: boolean): string => (present ? args[at++]!.name : "null");
const box = take(((flags >> 1) & 3) === 2);
const box = take(((flags >> 1) & 3) >= 2);
const signal = take((flags & 32) !== 0);
const abort = take((flags & 32) !== 0);
const head =
@@ -12,13 +12,18 @@ import type { CallLifetimes } from "./call-lifetimes.js";
import type { ReferenceEffects } from "./reference-effects.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import {
emitCompactMapSlot,
emitNumberMapGetRef,
emitNumberMapGetScalar,
emitNumberMapSlot,
emitStringMapEntry,
emitStringMapGetScalar,
emitStringMapGetRef,
compactMapValues,
noteNumberMapKey,
} from "./map-number-lookup.js";
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
import { rememberMapEntry } from "./map-entry-fusion.js";
@@ -490,3 +495,62 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
B.line(`store i32 ${selected}, ptr ${tag}`);
return { value: { name: box, type: read.type }, owner };
}
/** A string-keyed Map.get whose result is a nullable-pointer union over the
* map's own value type (`Map<string, F>` read as `F | undefined`): a hit is
* the stored pointer itself and a miss the immortal undefined encoding, so
* the read can lend the stored value (emitBorrowedMapGet). */
export function pointerMapGet(host: LlvmEmitterContext, e: IrExpr): boolean {
if (
e.kind !== "mapIntrinsic" ||
e.method !== "get" ||
e.args.length !== 1 ||
e.type.kind !== "union" ||
e.receiver.type.kind !== "map" ||
e.receiver.type.key.kind !== "string" ||
!borrowsMapReadInputs(e)
)
return false;
const nullable = host.nullableUnions.single(e.type.unionId);
return (
nullable !== null &&
typeEquals(nullable.arm, e.receiver.type.value) &&
undefinedArmTag(e.type, host.unionsById) >= 0
);
}
/** A pointerMapGet read without a reference: the map (and through it the
* stored value) must stay unchanged and alive until the consumer is done
* with the pointer (canBorrowReceiver). */
export function emitBorrowedMapGet(
host: LlvmEmitterContext,
e: IrExpr & { kind: "mapIntrinsic" },
): LlValue {
const B = host.B;
const mapType = e.receiver.type;
if (e.type.kind !== "union" || mapType.kind !== "map")
throw new InternalCompilerError("llvm emitter bug: borrowed Map.get shape");
const receiver = host.emitReadReceiver(e.receiver);
const keyExpr = e.args[0]!;
const key = host.canBorrowReceiver(keyExpr)
? host.emitReadReceiver(keyExpr)
: host.emitExpr(keyExpr);
const compact = compactMapValues(host, mapType.key, mapType.value);
const entry = emitStringMapEntry(host, receiver.name, key.name, compact);
const found = B.tmp();
B.line(`${found} = icmp ne ptr ${entry}, null`);
const hit = B.newLabel("mapb.hit"),
miss = B.newLabel("mapb.miss"),
join = B.newLabel("mapb.join");
B.condBr(found, hit, miss);
B.startBlock(hit);
const value = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr");
B.br(join);
B.startBlock(miss);
B.br(join);
B.startBlock(join);
const absent = host.unitInstanceRef(e.type.unionId, undefinedArmTag(e.type, host.unionsById));
const result = B.tmp();
B.line(`${result} = phi ptr [ ${value}, %${hit} ], [ ${absent}, %${miss} ]`);
return { name: result, type: e.type };
}
@@ -7,7 +7,7 @@ import {
type IrType,
type IrUnionDef,
} from "../../ir/ir.js";
import { NullableUnions } from "./nullable-unions.js";
import { isObjectArm, isPointerIdentityArm, NullableUnions } from "./nullable-unions.js";
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
const a: IrType = { kind: "record", shapeId: "a" };
@@ -111,3 +111,22 @@ test("library, lifetime-region and debug builds keep record unions boxed", () =>
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
});
test("function arms are nullable pointers compared by identity root", () => {
const fn: IrType = { kind: "func", params: [F64], ret: F64 };
const unions = new NullableUnions(
module([
{ id: "fu", arms: [fn, UNDEFINED_T] },
{ id: "fnu", arms: [NULL_T, fn, UNDEFINED_T] },
{ id: "ff", arms: [fn, F64] },
]),
);
expect(unions.single("fu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: fn });
expect(unions.single("fnu")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0 });
expect(unions.get("ff")).toBeNull();
const single = unions.single("fu")!;
// Always truthy, but `===` compares adapters by the function they adapt.
expect(isObjectArm(single)).toBe(true);
expect(isPointerIdentityArm(single)).toBe(false);
expect(unions.armTypes().get("fu")).toEqual(fn);
});
@@ -89,21 +89,30 @@ export const NULLABLE_ABSENT = "@sc_nullable_absent";
/** The reference arms whose instances are never NULL. Class and record
* entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and
* sets retain through a NULL-skipping wrapper (shapes.ts). Object arms are
* always truthy and compare by identity; string arms are truthy when
* non-empty and compare bytes. */
* sets retain through a NULL-skipping wrapper (shapes.ts), as do closures
* (function arms). Object arms are always truthy and compare by identity
* (function arms by identity root); string arms are truthy when non-empty
* and compare bytes. */
export type NullableRefArm =
| { kind: "object"; className: string }
| { kind: "record"; shapeId: string }
| { kind: "string" }
| { kind: "stats" }
| (IrType & { kind: "array" | "map" | "set" });
| (IrType & { kind: "array" | "map" | "set" | "func" });
/** The arm is a JS object: always truthy, compared by identity. */
/** The arm is a JS object: always truthy. */
export function isObjectArm(nullable: PointerUnion): boolean {
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
}
/** The arm compares by pointer identity. Function arms compare identity
* roots instead: a signature adapter is the function it adapts. */
export function isPointerIdentityArm(nullable: PointerUnion): boolean {
return (
nullable.repr === "shapes" || (nullable.arm.kind !== "string" && nullable.arm.kind !== "func")
);
}
/** The representation decision for every union of one module. It is a
* pure function of the module, so serialized-IR and in-memory emission
* agree. */
@@ -264,7 +273,8 @@ function nullableShape(
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set") return { ...base, arm };
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func")
return { ...base, arm };
return null;
}
@@ -11,14 +11,24 @@ import type { LlValue } from "./expr-context.js";
import { guardedValue } from "./borrowed-receivers.js";
import { InternalCompilerError } from "../../errors.js";
import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js";
import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js";
import {
emitBorrowedMapGet,
emitStackMapRead,
matchMapRead,
pointerMapGet,
} from "./map-read-lifetimes.js";
import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
import { emitCheckedNarrowReceiver } from "./checked-narrows.js";
import { borrowsCheckedNarrow, emitCheckedNarrowReceiver } from "./checked-narrows.js";
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
import { borrowableInputs } from "./borrowed-inputs.js";
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
import {
emitBorrowedRecordKeyGet,
emitStackRecordKeyGet,
pointerRecordKeyGet,
scalarRecordKeyGet,
} from "./expr-records.js";
import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js";
/** Read a receiver whose value is consumed immediately by a field or tag
@@ -92,6 +102,10 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
return emitBorrowedResultCall(emitter, e);
const arrayRead = emitter.optionalArrayReads.get(e);
if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead);
if (e.kind === "recordKeyGet" && emitter.canBorrowReceiver(e))
return emitBorrowedRecordKeyGet(emitter, e);
if (e.kind === "mapIntrinsic" && emitter.canBorrowReceiver(e))
return emitBorrowedMapGet(emitter, e);
const narrow = emitter.checkedNarrows.get(e);
const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null;
if (narrowed) return narrowed;
@@ -294,7 +308,27 @@ export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean {
// snapshot, so neither the callee nor a later argument may replace
// what it was read from: a callee that preserves every edge cannot,
// and any other one must leave the argument's locations unwritten.
// A keyed read lends the stored pointer while the record keeps it.
case "recordKeyGet":
return (
pointerRecordKeyGet(emitter, e) &&
emitter.canBorrowReceiver(e.obj) &&
emitter.referenceEffects.preserves(e.key)
);
// So does a Map.get while the map keeps it.
case "mapIntrinsic":
return (
pointerMapGet(emitter, e) &&
emitter.canBorrowReceiver(e.receiver) &&
emitter.referenceEffects.preserves(e.args[0]!)
);
case "call": {
// A checked extraction lends the payload of the union it narrows
// (emitCheckedNarrowReceiver).
if (borrowsCheckedNarrow(emitter, e)) {
const narrow = emitter.checkedNarrows.get(e)!;
return emitter.canBorrowReceiver(narrow.value);
}
if (!emitter.borrowedReturns.has(e.callee)) return false;
if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false;
const preserving = emitter.referenceEffects.functions.has(e.callee);
@@ -97,3 +97,46 @@ test("an array that is read back or escapes keeps its element writes", () => {
const input = fill("input", [store("input", element("input"))]);
expect(writes(input).conflicts(["a"], writes(input).summary("input"))).toBe(true);
});
test("a keyed record read depends on every field and keyed entry of records", () => {
const handler: IrType = { kind: "func", params: [F64], ret: F64 };
const table: IrType = { kind: "record", shapeId: "t" };
const key: IrExpr = { kind: "strLit", value: "k", type: { kind: "string" }, loc };
const read: IrExpr = {
kind: "recordKeyGet",
obj: ref("table", table),
shapeId: "t",
key,
type: { kind: "union", unionId: "hu" },
loc,
};
const keyedWrite: IrStmt = {
kind: "recordKeySet",
obj: ref("table", table),
shapeId: "t",
key,
value: ref("h", handler),
loc,
};
const fn = (name: string, body: IrStmt[]): IrFunction => ({
name,
loc,
returnType: F64,
params: [{ localId: "table", name: "table", type: table }],
locals: [
{ id: "table", name: "table", type: table, mutable: false },
{ id: "h", name: "h", type: handler, mutable: false },
],
body,
});
const records = [{ id: "t", fields: [{ name: "add", type: handler }], indexValue: handler }];
const quiet = fn("quiet", []);
const writer = fn("writer", [keyedWrite]);
const functions = new Map([quiet, writer].map((f) => [f.name, f]));
const rw = new ReferenceWrites(functions, [], () => false, new Set(), { records });
rw.enter(quiet);
const keys = rw.readKeys(read);
expect(keys).toEqual(expect.arrayContaining(["r:*", "r:add"]));
expect(rw.conflicts(keys!, rw.summary("quiet"))).toBe(false);
expect(rw.conflicts(keys!, rw.summary("writer"))).toBe(true);
});
@@ -92,6 +92,8 @@ const INERT_LIB_PREFIXES = [
];
const INERT_LIB_CALLS = new Set([
"dyn.compare",
// Reads the ambient receiver (a fresh reference to the bound value).
"dyn.this",
"error.new",
"error.nodeThrow",
"dyn.typedRefIs",
@@ -923,6 +925,17 @@ export class ReferenceWrites {
case "arrayGet":
out.push(ARRAY_KEY);
return this.expr(node.index) === null && walk(node.arr);
case "recordKeyGet":
// Any declared field or overflow entry the key may name.
out.push(ANY_RECORD_KEY);
for (const f of this.records.get(node.shapeId)?.fields ?? []) out.push(`r:${f.name}`);
return this.expr(node.key) === null && walk(node.obj);
case "mapIntrinsic":
// A typed Map.get: any entry of any map.
if (node.method !== "get" || node.args.length !== 1 || !borrowsMapReadInputs(node))
return false;
out.push(MAP_KEY);
return this.expr(node.args[0]!) === null && walk(node.receiver);
case "unionNarrow":
case "upcast":
case "downcast":
@@ -32,10 +32,12 @@ export class StackCallbacks {
const count = B.tmp();
B.line(`${count} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 2`);
B.line(`store ${host.sizeType} ${value.captures.length}, ptr ${count}`);
if (this.functions.get(value.fnName)?.ownsPrototype) {
const target = this.functions.get(value.fnName);
const functionKind = target ? host.closureFunctionKind(target) : 0;
if (functionKind !== 0) {
const kind = B.tmp();
B.line(`${kind} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 4`);
B.line(`store i32 4, ptr ${kind}`);
B.line(`store i32 ${functionKind}, ptr ${kind}`);
}
value.captures.forEach((localId, index) => {
const box = host.retainBox(host.captureBox(localId));
-9
View File
@@ -221,15 +221,6 @@ export function mangleGenDrop(fnName: string): string {
export function mangleGenSourceClose(fnName: string): string {
return `sc_gc_${sanitize(fnName)}`;
}
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
export function mangleResolveThunk(n: number): string {
return `sc_rslv_${n}`;
}
/** Emitted generator-resume result builder (the IteratorResult record),
* interned per generator-type key. */
export function mangleGenResThunk(n: number): string {
return `sc_gres_${n}`;
}
/** Emitted Promise.race fulfillment adapter (entry inner type → result
* inner type), interned per type pair. */
export function mangleRaceThunk(n: number): string {
@@ -71,6 +71,9 @@ export interface NativeLinkFeatures {
/** Builtin functions read as values (Buffer.byteLength, Buffer.alloc,
* fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */
builtinValues?: boolean;
/** createRequire's computed require and require.resolve link
* scr_require.c; absent (false) otherwise. */
computedRequire?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -138,6 +138,9 @@ export interface CcOptions {
/** Buffer.prototype methods called on checked-dynamic values:
* compiles scr_bytes_dyn.c. */
dynBuffer?: boolean;
/** createRequire's computed require and require.resolve: compiles
* scr_require.c. */
computedRequire?: boolean;
/** Builtin functions read as values: compiles scr_builtin_values.c. */
builtinValues?: boolean;
/** The program constructs EventTarget/Event/CustomEvent values or
@@ -191,6 +191,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
// Builtin functions read as values in JavaScript dependencies.
unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"),
// createRequire's computed require and require.resolve.
unit((u) => u.opts.computedRequire === true, "scr_require.c"),
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
+2 -2
View File
@@ -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 = 45 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v45" as const;
export const RUNTIME_ABI_VERSION = 46 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v46" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
@@ -93,6 +93,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
utilTiming: boolean;
dynBuffer: boolean;
builtinValues: boolean;
computedRequire: boolean;
warningReplace: boolean;
stdioExtended: boolean;
brotli: boolean;
@@ -295,6 +296,7 @@ export function effectiveRuntimeFeatures(
utilTiming: features.utilTiming === true,
dynBuffer: features.dynBuffer === true,
builtinValues: features.builtinValues === true,
computedRequire: features.computedRequire === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
brotli: features.brotli === true,
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -129,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -879,6 +879,21 @@ export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
};
}
/** A createRequire require or require.resolve call whose specifier is
* computed at run time, where the runtime resolves it like Node
* (scr_require.c). Coverage lists the call as deferred: a require whose
* specifier resolves to a module ends the program with this code, because
* the module graph is fixed at build time. */
export function computedRequireDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
return {
code: "SC2020",
message: `${surface} with a computed specifier resolves at run time; a require of a specifier that resolves to a module, such as a native addon, ends the program`,
loc,
milestone: "later",
hint: 'require("<static string literal>") compiles the module into the program; native addons need a replacement through native FFI (https://scriptc.dev/ffi#replacing-a-node-api-addon)',
};
}
/** Records are monomorphic structs, so a value's shape must match the
* expected shape exactly OR narrow through the width-copy family
* (recordWidthPlan / the overflow capture: target fields copied off the
@@ -80,6 +80,7 @@ export interface EarlyExecutableNativeFeatures {
utilTiming?: boolean;
dynBuffer?: boolean;
builtinValues?: boolean;
computedRequire?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -1,4 +1,4 @@
import { nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js";
import { dynUndefinedExpr, nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js";
import { dirname, isAbsolute, resolve } from "node:path";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, PoisonError } from "../lowerer.js";
@@ -15,12 +15,14 @@ import {
type RuntimeResolveResult,
} from "../../runtime-resolve.js";
import {
computedRequireDiag,
invalidJsonModuleDiag,
nativeAddonDiag,
noLoweringDiag,
unsupportedPackageImportDiag,
} from "../../../diagnostics/diagnostic.js";
import { NODE_BUILTIN_MODULES_V24 } from "../surfaces.js";
import { NODE_BUILTIN_MODULES_V24, builtinModulesArrayLit } from "../surfaces.js";
import { moduleFileName } from "../import-meta.js";
import {
BOOL,
DYN,
@@ -138,6 +140,51 @@ export function lowerNodeModuleCall(
};
}
/** The run-time arguments of scr_require.c's computed require and
* require.resolve after the specifier: the requiring file (Node's require
* stack names it), the call's "file:line" for the SC2020 refusal, and the
* builtin ids. The call joins the runtime-fence ledger, so coverage lists
* it as deferred to runtime. */
function computedRequireArgs(
lowerer: Lowerer,
surface: string,
node: ts.Node,
baseFile: ts.SourceFile,
): IrExpr[] {
const loc = locOf(node);
lowerer.runtimeFences.push(computedRequireDiag(surface, loc));
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
return [
strLit(moduleFileName(lowerer, baseFile), loc),
strLit(`${loc.file}:${line}`, loc),
builtinModulesArrayLit(loc),
];
}
/** A createRequire loader's require.resolve with a computed request, in
* JavaScript source: Node's resolution at run time (scr_require.c), with
* the options argument checked there. */
function computedResolveCall(
lowerer: Lowerer,
node: ts.Node,
baseFile: ts.SourceFile,
request: IrExpr,
options: IrExpr,
loc: SrcLoc,
): IrExpr {
return {
kind: "libCall",
fn: "module.resolveComputed",
args: [
request,
options,
...computedRequireArgs(lowerer, "createRequire's require.resolve", node, baseFile),
],
type: STRING,
loc,
};
}
/** The fence a createRequire use with no build-time lowering throws in
* JavaScript source, where preflight admits it inside a try block
* (createRequireUse7 in program.ts). The diagnostic joins the runtime-fence
@@ -160,18 +207,67 @@ function createRequireFence(
};
}
/** A createRequire loader's `resolve` (or `resolve.paths`) read as a value
* in JavaScript source: a function that throws the fence when called, so
* `(options.resolve ?? require.resolve)(id)` still calls the option when
* one is given. */
/** A createRequire loader's `resolve` read as a value in JavaScript source
* (passed on, or picked by `??`): a function that resolves its request at
* run time, so `(options.resolve ?? require.resolve)(id)` behaves as in
* Node. */
export function createRequireResolveValue(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
loc: SrcLoc,
): IrExpr {
const baseFile = createRequireCalleeFileOf(lowerer, access.expression);
if (baseFile === null || !isJsSourceFile(ts.getSourceFileOfNode(access))) {
lowerer.noLowering(
"createRequire's require.resolve as a value",
access,
'call it with a string literal (require.resolve("<specifier>")), which resolves at build time',
);
}
const name = keyedHelperName(lowerer, "%createRequire.resolve", `${loc.file}:${loc.start}`);
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const call = computedResolveCall(
lowerer,
access,
baseFile,
varRef("request", DYN, loc),
varRef("options", DYN, loc),
loc,
);
lowerer.liftedFns.push({
name,
params: [
{ localId: "request", name: "request", type: DYN },
{ localId: "options", name: "options", type: DYN },
],
locals: [
{ id: "request", name: "request", type: DYN, mutable: false },
{ id: "options", name: "options", type: DYN, mutable: false },
],
returnType: STRING,
body: [{ kind: "return", value: call, loc }],
loc,
});
}
return {
kind: "dynFrom",
value: { kind: "closure", fnName: name, captures: [], type: funcOf([DYN, DYN], STRING), loc },
fnName: access.name.text,
type: DYN,
loc,
};
}
/** A createRequire loader's `resolve.paths` read as a value in JavaScript
* source: a function that throws the fence when called. */
export function createRequireResolveFenceValue(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
loc: SrcLoc,
): IrExpr {
const surface = "createRequire's require.resolve as a value";
const surface = "createRequire's require.resolve.paths as a value";
const hint =
'call it with a string literal (require.resolve("<specifier>")), which resolves at build time';
'call it with a string literal (require.resolve.paths("<specifier>")), which resolves at build time';
if (!isJsSourceFile(ts.getSourceFileOfNode(access))) lowerer.noLowering(surface, access, hint);
const name = keyedHelperName(lowerer, "%createRequire.fence", `${loc.file}:${loc.start}`);
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
@@ -234,12 +330,38 @@ export function lowerCreateRequireCall(
const cr = createRequireSpecOf(lowerer, call);
if (cr === null) return null;
if (cr.spec === null) {
const args = call.arguments.nodes;
const surface = "createRequire's require with a computed specifier or this argument shape";
const hint =
'the compiled module graph is fixed at build time — the one lowered form is require("<static string literal>"); native addons are not loaded';
if (!isJsSourceFile(ts.getSourceFileOfNode(call)))
lowerer.noLowering(surface, call, hint, undefined, "dynamic-require");
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
// Spread arguments have no run-time resolution: they run, then the
// call throws the fence.
if (args.some((a) => ts.isSpreadElement(a)))
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
// Node evaluates every argument, then requires the first.
const stmts: IrStmt[] = [];
let id = args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefinedExpr(loc);
if (args.length > 1) {
const slot = lowerer.declareHiddenLocal("%require.id", DYN);
stmts.push({ kind: "varDecl", localId: slot.id, init: id, loc });
for (const arg of args.slice(1))
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) });
id = varRef(slot.id, DYN, loc);
}
const require: IrExpr = {
kind: "libCall",
fn: "module.requireComputed",
args: [id, ...computedRequireArgs(lowerer, "createRequire's require", call, cr.baseFile)],
type: DYN,
loc,
};
return lowerer.coerceInto(
call,
stmts.length === 0 ? require : { kind: "seqExpr", stmts, result: require, type: DYN, loc },
lowerer.irTypeOf(call),
);
}
const spec = cr.spec;
if (spec === "node:ffi") {
@@ -492,6 +614,26 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio
);
}
const specifier = staticString(call.arguments.nodes[0]);
// A computed request (or options) through a createRequire loader in
// JavaScript source resolves at run time.
if (
(specifier === null || staticResolveOptionPaths(call.arguments.nodes[1]) === null) &&
!pathsCall &&
createRequireCalleeFileOf(lowerer, receiver) !== null &&
isJsSourceFile(ts.getSourceFileOfNode(call))
) {
const [request, options] = call.arguments.nodes;
return computedResolveCall(
lowerer,
call,
baseFile,
lowerer.lowerExprExpecting(request!, DYN),
options === undefined
? dynUndefinedExpr(locOf(call))
: lowerer.lowerExprExpecting(options, DYN),
locOf(call),
);
}
if (specifier === null) {
const fenced = deferred("with a computed request", resolveHint);
if (fenced) return fenced;
@@ -81,6 +81,7 @@ import { isClassCallback } from "./class-callbacks.js";
import { CaughtValueDispatch } from "./caught-value-dispatch.js";
import { refreshDescriptorGuards } from "./class-descriptors.js";
import { ClassConstructionDispatch } from "./class-construction.js";
import { keyedHelperName } from "./stable-names.js";
import { DispatchSites } from "./dispatch-sites.js";
import {
emitterRooted,
@@ -3915,7 +3916,7 @@ export class ClassDynamicDispatch {
if (!dispatch.fn.params.some((param) => param.localId === dispatch.keyLocal))
params.push({ localId: dispatch.keyLocal, name: "key", type: STRING });
const helper: IrFunction = {
name: `%dyn.class.keys.${lowerer.liftedFns.length}`,
name: keyedHelperName(lowerer, "%dyn.class.keys", helperKey),
params,
returnType: dispatch.fn.returnType,
locals: params.map((p) => ({
@@ -6793,24 +6794,41 @@ export class ClassDynamicDispatch {
type: result,
loc,
};
// A void method dispatches its overrides with statements: a ternary
// has no void form.
let voidDispatch: IrStmt[] = [{ kind: "exprStmt", expr: call, loc }];
for (const override of overrides.toReversed()) {
const subType: IrType = { kind: "object", className: override.cls.def.name };
const isOverride: IrExpr = {
kind: "instanceOf",
value: receiver,
className: override.cls.def.name,
type: BOOL,
loc,
};
const overrideCall: IrExpr = {
kind: "call",
callee: override.callee,
args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
type: result,
loc,
};
if (result.kind === "void") {
voidDispatch = [
{
kind: "if",
cond: isOverride,
then: [{ kind: "exprStmt", expr: overrideCall, loc }],
else_: voidDispatch,
loc,
},
];
continue;
}
call = {
kind: "ternary",
cond: {
kind: "instanceOf",
value: receiver,
className: override.cls.def.name,
type: BOOL,
loc,
},
then: {
kind: "call",
callee: override.callee,
args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
type: result,
loc,
},
cond: isOverride,
then: overrideCall,
else_: call,
type: result,
loc,
@@ -6818,10 +6836,7 @@ export class ClassDynamicDispatch {
}
let body: IrStmt[];
if (result.kind === "void")
body = [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: dynUndefinedExpr(loc), loc },
];
body = [...voidDispatch, { kind: "return", value: dynUndefinedExpr(loc), loc }];
else {
const boxed = lowerer.coerceToExpected(call, DYN);
if (boxed.type.kind !== "dyn") return fence();
@@ -0,0 +1,87 @@
/* Slots reached only through what a type assertion claims. */
import type { IrExpr, IrType, SrcLoc } from "../../../ir/ir.js";
import { canDynCheckTo, DYN, isUnitType, typeEquals } from "../../../ir/ir.js";
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
import type { Lowerer } from "../lowerer.js";
import { unionRetagHelper } from "./unions.js";
/** A value an erased assertion produced (`x as T`, lowerer.assertedValues)
* flowing into a slot that no ordinary conversion reaches. Runs only where
* the slot would otherwise refuse the value (SC2002/SC2003), so every
* conversion that already lowered keeps its code.
*
* - A union whose arms only partly convert (`api as Partial<Record<string,
* S>>` on `S | Partial<Record<K, S>> | undefined`): the arms that convert
* into T convert into the slot, and an arm the assertion excludes (no
* conversion into T) throws the catchable TypeError of a lying assertion.
* - A record or array whose member types the assertion narrows (`models as
* Model<"one">[]` on a union-element array): the checked deep conversion
* a cast of an unknown value takes, which validates the value member by
* member and names the deepest failing path. */
export function assertedConversion(
lowerer: Lowerer,
expr: IrExpr,
expected: IrType,
): IrExpr | null {
const asserted = lowerer.assertedValues.get(expr);
if (!asserted) return null;
const loc = expr.loc;
if (expr.type.kind === "union") {
const from = lowerer.unions.get(expr.type.unionId);
if (!from) return null;
const values = from.arms.filter((arm) => !isUnitType(arm));
const kept = values.filter((arm) => convertsInto(lowerer, arm, asserted, loc));
if (kept.length === 0 || kept.length === values.length) return null;
if (asserted.kind === "union" && expected.kind === "union") {
// The arms T excludes trap; the others (units included) re-tag.
const trappable = new Set<number>();
from.arms.forEach((arm, tag) => {
if (!isUnitType(arm) && !kept.includes(arm)) trappable.add(tag);
});
const helper = unionRetagHelper(lowerer, from.id, expected.unionId, loc, trappable);
return helper ? { kind: "call", callee: helper, args: [expr], type: expected, loc } : null;
}
// A record or array T holds one arm's values: extract that arm (every
// other arm, a unit too, contradicts the assertion) and convert it.
if (asserted.kind === "union" || kept.length !== 1) return null;
const arm = kept[0]!;
const helper = lowerer.narrowedArmHelper(from.id, arm, loc);
if (!helper) return null;
const narrowed = lowerer.checkedNarrowCall(helper, expr, arm, loc);
if (typeEquals(arm, expected)) return narrowed;
const converted = lowerer.coerceToExpected(narrowed, expected);
return typeEquals(converted.type, expected) ? converted : null;
}
if (expected.kind !== "record" && expected.kind !== "array") return null;
if (expected.kind === "record" && lowerer.shapes.get(expected.shapeId)?.indexValue !== undefined)
return null;
if (
!lowerer.dynConvertible(expr.type) ||
!canDynCheckTo(
expected,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
)
return null;
return { kind: "dynCheck", value: lowerer.coerceToExpected(expr, DYN), type: expected, loc };
}
/** True when a value of `arm` has a structural conversion into `target`:
* the same type, one of its arms, a width lift, or (into an
* index-signature record) the overflow capture. */
function convertsInto(lowerer: Lowerer, arm: IrType, target: IrType, loc: SrcLoc): boolean {
if (typeEquals(arm, target)) return true;
if (
target.kind === "union" &&
lowerer.unions.get(target.unionId)?.arms.some((candidate) => typeEquals(candidate, arm))
)
return true;
if (lowerer.widthLiftPlan(arm, target) !== null) return true;
return (
arm.kind === "record" &&
target.kind === "record" &&
lowerer.shapes.get(target.shapeId)?.indexValue !== undefined &&
lowerRecordOvfCaptureHelper(lowerer, arm.shapeId, target.shapeId, loc) !== null
);
}
@@ -91,7 +91,7 @@ import { lowerGlobalValue } from "../lower-global-value.js";
import { fenceEnumObjectValue } from "../lower-enums.js";
import { mixinFnOfCallee } from "../lower-mixins.js";
import { isNavigatorGlobal, navigatorValue } from "../builtins/navigator.js";
import { wasiGuestPath } from "../../../wasi-paths.js";
import { moduleFileName } from "../import-meta.js";
export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr {
if (lowerer.isStdlibGlobal(expr, "console")) {
@@ -1026,12 +1026,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
);
}
function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string {
return lowerer.targetPlatform === "wasi"
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
: sf.fileName;
}
/** A computed-key access (`value[key]` after `typeof value === "object"`)
* of a runtime-optional binding the checker narrowed to one record arm of a
* wider union. The binding re-tags to that record or undefined, so the keyed
@@ -1481,6 +1481,7 @@ export function lowerAsExpression(
target?.kind === "record" &&
inner.type.shapeId !== target.shapeId
) {
lowerer.assertedValues.set(inner, target);
return lowerer.coerceInto(expr, inner, target);
}
// `u as Arm` on a UNION value is `u!`'s spelling with a named arm
@@ -1515,6 +1516,15 @@ export function lowerAsExpression(
const asserted = checkedClassUnionAssertion(lowerer, inner, target, locOf(expr));
if (asserted) return asserted;
}
// The erased value still answers to the assertion: a slot it cannot
// reach by an ordinary conversion converts it as asserted
// (assertedConversion).
if (
target &&
(target.kind === "union" || target.kind === "record" || target.kind === "array") &&
(inner.type.kind === "union" || inner.type.kind === "array")
)
lowerer.assertedValues.set(inner, target);
return inner; // erasure, unchanged
}
@@ -2529,6 +2529,20 @@ export function lowerObjectLiteral(
: { kind: "seqExpr", stmts: spreadStmts, result, type, loc };
}
/** The field type is an array, or a union with exactly one array arm. */
function singleArrayArm(lowerer: Lowerer, type: IrType): boolean {
if (type.kind === "array") return true;
if (type.kind !== "union") return false;
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
return arms.filter((arm) => arm.kind === "array").length === 1;
}
function isEmptyArrayLiteral(node: ts.Node): boolean {
let inner = node;
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
return ts.isArrayLiteralExpression(inner) && inner.elements.nodes.length === 0;
}
/** A union of plain records can preserve its existing variant while
* replacing shared fields. Dispatch before constructing the copy: only
* the selected arm evaluates overrides, and recordClone snapshots fields
@@ -2577,8 +2591,11 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio
for (let index = 0; index < props.length; index++) {
const prop = props[index]!;
const node = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
const valueType = lowerer.mapTypeOf(lowerer.typeOf(node));
const fieldType = shape.fields.find((field) => field.name === names[index])!.type;
// An empty array literal (`content: []`, typed `never[]`) builds a
// fresh array of each arm's own element type.
if (isEmptyArrayLiteral(node) && singleArrayArm(lowerer, fieldType)) continue;
const valueType = lowerer.mapTypeOf(lowerer.typeOf(node));
const lift = valueType ? lowerer.widthLiftPlan(valueType, fieldType) : null;
// The inferred result must retain an override's own fields and
// aliases. A structural copy into the source field's narrower shape
@@ -156,7 +156,10 @@ import { blockedBindingUseDiag } from "../../../diagnostics/diagnostic.js";
import { npmStaticPackageOfPath } from "../../npm-static.js";
import { importMetaField, isImportMeta } from "../import-meta.js";
import { createRequireCalleeFileOf } from "../builtins/module-bindings.js";
import { createRequireResolveFenceValue } from "../builtins/module-resolution.js";
import {
createRequireResolveFenceValue,
createRequireResolveValue,
} from "../builtins/module-resolution.js";
import {
builtinPrototypeMethod,
lowerArrayIsArrayValue,
@@ -307,6 +310,7 @@ function isCreateRequireResolveValue(lowerer: Lowerer, expr: ts.PropertyAccessEx
)
resolve = expr.expression;
return (
!expr.questionDotToken &&
ts.isPropertyAccessExpression(resolve) &&
resolve.name.text === "resolve" &&
createRequireCalleeFileOf(lowerer, resolve.expression) !== null
@@ -1349,10 +1353,13 @@ export function lowerPropertyAccessExpr(
}
}
// A createRequire loader's resolve read as a value (passed on, or
// picked by `??`): resolution happens at build time, so only calls
// with a literal specifier lower.
// picked by `??`): a function that resolves its request at run time.
// resolve.paths read as a value has no run-time lowering: a function
// that throws the fence when called.
if (isCreateRequireResolveValue(lowerer, expr))
return createRequireResolveFenceValue(lowerer, expr, loc);
return expr.name.text === "paths"
? createRequireResolveFenceValue(lowerer, expr, loc)
: createRequireResolveValue(lowerer, expr, loc);
// The lib fence's PROPERTY chokepoint: a stdlib-declared member that
// no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all,
// re.exec as a value, ...) reports SC2020 here.
@@ -17,14 +17,19 @@ export function isImportMeta(expr: ts.Expression): boolean {
);
}
/** The module's file name as the compiled program reports it
* (import.meta.filename): the WASI guest path on wasi targets. */
export function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string {
return lowerer.targetPlatform === "wasi"
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
: sf.fileName;
}
/** File-backed metadata is fixed by the compiled module graph. Reads share
* one implementation across dot access, folded keys and selected bindings. */
export function importMetaField(lowerer: Lowerer, node: ts.Node, name: string): IrExpr {
const sf = ts.getSourceFileOfNode(node);
const fileName =
lowerer.targetPlatform === "wasi"
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
: sf.fileName;
const fileName = moduleFileName(lowerer, sf);
const loc = locOf(node);
switch (name) {
case "env":
@@ -10107,7 +10107,13 @@ function lowerObjLitGenericMethodCall(
type: DYN,
loc,
};
return lowerer.coerceInto(call, invoked, lowerer.irTypeOf(call));
const resultType = lowerer.irTypeOf(call);
// A void method's result stays the runtime's untyped value, as a
// call through a non-generic void member's dispatch keeps it: a class
// may implement the member with a method that returns a value, which
// JavaScript passes on (`console.log(c.publish(m))`).
if (resultType.kind === "void") return invoked;
return lowerer.coerceInto(call, invoked, resultType);
}
}
if (onInterface) {
@@ -167,6 +167,23 @@ function declareFunctionArguments(
}
}
/** The type parameters a lambda without its own takes from a generic
* contextual signature, unbound in the current scope. */
function contextualTypeParameterSymbols(
lowerer: Lowerer,
node: ts.ArrowFunction | ts.FunctionExpression,
): ts.Symbol[] {
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node));
if (signatures.length !== 1) return [];
const symbols: ts.Symbol[] = [];
for (const parameter of signatures[0]!.getTypeParameters()) {
const symbol = parameter.getSymbol();
if (!symbol || lowerer.typeParamBindings?.has(symbol)) return [];
symbols.push(symbol);
}
return symbols;
}
/** Lifts an arrow function / function expression / nested declaration /
* object-literal shorthand method to a module-level function and yields
* the `closure` expression creating it. */
@@ -202,6 +219,31 @@ export function lowerLambda(
lowerer.typeParamBindings = previous;
}
}
// A lambda contextually typed by a GENERIC signature (`send: (m) => …`
// for `send<T>(m: Msg<T>): string`) takes that signature's type
// parameters as its own; it lowers once, erased, like a generic lambda.
if (
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
!node.typeParameters?.nodes.length &&
!lowerer.erasingLambdas.has(node)
) {
const contextual = contextualTypeParameterSymbols(lowerer, node);
if (contextual.length > 0) {
const previous = lowerer.typeParamBindings;
const bindings = new Map(previous ?? []);
for (const symbol of contextual) bindings.set(symbol, DYN);
lowerer.typeParamBindings = bindings;
lowerer.erasingLambdas.add(node);
lowerer.erasingDepth++;
try {
return lowerLambda(lowerer, node, checkedReceiver);
} finally {
lowerer.erasingDepth--;
lowerer.erasingLambdas.delete(node);
lowerer.typeParamBindings = previous;
}
}
}
const prepared =
ts.isArrowFunction(node) || ts.isFunctionExpression(node)
? lowerer.preparedClassFactories.get(node)
@@ -2837,20 +2837,24 @@ export function streamClassAliasDecl(
const PIPELINE_STAGE_EXPECTED =
"of type function or an instance of Blob, ReadableStream, WritableStream, Stream, Iterable, AsyncIterable, or Promise or { readable, writable } pair";
/** A checked-dynamic pipeline stage (an untyped source or destination):
* a boxed native stream unwraps at run time; other values (functions,
* pairs) throw the not-supported Error there. */
/** A checked-dynamic pipeline stage taken as a stream: a boxed native
* stream unwraps at run time, and a value that is not stream-like or a
* function (an iterable after the first stage) becomes Duplex.from of it,
* as pipelineImpl's last branch does. Stream-like objects, web streams and
* { readable, writable } pairs throw the not-supported Error there, and so
* do functions, which reach this lowering only past
* MAX_DYN_FUNCTION_STAGES. */
function unboxDynStage(lowerer: Lowerer, v: IrExpr, node: ts.Expression): IrExpr {
noteUntypedStreamFlow(lowerer, null);
return {
kind: "libCall",
fn: "stream.unboxDyn",
fn: "stream.unboxDynStage",
args: [
v,
strLit(
lowerer.dynNodeStreams
? "pipeline stages other than native streams (JavaScript stream objects, iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet"
: "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet",
? "pipeline stages other than native streams, iterables and functions (JavaScript stream objects, web streams, { readable, writable } pairs after the first stage, and functions after the first three untyped stages) are not supported yet"
: "pipeline stages other than native streams, iterables and functions (web streams, { readable, writable } pairs after the first stage, and functions after the first three untyped stages) are not supported yet",
locOf(node),
),
],
@@ -2930,6 +2934,102 @@ const BOXED_SOURCE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
"string",
]);
/** The most checked-dynamic stages after the first that a pipeline
* branches on as functions; each one doubles the lowered pipeline calls. */
const MAX_DYN_FUNCTION_STAGES = 3;
/** A pipeline's arguments for one of the run-time branches
* lowerDynStagePipeline lowers. */
interface DynStagePlan {
/** the first stage when it is checked-dynamic */
source: DynPipelineSource | null;
/** argument values evaluated before the branches, in argument order */
bound: ReadonlyMap<number, IrExpr>;
/** a typed iterable first stage's source, lowered before the branches */
preSource?: { closure: IrExpr; out: SyncGenType } | undefined;
/** the checked-dynamic stages this branch takes as functions */
fns: ReadonlySet<number>;
}
/** pipeline() whose stages after the first include untyped values:
* pipelineImpl calls a stage that is a function with the previous stage
* and { signal }, and pipes into or takes any other value, so the call is
* lowered once per combination of those stages being functions or not,
* up to MAX_DYN_FUNCTION_STAGES of them, and a typeof test on each
* picks the branch at run time. The arguments other than function stages
* are evaluated first, in argument order. */
function lowerDynStagePipeline(
lowerer: Lowerer,
stageArgs: readonly ts.Expression[],
src: DynPipelineSource | null,
loc: SrcLoc,
build: (plan: DynStagePlan) => IrExpr,
): IrExpr {
const candidates: number[] = [];
for (let j = 1; j < stageArgs.length; j++) {
const arg = stageArgs[j]!;
if (pipelineStageFunction(lowerer, arg) !== null || isValueStream(lowerer, arg)) continue;
if ((lowerer.typeOf(arg).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
candidates.push(j);
}
}
if (candidates.length === 0) return build({ source: src, bound: new Map(), fns: new Set() });
const stmts: IrStmt[] = [];
const bind = (init: IrExpr): IrExpr => {
const local = lowerer.declareHiddenLocal("%pipelineArg", init.type);
stmts.push({ kind: "varDecl", localId: local.id, init, loc: init.loc });
return varRef(local.id, init.type, init.loc);
};
const source = src !== null && !src.iterable ? { expr: bind(src.expr), iterable: false } : src;
const bound = new Map<number, IrExpr>();
let preSource: DynStagePlan["preSource"];
for (let j = src !== null ? 1 : 0; j < stageArgs.length; j++) {
const arg = stageArgs[j]!;
if (pipelineStageFunction(lowerer, arg) !== null) continue;
if (j === 0 && iterableSourceType(lowerer, arg) !== null) {
const lowered = lowerIterableSource(lowerer, arg, null, loc);
if (lowered === null) lowerer.noLowering("pipeline over this source", arg);
preSource = { closure: bind(lowered!.closure), out: lowered!.out };
continue;
}
let value = lowerer.lowerExpr(arg);
// an untyped array, string, Set, Map or generator object that keeps
// its static type is taken through its checked-dynamic value
if (
candidates.includes(j) &&
BOXED_SOURCE_KINDS.has(value.type.kind) &&
lowerer.dynConvertible(value.type)
) {
value = { kind: "dynFrom", value, type: DYN, loc: value.loc };
}
bound.set(j, bind(value));
}
const dynStages = candidates
.filter((j) => bound.get(j)!.type.kind === "dyn")
.slice(0, MAX_DYN_FUNCTION_STAGES);
const branch = (i: number, fns: ReadonlySet<number>): IrExpr => {
if (i === dynStages.length) return build({ source, bound, preSource, fns });
const j = dynStages[i]!;
const asStream = branch(i + 1, fns);
const asFunction = branch(i + 1, new Set([...fns, j]));
// a function destination's promise resolves to its value
const type =
asFunction.type.kind === "promise" && asFunction.type.inner.kind === "dyn"
? asFunction.type
: asStream.type;
return {
kind: "ternary",
cond: { kind: "dynTest", test: "function", value: bound.get(j)!, type: BOOL, loc },
then: lowerer.coerceToExpected(asFunction, type),
else_: lowerer.coerceToExpected(asStream, type),
type,
loc,
};
};
const result = branch(0, new Set());
return { kind: "seqExpr", stmts, result, type: result.type, loc };
}
/** lowerPipelineStages' source arguments for a checked-dynamic first stage. */
function stagesSource(
src: DynPipelineSource | null,
@@ -2938,6 +3038,22 @@ function stagesSource(
return src.iterable ? [undefined, src.expr] : [new Map([[0, src.expr]]), undefined];
}
/** A pipeline stage taken as a stream under a DynStagePlan: the
* checked-dynamic first stage's stream, a value bound before the
* branches, or the argument lowered here. */
function planStreamArg(
lowerer: Lowerer,
plan: DynStagePlan,
node: ts.Expression,
i: number,
): IrExpr {
if (i === 0 && plan.source !== null) return plan.source.expr;
const bound = plan.bound.get(i);
return bound !== undefined
? streamArgOf(lowerer, node, bound, "pipeline")
: lowerStreamArg(lowerer, node, "pipeline");
}
/** A stream-rooted argument, lowered — or the pointed fence (pipeline's
* iterable/generator/function stages have no lowering yet). `what` picks
* the validation ladder: finished() rejects EVERY non-stream through
@@ -3304,7 +3420,9 @@ export function lowerStreamModuleCall(
: null;
const stageArgs = opts ? args.slice(0, -1) : args;
const build = (src: DynPipelineSource | null): IrExpr =>
lowerPromisePipeline(lowerer, stageArgs, opts, src, loc);
lowerDynStagePipeline(lowerer, stageArgs, src, loc, (plan) =>
lowerPromisePipeline(lowerer, stageArgs, opts, plan, loc),
);
return lowerDynSourcePipeline(lowerer, stageArgs[0]!, loc, build) ?? build(null);
}
return null;
@@ -3312,17 +3430,23 @@ export function lowerStreamModuleCall(
return lowerStreamModuleCallbackCall(lowerer, call, bi, loc);
}
/** stream/promises pipeline(...stages[, options]), with the first stage
* pre-lowered when it is checked-dynamic (lowerDynSourcePipeline). */
/** stream/promises pipeline(...stages[, options]), for one branch of
* lowerDynStagePipeline's plan. */
function lowerPromisePipeline(
lowerer: Lowerer,
stageArgs: readonly ts.Expression[],
opts: ReturnType<typeof pipelineOptions> | null,
src: DynPipelineSource | null,
plan: DynStagePlan,
loc: SrcLoc,
): IrExpr {
if (src?.iterable === true || hasPipelineFunctionStage(lowerer, stageArgs)) {
const staged = lowerPipelineStages(lowerer, stageArgs, loc, ...stagesSource(src));
const src = plan.source;
if (
src?.iterable === true ||
plan.fns.size > 0 ||
plan.preSource !== undefined ||
hasPipelineFunctionStage(lowerer, stageArgs)
) {
const staged = lowerPipelineStages(lowerer, stageArgs, loc, ...stagesSource(src), plan);
const type = staged.destPromise ?? { kind: "promise", inner: VOID };
// A promise destination's value settles the result (same type).
if (!opts) {
@@ -3346,9 +3470,7 @@ function lowerPromisePipeline(
}
let streams: IrExpr[];
try {
streams = stageArgs.map((a, i) =>
i === 0 && src !== null ? src.expr : lowerStreamArg(lowerer, a, "pipeline"),
);
streams = stageArgs.map((a, i) => planStreamArg(lowerer, plan, a, i));
} catch (e) {
if (e instanceof StreamArgTypeThrow) return e.expr;
throw e;
@@ -3530,7 +3652,9 @@ function lowerStreamModuleCallbackCall(
);
}
const build = (src: DynPipelineSource | null): IrExpr =>
lowerCallbackPipeline(lowerer, call, args, src, loc);
lowerDynStagePipeline(lowerer, args.slice(0, -1), src, loc, (plan) =>
lowerCallbackPipeline(lowerer, call, args, plan, loc),
);
return lowerDynSourcePipeline(lowerer, args[0]!, loc, build) ?? build(null);
}
@@ -3544,23 +3668,32 @@ function lowerStreamModuleCallbackCall(
return null;
}
/** pipeline(...stages, callback), with the first stage pre-lowered when it
* is checked-dynamic (lowerDynSourcePipeline). */
/** pipeline(...stages, callback), for one branch of lowerDynStagePipeline's
* plan. */
function lowerCallbackPipeline(
lowerer: Lowerer,
call: ts.CallExpression,
args: readonly ts.Expression[],
src: DynPipelineSource | null,
plan: DynStagePlan,
loc: SrcLoc,
): IrExpr {
if (src?.iterable === true || hasPipelineFunctionStage(lowerer, args.slice(0, -1))) {
const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc, ...stagesSource(src));
const cbNode = args[args.length - 1]!;
const cbParams =
ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode)
? cbNode.parameters.nodes.length
: 0;
if (staged.dest === 2 || (staged.dest === 1 && cbParams > 1)) {
const src = plan.source;
const cbNode = args[args.length - 1]!;
const cbParams =
ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode)
? cbNode.parameters.nodes.length
: 0;
// A branch over untyped stages whose destination is a stream: the
// callback still receives (err, value), value undefined, as in Node.
const dynCallback = plan.bound.size > 0 && cbParams > 1;
if (
src?.iterable === true ||
plan.fns.size > 0 ||
plan.preSource !== undefined ||
hasPipelineFunctionStage(lowerer, args.slice(0, -1))
) {
const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc, ...stagesSource(src), plan);
if (staged.dest >= 2 || (staged.dest === 1 && cbParams > 1) || dynCallback) {
// callback(err, value): the destination's value (undefined for
// an async generator) reaches the callback through the
// checked-dynamic path.
@@ -3595,15 +3728,15 @@ function lowerCallbackPipeline(
}
let streams: IrExpr[];
try {
streams = args
.slice(0, -1)
.map((a, i) => (i === 0 && src !== null ? src.expr : lowerStreamArg(lowerer, a, "pipeline")));
streams = args.slice(0, -1).map((a, i) => planStreamArg(lowerer, plan, a, i));
} catch (e) {
if (e instanceof StreamArgTypeThrow) return e.expr;
throw e;
}
const last = streams[streams.length - 1]!;
const { cb, dyn } = lowerEosCallback(lowerer, "pipeline", args[args.length - 1]!, last.type);
const { cb, dyn } = dynCallback
? { cb: lowerer.lowerExprExpecting(cbNode, DYN), dyn: true }
: lowerEosCallback(lowerer, "pipeline", cbNode, last.type);
return {
kind: "libCall",
fn: dyn ? "stream.pipelineDyn" : "stream.pipeline",
@@ -4213,6 +4346,397 @@ function lowerDynIterableSource(
};
}
/** The async generator a checked-dynamic stage's iterable is pumped
* through. */
function dynStageGenType(): SyncGenType {
return { kind: "generator", async: true, yieldT: DYN, retT: VOID, nextT: UNDEFINED_T };
}
/** A checked-dynamic pipeline stage after the first that is a function at
* run time, as the stage closure the runtime calls with the previous
* stage and { signal } (pipelineImpl's `typeof stream === 'function'`
* branch): it calls the function with the previous stream or generator
* as an untyped value. Before the last stage the result must be async
* iterable (ERR_INVALID_RETURN_VALUE otherwise) and is pumped on through
* lowerDynIterableSource's generator; the last stage answers the result,
* which the runtime classifies (SCR_PIPE_DEST_DYN). A previous stage
* whose type has no untyped form throws the not-supported Error. */
function lowerDynFunctionStage(
lowerer: Lowerer,
fnValue: IrExpr,
srcT: IrType,
j: number,
isLast: boolean,
loc: SrcLoc,
handoff?: DynStageHandoff,
): { closure: IrExpr; out: IrType } {
const out: IrType = isLast ? DYN : dynStageGenType();
const stageT = funcOf([srcT, DYN], out);
const take = handoff?.take === true;
const give = handoff?.give === true && !isLast;
const key = `dynFunctionStage:${isLast ? "dest" : "transform"}:${typeKey(srcT)}:${take ? 1 : 0}${give ? 1 : 0}`;
let maker = lowerer.valueHelpers.get(key);
if (!maker) {
maker = `%stream.dynFunctionStage.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, maker);
const stageName = `${maker}.stage`;
const frame = new LiftedFrame(loc);
const fnId = frame.local("fn", DYN, false);
const labelId = frame.local("label", STRING, false);
const cellId = frame.local("cell", DYN, false);
const srcId = frame.param("src", srcT);
const optsId = frame.param("opts", DYN);
const inputId = frame.local("input", DYN, false);
const resultId = frame.local("result", DYN, false);
const nextId = frame.local("next", DYN, false);
const cell = (): IrExpr => varRef(cellId, DYN, loc);
const body: IrStmt[] = [];
if (take) {
// the previous untyped stage's value, handed over as it is
// (makeAsyncIterable answers an iterable itself)
body.push(
{ kind: "varDecl", localId: inputId, init: handoffTake(cell(), loc), loc },
handoffPut(cell(), dynUndefinedExpr(loc), loc),
);
}
if (!take && !lowerer.dynConvertible(srcT)) {
body.push({
kind: "return",
value: nodeThrowExpr(
0,
"SC2020",
`pipeline function stages after a stage of type '${lowerer.fmt(srcT)}' are not supported yet`,
out,
loc,
),
loc,
});
} else {
body.push({
kind: "varDecl",
localId: resultId,
init: {
kind: "dynCall",
callee: varRef(fnId, DYN, loc),
receiver: dynUndefinedExpr(loc),
calleeName: "transform",
args: [
take
? varRef(inputId, DYN, loc)
: { kind: "dynFrom", value: varRef(srcId, srcT, loc), type: DYN, loc },
varRef(optsId, DYN, loc),
],
type: DYN,
loc,
},
loc,
});
if (isLast) {
body.push({ kind: "return", value: varRef(resultId, DYN, loc), loc });
} else {
body.push({
kind: "varDecl",
localId: nextId,
init: {
kind: "libCall",
fn: "stream.dynTransformResult",
args: [varRef(resultId, DYN, loc), varRef(labelId, STRING, loc)],
type: DYN,
loc,
},
loc,
});
// the next untyped function stage takes the result itself; the
// generator returned for the runtime is then never read
if (give) body.push(handoffPut(cell(), varRef(nextId, DYN, loc), loc));
const iterable = lowerDynIterableSource(lowerer, varRef(nextId, DYN, loc), loc);
if (!iterable)
throw new InternalCompilerError("dyn iterable generator without a result record");
body.push({
kind: "return",
value: { kind: "callValue", callee: iterable.closure, args: [], type: out, loc },
loc,
});
}
}
const captured = take || give ? [fnId, labelId, cellId] : [fnId, labelId];
for (const id of captured) frame.locals.find((l) => l.id === id)!.boxed = true;
lowerer.liftedFns.push({
name: stageName,
params: frame.params,
returnType: out,
captures: [
{ localId: fnId, name: "fn", type: DYN },
{ localId: labelId, name: "label", type: STRING },
...(take || give ? [{ localId: cellId, name: "cell", type: DYN }] : []),
],
locals: frame.locals,
body,
loc,
});
const makerFn: IrLocal = { id: "fn.0", name: "fn", type: DYN, mutable: false, boxed: true };
const makerLabel: IrLocal = {
id: "label.1",
name: "label",
type: STRING,
mutable: false,
boxed: true,
};
const makerCell: IrLocal = {
id: "cell.2",
name: "cell",
type: DYN,
mutable: false,
boxed: true,
};
const makerLocals = take || give ? [makerFn, makerLabel, makerCell] : [makerFn, makerLabel];
lowerer.liftedFns.push({
name: maker,
params: makerLocals.map((l) => ({ localId: l.id, name: l.name, type: l.type })),
returnType: stageT,
locals: makerLocals,
body: [
{
kind: "return",
value: {
kind: "closure",
fnName: stageName,
captures: makerLocals.map((l) => l.id),
type: stageT,
loc,
},
loc,
},
],
loc,
});
}
return {
closure: {
kind: "call",
callee: maker,
args: [fnValue, strLit(`transform[${j - 1}]`, loc), ...(take || give ? [handoff!.cell] : [])],
type: stageT,
loc,
},
out,
};
}
/** The cell that carries an untyped stage's value to the untyped function
* stage after it, created per pipeline call. A stage that gives stores its
* value; the next stage takes it instead of the generator the runtime
* passes, as Node's makeAsyncIterable answers an iterable itself. */
interface DynStageHandoff {
cell: IrExpr;
take: boolean;
give: boolean;
}
function handoffPut(cell: IrExpr, value: IrExpr, loc: SrcLoc): IrStmt {
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.keySet",
args: [cell, strLit("v", loc), value],
type: VOID,
loc,
},
loc,
};
}
function handoffTake(cell: IrExpr, loc: SrcLoc): IrExpr {
return { kind: "dynKeyGet", value: cell, key: strLit("v", loc), type: DYN, loc };
}
/** `(value) => generator over value`: the async generator an untyped
* destination function's async iterable result is pumped through
* (SCR_PIPE_DEST_DYN's box), interned. */
function dynStageIterHelper(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const genT = dynStageGenType();
const type = funcOf([DYN], genT);
const key = "dynStageIter";
let name = lowerer.valueHelpers.get(key);
if (!name) {
name = `%stream.dynStageIter.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, name);
const v: IrLocal = { id: "value.0", name: "value", type: DYN, mutable: false };
const iterable = lowerDynIterableSource(lowerer, varRef(v.id, DYN, loc), loc);
if (!iterable)
throw new InternalCompilerError("dyn iterable generator without a result record");
lowerer.liftedFns.push({
name,
params: [{ localId: v.id, name: v.name, type: DYN }],
returnType: genT,
locals: [v],
body: [
{
kind: "return",
value: { kind: "callValue", callee: iterable.closure, args: [], type: genT, loc },
loc,
},
],
loc,
});
}
return { kind: "closure", fnName: name, captures: [], type, loc };
}
/** A checked-dynamic first pipeline stage pumped as an iterable
* (pipelineImpl's i === 0 branch), as the stage closure the runtime calls
* with { signal } while it sets the pipeline up: a function is called
* with { signal } and its result must be iterable, a { readable, writable }
* pair pumps its readable side, and any other value is pumped itself,
* through lowerDynIterableSource's generator. */
function lowerDynSourceStage(
lowerer: Lowerer,
value: IrExpr,
loc: SrcLoc,
cell?: IrExpr,
): { closure: IrExpr; out: SyncGenType } | null {
const out: SyncGenType = {
kind: "generator",
async: true,
yieldT: DYN,
retT: VOID,
nextT: UNDEFINED_T,
};
const stageT = funcOf([DYN], out);
const give = cell !== undefined;
const key = give ? "dynSourceStage:give" : "dynSourceStage";
let maker = lowerer.valueHelpers.get(key);
if (!maker) {
maker = `%stream.dynSourceStage.${lowerer.valueHelpers.size}`;
const stageName = `${maker}.stage`;
const frame = new LiftedFrame(loc);
const srcId = frame.local("src", DYN, false);
const cellId = frame.local("cell", DYN, false);
const optsId = frame.param("opts", DYN);
const valueId = frame.local("value", DYN, false);
const genId = frame.local("gen", out, false);
const src = (): IrExpr => varRef(srcId, DYN, loc);
const sourceValue = (v: IrExpr, returned: boolean): IrExpr => ({
kind: "libCall",
fn: "stream.dynSourceValue",
args: [v, boolLit(returned, loc)],
type: DYN,
loc,
});
const iterable = lowerDynIterableSource(lowerer, varRef(valueId, DYN, loc), loc);
if (!iterable) return null;
lowerer.valueHelpers.set(key, maker);
const body: IrStmt[] = [
{
kind: "varDecl",
localId: valueId,
init: {
kind: "ternary",
cond: { kind: "dynTest", test: "function", value: src(), type: BOOL, loc },
then: sourceValue(
{
kind: "dynCall",
callee: src(),
receiver: dynUndefinedExpr(loc),
calleeName: "source",
args: [varRef(optsId, DYN, loc)],
type: DYN,
loc,
},
true,
),
else_: sourceValue(src(), false),
type: DYN,
loc,
},
loc,
},
{
kind: "varDecl",
localId: genId,
init: { kind: "callValue", callee: iterable.closure, args: [], type: out, loc },
loc,
},
];
if (give) {
// the untyped function stage after the source takes the iterable
// itself, as Node's makeAsyncIterable answers it; a stream (a pair's
// readable side) goes as the generator over it, as Node iterates a
// stream there
const v = varRef(valueId, DYN, loc);
body.push(
handoffPut(
varRef(cellId, DYN, loc),
{
kind: "ternary",
cond: { kind: "libCall", fn: "stream.dynIterableSource", args: [v], type: BOOL, loc },
then: v,
else_: { kind: "dynFrom", value: varRef(genId, out, loc), type: DYN, loc },
type: DYN,
loc,
},
loc,
),
);
}
body.push({ kind: "return", value: varRef(genId, out, loc), loc });
const captured = give ? [srcId, cellId] : [srcId];
for (const id of captured) frame.locals.find((l) => l.id === id)!.boxed = true;
lowerer.liftedFns.push({
name: stageName,
params: frame.params,
returnType: out,
captures: [
{ localId: srcId, name: "src", type: DYN },
...(give ? [{ localId: cellId, name: "cell", type: DYN }] : []),
],
locals: frame.locals,
body,
loc,
});
const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true };
const makerCell: IrLocal = {
id: "cell.1",
name: "cell",
type: DYN,
mutable: false,
boxed: true,
};
const makerLocals = give ? [makerSrc, makerCell] : [makerSrc];
lowerer.liftedFns.push({
name: maker,
params: makerLocals.map((l) => ({ localId: l.id, name: l.name, type: l.type })),
returnType: stageT,
locals: makerLocals,
body: [
{
kind: "return",
value: {
kind: "closure",
fnName: stageName,
captures: makerLocals.map((l) => l.id),
type: stageT,
loc,
},
loc,
},
],
loc,
});
}
return {
closure: {
kind: "call",
callee: maker,
args: give ? [value, cell!] : [value],
type: stageT,
loc,
},
out,
};
}
function lowerIterableSource(
lowerer: Lowerer,
node: ts.Expression,
@@ -4356,7 +4880,7 @@ function lowerIterableSource(
interface PipelineStagesLowering {
args: IrExpr[];
last: IrType;
dest: 0 | 1 | 2;
dest: 0 | 1 | 2 | 3;
destPromise: (IrType & { kind: "promise" }) | null;
wrap: (call: IrExpr) => IrExpr;
}
@@ -4377,6 +4901,7 @@ function lowerPipelineStages(
loc: SrcLoc,
preLowered?: ReadonlyMap<number, IrExpr>,
dynSource?: IrExpr,
plan?: DynStagePlan,
): PipelineStagesLowering {
if (stageArgs.length > 52) {
lowerer.noLowering(
@@ -4390,7 +4915,7 @@ function lowerPipelineStages(
const helperArgs: IrExpr[] = [];
let positions = 0;
let mask = 0;
let dest: 0 | 1 | 2 = 0;
let dest: 0 | 1 | 2 | 3 = 0;
let destPromise: (IrType & { kind: "promise" }) | null = null;
// What the next function stage receives: null before the first stage,
// else the previous stream's type or the previous stage's generator.
@@ -4399,17 +4924,69 @@ function lowerPipelineStages(
// before all stages, so when a stage precedes a stream the arguments
// are bound to locals first, keeping JavaScript's evaluation order.
const argOrder: { list: IrExpr[]; index: number }[] = [];
// An untyped stage followed by an untyped function stage hands its value
// over through one cell per call (DynStageHandoff).
let handoffCell: IrLocal | null = null;
const cellRef = (): IrExpr => {
handoffCell ??= lowerer.declareHiddenLocal("%pipelineHandoff", DYN);
return varRef(handoffCell.id, DYN, loc);
};
let takes = false;
for (let j = 0; j < stageArgs.length; j++) {
const arg = stageArgs[j]!;
const givesToNext = plan?.fns.has(j + 1) === true;
if (j === 0 && dynSource !== undefined) {
const source = lowerDynIterableSource(lowerer, dynSource, loc);
const source = lowerDynSourceStage(
lowerer,
dynSource,
loc,
givesToNext ? cellRef() : undefined,
);
takes = givesToNext;
if (source === null) lowerer.noLowering("pipeline over this source", arg);
// a source function takes { signal }
mask |= 1 << stages.length;
positions += 1;
argOrder.push({ list: stages, index: stages.length });
stages.push(source!.closure);
srcT = source!.out;
continue;
}
if (plan?.fns.has(j) === true) {
// a checked-dynamic stage that is a function at run time
const isLast = j === stageArgs.length - 1;
const take = takes;
const give = givesToNext && !isLast;
takes = give;
const lowered = lowerDynFunctionStage(
lowerer,
plan.bound.get(j)!,
srcT!,
j,
isLast,
loc,
take || give ? { cell: cellRef(), take, give } : undefined,
);
mask |= 1 << stages.length;
positions += 2 ** j;
argOrder.push({ list: stages, index: stages.length });
stages.push(lowered.closure);
if (isLast) {
// its result is classified at run time: a promise settles the
// pipeline's value, and an async iterable is pumped into the
// PassThrough
const genT = dynStageGenType();
const helpers = fromGenHelpers(lowerer, genT, loc);
if (!helpers) lowerer.noLowering("a pipeline destination with this generator type", arg);
helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!);
dest = 3;
destPromise = { kind: "promise", inner: DYN };
srcT = null;
} else {
srcT = lowered.out;
}
continue;
}
// compose's head and tail functions arrive as Duplex.from streams
const fn = preLowered?.has(j) ? null : pipelineStageFunction(lowerer, arg);
const syncSourceFn =
@@ -4423,7 +5000,9 @@ function lowerPipelineStages(
!preLowered?.has(j) &&
(syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null))
) {
const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc);
const source =
(syncSourceFn ? undefined : plan?.preSource) ??
lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc);
if (source === null) lowerer.noLowering("pipeline over this source", arg);
positions += 1;
argOrder.push({ list: stages, index: stages.length });
@@ -4448,7 +5027,12 @@ function lowerPipelineStages(
}
let stream: IrExpr;
try {
stream = preLowered?.get(j) ?? lowerStreamArg(lowerer, arg, "pipeline");
const bound = plan?.bound.get(j);
stream =
preLowered?.get(j) ??
(bound !== undefined
? streamArgOf(lowerer, arg, bound, "pipeline")
: lowerStreamArg(lowerer, arg, "pipeline"));
} catch (e) {
if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg);
throw e;
@@ -4527,6 +5111,8 @@ function lowerPipelineStages(
);
}
helperArgs.push(pipelineBoxHelper(lowerer, destPromise!, loc));
} else if (dest === 3) {
helperArgs.push(dynStageIterHelper(lowerer, loc));
}
const argDecls: IrStmt[] = [];
const firstStage = argOrder.findIndex((a) => a.list === stages);
@@ -4538,6 +5124,15 @@ function lowerPipelineStages(
a.list[a.index] = varRef(local.id, init.type, init.loc);
}
}
if (handoffCell !== null) {
const cell: IrLocal = handoffCell;
argDecls.unshift({
kind: "varDecl",
localId: cell.id,
init: { kind: "dynObjLit", fields: [], type: DYN, loc },
loc,
});
}
const bindArgs = (call: IrExpr): IrExpr =>
argDecls.length === 0
? call
@@ -120,6 +120,7 @@ import {
deferredReadHelper,
widthNarrowHelper,
} from "./coercions/unions.js";
import { assertedConversion } from "./coercions/asserted.js";
import {
lowerUtilTypeValue,
pathModuleValue,
@@ -2249,6 +2250,10 @@ export class Lowerer {
ts.Type,
{ tsBindings: Map<ts.Symbol, ts.Type>; instance: ts.Type }[]
>();
/** Values an erased type assertion produced, with the asserted type
* (`x as T` keeps x's representation): a slot that no ordinary
* conversion reaches converts them as asserted (assertedConversion). */
readonly assertedValues = new Map<IrExpr, IrType>();
/** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS;
* see generic-functions.ts): bound param symbol →
* the call site's checker type, consulted by typeOf for identifier
@@ -10620,6 +10625,8 @@ export class Lowerer {
if (!typeEquals(expr.type, expected)) {
const w = this.widthCoerce(expr, expected);
if (w) return w;
const asserted = assertedConversion(this, expr, expected);
if (asserted) return asserted;
// A UNIT (null/undefined) flowing into a plain non-nullable slot
// the checker approved (`null!` casts, non-strict assignments):
// the stranded-source stance (divergence 38) without a union in
@@ -10638,7 +10645,7 @@ export class Lowerer {
if (helper) {
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
return expr;
return assertedConversion(this, expr, expected) ?? expr;
}
if (expr.type.kind === "void") {
// A void CALL RESULT flowing into a union with an undefined arm
@@ -2,7 +2,15 @@ import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import { isJsSourceFile, locOf } from "../program.js";
import { restTupleElementTypes, unitOnlyUnion } from "../type-mapper.js";
import { DYN, type IrExpr, type IrLocal, type IrStmt, type IrType, arrayOf } from "../../ir/ir.js";
import {
DYN,
type IrExpr,
type IrLocal,
type IrParam,
type IrStmt,
type IrType,
arrayOf,
} from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { captureContextArguments } from "./function-context.js";
import { isThisParameter, type ParamShape } from "./call-signatures.js";
@@ -156,7 +164,7 @@ export function declareTypedArguments(
lowerer: Lowerer,
node: ts.FunctionExpression,
shapes: readonly ParamShape[],
params: { localId: string; name: string; type: IrType }[],
params: IrParam[],
prologue: IrStmt[],
): void {
const loc = locOf(node);
+34 -12
View File
@@ -2535,24 +2535,36 @@ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifi
/** How a use of a createRequire loader lowers:
* - "static": a literal call or a literal resolution query, which erases
* the loader value.
* - "deferred": a call with a computed specifier or another argument shape,
* or `resolve` / `resolve.paths` read as a value, inside the try block
* of a try statement in the same function. The call throws a runtime
* fence, and the resolve value is a function that throws one when called
* (module-resolution.ts), so the statement's catch clause handles them
* (packages that look for optional native addons).
* - "computed": the same shapes outside a try block.
* - "runtime": in JavaScript source, a call with a computed specifier, a
* require.resolve call with a computed request, or `resolve` read as a
* value. The runtime resolves the specifier like Node
* (module-resolution.ts, scr_require.c).
* - "deferred": in JavaScript source, inside the try block of a try
* statement in the same function, the other shapes: spread arguments,
* require.resolve with another argument count, and `resolve.paths` with
* a computed request or read as a value. The call throws a runtime
* fence, and the resolve.paths value is a function that throws one when
* called (module-resolution.ts), so the statement's catch clause handles
* them (packages that look for optional native addons).
* - "computed": the "runtime" uses in TypeScript source, and the
* "deferred" shapes outside a try block.
* - "escaping": the loader itself as a value, writes, optional calls, and
* other members. */
const COMPUTED_CREATE_REQUIRE_REASON =
"its createRequire result is used with a computed specifier, or its resolve as a value, outside a try block (static requires need literal specifiers)";
"its createRequire result is used with a computed specifier in TypeScript, or with spread arguments or resolve.paths outside a try block (static requires need literal specifiers)";
type CreateRequireUse7 = "static" | "deferred" | "computed" | "escaping";
type CreateRequireUse7 = "static" | "runtime" | "deferred" | "computed" | "escaping";
function createRequireUse7(expr: ts.Expression): CreateRequireUse7 {
const use = createRequireUseShape7(expr);
return use === "runtime" && !isJsSourceFile(ts.getSourceFileOfNode(expr)) ? "computed" : use;
}
function createRequireUseShape7(expr: ts.Expression): CreateRequireUse7 {
let use: ts.Node = expr;
while (ts.isParenthesizedExpression(use.parent)) use = use.parent;
let member = false;
let paths = false;
if (
ts.isPropertyAccessExpression(use.parent) &&
use.parent.expression === use &&
@@ -2566,16 +2578,26 @@ function createRequireUse7(expr: ts.Expression): CreateRequireUse7 {
use.parent.expression === use &&
use.parent.name.text === "paths" &&
!use.parent.questionDotToken
)
) {
use = use.parent;
paths = true;
}
}
const call = use.parent;
if (ts.isCallExpression(call) && call.expression === use) {
if (call.questionDotToken) return "escaping";
if (call.arguments.nodes.length === 1 && ts.isStringLiteralLike(call.arguments.nodes[0]!))
return "static";
const args = call.arguments.nodes;
if (args.length === 1 && ts.isStringLiteralLike(args[0]!)) return "static";
if (
!paths &&
!args.some((arg) => ts.isSpreadElement(arg)) &&
!(member && (args.length === 0 || args.length > 2))
)
return "runtime";
} else if (!member || isAssignmentTarget7(use)) {
return "escaping";
} else if (!paths) {
return "runtime";
}
return insideTryBlock7(use) ? "deferred" : "computed";
}
@@ -4,10 +4,14 @@ import { join } from "node:path";
import { afterAll, afterEach, expect, test } from "vitest";
import type { SourceFile } from "./ast-types.js";
import {
prebuiltLibDiagnosticsText,
readPrebuiltLibDiagnostics,
recordPrebuiltLibDiagnostics,
reusedKeptLibDiagnostics,
reusedLibDiagnostics,
setLibDiagnosticsStore,
type LibDiagnosticsStore,
type PrebuiltLibDiagnostics,
} from "./lib-diagnostics.js";
import type { Ts7DiagnosticData } from "./session-schema.generated.js";
import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js";
@@ -24,7 +28,10 @@ const main = join(dir, "main.ts");
const util = join(dir, "\u{1F600}.ts");
const other = join(dir, "�.ts");
afterEach(() => setLibDiagnosticsStore(null));
afterEach(() => {
setLibDiagnosticsStore(null);
recordPrebuiltLibDiagnostics(null);
});
afterAll(() => {
closeSourceParser();
rmSync(dir, { recursive: true, force: true });
@@ -309,3 +316,69 @@ test("a script that uses an answer stored without global names collects and stor
expect(store.entries.size).toBe(1);
expect(JSON.parse([...store.entries.values()][0]!).globals).not.toBeNull();
});
/** The content keys a full check of the program records, with checker release `checker`. */
function recorded(
checker: string,
semantic: Map<string, Ts7DiagnosticData[]>,
): PrebuiltLibDiagnostics {
const keys = new Set<string>();
recordPrebuiltLibDiagnostics({ checker, record: (key) => keys.add(key) });
setLibDiagnosticsStore(memoryStore(), "build-time checker");
reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile);
recordPrebuiltLibDiagnostics(null);
return { checker, keys };
}
test("a prebuilt answer stands in for checking the supplied files in an empty store", () => {
const semantic = new Map([[main, [diagnostic(main, 5)]]]);
const prebuilt = recorded("1.0.0", semantic);
expect(prebuilt.keys.size).toBe(1);
// Another installation: its own store, its own checker identity.
const store = memoryStore();
setLibDiagnosticsStore(store, "installed checker", () => prebuilt);
const program = sessionProgram({ semantic });
expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toEqual({
semantic: [diagnostic(main, 5)],
global: [],
});
expect(program.asked).toEqual([`checked ${main}`]);
// The answer is stored, so the next build reads it from the store.
expect(store.entries.size).toBe(1);
const next = sessionProgram({ semantic });
setLibDiagnosticsStore(store, "installed checker", () => {
throw new Error("the store answers first");
});
reusedLibDiagnostics(next, names, supplied, moduleFile);
expect(next.asked).toEqual([`checked ${main}`]);
});
test("prebuilt answers apply only to the same checker release and file contents", () => {
const semantic = new Map([[main, []]]);
const prebuilt = recorded("1.0.0", semantic);
const other: PrebuiltLibDiagnostics = { checker: "1.0.1", keys: prebuilt.keys };
setLibDiagnosticsStore(memoryStore(), "installed checker", () => other);
const release = sessionProgram({ semantic });
reusedLibDiagnostics(release, names, supplied, moduleFile);
expect(release.asked[0]).toBe("getGlobalDiagnostics");
writeFileSync(fallback, fallbackText + "// edited\n");
setLibDiagnosticsStore(memoryStore(), "installed checker", () => prebuilt);
const edited = sessionProgram({ semantic });
reusedLibDiagnostics(edited, names, supplied, moduleFile);
expect(edited.asked[0]).toBe("getGlobalDiagnostics");
writeFileSync(fallback, fallbackText);
});
test("a full check that reports diagnostics in a supplied file records no prebuilt answer", () => {
const prebuilt = recorded("1.0.0", new Map([[lib, [diagnostic(lib, 1)]]]));
expect(prebuilt.keys.size).toBe(0);
});
test("prebuilt answer files round-trip, and a malformed one is no answer", () => {
const path = join(dir, "lib-diagnostics.json");
writeFileSync(path, prebuiltLibDiagnosticsText("1.0.0", ["b", "a", "b"]));
expect(readPrebuiltLibDiagnostics(path)).toEqual({ checker: "1.0.0", keys: new Set(["a", "b"]) });
writeFileSync(path, '{ "schema": "other", "checker": "1.0.0", "keys": [] }');
expect(readPrebuiltLibDiagnostics(path)).toBeNull();
expect(readPrebuiltLibDiagnostics(join(dir, "missing.json"))).toBeNull();
});
@@ -17,9 +17,17 @@
* which a program of modules does not otherwise read (about 4 MB at the
* peak of a small build). An answer stored by such a program leaves them
* out; the first program with scripts that uses the answer collects them
* and stores them with it. */
* and stores them with it.
*
* A native distribution also ships prebuilt answers: the content keys of
* the supplied file sets the checker reports no diagnostics in, recorded when
* the distribution was built. A build whose store has no answer uses a
* prebuilt one when the checker is the distribution's own and the files'
* content key is among them, and stores it, so builds with an empty cache
* skip the check of the supplied files too. */
import { createHash } from "node:crypto";
import { statSync } from "node:fs";
import { readFileSync, statSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import type { Node, SourceFile } from "./ast-types.js";
import type { Ts7DiagnosticData } from "./session-schema.generated.js";
import type { Ts7SessionProgram } from "./session.js";
@@ -32,15 +40,108 @@ export interface LibDiagnosticsStore {
write(key: string, text: string): void;
}
const SCHEMA = "scriptc.lib-diagnostics.v2";
/** The answers a distribution ships: the content keys (contentKey) of the
* supplied file sets whose full check reported no diagnostics with the
* checker release `checker`. */
export interface PrebuiltLibDiagnostics {
checker: string;
keys: ReadonlySet<string>;
}
let configured: { store: LibDiagnosticsStore; checker: string } | null = null;
const SCHEMA = "scriptc.lib-diagnostics.v2";
const PREBUILT_SCHEMA = "scriptc.lib-diagnostics-prebuilt.v1";
let configured: {
store: LibDiagnosticsStore;
checker: string;
/** Read on the first store miss; null when there are none. */
prebuilt: (() => PrebuiltLibDiagnostics | null) | null;
} | null = null;
/** Has later builds of this process reuse the diagnostics of the supplied
* declaration files through `store`, for the checker identified by
* `checker` (its executable's identity), or stop reusing them (null). */
export function setLibDiagnosticsStore(store: LibDiagnosticsStore | null, checker = ""): void {
configured = store === null ? null : { store, checker };
* `checker` (its executable's identity), or stop reusing them (null).
* `prebuilt` reads the answers the distribution ships for that checker. */
export function setLibDiagnosticsStore(
store: LibDiagnosticsStore | null,
checker = "",
prebuilt: (() => PrebuiltLibDiagnostics | null) | null = null,
): void {
if (store === null) {
configured = null;
return;
}
let loaded: PrebuiltLibDiagnostics | null | undefined;
configured = {
store,
checker,
prebuilt:
prebuilt === null ? null : () => (loaded === undefined ? (loaded = prebuilt()) : loaded),
};
}
let prebuiltRecorder: { checker: string; record: (key: string) => void } | null = null;
/** Has later full checks that report no diagnostics in the supplied files
* pass their content key (contentKey, for the checker release `checker`) to
* `record`, or stop (null). A distribution build collects its prebuilt
* answers this way. */
export function recordPrebuiltLibDiagnostics(
recorder: { checker: string; record: (key: string) => void } | null,
): void {
prebuiltRecorder = recorder;
}
/** Reads a prebuilt answers file (`{ schema, checker, keys }`); null when
* it is missing or malformed. */
export function readPrebuiltLibDiagnostics(path: string): PrebuiltLibDiagnostics | null {
try {
const value = JSON.parse(readFileSync(path, "utf8")) as {
schema?: unknown;
checker?: unknown;
keys?: unknown;
};
if (
value.schema !== PREBUILT_SCHEMA ||
typeof value.checker !== "string" ||
!Array.isArray(value.keys) ||
!value.keys.every((key) => typeof key === "string")
)
return null;
return { checker: value.checker, keys: new Set(value.keys as string[]) };
} catch {
return null;
}
}
/** A distribution's prebuilt answers at `path`, for its tsc-ts at `checker`
* (`<package>/lib/tsc-ts`): null unless their checker release is the
* version of the package the executable belongs to. */
export function distributionPrebuiltLibDiagnostics(
path: string,
checker: string,
): PrebuiltLibDiagnostics | null {
const prebuilt = readPrebuiltLibDiagnostics(path);
return prebuilt !== null && checkerPackageVersion(checker) === prebuilt.checker ? prebuilt : null;
}
/** The version of the tsc-ts package whose executable is `checker`
* (`<package>/lib/tsc-ts`), or null when it cannot be read. */
export function checkerPackageVersion(checker: string): string | null {
try {
const pkg = JSON.parse(
readFileSync(join(dirname(dirname(checker)), "package.json"), "utf8"),
) as { version?: unknown };
return typeof pkg.version === "string" ? pkg.version : null;
} catch {
return null;
}
}
/** The text of a prebuilt answers file holding `keys` for the checker
* release `checker`. */
export function prebuiltLibDiagnosticsText(checker: string, keys: Iterable<string>): string {
return `${JSON.stringify({ schema: PREBUILT_SCHEMA, checker, keys: [...new Set(keys)].sort() }, null, 1)}\n`;
}
/** A checker executable's identity for the stored diagnostics' key: its
@@ -120,7 +221,7 @@ function reuse(
const options = { ...program.getCompilerOptions(), configFilePath: undefined };
const key = entryKey(checker, options, libs);
if (key === null) return null;
const stored = readEntry(store, key, libs);
const stored = readEntry(store, key, libs) ?? prebuiltEntry(store, key, options, libs);
// An answer stored without the supplied files' global names: a program
// with scripts collects them and stores them with the answer.
let globals = stored?.globals ?? null;
@@ -152,6 +253,10 @@ function reuse(
const files: Record<string, Ts7DiagnosticData[]> = {};
for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name);
writeEntry(store, key, names, files);
if (prebuiltRecorder !== null && Object.values(files).every((list) => list.length === 0)) {
const content = contentKey(prebuiltRecorder.checker, options, libs);
if (content !== null) prebuiltRecorder.record(content);
}
}
}
return { semantic, global };
@@ -330,6 +435,42 @@ function entryKey(checker: string, options: unknown, libs: readonly string[]): s
.digest("hex");
}
/** The supplied files' answer from the distribution's prebuilt answers,
* stored under `key` for later builds: every file without diagnostics, when
* they hold the files' content key; null otherwise. */
function prebuiltEntry(
store: LibDiagnosticsStore,
key: string,
options: unknown,
libs: readonly string[],
): { files: Map<string, Ts7DiagnosticData[]>; globals: Set<string> | null } | null {
const prebuilt = configured?.prebuilt?.() ?? null;
if (prebuilt === null || prebuilt.keys.size === 0) return null;
const content = contentKey(prebuilt.checker, options, libs);
if (content === null || !prebuilt.keys.has(content)) return null;
const files: Record<string, Ts7DiagnosticData[]> = {};
for (const name of libs) files[name] = [];
writeEntry(store, key, null, files);
return { files: new Map(Object.entries(files)), globals: null };
}
/** A supplied file set's key independent of where the files are installed:
* the checker release, the compiler options and each file's name and
* content digest, in program order; null when a file cannot be read. */
function contentKey(checker: string, options: unknown, libs: readonly string[]): string | null {
const files: [string, string][] = [];
for (const name of libs) {
try {
files.push([basename(name), createHash("sha256").update(readFileSync(name)).digest("hex")]);
} catch {
return null;
}
}
return createHash("sha256")
.update(JSON.stringify({ schema: PREBUILT_SCHEMA, checker, options, files }))
.digest("hex");
}
function readEntry(
store: LibDiagnosticsStore,
key: string,
@@ -5297,10 +5297,12 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
// A parameter a TypeScript program types `any` takes whatever the
// caller passes: the checked-dynamic slot, as an `any`-typed binding
// already is (dynFallbackType). Declaration files keep their paths.
// Only function declarations, spelled function types and lambda
// parameters whose default is the only source of their type (`(n =
// lookup() ?? 5) => ...` with an `any` lookup): other lambdas and
// methods in object literals keep their existing checked-dynamic story.
// Only function declarations, spelled function types, method and call
// signatures of interfaces and type literals (`execute(params: any):
// Promise<R>`), and lambda parameters whose default is the only source
// of their type (`(n = lookup() ?? 5) => ...` with an `any` lookup):
// other lambdas and methods in object literals keep their existing
// checked-dynamic story.
if (
!pt &&
!rest &&
@@ -5309,6 +5311,8 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
decl.parent !== undefined &&
(ts.isFunctionDeclaration(decl.parent) ||
ts.isFunctionTypeNode(decl.parent) ||
ts.isMethodSignatureDeclaration(decl.parent) ||
ts.isCallSignatureDeclaration(decl.parent) ||
((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) &&
decl.initializer !== undefined &&
decl.type === undefined &&
+1
View File
@@ -654,6 +654,7 @@ async function compileExecutableNative(
...(features.utilTiming ? { utilTiming: true } : {}),
...(features.dynBuffer ? { dynBuffer: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
...(features.computedRequire ? { computedRequire: true } : {}),
events: features.events,
emitter: features.emitter,
symbol: features.symbol,
@@ -92,6 +92,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.stdioStream",
"stream.unboxDyn",
"stream.dynIterableSource",
"stream.dynSourceValue",
"stream.unboxDynStage",
"stream.dynTransformResult",
"stream.handleStream",
"stream.pipeDyn",
"stream.unpipeDyn",
@@ -703,6 +706,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.builtinId",
"process.builtinModule",
"process.builtinUnsupported",
"module.requireComputed",
"module.resolveComputed",
"fs.callbackValue",
"fs.callbackCall",
"dyn.keyDelete",
+37 -5
View File
@@ -385,6 +385,18 @@ function buildLibFnSignatures() {
"module.parent": { argTypes: [F64], result: F64 },
"module.loaded": { argTypes: [F64], result: BOOL },
"module.cacheGet": { argTypes: [STRING], result: F64 },
/** createRequire's require(id) and require.resolve(request, options)
* with a specifier computed at run time (scr_require.c): the arguments,
* then the requiring file, the call's "file:line" and the builtin ids.
* Both resolve like Node at run time. requireComputed always throws
* (Node's argument errors or MODULE_NOT_FOUND), or ends the program with
* an SC2020 refusal when the specifier resolves; resolveComputed answers
* the resolved id or real path. */
"module.requireComputed": { argTypes: [DYN, STRING, STRING, arrayOf(STRING)], result: DYN },
"module.resolveComputed": {
argTypes: [DYN, DYN, STRING, STRING, arrayOf(STRING)],
result: STRING,
},
"module.cacheHas": { argTypes: [STRING], result: BOOL },
"module.cacheKeys": { argTypes: [], result: arrayOf(STRING) },
"timers.setTimeout": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: VOID },
@@ -2870,9 +2882,27 @@ function buildLibFnSignatures() {
result: { kind: "object", className: "%Duplex" },
},
/** Whether pipeline() pumps a checked-dynamic first stage as an
* iterable (a string, or a value with an iterator method that is not a
* stream with pipe() and on()) instead of piping it as a stream. */
* iterable (a string, a value with an iterator method that is not a
* stream with pipe() and on(), a { readable, writable } pair, or a
* source function's result) instead of piping it as a stream. */
"stream.dynIterableSource": { argTypes: [DYN], result: BOOL },
/** The value pipeline() pumps for a checked-dynamic first stage: a
* source function's result (the flag set), which must be iterable, else
* a { readable, writable } pair's readable side or the value itself. */
"stream.dynSourceValue": { argTypes: [DYN, BOOL], result: DYN },
/** An untyped pipeline function stage's result before the last stage,
* which must be async iterable: ERR_INVALID_RETURN_VALUE naming the
* stage ("transform[i]") otherwise. Answers the value (+1). */
"stream.dynTransformResult": { argTypes: [DYN, STRING], result: DYN },
/** A checked-dynamic pipeline() stage taken as a stream: a native stream
* (+1), or Duplex.from of a value that is not stream-like or a function
* (an iterable), which the pipeline uses without piping into it. Other
* values throw: Duplex.from's ERR_INVALID_ARG_TYPE, or the given
* not-supported message. */
"stream.unboxDynStage": {
argTypes: [DYN, STRING],
result: { kind: "object", className: "%Duplex" },
},
/** The Transform a Cipheriv or Decipheriv reads and writes as a stream
* (+1, created on first use and owned by the cipher). */
"stream.handleStream": {
@@ -5246,10 +5276,12 @@ export type IrLibFn = keyof ReturnType<typeof buildLibFnSignatures>;
/** The function-stage runs of a pipeline of `total` stages that a stream
* follows (positions bit j: stage j is a function), and an async
* generator destination's run (flags bits 1-2 = 1, pumped into the
* PassThrough pipeline() answers): each pumps its last generator through
* one (next, read, return) triple. */
* PassThrough pipeline() answers, or 3, an untyped destination whose
* result may be pumped there): each pumps its last generator through one
* (next, read, return) triple. */
export function pipelinePumpedRuns(total: number, positions: number, flags: number): number {
let runs = ((flags >> 1) & 3) === 1 ? 1 : 0;
const dest = (flags >> 1) & 3;
let runs = dest === 1 || dest === 3 ? 1 : 0;
for (let j = 0; j + 1 < total; j++) {
if (Math.floor(positions / 2 ** j) % 2 === 1 && Math.floor(positions / 2 ** (j + 1)) % 2 === 0)
runs++;
+66 -26
View File
@@ -1,6 +1,7 @@
import { isBorrowSafeMath } from "./analysis.js";
import type { IrExpr, IrFunction, IrLibFn, IrStmt } from "./ir.js";
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
import { everyExprChild, everyStmtChild } from "./traverse.js";
/** Expression kinds that run no user code of their own and never consult
* the ambient receiver stack. Calls are admitted here and resolved through
@@ -151,6 +152,7 @@ function opaqueOperand(e: IrExpr): boolean {
* this set needs no receiver bracket. */
export class AmbientReceiverReaders {
private readers: Set<string> | null = null;
private byMethod: Map<string, string[]> | null = null;
constructor(
private readonly functions: ReadonlyMap<string, IrFunction>,
@@ -163,8 +165,63 @@ export class AmbientReceiverReaders {
return !this.compute().has(fn.name);
}
private compute(): Set<string> {
if (this.readers) return this.readers;
/** A signature adapter body that observes the ambient receiver only by
* forwarding it, unchanged, to the original it captures (a callValue on
* that capture whose receiver is the plain `this` read). Such an adapter
* ignores the receiver exactly when its original does. */
forwardsOnly(fn: IrFunction): boolean {
if (fn.async || fn.generator) return false;
const original = fn.captures?.[0]?.localId;
if (original === undefined) return false;
const readers = this.compute();
const edge = (name: string): boolean => this.functions.has(name) && !readers.has(name);
const stmt = (node: IrStmt): boolean =>
RECEIVER_FREE_STMTS.has(node.kind) && everyStmtChild(node, expr, stmt);
const expr = (node: IrExpr): boolean => {
if (node.kind === "callValue" && node.receiver !== undefined) {
const receiver = node.receiver;
return (
receiver.kind === "libCall" &&
receiver.fn === "dyn.this" &&
receiver.args.length === 0 &&
node.callee.kind === "varRef" &&
node.callee.localId === original &&
node.args.every(expr)
);
}
return this.admits(node, edge) && everyExprChild(node, expr, stmt);
};
return fn.body.every(stmt);
}
/** Whether one expression node, apart from its children, leaves the
* ambient receiver alone; `edge` admits a direct callee. */
private admits(expr: IrExpr, edge: (name: string) => boolean): boolean {
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
switch (expr.kind) {
case "call":
return edge(expr.callee);
case "new":
return edge(`%${expr.className}.constructor`);
case "virtualCall": {
// Any class's override may run: admit only when every
// same-named method is itself receiver-free.
const targets = this.methods().get(expr.method) ?? [];
return targets.length > 0 && targets.every(edge);
}
case "libCall":
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
case "toString":
return !opaqueOperand(expr.operand);
default: {
const operands = dataIntrinsicOperands(expr);
return operands === null || !operands.some(opaqueOperand);
}
}
}
private methods(): Map<string, string[]> {
if (this.byMethod) return this.byMethod;
const byMethod = new Map<string, string[]>();
for (const name of this.functions.keys()) {
const dot = name.lastIndexOf(".");
@@ -174,6 +231,12 @@ export class AmbientReceiverReaders {
list.push(name);
byMethod.set(method, list);
}
this.byMethod = byMethod;
return byMethod;
}
private compute(): Set<string> {
if (this.readers) return this.readers;
const readers = new Set<string>();
const callers = new Map<string, string[]>();
const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE);
@@ -185,34 +248,11 @@ export class AmbientReceiverReaders {
callees.add(name);
return true;
};
const expr = (expr: IrExpr): boolean => {
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
switch (expr.kind) {
case "call":
return edge(expr.callee);
case "new":
return edge(`%${expr.className}.constructor`);
case "virtualCall": {
// Any class's override may run: admit only when every
// same-named method is itself receiver-free.
const targets = byMethod.get(expr.method) ?? [];
return targets.length > 0 && targets.every(edge);
}
case "libCall":
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
case "toString":
return !opaqueOperand(expr.operand);
default: {
const operands = dataIntrinsicOperands(expr);
return operands === null || !operands.some(opaqueOperand);
}
}
};
if (!reads)
eachNodeOfKinds(fn, scan, (node, statement) => {
const free = statement
? RECEIVER_FREE_STMTS.has((node as IrStmt).kind)
: expr(node as IrExpr);
: this.admits(node as IrExpr, edge);
if (!free) reads = true;
return free;
});
@@ -278,6 +278,9 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.stdioWritable",
"stream.unboxDyn",
"stream.dynIterableSource",
"stream.dynSourceValue",
"stream.dynTransformResult",
"stream.unboxDynStage",
"stream.handleStream",
"stream.unpipeDyn",
"stream.workerStdio",
@@ -314,6 +317,7 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.duplexFromAsyncFn",
"stream.duplexFromGenSignal",
"stream.duplexFromAsyncFnSignal",
"stream.unboxDynStage",
]);
/** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */
@@ -973,6 +977,9 @@ const RUNTIME_FEATURE_RULES = {
fns: ["buffer.prototypeMember"],
expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method),
}),
/** createRequire's require and require.resolve with specifiers computed
* at run time (scr_require.c). */
computedRequire: rule({ fns: ["module.requireComputed", "module.resolveComputed"] }),
/** Builtin functions read as values (scr_builtin_values.c). */
builtinValues: rule({
fns: [
@@ -1264,6 +1271,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
dynForAwait: false,
dynBuffer: false,
builtinValues: false,
computedRequire: false,
symbol: false,
tls: false,
tlsCa: false,
+3 -3
View File
@@ -5761,7 +5761,7 @@ function validateFunction(
}
const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1;
const helperCount = 3 * pipelinePumpedRuns(total, positions!, flags!);
const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0);
const groups = helperCount + (destKind >= 2 ? 1 : 0) + (flags! & 32 ? 2 : 0);
const cbArgs =
e.fn === "sp.pipelineStagesOpts"
? 1 + (flags! & 64 ? 1 : 0)
@@ -5776,7 +5776,7 @@ function validateFunction(
total > 52 ||
positions! >= 2 ** total ||
functions !== k ||
destKind === 3 ||
(destKind === 3 && e.fn === "stream.composeStages") ||
(destKind !== 0) !== lastIsFunction ||
total < 2 ||
e.args.length !== 5 + n! + k! + groups + cbArgs
@@ -5795,7 +5795,7 @@ function validateFunction(
err(`libCall ${e.fn} stage function ${i} must be a func`, e.loc);
}
at2 += k!;
const helpers = helperCount + (destKind === 2 ? 1 : 0);
const helpers = helperCount + (destKind >= 2 ? 1 : 0);
for (let i = at2; i < at2 + helpers; i++) {
if (e.args[i]?.type.kind !== "func")
err(`libCall ${e.fn} helper ${i} must be a func`, e.loc);
+20 -1
View File
@@ -1,5 +1,5 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { appendFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCli } from "../cli/command.js";
@@ -19,6 +19,9 @@ import { NativeCompiler } from "./compiler.js";
import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js";
import {
checkerExecutableIdentity,
checkerPackageVersion,
distributionPrebuiltLibDiagnostics,
recordPrebuiltLibDiagnostics,
setLibDiagnosticsStore,
} from "../frontend/ts7/lib-diagnostics.js";
import { setCheckerFeatureStore } from "../frontend/ts7/checker-features.js";
@@ -67,7 +70,23 @@ async function main(): Promise<number> {
write: (key, text) => store.write("lib-diagnostics", key, text),
},
checkerExecutableIdentity(toolchain.ts7Executable),
toolchain.prebuiltLibDiagnostics === undefined
? null
: () =>
distributionPrebuiltLibDiagnostics(
toolchain.prebuiltLibDiagnostics!,
toolchain.ts7Executable,
),
);
// A distribution build records its prebuilt lib diagnostics (build-native-cli.mts).
const recordPath = process.env["SCRIPTC_LIB_DIAGNOSTICS_RECORD"];
const checkerVersion =
recordPath === undefined ? null : checkerPackageVersion(toolchain.ts7Executable);
if (recordPath !== undefined && checkerVersion !== null)
recordPrebuiltLibDiagnostics({
checker: checkerVersion,
record: (key) => appendFileSync(recordPath, `${key}\n`),
});
setCheckerFeatureStore({
read: (key) =>
store.read("checker-features", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null,
@@ -87,6 +87,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
utilTiming: false,
dynBuffer: false,
builtinValues: false,
computedRequire: false,
warningReplace: false,
stdioExtended: false,
brotli: false,
@@ -54,6 +54,40 @@ test("native toolchain installation paths relocate with the manifest", () => {
}
});
test("prebuilt lib diagnostics apply while the checker is the distribution's tsc-ts", () => {
const root = realpathSync(
mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-tools-")),
);
const path = join(root, "toolchain.json");
const manifest: NativeToolchainManifest = {
schema: "scriptc.native-toolchain.v1",
compiler_version: "1.2.3",
target: "macos-arm64",
ts7: "typescript/tsc",
tsc_ts: "tsc-ts/lib/tsc-ts",
llvm_package: "llvm",
runtime_pack: "runtime",
linker: "tools/linker",
linker_args: [],
dsymutil: "dsymutil",
lib_diagnostics: "declarations/lib-diagnostics.json",
};
try {
writeFileSync(path, JSON.stringify(manifest));
expect(loadNativeToolchain(path).prebuiltLibDiagnostics).toBe(
join(root, "declarations/lib-diagnostics.json"),
);
// tsgo checks without them.
expect(
loadNativeToolchain(path, { SCRIPTC_CHECKER: "tsgo" }).prebuiltLibDiagnostics,
).toBeUndefined();
writeFileSync(path, JSON.stringify({ ...manifest, lib_diagnostics: "" }));
expect(() => loadNativeToolchain(path)).toThrow();
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("native toolchain assets resolve from the physical manifest through package links", () => {
const root = realpathSync(
mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-linked-tools-")),
+12
View File
@@ -30,6 +30,9 @@ export interface NativeToolchain {
declarationsRoot?: string;
comptimeExecutable?: string;
wasiNodeRunner?: string;
/** The prebuilt lib diagnostics the distribution ships, present while the
* checker is the distribution's own tsc-ts (lib-diagnostics.ts). */
prebuiltLibDiagnostics?: string;
}
/** Distribution paths are relative to this manifest, so moving an installed
@@ -54,6 +57,8 @@ export interface NativeToolchainManifest {
relocatable_linker?: string;
comptime?: string;
wasi_node_runner?: string;
/** Answers for the supplied declaration files, computed with `tsc_ts`. */
lib_diagnostics?: string;
}
function pathFrom(root: string, value: string): string {
@@ -120,6 +125,7 @@ export function loadNativeToolchain(
"relocatable_linker",
"comptime",
"wasi_node_runner",
"lib_diagnostics",
]) {
if (value[name] !== undefined && (typeof value[name] !== "string" || value[name] === ""))
throw new Error(`native toolchain requires a nonempty string for ${name}`);
@@ -221,5 +227,11 @@ export function loadNativeToolchain(
...(manifest.wasi_node_runner === undefined
? {}
: { wasiNodeRunner: pathFrom(root, manifest.wasi_node_runner) }),
// The prebuilt answers are the distribution's tsc-ts's.
...(manifest.lib_diagnostics === undefined ||
manifest.tsc_ts === undefined ||
checkerBackend(env) === "tsgo"
? {}
: { prebuiltLibDiagnostics: pathFrom(root, manifest.lib_diagnostics) }),
};
}
@@ -40,9 +40,11 @@ try {
const module = deserializeModule(readFileSync(output, "utf8"));
expect(validateModule(module)).toEqual([]);
const llvm = emitLlvmModule(module, {});
// The fallback calls the closure's entry, after giving it an undefined
// receiver when it reads one, so its edge comes from receiver.done then.
const guards = [
...llvm.matchAll(
/(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %callback\.indirect\d+ \]/g,
/(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %(?:callback\.indirect|receiver\.done)\d+ \]/g,
),
];
// The quiet every and map callbacks; not the throwing one.
@@ -95,7 +95,8 @@ test("a Promise.all tuple adapter requested in a range is defined", async () =>
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const ir = await readFile(outPath, "utf8");
const serial = text(emitLlvmModuleSource(deserializeModule(ir), {}));
expect(serial).toContain("define internal void @sc_all_tuple_0(");
// The adapter is named from its key (contentSymbol).
expect(serial).toMatch(/define internal void @sc_all_tuple_\d+\(/);
const chunked = text(emitLlvmModuleSource(deserializeModule(ir), { functionChunks: 2 }));
expect(chunked === serial).toBe(true);
} finally {
@@ -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_v45");
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v46");
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_v45");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v46");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7508-pipeline-untyped-source-functions.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7509-pipeline-untyped-function-stages.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7510-pipeline-untyped-later-iterables.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7511-pipeline-untyped-stages.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7512-pipeline-any-stages.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7513-pipeline-untyped-stage-errors.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8210-require-computed-missing/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8211-require-resolve-computed/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8212-native-helper-probe-no-display/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8230-function-record-dispatch.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8231-function-table-shapes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8232-optional-function-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/checked-union-record-assertions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/generic-interface-void-calls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/generic-member-lambda-impls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/method-signature-any-params.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/union-clone-empty-array-overrides.ts"
],
"diags": []
}
@@ -366,7 +366,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target,
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
compiler: {
command: compiler,
identity: compilerVersion,
@@ -68,6 +68,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_bytes_dyn.c", all("dynBuffer", "dynInvoke")],
// Builtin functions read as values in JavaScript dependencies.
["scr_builtin_values.c", "builtinValues"],
// createRequire's computed require and require.resolve.
["scr_require.c", "computedRequire"],
["scr_dc.c", "dc"],
// The dynamic tier's jsval values (two-tier design I3): linked only into
// programs whose IR has dynamic-tier operations.
@@ -469,7 +469,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: matrix.target,
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
compiler: {
command: compiler,
identity: compilerVersion,
+8 -1
View File
@@ -98,7 +98,7 @@ typedef struct ScrClosure {
* (like the dyn→closure edge): a cycle through it is merely never
* collected. */
ScrBox *props;
uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0] */
uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0]; bit 4: SCR_FN_RECEIVER_FREE */
/* The root's `length` as boxed values report it; meaningful only when
* identity is set (the root's own boxes carry their arity). */
uint32_t identity_length;
@@ -117,6 +117,13 @@ typedef struct ScrClosure {
ScrBox *caps[];
} ScrClosure;
/* function_kind bit 4: the body never observes the ambient receiver (the
* `this` an unbound call binds), so compiled callers skip the receiver
* bracket around an indirect call. Set only by the compiler; the kind
* proper is function_kind & SCR_FN_KIND_MASK. */
#define SCR_FN_RECEIVER_FREE 16u
#define SCR_FN_KIND_MASK 15u
ScrClosure *scr_closure_new(void *fn, size_t ncaps); /* +1; caller fills caps with +1 box refs */
static inline ScrClosure *scr_closure_retain(ScrClosure *c) {
+1 -1
View File
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
void scr_runtime_abi_v45(void) {}
void scr_runtime_abi_v46(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
+1 -1
View File
@@ -81,7 +81,7 @@ void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */
void scr_runtime_abi_v45(void);
void scr_runtime_abi_v46(void);
void scr_runtime_workers_v8(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────

Some files were not shown because too many files have changed in this diff Show More