From 321873b9576c9692d9193c2193b18a7da00d4ccc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:18:36 -0500 Subject: [PATCH 01/12] Compile JavaScript-path calls that Node runs --- .../builtin-apis/streams-and-readline.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/frontend/lowering/builtins/atomics.ts | 16 +++-- .../src/frontend/lowering/builtins/numbers.ts | 18 ++--- .../frontend/lowering/builtins/readline.ts | 34 +++++++--- .../lowering/containers/string-and-regexp.ts | 36 ++++++++-- packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 3 + .../6862-readline-chained-listeners.cjs.json | 6 ++ ...6865-number-parseint-default-radix.ts.json | 6 ++ .../6866-number-parseint-untyped.js.json | 6 ++ .../6867-string-match-optional-regexp.js.json | 6 ++ ...-buffer-from-untyped-array-buffer.cjs.json | 6 ++ .../6869-atomics-untyped-arrays.cjs.json | 6 ++ packages/runtime/src/scr_bytes_io.c | 13 ++++ packages/runtime/src/scr_lib.h | 3 + packages/runtime/src/scr_shared.c | 68 +++++++++++++++++++ .../6862-readline-chained-listeners.cjs | 29 ++++++++ .../6865-number-parseint-default-radix.ts | 16 +++++ tests/corpus/6866-number-parseint-untyped.js | 9 +++ .../6867-string-match-optional-regexp.js | 20 ++++++ .../6868-buffer-from-untyped-array-buffer.cjs | 38 +++++++++++ tests/corpus/6869-atomics-untyped-arrays.cjs | 65 ++++++++++++++++++ .../6862-readline-chained-listeners.cjs.json | 1 + ...6865-number-parseint-default-radix.ts.json | 1 + .../6866-number-parseint-untyped.js.json | 1 + .../6867-string-match-optional-regexp.js.json | 1 + ...-buffer-from-untyped-array-buffer.cjs.json | 1 + .../6869-atomics-untyped-arrays.cjs.json | 1 + 29 files changed, 388 insertions(+), 29 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json create mode 100644 tests/corpus/6862-readline-chained-listeners.cjs create mode 100644 tests/corpus/6865-number-parseint-default-radix.ts create mode 100644 tests/corpus/6866-number-parseint-untyped.js create mode 100644 tests/corpus/6867-string-match-optional-regexp.js create mode 100644 tests/corpus/6868-buffer-from-untyped-array-buffer.cjs create mode 100644 tests/corpus/6869-atomics-untyped-arrays.cjs create mode 100644 tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json create mode 100644 tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json create mode 100644 tests/harness/corpus-costs/6866-number-parseint-untyped.js.json create mode 100644 tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json create mode 100644 tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json create mode 100644 tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index a21f7a617..046f7d6b5 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -19,6 +19,7 @@ Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. + `on`, `once`, and `addListener` return the interface, so registrations chain. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output. The output's `'resize'` event does not redraw the line. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aa..683211762 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1037,6 +1037,9 @@ export const LIB_FN_SYMS: Record = { "atomics.op": "scr_atomics_op", "atomics.notify": "scr_atomics_notify", "atomics.wait": "scr_atomics_wait", + "atomics.opDyn": "scr_atomics_op_dyn", + "atomics.waitDyn": "scr_atomics_wait_dyn", + "atomics.notifyDyn": "scr_atomics_notify_dyn", "worker.new": "scr_worker_new", "worker.isMainThread": "scr_context_is_main", "worker.threadId": "scr_context_thread_number", diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts index 41e35a511..5c72ed08c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts +++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts @@ -60,10 +60,14 @@ export function lowerAtomicsCall( // has a concrete typed-array binding. Validate the actual lowered storage. const array = lowerer.lowerExpr(source); const type = array.type; + // An untyped array (a JavaScript `workerData` or field): the runtime + // checks its kind and throws Node's TypeError for any other value. + const dynamic = type?.kind === "dyn"; if ( - type?.kind !== "bytes" || - !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) || - ((member === "wait" || member === "notify") && type.elem !== "i32") + !dynamic && + (type?.kind !== "bytes" || + !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) || + ((member === "wait" || member === "notify") && type.elem !== "i32")) ) { lowerer.noLowering( `Atomics.${member} over this array`, @@ -82,7 +86,7 @@ export function lowerAtomicsCall( if (member === "wait") return { kind: "libCall", - fn: "atomics.wait", + fn: dynamic ? "atomics.waitDyn" : "atomics.wait", args: [array, index, arg(2, 0), arg(3, Infinity)], type: STRING, loc, @@ -90,14 +94,14 @@ export function lowerAtomicsCall( if (member === "notify") return { kind: "libCall", - fn: "atomics.notify", + fn: dynamic ? "atomics.notifyDyn" : "atomics.notify", args: [array, index, arg(2, Infinity)], type: F64, loc, }; return { kind: "libCall", - fn: "atomics.op", + fn: dynamic ? "atomics.opDyn" : "atomics.op", args: [array, index, arg(2, 0), arg(3, 0), number(operation)], type: F64, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 18ca0f1f3..1ff35fbb5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -72,17 +72,14 @@ export function lowerNumberStaticCall( return { kind: "libCall", fn, args: [arg], type: BOOL, loc }; } if (member === "parseFloat" || member === "parseInt") { - const want = member === "parseFloat" ? 1 : 2; - if (call.arguments.nodes.length !== want) { - lowerer.noLowering( - `Number.${member} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, - call, - member === "parseInt" ? "pass an explicit radix: Number.parseInt(s, 10)" : undefined, - ); + const count = call.arguments.nodes.length; + if (count < 1 || count > (member === "parseFloat" ? 1 : 2)) { + lowerer.noLowering(`Number.${member} with ${count} argument${count === 1 ? "" : "s"}`, call); } // Number.parseFloat/Number.parseInt ARE the global parsers: over a // string argument they lower to the same static parse (parseInt with - // its explicit radix); only the coercing argument shapes are refused. + // its radix, or radix 0 when it is missing); only the coercing argument + // shapes are refused. const probed = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (probed !== null && isStringLike(lowerer, probed.type)) { const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); @@ -101,7 +98,10 @@ export function lowerNumberStaticCall( }; if (member === "parseFloat") return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc }; - const radix = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); + const radixNode = call.arguments.nodes[1]; + const radix: IrExpr = radixNode + ? lowerer.lowerExprExpecting(radixNode, F64) + : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } const argText = diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index ede65d788..846b9a926 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -353,7 +353,8 @@ function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; * - on/once/addListener for 'line' ((input: string) or no parameters), * 'error' ((err: Error) or no parameters), 'history' ((history: * string[]) or no parameters), and 'close'/'pause'/'resume'/'SIGINT'/ - * 'SIGTSTP'/'SIGCONT' (no parameters), as statements. + * 'SIGTSTP'/'SIGCONT' (no parameters); as an expression the call yields + * the interface, so registrations chain. * Everything else the lib declares fences member-qualified. Null for * non-Interface receivers. */ export function lowerReadlineMethodCall( @@ -505,14 +506,19 @@ export function lowerReadlineMethodCall( `${RL_EVENT_LIST} are the supported readline events`, ); } - if (!ts.isExpressionStatement(call.parent)) { - lowerer.unsupported( - "SC1090", - call, - "chaining readline listener registration (register each listener as its own statement)", - ); - } - const recv = receiver(); + // As an expression (`rl.on("line", f).on("close", g)`), the call + // registers the listener and yields the interface, as Node's on() does. + const recvLocal = ts.isExpressionStatement(call.parent) + ? undefined + : lowerer.declareHiddenLocal("%readlineListener", F64); + const saved: IrStmt[] = + recvLocal === undefined + ? [] + : [{ kind: "varDecl", localId: recvLocal.id, init: receiver(), loc }]; + const recv: IrExpr = + recvLocal === undefined + ? receiver() + : { kind: "varRef", localId: recvLocal.id, type: F64, loc }; const cb = lowerer.lowerExpr(args[1]!); const want = event === "line" @@ -547,13 +553,21 @@ export function lowerReadlineMethodCall( : `'${event}' listeners with parameters or a return value (use ())`, ); } - return { + const register: IrExpr = { kind: "libCall", fn: "rl.on", args: [recv, numLit(ev, loc), cb, boolLit(name === "once", loc)], type: VOID, loc, }; + if (recvLocal === undefined) return register; + return { + kind: "seqExpr", + stmts: [...saved, { kind: "exprStmt", expr: register, loc }], + result: { kind: "varRef", localId: recvLocal.id, type: F64, loc }, + type: F64, + loc, + }; } lowerer.noLowering( `readline.Interface.${name}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 9567ee0f6..38865093f 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -919,12 +919,12 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence + if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence if (call.arguments.nodes.length !== 1) return null; const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); + const re = lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -982,10 +982,10 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence + if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); - const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); + const re = lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1920,3 +1920,31 @@ export function lowerDynPrimitiveMethodCall( ); return seq(stmts, result); } + +/** Whether the pattern of `match` or `search` is a RegExp, or a RegExp that + * may be undefined (`s.match(patterns[i])` under noUncheckedIndexedAccess). */ +function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + const t = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (t?.kind === "regex") return true; + if (t?.kind !== "union") return false; + const arms = lowerer.unions.get(t.unionId)?.arms ?? []; + return ( + arms.length === 2 && + arms.some((arm) => arm.kind === "regex") && + arms.some((arm) => arm.kind === "undefinedT") + ); +} + +/** The pattern of `match` or `search` as a RegExp: an undefined pattern is + * `new RegExp(undefined)`, the empty pattern, as in Node. */ +function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const regex: IrType = { kind: "regex" }; + const empty: IrExpr = { + kind: "regexLit", + pattern: "(?:)", + flags: "", + type: regex, + loc: locOf(node), + }; + return lowerOptionalArgument(lowerer, node, regex, empty); +} diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c52118..482739db5 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -97,6 +97,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "atomics.op", "atomics.wait", "atomics.notify", + "atomics.opDyn", + "atomics.waitDyn", + "atomics.notifyDyn", "arrayBuffer.byteLengthGetter", "arrayBuffer.viewU8C", "arrayBuffer.viewI8", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa7876579..49683550e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1252,6 +1252,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING }, "atomics.notify": { argTypes: [bytesOf("i32"), F64, F64], result: F64 }, "atomics.op": { argTypes: [null, F64, F64, F64, F64], result: F64 }, + "atomics.opDyn": { argTypes: [DYN, F64, F64, F64, F64], result: F64 }, + "atomics.waitDyn": { argTypes: [DYN, F64, F64, F64], result: STRING }, + "atomics.notifyDyn": { argTypes: [DYN, F64, F64], result: F64 }, "worker.new": { argTypes: [F64, STRING, DYN], result: DYN }, "worker.isMainThread": { argTypes: [], result: BOOL }, "worker.threadId": { argTypes: [], result: F64 }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json new file mode 100644 index 000000000..5beb42eaf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6862-readline-chained-listeners.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json new file mode 100644 index 000000000..225f2c462 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6865-number-parseint-default-radix.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json new file mode 100644 index 000000000..f000b3a36 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6866-number-parseint-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json new file mode 100644 index 000000000..1d1105bab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6867-string-match-optional-regexp.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json new file mode 100644 index 000000000..baa6e1935 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json new file mode 100644 index 000000000..f91624eb9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6869-atomics-untyped-arrays.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 5532d96da..ac8fad02e 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,8 +1299,19 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } +/* Buffer.from(arrayBuffer) over a checked value: a Buffer over all of the + * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. A + * second argument would be the byte offset, and an encoding name converts + * to NaN, which is offset 0. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage) { + ScrBytes *out = scr_bytes_buffer_view(storage->v.handle.ptr, SCR_BYTES_U8, 0, false, 0); + if (out) out->is_buffer = true; + return out; +} + ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1344,6 +1355,8 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding if (value->kind == SCR_DYN_STR) return scr_buffer_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) return scr_buffer_from_bytes(value->v.bytes); + /* A view of an ArrayBuffer shares its memory, so it never comes from the pool. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 1b859cea1..3ce18d793 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -365,6 +365,9 @@ typedef struct ScrBytes ScrBytes; ScrStr *scr_atomics_wait(ScrBytes *, double, double, double); double scr_atomics_notify(ScrBytes *, double, double); double scr_atomics_op(ScrBytes *, double, double, double, double); +double scr_atomics_op_dyn(const ScrDyn *, double, double, double, double); +ScrStr *scr_atomics_wait_dyn(const ScrDyn *, double, double, double); +double scr_atomics_notify_dyn(const ScrDyn *, double, double); /* isatty(3) over an fd (0/1/2): a real boolean — false where Node's * non-TTY streams expose undefined (SEMANTICS.md). stdin_destroy is a diff --git a/packages/runtime/src/scr_shared.c b/packages/runtime/src/scr_shared.c index 2dac5a4d1..6505a71ab 100644 --- a/packages/runtime/src/scr_shared.c +++ b/packages/runtime/src/scr_shared.c @@ -7,7 +7,9 @@ #include "scr_numeric.h" #include #include +#include #include +#include #include #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) #ifdef _WIN32 @@ -360,3 +362,69 @@ double scr_atomics_notify(ScrBytes *bytes, double index, double count) { scr_wait_unlock(bucket); return notified; } + +/* V8's text for a value in the TypeErrors below: a typed array as + * [object Int8Array] and so on, an array as [object Array], an ArrayBuffer + * or other native object as #, an object as #, and + * other values as String() gives them (undefined and null by name). */ +static ScrStr *scr_atomics_value_text(const ScrDyn *value) { + char text[64]; + int n; + if (value->kind == SCR_DYN_BYTES) { + if (value->v.bytes->is_data_view) n = snprintf(text, sizeof text, "#"); + else n = snprintf(text, sizeof text, "[object %s]", scr_bytes_elem_name(value->v.bytes->elem)); + } else if (value->kind == SCR_DYN_ARR) { + n = snprintf(text, sizeof text, "[object Array]"); + } else if (value->kind == SCR_DYN_HANDLE) { + n = snprintf(text, sizeof text, "#<%s>", scr_dyn_handle_ops_of(value)->cls); + } else if (value->kind == SCR_DYN_OBJ) { + const char *name = scr_dyn_class_name(value); + n = snprintf(text, sizeof text, "#<%s>", name ? name : "Object"); + } else if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_name(value)) { + const ScrStr *name = scr_dyn_typed_ref_class_name(value); + n = snprintf(text, sizeof text, "#<%.*s>", (int)name->len, (const char *)name->data); + } else if (value->kind == SCR_DYN_UNDEF) { + n = snprintf(text, sizeof text, "undefined"); + } else if (value->kind == SCR_DYN_NULL) { + n = snprintf(text, sizeof text, "null"); + } else { + return scr_dyn_to_string(value, NULL); + } + return scr_str_new(text, n < (int)sizeof text ? (size_t)n : sizeof text - 1); +} + +/* The integer typed array an untyped Atomics argument holds, or NULL with + * Node's TypeError pending. `waitable` requires an Int32Array, as wait and + * notify do. */ +static ScrBytes *scr_atomics_dyn_array(const ScrDyn *value, bool waitable) { + if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) { + ScrBytesElem elem = value->v.bytes->elem; + bool integer = elem == SCR_BYTES_I8 || elem == SCR_BYTES_U8 || elem == SCR_BYTES_I16 || + elem == SCR_BYTES_U16 || elem == SCR_BYTES_I32 || elem == SCR_BYTES_U32; + if (waitable ? elem == SCR_BYTES_I32 : integer) return value->v.bytes; + } + ScrStr *shown = scr_atomics_value_text(value); + if (!shown) return NULL; + const char *tail = waitable ? " is not an int32 or BigInt64 typed array." : " is not an integer typed array."; + ScrStr *parts[2] = {shown, scr_str_new(tail, strlen(tail))}; + ScrStr *message = scr_str_concat_parts(parts, 2); + scr_str_release(parts[0]); + scr_str_release(parts[1]); + scr_throw_error(SCR_ERR_TYPE, message); + return NULL; +} + +double scr_atomics_op_dyn(const ScrDyn *array, double index, double value, double replacement, double operation) { + ScrBytes *bytes = scr_atomics_dyn_array(array, false); + return bytes ? scr_atomics_op(bytes, index, value, replacement, operation) : 0; +} + +ScrStr *scr_atomics_wait_dyn(const ScrDyn *array, double index, double expected, double timeout) { + ScrBytes *bytes = scr_atomics_dyn_array(array, true); + return bytes ? scr_atomics_wait(bytes, index, expected, timeout) : NULL; +} + +double scr_atomics_notify_dyn(const ScrDyn *array, double index, double count) { + ScrBytes *bytes = scr_atomics_dyn_array(array, true); + return bytes ? scr_atomics_notify(bytes, index, count) : 0; +} diff --git a/tests/corpus/6862-readline-chained-listeners.cjs b/tests/corpus/6862-readline-chained-listeners.cjs new file mode 100644 index 000000000..78c05063f --- /dev/null +++ b/tests/corpus/6862-readline-chained-listeners.cjs @@ -0,0 +1,29 @@ +// Chained readline listener registration: on(), once() and addListener() +// return the interface, so `createInterface(...).on('line', f).on('close', g)` +// registers both listeners, in order, on the same interface; a chain's value +// is the interface itself. +'use strict'; +const readline = require('node:readline'); +const { Readable } = require('node:stream'); + +const lines = []; +readline + .createInterface({ input: Readable.from(['alpha\nbeta\n', 'gamma\n']), terminal: false }) + .on('line', (line) => lines.push(line)) + .once('line', (line) => console.log('first line', line)) + .on('close', () => console.log('lines', lines.length, lines.join(','))); + +const rl = readline.createInterface({ input: Readable.from(['one\ntwo']) }); +const same = rl.on('line', (line) => console.log('rl line', line)).addListener('close', () => { + console.log('rl close'); +}); +console.log('chain returns the interface', same === rl); + +let count = 0; +readline + .createInterface({ input: Readable.from(['x\ny\nz\n']) }) + .on('line', () => { + count++; + }) + .on('pause', () => console.log('pause after', count)) + .on('close', () => console.log('count', count)); diff --git a/tests/corpus/6865-number-parseint-default-radix.ts b/tests/corpus/6865-number-parseint-default-radix.ts new file mode 100644 index 000000000..dc6d2a4ad --- /dev/null +++ b/tests/corpus/6865-number-parseint-default-radix.ts @@ -0,0 +1,16 @@ +// Number.parseInt without a radix parses as the global parseInt does: radix +// 10, or 16 after a 0x prefix, after leading whitespace and a sign, stopping +// at the first character that is not a digit. +const inputs = ["42", " 17px", "0x1F", "-0x10", "0X0a", "08", "010", "1e3", "", " ", "abc", " -0", "+12", "9007199254740993", "0b11", "0o17", "\t\n 99 bottles", "١٢", "-", "0x"]; +for (const text of inputs) { + const parsed = Number.parseInt(text); + console.log(JSON.stringify(text), Object.is(parsed, -0) ? "-0" : String(parsed), parsed === parseInt(text)); +} + +function depth(arg: string | undefined): number { + return Math.max(6, Number.parseInt(arg ?? "10")); +} +console.log("depth", depth(process.argv[2]), depth("21"), depth("3")); + +const sizes = "640x480".split("x").map((part) => Number.parseInt(part)); +console.log("sizes", sizes[0]! * sizes[1]!); diff --git a/tests/corpus/6866-number-parseint-untyped.js b/tests/corpus/6866-number-parseint-untyped.js new file mode 100644 index 000000000..48223bd9b --- /dev/null +++ b/tests/corpus/6866-number-parseint-untyped.js @@ -0,0 +1,9 @@ +// Number.parseInt over untyped values in JavaScript, with and without a +// radix: the argument converts with ToString first ("undefined", "null", +// "1,2" for an array), then parses as the global parseInt does. +const config = JSON.parse('{"port":"8080","hex":"0xff","list":[3,4],"flag":true,"none":null}'); +for (const key of ["port", "hex", "list", "flag", "none", "missing"]) { + const value = config[key]; + console.log(key, Number.parseInt(value), Number.parseInt(value, 16), Number.parseInt(value, 36)); +} +console.log("argv", Number.parseInt(process.argv[2])); diff --git a/tests/corpus/6867-string-match-optional-regexp.js b/tests/corpus/6867-string-match-optional-regexp.js new file mode 100644 index 000000000..e847bda36 --- /dev/null +++ b/tests/corpus/6867-string-match-optional-regexp.js @@ -0,0 +1,20 @@ +// String match and search in JavaScript with a pattern that may be undefined +// (a Map lookup or an array read): a RegExp matches as usual, and an +// undefined pattern is `new RegExp(undefined)`, the empty pattern. +const text = 'agggtaaa tttaccct agggtaaa'; +const patterns = new Map([ + ['forward', /agggtaaa/g], + ['either', /agggtaaa|tttaccct/gi], + ['first', /t+a/], +]); +for (const name of ['forward', 'either', 'first', 'missing']) { + const re = patterns.get(name); + const m = text.match(re); + console.log(name, m === null ? null : m.length, m === null ? '' : JSON.stringify([...m]), text.search(re)); +} + +const list = [/a(g+)/, /x/]; +for (let i = 0; i < 3; i++) { + const m = text.match(list[i]); + console.log(i, m === null ? 'null' : `${m[0]} at ${m.index} ${m[1] ?? ''}`, text.search(list[i])); +} diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs new file mode 100644 index 000000000..f525f24cf --- /dev/null +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -0,0 +1,38 @@ +// Buffer.from over an untyped ArrayBuffer or SharedArrayBuffer (read from a +// Map, an untyped object field or a worker message): the Buffer covers the +// whole buffer and shares its memory, in both directions, as in Node. An +// encoding name in the second argument converts to byte offset 0. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +if (isMainThread) { + const store = new Map(); + store.set('plain', new ArrayBuffer(6)); + store.set('shared', new SharedArrayBuffer(6)); + for (const key of ['plain', 'shared']) { + const storage = store.get(key); + const buf = Buffer.from(storage); + buf[1] = 0x41; + new Uint8Array(storage)[2] = 0x42; + console.log(key, buf.length, Buffer.isBuffer(buf), buf.toString('hex'), new Uint8Array(storage).join(',')); + const encoded = Buffer.from(storage, 'utf8'); + console.log(key, 'with encoding', encoded.length, encoded.toString('hex')); + } + + const holder = JSON.parse('{}'); + holder.data = new ArrayBuffer(3); + const view = Buffer.from(holder.data); + view.write('xyz'); + console.log('field', Buffer.from(holder.data).toString()); + + const sab = new SharedArrayBuffer(8); + const worker = new Worker(__filename, { workerData: sab }); + worker.on('message', (message) => { + const seen = Buffer.from(message.storage); + console.log('worker wrote', seen.toString('latin1'), new Uint8Array(sab).join(',')); + }); +} else { + const out = Buffer.from(workerData); + out.write('shared!'); + parentPort.postMessage({ storage: workerData }); +} diff --git a/tests/corpus/6869-atomics-untyped-arrays.cjs b/tests/corpus/6869-atomics-untyped-arrays.cjs new file mode 100644 index 000000000..18fe07319 --- /dev/null +++ b/tests/corpus/6869-atomics-untyped-arrays.cjs @@ -0,0 +1,65 @@ +// Atomics over untyped arrays in JavaScript: a worker's workerData carries +// Int32Array and Uint8Array views of a SharedArrayBuffer, and load, store, +// add, sub, and, or, xor, exchange, compareExchange, wait and notify run on +// them as on typed bindings. Other values throw Node's +// TypeError, and an index outside the array Node's RangeError. The results +// go to an untyped array. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +const lines = []; +function attempt(label, run) { + try { + run(); + lines.push(`${label} ok`); + } catch (error) { + lines.push(`${label} ${error.name} ${error.message}`); + } +} + +if (isMainThread) { + const sab = new SharedArrayBuffer(16); + const counters = new Int32Array(sab); + const worker = new Worker(__filename, { workerData: { counters, bytes: new Uint8Array(sab, 8, 8) } }); + worker.on('message', (message) => { + if (message === 'ready') { + Atomics.store(counters, 1, 42); + console.log('notified', Atomics.notify(counters, 1, 1) >= 0); + return; + } + for (const line of message.lines) console.log(line); + console.log('worker results', JSON.stringify(message.out)); + console.log('counters', Array.from(counters).join(',')); + }); +} else { + const counters = workerData.counters; + const bytes = workerData.bytes; + const out = []; + out.push(Atomics.add(counters, 0, 5), Atomics.sub(counters, 0, 2), Atomics.load(counters, 0)); + out.push(Atomics.or(counters, 2, 6), Atomics.and(counters, 2, 3), Atomics.xor(counters, 2, 1), Atomics.load(counters, 2)); + out.push(Atomics.exchange(counters, 3, 9), Atomics.compareExchange(counters, 3, 9, 11), Atomics.compareExchange(counters, 3, 0, 1)); + out.push(Atomics.add(bytes, 0, 300), Atomics.load(bytes, 0), Atomics.sub(bytes, 1, 1), Atomics.load(bytes, 1)); + out.push(Atomics.load(counters, 1.9), Atomics.load(counters, NaN)); + attempt('index past the end', () => { + Atomics.load(counters, 4); + }); + attempt('negative index', () => { + Atomics.add(bytes, -1, 1); + }); + const values = [[], {}, undefined, null, 5, 'text', new Float64Array(2), new Map(), new ArrayBuffer(4), new DataView(new ArrayBuffer(4))]; + for (const value of values) { + attempt('load', () => { + Atomics.load(value, 0); + }); + attempt('notify', () => { + Atomics.notify(value, 0); + }); + } + attempt('wait on Uint8Array', () => { + Atomics.wait(bytes, 0, 0, 0); + }); + out.push(Atomics.wait(counters, 1, 0, 0)); + parentPort.postMessage('ready'); + out.push(Atomics.wait(counters, 1, 0, 5000) === 'ok' || Atomics.load(counters, 1) === 42); + parentPort.postMessage({ out, lines }); +} diff --git a/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json b/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json new file mode 100644 index 000000000..5c8f4dafd --- /dev/null +++ b/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json @@ -0,0 +1 @@ +{ "plain": 1930, "san": 4280 } diff --git a/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json b/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json new file mode 100644 index 000000000..ad095e0af --- /dev/null +++ b/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json @@ -0,0 +1 @@ +{ "plain": 1940, "san": 4550 } diff --git a/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json b/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json new file mode 100644 index 000000000..03bfc0b22 --- /dev/null +++ b/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 1830, "san": 3890 } diff --git a/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json b/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json new file mode 100644 index 000000000..f6a23dfa9 --- /dev/null +++ b/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json @@ -0,0 +1 @@ +{ "plain": 1960, "san": 4170 } diff --git a/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json b/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json new file mode 100644 index 000000000..a1967e528 --- /dev/null +++ b/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json @@ -0,0 +1 @@ +{ "plain": 2070, "san": 4110 } diff --git a/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json b/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json new file mode 100644 index 000000000..114626b53 --- /dev/null +++ b/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 4710 } From d68b73e3b4636308cb16fdecc21b4c5d03aa3e03 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:10:25 -0500 Subject: [PATCH 02/12] Extend a generic factory's local class returned through a constructor type A generic factory declared to return a constructor type (`Struct = (abstract new () => S) & { decode }`) whose every return gives back one of its own local classes (`return Base as Struct`) is now that local class's factory: each instance has its own class, the result maps to the class value while the instance lowers and at its calls, and `class X extends struct(...)` extends it like a factory returning the class directly. Before, the result type mapped to nothing (SC2008) and the extends clause refused as a computed expression. Static methods of a function-local class in a TypeScript file now lower as the class value's runtime statics, as they already do in JavaScript files: they read the enclosing function's bindings, call each other through `this`, and inherit into a subclass. Before, they refused with SC1090. Static fields and static blocks of such classes keep the fence. Fixes #1372 Co-authored-by: blurname <50226652+blurname@users.noreply.github.com> --- .../local-class-expressions.mdx | 3 +- .../frontend/lowering/generic-functions.ts | 123 ++++++++++++++++-- .../src/frontend/lowering/lower-classes.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 10 ++ packages/compiler/src/frontend/type-mapper.ts | 11 ++ .../6912-extend-factory-local-class.ts.json | 6 + .../corpus/6912-extend-factory-local-class.ts | 67 ++++++++++ .../6912-extend-factory-local-class.ts.json | 1 + 8 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json create mode 100644 tests/corpus/6912-extend-factory-local-class.ts create mode 100644 tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json diff --git a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx index 8b653b25e..173bdc0c6 100644 --- a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx +++ b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx @@ -1,4 +1,5 @@ -- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported. +- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported. + Static methods read the enclosing function's bindings and are inherited by subclasses. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index abcd5a584..0f549d889 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -129,6 +129,10 @@ export interface GenericInstance { tsBindings?: Map; /** Rendered type arguments ("") for diagnostics. */ typeArgsText: string; + /** A factory returning one of its local classes through a declared + * constructor type: that type (by id) and the class value it maps to + * while the body lowers (Lowerer.instanceResultClass). */ + resultClass?: { declared: number; value: IrType }; /** Implicit instances only: param symbol → the call site's (widened) * checker type, consulted by the Lowerer's typeOf while this instance's * body lowers (the implicit twin of `bindings`). */ @@ -468,7 +472,8 @@ function localClassResultInstance( params: ParamShape[], retTs: ts.Type, ): GenericInstance | null { - if (!localClassOfResult(lowerer, info, retTs)) return null; + const resultClass = localClassOfResult(lowerer, info, retTs); + if (!resultClass) return null; const tsBindings = new Map(); const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings); const bindingKey = info.typeParams @@ -483,12 +488,29 @@ function localClassResultInstance( tsBindings, }); if (inst.returnType !== pending) return inst; - const returnType = withInstanceScope(lowerer, info, inst, () => lowerer.mapTypeOf(retTs)); + // A result typed by a declared constructor type rather than the class + // (`Struct`) is the class value of the local class the body returns. + const declaredResult = info.decl.type + ? lowerer.checker.getTypeFromTypeNode(info.decl.type) + : undefined; + const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass; + const returnType = withInstanceScope(lowerer, info, inst, () => { + if (!viaConstructorType) return lowerer.mapTypeOf(retTs); + const classSymbol = resultClass.name + ? lowerer.checker.getSymbolAtLocation(resultClass.name) + : undefined; + return classSymbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(classSymbol)) : null; + }); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } inst.returnType = returnType; + if (viaConstructorType && returnType.kind === "classval") { + if (declaredResult) inst.resultClass = { declared: declaredResult.id, value: returnType }; + if (lowerer.typeParamBindings === null) lowerer.localClassValueTypes.set(retTs.id, returnType); + return inst; + } // Values of the class outside the factory (`const b = new B()`, `b.read()`) // carry the instantiated checker type; record which class it names. A // call inside another generic instance can have a type that depends on @@ -563,12 +585,94 @@ function localClassOfResult( type: ts.Type, ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); - if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; - const decl = symbol.valueDeclaration; - if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; - for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) - if (parent === info.decl) return decl; - return null; + const decl = symbol && symbol.flags & ts.SymbolFlags.Class ? symbol.valueDeclaration : undefined; + if (decl && (ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) + return declaredInside(decl, info.decl) ? decl : null; + // A result typed by a constructor type (`Struct`, `new () => S`) whose + // every return gives back the one local class, through parentheses and + // assertions (`return Base as Struct`). + // A mixin (a class extending one of the function's parameters) keeps the + // mixin instantiation, which keys on the argument class. + if (lowerer.checker.getConstructSignatures(type).length === 0) return null; + const returned = returnedLocalClass(lowerer, info); + return returned && !extendsParameter(lowerer, returned, info) ? returned : null; +} + +function extendsParameter( + lowerer: Lowerer, + decl: ts.ClassLikeDeclaration, + info: GenericFnInfo, +): boolean { + const params = new Set(); + const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } }; + for (const param of fn.parameters?.nodes ?? []) { + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol !== undefined) params.add(symbol); + } + for (const clause of decl.heritageClauses?.nodes ?? []) + for (const base of clause.types.nodes) { + const symbol = ts.isIdentifier(base.expression) + ? lowerer.checker.getSymbolAtLocation(base.expression) + : undefined; + if (symbol !== undefined && params.has(symbol)) return true; + } + return false; +} + +function declaredInside(node: ts.Node, owner: ts.Node): boolean { + for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) + if (parent === owner) return true; + return false; +} + +/** The local class every return of `info`'s body gives back, or null when a + * return gives anything else (or nothing). Nested functions and classes keep + * their own returns. */ +function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null { + const fn = info.decl as ts.Node & { body?: ts.Node }; + const body = fn.body; + if (!body) return null; + const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => { + let expr = node; + while ( + ts.isParenthesizedExpression(expr) || + ts.isAsExpression(expr) || + ts.isSatisfiesExpression(expr) || + ts.isNonNullExpression(expr) + ) + expr = expr.expression; + if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null; + if (!ts.isIdentifier(expr)) return null; + const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration; + return declaration && + ts.isClassDeclaration(declaration) && + declaredInside(declaration, info.decl) + ? declaration + : null; + }; + if (!ts.isBlock(body)) return classOf(body as ts.Expression); + let found: ts.ClassLikeDeclaration | null = null; + let valid = true; + const visit = (node: ts.Node): void => { + if ( + !valid || + ts.isFunctionLike(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node) + ) + return; + if (ts.isReturnStatement(node)) { + const returned = node.expression ? classOf(node.expression) : null; + if (!returned || (found !== null && found !== returned)) valid = false; + else found = returned; + return; + } + ts.forEachChild(node, visit); + }; + ts.forEachChild(body, visit); + return valid ? found : null; } /** Runs `fn` under an instance's type bindings and local-class names, as @@ -583,14 +687,17 @@ function withInstanceScope( const prevTsBindings = lowerer.typeParamTsBindings; const previousPath = lowerer.genericInstantiationPath; const prevContext = lowerer.instantiationContext; + const prevResultClass = lowerer.instanceResultClass; lowerer.typeParamBindings = inst.bindings; lowerer.typeParamTsBindings = inst.tsBindings ?? null; lowerer.genericInstantiationPath = inst.path; lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`; + lowerer.instanceResultClass = inst.resultClass ?? null; lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name }); try { return fn(); } finally { + lowerer.instanceResultClass = prevResultClass; lowerer.localClassInstantiations.pop(); lowerer.typeParamBindings = prevBindings; lowerer.typeParamTsBindings = prevTsBindings; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988..505b4d709 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2404,7 +2404,8 @@ export function collectClassShapeInner( for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && - isJsSourceFile(ts.getSourceFileOfNode(decl)) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) || + (functionLocalClass(decl) && ts.isMethodDeclaration(member))) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -5872,9 +5873,10 @@ export function lowerClassExpressionInfo( decoratorNodesOf(member).length || (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && (ts.isClassStaticBlockDeclaration(member) || - ts - .getModifiers(member) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) || + (!ts.isMethodDeclaration(member) && + ts + .getModifiers(member) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)))) || (ts.isMethodDeclaration(member) && member.typeParameters?.nodes.length), )) ) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecb..c1074f0a5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2171,6 +2171,12 @@ export class Lowerer { /** Instance types (by checker type id) of the local classes generic * factory calls return, and the per-instance class each names. */ readonly localClassInstanceTypes = new Map(); + /** A generic factory's instantiated result type → the class value of the + * local class it returns (TypeMapperCtx.localClassValue). */ + readonly localClassValueTypes = new Map(); + /** While a generic factory instance lowers: its declared result type → the + * class value of the local class it returns. */ + instanceResultClass: { declared: number; value: IrType } | null = null; /** The ts-level twin of typeParamBindings, non-null while a CALL-keyed * instance body lowers: type-parameter symbol → the bound CHECKER type, * consulted where the mapped IrType has already widened away information @@ -3529,6 +3535,10 @@ export class Lowerer { genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), runtimeClassValue: (type) => runtimeClassValueOfType(this, type), + localClassValue: (type) => + this.instanceResultClass?.declared === type.id + ? this.instanceResultClass.value + : (this.localClassValueTypes.get(type.id) ?? null), mixinClassInstance: (decl) => this.mixinTypeContext && this.mixinTypeContext.classNode === decl ? { kind: "object", className: this.mixinTypeContext.className } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186..25b8c45a9 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1114,6 +1114,12 @@ export interface TypeMapperCtx { * only the literal's objects, so it maps to the empty record (see the * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; + /** The class value a generic factory's result type names when the factory + * returns one of its own local classes through a constructor type + * (`function struct(v: S): Struct { class Base {}; return Base as + * Struct; }`), by the checker type: the declared result inside the + * instance being lowered, the instantiated result at its calls. */ + localClassValue?: (type: ts.Type) => IrType | null; checker: ts.TypeChecker; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). @@ -1653,6 +1659,11 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return STRING; if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return BOOL; } + const localValue = ctx.localClassValue?.(type); + if (localValue) { + contextResolutions++; + return localValue; + } const topLevel = mapTypeDepth === 0; const typeId = (type as { id?: number }).id; const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json new file mode 100644 index 000000000..5fc7da1eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6912-extend-factory-local-class.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6912-extend-factory-local-class.ts b/tests/corpus/6912-extend-factory-local-class.ts new file mode 100644 index 000000000..8848280e2 --- /dev/null +++ b/tests/corpus/6912-extend-factory-local-class.ts @@ -0,0 +1,67 @@ +// A generic factory returns its own local class through a declared +// constructor type, and a class extends the factory's result. The local +// class's static methods read the factory's parameter, inherit into the +// subclass, and call each other through `this`. +type Struct = (abstract new () => S) & { readonly decode: (value: unknown) => S }; +function struct(value: S): Struct { + abstract class Base { + static decode(_input: unknown): S { + return value; + } + } + return Base as Struct; +} +class Counter extends struct({ value: 42 }) {} +console.log(Counter.decode(null).value); + +type Named = (new (label: string) => { label: string; payload: T }) & { + readonly describe: () => string; + readonly make: (label: string) => { label: string; payload: T }; +}; +function named(kind: string, payload: T): Named { + class Entry { + label: string; + payload: T; + constructor(label: string) { + this.label = label; + this.payload = payload; + } + static describe(): string { + return `entry of ${kind}`; + } + static make(label: string): Entry { + return new this(label); + } + } + return Entry; +} +class Tag extends named("tag", [1, 2, 3]) { + shout(): string { + return this.label.toUpperCase(); + } +} +const tag = new Tag("hello"); +console.log(tag.shout(), tag.payload.join("+"), Tag.describe()); +const made = Tag.make("made"); +console.log(made.label, made.payload.length); + +const Flags = named("flag", { on: true }); +const flag = new Flags("first"); +console.log(flag.label, flag.payload.on, Flags.describe()); +const Other = named("other", 5); +console.log(Other.describe(), new Other("x").payload, Flags.describe()); + +// A function-local class's static methods read the function's parameter and +// call each other through `this`. +function twice(n: number): number { + class K { + static double(): number { + return n * 2; + } + static quad(): number { + return this.double() * 2; + } + } + return K.double() + K.quad(); +} +console.log(twice(3), twice(5)); diff --git a/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json b/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json new file mode 100644 index 000000000..a44d195f2 --- /dev/null +++ b/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json @@ -0,0 +1 @@ +{ "plain": 3633, "san": 5578 } From c081d5a3a771da3b09b3c6d6ec4d90f3e311bb3e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:16:55 -0500 Subject: [PATCH 03/12] Apply the byte offset in Buffer.from over untyped ArrayBuffers Buffer.from(arrayBuffer, byteOffset) with an untyped ArrayBuffer or SharedArrayBuffer now returns a Buffer from that offset to the end, as Node's fromArrayBuffer does, where the offset was dropped and the Buffer covered the whole buffer. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/containers/bytes.ts | 11 +++-- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/runtime/src/scr_bytes.h | 1 + packages/runtime/src/scr_bytes_io.c | 43 +++++++++++++++++-- .../6868-buffer-from-untyped-array-buffer.cjs | 22 +++++++++- 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 683211762..cf2255f89 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -448,6 +448,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromArray": "scr_buffer_from_arr", "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", + "buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index bf9821d31..afd108eb4 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1743,8 +1743,9 @@ function knownBufferProducer( /** Buffer.from over a checked-dynamic source and encoding, both already in * repeatable locals: Node's fromString takes utf8 for an absent, non-string - * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; - * other sources ignore the encoding. */ + * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an + * ArrayBuffer takes the second argument as its byte offset; other sources + * ignore it. */ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; const isString = (x: IrExpr): IrExpr => ({ @@ -1786,10 +1787,12 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx type: BYTES_U8, loc, }, + // An ArrayBuffer takes the second argument as its byte offset + // (scr_buffer_from_dyn_offset_pooled); other values convert with utf8. else_: { kind: "libCall", - fn: "buffer.poolFromDynRef", - args: [source, utf8], + fn: "buffer.poolFromDynOffset", + args: [source, encoding], type: BYTES_U8, loc, }, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 482739db5..ae6b663ca 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -789,6 +789,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromArray", "buffer.poolFromDyn", "buffer.poolFromDynRef", + "buffer.poolFromDynOffset", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 49683550e..c92cd40d4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2921,6 +2921,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, /** poolFromDyn that also copies an array crossing by reference. */ "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9c81caea5..9b382dda5 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -397,6 +397,7 @@ ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed arr ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index ac8fad02e..bd79d9089 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1300,9 +1300,10 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { } /* Buffer.from(arrayBuffer) over a checked value: a Buffer over all of the - * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. A - * second argument would be the byte offset, and an encoding name converts - * to NaN, which is offset 0. */ + * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. The + * calls that reach it have no second argument or a literal encoding name, + * which as a byte offset converts to NaN, offset 0; other second arguments + * take scr_buffer_from_dyn_offset_pooled, which applies the offset. */ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage) { ScrBytes *out = scr_bytes_buffer_view(storage->v.handle.ptr, SCR_BYTES_U8, 0, false, 0); if (out) out->is_buffer = true; @@ -1381,6 +1382,42 @@ ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *enco return scr_buffer_from_dyn_pooled(value, encoding); } +/* Buffer.from(value, second) over checked values whose second argument is + * not a literal encoding name: an ArrayBuffer or SharedArrayBuffer takes it + * as the byte offset, as Node's fromArrayBuffer does (undefined and NaN are + * 0, the rest of the buffer is the length, ERR_BUFFER_OUT_OF_BOUNDS past the + * end); other values convert as scr_buffer_from_dyn_ref_pooled with utf8. */ +ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second) { + if (!scr_buffer_storage_is(value)) { + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *out = scr_buffer_from_dyn_ref_pooled(value, utf8); + scr_str_release(utf8); + return out; + } + double offset = 0; + if (second->kind != SCR_DYN_UNDEF) { + if (!scr_dyn_number_coerce_js(second, &offset)) return NULL; + if (isnan(offset)) offset = 0; + } + ScrBytes *storage = value->v.handle.ptr; + ScrBytes *owner = storage->backing ? storage->backing : storage; + if ((double)scr_bytes_byte_len(owner) - offset < 0) { + static const char msg[] = "\"offset\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + double start = trunc(offset); + if (start < 0) { + char msg[96]; + int n = snprintf(msg, sizeof msg, "Start offset %.0f is outside the bounds of the buffer", start); + scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)n); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_view(storage, SCR_BYTES_U8, start, false, 0); + if (out) out->is_buffer = true; + return out; +} + /* new Buffer(number, encoding) — the deprecated ctor's string arm with a * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE (the * throwing path never fires DEP0005, so compiled silence matches). */ diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs index f525f24cf..e30b5873e 100644 --- a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -1,7 +1,8 @@ // Buffer.from over an untyped ArrayBuffer or SharedArrayBuffer (read from a // Map, an untyped object field or a worker message): the Buffer covers the -// whole buffer and shares its memory, in both directions, as in Node. An -// encoding name in the second argument converts to byte offset 0. +// buffer from its byte offset (the second argument) and shares its memory, in +// both directions, as in Node. An encoding name or NaN in the second argument +// is offset 0; an offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS. 'use strict'; const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); @@ -19,6 +20,23 @@ if (isMainThread) { console.log(key, 'with encoding', encoded.length, encoded.toString('hex')); } + const offsets = new Map(); + offsets.set('storage', new ArrayBuffer(8)); + const storage = offsets.get('storage'); + for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5]) { + try { + const view = Buffer.from(storage, offset); + view.fill(offset === 2 ? 0x11 : 0); + console.log('offset', String(offset), view.byteOffset, view.length, new Uint8Array(storage).join(',')); + } catch (error) { + console.log('offset', String(offset), error.name, error.code, error.message); + } + } + const sharedOffsets = new Map([['s', new SharedArrayBuffer(6)]]); + const tail = Buffer.from(sharedOffsets.get('s'), 4); + tail[0] = 0x7f; + console.log('shared offset', tail.length, new Uint8Array(sharedOffsets.get('s')).join(',')); + const holder = JSON.parse('{}'); holder.data = new ArrayBuffer(3); const view = Buffer.from(holder.data); From 777bfb43f2e126e5d5fa0f8046eca3944d87548d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:40:17 -0500 Subject: [PATCH 04/12] Revert "Name lowered helpers and closures independently of lowering order" This reverts #1750 (05e2f404d), which made main CI fail. --- package.json | 2 +- packages/compiler/src/contracts/check.ts | 3 +- .../lowering/array-indexed-mutation.ts | 3 +- .../src/frontend/lowering/array-values.ts | 5 +- .../frontend/lowering/builtins/arguments.ts | 3 +- .../lowering/builtins/child-process-async.ts | 3 +- .../lowering/builtins/child-process-sync.ts | 3 +- .../lowering/builtins/disposable-stack.ts | 3 +- .../frontend/lowering/builtins/filesystem.ts | 3 +- .../src/frontend/lowering/builtins/json.ts | 3 +- .../lowering/builtins/operating-system.ts | 7 +- .../frontend/lowering/builtins/performance.ts | 5 +- .../src/frontend/lowering/builtins/process.ts | 5 +- .../frontend/lowering/builtins/promises.ts | 5 +- .../lowering/builtins/promisify-callback.ts | 3 +- .../frontend/lowering/builtins/text-codecs.ts | 3 +- .../src/frontend/lowering/builtins/timers.ts | 2 +- .../src/frontend/lowering/builtins/url.ts | 7 +- .../src/frontend/lowering/builtins/v8.ts | 3 +- .../src/frontend/lowering/call-arguments.ts | 3 +- .../src/frontend/lowering/call-signatures.ts | 7 +- .../src/frontend/lowering/class-assertions.ts | 5 +- .../src/frontend/lowering/class-unions.ts | 3 +- .../lowering/coercions/function-adapters.ts | 11 +- .../frontend/lowering/coercions/structural.ts | 15 ++- .../src/frontend/lowering/coercions/unions.ts | 15 ++- .../frontend/lowering/collection-ingestion.ts | 3 +- .../lowering/containers/array-callbacks.ts | 25 ++-- .../lowering/containers/array-concat.ts | 3 +- .../lowering/containers/array-construction.ts | 7 +- .../lowering/containers/array-methods.ts | 11 +- .../lowering/containers/array-pipelines.ts | 3 +- .../src/frontend/lowering/containers/bytes.ts | 5 +- .../lowering/containers/callback-arguments.ts | 3 +- .../containers/collection-grouping.ts | 5 +- .../lowering/containers/collection-methods.ts | 15 ++- .../lowering/containers/indexed-objects.ts | 15 ++- .../lowering/containers/iterator-helpers.ts | 7 +- .../lowering/containers/string-and-regexp.ts | 11 +- .../lowering/containers/string-replacement.ts | 3 +- .../frontend/lowering/expressions/binary.ts | 3 +- .../lowering/expressions/object-literals.ts | 7 +- .../lowering/expressions/optional-chains.ts | 10 +- .../lowering/expressions/property-access.ts | 3 +- .../lowering/expressions/relational.ts | 5 +- .../frontend/lowering/generic-functions.ts | 17 +-- .../src/frontend/lowering/intern-tables.ts | 7 +- .../frontend/lowering/iterator-adapters.ts | 17 ++- .../frontend/lowering/js-dynamic-objects.ts | 5 +- .../src/frontend/lowering/json-require.ts | 3 +- .../src/frontend/lowering/lifetime-regions.ts | 3 +- .../src/frontend/lowering/lower-assert.ts | 11 +- .../src/frontend/lowering/lower-calls.ts | 49 ++++---- .../src/frontend/lowering/lower-classes.ts | 9 +- .../src/frontend/lowering/lower-dgram.ts | 5 +- .../frontend/lowering/lower-event-emitter.ts | 13 +- .../src/frontend/lowering/lower-exprs.ts | 9 +- .../src/frontend/lowering/lower-functions.ts | 2 +- .../src/frontend/lowering/lower-inspect.ts | 7 +- .../src/frontend/lowering/lower-mixins.ts | 3 +- .../src/frontend/lowering/lower-mock.ts | 3 +- .../frontend/lowering/lower-modules.test.ts | 4 +- .../src/frontend/lowering/lower-modules.ts | 15 +-- .../src/frontend/lowering/lower-namespaces.ts | 11 +- .../src/frontend/lowering/lower-server.ts | 13 +- .../src/frontend/lowering/lower-stdlib.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 15 ++- .../src/frontend/lowering/lower-timers.ts | 7 +- .../src/frontend/lowering/lowerer.test.ts | 30 ++--- .../compiler/src/frontend/lowering/lowerer.ts | 101 +++------------- .../src/frontend/lowering/native-ffi.ts | 2 +- .../lowering/object-enumeration-receiver.ts | 3 +- .../src/frontend/lowering/pairs-snapshot.ts | 3 +- .../lowering/promise-executor-adapter.ts | 3 +- .../lowering/require-initialization.ts | 3 +- .../src/frontend/lowering/stable-names.ts | 48 -------- .../src/frontend/lowering/strict-equality.ts | 3 +- .../frontend/lowering/structural-positions.ts | 107 ----------------- packages/compiler/src/frontend/ts7/ast.ts | 90 +------------- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 92 ++------------ .../test/array-pipeline-fusion.test.ts | 2 +- .../test/format-float-arguments.test.ts | 2 +- .../test/in-runtime-key-emission.test.ts | 2 +- packages/compiler/test/stable-names.test.ts | 112 ------------------ .../compiler/test/type-mapper-unions.test.ts | 3 +- 86 files changed, 258 insertions(+), 854 deletions(-) delete mode 100644 packages/compiler/src/frontend/lowering/stable-names.ts delete mode 100644 packages/compiler/src/frontend/lowering/structural-positions.ts delete mode 100644 packages/compiler/test/stable-names.test.ts diff --git a/package.json b/package.json index d1a8e34ae..5cc30d9f8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", - "test:lowering-order": "node scripts/lowering-order.mjs --fail-on names", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index c80468961..5934624bf 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -174,8 +174,7 @@ class ContractChecker { return `${owner}.${method[2]}`; } if (fn?.sourceName !== undefined && fn.sourceName !== "") return fn.sourceName; - if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName)) - return `the function at ${this.where(fn.loc)}`; + if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`; return fnName.replace(/^%/, ""); } diff --git a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts index 0ded460da..6d539212e 100644 --- a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts +++ b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts @@ -9,7 +9,6 @@ import { type SrcLoc, typeEquals, } from "../../ir/ir.js"; -import { keyedHelperName } from "./stable-names.js"; import { numLit, varRef } from "../../ir/build.js"; import { typeKey } from "../type-mapper.js"; import type { Lowerer } from "./lowerer.js"; @@ -126,7 +125,7 @@ export function lowerArrayFill( writeNull; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.fill", key); + name = "%arr.fill." + lowerer.arrHofHelpers.size; lowerer.arrHofHelpers.set(key, name); const a = varRef("a.0", arrType, loc); const n = varRef("n.0", F64, loc); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 314cdfd24..0ca8503df 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { @@ -253,7 +252,7 @@ export function lowerSafeIndexRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = keyedHelperName(lowerer, "%arr.idxOr", key); + name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arr.type; lowerer.liftedFns.push({ @@ -299,7 +298,7 @@ export function lowerSafeBytesRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = keyedHelperName(lowerer, "%bytes.idxOr", key); + name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const b = varRef("b.0", bytesT, at); const i = varRef("i.0", F64, at); diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index d2fc430dd..7f862f603 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { BYTES_U8, BOOL, @@ -73,7 +72,7 @@ export function lowerOptionalNumberPredicate( const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%number.optionalPredicate", key); + helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index d0991dce5..5590fbe9f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; @@ -219,7 +218,7 @@ function execFileAsyncHelper( const key = shell ? "execAsync" : "execFileAsync"; const existing = lowerer.valueHelpers.get(key); if (existing) return { name: existing, shapeId, envShapeId }; - const name = keyedHelperName(lowerer, `%${key}`, key); + const name = `%${key}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const envRecT: IrType = { kind: "record", shapeId: envShapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index b9858e768..24adcc1ba 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -542,7 +541,7 @@ function execStdioModeHelper( const key = `cp.stdiomode:${shapeId}:${fd}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key); + const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const optsT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 375c3f1e7..1b9a9c03a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -85,7 +84,7 @@ export function lowerDisposableStackUseCall( const key = `disposableStack.use:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%disposableStack.use", key); + helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const context = newFnCtx(false, null, null, VOID); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 6b988c491..0e1c062c5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; @@ -443,7 +442,7 @@ export function lowerStringOrBytesWrite( const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%fs.writeData", key); + helper = `%fs.writeData.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; for (let tag = 0; tag < arms.length; tag++) { diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 64f829865..7eb8aa9b2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -254,7 +253,7 @@ function lowerOptionalStringifyRoot( const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%json.optionalString", key); + helper = `%json.optionalString.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", value.type, loc); const serialized: IrExpr = { diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index ee189a1f3..c10465039 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -220,7 +219,7 @@ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: const key = `os.cpus:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%os.cpus", key); + helper = `%os.cpus.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const index = varRef("i.0", F64, loc); const field = (k: number): IrExpr => ({ @@ -347,7 +346,7 @@ export function lowerPathParseCall( const key = `${fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%path.parse", key); + helper = `%path.parse.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const parts = varRef("parts.0", arrayOf(STRING), loc); lowerer.liftedFns.push({ @@ -449,7 +448,7 @@ export function lowerPathFormatCall( const key = `${fn}:${typeKey(recordT)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%path.format", key); + helper = `%path.format.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const object = varRef("object.0", recordT, loc); const read = (name: string): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index bae525c17..72304e6a9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -331,7 +330,7 @@ function lowerPerfTimelineCall( const key = `perf.${many ? "entries" : timeline.fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%perf.${many ? "entries" : member}`, key); + helper = `%perf.${many ? "entries" : member}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const argsRef = varRef("a.0", DYN, loc); const whichRef = varRef("w.0", F64, loc); @@ -462,7 +461,7 @@ export function perfEntryTag( const key = `perf.tag:${entryT.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%perf.tag", key); + helper = `%perf.tag.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const e = varRef("e.0", entryT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 842f42b57..25653f541 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { stdinKeypressListener } from "./readline.js"; import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -1562,7 +1561,7 @@ export function lowerProcessMethodCall( const key = `${prefix}usage.diff:${prevT.shapeId}:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%${prefix}usage.diff`, key); + helper = `%${prefix}usage.diff.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const pRef: IrExpr = { kind: "varRef", localId: "p.0", type: prevT, loc }; const fieldOf = (name: string): IrExpr => ({ @@ -1637,7 +1636,7 @@ export function lowerProcessMethodCall( const key = `process.memoryUsage:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%process.memoryUsage", key); + helper = `%process.memoryUsage.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); lowerer.liftedFns.push({ name: helper, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5..668ef01f8 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -56,7 +55,7 @@ function lowerTuplePromiseAll( const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { - name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); + name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); const ctx = newFnCtx(false, null, null, resultT.inner); ctx.isAsync = true; @@ -558,7 +557,7 @@ export function lowerPromiseStaticCall( const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%promise.try", key); + helper = `%promise.try.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index b4dbbf513..822a4fecc 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; @@ -171,7 +170,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const base = keyedHelperName(lowerer, "%promisify", key); + const base = `%promisify.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, base); const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; const promiseT = resultT.ret; diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 8d47c8e3a..7229d5b84 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { isBuiltinClassInstance } from "./receiver-types.js"; import { strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import { staticTextDecoderEncoding } from "../text-decoder-encoding.js"; @@ -130,7 +129,7 @@ function stringDecoderHelper( const key = `strdec.${op}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%strdec.${op}`, key); + const name = `%strdec.${op}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index d7cbd63b5..2d531663f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -85,7 +85,7 @@ export function lowerTimersPromisesSetInterval( const genT = timersPromisesIntervalType(lowerer, expr); if (genT === null) lowerer.badType(expr, lowerer.typeOf(expr)); const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); - const fnName = lowerer.closureName(loc, "_tpInterval"); + const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; const promiseVoid: IrType = { kind: "promise", inner: VOID }; diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 2a77fa85c..dca8a7cc0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, countedFor, strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -293,7 +292,7 @@ function searchParamsFromTuples( const key = `sp.fromTuples:${row.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%sp.fromTuples", key); + helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const pairsT = init.type; const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); @@ -373,7 +372,7 @@ function lowerOptionalStringSearchParams( const key = `sp.optionalString:${init.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%sp.optionalString", key); + helper = `%sp.optionalString.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("init.0", init.type, loc); lowerer.liftedFns.push({ @@ -679,7 +678,7 @@ function lowerSpForEachCall( const key = `${arity}:${typeKey(fnRet)}`; let helper = lowerer.spHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%sp.forEach", key); + helper = `%sp.forEach.${lowerer.spHofHelpers.size}`; lowerer.spHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSpForEachFn(helper, arity, fnRet, loc)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 989a61a3f..3582ce8ae 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -5,7 +5,6 @@ * spirit of process.memoryUsage(): approximate values, not V8 heap * figures (a compiled binary embeds no V8 engine). Every other member * fences at its use with the by-design reason (builtinFenceHintOf). */ -import { keyedHelperName } from "../stable-names.js"; import { numLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -56,7 +55,7 @@ export function lowerV8HeapStatisticsCall( const key = `v8.getHeapStatistics:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%v8.getHeapStatistics", key); + helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const sample = (index: number): IrExpr => ({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 060ffe9d2..49f7c8ec0 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -754,7 +753,7 @@ function holeReadingLift(lowerer: Lowerer, from: IrType, to: IrType, loc: SrcLoc const key = `arr.holes:${typeKey(elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.holes", key); + const name = `%arr.holes.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push( buildArrayConversion( diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index dacb4d324..34343ad27 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -849,10 +849,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify( - ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, decl) + nameText, - ), + qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -900,7 +897,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, decl) + (decl.name?.text ?? "%default"), + nsPathPrefix(decl) + (decl.name?.text ?? "%default"), ), params, returnType, diff --git a/packages/compiler/src/frontend/lowering/class-assertions.ts b/packages/compiler/src/frontend/lowering/class-assertions.ts index 52654fab8..900c32426 100644 --- a/packages/compiler/src/frontend/lowering/class-assertions.ts +++ b/packages/compiler/src/frontend/lowering/class-assertions.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -123,7 +122,7 @@ export function checkedClassAssertion( const key = `classassert:${typeKey(source.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%class.assert", key); + name = `%class.assert.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); const input = varRef("value.0", source.type, loc); const body: IrStmt[] = []; @@ -229,7 +228,7 @@ export function checkedClassUnionAssertion( const key = `classunionassert:${typeKey(value.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%class.assert", key); + name = `%class.assert.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); const input = varRef("value.0", value.type, loc); const locals = [{ id: "value.0", name: "value", type: value.type, mutable: false }]; diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index 0021b2cf8..f52026ba4 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -133,7 +132,7 @@ export function narrowClassUnion( const key = `classUnion:${source.className}:${target.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%class.union", key); + helper = `%class.union.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", source, loc); // Rank by ancestry before sorting: returning zero for unrelated pairs diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index fe3c14605..ffdac19f0 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { buildFunctionAdapter } from "./builders.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -34,7 +33,7 @@ export function funcReturnWidthAdapter( const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.width", key); + const name = `%fn.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); // The returned closure's body: call the captured original, width-map. @@ -164,7 +163,7 @@ function restPackingAdapter( const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.adapt", key); + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const fixed = fromT.params.length - 1; @@ -221,7 +220,7 @@ function funcIntoDynRestAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.adapt", key); + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; @@ -377,7 +376,7 @@ export function funcCoerceAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.adapt", key); + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((t, i) => ({ @@ -605,7 +604,7 @@ export function spawnResFnAdapter( const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fnval.spawnres", key); + const name = `%fnval.spawnres.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((p, i) => ({ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 5b5e283b3..496a29607 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; @@ -254,7 +253,7 @@ export function unionRecordWidthHelper( const key = `unionWidth:${fromId}:${target.shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%union.record", key); + const name = `%union.record.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const type: IrType = { kind: "union", unionId: fromId }; const value = varRef("value.0", type, loc); @@ -313,7 +312,7 @@ export function recordWidthHelper( const key = `rec:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%rec.width", key); + const name = `%rec.width.${lowerer.valueHelpers.size}`; // Interned BEFORE the body builds: a recursive nested-width field // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); @@ -342,7 +341,7 @@ export function tupleArrayWidthHelper( const key = `tuparr:${fromId}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%tup.arr", key); + const name = `%tup.arr.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc }; @@ -389,7 +388,7 @@ export function emptyArrayLiftHelper( const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.empty", key); + const name = `%arr.empty.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc }; lowerer.liftedFns.push({ @@ -453,7 +452,7 @@ export function arrayWidthHelper( const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.width", key); + const name = `%arr.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const arrT: IrType = { kind: "array", elem: fromElem }; const outT: IrType = { kind: "array", elem: toElem }; @@ -493,7 +492,7 @@ export function objRecordWidthHelper( const key = `obj:${className}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%obj.width", key); + const name = `%obj.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "object", className }; lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); @@ -518,7 +517,7 @@ export function recordClassWidthHelper( const key = `cls:${fromId}:${className}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%cls.width", key); + const name = `%cls.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.noteEdge(`%${className}.constructor`); const fromT: IrType = { kind: "record", shapeId: fromId }; diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 434e687a5..06c0af9c6 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; import { nodeThrowExpr } from "../../../ir/build.js"; @@ -148,7 +147,7 @@ export function strandedUnitTrap( const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%unit.strand", key); + name = `%unit.strand.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push({ name, @@ -228,7 +227,7 @@ export function strandedCoercionTrap( const key = `strand:${expected.unionId}:${typeKey(src)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%union.strand", key); + name = `%union.strand.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); const toT: IrType = { kind: "union", unionId: expected.unionId }; lowerer.liftedFns.push({ @@ -276,7 +275,7 @@ export function recordUnionWrapHelper( const key = `recordWrap:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%record.wrap", key); + const name = `%record.wrap.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -308,7 +307,7 @@ export function recordUnionRouteHelper( (from, dst) => lowerer.widthLiftPlan(from, dst), ); if (!plan) return null; - const name = keyedHelperName(lowerer, "%record.route", key); + const name = `%record.route.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -353,7 +352,7 @@ export function unionRetagHelper( }); const key = `${fromId}:${toId}:${stranded.join(".")}`; const existing = lowerer.coercions.retags.get(key); - const name = existing ?? keyedHelperName(lowerer, "%union.retag", key); + const name = existing ?? `%union.retag.${lowerer.coercions.retags.size}`; if (!existing) { lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( @@ -411,7 +410,7 @@ export function narrowedArmHelper( lowerer.coercions.checkedNarrows.add(existing); return existing; } - const name = keyedHelperName(lowerer, "%union.narrow", key); + const name = `%union.narrow.${lowerer.coercions.narrows.size}`; lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); @@ -588,7 +587,7 @@ export function deferredReadHelper( const key = `deferred:${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%deferred.read", key); + const name = `%deferred.read.${lowerer.coercions.narrows.size}`; lowerer.coercions.narrows.set(key, name); const dflt: IrExpr = target.kind === "bool" diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 8fe2a6bbb..5d14d5492 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import { typeKey } from "../type-mapper.js"; @@ -198,7 +197,7 @@ export function ingestCollection( const key = `ingest:${input.collection ? typeKey(input.collection) + ":" + input.selection : "cursor:" + typeKey(input.element)}:${typeKey(destination)}:${mapper ? typeKey(mapper.type) : "copy"}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%collection.ingest", key); + name = `%collection.ingest.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const context = newFnCtx(false, null, null, destination); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 7ca69d9ba..eb99592bb 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { transformStmtList } from "../../../ir/traverse.js"; import type { ScrDiagnostic } from "../../../diagnostics/diagnostic.js"; @@ -507,7 +506,7 @@ function arrayHofHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); const fn = buildArrayHofFn( @@ -620,7 +619,7 @@ function arrayUnionHofHelper( const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.union", key); + const name = `%arr.union.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const receiver = varRef("a.0", receiverT, loc); const callback = varRef("f.0", callbackT, loc); @@ -1205,7 +1204,7 @@ function findHelper( const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -1305,7 +1304,7 @@ function findIndexHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); if (!last) lowerer.arrHofInfo.set(name, { @@ -1389,7 +1388,7 @@ function someEveryHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, @@ -1588,7 +1587,7 @@ function flatMapHelper( const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.flatMap", key); + const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method: "flatMapArray", @@ -1830,7 +1829,7 @@ function reduceHelper( const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); if (method === "reduce") lowerer.arrHofInfo.set(name, { @@ -2122,7 +2121,7 @@ export function lowerArraySortCall( const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${order}${reads ? `:field${reads.length}` : ""}` : ""}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%arr.${method}`, key); + helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const undefinedTag = elem.kind === "union" ? lowerer.armTag(elem.unionId, UNDEFINED_T) : -1; lowerer.liftedFns.push( @@ -2168,7 +2167,7 @@ function sortKeyReader( const cacheKey = `sortKey:${typeKey(elem)}:${sortKeysId(reads.map((read) => ({ order: 1, read })))}`; let reader = lowerer.arrHofHelpers.get(cacheKey); if (!reader) { - reader = keyedHelperName(lowerer, "%arr.sortKey", cacheKey); + reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(cacheKey, reader); lowerer.liftedFns.push(buildSortKeyFn(reader, elem, reads, loc)); } @@ -2309,7 +2308,7 @@ function dynFlatMapHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: S const key = `dynflatmap:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%dyn.flatmap", key); + const name = `%dyn.flatmap.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFlatMapFn(name, elemT, arity, loc)); return name; @@ -2482,7 +2481,7 @@ function dynFilterHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: Sr const key = `dynfilter:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%dyn.filter", key); + const name = `%dyn.filter.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFilterFn(name, elemT, arity, loc)); return name; @@ -2677,7 +2676,7 @@ export function lowerTuplePredicateCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%tuple.${method}`, key); + helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const v = varRef("v.0", elem, loc); const out = varRef("out.0", resultT, loc); diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index b4e285ee9..4f6e4ca43 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { @@ -29,7 +28,7 @@ export function arrayConcatHelper( const key = `concat:${typeKey(elem)}:${shape.map((type) => (type ? `a:${typeKey(type)}` : "e")).join(";")}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.concat", key); + const name = `%arr.concat.${lowerer.arrHofHelpers.size}`; const arrT = arrayOf(elem); const params = [ { localId: "a.0", name: "a", type: arrT }, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 1f10640c0..40e4d6436 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; @@ -257,7 +256,7 @@ function appendWidenedArrayValues( const key = `insertValues:${typeKey(sourceType.elem)}:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.insertValues", key); + name = `%arr.insertValues.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const outType = arrayOf(elem); const valueType = arrayValueType(lowerer, source.type.elem); @@ -1036,7 +1035,7 @@ export function lowerArrayFromCall( const key = `fromArray:${typeKey(arrT.elem)}:${typeKey(outElem)}:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%arr.fromArray", key); + helper = `%arr.fromArray.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildArrayFromArrayFn(lowerer, helper, arrT.elem, outElem, fnRet, arity, loc), @@ -1123,7 +1122,7 @@ export function lowerArrayFromCall( const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%arr.fromLen", key); + helper = `%arr.fromLen.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push(buildArrayFromLenFn(helper, fnRet, arity, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda..e9a130a2d 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; @@ -1617,7 +1616,7 @@ function arraySearchHelper( const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const resultT = method === "includes" ? BOOL : F64; @@ -1973,7 +1972,7 @@ export function lowerTupleReadMethodCall( let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { - helper = keyedHelperName(lowerer, `%tuple.${method}`, key); + helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params = [ { localId: "a.0", name: "a", type: receiver.type }, @@ -2035,7 +2034,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%tuple.slice", key); + helper = `%tuple.slice.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params = [receiver, ...args].map((arg, i) => ({ localId: i === 0 ? "a.0" : `arg.${i}`, @@ -2126,7 +2125,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%tuple.${method}`, key); + helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const out = varRef("out.0", outType, loc); const result = varRef("r.0", ret, loc); @@ -2254,7 +2253,7 @@ function lowerArrayAtCall( const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.at", key); + name = `%arr.at.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 180f40435..cf45a7575 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -852,7 +851,7 @@ class PipelineFusion { if (!built) return null; let name = this.helpers.get(built.key); if (!name) { - name = keyedHelperName(this.lowerer, "%arr.fused", built.key); + name = `%arr.fused.${this.helpers.size}`; this.helpers.set(built.key, name); this.added.push({ ...built.fn, name }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 4b20d7bf5..bf9821d31 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; @@ -78,7 +77,7 @@ function lowerBytesSortCall( const cacheKey = `bytes.${method}:${bytesT.elem}:${key}`; let helper = lowerer.arrHofHelpers.get(cacheKey); if (!helper) { - helper = keyedHelperName(lowerer, `%bytes.${method}`, cacheKey); + helper = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(cacheKey, helper); lowerer.liftedFns.push( buildBytesSortFn(helper, bytesT, arity, hasComparator, copyFirst, order, loc), @@ -271,7 +270,7 @@ function bytesCallbackHelper( const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%bytes.${method}`, key); + const name = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const reduce = method === "reduce" || method === "reduceRight"; const backward = method === "reduceRight" || method === "findLast" || method === "findLastIndex"; diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index f39c8e86c..18754f088 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -286,7 +285,7 @@ export function truthyFilterCallback( const key = `filterBoolean:${typeKey(valueT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.filterBoolean", key); + name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const value = varRef("v.0", valueT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index dae68d518..0bf126830 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; @@ -105,7 +104,7 @@ export function lowerGroupByStaticCall( const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.groupBy", key); + helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } @@ -142,7 +141,7 @@ export function lowerGroupByStaticCall( const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%object.groupBy", key); + helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index d3169ec5c..370974c85 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -233,7 +232,7 @@ function lowerMapIterDrainCall( const key = `${method}:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%map.${method}`, key); + helper = `%map.${method}.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapIterDrainFn(helper, mapT, method, elemT, tupleT, loc)); } @@ -349,7 +348,7 @@ export function mapEntriesArray(lowerer: Lowerer, receiver: IrExpr): IrExpr { const key = `entries:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.entries", key); + helper = `%map.entries.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push( buildMapIterDrainFn(helper, mapT, "entries", tupleT, tupleT, receiver.loc), @@ -520,7 +519,7 @@ export function lowerMapForEachCall( const key = `${typeKey(mapT.key)}:${typeKey(mapT.value)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.forEach", key); + helper = `%map.forEach.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, mapT, arity, fnRet, loc)); } @@ -694,7 +693,7 @@ export function lowerSetForEachCall( const key = `${typeKey(setT.elem)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%set.forEach", key); + helper = `%set.forEach.${lowerer.setHofHelpers.size}`; lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, setT, arity, fnRet, loc)); } @@ -746,7 +745,7 @@ function lowerSetCombineCall( const key = `${name}:${typeKey(setT.elem)}:${typeKey(otherT)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%set.${name}`, key); + helper = `%set.${name}.${lowerer.setHofHelpers.size}`; lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSetCombineFn(helper, name, setT, otherT, loc)); } @@ -1358,7 +1357,7 @@ export function mapFromSeedValue( const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%map.checkedSeed", key); + name = `%map.checkedSeed.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, name); const source = varRef("source.0", DYN, loc); const entries = varRef("entries.0", DYN, loc); @@ -1458,7 +1457,7 @@ export function mapFromSeedValue( const key = `seed:${typeKey(mapT.key)}:${typeKey(mapT.value)}:${elem.shapeId}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.seed", key); + helper = `%map.seed.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapSeedFn(mapT, helper, seed.type, elem, kT, vT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index b3faf649e..efc173ffc 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; @@ -185,7 +184,7 @@ export function objectIterOverIndexShape( const key = `obj.${member}:ovf:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%obj.${member}`, key); + helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; const outRef = varRef("out.0", resultT, loc); const rRef = varRef("r.0", argIr, loc); @@ -609,7 +608,7 @@ function lowerTypedObjectFromEntriesCall( const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.fromEntries", key); + helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; const tupleT = argIr.elem as IrType & { kind: "record" }; const tRef = varRef("t.0", tupleT, loc); @@ -713,7 +712,7 @@ function lowerFromEntriesStringRows( const key = `obj.fromEntries:strrows:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.fromEntries", key); + helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; const tRef = varRef("t.0", rowT, loc); const rowLen: IrExpr = { @@ -932,7 +931,7 @@ export function lowerRecordOvfCaptureHelper( const key = `ovf:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%rec.capture", key); + const name = `%rec.capture.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const toT: IrType = { kind: "record", shapeId: toId }; @@ -1205,7 +1204,7 @@ export function lowerObjectAssignIndexShape( const key = `assign:${targetIr.shapeId}:${plan.fromId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%obj.assign", key); + const name = `%obj.assign.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: targetIr.shapeId }; const fromT: IrType = { kind: "record", shapeId: plan.fromId }; @@ -1475,7 +1474,7 @@ export function lowerIndexMergeHelper( .join(","); const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%rec.merge", key); + const name = `%rec.merge.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: toId }; const ksT = arrayOf(STRING); @@ -1806,7 +1805,7 @@ export function lowerEnvToPairsHelper( const key = `env.pairs:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%env.pairs", key); + const name = `%env.pairs.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const arrT = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 48b4caabd..c23e6931c 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -967,7 +966,7 @@ function lowerFusedChain( const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%iter.${terminal}`, key); + helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildIterChainFn( @@ -1039,7 +1038,7 @@ function lowerHelperObject( const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, `%iter.${stage.kind}`, key); + name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), @@ -1059,7 +1058,7 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = srcT === null ? "%iter.budget" : keyedHelperName(lowerer, "%iter.budget.close", key); + name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index fe4ddbc6c..9567ee0f6 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { boolLit, @@ -160,7 +159,7 @@ export function lowerStringSplitCall( const key = `str.split:${receiver.type.kind === "union" ? typeKey(receiver.type) : receiver.type.kind}:${typeKey(separator.type)}:${typeKey(limit.type)}:${absent}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.split", key); + helper = `%str.split.${lowerer.valueHelpers.size}`; const values = [receiver, separator, limit]; const params = values.map((value, index) => ({ localId: `arg.${index}`, @@ -396,7 +395,7 @@ function stringIndexHelperCall( const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.index", key); + helper = `%str.index.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const rawReceiver = varRef("arg.0", receiver.type, loc); const rawIndex = varRef("arg.1", index.type, loc); @@ -674,7 +673,7 @@ export function lowerStringPaddingCall( const key = `str.pad:${method}:${values.map((value) => typeKey(value.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.pad", key); + helper = `%str.pad.${lowerer.valueHelpers.size}`; const params = values.map((value, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1310,7 +1309,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.positions", key); + helper = `%str.positions.${lowerer.valueHelpers.size}`; const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1412,7 +1411,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${args.map((arg) => typeKey(arg.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.positions", key); + helper = `%str.positions.${lowerer.valueHelpers.size}`; const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 780c77d0f..6856bc50b 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -163,7 +162,7 @@ function lowerStringReplacementValues( const key = `str.${method}:${typeKey(replacement.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%str.${method}`, key); + helper = `%str.${method}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const subject = varRef("s.0", STRING, loc); const needle = varRef("search.0", STRING, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 7e449f12b..fafbbf695 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,5 +1,4 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; @@ -802,7 +801,7 @@ function nullProtoToString(lowerer: Lowerer, e: IrExpr, t: IrType & { kind: "rec const key = `nullProtoToString:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%rec.tostr", key); + helper = `%rec.tostr.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const r = varRef("r.0", t, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 93bf43e95..565db0cd8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { pathModuleValue } from "../lower-builtin-values.js"; @@ -407,7 +406,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", - name: () => lowerer.closureName(loc, "_dynfence"), + name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -462,7 +461,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError)) throw err; const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, { kind: "closure", - name: () => lowerer.closureName(loc, "_dynfence"), + name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -2957,7 +2956,7 @@ function lowerDeclaredSpreadMerge( const key = `declmerge-source:${type.shapeId}:${source.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%rec.declmerge", key); + helper = `%rec.declmerge.${lowerer.valueHelpers.size}`; const sourceType = source.value.type; const destination = varRef("out.0", type, loc); const receiver = varRef("source.0", sourceType, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 4b8c5692a..fbcecdc32 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -240,7 +240,7 @@ export function lowerOptionalChain( const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; const stmts: IrStmt[] = []; let guarded = receiver; @@ -298,7 +298,7 @@ export function lowerOptionalChain( ); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -331,7 +331,7 @@ export function lowerOptionalChain( dotNode === expr.expression ) { const m = dotNode.name.text; - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const recvRef: IrExpr = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) }; lowerer.chainRecvByNode.set(recvNode, recvRef); lowerer.chainHandled.add(dotNode); @@ -358,7 +358,7 @@ export function lowerOptionalChain( } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -420,7 +420,7 @@ export function lowerOptionalChain( rest.length === 1 ? rest[0]! : { kind: "union" as const, unionId: lowerer.unions.transform(def, rest) }; - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; // A single present arm binds its payload. Multiple present arms bind // the original tagged value, then retag inside the guarded body. The // nullish arms never enter that body, and each record keeps its identity. diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 6c8dfa3bf..f0b980da0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,5 +1,4 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ -import { keyedHelperName } from "../stable-names.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; @@ -2784,7 +2783,7 @@ export function lowerGroupsProjection( const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%regex.groups", key); + helper = `%regex.groups.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; const locals: IrLocal[] = [{ id: "m.0", name: "m", type: recv.type, mutable: true }]; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 836391835..088b45cff 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,5 +1,4 @@ /* instanceof and in, including private-name and runtime-key in. */ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -648,7 +647,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const key = `instanceof.union:${unionId}:${target.def.name}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%instanceof.union", key); + helper = `%instanceof.union.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc }; const body: IrStmt[] = []; @@ -1848,7 +1847,7 @@ function lowerRuntimeKeyIn( const hkey = `haskey:${recvT.shapeId}`; let helper = lowerer.valueHelpers.get(hkey); if (!helper) { - helper = keyedHelperName(lowerer, "%rec.haskey", hkey); + helper = `%rec.haskey.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(hkey, helper); const recT: IrType = { kind: "record", shapeId: recvT.shapeId }; diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index daf082db7..abcd5a584 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,6 +1,4 @@ import { staticNameBindings } from "./static-function-names.js"; -import { positionTag } from "./structural-positions.js"; -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -231,10 +229,7 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify( - ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, decl) + nameText, - ), + qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), typeParams, instances: new Map(), }); @@ -656,7 +651,7 @@ function internGenericInstance( const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; inst = { path, - name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), + name: `${info.qualifiedName}%${info.instances.size}`, ordinal: info.instances.size, params, returnType, @@ -2134,7 +2129,7 @@ function internImplicitInstance( const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); inst = { path, - name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), + name: `${info.qualifiedName}%${info.instances.size}`, ordinal: info.instances.size, params: shapes, // The promise callers rely on before the body settles it: the @@ -2322,7 +2317,7 @@ export function implicitLocalFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`, + nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, ), typeParams: [], instances: new Map(), @@ -2418,7 +2413,7 @@ export function objLitGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(fnNode), - `%ol${positionTag(lowerer, fnNode)}.${name}`, + `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, ), typeParams, instances: new Map(), @@ -2564,7 +2559,7 @@ export function bindingGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, stmt ?? decl, decl) + + nsPathPrefix(stmt ?? decl, decl) + (topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`), ), typeParams, diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index e3f2b0889..fc69237db 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -23,7 +23,6 @@ * The entry handles that itable.find and itable.insert answer stay inside * these lowerings: f64 locals that only reach toBool and itable.value. * Any other use of the class or its members as values is refused. */ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { BOOL, F64, STRING, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; @@ -448,11 +447,7 @@ function helperName(lowerer: Lowerer, member: InternTableMember, signature: stri } function registerHelper(lowerer: Lowerer, member: InternTableMember, signature: string): string { - const name = keyedHelperName( - lowerer, - `%${INTERN_TABLE_CLASS}.${member}`, - `${member}|${signature}`, - ); + const name = `%${INTERN_TABLE_CLASS}.${member}.${lowerer.internTableHelpers.size}`; lowerer.internTableHelpers.set(`${member}|${signature}`, name); return name; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index e50652182..898c46f20 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -87,7 +86,7 @@ function dynIteratorAdapter( const key = `iterAdapt:${typeKey(expected)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%iter.adapt", key); + name = `%iter.adapt.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const locals: IrLocal[] = []; const local = (id: string, type: IrType, mutable = false): IrExpr => { @@ -314,7 +313,7 @@ export function lowerCollectionIteratorValue( loc, }; if (!typeEquals(yielded.type, type.yieldT)) return null; - name = keyedHelperName(lowerer, "%coll.iter", key); + name = `%coll.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -412,7 +411,7 @@ export function lowerBytesIteratorValue( const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%bytes.iter", key); + name = `%bytes.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const list = varRef("list.0", bytesT, loc); const index = varRef("index.0", F64, loc); @@ -518,7 +517,7 @@ export function lowerStringIteratorValue( const key = `strIter:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%str.iter", key); + name = `%str.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const text = varRef("text.0", STRING, loc); const index = varRef("index.0", F64, loc); @@ -678,7 +677,7 @@ function generatorRetypeAdapter( type: resumeType(expected.nextT), loc, }; - name = keyedHelperName(lowerer, "%gen.retype", key); + name = `%gen.retype.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -795,7 +794,7 @@ export function generatorDrain( const outRef = varRef("out.0", arrT, loc); const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); if (!typeEquals(value.type, elem)) return null; - name = keyedHelperName(lowerer, "%gen.drain", key); + name = `%gen.drain.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -857,7 +856,7 @@ export function generatorDrain( if (!raw) return null; const value = lowerer.coerceToExpected(raw, elem); if (!typeEquals(value.type, elem)) return null; - name = keyedHelperName(lowerer, "%gen.drain", key); + name = `%gen.drain.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -979,7 +978,7 @@ export function lowerArrayIteratorValue( }; } if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null; - name = keyedHelperName(lowerer, "%arr.iter", key); + name = `%arr.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index e27dd4183..b9bbf6216 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -21,7 +21,6 @@ * files only, and an empty answer leaves every mapping unchanged, so * TypeScript programs and JavaScript that never uses these patterns * compile exactly as before. */ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile, locOf } from "../program.js"; import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js"; @@ -216,10 +215,10 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${lowerer.positions.of(decl)}`; + const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%ctorValue", key); + name = `%ctorValue.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const savedClass = lowerer.currentClass; lowerer.currentClass = null; diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index d6c2da571..927745253 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -10,7 +10,6 @@ * JSON module is ordinary storage holding that object — destructuring * copies its properties at the require, exactly like Node. ES-module JSON * imports keep their own record (Node's ESM loader caches separately). */ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -85,7 +84,7 @@ export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void ); throw new PoisonError(); } - const id = keyedHelperName(lowerer, "%g.jsonRequire", jsonSf.fileName); + const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; const loadedId = `${id}%loaded`; lowerer.globalsList.push(global, { diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index b684c0395..b212ae473 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -14,7 +14,6 @@ * Any other use of the class or its methods as values is refused. The * module is marked lifetimeRegions, which moves every ProgramLifetime * class into counted regions. */ -import { keyedHelperName } from "./stable-names.js"; import { F64, VOID, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -153,7 +152,7 @@ function runHelper(lowerer: Lowerer, fnType: IrType & { kind: "func" }, loc: Src const key = typeKey(fnType); const known = lowerer.regionRunHelpers.get(key); if (known !== undefined) return known; - const name = keyedHelperName(lowerer, `%${LIFETIME_REGION_CLASS}.run`, key); + const name = `%${LIFETIME_REGION_CLASS}.run.${lowerer.regionRunHelpers.size}`; lowerer.regionRunHelpers.set(key, name); const regionT = lifetimeRegionType(lowerer); const ret = fnType.ret; diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index bc51a9382..460f4d53a 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:assert lowering (a spoke module like lower-dgram.ts): the * callable module form (`assert(x)` through a default import or a CJS @@ -668,7 +667,7 @@ function partialEqHelper(lowerer: Lowerer, ta: IrType, tb: IrType, loc: SrcLoc): const key = `assert.pdeq:${typeKey(ta)}|${typeKey(tb)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%assert.pdeq", key); + const name = `%assert.pdeq.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", ta, loc); const b = (): IrExpr => varRef("b.0", tb, loc); @@ -1281,7 +1280,7 @@ function deepCheckCall( const key = `assert.dcheck:${variant}:${negated}:${typeKey(a.type)}:${typeKey(b.type)}`; let name = lowerer.assertHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%assert.dcheck", key); + name = `%assert.dcheck.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const pa = varRef("a.0", a.type, loc); const pb = varRef("b.0", b.type, loc); @@ -1845,7 +1844,7 @@ function assertThrowsHelper( const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%assert.${mode}`, key); + const name = `%assert.${mode}.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const caughtRef = (): IrExpr => varRef("e.0", CAUGHT, loc); @@ -2294,7 +2293,7 @@ function ifErrorHelper(lowerer: Lowerer, t: IrType & { kind: "union" }, loc: Src const key = `assert.iferr:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%assert.iferr", key); + const name = `%assert.iferr.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`assert.ifError over unknown union ${t.unionId}`); @@ -2465,7 +2464,7 @@ function deepEqHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc, loose = false): const key = `assert.${prefix}:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%assert.${prefix}`, key); + const name = `%assert.${prefix}.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", t, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 63b8781f2..7102f37ab 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -2095,7 +2094,7 @@ function lowerOptionalNumberDefault( const key = `number.optionalDefault:${widened.unionId}:${dflt}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%number.optionalDefault", key); + helper = `%number.optionalDefault.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ @@ -2204,7 +2203,7 @@ function primitiveConversionHandler( const key = `then.${name}:${typeKey(inner)}`; let fnName = lowerer.arrHofHelpers.get(key); if (fnName === undefined) { - fnName = keyedHelperName(lowerer, `%then.${name}`, key); + fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, fnName); const value = varRef("v.0", inner, loc); let body: IrExpr; @@ -2373,7 +2372,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) const key = `bigint.scalar:${unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%bigint.scalar", key); + helper = `%bigint.scalar.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const body: IrStmt[] = []; @@ -3235,7 +3234,7 @@ function lowerScalarUnionNumber( const key = `number.scalar:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%number.scalar", key); + helper = `%number.scalar.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const nan: IrExpr = { @@ -3280,7 +3279,7 @@ function lowerOptionalStringNumber( const key = `${label}.optionalString:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%${label}.optionalString`, key); + helper = `%${label}.optionalString.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); lowerer.liftedFns.push({ @@ -4707,7 +4706,7 @@ function lowerDefaultToStringCall( const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.tostr", key); + helper = `%obj.tostr.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const o = varRef("o.0", recv.type, loc); lowerer.liftedFns.push({ @@ -5394,7 +5393,7 @@ function loweredTemplateStrings( const site = lowerer.globalsList.length; const globalId = `%g.templateStrings.${site}`; const readyId = `%g.templateStringsReady.${site}`; - getter = keyedHelperName(lowerer, "%templateStrings.get", key); + getter = `%templateStrings.get.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, getter); lowerer.globalsList.push( { id: globalId, name: "%templateStrings", type, mutable: true }, @@ -5575,7 +5574,7 @@ export function lowerPromiseMethodCall( // IS the spec's identity/thrower/no-op substitution for all three // members. An argument expression, when present, is undefined/null // by construction — nothing to evaluate. - const fnName = lowerer.closureName(loc, `_${member}pass`); + const fnName = `%fn${lowerer.lambdaCounter++}_${member}pass`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: promT }; const fnCtx = newFnCtx(true, null, funcType, inner); fnCtx.isAsync = true; @@ -5672,7 +5671,7 @@ export function lowerPromiseMethodCall( // Evaluate both callback expressions before the wrapper runs, in source order. const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true); const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false); - const fnName = lowerer.closureName(loc, "_then2"); + const fnName = `%fn${lowerer.lambdaCounter++}_then2`; const paramTypes: IrType[] = [ promT, ...(fulfilled ? [fulfilled.value.type] : []), @@ -5921,7 +5920,7 @@ export function lowerPromiseMethodCall( ); }; const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = lowerer.closureName(loc, "_then"); + const fnName = `%fn${lowerer.lambdaCounter++}_then`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6016,7 +6015,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = lowerer.closureName(loc, "_then"); + const fnName = `%fn${lowerer.lambdaCounter++}_then`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6119,7 +6118,7 @@ export function lowerPromiseMethodCall( "finally callbacks with parameters or a return value (use () => { ... })", ); } - const fnName = lowerer.closureName(loc, "_finally"); + const fnName = `%fn${lowerer.lambdaCounter++}_finally`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6222,7 +6221,7 @@ export function lowerPromiseMethodCall( } if (cb.type.kind === "dyn") { const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = lowerer.closureName(loc, "_catchdyn"); + const fnName = `%fn${lowerer.lambdaCounter++}_catchdyn`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6352,7 +6351,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = lowerer.closureName(loc, "_catch"); + const fnName = `%fn${lowerer.lambdaCounter++}_catch`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: resultT }; const fnCtx = newFnCtx(true, null, funcType, R); fnCtx.isAsync = true; @@ -6589,7 +6588,7 @@ function filterNarrowHelper( const key = `filterNarrow:${test}:${typeKey(elem)}:${typeKey(outElem)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.filterNarrow", key); + const name = `%arr.filterNarrow.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); @@ -6835,7 +6834,7 @@ export function recordKeysArrayCall( const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.keys", key); + helper = `%obj.keys.${lowerer.arrHofHelpers.size}`; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; lowerer.arrHofHelpers.set(key, helper); @@ -6905,7 +6904,7 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = keyedHelperName(lowerer, "%obj.hasOwn", key); + const helper = `%obj.hasOwn.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const shape = lowerer.shapes.get(shapeId)!; const recT: IrType = { kind: "record", shapeId }; @@ -6990,7 +6989,7 @@ function classAssignHelper( const key = `obj.assignClass:${className}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = keyedHelperName(lowerer, "%obj.assignClass", key); + const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const oT: IrType = { kind: "object", className }; @@ -7058,7 +7057,7 @@ function recordAssignHelper( const key = `obj.assign:${targetShapeId}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = keyedHelperName(lowerer, "%obj.assign", key); + const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const tShape = lowerer.shapes.get(targetShapeId)!; @@ -8515,7 +8514,7 @@ function lowerObjectStaticCall( const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%obj.${member}`, key); + helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; const recT = argIr; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; @@ -9151,7 +9150,7 @@ function lowerInlineArrowBind( const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%arrow.bind", key); + const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); @@ -10080,7 +10079,7 @@ function lowerUnionObjectDynFieldCall( const receiverRef = varRef(receiverLocal.id, receiverT, loc); let helper = lowerer.unionCallHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%union.call", key); + helper = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, helper); const receiverRef = varRef("this.0", receiverT, loc); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -10173,7 +10172,7 @@ function unionObjectMethodHelper( const key = `${receiverT.unionId}:${method}:${shapes.map((shape) => `${shape.mode}:${typeKey(shape.type)}`).join(",")}:${typeKey(resultT)}`; const existing = lowerer.unionCallHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%union.call", key); + const name = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, name); // A probe that catches a refusal while the body lowers must not leave // the name registered for a helper that was never emitted. @@ -10367,7 +10366,7 @@ export function lowerStaticNumberGlobal( const key = `parseInt.optional:${unionT.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%parseInt.optional", key); + helper = `%parseInt.optional.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc }; const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index fcea03790..790efe988 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -4664,7 +4663,7 @@ export function genericClassInstanceType( ); } const ordinal = gci.instances.size; - const name = keyedHelperName(lowerer, `${gci.family.def.name}%`, key, ""); + const name = `${gci.family.def.name}%${ordinal}`; const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); @@ -7911,7 +7910,7 @@ export function classIteratorDrainCall( const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%iter.drain", key); + name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; @@ -8025,7 +8024,7 @@ export function classIteratorRestDrainCall( const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%iter.drain", key); + name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; @@ -10533,7 +10532,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%x509.record", key); + helper = `%x509.record.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index aa90fc7c7..5c99ebc82 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit } from "../../ir/build.js"; /* The dgram/dns-surface lowering (node:dgram + node:dns — a spoke module * like lower-server.ts): module-function calls (dgram createSocket, dns @@ -358,7 +357,7 @@ function checkedDynPromise( const key = `dns.promise:${typeKey(declared)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%dns.promise", key); + helper = `%dns.promise.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const promiseT: IrType = { kind: "promise", inner: DYN }; lowerer.liftedFns.push({ @@ -892,7 +891,7 @@ export function lowerPromisifiedDnsCall( const key = `dnsPromise:${JSON.stringify(value)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%dnsPromise", key); + name = `%dnsPromise.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f105af786..f472672b2 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -42,7 +42,6 @@ * emit-overrides block at the bottom of this file (per-event * monomorphization of the override body, dispatch on the dynamic class, * super.emit as the prototype chain). */ -import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -632,7 +631,7 @@ function lowerFlexListenerCall( const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); + helper = `%emitter.${registering ? "onFlex" : "offFlex"}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -851,7 +850,7 @@ function lowerDynListenerCall( const existing = lowerer.arrHofHelpers.get(key); let helper = existing; if (!helper) { - helper = keyedHelperName(lowerer, `%emitter.${registering ? "onDyn" : "offDyn"}`, key); + helper = `%emitter.${registering ? "onDyn" : "offDyn"}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const check: IrStmt = { kind: "exprStmt", @@ -1001,7 +1000,7 @@ export function lowerDynamicEmitterEmit( const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%emitter.dynEmit", key); + helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -1093,7 +1092,7 @@ export function lowerDynamicEmitterOn( const key = `emitter.dynOn:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%emitter.dynOn", key); + helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2360,7 +2359,7 @@ function emitDispatchExpr( const key = `emitter.emitDispatch:${typeKey(recvT)}:${name}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%emitter.emitDispatch", key); + helper = `%emitter.emitDispatch.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2425,7 +2424,7 @@ function eventsOnceBoxer( const key = `eventsOnce:${typeKey(fnType)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%events.onceArgs", key); + name = `%events.onceArgs.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const params: IrParam[] = tuple.map((type, i) => ({ localId: `%once.a${i}`, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ba96bea8c..e7a1b79a8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -2275,11 +2274,7 @@ export function cjsAccessorSnapshotOf( if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; - const at = locOf(getter); - const snapshot = { - id: keyedHelperName(lowerer, "%g.cjsSnapshot", `${at.file}:${at.start}:${at.end}`), - type: call.type, - }; + const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; lowerer.globalsList.push({ id: snapshot.id, name: "%cjsSnapshot", @@ -2304,7 +2299,7 @@ export function cjsAccessorSnapshotStores( if (getters.length === 0) return []; let readyId = lowerer.cjsSnapshotReady.get(dep); if (readyId === undefined) { - readyId = keyedHelperName(lowerer, "%g.cjsSnapshotReady", dep.fileName); + readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); lowerer.cjsSnapshotReady.set(dep, readyId); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 653c0d837..cd8dc31d1 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,7 +270,7 @@ export function lowerLambda( const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; - const fnName = lowerer.closureName(locOf(node), baseName ? `_${baseName}` : ""); + const fnName = `%fn${lowerer.lambdaCounter++}${baseName ? `_${baseName}` : ""}`; // Named function expressions/declarations can self-reference by name; an // object-literal method's name is a PROPERTY, not a binding — no self. const selfSymbol = diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1979b43aa..4be5d0b7e 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit, @@ -269,7 +268,7 @@ function errorInspectHelper( const key = `insp.userError:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%util.insp", key); + const name = `%util.insp.${lowerer.inspectHelpers.size}`; lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); const r = (): IrExpr => varRef("r.0", F64, loc); @@ -1146,7 +1145,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const key = `insp:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%util.insp", key); + const name = `%util.insp.${lowerer.inspectHelpers.size}`; lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); @@ -2075,7 +2074,7 @@ function formatUnionHelper( const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; const loc = helperLoc(site); - const name = keyedHelperName(lowerer, "%util.fmtv", key); + const name = `%util.fmtv.${lowerer.inspectHelpers.size}`; lowerer.inspectHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`format value over unknown union ${t.unionId}`); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 9de027563..766059dfb 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -34,7 +34,6 @@ * statics-bearing mixin classes outside a plain top-level statement * (their declaration-time code must run exactly where the call * evaluates). */ -import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -453,7 +452,7 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( ts.getSourceFileOfNode(call), - `%mx${positionTag(lowerer, call)}.${shape.classNode.name?.text ?? ""}`, + `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 8c50dee46..fcc68c787 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -17,7 +17,6 @@ * writes the original back on restore(). Each mock registers a closure * that calls its restore() with its tracker (scr_test.c): the module's * `mock`, or the test's t.mock, which is reset when the test ends. */ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -137,7 +136,7 @@ function liftOnce( const key = `mock.${role}:${owner}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%mock.${role}`, key); + const name = `%mock.${role}.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); build(name); return name; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.test.ts b/packages/compiler/src/frontend/lowering/lower-modules.test.ts index f1021e4da..84cc96e7d 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.test.ts @@ -34,7 +34,7 @@ test("retains dependencies from function storage, captures, generator channels a const completed = record("completed"); const body = record("body"); const global = record("global", record("nested")); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id); record("unused"); const unionId = lowerer.unions.intern([body, UNDEFINED_T]); const unusedUnion = lowerer.unions.intern([F64, UNDEFINED_T]); @@ -162,7 +162,7 @@ test("closes nested structural types and index-signature values", () => { sent = record("sent"), indexed = record("indexed"); const containerId = lowerer.shapes.intern([], false, indexed); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id); record("unused"); const nested: IrType = { kind: "func", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 0b99e88d3..d1f276d44 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,5 +1,4 @@ import { THREADS_MODULE } from "../threads-import.js"; -import { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { everyStmtList, everyTypeChild } from "../../ir/traverse.js"; @@ -1075,14 +1074,8 @@ export function moduleArtifacts( else delete rest.abstractMethods; return rest; }), - // Ids come from what each shape and union is (keyedHelperName), so - // ordering by id gives every lowering order the same module. - records: lowerer.shapes.shapes - .filter((r) => shapeIds.has(r.id)) - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), - unions: lowerer.unions.unions - .filter((u) => unionIds.has(u.id)) - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + records: lowerer.shapes.shapes.filter((r) => shapeIds.has(r.id)), + unions: lowerer.unions.unions.filter((u) => unionIds.has(u.id)), }; } @@ -1684,7 +1677,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(lowerer, stmt)}%alias.${stmt.name.text}%${positionTag(lowerer, stmt)}`, + id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1761,7 +1754,7 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(lowerer, stmt, list.declarations.nodes[0]); + const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 19123dae3..dca835d77 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -36,7 +36,6 @@ * of the compiler already takes (a body running before the declaring * statement is not modeled — same as top-level const reads through early * calls). */ -import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; @@ -165,11 +164,7 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v * distinct (resolution is by symbol; names only need uniqueness and * determinism). `modifierDecl` carries the declaration whose modifiers * decide exportedness (the first declarator of a variable statement). */ -export function nsPathPrefix( - owner: PositionTagOwner, - node: ts.Node, - modifierDecl?: ts.Node, -): string { +export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const parent = node.parent; if (parent === undefined || !ts.isModuleBlock(parent)) return ""; const flagsHost = modifierDecl ?? node; @@ -184,10 +179,10 @@ export function nsPathPrefix( const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${positionTag(owner, p)}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${positionTag(owner, parent)}`}.`; + return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 94d49d8b3..058d7874a 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -184,7 +183,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE const toT = funcOf([...fromT.params], VOID); const key = `server.voidcb:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%server.voidcb", key); + const name = existing ?? `%server.voidcb.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -280,7 +279,7 @@ function receiverReturningCall( ): IrExpr { const key = `server.recvret:${fn}:${opts?.prefix?.fn ?? ""}:${(opts?.mainArgIndices ?? []).join(".")}:${callArgs.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%server.recvret", key); + const name = existing ?? `%server.recvret.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = callArgs.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); @@ -591,7 +590,7 @@ function h2HeadersCallbackAdapter( ); const key = `h2.hdrcb:${handleKind}:${withStatus}:${typeKey(fromT)}:${shapeId ?? ""}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%h2.hdrcb", key); + const name = existing ?? `%h2.hdrcb.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -3584,7 +3583,7 @@ export function lowerCompatReqStreamOptionalCall( type: unionT, loc, }; - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const body = lowerH2StreamMethodCall(lowerer, call, call.expression, { kind: "chainRecv", id, @@ -4425,7 +4424,7 @@ function lowerHttpCreateServerForms( const hasMax = opts.maxHeaderSize !== null; const key = `server.opts:${opts.joinDup ? 1 : 0}:${opts.allowMissingHost ? 1 : 0}:${hasBuffer ? 1 : 0}:${hasMax ? 1 : 0}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%server.opts", key); + const name = existing ?? `%server.opts.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = [ @@ -8010,7 +8009,7 @@ function lowerHttpResMethodCall( const all = [...callArgs, cb]; const key = `server.endcb:${fn}:${all.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const helper = existing ?? keyedHelperName(lowerer, "%server.endcb", key); + const helper = existing ?? `%server.endcb.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, helper); const params = all.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 0beb24a0e..2ad3657c7 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2278,7 +2278,7 @@ function lowerStaticNamespaceImport( ): IrExpr { const loc = locOf(call); const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; - const fnName = lowerer.closureName(loc, "_staticImport"); + const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; const fnCtx = newFnCtx(true, null, fnType, ns); fnCtx.isAsync = true; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index a75ec43b7..b9add95a6 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; import { hashStreamOf, isHashValueType } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; @@ -312,7 +311,7 @@ function lowerStreamCallback( ); } } - const fnName = lowerer.closureName(loc, `_${which}cb`); + const fnName = `%fn${lowerer.lambdaCounter++}_${which}cb`; const fnCtx = newFnCtx(true, null, funcType, funcType.ret); lowerer.fnStack.push(fnCtx); try { @@ -1145,7 +1144,7 @@ function streamMethodWrapper( paramTypes.push(got.type); } const ret = found.sig.ret; - const fnName = lowerer.closureName(loc, `_${which}vt`); + const fnName = `%fn${lowerer.lambdaCounter++}_${which}vt`; const funcType = funcOf([thisType, ...paramTypes], ret); lowerer.fnStack.push(newFnCtx(true, null, funcType, ret)); try { @@ -1691,7 +1690,7 @@ function fromGenHelpers( const key = `streamFromGen:${role}:${typeKey(gen)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, `%stream.fromGen.${role}`, key); + name = `%stream.fromGen.${role}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -1780,7 +1779,7 @@ function classIteratorObject( const key = `streamFromIter:${cit.iterT.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%stream.fromIter", key); + name = `%stream.fromIter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const nextName = `${name}.next`; const nextT = funcOf([], DYN); @@ -1941,7 +1940,7 @@ function classAsyncIteratorObject( const key = `streamFromAsyncIter:${cit.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%stream.fromAsyncIter", key); + name = `%stream.fromAsyncIter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const it: IrLocal = { id: "it.0", name: "it", type: cit.iterT, mutable: false, boxed: true }; const src: IrLocal = { @@ -2895,7 +2894,7 @@ function pipelineBoxHelper( const key = `pipelineBox:${typeKey(promiseT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%stream.pipelineBox", key); + name = `%stream.pipelineBox.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -2922,7 +2921,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr let name = lowerer.valueHelpers.get(key); const abortT = funcOf([], VOID); if (!name) { - name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); + name = `%stream.pipelineAbort.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 03c9c5c1f..7d4bba34b 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile } from "../program.js"; @@ -56,7 +55,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.dropret:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%timer.dropret", key); + const name = existing ?? `%timer.dropret.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -118,7 +117,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.droparg:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%timer.droparg", key); + const name = existing ?? `%timer.droparg.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -204,7 +203,7 @@ function makeTimerArgsThunk( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.argsthunk:${n}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%timer.argsthunk", key); + const name = existing ?? `%timer.argsthunk.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 7fbb9661f..bacdc7941 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -10,7 +10,6 @@ import { } from "../../ir/ir.js"; import type { Node, Program, SourceFile, Symbol, Type } from "../ts7/adapter.js"; import { Lowerer } from "./lowerer.js"; -import { UnionRegistry } from "../type-mapper.js"; import { classMethodValue } from "./class-method-values.js"; const loc = { file: "island.ts", start: 0, end: 1 }; @@ -33,7 +32,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive, () => "Recursive"); + lowerer.shapes.recursiveRef(recursive); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -55,7 +54,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive, () => "Recursive"); + const to = lowerer.unions.recursiveRef(recursive); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -76,7 +75,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); + const target = record(lowerer.shapes.recursiveRef(recursive)); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -130,7 +129,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive)); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -144,7 +143,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive, () => "Recursive"); + const union = lowerer.unions.recursiveRef(recursive); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -183,10 +182,7 @@ test("removing undefined reuses the discriminator transformation without hiding ); const recursive = {} as Type; - const pending: IrType = { - kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), - }; + const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); expect(lowerer.stripUndefinedArm(pending)).toEqual(STRING); @@ -219,10 +215,7 @@ test("adding undefined reuses semantic union transformations and sees recursive expect(lowerer.unions.get(added.unionId)?.discriminant).toEqual(discriminant); const recursive = {} as Type; - const pending: IrType = { - kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), - }; + const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); expect(lowerer.withUndefinedArmOf(pending)).toEqual(added); @@ -234,18 +227,13 @@ test("adding undefined reuses semantic union transformations and sees recursive test("optional union widening keeps refused and missing contracts current", () => { const lowerer = context(); const recursive = {} as Type; - const pending: IrType = { - kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), - }; + const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); - // Union ids come from their arms, so a fresh registry names the union - // this one has not interned yet. - const missing: IrType = { kind: "union", unionId: new UnionRegistry().intern([F64, STRING]) }; + const missing: IrType = { kind: "union", unionId: `u${lowerer.unions.unions.length}` }; expect(lowerer.withUndefinedArmOf(missing)).toBeNull(); expect(lowerer.unions.intern([F64, STRING])).toBe(missing.unionId); expect(lowerer.withUndefinedArmOf(missing)?.kind).toBe("union"); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3f8a0a66e..ee8ba7ecb 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,6 +1,4 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; -import { keyedHelperName } from "./stable-names.js"; -import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, @@ -1006,11 +1004,6 @@ function lowerShuffle(): ((wave: string[]) => void) | null { * the bodies reachable emit did NOT mark (plus deferred collection * diagnostics nothing flushed), reported separately: whole-program analysis without * letting unreached code fail builds. */ -/** The closure root of an emit-override specialization body. */ -function emitSpecRoot(spec: EmitSpecRequest): string { - return `%emit.${spec.info.def.name}.${spec.event}.${spec.ordinal}`; -} - export function lowerToIr( program: ts.Program, entry: ts.SourceFile, @@ -2808,8 +2801,6 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); - /** Each name keyedHelperName made, with a second hash of its key. */ - readonly keyedNameKeys = new Map(); /** structuralNarrowing's assignable classes, keyed by static and * narrowed class name. */ readonly structuralClassPeers = new Map(); @@ -2946,6 +2937,7 @@ export class Lowerer { ctx: FnCtx; } >(); + chainCounter = 0; /** Keyed by program-wide qualified class name (what IR object types carry). */ readonly classes = new Map(); readonly classBySymbol = new Map(); @@ -3175,47 +3167,7 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; - /** Structural source positions (structural-positions.ts) for names that - * stay the same when unrelated code moves. */ - readonly positions = new StructuralPositions(); - - /** The outermost node whose token span is exactly `loc`, or null. */ - nodeAtLoc(loc: SrcLoc): ts.Node | null { - const sf = this.program.getSourceFile(loc.file); - return sf === undefined ? null : (ts.nodeAtSpan(sf, loc.start, loc.end) ?? null); - } - - /** The lowering unit whose name scopes the closures it lifts. */ - private closureRoot = "%pre"; - private readonly closureNameCounts = new Map(); - - /** Runs `fn` with the closures it lifts named under `root`. */ - withClosureRoot(root: string, fn: () => T): T { - const previous = this.closureRoot; - this.closureRoot = root; - try { - return fn(); - } finally { - this.closureRoot = previous; - } - } - - /** The name of a closure (or wrapper) lifted for the source at `loc`: - * `.` of the current lowering unit, the location and - * `suffix`, then `suffix`. A unit that lifts several functions for the same - * location and suffix numbers them in the order it lifts them, so a - * closure's name depends only on its unit, not on which units lowered - * before it. */ - closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { - const node = this.nodeAtLoc(loc); - const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); - const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; - const first = keyedHelperName(this, prefix, key); - const seen = this.closureNameCounts.get(first) ?? 0; - this.closureNameCounts.set(first, seen + 1); - return (seen === 0 ? first : keyedHelperName(this, prefix, `${key}|${seen}`)) + suffix; - } - + lambdaCounter = 0; readonly preparedClassFactories = new Map(); readonly preparingClassFactories = new Set(); @@ -3568,7 +3520,6 @@ export class Lowerer { this.checker = program.getTypeChecker(); this.typeCtx = { checker: this.checker, - positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, classNamer: this.classNamer, @@ -3857,11 +3808,11 @@ export class Lowerer { })() ? this.qualify( ts.getSourceFileOfNode(decl), - `%cx${positionTag(this, decl)}.${decl.name?.text ?? ""}`, + `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, ) : this.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(this, decl) + (decl.name ? decl.name.text : "%anon"), + nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { const entry = this.localClassInstantiations[index]!; @@ -7420,10 +7371,7 @@ export class Lowerer { } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); - if (info) - functions.push( - ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), - ); + if (info) functions.push(...this.lowerClassMembers(info)); else if (this.countsSkips()) this.countSkippedBody(decl); } } @@ -7436,10 +7384,7 @@ export class Lowerer { // are reachable by definition, already counted by reachable emit. if (!this.remainder) { for (const fp of parts) { - const initName = this.initNameOf.get(fp.sf)!; - functions.push( - this.withClosureRoot(initName, () => this.lowerFileInit(fp.sf, fp.topStmts, initName)), - ); + functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!)); } functions.push(this.buildMain()); } @@ -7481,9 +7426,7 @@ export class Lowerer { // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). try { - functions.push( - this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), - ); + functions.push(this.lowerGenericInstance(info, inst)); } catch (e) { if (!(e instanceof PoisonError)) throw e; } @@ -7493,10 +7436,7 @@ export class Lowerer { // instances of its own; the joint fixpoint covers both). while (es < this.emitSpecQueue.length) { try { - const spec = this.emitSpecQueue[es++]!; - const fn = this.withClosureRoot(emitSpecRoot(spec), () => - lowerEmitOverrideSpec(this, spec), - ); + const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!); if (fn) functions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8050,9 +7990,7 @@ export class Lowerer { this.genericClassDemandPriority.set(info, ref); instanceFunctions.push( ...this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => - this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), - ), + this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info)), ), ); relieveHeapPressure(); @@ -8079,7 +8017,7 @@ export class Lowerer { instanceFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(ref, () => - this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + this.lowerGenericInstance(info, inst), ), ), ); @@ -8112,12 +8050,9 @@ export class Lowerer { ); while (specLowered < waveEnd) { try { - const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => { - const spec = this.emitSpecQueue[specLowered++]!; - return this.withClosureRoot(emitSpecRoot(spec), () => - lowerEmitOverrideSpec(this, spec), - ); - }); + const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => + lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!), + ); if (fn) instanceFunctions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8145,9 +8080,7 @@ export class Lowerer { initFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(priority, () => - this.withClosureRoot(this.initNameOf.get(fp.sf)!, () => - this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), - ), + this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), ), ), ); @@ -8173,9 +8106,7 @@ export class Lowerer { const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++]; const owner = demandOwner(priority); const fn = this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(priority, () => - this.withClosureRoot(name, unit.lower), - ), + this.shapes.withDeclaredOrderPriority(priority, unit.lower), ); if (fn) loweredUnits.set(name, fn); relieveHeapPressure(); @@ -13558,7 +13489,7 @@ export class Lowerer { const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); + fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 62f2be9f1..0f932ba93 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -562,7 +562,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr { const entries = lowerer.ffiImports .filter((entry) => entry.library !== undefined) .map((entry) => { - const name = lowerer.closureName(loc, "", "%ffi.module"); + const name = `%ffi.module.${lowerer.lambdaCounter++}`; const sourceTypes = ffiSourceParamTypes(entry.params); const locals: IrLocal[] = sourceTypes.map((_, i) => ({ id: `%arg${i}`, diff --git a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts index 388ac3904..855da3bda 100644 --- a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts +++ b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { BOOL, STRING, @@ -28,7 +27,7 @@ export function objectEnumerationReceiver( const key = `obj.receiver:${source.unionId}:${expected.shapeId}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%obj.receiver", key); + name = `%obj.receiver.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const value = varRef("value.0", source, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts index acf7b3bf9..3b33fb0cd 100644 --- a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts +++ b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { arrayOf, BOOL, @@ -50,7 +49,7 @@ export function pairsSnapshotHelper( const key = `${options.keyPrefix}.snapshot:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%${options.keyPrefix}.snapshot`, key); + const name = `%${options.keyPrefix}.snapshot.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recordType: IrType = { kind: "record", shapeId }; const pairsType = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts index 84aa8dac0..09d228b05 100644 --- a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { varRef } from "../../ir/build.js"; import { type IrExpr, @@ -41,7 +40,7 @@ export function adaptPromiseExecutor( const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%promise.executor", key); + name = `%promise.executor.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index c8fc9c730..d979423b0 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -9,7 +9,6 @@ * and every reference that can run earlier checks it first — the same * module.tdzCheck the import-cycle bindings use. A reference whose lowering * leaves no expression to check keeps the SC1013 fence. */ -import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -120,7 +119,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${positionTag(lowerer, decl)}`; + const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts deleted file mode 100644 index 8f7d0967e..000000000 --- a/packages/compiler/src/frontend/lowering/stable-names.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; - -/** The owner of the names keyedHelperName hands out. */ -export interface KeyedNameOwner { - /** Each keyed name with a second hash of the key it was made from, which - * tells a repeated key from a different key with the same name. */ - readonly keyedNameKeys: Map; -} - -/** 48 bits of two FNV-1a hashes of `key`, as 12 hex digits. */ -function keyHash(key: string): string { - let a = 0x811c9dc5; - let b = 0x01000193 ^ 0x5bd1e995; - for (let i = 0; i < key.length; i++) { - const c = key.charCodeAt(i); - a = Math.imul(a ^ c, 0x01000193); - b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13); - } - const high = (b >>> 0).toString(16).padStart(8, "0").slice(4); - return high + (a >>> 0).toString(16).padStart(8, "0"); -} - -/** A 32-bit hash of `key` independent of keyHash's bits. */ -function checkHash(key: string): number { - let h = 0x9e3779b9; - for (let i = 0; i < key.length; i++) - h = Math.imul(h ^ key.charCodeAt(i), 0x85ebca6b) ^ (h >>> 15); - return h >>> 0; -} - -/** The name of a helper interned by `key`: ``. It depends only on the key, not on how many helpers were interned - * before, so a helper has the same name whichever unit first needs it. Two - * keys that hash to the same name throw an InternalCompilerError. */ -export function keyedHelperName( - owner: KeyedNameOwner, - prefix: string, - key: string, - separator = ".", -): string { - const name = `${prefix}${separator}${keyHash(key)}`; - const check = checkHash(key); - const previous = owner.keyedNameKeys.get(name); - if (previous === undefined) owner.keyedNameKeys.set(name, check); - else if (previous !== check) - throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); - return name; -} diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 8a87c8111..7b9b6902b 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { boolLit, varRef } from "../../ir/build.js"; import { BOOL, @@ -136,7 +135,7 @@ export function lowerUnionEquality( loc, ); if (!result) return null; - name = keyedHelperName(lowerer, "%value.eq", key); + name = `%value.eq.${lowerer.equalityHelpers.size}`; lowerer.equalityHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts deleted file mode 100644 index ea8535d9e..000000000 --- a/packages/compiler/src/frontend/lowering/structural-positions.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as ts from "../ts7/adapter.js"; -import { keyedHelperName, type KeyedNameOwner } from "./stable-names.js"; - -/** The declaration kinds whose names anchor the positions inside them. */ -const ANCHOR_KINDS: ReadonlySet = new Set([ - ts.SyntaxKind.FunctionDeclaration, - ts.SyntaxKind.ClassDeclaration, - ts.SyntaxKind.ClassExpression, - ts.SyntaxKind.VariableDeclaration, - ts.SyntaxKind.MethodDeclaration, - ts.SyntaxKind.GetAccessor, - ts.SyntaxKind.SetAccessor, - ts.SyntaxKind.PropertyDeclaration, - ts.SyntaxKind.PropertyAssignment, - ts.SyntaxKind.ModuleDeclaration, - ts.SyntaxKind.EnumDeclaration, - ts.SyntaxKind.Constructor, - ts.SyntaxKind.InterfaceDeclaration, - ts.SyntaxKind.TypeAliasDeclaration, -]); - -/** The name of `node` when it is a named declaration that anchors the - * positions of the nodes inside it, or null. */ -function anchorName(node: ts.Node): string | null { - return ts.declarationNameAt(node, node.index, ANCHOR_KINDS); -} - -/** The nearest enclosing anchor of `node` (not `node` itself), or its - * source file. */ -function anchorOf(node: ts.Node): ts.Node { - for (let parent = node.parent; parent !== undefined; parent = parent.parent) { - if (ts.isSourceFile(parent) || anchorName(parent) !== null) return parent; - } - return ts.getSourceFileOfNode(node); -} - -/** Source positions that stay the same when code outside the enclosing - * declaration changes. A node's structural position is its file, the chain - * of enclosing named declarations (each with its kind, its name and its - * ordinal among the declarations of the same kind and name in its own - * anchor), and its ordinal among the nodes of its kind whose nearest anchor - * is the same declaration. Editing another declaration, or code nested in another - * named declaration of the same anchor, leaves the position unchanged. */ -export class StructuralPositions { - /** Per anchor and key: the indices of the nodes with that key whose - * nearest anchor it is, in preorder (anchoredNodes). */ - private readonly nodesByKey = new Map>(); - private readonly paths = new Map(); - - /** The ordinal of `node` among the nodes of `anchor` with its key. */ - private ordinalIn(anchor: ts.Node, node: ts.Node, key: string): number { - let byKey = this.nodesByKey.get(anchor); - if (byKey === undefined) this.nodesByKey.set(anchor, (byKey = new Map())); - let indices = byKey.get(key); - if (indices === undefined) { - indices = ts.anchoredNodes(anchor, key, ANCHOR_KINDS); - byKey.set(key, indices); - } - // Indices increase in preorder. - let low = 0; - let high = indices.length - 1; - while (low <= high) { - const middle = (low + high) >> 1; - const at = indices[middle]!; - if (at === node.index) return middle; - if (at < node.index) low = middle + 1; - else high = middle - 1; - } - return -1; - } - - /** The anchor chain of `anchor` from the file down. */ - private pathOf(anchor: ts.Node): string { - const cached = this.paths.get(anchor); - if (cached !== undefined) return cached; - let path: string; - if (ts.isSourceFile(anchor)) path = anchor.fileName; - else { - const parent = anchorOf(anchor); - // The kind keeps declarations of one name apart: a getter and its - // setter, or a class and the namespace merged with it. - const key = `${anchor.kind}:${anchorName(anchor)}`; - path = `${this.pathOf(parent)}/${key}#${this.ordinalIn(parent, anchor, key)}`; - } - this.paths.set(anchor, path); - return path; - } - - /** The structural position of `node`. */ - of(node: ts.Node): string { - const anchor = anchorOf(node); - const name = anchorName(node); - const key = name === null ? `${node.kind}` : `${node.kind}:${name}`; - return `${this.pathOf(anchor)}|${key}#${this.ordinalIn(anchor, node, key)}`; - } -} - -/** The owner of structural positions and the names made from them. */ -export interface PositionTagOwner extends KeyedNameOwner { - readonly positions: StructuralPositions; -} - -/** A short tag for `node`'s structural position, for names that must stay - * unique per source node (class expressions, namespace blocks). */ -export function positionTag(owner: PositionTagOwner, node: ts.Node): string { - return keyedHelperName(owner, "", owner.positions.of(node), ""); -} diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 0bddd1a80..369213d89 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -47,9 +47,8 @@ import { } from "./ast-guards.generated.js"; import type { Ts7DiagnosticData as Diagnostic } from "./session-schema.generated.js"; import { ModifierFlags, SyntaxKind } from "./enums.js"; -import { AstKind, KIND_NODE_LIST } from "./ast-schema.generated.js"; +import { AstKind } from "./ast-schema.generated.js"; import { PREORDER_LIST } from "./ast-wire.js"; -import { tokenStartOfNode } from "./node-functions.js"; /* ---- everything 7 kept under the same name ---- */ @@ -197,93 +196,6 @@ function walkLinkedPreorder( } } -/** The semantic children of a wire node: list items stand in for their - * list, and attached JSDoc trees are left out (the walkPreorder view). */ -function semanticChildIndices(root: Node, index: number, out: number[]): void { - const wire = root.file.wire; - for (let child = wire.firstChild(index); child !== 0; child = wire.next(child)) { - const kind = wire.kind(child); - if (kind === KIND_NODE_LIST) { - for (let item = wire.firstChild(child); item !== 0; item = wire.next(item)) out.push(item); - } else if (kind !== AstKind.JSDoc) out.push(child); - } -} - -/** The outermost node of `sourceFile` whose token span is exactly - * [start, end), or undefined. Only nodes ending at `end` are - * materialized. */ -export function nodeAtSpan(sourceFile: SourceFile, start: number, end: number): Node | undefined { - const wire = sourceFile.file.wire; - const children: number[] = []; - let index = sourceFile.index; - for (;;) { - children.length = 0; - semanticChildIndices(sourceFile, index, children); - let next = 0; - for (const child of children) { - if (wire.pos(child) > start || wire.end(child) < end) continue; - if (wire.end(child) === end) { - const node = sourceFile.file.node(child); - if (tokenStartOfNode(node) === start) return node; - } - next = child; - break; - } - if (next === 0) return undefined; - index = next; - } -} - -/** The name of the declaration at wire `index` in `root`'s file when its - * kind is in `kinds`: its identifier, private identifier or string literal - * name, "constructor" for a constructor, or null. Reads the wire without - * materializing nodes. */ -export function declarationNameAt( - root: Node, - index: number, - kinds: ReadonlySet, -): string | null { - const wire = root.file.wire; - const kind = wire.kind(index); - if (!kinds.has(kind)) return null; - if (kind === SyntaxKind.Constructor) return "constructor"; - const name = wire.namedChild(index, "name"); - if (name === 0) return null; - const nameKind = wire.kind(name); - if ( - nameKind !== SyntaxKind.Identifier && - nameKind !== SyntaxKind.PrivateIdentifier && - nameKind !== SyntaxKind.StringLiteral - ) - return null; - return wire.text(name) ?? null; -} - -/** The indices, in preorder, of the nodes below `root` that have `key` and - * are not inside a nested named declaration (declarationNameAt with - * `kinds`). A named declaration is keyed `:` and its subtree is - * not visited; other nodes are keyed by their kind. No node is - * materialized. */ -export function anchoredNodes(root: Node, key: string, kinds: ReadonlySet): number[] { - const wire = root.file.wire; - const out: number[] = []; - const children: number[] = []; - const stack: number[] = []; - semanticChildIndices(root, root.index, children); - for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); - while (stack.length > 0) { - const index = stack.pop()!; - const kind = wire.kind(index); - const name = declarationNameAt(root, index, kinds); - if ((name === null ? String(kind) : `${kind}:${name}`) === key) out.push(index); - if (name !== null) continue; - children.length = 0; - semanticChildIndices(root, index, children); - for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); - } - return out; -} - export function getLineAndCharacterOfPosition( sourceFile: SourceFile, position: number, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index 163ba7b9e..cf78554b4 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive, () => "Recursive"); + const union = unions.recursiveRef(recursive); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 79091cadc..a233f0186 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,5 +1,4 @@ import { InternalCompilerError } from "../errors.js"; -import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -253,8 +252,6 @@ function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriori export class ShapeRegistry { /** Existing layouts change only when a recursive placeholder closes. */ revision = 0; - /** Each shape id with a second hash of its key (keyedHelperName). */ - readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); /** Historical emit-order rank of the declaration-order metadata each @@ -273,7 +270,7 @@ export class ShapeRegistry { * writer after all priority candidates have closed. */ private readonly derivedDeclaredOrderFinalizers: (() => void)[] = []; private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0]; - /** All interned shapes in first-seen order. */ + /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */ readonly shapes: IrRecordShape[] = []; /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is * the recursive knot (`interface TreeNode { children: TreeNode[] }`): @@ -393,12 +390,11 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty fields) the outer frame finalizes. `describe` names a new - * placeholder (recursiveTypeName). */ - recursiveRef(t: ts.Type, describe: () => string): string { + * entry (empty fields) the outer frame finalizes. */ + recursiveRef(t: ts.Type): string { let id = this.recIds.get(t); if (id === undefined) { - id = recursivePlaceholderName(this, "r", describe(), (name) => this.byId.has(name)); + id = `r${this.shapes.length}`; const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -467,7 +463,7 @@ export class ShapeRegistry { const key = this.keyOf(fields, tuple, indexValue); let id = this.byKey.get(key); if (id === undefined) { - id = keyedHelperName(this, "r", key, ""); + id = `r${this.shapes.length}`; const shape: IrRecordShape = { id, fields, @@ -545,57 +541,6 @@ export class ShapeRegistry { } } -/** The id of a recursive placeholder described by `description`. Two - * different types can still share a description (recursiveTypeName stops - * after a few levels of type arguments); the later one takes the first free - * name with a numbered suffix. */ -function recursivePlaceholderName( - owner: KeyedNameOwner, - prefix: string, - description: string, - taken: (name: string) => boolean, -): string { - let name = keyedHelperName(owner, prefix, `recursive:${description}`, ""); - for (let n = 1; taken(name); n++) - name = keyedHelperName(owner, prefix, `recursive:${description}#${n}`, ""); - return name; -} - -/** How many levels of type arguments recursiveTypeName describes. */ -const RECURSIVE_NAME_DEPTH = 3; - -/** A recursive checker type's description, which names its placeholder - * whichever frame first reaches it: its printed form, the structural - * position of its first declaration, and the same for its type arguments - * (two instantiations print the same when their type arguments are type - * parameters or classes of the same name from different declarations). */ -function recursiveTypeName( - t: ts.Type, - ctx: Pick, - depth = 0, -): string { - const symbol = t.getAliasSymbol() ?? t.getSymbol(); - const declaration = symbol?.declarations[0]; - const where = declaration - ? (ctx.positionOf?.(declaration) ?? - `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}`) - : ""; - let description = `${ctx.checker.typeToString(t)}@${where}`; - if (depth < RECURSIVE_NAME_DEPTH) { - const aliasArguments = - t.getAliasSymbol() !== undefined ? (t.getAliasTypeArguments() ?? []) : []; - const typeArguments = - aliasArguments.length > 0 - ? aliasArguments - : t.isTypeReference() - ? ctx.checker.getTypeArguments(t) - : []; - if (typeArguments.length > 0) - description += `<${typeArguments.map((arg) => recursiveTypeName(arg, ctx, depth + 1)).join(",")}>`; - } - return description; -} - /** The frontend's union interner — mirrors ShapeRegistry. A union's * canonical identity includes its typeKey-sorted arms and literal * discriminator contract. An arm's index in that list IS its runtime tag. @@ -603,12 +548,10 @@ function recursiveTypeName( export class UnionRegistry { /** Existing arm/discriminator contracts change when a placeholder closes. */ revision = 0; - /** Each union id with a second hash of its key (keyedHelperName). */ - readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); private readonly armTags = new Map>(); - /** All interned unions in first-seen order. */ + /** All interned unions in first-seen (`u0`, `u1`, ...) order. */ readonly unions: IrUnionDef[] = []; /** ts.Types currently being mapped — a back-reference to one is the * recursive knot passing through a union (`type Tree = Leaf | Branch` @@ -624,12 +567,11 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty arms) the outer frame finalizes. `describe` names a new - * placeholder (recursiveTypeName). */ - recursiveRef(t: ts.Type, describe: () => string): string { + * entry (empty arms) the outer frame finalizes. */ + recursiveRef(t: ts.Type): string { let id = this.recIds.get(t); if (id === undefined) { - id = recursivePlaceholderName(this, "u", describe(), (name) => this.byId.has(name)); + id = `u${this.unions.length}`; const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -679,7 +621,7 @@ export class UnionRegistry { const key = JSON.stringify([arms.map(typeKey), discriminant]); let id = this.byKey.get(key); if (id === undefined) { - id = keyedHelperName(this, "u", key, ""); + id = `u${this.unions.length}`; const def: IrUnionDef = { id, arms, ...(discriminant ? { discriminant } : {}) }; this.byKey.set(key, id); this.byId.set(id, def); @@ -1173,10 +1115,6 @@ export interface TypeMapperCtx { * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; checker: ts.TypeChecker; - /** The structural position of a declaration (structural-positions.ts), - * which names recursive placeholders independently of where unrelated - * code sits. The Lowerer supplies it. */ - positionOf?: (node: ts.Node) => string; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). * Absent when the program has none: every mapping is unchanged. */ @@ -4713,10 +4651,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const knownRecursive = unions.recursiveUnionFor(widened); if (knownRecursive !== undefined) return { kind: "union", unionId: knownRecursive }; if (unions.inProgress.has(widened)) { - return { - kind: "union", - unionId: unions.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), - }; + return { kind: "union", unionId: unions.recursiveRef(widened) }; } unions.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; @@ -7060,10 +6995,7 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // frame completes (finalizeRecursive below). Backends represent records // as heap references, so the self-reference is an ordinary pointer. if (shapes.inProgress.has(widened)) { - return { - kind: "record", - shapeId: shapes.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), - }; + return { kind: "record", shapeId: shapes.recursiveRef(widened) }; } shapes.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index 55cd0025e..f5d743265 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -26,7 +26,7 @@ function helperCalls(fn: IrFunction): string[] { everyStmtList(fn.body, { expr: (e) => { if (e.kind === "call" && e.callee.startsWith("%arr.")) - calls.push(e.callee.replace(/\.[0-9a-f]+$/, "")); + calls.push(e.callee.replace(/\.\d+$/, "")); return true; }, stmt: () => true, diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index 6ce9c4bc0..e59e023ca 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,7 +27,7 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.[0-9a-f]+$/, "")) + .map((name) => name.replace(/\.\d+$/, "")) .sort(); expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { diff --git a/packages/compiler/test/in-runtime-key-emission.test.ts b/packages/compiler/test/in-runtime-key-emission.test.ts index 589685a7a..aebbb2ea4 100644 --- a/packages/compiler/test/in-runtime-key-emission.test.ts +++ b/packages/compiler/test/in-runtime-key-emission.test.ts @@ -55,7 +55,7 @@ const d: Record = {}; d["a"] = 1; console.log(has(d, "a"), has(d, "b")); `); - const helper = body(llvm, /sc_b?f__x25_rec_haskey_[0-9a-f]+/); + const helper = body(llvm, /sc_b?f__x25_rec_haskey_\d+/); expect(helper).toContain("@scr_map_has_str("); // Not a scan over a fresh key snapshot per probe. expect(helper).not.toContain("@scr_map_keys_js_order("); diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts deleted file mode 100644 index 720472556..000000000 --- a/packages/compiler/test/stable-names.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { expect, test } from "vitest"; -import { compile, deserializeModule, type IrModule } from "../src/index.js"; - -/** Lowers each source in turn as the same file, so names that include the - * file's path compare equal. */ -async function lower(...sources: string[]): Promise { - const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); - try { - const entry = join(dir, "main.ts"); - const outPath = join(dir, "main.ir.json"); - const modules: IrModule[] = []; - for (const source of sources) { - await writeFile(entry, source); - const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); - if (!result.ok) - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - modules.push(deserializeModule(await readFile(outPath, "utf8"))); - } - return modules; - } finally { - await rm(dir, { recursive: true, force: true }); - } -} - -function names(mod: IrModule): string[] { - return [ - ...mod.records.map((record) => record.id), - ...mod.unions.map((union) => union.id), - ...mod.functions.map((fn) => fn.name), - ...mod.classes.map((cls) => cls.name), - ].sort(); -} - -const recursive = ` -type Tree = { value: number; children: Tree[]; parent: Tree | null }; -function total(tree: Tree): number { - let sum = tree.value; - for (const child of tree.children) sum += total(child); - return sum; -} -const root: Tree = { value: 1, children: [], parent: null }; -root.children.push({ value: 2, children: [], parent: root }); -console.log(total(root)); -`; - -test("recursive record and union ids do not depend on the code above the type", async () => { - const [before, after] = await lower( - recursive, - `// A comment above the type.\n\ntype Unrelated = { name: string };\n\n${recursive}`, - ); - expect(before.records.some((record) => record.fields.some((f) => f.name === "children"))).toBe( - true, - ); - expect(names(after)).toEqual(names(before)); -}); - -test("recursive instantiations that print alike get distinct placeholders", async () => { - const [mod] = await lower(` -namespace A { - export class Item { - a = 1; - } -} -namespace B { - export class Item { - b = "b"; - } -} -type Chain = { item: T; next: Chain | null }; -const first: Chain = { item: new A.Item(), next: null }; -const second: Chain = { item: new B.Item(), next: null }; -first.next = { item: new A.Item(), next: null }; -second.next = { item: new B.Item(), next: null }; -console.log(first.next.item.a, second.next.item.b); -`); - const chains = mod.records.filter((record) => record.fields.some((f) => f.name === "next")); - expect(new Set(chains.map((record) => record.id)).size).toBe(chains.length); - expect(chains.length).toBeGreaterThanOrEqual(2); -}); - -test("a getter and a setter of one name keep their local classes apart", async () => { - const [mod] = await lower(` -class Box { - private v = 0; - get value(): number { - class Local { - n(): number { - return 1; - } - } - return new Local().n() + this.v; - } - set value(x: number) { - class Local { - n(): number { - return 2; - } - } - this.v = x * new Local().n(); - } -} -const box = new Box(); -box.value = 5; -console.log(box.value); -`); - const locals = mod.classes.filter((cls) => cls.name.includes("Local")); - expect(locals).toHaveLength(2); - expect(locals[0]!.name).not.toBe(locals[1]!.name); -}); diff --git a/packages/compiler/test/type-mapper-unions.test.ts b/packages/compiler/test/type-mapper-unions.test.ts index 684960002..c714df4d7 100644 --- a/packages/compiler/test/type-mapper-unions.test.ts +++ b/packages/compiler/test/type-mapper-unions.test.ts @@ -139,8 +139,7 @@ describe("checker and synthesized optional union identity", () => { nullable.arms.filter((arm) => arm.kind !== "nullT" && arm.kind !== "undefinedT"), ), ).toBe(required.unionId); - // Cases follow the arms, which are ordered by their type keys. - expect(nullable.discriminant!.cases.map((entry) => entry.values).sort()).toEqual([ + expect(nullable.discriminant!.cases.map((entry) => entry.values)).toEqual([ ["a", "alias"], ["b"], ]); From e7e943a1d3ffd4a8e44fc40ffcd19b217ba8ad4c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:00:06 -0500 Subject: [PATCH 05/12] Accept only nested scans that highestCount walks pass by pass The first pass of guardedLoopCounterFacts accepted a nested while loop anywhere outside an opaque region, and highestCount then left counters unbounded where it does not walk such a loop. The first pass now accepts the same loops highestCount walks: statements of the body, its blocks and its branches. A loop in a labeled block or among an expression's statements is an ordinary loop region, whose counter writes are refused. --- packages/compiler/src/ir/integer-loop-facts.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/ir/integer-loop-facts.ts b/packages/compiler/src/ir/integer-loop-facts.ts index 31edc2f48..8807212dc 100644 --- a/packages/compiler/src/ir/integer-loop-facts.ts +++ b/packages/compiler/src/ir/integer-loop-facts.ts @@ -276,16 +276,26 @@ export function guardedLoopCounterFacts( else budget.set(id, (budget.get(id) ?? 0) + delta); }; // Nested while loops outside opaque regions, checked against their own - // conditions once the stable bounds are known. + // conditions once the stable bounds are known. Only loops that highestCount + // walks pass by pass qualify: statements of the body, its blocks and its + // branches. One in a labeled block or among an expression's statements + // (`indirect`) is an ordinary loop region. const nested: (IrStmt & { kind: "while" })[] = []; + let indirect = 0; const expr = (e: IrExpr): boolean => { if (e.kind === "assignExpr") write(e.localId, increment(e.localId, e.value)); if (e.kind === "incDec") write(e.localId, e.op === "+" ? 1 : null); - return everyExprChild(e, expr, stmt); + return everyExprChild(e, expr, (x) => { + indirect++; + const completed = stmt(x); + indirect--; + return completed; + }); }; const condition = new Set(); const stmt = (s: IrStmt): boolean => { - const bounded = s.kind === "while" && opaque === 0 && !s.labels?.length; + const bounded = s.kind === "while" && opaque === 0 && indirect === 0 && !s.labels?.length; + const labeled = s.kind === "block" && !!s.labels?.length; const region = !bounded && (s.kind === "for" || @@ -299,10 +309,12 @@ export function guardedLoopCounterFacts( conditionWrites(s.cond); } if (region) opaque++; + if (labeled) indirect++; if (s.kind === "assign") write(s.localId, increment(s.localId, s.value)); if (s.kind === "varDecl" || s.kind === "forOf") write(s.localId, null); if (s.kind === "tryCatch" && s.catchLocalId) write(s.catchLocalId, null); const completed = everyStmtChild(s, expr, stmt); + if (labeled) indirect--; if (region) opaque--; return completed; }; From 491480e1ad3990c5d6f834e6afbd910420a34ee3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:09:43 -0500 Subject: [PATCH 06/12] Convert clamped stores inline and test thread-owned views once in typed-array loops A Uint8ClampedArray store called scr_bytes_to_u8_clamp for every element, which kept loops of clamped stores scalar. The store now computes ToUint8Clamp in line: NaN and values at most 0 store 0, values at least 255 store 255, and values between round through (v + 2^52) - 2^52, which rounds a value in (0, 255) to the nearest integer, ties to even, as the runtime function does. In worker-enabled programs, every typed-array element access tests the view's shared storage and branches between a relaxed atomic access and the ordinary one, so loops over thread-owned views kept one branch per access. A view's shared storage is fixed at construction, like its length, so the typed-array loop version guard now tests it once for each invariant receiver, and the fast copy reads and writes those views directly. In worker programs a loop is versioned for this even when no bound needs a guard. When any receiver is shared, the checked copy runs unchanged. Programs without workers emit the same code as before. Adds a Uint8ClampedArray brighten kernel to the typed-array-kernels workload. --- benchmarks/runtime/README.md | 2 +- .../runtime/workloads/typed-array-kernels.ts | 12 +++ .../backend/llvm/bytes-loop-versions.test.ts | 31 ++++++++ .../src/backend/llvm/bytes-loop-versions.ts | 46 ++++++++++- .../src/backend/llvm/emit-function.ts | 1 + .../src/backend/llvm/emit-statements.ts | 5 ++ packages/compiler/src/backend/llvm/emitter.ts | 11 ++- .../compiler/src/backend/llvm/expr-bytes.ts | 52 ++++++++++--- .../test/bytes-element-emission.test.ts | 42 ++++++++++ .../6994-uint8-clamped-store-rounding.ts.json | 6 ++ ...5-worker-typed-array-loop-versions.ts.json | 6 ++ .../6994-uint8-clamped-store-rounding.ts | 77 +++++++++++++++++++ .../6995-worker-typed-array-loop-versions.ts | 74 ++++++++++++++++++ .../6994-uint8-clamped-store-rounding.ts.json | 1 + ...5-worker-typed-array-loop-versions.ts.json | 1 + 15 files changed, 348 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json create mode 100644 tests/corpus/6994-uint8-clamped-store-rounding.ts create mode 100644 tests/corpus/6995-worker-typed-array-loop-versions.ts create mode 100644 tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json create mode 100644 tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 1acdc9ce7..cbf47675a 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, RGBA to gray | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index 87c227bbc..ba68f7241 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -17,6 +17,10 @@ function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void { for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0; } +function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): void { + for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; +} + function gray(rgba: Uint8Array, out: Uint8Array): void { for (let i = 0; i < out.length; i++) { const j = i * 4; @@ -38,6 +42,8 @@ const masked = new Uint8Array(n); const ia = new Int32Array(n); const ib = new Int32Array(n); const io = new Int32Array(n); +const pixels = new Uint8ClampedArray(n); +const bright = new Uint8ClampedArray(n); const rgba = new Uint8Array(n * 4); const luma = new Uint8Array(n); let state = 2463534242; @@ -56,6 +62,7 @@ for (let i = 0; i < n; i++) { key[i] = next() & 255; ia[i] = next() | 0; ib[i] = next() | 0; + pixels[i] = next() & 255; } for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255; @@ -64,6 +71,7 @@ let f64Sum = 0; let f32Sum = 0; let byteSum = 0; let intSum = 0; +let brightSum = 0; let lumaSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. @@ -72,21 +80,25 @@ for (let r = 0; r < rounds; r++) { fa[at] = r; data[at] = r & 255; ia[at] = r * 65537; + pixels[at] = r & 255; rgba[at * 4] = r & 255; axpy(1.5, x, y, d); blend((r & 15) / 16, fa, fb, fo); mask(data, key, masked); addWrap(ia, ib, io); + brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; f32Sum += fo[probe]!; byteSum += masked[probe]!; intSum = (intSum + io[probe]!) | 0; + brightSum += bright[probe]!; lumaSum += luma[probe]!; } console.log(`float64 ${f64Sum.toFixed(3)}`); console.log(`float32 ${f32Sum.toFixed(3)}`); console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); +console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index ea568bc3f..3fbc01cc5 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -109,6 +109,8 @@ function match( ranges, initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), proven: new Set(), + workers: false, + owned: new Set(), }); } @@ -250,6 +252,35 @@ test("loops to a typed array's length version the other typed arrays they index" expect(rebound).toContain("bytes.index.invalid"); }); +test("worker programs test each receiver's shared storage once in the guard", () => { + // for (let i = 0; i < n; i++) out[i] = src[i] * 2, where either view may + // be shared. + const loop = counted([ + set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))), + ]); + const kernel = fn([loop]); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + workers: true, + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m)); + expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.shared"); + expect(fast).not.toMatch(/(load|store) atomic double/); + // The checked copy keeps the shared-memory path for each access. + const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast)); + expect(slow).toContain("load atomic double"); + expect(slow).toContain("store atomic double"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index df8977426..dcb735e61 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -6,7 +6,7 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; -import { emitBytesHeaderLength } from "./expr-bytes.js"; +import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -41,8 +41,13 @@ interface AffineCheck { export interface BytesLoopVersion { /** Typed-array accesses that are in bounds whenever the guard holds. */ bounds: ReadonlySet; + /** Worker programs: typed-array accesses whose views the guard proves + * thread-owned (no shared storage). */ + owned: ReadonlySet; /** Root receiver bindings, read once before the loop. */ receivers: { ref: VarRef; global: boolean }[]; + /** Receivers the guard requires to have no shared storage. */ + ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; /** The counted loop whose iteration space the affine checks cover. */ @@ -66,6 +71,11 @@ export interface BytesLoopFacts { initialized: ReadonlySet; /** Accesses already proved in bounds without a guard. */ proven: ReadonlySet; + /** Worker programs: an access of a view that may be shared tests the + * view's shared storage first. */ + workers: boolean; + /** Accesses already proved thread-owned by an enclosing version. */ + owned: ReadonlySet; } const MAX_COST = 400; @@ -87,6 +97,11 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * and last values in bounds, such as `out[base + i]` for `i < n` or * `view.getUint32(i * 4)`. * + * In worker programs, every access of a view that may be shared tests its + * shared storage. A view's storage is also fixed at construction, so the + * same guard tests each invariant receiver once, and the fast copy reads + * and writes thread-owned views directly. + * * Only small synchronous loops are versioned (nested loops included), and * the emitter bounds how deeply versions nest. The checked loop runs * whenever the guard fails, and keeps every out-of-bounds read, ignored @@ -314,10 +329,19 @@ export function matchBytesLoopVersion( } return index; }; + // Worker programs: accesses through invariant receivers, proven or not, + // become thread-owned when the guard finds no shared storage. + const owned = new Map(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { - if (facts.proven.has(node)) return; const ref = root(array); if (!ref) return; + if (facts.workers && !facts.owned.has(node)) { + const receiver = receiverOf(ref); + const nodes = owned.get(receiver); + if (nodes) nodes.push(node); + else owned.set(receiver, [node]); + } + if (facts.proven.has(node)) return; const shape = counter !== null ? affine(index) : null; if (shape && shape.counter !== 0) { const operands = new Set(); @@ -383,9 +407,15 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); - if (fixedKept.length === 0 && affineKept.length === 0) return null; + const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); + if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; const bounds = new Set(); + const ownedNodes = new Set(); const used = new Set(); + for (const [receiver, nodes] of ownedKept) { + used.add(receiver); + for (const node of nodes) ownedNodes.add(node); + } const ranges = new Map(); const counterLow = Math.max(counted?.range.min ?? 0, -bound), counterHigh = Math.min(counted?.range.max ?? 0, bound); @@ -420,7 +450,9 @@ export function matchBytesLoopVersion( (counted.range.min < -bound || counted.range.max > bound); return { bounds, + owned: ownedNodes, receivers: order.map((index) => receivers[index]!), + ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!), fixed: fixedKept.map((g) => ({ receiver: renumber.get(g.check.receiver)!, end: g.check.end, @@ -439,7 +471,8 @@ export function matchBytesLoopVersion( } /** Branches to `fast` when every access the version proves is in bounds for - * every iteration, and to `slow` otherwise (including a counted loop whose + * every iteration and every view it proves thread-owned has no shared + * storage, and to `slow` otherwise (including a counted loop whose * integer form is not established, or a module receiver not yet set). */ export function emitBytesLoopVersionGuard( host: LlvmEmitterContext, @@ -473,6 +506,11 @@ export function emitBytesLoopVersionGuard( return wide; }); const conditions: string[] = []; + for (const index of version.ownedReceivers) { + const owned = B.tmp(); + B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`); + conditions.push(owned); + } for (const check of version.fixed) { const ok = B.tmp(); B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index d83b9ff64..5053c1b54 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -228,6 +228,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerLoopRanges = rangeFacts.loopRanges; emitter.int32Elements = slotFacts.elements ?? null; emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); + emitter.bytesOwned = new Set(); // Debug builds keep every number local a double for the debugger's // variable view. Lazily boxed captured parameters keep their double, // which the box is created from, and so do plain copies of a capture or diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 433029651..d2e96994b 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -517,6 +517,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.index, s.value, emitter.bytesBounds.has(s), + emitter.bytesOwned.has(s), ); break; } @@ -970,6 +971,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { done = B.newLabel("bytes.version.done"); emitBytesLoopVersionGuard(emitter, version, fast, slow); const originalBounds = emitter.bytesBounds, + originalOwned = emitter.bytesOwned, originalRanges = emitter.integerRanges; emitter.bytesLoopVersioning.add(s); try { @@ -978,6 +980,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.br(done); B.startBlock(fast); emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.owned.size > 0) + emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); if (version.counted?.guarded) emitter.provenCountedGuards.add(s); @@ -985,6 +989,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.br(done); } finally { emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; emitter.integerRanges = originalRanges; emitter.provenCountedGuards.delete(s); emitter.bytesLoopVersioning.delete(s); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1f2be4439..c254d0907 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1125,6 +1125,9 @@ export class LlEmitter { * guard already passed. */ readonly provenCountedGuards = new Set(); bytesBounds: ReadonlySet = new Set(); + /** Worker programs: typed-array accesses whose views an enclosing version + * guard proved thread-owned. */ + bytesOwned: ReadonlySet = new Set(); integerViews = new Map(); /** Module globals the current function owns outright (initializer * bindings): their function-local slot, which stands in for the global. */ @@ -3836,6 +3839,8 @@ export class LlEmitter { ranges: this.integerRanges, initialized, proven: this.bytesBounds, + workers: this.mod.workers === true, + owned: this.bytesOwned, }); } @@ -6196,8 +6201,9 @@ export class LlEmitter { expr?: IrExpr, inBounds = false, invalidNaN = false, + owned = false, ): LlValue { - return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN); + return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned); } emitToUint32(value: string, expr?: IrExpr, uint32?: string): string { @@ -6260,8 +6266,9 @@ export class LlEmitter { indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, + owned = false, ): void { - return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds); + return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned); } emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue { diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index d36001bf6..062bfdfbe 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -176,6 +176,17 @@ export function emitBytesLength( return { name: out, type: F64 }; } +/** A view's shared backing (null for a buffer this thread owns). It is + * set at construction, before script code can see the view. */ +export function emitBytesSharedStorage(host: LlvmEmitterContext, receiver: string): string { + const B = host.B; + const slot = B.tmp(), + storage = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); + B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`); + return storage; +} + /** Worker programs: an element of a view of a SharedArrayBuffer (`shared` is * fixed before script code can see the view) is read and written with one * relaxed atomic access of its width, as the runtime's shared accessors do; @@ -186,12 +197,9 @@ export function emitBytesLength( function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): string { const B = host.B; host.declare(`declare i1 @llvm.expect.i1(i1, i1)`); - const slot = B.tmp(), - storage = B.tmp(), + const storage = emitBytesSharedStorage(host, receiver), isShared = B.tmp(), expected = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); - B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`); B.line(`${isShared} = icmp ne ptr ${storage}, null`); B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`); const shared = B.newLabel("bytes.shared"), @@ -209,15 +217,17 @@ export function emitBytesGet( expr?: IrExpr, inBounds = false, invalidNaN = false, + owned = false, ): LlValue { const B = host.B; // Any invalid index traps (the array runtime's discipline). - if (!invalidNaN) return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined); + if (!invalidNaN) + return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined, owned); // The numeric view of an ordinary read: an invalid index never touches // storage and reads as NaN — ToNumber of Node's undefined. const invalid = B.newLabel("bytes.read.invalid"); const done = B.newLabel("bytes.read.done"); - const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid); + const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid, owned); const ok = B.newLabel("bytes.read.ok"); B.br(ok); B.startBlock(ok); @@ -260,8 +270,10 @@ function emitBytesLoad( expr: IrExpr | undefined, inBounds: boolean, invalid: string | undefined, + owned: boolean, ): LlValue { - if (host.mod.workers) { + // `owned`: an enclosing version guard proved the view thread-owned. + if (host.mod.workers && !owned) { const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds); return emitSharedOrLocalLoad(host, elem, receiver, idx); } @@ -515,13 +527,14 @@ export function emitBytesSet( indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, + owned = false, ): void { const B = host.B; // Invalid integer-indexed writes are ignored after evaluating the RHS. const done = B.newLabel("bytes.store.done"); const data = host.emitBytesData(receiver); const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds); - if (host.mod.workers && elem === "f16") { + if (host.mod.workers && !owned && elem === "f16") { host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`); const local = emitSharedBytesSplit(host, receiver); B.line( @@ -532,10 +545,24 @@ export function emitBytesSet( } let stored: string | null; if (elem === "u8c") { - host.declare(`declare double @scr_bytes_to_u8_clamp(double)`); - const clamped = B.tmp(); + // ToUint8Clamp, as scr_bytes_to_u8_clamp computes it: NaN and values + // at most 0 store 0, values at least 255 store 255, and values between + // round to the nearest integer, ties to even. Adding and subtracting + // 2^52 rounds a value in (0, 255) that way in the default rounding mode, + // with no call or branch, so loops of clamped stores can vectorize. + const positive = B.tmp(), + high = B.tmp(), + shifted = B.tmp(), + rounded = B.tmp(), + upper = B.tmp(), + clamped = B.tmp(); stored = B.tmp(); - B.line(`${clamped} = call double @scr_bytes_to_u8_clamp(double ${value.name})`); + B.line(`${positive} = fcmp ogt double ${value.name}, 0.0`); + B.line(`${high} = fcmp oge double ${value.name}, 255.0`); + B.line(`${shifted} = fadd double ${value.name}, 0x4330000000000000`); + B.line(`${rounded} = fsub double ${shifted}, 0x4330000000000000`); + B.line(`${upper} = select i1 ${high}, double 255.0, double ${rounded}`); + B.line(`${clamped} = select i1 ${positive}, double ${upper}, double 0.0`); B.line(`${stored} = fptoui double ${clamped} to i32`); } else stored = @@ -572,7 +599,7 @@ export function emitBytesSet( ty = "i32"; operand = stored!; } - if (host.mod.workers) { + if (host.mod.workers && !owned) { // A shared view's element takes a relaxed atomic store of its width. const local = emitSharedBytesSplit(host, receiver); const p = B.tmp(); @@ -721,6 +748,7 @@ export function emitBytesIntrinsic( e.args[0], host.bytesBounds.has(e), e.invalidNaN === true, + host.bytesOwned.has(e), ); case "slice": return call( diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 505b51963..c0a78a898 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -406,3 +406,45 @@ test("large record clones stay outlined while small clones remain inlineable", ( expect(large).toContain("define internal ptr @sc_rclone_large(ptr %src) #2"); expect(large).toContain("attributes #2 = { noinline sanitize_address }"); }); + +test("Uint8ClampedArray stores clamp and round without a runtime call", () => { + const type = bytesOf("u8c"); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + { + name: "store", + params: [ + { localId: "px", name: "px", type }, + { localId: "v", name: "v", type: F64 }, + ], + returnType: VOID, + locals: [ + { id: "px", name: "px", type, mutable: false }, + { id: "v", name: "v", type: F64, mutable: false }, + ], + body: [ + { + kind: "bytesSet", + arr: { kind: "varRef", localId: "px", type, loc }, + index: { kind: "numLit", value: 0, type: F64, loc }, + value: { kind: "varRef", localId: "v", type: F64, loc }, + loc, + }, + ], + loc, + }, + ], + }; + expect(validateModule(mod)).toEqual([]); + const body = /^define internal [^\n]*@sc_bf_store[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + expect(body).not.toContain("scr_bytes_to_u8_clamp"); + expect(body).toMatch(/fcmp ogt double %\S+, 0\.0/); + expect(body).toMatch(/fcmp oge double %\S+, 255\.0/); + expect(body).toMatch(/fadd double %\S+, 0x4330000000000000/); + expect(body).toMatch(/fsub double %\S+, 0x4330000000000000/); + expect(body).toMatch(/store i8 [^\n]*, align 1/); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json new file mode 100644 index 000000000..f5a6b8742 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6994-uint8-clamped-store-rounding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json new file mode 100644 index 000000000..8078248f5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6995-worker-typed-array-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6994-uint8-clamped-store-rounding.ts b/tests/corpus/6994-uint8-clamped-store-rounding.ts new file mode 100644 index 000000000..5fc3295da --- /dev/null +++ b/tests/corpus/6994-uint8-clamped-store-rounding.ts @@ -0,0 +1,77 @@ +// Uint8ClampedArray stores: NaN and values at most 0 store 0, values at +// least 255 store 255, and values between round to the nearest integer, +// ties to even. Covers single stores and loops, every tie between 0 and +// 255 with the doubles on either side of it, and values far outside. + +/** The largest power of two at most `x` (x > 0). */ +function floorPower(x: number): number { + let p = 1; + while (p * 2 <= x) p *= 2; + while (p > x) p /= 2; + return p; +} + +/** The doubles just above and below `x` (x >= 0). */ +function nextUp(x: number): number { + return x === 0 ? 5e-324 : x + floorPower(x) * 2 ** -52; +} + +function nextDown(x: number): number { + if (x === 0) return -5e-324; + const p = floorPower(x); + return x - (p === x ? p * 2 ** -53 : p * 2 ** -52); +} + +function storeAll(values: Float64Array, out: Uint8ClampedArray): void { + const n = out.length; + for (let i = 0; i < n; i++) out[i] = values[i]!; +} + +function scale(px: Uint8ClampedArray, k: number): void { + const n = px.length; + for (let i = 0; i < n; i++) px[i] = px[i]! * k; +} + +const special = [ + NaN, -NaN, Infinity, -Infinity, 0, -0, 5e-324, -5e-324, 2.2250738585072014e-308, 0.49999999999999994, + 0.5, 0.5000000000000001, 1, 1.5, 2.5, 3.5, 127.5, 128.5, 253.5, 254.49999999999997, 254.5, + 254.50000000000003, 254.99999999999997, 255, 255.00000000000003, 255.5, 256, -1, -0.5, -0.4, + 1e300, -1e300, 2 ** 31, 2 ** 32 + 0.5, 2 ** 52, 2 ** 52 + 1, 2 ** 53, -(2 ** 52), +]; +const one = new Uint8ClampedArray(1); +const singles: number[] = []; +for (const v of special) { + one[0] = v; + singles.push(one[0]!); +} +console.log("single", singles.join(",")); + +const ties: number[] = []; +for (let k = 0; k <= 255; k++) { + ties.push(k + 0.5, nextDown(k + 0.5), nextUp(k + 0.5), k, nextDown(k), nextUp(k)); +} +const values = new Float64Array(ties.length + special.length); +values.set(ties); +values.set(special, ties.length); +const stored = new Uint8ClampedArray(values.length); +storeAll(values, stored); +const expected = new Uint8ClampedArray(values.length); +for (let i = 0; i < values.length; i++) expected[i] = values[i]!; +let mismatches = 0; +for (let i = 0; i < values.length; i++) if (stored[i] !== expected[i]) mismatches++; +console.log("loop", values.length, "mismatches", mismatches); +let digest = 0; +for (let i = 0; i < stored.length; i++) digest = (Math.imul(digest, 31) + stored[i]!) | 0; +console.log("digest", digest, stored.subarray(0, 24).join(",")); +console.log("tail", stored.subarray(ties.length).join(",")); + +const px = new Uint8ClampedArray(64); +for (let i = 0; i < px.length; i++) px[i] = i * 4.25; +scale(px, 1.37); +console.log("scaled", px.join(",")); +scale(px, -1); +console.log("negated", px.join(",")); +const fromInts = new Uint8ClampedArray(8); +const ints = new Int32Array([-300, -1, 0, 1, 254, 255, 256, 2147483647]); +for (let i = 0; i < fromInts.length; i++) fromInts[i] = ints[i]!; +console.log("ints", fromInts.join(",")); diff --git a/tests/corpus/6995-worker-typed-array-loop-versions.ts b/tests/corpus/6995-worker-typed-array-loop-versions.ts new file mode 100644 index 000000000..5b85271b9 --- /dev/null +++ b/tests/corpus/6995-worker-typed-array-loop-versions.ts @@ -0,0 +1,74 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed-array loops in a worker-enabled program, over views of a thread's +// own ArrayBuffer, views of a SharedArrayBuffer, and both in one loop. Each +// loop gives the same results whichever kind of buffer its views use, and +// writes through shared views are visible to the other thread. + +function scale(src: Float64Array, dst: Float32Array, k: number): void { + for (let i = 0; i < dst.length; i++) dst[i] = src[i]! * k; +} + +function countAbove(view: Int16Array, limit: number): number { + let n = 0; + for (let i = 0; i < view.length; i++) if (view[i]! > limit) n++; + return n; +} + +function mix(a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): void { + const n = out.length; + for (let i = 0; i < n; i++) out[i] = a[i]! * 2 - b[i]!; +} + +function bump(view: Int32Array, by: number): void { + for (let i = 0; i < view.length; i++) view[i] = (view[i]! + by) | 0; +} + +function report(label: string, src: Float64Array, dst: Float32Array, ints: Int16Array, a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): string { + for (let i = 0; i < src.length; i++) src[i] = i * 1.5 - 4; + for (let i = 0; i < ints.length; i++) ints[i] = (i * 7919) % 2001 - 1000; + for (let i = 0; i < a.length; i++) { + a[i] = i * 37; + b[i] = 255 - i * 11; + } + scale(src, dst, 0.1); + mix(a, b, out); + return `${label}: ${dst.join(",")} | ${countAbove(ints, 250)} ${countAbove(ints.subarray(3), -1)} | ${out.join(",")}`; +} + +function views(buffer: ArrayBuffer | SharedArrayBuffer, other: ArrayBuffer | SharedArrayBuffer): string { + const src = new Float64Array(buffer, 0, 12); + const dst = new Float32Array(other, 0, 10); + const ints = new Int16Array(buffer, 96, 40); + const a = new Uint8Array(other, 40, 16); + const b = new Uint8Array(buffer, 176, 16); + const out = new Uint8ClampedArray(other, 56, 16); + return report("pair", src, dst, ints, a, b, out); +} + +if (isMainThread) { + // Bytes 224-255 of the shared buffer hold counters the views never use. + const shared = new SharedArrayBuffer(256); + console.log(views(new ArrayBuffer(256), new ArrayBuffer(256))); + console.log(views(shared, new ArrayBuffer(256))); + console.log(views(new ArrayBuffer(256), shared)); + console.log(views(shared, new SharedArrayBuffer(256))); + const own = new Int32Array(8); + bump(own, 5); + bump(new Int32Array(shared, 224, 8), 1); + const worker = new Worker(new URL(import.meta.url), { workerData: shared }); + worker.on("message", (line: string) => console.log(line)); + worker.on("exit", (code: number) => { + const seen = new Int32Array(shared, 224, 8); + bump(seen, 1000); + console.log(`exit ${code}: ${seen.join(",")} own ${own.join(",")}`); + console.log(`dst after worker: ${new Float32Array(shared, 0, 10).join(",")}`); + }); +} else { + const shared = workerData as SharedArrayBuffer; + const counters = new Int32Array(shared, 224, 8); + bump(counters, 10); + bump(counters.subarray(2, 5), -3); + parentPort!.postMessage(`worker: ${counters.join(",")}`); + parentPort!.postMessage(views(new ArrayBuffer(256), shared)); +} diff --git a/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json b/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json new file mode 100644 index 000000000..a52f2c454 --- /dev/null +++ b/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3300 } diff --git a/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json b/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json new file mode 100644 index 000000000..c0aef3bb7 --- /dev/null +++ b/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 3900 } From 16cf12c20d66aa23a18308446871e7d55dc18d01 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:11:54 -0500 Subject: [PATCH 07/12] Run two-counter for loops on integer counters `for (let lo = a, hi = b; lo < hi; lo++, hi--)` kept both counters as doubles: the counted-loop match takes a single counter, so every `a[lo]`/`a[hi]` converted and tested its index. A loop whose two counters close in on each other by positive literal steps, are not written by the body and index something now stores both as i64: on every body entry both lie between the first start and the second. A start without a proven integer range is tested at run time on a versioned loop, as a counted loop's is. --- .../src/backend/llvm/counted-loops.ts | 81 ++++++++----- .../src/backend/llvm/emit-statements.ts | 58 ++++++++- .../compiler/src/ir/integer-loops.test.ts | 73 ++++++++++- packages/compiler/src/ir/integer-loops.ts | 114 ++++++++++++++++++ .../corpus/7140-paired-loop-counters.ts.json | 6 + tests/corpus/7140-paired-loop-counters.ts | 81 +++++++++++++ .../7140-paired-loop-counters.ts.json | 1 + 7 files changed, 377 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json create mode 100644 tests/corpus/7140-paired-loop-counters.ts create mode 100644 tests/harness/corpus-costs/7140-paired-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 36fc63e1f..02943b852 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,4 +1,5 @@ -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; +import type { IrExpr } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -59,37 +60,51 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) { - // An exact integer of at most 2^53 - 1 in magnitude, and not -0 - // (the bit patterns of the value and its round trip agree). - // NaN and infinities fail the ordered range tests. - const start = host.emitExpr(loop.start); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - wide = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${wide} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${wide} to double`); - B.line(`${bits} = bitcast double ${start.name} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - if (safe === "true") safe = ok; - else { - const both = B.tmp(); - B.line(`${both} = and i1 ${safe}, ${ok}`); - safe = both; - } - } + if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); return safe; } + +/** The run-time test of a guarded paired loop: each unproven start is an + * exact safe integer. */ +export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { + let safe = "true"; + for (const counter of [loop.low, loop.high]) + if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start)); + return safe; +} + +function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { + if (safe === "true") return ok; + const both = host.B.tmp(); + host.B.line(`${both} = and i1 ${safe}, ${ok}`); + return both; +} + +/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit + * patterns of the value and its round trip agree). NaN and infinities fail + * the ordered range tests. */ +function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { + const B = host.B; + const start = host.emitExpr(expr); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + wide = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${wide} to double`); + B.line(`${bits} = bitcast double ${start.name} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return ok; +} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 433029651..cf83fc945 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -42,10 +42,15 @@ import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, + matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; -import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; +import { + emitCountedLoopLimit, + emitCountedLoopSafety, + emitPairedLoopSafety, +} from "./counted-loops.js"; import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -865,6 +870,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.unaliasedArrays, ) : null; + // Two counters closing in on each other (`lo++, hi--`) take the same + // integer storage as a counted loop's one. + const pairedLoop = + !integerLoop && !countedLoop && emitter.countedLoopsEnabled + ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) + : null; const window = countedLoop && emitter.byteWindowsEnabled ? matchByteWindow( @@ -993,8 +1004,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } let countedJoin: string | null = null; - if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) { - const safe = emitCountedLoopSafety(emitter, countedLoop); + if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { + const safe = countedLoop + ? emitCountedLoopSafety(emitter, countedLoop) + : emitPairedLoopSafety(emitter, pairedLoop!); const fast = B.newLabel("counted.fast"); const slow = B.newLabel("counted.slow"); countedJoin = B.newLabel("counted.done"); @@ -1009,6 +1022,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.scopes.push([]); let integerSlot: string | null = null; let countedLimit: string | null = null; + const pairedSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1053,6 +1067,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); + } else if (pairedLoop) { + for (const counter of [pairedLoop.low, pairedLoop.high]) { + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, + ); + const value = emitter.emitExpr(counter.start); + const start = B.tmp(); + B.line(`${start} = fptosi double ${value.name} to i64`); + B.line(`store i64 ${start}, ptr ${slot}`); + pairedSlots.push(slot); + emitter.integerLoopBindings.set(counter.localId, { + slot, + type: "i64", + signed: true, + range: pairedLoop.range, + }); + } } else if (s.init) { // A multi-declarator head shares the loop's scope. Emitting its // IR block as an ordinary block would release captured/ref locals @@ -1134,6 +1166,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, ); B.condBr(inBounds, lb, exit); + } else if (pairedLoop) { + const low = B.tmp(), + high = B.tmp(), + open = B.tmp(); + B.line(`${low} = load i64, ptr ${pairedSlots[0]}`); + B.line(`${high} = load i64, ptr ${pairedSlots[1]}`); + B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`); + B.condBr(open, lb, exit); } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); else B.br(lb); B.startBlock(lb); @@ -1165,6 +1205,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); + } else if (pairedLoop) { + [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { + const old = B.tmp(); + const next = B.tmp(); + B.line(`${old} = load i64, ptr ${pairedSlots[i]}`); + B.line(`${next} = add nsw i64 ${old}, ${counter.step}`); + B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); + }); } else if (s.update) emitter.emitStmt(s.update); B.br(lc); } @@ -1173,6 +1221,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (pairedLoop) { + emitter.integerLoopBindings.delete(pairedLoop.low.localId); + emitter.integerLoopBindings.delete(pairedLoop.high.localId); + } if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); if (countedJoin) { B.br(countedJoin); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index aada3b053..24bbf5be0 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -1,6 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js"; -import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js"; +import { + matchIntegerArrayForLoop, + matchIntegerCountedForLoop, + matchIntegerPairedForLoop, +} from "./integer-loops.js"; const loc = { file: "integer-array-loop.ts", start: 0, end: 0 }; const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); @@ -355,3 +359,70 @@ test("a descending loop from an array's last index takes an integer counter for range: { min: 0, max: 4294967294 }, }); }); + +test("paired counters closing in on each other run as integers", () => { + const vars = new Map(locals); + for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true }); + const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, + loc, + }); + const element = (index: IrExpr): IrExpr => ({ + kind: "arrayGet", + arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc }, + index, + type: F64, + loc, + }); + const paired = ( + lowStart: IrExpr, + highStart: IrExpr, + update = [step("lo", "+"), step("hi", "-")], + ) => + ({ + kind: "for", + loc, + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo", init: lowStart, loc }, + { kind: "varDecl", localId: "hi", init: highStart, loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc }, + update: { kind: "block", body: update, loc }, + body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }], + }) as IrStmt & { kind: "for" }; + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({ + low: { localId: "lo", step: 1, startGuarded: false }, + high: { localId: "hi", step: -1, startGuarded: false }, + guarded: false, + range: { min: 0, max: 9 }, + }); + // A start without a known range is tested at run time. + expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ + high: { startGuarded: true }, + guarded: true, + }); + // Counters moving the same way, written by the body, captured, started + // from each other, or never used as an index are refused. + expect( + matchIntegerPairedForLoop( + paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]), + vars, + ), + ).toBeNull(); + const writing = paired(number(0), number(9)); + writing.body.push(step("hi", "-", 2)); + expect(matchIntegerPairedForLoop(writing, vars)).toBeNull(); + const boxed = new Map(vars); + boxed.set("hi", { ...vars.get("hi")!, boxed: true }); + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull(); + expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull(); + const unused = paired(number(0), number(9)); + unused.body = []; + expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 9b4675685..e80680d6e 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -281,6 +281,120 @@ export function matchIntegerCountedForLoop( }; } +/** A for loop whose two counters close in on each other: + * + * for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... } + * + * The first counter only grows and the second only shrinks, by positive + * literal steps, the body writes neither, and the loop runs while the first + * is below (or at most) the second. Every body entry therefore holds both + * between the first start and the second start: exact integers when the + * starts are. A start without a proven integer range is tested at run time + * on a versioned loop, as a counted loop's is. */ +export interface IntegerPairedCounter { + localId: string; + start: IrExpr; + /** The signed step of each update. */ + step: number; + startGuarded: boolean; +} +export interface IntegerPairedForLoop { + low: IntegerPairedCounter; + high: IntegerPairedCounter; + inclusive: boolean; + guarded: boolean; + /** Both counters' values on every body entry. */ + range: IntegerRange; +} + +export function matchIntegerPairedForLoop( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, + ranges: IntegerRanges = new Map(), +): IntegerPairedForLoop | null { + const init = stmt.init, + update = stmt.update, + cond = stmt.cond; + if (init?.kind !== "block" || init.body.length !== 2) return null; + if (update?.kind !== "block" || update.body.length !== 2) return null; + if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const lowId = cond.left.localId, + highId = cond.right.localId; + if (lowId === highId) return null; + const counter = (id: string): IntegerPairedCounter | null => { + const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); + if (decl?.kind !== "varDecl" || !decl.init) return null; + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + const start = decl.init; + // Each start is evaluated once in the head and again on a fallback path. + if ( + !stableLoopValue(start, locals, stmt.body, false, lowId) || + !stableLoopValue(start, locals, stmt.body, false, highId) + ) + return null; + const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null); + if (steps.length !== 1 || writesLocal(stmt.body, id)) return null; + const known = + start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) + ? { min: start.value, max: start.value } + : ranges.get(start); + const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; + if (!known && !startGuarded) return null; + if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER)) + return null; + return { localId: id, start, step: steps[0]!, startGuarded }; + }; + const low = counter(lowId), + high = counter(highId); + if (!low || !high || low.step <= 0 || high.step >= 0) return null; + const startRange = (c: IntegerPairedCounter): IntegerRange => + c.startGuarded + ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } + : c.start.kind === "numLit" + ? { min: c.start.value, max: c.start.value } + : ranges.get(c.start)!; + const guarded = low.startGuarded || high.startGuarded; + // Only a loop whose counters index something gains from integer storage. + let useful = false, + cost = 0; + const isCounter = (e: IrExpr): boolean => + (e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) || + (e.kind === "bin" && + (e.op === "+" || e.op === "-") && + (isCounter(e.left) || isCounter(e.right))); + const expr = (e: IrExpr): boolean => { + if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; + if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) + useful = true; + if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) + useful = true; + return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt); + }; + const bodyStmt = (s: IrStmt): boolean => { + if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true; + if ( + guarded && + (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") + ) + return false; + return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); + }; + if (!stmt.body.every(bodyStmt) || !useful) return null; + return { + low, + high, + inclusive: cond.op === "<=", + guarded, + // A loop whose starts already fail the condition never enters the body. + range: { + min: startRange(low).min, + max: Math.max(startRange(low).min, startRange(high).max), + }, + }; +} + /** True when a lowered subtree writes `localId`. Local ids are unique per * function, so typed traversal includes writes * nested in expressions, branches, nested loops, and try/finally bodies. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json new file mode 100644 index 000000000..122d64bb6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7140-paired-loop-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7140-paired-loop-counters.ts b/tests/corpus/7140-paired-loop-counters.ts new file mode 100644 index 000000000..ec1610d1a --- /dev/null +++ b/tests/corpus/7140-paired-loop-counters.ts @@ -0,0 +1,81 @@ +// For loops with two counters closing in on each other (`lo++, hi--`): +// in-place reversals, inclusive bounds, larger steps, early exits, starts +// that are fractions, negative zero, NaN, huge or already crossed, nested +// reversals, and counters a closure captures or the body writes. +function reverse(a: number[], from: number, to: number): number[] { + for (let lo = from, hi = to; lo < hi; lo++, hi--) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +// Unproven starts: fractions, negative zero, NaN and huge values keep JS +// semantics (charCodeAt truncates its index; out-of-range reads are NaN). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + return out.join(","); +} +const word = "abcdefgh"; +console.log(codes(word, 0.5, 4.5)); +console.log(codes(word, -0, 3), Object.is(-0 + 0, 0)); +console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53)); +console.log(codes(word, -3, 2)); + +function pairs(n: number): string { + const out: string[] = []; + for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`); + return out.join(" "); +} +console.log(pairs(20), "|", pairs(0), "|", pairs(-1)); + +function firstGap(a: number[]): number { + let found = -1; + for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) { + if (a[lo] === a[hi]) continue; + found = lo; + break; + } + return found; +} +console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([])); + +// The flip loop of a permutation puzzle: the next index comes from the array. +function flips(perm: number[]): number { + const work = perm.slice(); + let count = 0; + let head = work[0]!; + while (head !== 0) { + for (let lo = 0, hi = head; lo < hi; lo++, hi--) { + const t = work[lo]!; + work[lo] = work[hi]!; + work[hi] = t; + } + count++; + head = work[0]!; + } + return count; +} +console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2])); + +// Counters a closure reads, or the body writes, stay ordinary bindings. +const readers: (() => number)[] = []; +for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi); +console.log(readers.map((f) => f()).join(",")); +const seen: number[] = []; +for (let lo = 0, hi = 10; lo < hi; lo++, hi--) { + seen.push(lo, hi); + if (lo === 2) hi -= 3; +} +console.log(seen.join(",")); +const text = "abcdefgh"; +let mirrored = ""; +for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo); +console.log(mirrored); diff --git a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json new file mode 100644 index 000000000..549fd6a4a --- /dev/null +++ b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3880, "san": 6000 } From d1ff061833279865ddff6cba84b4f2d5dd362ebc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:48:13 -0500 Subject: [PATCH 08/12] Revert "Run two-counter for loops on integer counters" This reverts #1768 (8fec4484d), which made main CI fail. --- .../src/backend/llvm/counted-loops.ts | 81 +++++-------- .../src/backend/llvm/emit-statements.ts | 58 +-------- .../compiler/src/ir/integer-loops.test.ts | 73 +---------- packages/compiler/src/ir/integer-loops.ts | 114 ------------------ .../corpus/7140-paired-loop-counters.ts.json | 6 - tests/corpus/7140-paired-loop-counters.ts | 81 ------------- .../7140-paired-loop-counters.ts.json | 1 - 7 files changed, 37 insertions(+), 377 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json delete mode 100644 tests/corpus/7140-paired-loop-counters.ts delete mode 100644 tests/harness/corpus-costs/7140-paired-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 02943b852..36fc63e1f 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,5 +1,4 @@ -import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; -import type { IrExpr } from "../../ir/ir.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -60,51 +59,37 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); + if (loop.startGuarded) { + // An exact integer of at most 2^53 - 1 in magnitude, and not -0 + // (the bit patterns of the value and its round trip agree). + // NaN and infinities fail the ordered range tests. + const start = host.emitExpr(loop.start); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + wide = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${wide} to double`); + B.line(`${bits} = bitcast double ${start.name} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + if (safe === "true") safe = ok; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${safe}, ${ok}`); + safe = both; + } + } return safe; } - -/** The run-time test of a guarded paired loop: each unproven start is an - * exact safe integer. */ -export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { - let safe = "true"; - for (const counter of [loop.low, loop.high]) - if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start)); - return safe; -} - -function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { - if (safe === "true") return ok; - const both = host.B.tmp(); - host.B.line(`${both} = and i1 ${safe}, ${ok}`); - return both; -} - -/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit - * patterns of the value and its round trip agree). NaN and infinities fail - * the ordered range tests. */ -function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { - const B = host.B; - const start = host.emitExpr(expr); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - wide = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${wide} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${wide} to double`); - B.line(`${bits} = bitcast double ${start.name} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - return ok; -} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index e15de353a..d2e96994b 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -42,15 +42,10 @@ import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, - matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; -import { - emitCountedLoopLimit, - emitCountedLoopSafety, - emitPairedLoopSafety, -} from "./counted-loops.js"; +import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -871,12 +866,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.unaliasedArrays, ) : null; - // Two counters closing in on each other (`lo++, hi--`) take the same - // integer storage as a counted loop's one. - const pairedLoop = - !integerLoop && !countedLoop && emitter.countedLoopsEnabled - ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) - : null; const window = countedLoop && emitter.byteWindowsEnabled ? matchByteWindow( @@ -1009,10 +998,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } let countedJoin: string | null = null; - if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { - const safe = countedLoop - ? emitCountedLoopSafety(emitter, countedLoop) - : emitPairedLoopSafety(emitter, pairedLoop!); + if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) { + const safe = emitCountedLoopSafety(emitter, countedLoop); const fast = B.newLabel("counted.fast"); const slow = B.newLabel("counted.slow"); countedJoin = B.newLabel("counted.done"); @@ -1027,7 +1014,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.scopes.push([]); let integerSlot: string | null = null; let countedLimit: string | null = null; - const pairedSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1072,24 +1058,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); - } else if (pairedLoop) { - for (const counter of [pairedLoop.low, pairedLoop.high]) { - const slot = B.slot(); - B.entryAllocas.push( - `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, - ); - const value = emitter.emitExpr(counter.start); - const start = B.tmp(); - B.line(`${start} = fptosi double ${value.name} to i64`); - B.line(`store i64 ${start}, ptr ${slot}`); - pairedSlots.push(slot); - emitter.integerLoopBindings.set(counter.localId, { - slot, - type: "i64", - signed: true, - range: pairedLoop.range, - }); - } } else if (s.init) { // A multi-declarator head shares the loop's scope. Emitting its // IR block as an ordinary block would release captured/ref locals @@ -1171,14 +1139,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, ); B.condBr(inBounds, lb, exit); - } else if (pairedLoop) { - const low = B.tmp(), - high = B.tmp(), - open = B.tmp(); - B.line(`${low} = load i64, ptr ${pairedSlots[0]}`); - B.line(`${high} = load i64, ptr ${pairedSlots[1]}`); - B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`); - B.condBr(open, lb, exit); } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); else B.br(lb); B.startBlock(lb); @@ -1210,14 +1170,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); - } else if (pairedLoop) { - [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { - const old = B.tmp(); - const next = B.tmp(); - B.line(`${old} = load i64, ptr ${pairedSlots[i]}`); - B.line(`${next} = add nsw i64 ${old}, ${counter.step}`); - B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); - }); } else if (s.update) emitter.emitStmt(s.update); B.br(lc); } @@ -1226,10 +1178,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); - if (pairedLoop) { - emitter.integerLoopBindings.delete(pairedLoop.low.localId); - emitter.integerLoopBindings.delete(pairedLoop.high.localId); - } if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); if (countedJoin) { B.br(countedJoin); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 24bbf5be0..aada3b053 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -1,10 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js"; -import { - matchIntegerArrayForLoop, - matchIntegerCountedForLoop, - matchIntegerPairedForLoop, -} from "./integer-loops.js"; +import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js"; const loc = { file: "integer-array-loop.ts", start: 0, end: 0 }; const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); @@ -359,70 +355,3 @@ test("a descending loop from an array's last index takes an integer counter for range: { min: 0, max: 4294967294 }, }); }); - -test("paired counters closing in on each other run as integers", () => { - const vars = new Map(locals); - for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true }); - const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ - kind: "assign", - localId: id, - value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, - loc, - }); - const element = (index: IrExpr): IrExpr => ({ - kind: "arrayGet", - arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc }, - index, - type: F64, - loc, - }); - const paired = ( - lowStart: IrExpr, - highStart: IrExpr, - update = [step("lo", "+"), step("hi", "-")], - ) => - ({ - kind: "for", - loc, - init: { - kind: "block", - body: [ - { kind: "varDecl", localId: "lo", init: lowStart, loc }, - { kind: "varDecl", localId: "hi", init: highStart, loc }, - ], - loc, - }, - cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc }, - update: { kind: "block", body: update, loc }, - body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }], - }) as IrStmt & { kind: "for" }; - expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({ - low: { localId: "lo", step: 1, startGuarded: false }, - high: { localId: "hi", step: -1, startGuarded: false }, - guarded: false, - range: { min: 0, max: 9 }, - }); - // A start without a known range is tested at run time. - expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ - high: { startGuarded: true }, - guarded: true, - }); - // Counters moving the same way, written by the body, captured, started - // from each other, or never used as an index are refused. - expect( - matchIntegerPairedForLoop( - paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]), - vars, - ), - ).toBeNull(); - const writing = paired(number(0), number(9)); - writing.body.push(step("hi", "-", 2)); - expect(matchIntegerPairedForLoop(writing, vars)).toBeNull(); - const boxed = new Map(vars); - boxed.set("hi", { ...vars.get("hi")!, boxed: true }); - expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull(); - expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull(); - const unused = paired(number(0), number(9)); - unused.body = []; - expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); -}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index e80680d6e..9b4675685 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -281,120 +281,6 @@ export function matchIntegerCountedForLoop( }; } -/** A for loop whose two counters close in on each other: - * - * for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... } - * - * The first counter only grows and the second only shrinks, by positive - * literal steps, the body writes neither, and the loop runs while the first - * is below (or at most) the second. Every body entry therefore holds both - * between the first start and the second start: exact integers when the - * starts are. A start without a proven integer range is tested at run time - * on a versioned loop, as a counted loop's is. */ -export interface IntegerPairedCounter { - localId: string; - start: IrExpr; - /** The signed step of each update. */ - step: number; - startGuarded: boolean; -} -export interface IntegerPairedForLoop { - low: IntegerPairedCounter; - high: IntegerPairedCounter; - inclusive: boolean; - guarded: boolean; - /** Both counters' values on every body entry. */ - range: IntegerRange; -} - -export function matchIntegerPairedForLoop( - stmt: IrStmt & { kind: "for" }, - locals: ReadonlyMap, - ranges: IntegerRanges = new Map(), -): IntegerPairedForLoop | null { - const init = stmt.init, - update = stmt.update, - cond = stmt.cond; - if (init?.kind !== "block" || init.body.length !== 2) return null; - if (update?.kind !== "block" || update.body.length !== 2) return null; - if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; - if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; - const lowId = cond.left.localId, - highId = cond.right.localId; - if (lowId === highId) return null; - const counter = (id: string): IntegerPairedCounter | null => { - const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); - if (decl?.kind !== "varDecl" || !decl.init) return null; - const local = locals.get(id); - if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; - const start = decl.init; - // Each start is evaluated once in the head and again on a fallback path. - if ( - !stableLoopValue(start, locals, stmt.body, false, lowId) || - !stableLoopValue(start, locals, stmt.body, false, highId) - ) - return null; - const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null); - if (steps.length !== 1 || writesLocal(stmt.body, id)) return null; - const known = - start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) - ? { min: start.value, max: start.value } - : ranges.get(start); - const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; - if (!known && !startGuarded) return null; - if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER)) - return null; - return { localId: id, start, step: steps[0]!, startGuarded }; - }; - const low = counter(lowId), - high = counter(highId); - if (!low || !high || low.step <= 0 || high.step >= 0) return null; - const startRange = (c: IntegerPairedCounter): IntegerRange => - c.startGuarded - ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } - : c.start.kind === "numLit" - ? { min: c.start.value, max: c.start.value } - : ranges.get(c.start)!; - const guarded = low.startGuarded || high.startGuarded; - // Only a loop whose counters index something gains from integer storage. - let useful = false, - cost = 0; - const isCounter = (e: IrExpr): boolean => - (e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) || - (e.kind === "bin" && - (e.op === "+" || e.op === "-") && - (isCounter(e.left) || isCounter(e.right))); - const expr = (e: IrExpr): boolean => { - if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; - if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) - useful = true; - if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) - useful = true; - return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt); - }; - const bodyStmt = (s: IrStmt): boolean => { - if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true; - if ( - guarded && - (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") - ) - return false; - return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); - }; - if (!stmt.body.every(bodyStmt) || !useful) return null; - return { - low, - high, - inclusive: cond.op === "<=", - guarded, - // A loop whose starts already fail the condition never enters the body. - range: { - min: startRange(low).min, - max: Math.max(startRange(low).min, startRange(high).max), - }, - }; -} - /** True when a lowered subtree writes `localId`. Local ids are unique per * function, so typed traversal includes writes * nested in expressions, branches, nested loops, and try/finally bodies. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json deleted file mode 100644 index 122d64bb6..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7140-paired-loop-counters.ts" - ], - "diags": [] -} diff --git a/tests/corpus/7140-paired-loop-counters.ts b/tests/corpus/7140-paired-loop-counters.ts deleted file mode 100644 index ec1610d1a..000000000 --- a/tests/corpus/7140-paired-loop-counters.ts +++ /dev/null @@ -1,81 +0,0 @@ -// For loops with two counters closing in on each other (`lo++, hi--`): -// in-place reversals, inclusive bounds, larger steps, early exits, starts -// that are fractions, negative zero, NaN, huge or already crossed, nested -// reversals, and counters a closure captures or the body writes. -function reverse(a: number[], from: number, to: number): number[] { - for (let lo = from, hi = to; lo < hi; lo++, hi--) { - const t = a[lo]!; - a[lo] = a[hi]!; - a[hi] = t; - } - return a; -} -const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; -console.log(reverse(base(), 0, 9).join(",")); -console.log(reverse(base(), 2, 6).join(",")); -console.log(reverse(base(), 6, 2).join(",")); -console.log(reverse(base(), 3, 3).join(",")); -// Unproven starts: fractions, negative zero, NaN and huge values keep JS -// semantics (charCodeAt truncates its index; out-of-range reads are NaN). -function codes(s: string, from: number, to: number): string { - const out: number[] = []; - for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); - return out.join(","); -} -const word = "abcdefgh"; -console.log(codes(word, 0.5, 4.5)); -console.log(codes(word, -0, 3), Object.is(-0 + 0, 0)); -console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN)); -console.log(codes(word, 2 ** 53 - 3, 2 ** 53)); -console.log(codes(word, -3, 2)); - -function pairs(n: number): string { - const out: string[] = []; - for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`); - return out.join(" "); -} -console.log(pairs(20), "|", pairs(0), "|", pairs(-1)); - -function firstGap(a: number[]): number { - let found = -1; - for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) { - if (a[lo] === a[hi]) continue; - found = lo; - break; - } - return found; -} -console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([])); - -// The flip loop of a permutation puzzle: the next index comes from the array. -function flips(perm: number[]): number { - const work = perm.slice(); - let count = 0; - let head = work[0]!; - while (head !== 0) { - for (let lo = 0, hi = head; lo < hi; lo++, hi--) { - const t = work[lo]!; - work[lo] = work[hi]!; - work[hi] = t; - } - count++; - head = work[0]!; - } - return count; -} -console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2])); - -// Counters a closure reads, or the body writes, stay ordinary bindings. -const readers: (() => number)[] = []; -for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi); -console.log(readers.map((f) => f()).join(",")); -const seen: number[] = []; -for (let lo = 0, hi = 10; lo < hi; lo++, hi--) { - seen.push(lo, hi); - if (lo === 2) hi -= 3; -} -console.log(seen.join(",")); -const text = "abcdefgh"; -let mirrored = ""; -for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo); -console.log(mirrored); diff --git a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json deleted file mode 100644 index 549fd6a4a..000000000 --- a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 3880, "san": 6000 } From 1773ffbef35e8ffb2cdfb060278ac316df85116d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:52:01 -0500 Subject: [PATCH 09/12] WebCrypto: the crypto global, node:crypto's webcrypto, and secret-key SubtleCrypto --- .../limitations/builtin-apis/crypto.mdx | 3 + .../src/generated/node-v24-compatibility.json | 58 +- .../generated/node-v24-internal.json | 198 ++- internal/compatibility/static-support.json | 11 +- .../ambient/scriptc-node-fallback.d.ts | 103 +- .../compiler/src/backend/llvm/lib-shared.ts | 12 + .../src/backend/native-link-info-core.ts | 5 +- .../compiler/src/backend/native/contracts.ts | 5 +- .../compiler/src/backend/native/executable.ts | 3 + .../src/backend/target-diagnostics.ts | 8 + .../src/frontend/lowering/builtins/crypto.ts | 5 +- .../frontend/lowering/builtins/webcrypto.ts | 60 +- .../lowering/expressions/identifiers.ts | 5 +- .../lowering/expressions/property-access.ts | 14 +- .../src/frontend/lowering/lower-calls.ts | 35 +- .../src/frontend/lowering/lower-stmts.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 4 + .../src/frontend/lowering/surfaces.ts | 20 +- packages/compiler/src/frontend/type-mapper.ts | 10 + .../compiler/src/ir/builtin-signatures.ts | 7 + packages/compiler/src/ir/runtime-features.ts | 14 +- packages/compiler/surface-manifest.json | 12 +- .../corpus/7120-webcrypto-random-uuid.ts.json | 6 + .../7121-webcrypto-crypto-object.mjs.json | 6 + .../corpus/7122-webcrypto-node-crypto.ts.json | 6 + .../7123-webcrypto-node-crypto.cjs.json | 6 + .../corpus/7124-webcrypto-secret-keys.ts.json | 6 + .../corpus/7125-webcrypto-key-errors.mjs.json | 6 + .../tests/corpus/7126-webcrypto-aes.ts.json | 6 + .../corpus/7127-webcrypto-aes-errors.mjs.json | 6 + .../corpus/7128-webcrypto-derive.ts.json | 6 + .../7129-webcrypto-derive-errors.mjs.json | 6 + .../node-types/webcrypto-keys.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 3 +- .../runtime-pack-matrix.mjs | 4 + .../runtime-pack-common/scripts/build.mjs | 3 +- .../runtime-wasm32-wasi/scripts/build.mjs | 1 + packages/runtime/src/scr_crypto.h | 44 +- packages/runtime/src/scr_webcrypto.c | 1372 +++++++++++++++-- packages/runtime/src/scr_webcrypto_aes.c | 137 ++ packages/runtime/src/scr_webcrypto_object.c | 53 + tests/corpus/7120-webcrypto-random-uuid.ts | 22 + tests/corpus/7121-webcrypto-crypto-object.mjs | 35 + tests/corpus/7122-webcrypto-node-crypto.ts | 38 + tests/corpus/7123-webcrypto-node-crypto.cjs | 31 + tests/corpus/7124-webcrypto-secret-keys.ts | 91 ++ tests/corpus/7125-webcrypto-key-errors.mjs | 95 ++ tests/corpus/7126-webcrypto-aes.ts | 77 + tests/corpus/7127-webcrypto-aes-errors.mjs | 71 + tests/corpus/7128-webcrypto-derive.ts | 45 + tests/corpus/7129-webcrypto-derive-errors.mjs | 72 + tests/diagnostics/webcrypto-refusals.ts | 19 +- tests/fixtures/node-types/fenced.ts | 12 +- tests/fixtures/node-types/webcrypto-keys.ts | 32 + .../__snapshots__/node-types-fenced.txt | 10 +- .../__snapshots__/webcrypto-refusals.ts.txt | 31 +- .../7120-webcrypto-random-uuid.ts.json | 1 + .../7121-webcrypto-crypto-object.mjs.json | 1 + .../7122-webcrypto-node-crypto.ts.json | 1 + .../7123-webcrypto-node-crypto.cjs.json | 1 + .../7124-webcrypto-secret-keys.ts.json | 1 + .../7125-webcrypto-key-errors.mjs.json | 1 + .../corpus-costs/7126-webcrypto-aes.ts.json | 1 + .../7127-webcrypto-aes-errors.mjs.json | 1 + .../7128-webcrypto-derive.ts.json | 1 + .../7129-webcrypto-derive-errors.mjs.json | 1 + tests/harness/errors.test.ts | 31 +- tests/harness/project-config.test.ts | 18 + 68 files changed, 2688 insertions(+), 337 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json create mode 100644 packages/runtime/src/scr_webcrypto_aes.c create mode 100644 packages/runtime/src/scr_webcrypto_object.c create mode 100644 tests/corpus/7120-webcrypto-random-uuid.ts create mode 100644 tests/corpus/7121-webcrypto-crypto-object.mjs create mode 100644 tests/corpus/7122-webcrypto-node-crypto.ts create mode 100644 tests/corpus/7123-webcrypto-node-crypto.cjs create mode 100644 tests/corpus/7124-webcrypto-secret-keys.ts create mode 100644 tests/corpus/7125-webcrypto-key-errors.mjs create mode 100644 tests/corpus/7126-webcrypto-aes.ts create mode 100644 tests/corpus/7127-webcrypto-aes-errors.mjs create mode 100644 tests/corpus/7128-webcrypto-derive.ts create mode 100644 tests/corpus/7129-webcrypto-derive-errors.mjs create mode 100644 tests/fixtures/node-types/webcrypto-keys.ts create mode 100644 tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json create mode 100644 tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json create mode 100644 tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json create mode 100644 tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json create mode 100644 tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json create mode 100644 tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json create mode 100644 tests/harness/corpus-costs/7126-webcrypto-aes.ts.json create mode 100644 tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json create mode 100644 tests/harness/corpus-costs/7128-webcrypto-derive.ts.json create mode 100644 tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d..b00ea438e 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -16,4 +16,7 @@ A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. Other public-key operations remain unsupported. + The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. + Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. + Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd..2c09eb9be 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -12442,9 +12442,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12465,9 +12465,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -79656,9 +79656,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -79680,7 +79680,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -79703,7 +79703,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -80001,9 +80001,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80024,9 +80024,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80047,9 +80047,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80139,9 +80139,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80162,9 +80162,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80208,9 +80208,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c..c9f6dc6be 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,6 +19313,8 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -21215,8 +21217,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:subtle", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "ancestor" }, @@ -21238,8 +21244,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.webcrypto" + "status": "partial", + "evidence": "compiler-dedicated:crypto.webcrypto", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "exact" }, @@ -106607,8 +106617,6 @@ "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", @@ -106616,11 +106624,20 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110078,8 +110095,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.Crypto", + "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6293-webcrypto-digest.ts" + ] }, "anchorSource": "exact" }, @@ -110102,8 +110124,10 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.getRandomValues", + "evidence": "compiler-feature:webcrypto.crypto.getRandomValues", "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js" ] @@ -110129,11 +110153,12 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.randomUUID", + "evidence": "compiler-feature:webcrypto.crypto.randomUUID", "tests": [ - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", - "tests/corpus/1358-crypto-random.ts" + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" ] }, "anchorSource": "exact" @@ -110189,6 +110214,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.CryptoKey", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110217,6 +110243,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110245,6 +110272,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.extractable", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110273,6 +110301,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.type", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110301,6 +110330,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.usages", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110471,8 +110501,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.decrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110494,8 +110539,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveBits", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110517,8 +110577,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110543,6 +110618,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110617,8 +110699,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.encrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110640,8 +110737,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.exportKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110686,8 +110798,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.generateKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110712,6 +110839,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110743,6 +110877,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110797,6 +110938,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b..e7b3486c4 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,16 +187,18 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], - "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], - "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.subtle": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, - { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, + { "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, @@ -413,6 +415,7 @@ { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] }, + { "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fd..489af1700 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -544,12 +544,15 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for - * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as +/* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's + * importKey, generateKey and exportKey for HMAC and AES keys (importKey + * also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt, + * PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as * exactly that surface (the declarations mirror @types/node's webcrypto - * shapes). Every other SubtleCrypto method and crypto member is absent, - * so a use is an honest type error. CryptoKey values are native handles - * whose four members read through the handle. */ + * shapes), plus the global's randomUUID and getRandomValues. Every other + * SubtleCrypto method and crypto member is absent, so a use is an honest + * type error. CryptoKey values are native handles whose four members read + * through the handle. */ declare var crypto: Crypto; /* The Crypto and SubtleCrypto constructors (feature probes only) stay * `unknown`; the values keep this declaration order. */ @@ -582,9 +585,44 @@ interface HmacImportParams extends Algorithm { hash: HashAlgorithmIdentifier; length?: number; } +interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface AesKeyGenParams extends Algorithm { + length: number; +} +interface AesGcmParams extends Algorithm { + additionalData?: BufferSource; + iv: BufferSource; + tagLength?: number; +} +interface AesCbcParams extends Algorithm { + iv: BufferSource; +} +interface AesCtrParams extends Algorithm { + counter: BufferSource; + length: number; +} +interface Pbkdf2Params extends Algorithm { + hash: HashAlgorithmIdentifier; + iterations: number; + salt: BufferSource; +} +interface HkdfParams extends Algorithm { + hash: HashAlgorithmIdentifier; + info: BufferSource; + salt: BufferSource; +} +interface AesDerivedKeyParams extends Algorithm { + length: number; +} interface KeyAlgorithm { name: string; } +interface AesKeyAlgorithm extends KeyAlgorithm { + length: number; +} interface HmacKeyAlgorithm extends KeyAlgorithm { hash: KeyAlgorithm; length: number; @@ -623,10 +661,37 @@ interface JsonWebKey { x?: string; y?: string; } -/* The 'jwk' form type-checks like @types/node's; the compiler refuses it - * (the JsonWebKey import is not implemented). */ interface SubtleCrypto { + decrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + deriveBits( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + length?: number | null, + ): Promise; + deriveKey( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + encrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + exportKey(format: "jwk", key: CryptoKey): Promise; + exportKey(format: Exclude, key: CryptoKey): Promise; + generateKey( + algorithm: AesKeyGenParams | HmacKeyGenParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; importKey( format: "jwk", keyData: JsonWebKey, @@ -651,6 +716,21 @@ interface SubtleCrypto { } interface Crypto { readonly subtle: SubtleCrypto; + getRandomValues< + T extends + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >( + typedArray: T, + ): T; + randomUUID(): `${string}-${string}-${string}-${string}-${string}`; } /* Capability-gated globals harness code probes behind feature guards @@ -2784,8 +2864,7 @@ declare module "crypto" { b: ArrayBufferView | ArrayBufferLike, ): boolean; /* WebCrypto's getRandomValues (also the webcrypto object's): integer - * typed arrays, filled in place and returned. The crypto GLOBAL stays - * unknown in fallback builds. */ + * typed arrays, filled in place and returned. */ type RandomValuesArray = | Int8Array | Uint8Array @@ -2797,9 +2876,9 @@ declare module "crypto" { | BigInt64Array | BigUint64Array; export function getRandomValues(typedArray: T): T; - export const webcrypto: { - getRandomValues(typedArray: T): T; - }; + /* The WebCrypto global's object and its SubtleCrypto. */ + export const webcrypto: Crypto; + export const subtle: SubtleCrypto; export function randomFillSync( buffer: T, offset?: number, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aa..507624652 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -615,10 +615,17 @@ export const LIB_FN_SYMS: Record = { "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.generateKey": "scr_webcrypto_generate_key", + "webcrypto.exportKey": "scr_webcrypto_export_key", + "webcrypto.encrypt": "scr_webcrypto_encrypt", + "webcrypto.decrypt": "scr_webcrypto_decrypt", + "webcrypto.deriveBits": "scr_webcrypto_derive_bits", + "webcrypto.deriveKey": "scr_webcrypto_derive_key", "webcrypto.sign": "scr_webcrypto_sign", "webcrypto.verify": "scr_webcrypto_verify", "webcrypto.digest": "scr_webcrypto_digest", "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", + "webcrypto.object": "scr_webcrypto_object", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -1573,6 +1580,11 @@ export const USES_TIMERS_LIB_FNS = new Set([ // ecdh.setPublicKey reports DEP0031 on the next tick. "crypto.ecdhNew", // WebCrypto results settle through the deferred crypto queue. + "webcrypto.generateKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index a476e1f7f..55301948b 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -84,8 +84,9 @@ export interface NativeLinkFeatures { foreignFfi: boolean; nodeTest: boolean; tls: boolean; - /** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS - * archive. */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: scr_cipher.c plus the mbedTLS archive (and, with webcrypto, + * scr_webcrypto_aes.c). */ cipher: boolean; /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b2..8df233585 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -243,8 +243,9 @@ export interface CcOptions { * The unit also compiles whenever `tls` does — scr_tls.c consults its * default-set override and shared Windows-certificate enumerator. */ tlsCa?: boolean; - /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links - * the mbedTLS archive (shared with tls). */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: compiles scr_cipher.c (and, with webcrypto, + * scr_webcrypto_aes.c) and links the mbedTLS archive (shared with tls). */ cipher?: boolean; /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600..229b2f967 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -297,6 +297,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), + // The crypto global's object extends scr_async_dyn.c's. + unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. unit((u) => u.tlsCa, "scr_tls_ca.c"), @@ -308,6 +310,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). // The unconditional win32 libs above do not carry it. diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index b7049f6fe..eb70a0d2c 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -208,6 +208,14 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") { + found = { + surface: + "WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "crypto.ecdhNew" || node.fn === "crypto.ecdhConvertKey") { found = { surface: "ECDH (the wasm32-wasi runtime does not include the mbedTLS library)", diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe8..fc1502958 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -7,6 +7,7 @@ import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; import { CIPHER_TYPE_NAMES } from "../../type-mapper.js"; import { CRYPTO_CIPHERS, CRYPTO_CURVES, CRYPTO_HASHES } from "../crypto-tables.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import { BOOL, BYTES_U8, @@ -1316,8 +1317,10 @@ export function lowerCryptoGetRandomValues( } /** Whether `receiver` names a WebCrypto Crypto object: the `crypto` - * global (also through globalThis) or node:crypto's `webcrypto`. */ + * global (also through globalThis or an alias of it) or node:crypto's + * `webcrypto`. */ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (stdlibGlobalNameOf(lowerer, receiver) === "crypto") return true; if (ts.isIdentifier(receiver)) { if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true; const imported = lowerer.builtinImportOf(receiver); diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 93cba84c5..fe310294c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -5,34 +5,57 @@ import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; +import { isWebCryptoReceiver } from "./crypto.js"; -/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, - * `sign`, `verify` and `digest` called on the crypto global (also through - * globalThis) lower to runtime entry points that take the argument vector - * as one dyn array and answer a promise, so Web IDL conversion, algorithm - * normalization and every error stay Node-shaped at run time. CryptoKey +/* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and + * decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`, + * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, + * `deriveKey`, `sign`, `verify` and `digest` + * called on the crypto global (also through + * globalThis, node:crypto's webcrypto and node:crypto's subtle) lower to + * runtime entry points that take the argument vector as one dyn array and + * answer a promise, so Web IDL conversion, algorithm normalization and + * every error stay Node-shaped at run time. CryptoKey * values are native handles; their type maps to dyn and their members * are handle reads. Other SubtleCrypto methods are refused by name. */ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ["importKey", "webcrypto.importKey"], + ["generateKey", "webcrypto.generateKey"], + ["exportKey", "webcrypto.exportKey"], + ["encrypt", "webcrypto.encrypt"], + ["decrypt", "webcrypto.decrypt"], + ["deriveBits", "webcrypto.deriveBits"], + ["deriveKey", "webcrypto.deriveKey"], ["sign", "webcrypto.sign"], ["verify", "webcrypto.verify"], ["digest", "webcrypto.digest"], ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; -/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +/** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto + * Crypto object (the global, globalThis.crypto or node:crypto's + * `webcrypto`) or node:crypto's own `subtle` export, named or through + * the namespace. */ function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { while (ts.isParenthesizedExpression(node)) node = node.expression; - return ( - ts.isPropertyAccessExpression(node) && - !node.questionDotToken && - node.name.text === "subtle" && - stdlibGlobalNameOf(lowerer, node.expression) === "crypto" - ); + if (ts.isIdentifier(node)) { + const imported = lowerer.builtinImportOf(node); + return imported?.module === "crypto" && imported.member === "subtle"; + } + if ( + !ts.isPropertyAccessExpression(node) || + node.questionDotToken || + node.name.text !== "subtle" + ) { + return false; + } + if (stdlibGlobalNameOf(lowerer, node.expression) === "crypto") return true; + if (isWebCryptoReceiver(lowerer, node.expression)) return true; + const member = lowerer.builtinMemberOf(node); + return member?.module === "crypto" && member.member === "subtle"; } /** `crypto.subtle.(...args)`: the lowered methods become their @@ -51,17 +74,6 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); } - // JsonWebKey imports are not implemented: a literal 'jwk' format is - // refused here (the runtime refuses a computed one). - const format = call.arguments.nodes[0]; - if ( - member === "importKey" && - format !== undefined && - ts.isStringLiteralLike(format) && - format.text === "jwk" - ) { - lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); - } const loc = locOf(call); const args = call.arguments.nodes.map((arg) => ts.isObjectLiteralExpression(arg) diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d682..16cca3b13 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -549,6 +549,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const callable = lowerer.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; // JavaScript sources: an otherwise unsupported builtin VALUE is // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). @@ -721,7 +724,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "closure", fnName: name, captures: [], type, loc }; } if (lowerer.isStdlibGlobal(expr, "crypto")) - return { kind: "libCall", fn: "crypto.native", args: [], type: DYN, loc }; + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da0..5cd70d2a5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -162,6 +162,7 @@ import { lowerUnionFieldRead } from "./union-fields.js"; import { errorPropertyRead } from "../error-methods.js"; import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js"; +import { isWebCryptoReceiver } from "../builtins/crypto.js"; import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js"; import { keyedFieldRead } from "./keyed-field-targets.js"; import { promisePrototypeValue } from "../thenables.js"; @@ -842,8 +843,16 @@ export function lowerPropertyAccessExpr( if (runtimeOptionalClassField(lowerer, handled, expr)) return handled; return iteratorResultValueRead(lowerer, handled, expr) ?? lowerer.maybeNarrow(handled, expr); } + // `globalThis.crypto` is the WebCrypto object the bare `crypto` answers. + if ( + !expr.questionDotToken && + stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && + stdlibGlobalNameOf(lowerer, expr) === "crypto" + ) { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // `globalThis.` that no lowering above claimed - // (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage + // (globalThis.SubtleCrypto, globalThis.localStorage // — the harness's capability-conditional knownGlobals adds): the // PROPERTY spelling of the identifier chokepoint's JS rule — a stdlib // global taken as a VALUE in a JavaScript source is the same opaque @@ -2959,6 +2968,9 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi if ( receiverIr?.kind !== "object" && propSym && + // The WebCrypto object's generic getRandomValues reads off its + // dynamic object. + !isWebCryptoReceiver(lowerer, expr.expression) && isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab..c503f0c9c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1542,9 +1542,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? - // crypto.getRandomValues on the WebCrypto global (untyped in - // fallback builds, so before the dyn receiver path) or webcrypto. - lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ?? + // crypto.getRandomValues / randomUUID on the WebCrypto global + // (untyped in fallback builds, so before the dyn receiver path) or + // webcrypto. + lowerWebCryptoMethodCall(lowerer, expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ?? @@ -10492,20 +10493,40 @@ export function lowerStaticNumberGlobal( return null; } -/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */ -function lowerWebCryptoGetRandomValues( +/** `crypto.getRandomValues(view)` and `crypto.randomUUID()` on the + * WebCrypto global or webcrypto. Other argument counts (JavaScript only) + * invoke the dynamic object's method, which ignores extra arguments and + * throws Node's missing-argument TypeError. */ +function lowerWebCryptoMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { + const member = access.name.text; if ( - access.name.text !== "getRandomValues" || + (member !== "getRandomValues" && member !== "randomUUID") || call.questionDotToken || access.questionDotToken || !isWebCryptoReceiver(lowerer, access.expression) ) return null; - return lowerCryptoGetRandomValues(lowerer, call); + const args = call.arguments.nodes; + if (member === "getRandomValues" && args.length === 1) { + return lowerCryptoGetRandomValues(lowerer, call); + } + if (member === "randomUUID" && args.length === 0) { + return { kind: "libCall", fn: "crypto.randomUUID", args: [], type: STRING, loc: locOf(call) }; + } + if (args.some(ts.isSpreadElement)) return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: member, + calleeName: v8CalleeText(access), + args: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc: locOf(call), + }; } /** A then handler's promise-armed result (`.then(() => cached ?? load())` diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c..7f0854523 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3964,9 +3964,13 @@ function stdlibGlobalTokenDestructure( const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; - // Builtin constructors read as their interned function values, the - // same values the bare and `globalThis.X` spellings read. - value = builtinGlobalValue(canonical, locOf(el), true); + // Builtin constructors read as their interned function values, and + // crypto as the WebCrypto object: the same values the bare and + // `globalThis.X` spellings read. + value = + canonical === "crypto" + ? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) } + : builtinGlobalValue(canonical, locOf(el), true); } else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) { token = `[builtin console.${propName}]`; } else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecb..3fbb110ed 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13410,6 +13410,10 @@ export class Lowerer { const callable = this.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // node:stream/web's exports are the web stream globals themselves: in // JavaScript sources the same identity token as the global spelling // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c77938337..67950a53f 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2210,15 +2210,26 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "stdlib.webcrypto.subtle", kind: "stdlib", name: "globalThis.crypto.subtle", - fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], - note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + fns: [ + "webcrypto.importKey", + "webcrypto.generateKey", + "webcrypto.exportKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", + ], + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", kind: "stdlib", name: "CryptoKey", fns: ["webcrypto.isCryptoKey"], - note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", }, { id: "stdlib.atomics", @@ -3088,9 +3099,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", - webcrypto: - "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, " + - "randomBytes, and the createHash chain", }, zlib: { brotliCompressSync: BROTLI_HINT, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186..a491edf95 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -146,6 +146,16 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "KeyAlgorithm", "HmacImportParams", "HmacKeyAlgorithm", + "HmacKeyGenParams", + "AesKeyGenParams", + "AesKeyAlgorithm", + "JsonWebKey", + "AesGcmParams", + "AesCbcParams", + "AesCtrParams", + "Pbkdf2Params", + "HkdfParams", + "AesDerivedKeyParams", ]); export const FETCH_AMBIENT_TYPES = [ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa7876579..416d5f18e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2846,10 +2846,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * one dyn array and answers a promise that argument and validation * failures reject. CryptoKey values are native handles. */ "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, + "webcrypto.object": { argTypes: [], result: DYN }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4142b6efe..61e04177b 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -75,6 +75,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "child.propsInstall", "child.propsResolve", "crypto.native", + // The crypto global's object extends crypto.native's. + "webcrypto.object", "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", @@ -647,8 +649,9 @@ const RUNTIME_FEATURE_RULES = { }), /** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the * process-warning unit, and unknown output encodings render through the - * inspect unit. */ - cipher: rule({ fns: ["crypto.cipherNew"] }), + * inspect unit. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c) + * link the same archive. */ + cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }), copying: rule({ expr: (node, _tables) => (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || @@ -675,12 +678,15 @@ const RUNTIME_FEATURE_RULES = { ), // The legacy url functions, crypto.createCipheriv and // ecdh.setPublicKey emit their deprecation warnings through the - // dyn-async unit; scr_wasm.c boxes its handles and promises through it. + // dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c + // too); scr_wasm.c boxes its handles and promises through it. fns: [ ...DYN_ASYNC_LIB_FNS, ...READABLE_DYN_ASYNC_LIB_FNS, ...LEGACY_URL_LIB_FNS, "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], @@ -787,6 +793,8 @@ const RUNTIME_FEATURE_RULES = { "global.native", "util.styleText", "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", "crypto.ecdhConvertKey", ...BUILTIN_CLASS_LIB_FNS, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index c05e994a8..3e8b9df73 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -762,14 +762,6 @@ "code": "SC2020", "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" }, - { - "id": "node-builtin.crypto.webcrypto", - "kind": "node-builtin", - "name": "crypto.webcrypto", - "status": "unsupported", - "code": "SC2020", - "note": "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, randomBytes, and the createHash chain" - }, { "id": "node-builtin.dgram", "kind": "node-builtin", @@ -5719,14 +5711,14 @@ "kind": "stdlib", "name": "CryptoKey", "status": "static", - "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + "note": "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey" }, { "id": "stdlib.webcrypto.subtle", "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 000000000..c70fdfa63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7120-webcrypto-random-uuid.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 000000000..b152699ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7121-webcrypto-crypto-object.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 000000000..02451a516 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7122-webcrypto-node-crypto.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 000000000..821fb378d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7123-webcrypto-node-crypto.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 000000000..8fc0dab2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7124-webcrypto-secret-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 000000000..942de8c10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7125-webcrypto-key-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json new file mode 100644 index 000000000..f113a3ca9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7126-webcrypto-aes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 000000000..3ec3d26f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7127-webcrypto-aes-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json new file mode 100644 index 000000000..b613fbce6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7128-webcrypto-derive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 000000000..11fc8c721 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7129-webcrypto-derive-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json new file mode 100644 index 000000000..19f14ed02 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-keys.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 85c5ffb00..3944b023c 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -172,7 +172,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0f40218a1..3529df137 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -72,6 +72,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_js_object.c", "dynTier"], ["scr_event_target.c", any("eventTarget", "nativeFetch")], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], + // The crypto global's object extends scr_async_dyn.c's crypto object. + ["scr_webcrypto_object.c", all("webcrypto", "dynAsync")], // The performance timeline emits its buffer warning through scr_async_dyn.c. ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. @@ -151,6 +153,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_ecdh.c", "ecdh"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], + // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. + ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], ]; diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b242b78aa..f59a2431f 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -223,7 +223,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 2dd983973..0a8699b23 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_webcrypto_aes.c", "scr_wasm.c", "scr_fetch.c", ], diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index d12858abf..ddc306598 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -20,6 +20,9 @@ ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc); typedef struct ScrCryptoHash ScrCryptoHash; ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg); ScrDyn *scr_crypto_native(void); +/* The crypto global as a value: scr_crypto_native's object with the Crypto + * methods (scr_webcrypto_object.c, linked with the webcrypto feature). */ +ScrDyn *scr_webcrypto_object(void); ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key); ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key); ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); @@ -202,19 +205,48 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); /* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── - * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and - * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * crypto.subtle.importKey (HMAC, AES, PBKDF2 and HKDF keys, raw and jwk), + * generateKey and exportKey (HMAC and AES keys), sign and verify (HMAC) + * and digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the * call's argument vector as one dyn array (borrowed) and answers a +1 * promise that every argument or validation failure rejects with Node's * TypeError or DOMException; algorithms and formats outside the slice - * reject with a coded SC2020 Error. importKey settles at once with a - * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and - * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key - * answers `value instanceof CryptoKey`. */ + * reject with a coded SC2020 Error. importKey and exportKey settle at + * once with a CryptoKey handle, an ArrayBuffer or a JWK object; + * generateKey fulfills with a CryptoKey, sign and digest with an + * ArrayBuffer and verify with a boolean on a later loop turn. + * scr_webcrypto_is_crypto_key answers `value instanceof CryptoKey`. */ ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args); +ScrPromise *scr_webcrypto_export_key(ScrDyn *args); ScrPromise *scr_webcrypto_sign(ScrDyn *args); ScrPromise *scr_webcrypto_verify(ScrDyn *args); ScrPromise *scr_webcrypto_digest(ScrDyn *args); +/* encrypt and decrypt (scr_webcrypto_aes.c, linked with the cipher + * feature's mbedTLS archive) run scr_webcrypto_cipher with the AES cipher + * as `aes`, which answers the +1 output bytes or NULL when the operation + * fails. encrypt and decrypt fulfill with an ArrayBuffer, or reject with + * a job failure, on a later loop turn. */ +typedef struct { + const unsigned char *iv; /* the IV, or the CTR counter block */ + size_t iv_len; + const unsigned char *aad; + size_t aad_len; + size_t tag_len; /* bytes */ + unsigned counter_bits; +} ScrWebCryptoAesParams; +typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *params, + const unsigned char *in, size_t len); +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args); +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args); +/* deriveBits and deriveKey (PBKDF2 and HKDF) over node:crypto's KDF core: + * deriveBits fulfills with an ArrayBuffer (at once for a zero length) and + * deriveKey with a CryptoKey of the derived key type, on a later loop + * turn. */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args); +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args); bool scr_webcrypto_is_crypto_key(const ScrDyn *value); /* The composed `new crypto.X509Certificate(data).fingerprint` read (the diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 546b24151..4d2c27be4 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -1,22 +1,27 @@ -/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), - * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values - * importKey answers. +/* WebCrypto's secret-key slice: crypto.subtle.importKey, generateKey and + * exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF + * keys), HMAC sign and verify, the SHA-1/SHA-2 digests, PBKDF2 and HKDF + * deriveBits and deriveKey, the argument and key handling of AES encrypt + * and decrypt (whose cipher runs in scr_webcrypto_aes.c), and the + * CryptoKey values those methods answer. * * This unit is only the WebCrypto layer, modeled on Node 24's - * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument - * conversion, algorithm normalization, the CryptoKey handle, promise - * wrapping and Node's error shapes. The MACs and digests themselves come - * from node:crypto's digest core (scr_crypto_hmac_raw and - * scr_crypto_digest_raw in scr_lib.c), and results settle through the - * deferred crypto queue that node:crypto's callback forms use, so sign, - * verify and digest settle on a later loop turn like Node's thread-pool - * jobs while importKey settles at once. + * lib/internal/crypto/webcrypto.js, mac.js, aes.js and keys.js: Web IDL + * argument conversion, algorithm normalization, the CryptoKey handle, the + * JSON Web Key forms, promise wrapping and Node's error shapes. The MACs + * and digests themselves come from node:crypto's digest core + * (scr_crypto_hmac_raw and scr_crypto_digest_raw in scr_lib.c), and + * results settle through the deferred crypto queue that node:crypto's + * callback forms use, so sign, verify, digest and generateKey settle on a + * later loop turn like Node's thread-pool jobs while importKey and + * exportKey settle at once. * * Every argument and validation failure rejects the returned promise. * Algorithms Node recognizes but this slice does not implement (RSA, - * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded - * SC2020 Error instead of silently diverging. The compiler links the unit - * only into programs that call these methods (the webcrypto feature). */ + * ECDSA, AES-OCB, SHA-3, ...) and the key formats of asymmetric keys + * reject with a coded SC2020 Error instead of silently diverging. The + * compiler links the unit only into programs that call these methods + * (the webcrypto feature). */ #include "scr_runtime.h" #include @@ -28,8 +33,9 @@ /* ── algorithm registry ─────────────────────────────────────────────── * The names Node 24 registers per operation (util.js's * kAlgorithmDefinitions with the conditional algorithms its OpenSSL - * build enables), matched case-insensitively. Only HMAC and the SHA-1/ - * SHA-2 digests are implemented. */ + * build enables), matched case-insensitively. HMAC, AES-CBC/CTR/GCM/KW + * keys, PBKDF2 and HKDF key imports and the SHA-1/SHA-2 digests are + * implemented. */ static const char *const scr_wc_import_names[] = { "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", @@ -43,6 +49,15 @@ static const char *const scr_wc_sign_names[] = { "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", }; +/* generateKey and exportKey register the same algorithms. */ +static const char *const scr_wc_key_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "ChaCha20-Poly1305", "ECDH", "ECDSA", "Ed25519", "Ed448", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-v1_5", + "X25519", "X448", +}; static const char *const scr_wc_digest_names[] = { "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", @@ -193,7 +208,24 @@ static const char *const scr_wc_usages[] = { "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", }; -enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; +enum { + SCR_WC_USAGE_ENCRYPT = 1u << 0, + SCR_WC_USAGE_DECRYPT = 1u << 1, + SCR_WC_USAGE_SIGN = 1u << 2, + SCR_WC_USAGE_VERIFY = 1u << 3, + SCR_WC_USAGE_DERIVE_KEY = 1u << 4, + SCR_WC_USAGE_DERIVE_BITS = 1u << 5, + SCR_WC_USAGE_WRAP_KEY = 1u << 6, + SCR_WC_USAGE_UNWRAP_KEY = 1u << 7, +}; + +/* The usage index of a KeyUsage string, or -1. */ +static int scr_wc_usage_index(const ScrStr *s) { + for (size_t k = 0; k < SCR_WC_COUNT(scr_wc_usages); k++) + if (s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0) + return (int)k; + return -1; +} /* sequence over an array: the usage mask (-1 on failure). */ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { @@ -215,11 +247,8 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { scr_dyn_release(plain); return -1; } - size_t k = 0; - while (k < SCR_WC_COUNT(scr_wc_usages) && - !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) - k++; - if (k == SCR_WC_COUNT(scr_wc_usages)) { + int k = scr_wc_usage_index(s); + if (k < 0) { scr_wc_type_error("ERR_INVALID_ARG_VALUE", "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), @@ -237,10 +266,10 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { /* ── algorithm normalization ────────────────────────────────────────── * normalizeAlgorithm(algorithm, op) for the members this slice reads: - * the canonical name, and for HMAC imports the hash and length. */ + * the canonical name, the HMAC hash and length, and AES key lengths. */ typedef struct { const char *name; /* canonical registered name */ - const char *hash; /* HMAC import: the normalized hash name */ + const char *hash; /* HMAC: the normalized hash name */ bool has_length; double length; } ScrWcAlgorithm; @@ -261,6 +290,28 @@ static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { return v ? v : scr_dyn_retain(scr_dyn_undefined()); } +/* An [EnforceRange] integer conversion: the truncated value in [0, max], + * or false with Node's TypeError pending. `context` names the value + * ("length in passed algorithm", "3rd argument"). */ +static bool scr_wc_enforce_range(const char *prefix, const char *context, const ScrDyn *value, + double max, double *out) { + double x; + if (!scr_dyn_number_coerce_js(value, &x)) return false; + if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s is not a finite number.", prefix, context); + return false; + } + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > max) { + scr_wc_type_error("ERR_OUT_OF_RANGE", "%s: %s is outside the expected range of 0 to %.0f.", + prefix, context, max); + return false; + } + *out = x; + return true; +} + /* The name an AlgorithmIdentifier carries (+1), with the Algorithm * dictionary's missing-name TypeError. `dict` names the dictionary the * value converts to. */ @@ -295,11 +346,13 @@ static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *metho return canonical; } -/* HmacImportParams over an object: hash (required, a SHA name), then the - * optional [EnforceRange] unsigned long length. */ -static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { +/* HmacImportParams / HmacKeyGenParams (`dict`) over an object: hash + * (required, a SHA name), then the optional [EnforceRange] unsigned long + * length, which cannot be 0 (`zero_error` names that DOMException). */ +static bool scr_wc_hmac_params(ScrDyn *identifier, const char *dict, const char *zero_error, + const char *method, ScrWcAlgorithm *out) { if (!scr_wc_is_object(identifier)) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); return false; } ScrDyn *plain = scr_wc_plain(identifier); @@ -307,7 +360,7 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *hash = scr_wc_member(plain, "hash"); bool ok = true; if (hash->kind == SCR_DYN_UNDEF) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); ok = false; } ScrDyn *hash_value = NULL; @@ -335,7 +388,9 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { (hash_name->data[2] | 32) == 'a'; if (hash_name) scr_str_release(hash_name); if (ok && !sha) { - scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict); + scr_throw_domex("NotSupportedError", message); ok = false; } } @@ -343,34 +398,23 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *length = scr_wc_member(plain, "length"); if (length->kind != SCR_DYN_UNDEF) { double x; - if (!scr_dyn_number_coerce_js(length, &x)) ok = false; - else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { - scr_wc_type_error("ERR_INVALID_ARG_TYPE", - "%s: length in passed algorithm is not a finite number.", - scr_wc_normalize_prefix); + if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 4294967295.0, &x)) + ok = false; + else if (x == 0) { + char message[96]; + snprintf(message, sizeof message, "%s.length cannot be 0", dict); + scr_throw_domex(zero_error, message); ok = false; } else { - x = trunc(x); - if (x == 0) x = 0; - if (x < 0 || x > 4294967295.0) { - scr_wc_type_error("ERR_OUT_OF_RANGE", - "%s: length in passed algorithm is outside the expected range of 0 to " - "4294967295.", - scr_wc_normalize_prefix); - ok = false; - } else if (x == 0) { - scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); - ok = false; - } else { - out->has_length = true; - out->length = x; - } + out->has_length = true; + out->length = x; } } scr_dyn_release(length); } if (ok) { - out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + out->hash = scr_wc_digest_algorithm(hash_value, method); if (!out->hash) ok = false; } if (hash_value) scr_dyn_release(hash_value); @@ -379,11 +423,51 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { return ok; } -/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with - * the error pending. Only HMAC parameters are read; any other recognized - * algorithm is refused by the caller. */ -static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, - bool import, ScrWcAlgorithm *out) { +/* AesKeyGenParams / AesDerivedKeyParams (`dict`): the required + * [EnforceRange] unsigned short length, 128, 192 or 256. */ +static bool scr_wc_aes_length_params(ScrDyn *identifier, const char *dict, ScrWcAlgorithm *out) { + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *length = scr_wc_member(plain, "length"); + bool ok = false; + double x; + if (length->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "length"); + else if (scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 65535.0, &x)) { + if (x != 128 && x != 192 && x != 256) { + scr_throw_domex("OperationError", "AES key length must be 128, 192, or 256 bits"); + } else { + out->has_length = true; + out->length = x; + ok = true; + } + } + scr_dyn_release(length); + scr_dyn_release(plain); + return ok; +} + +static bool scr_wc_is_aes(const char *name) { + return strcmp(name, "AES-CBC") == 0 || strcmp(name, "AES-CTR") == 0 || + strcmp(name, "AES-GCM") == 0 || strcmp(name, "AES-KW") == 0; +} + +static bool scr_wc_is_kdf(const char *name) { + return strcmp(name, "PBKDF2") == 0 || strcmp(name, "HKDF") == 0; +} + +typedef enum { + SCR_WC_OP_IMPORT, + SCR_WC_OP_SIGN, + SCR_WC_OP_GENERATE, +} ScrWcOp; + +/* normalizeAlgorithm for importKey, sign/verify and generateKey (`names`) + * — false with the error pending. Only the dictionaries of the + * implemented algorithms are read; any other recognized algorithm is + * refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, ScrWcOp op, + ScrWcAlgorithm *out) { memset(out, 0, sizeof *out); ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); if (!name) return false; @@ -393,19 +477,27 @@ static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_ scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); return false; } - if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + bool hmac = strcmp(out->name, "HMAC") == 0; + if (op == SCR_WC_OP_IMPORT && hmac) + return scr_wc_hmac_params(identifier, "HmacImportParams", "DataError", "importKey", out); + if (op == SCR_WC_OP_GENERATE && hmac) + return scr_wc_hmac_params(identifier, "HmacKeyGenParams", "OperationError", "generateKey", out); + if (op == SCR_WC_OP_GENERATE && scr_wc_is_aes(out->name)) + return scr_wc_aes_length_params(identifier, "AesKeyGenParams", out); return true; } /* ── CryptoKey ───────────────────────────────────────────────────────── - * A secret HMAC key: the bytes, the hash, the bit length, the usage mask - * and the extractable flag. The algorithm and usages getters answer the - * same object on every read (Node caches its clones), built on first - * read. The bytes are cleared when the last reference goes. */ + * A secret key: the algorithm name, the bytes, the HMAC hash, the bit + * length (HMAC and AES keys), the usage mask and the extractable flag. + * The algorithm and usages getters answer the same object on every read + * (Node caches its clones), built on first read. The bytes are cleared + * when the last reference goes. */ typedef struct ScrCryptoKey { size_t rc; - const char *hash; /* WebCrypto hash name */ - double length; /* bits */ + const char *name; /* canonical algorithm name */ + const char *hash; /* HMAC: WebCrypto hash name */ + double length; /* HMAC and AES: bits */ unsigned usages; bool extractable; ScrDyn *algorithm; /* cached getter results, or NULL */ @@ -446,26 +538,34 @@ static ScrDyn *scr_wc_cstr(const char *s) { return out; } +/* Node's algorithm objects: { name, length, hash } for HMAC keys, + * { name, length } for AES keys and { name } for PBKDF2 and HKDF keys. */ static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { if (!key->algorithm) { - ScrDyn *hash = scr_dyn_new_obj(); - scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); ScrDyn *algorithm = scr_dyn_new_obj(); - scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); - scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); - scr_dyn_obj_set(algorithm, "hash", 4, hash); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(key->name)); + if (!scr_wc_is_kdf(key->name)) + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + if (key->hash) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + } key->algorithm = algorithm; } return scr_dyn_retain(key->algorithm); } +/* The usages as a fresh array in Node's canonical order (+1). */ +static ScrDyn *scr_wc_usage_array(unsigned usages) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + return list; +} + static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { - if (!key->usage_list) { - ScrDyn *list = scr_dyn_new_arr(); - for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) - if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); - key->usage_list = list; - } + if (!key->usage_list) key->usage_list = scr_wc_usage_array(key->usages); return scr_dyn_retain(key->usage_list); } @@ -540,8 +640,9 @@ static const ScrDynHandleOps scr_crypto_key_ops = { .symbol_get = scr_crypto_key_symbol_get, }; -static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, - double length, unsigned usages, bool extractable) { +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *name, + const char *hash, double length, unsigned usages, + bool extractable) { static SCR_TL bool installed; if (!installed) { installed = true; @@ -550,6 +651,7 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); if (!key) scr_trap("scriptc: out of memory\n"); key->rc = 1; + key->name = name; key->hash = hash; key->length = length; key->usages = usages; @@ -563,6 +665,208 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c return box; } +/* numBitsToBytes: whole bytes for `bits`, rounding up. */ +static size_t scr_wc_bits_to_bytes(double bits) { + return (size_t)(floor(bits / 8) + floor((7 + fmod(bits, 8)) / 8)); +} + +/* Clears the unused low bits of the final byte of a `bits`-long key + * (truncateToBitLength). */ +static void scr_wc_truncate_bits(unsigned char *data, size_t len, double bits) { + int extra = (int)fmod(bits, 8); + if (extra && len) data[len - 1] = (unsigned char)(data[len - 1] & (0xff << (8 - extra))); +} + +/* The usages each key algorithm accepts. */ +static unsigned scr_wc_allowed_usages(const char *name) { + if (strcmp(name, "HMAC") == 0) return SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY; + if (strcmp(name, "AES-KW") == 0) return SCR_WC_USAGE_WRAP_KEY | SCR_WC_USAGE_UNWRAP_KEY; + if (scr_wc_is_kdf(name)) return SCR_WC_USAGE_DERIVE_KEY | SCR_WC_USAGE_DERIVE_BITS; + return SCR_WC_USAGE_ENCRYPT | SCR_WC_USAGE_DECRYPT | SCR_WC_USAGE_WRAP_KEY | + SCR_WC_USAGE_UNWRAP_KEY; +} + +/* validateKeyUsages: SyntaxError for a usage outside the algorithm's. */ +static bool scr_wc_check_usages(const char *name, long usages) { + if (!(usages & ~(long)scr_wc_allowed_usages(name))) return true; + char message[96]; + snprintf(message, sizeof message, "Unsupported key usage for %s%s key", + scr_wc_is_kdf(name) ? "a " : "", name); + scr_throw_domex("SyntaxError", message); + return false; +} + +/* The JWK "alg" of an HMAC hash or an AES key, or NULL. */ +static const char *scr_wc_jwk_alg(const char *name, const char *hash, double length, char *buf, + size_t size) { + if (strcmp(name, "HMAC") == 0) { + if (strcmp(hash, "SHA-1") == 0) return "HS1"; + if (strcmp(hash, "SHA-256") == 0) return "HS256"; + if (strcmp(hash, "SHA-384") == 0) return "HS384"; + if (strcmp(hash, "SHA-512") == 0) return "HS512"; + return NULL; + } + if (!scr_wc_is_aes(name)) return NULL; + snprintf(buf, size, "A%.0f%s", length, name + 4); + return buf; +} + +/* ── JSON Web Keys ───────────────────────────────────────────────────── + * The JsonWebKey dictionary members a secret key reads, converted in Web + * IDL's member order (every DOMString member converts, so a Symbol or a + * throwing toString in any of them rejects like Node). */ +typedef struct { + ScrStr *alg, *k, *kty, *use; + bool has_ext, ext; + ScrDyn *key_ops; /* array of strings, or NULL */ +} ScrWcJwk; + +static void scr_wc_jwk_drop(ScrWcJwk *jwk) { + if (jwk->alg) scr_str_release(jwk->alg); + if (jwk->k) scr_str_release(jwk->k); + if (jwk->kty) scr_str_release(jwk->kty); + if (jwk->use) scr_str_release(jwk->use); + if (jwk->key_ops) scr_dyn_release(jwk->key_ops); + memset(jwk, 0, sizeof *jwk); +} + +/* A DOMString dictionary member: NULL with *ok when absent; NULL with + * !*ok and the TypeError pending on failure. */ +static ScrStr *scr_wc_jwk_string(const char *prefix, const ScrDyn *dict, const char *key, bool *ok) { + ScrDyn *value = scr_wc_member(dict, key); + ScrStr *out = NULL; + if (value->kind == SCR_DYN_SYMBOL) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in 2nd argument is a Symbol and cannot be converted to a string.", + prefix, key); + *ok = false; + } else if (value->kind != SCR_DYN_UNDEF) { + out = scr_wc_dom_string(value); + if (!out) *ok = false; + } + scr_dyn_release(value); + return out; +} + +static bool scr_wc_jwk_convert(const char *prefix, ScrDyn *value, ScrWcJwk *out) { + memset(out, 0, sizeof *out); + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) return true; + if (!scr_wc_is_object(value)) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument cannot be converted to a dictionary", + prefix); + return false; + } + ScrDyn *dict = scr_wc_plain(value); + if (!dict) return false; + static const char *const members[] = {"alg", "crv", "d", "dp", "dq", "e", "ext", + "k", "key_ops", "kty", "n", "p", "priv", "pub", + "q", "qi", "use", "x", "y"}; + bool ok = true; + for (size_t i = 0; ok && i < SCR_WC_COUNT(members); i++) { + const char *key = members[i]; + if (strcmp(key, "ext") == 0) { + ScrDyn *ext = scr_wc_member(dict, key); + if (ext->kind != SCR_DYN_UNDEF) { + out->has_ext = true; + out->ext = scr_dyn_truthy(ext); + } + scr_dyn_release(ext); + } else if (strcmp(key, "key_ops") == 0) { + ScrDyn *ops = scr_wc_member(dict, key); + if (ops->kind != SCR_DYN_UNDEF) { + ScrDyn *list = scr_wc_plain(ops); + if (!list) ok = false; + else if (list->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: key_ops in 2nd argument cannot be converted to sequence.", prefix); + scr_dyn_release(list); + ok = false; + } else { + ScrDyn *strings = scr_dyn_new_arr(); + double n = scr_dyn_arr_len(list); + for (double j = 0; ok && j < n; j++) { + ScrDyn *item = scr_dyn_arr_at(list, j); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) ok = false; + else { + scr_dyn_arr_push(strings, scr_dyn_new_str(s)); + scr_str_release(s); + } + } + scr_dyn_release(list); + if (ok) out->key_ops = strings; + else scr_dyn_release(strings); + } + } + scr_dyn_release(ops); + } else { + ScrStr *s = scr_wc_jwk_string(prefix, dict, key, &ok); + if (strcmp(key, "alg") == 0) out->alg = s; + else if (strcmp(key, "k") == 0) out->k = s; + else if (strcmp(key, "kty") == 0) out->kty = s; + else if (strcmp(key, "use") == 0) out->use = s; + else if (s) scr_str_release(s); + } + } + scr_dyn_release(dict); + if (!ok) scr_wc_jwk_drop(out); + return ok; +} + +static bool scr_wc_str_is(const ScrStr *s, const char *literal) { + return s && s->len == strlen(literal) && memcmp(s->data, literal, s->len) == 0; +} + +/* validateJwk for an oct key, then the key bytes (+1): k decodes with + * Node's lenient base64 decoder, so any string answers bytes. */ +static ScrBytes *scr_wc_jwk_secret(const ScrWcJwk *jwk, bool extractable, long usages, + const char *expected_use) { + if (!jwk->kty) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (!scr_wc_str_is(jwk->kty, "oct")) { + scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter"); + return NULL; + } + if (!jwk->k) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (usages != 0 && jwk->use && !scr_wc_str_is(jwk->use, expected_use)) { + scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter"); + return NULL; + } + if (jwk->key_ops) { + long seen = 0; + double n = scr_dyn_arr_len(jwk->key_ops); + for (double i = 0; i < n; i++) { + ScrDyn *op = scr_dyn_arr_at(jwk->key_ops, i); + int k = scr_wc_usage_index(op->v.str); + scr_dyn_release(op); + if (k < 0) continue; + if (seen & (1L << k)) { + scr_throw_domex("DataError", "Duplicate key operation"); + return NULL; + } + seen |= 1L << k; + } + if ((seen & usages) != usages) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return NULL; + } + } + if (jwk->has_ext && !jwk->ext && extractable) { + scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch"); + return NULL; + } + ScrStr *encoding = scr_str_new("base64url", 9); + ScrBytes *bytes = scr_bytes_from_str(jwk->k, encoding); + scr_str_release(encoding); + return bytes; +} + /* ── the operations ─────────────────────────────────────────────────── * Each takes its argument vector as one dyn array (borrowed) and answers * a +1 promise. */ @@ -573,6 +877,19 @@ static ScrPromise *scr_wc_rejected(void) { return p; } +/* A promise already fulfilled with `value` (moves). */ +static ScrPromise *scr_wc_resolved(ScrDyn *value) { + return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* A promise that fulfills with `value` (moves) on a later loop turn, like + * Node's crypto jobs. */ +static ScrPromise *scr_wc_deferred(ScrDyn *value) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(value, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn); + return p; +} + /* The argument count check every method starts with (after the receiver * check the compiler's call shape already guarantees). */ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { @@ -595,9 +912,158 @@ static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); } +/* AlgorithmIdentifier: an object, or a value converted to a DOMString. */ +static bool scr_wc_algorithm_identifier(ScrDyn *value) { + if (scr_wc_is_object(value)) return true; + ScrStr *s = scr_wc_dom_string(value); + if (!s) return false; + scr_str_release(s); + return true; +} + +/* macImportKey: the usage check, the format, then normalizeKeyLength. */ +static ScrDyn *scr_wc_import_hmac(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages("HMAC", usages)) return NULL; + ScrBytes *bytes = NULL; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (strcmp(format, "jwk") == 0) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "sig"); + if (!bytes) return NULL; + char buf[16]; + const char *expected = scr_wc_jwk_alg("HMAC", algorithm->hash, 0, buf, sizeof buf); + if (jwk->alg && expected && !scr_wc_str_is(jwk->alg, expected)) { + scr_bytes_release(bytes); + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + return NULL; + } + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + if (algorithm->has_length) { + if (scr_wc_bits_to_bytes(algorithm->length) != len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + length = algorithm->length; + } + result = scr_crypto_key_new(view->data, len, "HMAC", algorithm->hash, length, + (unsigned)usages, extractable); + ScrCryptoKey *key = scr_crypto_key_of(result); + scr_wc_truncate_bits(key->data, key->len, length); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* aesImportKey: the usage check, then the format and the key length. */ +static ScrDyn *scr_wc_import_aes(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const char *name, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages(name, usages)) return NULL; + ScrBytes *bytes = NULL; + bool from_jwk = strcmp(format, "jwk") == 0; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (from_jwk) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "enc"); + if (!bytes) return NULL; + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (length != 128 && length != 192 && length != 256) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + char buf[16]; + if (from_jwk && jwk->alg && + !scr_wc_str_is(jwk->alg, scr_wc_jwk_alg(name, NULL, length, buf, sizeof buf))) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + result = scr_crypto_key_new(view->data, len, name, NULL, length, (unsigned)usages, extractable); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* importGenericSecretKey (PBKDF2, HKDF): never extractable, derivation + * usages only, raw bytes of any length. */ +static ScrDyn *scr_wc_import_kdf(const char *format, ScrBytes *data, const char *name, + bool extractable, long usages, bool *unsupported) { + if (extractable) { + char message[64]; + snprintf(message, sizeof message, "%s keys are not extractable", name); + scr_throw_domex("SyntaxError", message); + return NULL; + } + if (!scr_wc_check_usages(name, usages)) return NULL; + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + *unsupported = true; + return NULL; + } + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + return scr_crypto_key_new(view->data, (size_t)scr_bytes_byte_len(view), name, NULL, 0, + (unsigned)usages, false); +} + +/* importKeySync over converted arguments: the per-algorithm import, the + * unsupported-format NotSupportedError, then the empty-usages check. */ +static ScrDyn *scr_wc_import_key_sync(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, + long usages) { + bool unsupported = false; + ScrDyn *key; + if (strcmp(algorithm->name, "HMAC") == 0) + key = scr_wc_import_hmac(format, data, jwk, algorithm, extractable, usages, &unsupported); + else if (scr_wc_is_aes(algorithm->name)) + key = scr_wc_import_aes(format, data, jwk, algorithm->name, extractable, usages, &unsupported); + else if (scr_wc_is_kdf(algorithm->name)) + key = scr_wc_import_kdf(format, data, algorithm->name, extractable, usages, &unsupported); + else { + scr_wc_unsupported_algorithm("importKey", algorithm->name); + return NULL; + } + if (unsupported) { + char message[96]; + snprintf(message, sizeof message, "Unable to import %s using %s format", algorithm->name, + format); + scr_throw_domex("NotSupportedError", message); + return NULL; + } + if (key && usages == 0) { + scr_dyn_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + return NULL; + } + return key; +} + /* importKey(format, keyData, algorithm, extractable, keyUsages): Node's - * conversion order, then macImportKey's checks and importKeySync's empty - * usage check. */ + * conversion order, then normalization and importKeySync. */ static ScrDyn *scr_wc_import(const ScrDyn *args) { static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; if (!scr_wc_required(prefix, args, 5)) return NULL; @@ -605,77 +1071,28 @@ static ScrDyn *scr_wc_import(const ScrDyn *args) { scr_wc_args_take(&a, args, 5); ScrDyn *result = NULL; ScrBytes *data = NULL; + ScrWcJwk jwk; + memset(&jwk, 0, sizeof jwk); const char *format = scr_wc_key_format(prefix, a.v[0]); if (!format) goto done; - bool jwk = strcmp(format, "jwk") == 0; - /* The JsonWebKey dictionary is not implemented; that format is refused - * after the remaining arguments convert. */ - if (!jwk) { + if (strcmp(format, "jwk") == 0) { + if (!scr_wc_jwk_convert(prefix, a.v[1], &jwk)) goto done; + } else { data = scr_wc_buffer_source(prefix, 1, a.v[1]); if (!data) goto done; } - if (!scr_wc_is_object(a.v[2])) { - ScrStr *s = scr_wc_dom_string(a.v[2]); - if (!s) goto done; - scr_str_release(s); - } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; bool extractable = scr_dyn_truthy(a.v[3]); long usages = scr_wc_key_usages(prefix, 4, a.v[4]); if (usages < 0) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, - &algorithm)) + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &algorithm)) goto done; - if (strcmp(algorithm.name, "HMAC") != 0) { - scr_wc_unsupported_algorithm("importKey", algorithm.name); - goto done; - } - if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { - scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); - goto done; - } - if (jwk) { - scr_wc_unsupported("importKey", "the 'jwk' key format"); - goto done; - } - if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { - char message[96]; - snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); - scr_throw_domex("NotSupportedError", message); - goto done; - } - { - ScrBytes *view = data; - SCR_BYTES_SNAPSHOT(view); - size_t len = (size_t)scr_bytes_byte_len(view); - if (len == 0) { - scr_throw_domex("DataError", "Zero-length key is not supported"); - goto done; - } - double length = (double)len * 8; - unsigned char last = view->data[len - 1]; - if (algorithm.has_length) { - double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); - if (bytes != (double)len) { - scr_throw_domex("DataError", "Invalid key length"); - goto done; - } - /* A length that is not a whole number of bytes clears the unused - * low bits of the final byte (truncateToBitLength). */ - int extra = (int)fmod(algorithm.length, 8); - if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); - length = algorithm.length; - } - if (usages == 0) { - scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); - goto done; - } - result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, - extractable); - scr_crypto_key_of(result)->data[len - 1] = last; - } + result = scr_wc_import_key_sync(format, data, &jwk, &algorithm, extractable, usages); done: if (data) scr_bytes_release(data); + scr_wc_jwk_drop(&jwk); scr_wc_args_drop(&a, 5); return result; } @@ -683,7 +1100,118 @@ done: ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { ScrDyn *key = scr_wc_import(args); if (!key) return scr_wc_rejected(); - return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); + return scr_wc_resolved(key); +} + +/* generateKey(algorithm, extractable, keyUsages) for HMAC and AES keys: + * random bytes of the requested length (an HMAC key defaults to its + * hash's block size; a length that is not a whole number of bytes clears + * the final byte's unused low bits). */ +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrDyn *key = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + bool extractable = scr_dyn_truthy(a.v[1]); + long usages = scr_wc_key_usages(prefix, 2, a.v[2]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_key_names, SCR_WC_COUNT(scr_wc_key_names), + SCR_WC_OP_GENERATE, &algorithm)) + goto done; + bool hmac = strcmp(algorithm.name, "HMAC") == 0; + if (!hmac && !scr_wc_is_aes(algorithm.name)) { + scr_wc_unsupported_algorithm("generateKey", algorithm.name); + goto done; + } + if (!scr_wc_check_usages(algorithm.name, usages)) goto done; + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key."); + goto done; + } + double length = algorithm.has_length ? algorithm.length + : strcmp(algorithm.hash, "SHA-384") == 0 || strcmp(algorithm.hash, "SHA-512") == 0 + ? 1024 + : 512; + size_t len = scr_wc_bits_to_bytes(length); + unsigned char *bytes = malloc(len ? len : 1); + if (!bytes) scr_trap("scriptc: out of memory\n"); + arc4random_buf(bytes, len); + scr_wc_truncate_bits(bytes, len, length); + key = scr_crypto_key_new(bytes, len, algorithm.name, hmac ? algorithm.hash : NULL, length, + (unsigned)usages, extractable); + volatile unsigned char *wipe = bytes; + for (size_t i = 0; i < len; i++) wipe[i] = 0; + free(bytes); +done: + scr_wc_args_drop(&a, 3); + if (!key) return scr_wc_rejected(); + return scr_wc_deferred(key); +} + +/* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer + * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */ +static ScrDyn *scr_wc_export(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrDyn *result = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (scr_wc_is_kdf(key->name)) { + char message[64]; + snprintf(message, sizeof message, "%s key export is not supported", key->name); + scr_throw_domex("NotSupportedError", message); + goto done; + } + if (!key->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + goto done; + } + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } else if (strcmp(format, "jwk") == 0) { + char buf[16]; + const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + ScrStr *encoding = scr_str_new("base64url", 9); + ScrStr *k = scr_bytes_to_str(bytes, encoding); + scr_str_release(encoding); + scr_bytes_release(bytes); + result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "key_ops", 7, scr_wc_usage_array(key->usages)); + scr_dyn_obj_set(result, "ext", 3, scr_dyn_new_bool(key->extractable)); + if (alg) scr_dyn_obj_set(result, "alg", 3, scr_wc_cstr(alg)); + scr_dyn_obj_set(result, "kty", 3, scr_wc_cstr("oct")); + scr_dyn_obj_set(result, "k", 1, scr_dyn_new_str(k)); + scr_str_release(k); + } else { + char message[96]; + snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, + format); + scr_throw_domex("NotSupportedError", message); + } +done: + scr_wc_args_drop(&a, 2); + return result; +} + +ScrPromise *scr_webcrypto_export_key(ScrDyn *args) { + ScrDyn *result = scr_wc_export(args); + if (!result) return scr_wc_rejected(); + return scr_wc_resolved(result); } /* sign(algorithm, key, data) / verify(algorithm, key, signature, data): @@ -717,13 +1245,17 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); if (!data) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, - &algorithm)) + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), + SCR_WC_OP_SIGN, &algorithm)) goto done; if (strcmp(algorithm.name, "HMAC") != 0) { scr_wc_unsupported_algorithm(method, algorithm.name); goto done; } + if (strcmp(key->name, algorithm.name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { char message[64]; snprintf(message, sizeof message, "Unable to use this key to %s", method); @@ -827,3 +1359,563 @@ done: scr_crypto_promise_settle_array_buffer); return p; } + +/* ── encrypt and decrypt ────────────────────────────────────────────── + * The WebCrypto half of encrypt and decrypt: argument conversion, the + * AesCbcParams, AesCtrParams and AeadParams dictionaries, and the key + * checks. The cipher itself runs in scr_webcrypto_aes.c, which links + * with the mbedTLS archive and passes it in. */ +static const char *const scr_wc_cipher_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-OCB", "ChaCha20-Poly1305", "RSA-OAEP", +}; + +/* Node's limit on the data, IV and additional data (kMaxBufferLength). */ +static const double scr_wc_max_buffer = 2147483647.0; + +/* A BufferSource member of an algorithm dictionary: false with the error + * pending; *out stays NULL when an optional member is absent. */ +static bool scr_wc_member_buffer(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, ScrBytes **out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + if (value->kind == SCR_DYN_UNDEF) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) ok = false; + else if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + *out = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in passed algorithm is not instance of ArrayBuffer, Buffer, " + "TypedArray, or DataView.", + scr_wc_normalize_prefix, key); + ok = false; + } + if (plain) scr_dyn_release(plain); + } + scr_dyn_release(value); + return ok; +} + +/* validateByteLength / validateMaxBufferLength over a converted member. */ +static bool scr_wc_exact_length(const ScrBytes *bytes, const char *name, double length) { + if (scr_bytes_byte_len(bytes) == length) return true; + char message[96]; + snprintf(message, sizeof message, "%s must contain exactly %.0f bytes", name, length); + scr_throw_domex("OperationError", message); + return false; +} + +static bool scr_wc_max_length(const ScrBytes *bytes, const char *name) { + if (scr_bytes_byte_len(bytes) <= scr_wc_max_buffer) return true; + char message[96]; + snprintf(message, sizeof message, "%s must be at most %.0f bytes", name, scr_wc_max_buffer); + scr_throw_domex("OperationError", message); + return false; +} + +typedef struct { + ScrBytes *iv; /* the IV or the CTR counter block */ + ScrBytes *aad; + double tag_bits; + double counter_bits; +} ScrWcCipherParams; + +static void scr_wc_cipher_params_drop(ScrWcCipherParams *params) { + if (params->iv) scr_bytes_release(params->iv); + if (params->aad) scr_bytes_release(params->aad); + params->iv = params->aad = NULL; +} + +/* An optional or required [EnforceRange] octet member: false with the + * error pending; *present says whether it was given. */ +static bool scr_wc_member_octet(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, bool *present, double *out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + *present = value->kind != SCR_DYN_UNDEF; + if (!*present) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + char context[48]; + snprintf(context, sizeof context, "%s in passed algorithm", key); + ok = scr_wc_enforce_range(scr_wc_normalize_prefix, context, value, 255, out); + } + scr_dyn_release(value); + return ok; +} + +/* normalizeAlgorithm(algorithm, 'encrypt' / 'decrypt'): the canonical + * name and the AES dictionaries' members, in Web IDL member order with + * each member's validator right after its conversion. */ +static const char *scr_wc_normalize_cipher(ScrDyn *identifier, const char *method, + ScrWcCipherParams *params) { + memset(params, 0, sizeof *params); + params->tag_bits = 128; + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = + scr_wc_lookup(scr_wc_cipher_names, SCR_WC_COUNT(scr_wc_cipher_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_is_aes(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return NULL; + bool ok = true; + bool present; + if (strcmp(canonical, "AES-CBC") == 0) { + ok = scr_wc_member_buffer(dict, "AesCbcParams", "iv", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.iv", 16); + } else if (strcmp(canonical, "AES-CTR") == 0) { + ok = scr_wc_member_buffer(dict, "AesCtrParams", "counter", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.counter", 16) && + scr_wc_member_octet(dict, "AesCtrParams", "length", true, &present, + ¶ms->counter_bits); + if (ok && (params->counter_bits == 0 || params->counter_bits > 128)) { + scr_throw_domex("OperationError", "AES-CTR algorithm.length must be between 1 and 128"); + ok = false; + } + } else { + ok = scr_wc_member_buffer(dict, "AeadParams", "additionalData", false, ¶ms->aad) && + (!params->aad || scr_wc_max_length(params->aad, "algorithm.additionalData")) && + scr_wc_member_buffer(dict, "AeadParams", "iv", true, ¶ms->iv) && + scr_wc_max_length(params->iv, "algorithm.iv") && + scr_wc_member_octet(dict, "AeadParams", "tagLength", false, &present, ¶ms->tag_bits); + if (ok && !present) params->tag_bits = 128; + double t = params->tag_bits; + if (ok && t != 32 && t != 64 && t != 96 && t != 104 && t != 112 && t != 120 && t != 128) { + char message[64]; + snprintf(message, sizeof message, "%.0f is not a valid AES-GCM tag length", t); + scr_throw_domex("OperationError", message); + ok = false; + } + } + scr_dyn_release(dict); + if (!ok) { + scr_wc_cipher_params_drop(params); + return NULL; + } + return canonical; +} + +static void *scr_wc_caught_retain(void *c) { return scr_caught_retain(c); } +static void scr_wc_caught_release(void *c) { scr_caught_release(c); } + +static void scr_wc_settle_rethrow(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + (void)value; + ScrPromise *p; + ScrCaught *caught; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + memcpy(&caught, &cb->caps[1]->slot, sizeof caught); + scr_rethrow(caught); + scr_promise_reject_pending(p); +} + +/* A job failure: a fresh promise that rejects with the pending exception + * on a later loop turn, as Node's job promises do (+1). */ +static ScrPromise *scr_wc_deferred_rejection(void) { + ScrPromise *p = scr_promise_new(); + ScrClosure *cb = scr_closure_new(NULL, 2); + cb->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_promise_retain(p)); + cb->caps[1] = scr_box_new_obj(scr_wc_caught_retain, scr_wc_caught_release, scr_caught_trace_v); + scr_box_set_ref(cb->caps[1], scr_exc_take()); + scr_crypto_defer_bytes(NULL, NULL, cb, scr_wc_settle_rethrow); + return p; +} + +/* A promise that fulfills with an ArrayBuffer of `bytes` (moves) on a + * later loop turn. */ +static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(bytes, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes) { + const char *method = decrypt ? "decrypt" : "encrypt"; + const char *prefix = decrypt ? "Failed to execute 'decrypt' on 'SubtleCrypto'" + : "Failed to execute 'encrypt' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrBytes *data = NULL; + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + data = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!data) goto done; + const char *name = scr_wc_normalize_cipher(a.v[0], method, ¶ms); + if (!name) goto done; + if (strcmp(key->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(key->usages & (decrypt ? SCR_WC_USAGE_DECRYPT : SCR_WC_USAGE_ENCRYPT))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + if (!scr_wc_max_length(data, "data")) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + ScrBytes *iv = params.iv; + SCR_BYTES_SNAPSHOT(iv); + if (!params.aad) params.aad = scr_bytes_new(SCR_BYTES_U8, 0); + ScrBytes *aad = params.aad; + SCR_BYTES_SNAPSHOT(aad); + ScrWebCryptoAesParams run = { + .iv = iv->data, + .iv_len = (size_t)scr_bytes_byte_len(iv), + .aad = aad->data, + .aad_len = (size_t)scr_bytes_byte_len(aad), + .tag_len = (size_t)(params.tag_bits / 8), + .counter_bits = (unsigned)params.counter_bits, + }; + ScrBytes *out = aes(name, decrypt, key->data, key->len, &run, view->data, + (size_t)scr_bytes_byte_len(view)); + if (out) { + result = scr_wc_deferred_buffer(out); + } else { + scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); + result = scr_wc_deferred_rejection(); + } + } +done: + if (data) scr_bytes_release(data); + scr_wc_cipher_params_drop(¶ms); + scr_wc_args_drop(&a, 3); + return result ? result : scr_wc_rejected(); +} + +/* ── deriveBits and deriveKey ───────────────────────────────────────── + * PBKDF2 and HKDF over node:crypto's KDF core (scr_crypto_pbkdf2 and + * scr_crypto_hkdf_bytes in scr_lib.c), with Node's Pbkdf2Params and + * HkdfParams dictionaries, length rules and key checks. deriveKey derives + * the bits the derived key type needs and imports them as a raw-secret + * key of that type. */ +static const char *const scr_wc_derive_names[] = { + "Argon2d", "Argon2i", "Argon2id", "ECDH", "HKDF", "PBKDF2", "X25519", "X448", +}; +/* The algorithms 'get key length' registers. */ +static const char *const scr_wc_length_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", + "Argon2id", "ChaCha20-Poly1305", "HKDF", "HMAC", "KMAC128", "KMAC256", "PBKDF2", +}; + +typedef struct { + const char *name; + const char *hash; + double iterations; + ScrBytes *salt; + ScrBytes *info; +} ScrWcKdfParams; + +static void scr_wc_kdf_params_drop(ScrWcKdfParams *params) { + if (params->salt) scr_bytes_release(params->salt); + if (params->info) scr_bytes_release(params->info); + params->salt = params->info = NULL; +} + +/* A HashAlgorithmIdentifier member with ensureSHA (`dict` names the + * dictionary); *hash is the member value for the later digest + * normalization (+1). */ +static bool scr_wc_member_hash(const ScrDyn *dict, const char *dict_name, ScrDyn **hash) { + ScrDyn *value = scr_wc_member(dict, "hash"); + if (value->kind == SCR_DYN_UNDEF) { + scr_dyn_release(value); + scr_wc_missing_member(dict_name, "hash"); + return false; + } + ScrStr *name = NULL; + bool ok = true; + if (scr_wc_is_object(value)) { + ScrDyn *plain = scr_wc_plain(value); + ScrDyn *member = plain ? scr_wc_member(plain, "name") : NULL; + if (plain) scr_dyn_release(plain); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + *hash = scr_dyn_retain(value); + } else { + name = scr_wc_dom_string(value); + if (!name) ok = false; + *hash = name ? scr_dyn_new_str(name) : NULL; + } + scr_dyn_release(value); + bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && + (name->data[1] | 32) == 'h' && (name->data[2] | 32) == 'a'; + if (name) scr_str_release(name); + if (ok && !sha) { + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict_name); + scr_throw_domex("NotSupportedError", message); + ok = false; + } + if (!ok && *hash) { + scr_dyn_release(*hash); + *hash = NULL; + } + return ok; +} + +/* normalizeAlgorithm(algorithm, 'deriveBits') for PBKDF2 and HKDF. */ +static bool scr_wc_normalize_kdf(ScrDyn *identifier, const char *method, ScrWcKdfParams *params) { + memset(params, 0, sizeof *params); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + params->name = scr_wc_lookup(scr_wc_derive_names, SCR_WC_COUNT(scr_wc_derive_names), name); + scr_str_release(name); + if (!params->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (!scr_wc_is_kdf(params->name)) { + scr_wc_unsupported_algorithm(method, params->name); + return false; + } + bool pbkdf2 = strcmp(params->name, "PBKDF2") == 0; + const char *dict_name = pbkdf2 ? "Pbkdf2Params" : "HkdfParams"; + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return false; + ScrDyn *hash = NULL; + bool ok = scr_wc_member_hash(dict, dict_name, &hash); + if (ok && pbkdf2) { + ScrDyn *iterations = scr_wc_member(dict, "iterations"); + if (iterations->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member(dict_name, "iterations"); + ok = false; + } else if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "iterations in passed algorithm", + iterations, 4294967295.0, ¶ms->iterations)) { + ok = false; + } else if (params->iterations == 0) { + scr_throw_domex("OperationError", "iterations cannot be zero"); + ok = false; + } else if (params->iterations > 2147483647.0) { + scr_throw_domex("NotSupportedError", "iterations exceeds the implementation limit"); + ok = false; + } + scr_dyn_release(iterations); + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } else if (ok) { + ok = scr_wc_member_buffer(dict, dict_name, "info", true, ¶ms->info); + if (ok && scr_bytes_byte_len(params->info) > 1024) { + scr_throw_domex("OperationError", "algorithm.info must be at most 1024 bytes"); + ok = false; + } + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } + scr_dyn_release(dict); + if (ok) { + params->hash = scr_wc_digest_algorithm(hash, method); + if (!params->hash) ok = false; + } + if (hash) scr_dyn_release(hash); + if (!ok) scr_wc_kdf_params_drop(params); + return ok; +} + +/* The length rules of pbkdf2DeriveBits and hkdfDeriveBits; `null_length` + * says the length was null. */ +static bool scr_wc_kdf_length(const ScrWcKdfParams *params, bool null_length, double length) { + if (null_length) { + scr_throw_domex("OperationError", "length cannot be null"); + return false; + } + if (fmod(length, 8) != 0) { + scr_throw_domex("OperationError", "length must be a multiple of 8"); + return false; + } + if (strcmp(params->name, "HKDF") == 0) { + double digest = strcmp(params->hash, "SHA-1") == 0 ? 20 + : strcmp(params->hash, "SHA-256") == 0 ? 32 + : strcmp(params->hash, "SHA-384") == 0 ? 48 + : 64; + if (length > 255 * digest * 8) { + scr_throw_domex("OperationError", "length exceeds the maximum derived bit length"); + return false; + } + } + return true; +} + +/* The derived bytes (+1); length is a nonzero multiple of 8. */ +static ScrBytes *scr_wc_kdf_derive(const ScrWcKdfParams *params, const ScrCryptoKey *key, + double length) { + ScrBytes *secret = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(secret->data, key->data, key->len); + const char *native = scr_wc_native_hash(params->hash); + ScrStr *digest = scr_str_new(native, strlen(native)); + ScrBytes *out; + if (strcmp(params->name, "PBKDF2") == 0) { + out = scr_crypto_pbkdf2(secret, params->salt, params->iterations, length / 8, digest); + } else { + out = scr_crypto_hkdf_bytes(digest, secret, params->salt, params->info, length / 8); + } + scr_str_release(digest); + volatile unsigned char *wipe = secret->data; + for (size_t i = 0; i < key->len; i++) wipe[i] = 0; + scr_bytes_release(secret); + return out; +} + +/* The base key checks deriveBits and deriveKey share. */ +static bool scr_wc_check_base_key(const ScrCryptoKey *key, const char *algorithm, unsigned usage, + const char *usage_name) { + if (!(key->usages & usage)) { + char message[64]; + snprintf(message, sizeof message, "baseKey does not have %s usage", usage_name); + scr_throw_domex("InvalidAccessError", message); + return false; + } + if (strcmp(key->name, algorithm) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + return false; + } + return true; +} + +/* deriveBits(algorithm, baseKey[, length]). */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveBits' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + size_t argc = (size_t)scr_dyn_arr_len(args); + ScrWcArgs a; + size_t n = argc > 3 ? 3 : argc; + scr_wc_args_take(&a, args, n); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + /* length defaults to null when absent or undefined. */ + bool null_length = n < 3 || a.v[2]->kind == SCR_DYN_UNDEF || a.v[2]->kind == SCR_DYN_NULL; + double length = 0; + if (!null_length && !scr_wc_enforce_range(prefix, "3rd argument", a.v[2], 4294967295.0, &length)) + goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveBits", ¶ms)) goto done; + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_BITS, "deriveBits")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + if (length == 0) { + ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); + result = scr_wc_resolved(scr_array_buffer_from_bytes(empty)); + scr_bytes_release(empty); + goto done; + } + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (bits) result = scr_wc_deferred_buffer(bits); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, n); + return result ? result : scr_wc_rejected(); +} + +/* getKeyLength for the derived key type: the bits to derive, or *null + * for the KDF types (whose derivation then rejects). */ +static bool scr_wc_derived_length(ScrDyn *identifier, const ScrWcAlgorithm *imported, + bool *null_length, double *length) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + const char *canonical = + scr_wc_lookup(scr_wc_length_names, SCR_WC_COUNT(scr_wc_length_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + *null_length = false; + if (scr_wc_is_aes(canonical)) { + ScrWcAlgorithm aes; + memset(&aes, 0, sizeof aes); + if (!scr_wc_aes_length_params(identifier, "AesDerivedKeyParams", &aes)) return false; + *length = aes.length; + return true; + } + if (strcmp(canonical, "HMAC") == 0 && imported->hash) { + *length = imported->has_length ? imported->length + : strcmp(imported->hash, "SHA-384") == 0 || strcmp(imported->hash, "SHA-512") == 0 + ? 1024 + : 512; + return true; + } + *null_length = true; + return true; +} + +/* deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages): + * the derivation's errors reject at once; the import of the derived bits + * settles, or rejects, on a later loop turn. */ +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveKey", ¶ms)) goto done; + ScrWcAlgorithm imported; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &imported)) + goto done; + bool null_length; + double length; + if (!scr_wc_derived_length(a.v[2], &imported, &null_length, &length)) goto done; + if (strcmp(imported.name, "HMAC") != 0 && !scr_wc_is_aes(imported.name) && + !scr_wc_is_kdf(imported.name)) { + scr_wc_unsupported_algorithm("deriveKey", imported.name); + goto done; + } + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_KEY, "deriveKey")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (!bits) goto done; + ScrDyn *derived = scr_wc_import_key_sync("raw-secret", bits, NULL, &imported, extractable, usages); + scr_bytes_release(bits); + result = derived ? scr_wc_deferred(derived) : scr_wc_deferred_rejection(); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, 5); + return result ? result : scr_wc_rejected(); +} diff --git a/packages/runtime/src/scr_webcrypto_aes.c b/packages/runtime/src/scr_webcrypto_aes.c new file mode 100644 index 000000000..7cb63f561 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_aes.c @@ -0,0 +1,137 @@ +/* WebCrypto's AES ciphers: crypto.subtle.encrypt and decrypt with AES-CBC, + * AES-CTR and AES-GCM keys, over the vendored mbedTLS block cipher. + * + * scr_webcrypto.c converts and validates the arguments, normalizes the + * algorithm and checks the key; this unit runs the cipher itself, the way + * Node 24's src/crypto/crypto_aes.cc does: CBC with PKCS#7 padding, CTR + * with a counter that wraps within its low `length` bits (and fails when + * the input needs more blocks than those bits can count), and GCM with a + * 12 to 128 byte IV and a 4 to 16 byte tag appended to the ciphertext. + * The unit links only into programs that call these methods, alongside + * the cipher feature's mbedTLS archive. */ +#include "scr_runtime.h" + +#include +#include + +#include "mbedtls/aes.h" +#include "mbedtls/gcm.h" +#include "mbedtls/platform_util.h" + +static ScrBytes *scr_wc_aes_gcm(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (p->iv_len < 12 || p->iv_len > 128) return NULL; + if (decrypt && len < p->tag_len) return NULL; + size_t out_len = decrypt ? len - p->tag_len : len + p->tag_len; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + mbedtls_gcm_context gcm; + mbedtls_gcm_init(&gcm); + int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, (unsigned)(key_len * 8)); + if (rc == 0 && decrypt) { + rc = mbedtls_gcm_auth_decrypt(&gcm, out_len, p->iv, p->iv_len, p->aad, p->aad_len, + in + out_len, p->tag_len, in, out->data); + } else if (rc == 0) { + rc = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, len, p->iv, p->iv_len, p->aad, + p->aad_len, in, out->data, p->tag_len, out->data + len); + } + mbedtls_gcm_free(&gcm); + if (rc != 0) { + mbedtls_platform_zeroize(out->data, out_len); + scr_bytes_release(out); + return NULL; + } + return out; +} + +static ScrBytes *scr_wc_aes_cbc(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (decrypt && (len == 0 || len % 16 != 0)) return NULL; + size_t padded = decrypt ? len : (len / 16 + 1) * 16; + unsigned char *buf = malloc(padded); + if (!buf) scr_trap("scriptc: out of memory\n"); + memcpy(buf, in, len); + if (!decrypt) memset(buf + len, (int)(padded - len), padded - len); + unsigned char iv[16]; + memcpy(iv, p->iv, 16); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + int rc = decrypt ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8)) + : mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)); + if (rc == 0) + rc = mbedtls_aes_crypt_cbc(&aes, decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, padded, + iv, buf, buf); + mbedtls_aes_free(&aes); + size_t out_len = padded; + if (rc == 0 && decrypt) { + /* PKCS#7: the final byte names 1 to 16 bytes of padding, each of + * which holds that value. Checked without data-dependent branches. */ + unsigned char pad = buf[padded - 1]; + unsigned char diff = (unsigned char)-(pad == 0 || pad > 16); + for (unsigned i = 1; i <= 16; i++) + diff |= (unsigned char)((buf[padded - i] ^ pad) & (unsigned char)-(i <= pad)); + if (diff) rc = -1; + else out_len = padded - pad; + } + ScrBytes *out = NULL; + if (rc == 0) { + out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + memcpy(out->data, buf, out_len); + } + mbedtls_platform_zeroize(buf, padded); + free(buf); + return out; +} + +/* Adds one to the low `bits` bits of a big-endian counter block, leaving + * the bits above them unchanged. */ +static void scr_wc_ctr_increment(unsigned char block[16], unsigned bits) { + for (int i = 15; i >= 0 && bits > 0; i--) { + unsigned width = bits >= 8 ? 8 : bits; + unsigned mask = (1u << width) - 1; + unsigned low = ((unsigned)block[i] + 1) & mask; + block[i] = (unsigned char)((block[i] & ~mask) | low); + bits -= width; + if (low != 0) return; + } +} + +static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + size_t blocks = (len + 15) / 16; + if (p->counter_bits < 64 && (double)blocks > (double)(1ull << p->counter_bits)) return NULL; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + if (mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)) != 0) { + mbedtls_aes_free(&aes); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + unsigned char counter[16], stream[16]; + memcpy(counter, p->iv, 16); + for (size_t off = 0; off < len; off += 16) { + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, counter, stream); + size_t n = len - off < 16 ? len - off : 16; + for (size_t i = 0; i < n; i++) out->data[off + i] = (unsigned char)(in[off + i] ^ stream[i]); + scr_wc_ctr_increment(counter, p->counter_bits); + } + mbedtls_platform_zeroize(stream, sizeof stream); + mbedtls_aes_free(&aes); + return out; +} + +/* The cipher scr_webcrypto_cipher runs: +1 output bytes, or NULL when the + * operation fails (Node's OperationError). */ +static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *p, + const unsigned char *in, size_t len) { + if (strcmp(name, "AES-GCM") == 0) return scr_wc_aes_gcm(decrypt, key, key_len, p, in, len); + if (strcmp(name, "AES-CBC") == 0) return scr_wc_aes_cbc(decrypt, key, key_len, p, in, len); + return scr_wc_aes_ctr(key, key_len, p, in, len); +} + +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, false, scr_wc_aes); } + +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, true, scr_wc_aes); } diff --git a/packages/runtime/src/scr_webcrypto_object.c b/packages/runtime/src/scr_webcrypto_object.c new file mode 100644 index 000000000..c20ef13dc --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_object.c @@ -0,0 +1,53 @@ +/* The crypto global as a value: node:crypto's dynamic Crypto object + * (scr_crypto_native in scr_async_dyn.c, which carries subtle.digest) with + * the Crypto object's own randomUUID and getRandomValues methods, for + * untyped reads such as uuid's `crypto.randomUUID && crypto.randomUUID()`. + * Node's receiver check (ERR_INVALID_THIS on a detached call) is not + * modeled. There is one object per thread, so every spelling of the + * global (crypto, globalThis.crypto, node:crypto's webcrypto) is the same + * value; it is released at exit. Linked only into programs that read the + * global as a value. */ +#include "scr_runtime.h" + +static ScrDyn *scr_webcrypto_random_uuid_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrStr *uuid = scr_crypto_random_uuid(); + ScrDyn *result = scr_dyn_new_str(uuid); + scr_str_release(uuid); + return result; +} + +static ScrDyn *scr_webcrypto_get_random_values_call(ScrClosure *closure, ScrDyn *const *args, + size_t argc) { + (void)closure; + if (argc == 0) { + static const char message[] = + "Failed to execute 'getRandomValues' on 'Crypto': 1 argument required, but only 0 present."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + return scr_crypto_get_random_values_dyn(args[0]); +} + +static SCR_TL ScrDyn *scr_webcrypto_object_value; + +static void scr_webcrypto_object_cleanup(void) { + if (scr_webcrypto_object_value) scr_dyn_release(scr_webcrypto_object_value); + scr_webcrypto_object_value = NULL; +} + +ScrDyn *scr_webcrypto_object(void) { + if (scr_webcrypto_object_value) return scr_dyn_retain(scr_webcrypto_object_value); + ScrDyn *result = scr_crypto_native(); + scr_dyn_obj_set(result, "randomUUID", 10, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_random_uuid_call, 0, + "crypto.randomUUID", "randomUUID")); + scr_dyn_obj_set(result, "getRandomValues", 15, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_get_random_values_call, 1, + "crypto.getRandomValues", "getRandomValues")); + scr_webcrypto_object_value = result; + scr_atexit(scr_webcrypto_object_cleanup); + return scr_dyn_retain(result); +} diff --git a/tests/corpus/7120-webcrypto-random-uuid.ts b/tests/corpus/7120-webcrypto-random-uuid.ts new file mode 100644 index 000000000..d35df97cc --- /dev/null +++ b/tests/corpus/7120-webcrypto-random-uuid.ts @@ -0,0 +1,22 @@ +// crypto.randomUUID() on the WebCrypto global, directly and through +// globalThis: version-4 UUIDs, distinct on every call, and inside +// closures and default parameters the way request-id middleware uses it. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const direct: string = crypto.randomUUID(); +console.log(direct.length, shape.test(direct)); +const viaGlobalThis = globalThis.crypto.randomUUID(); +console.log(viaGlobalThis.length, shape.test(viaGlobalThis), viaGlobalThis !== direct); + +const seen = new Set(); +for (let i = 0; i < 100; i++) seen.add(crypto.randomUUID()); +console.log("distinct", seen.size); + +function requestId(generator: () => string = () => crypto.randomUUID()): string { + return generator(); +} +console.log(shape.test(requestId()), requestId(() => "fixed")); + +const ids = ["a", "b", "c"].map((name) => ({ name, id: crypto.randomUUID() })); +console.log(ids.every((entry) => shape.test(entry.id)), ids[0]!.id !== ids[1]!.id); +console.log(typeof crypto.randomUUID()); diff --git a/tests/corpus/7121-webcrypto-crypto-object.mjs b/tests/corpus/7121-webcrypto-crypto-object.mjs new file mode 100644 index 000000000..058e72153 --- /dev/null +++ b/tests/corpus/7121-webcrypto-crypto-object.mjs @@ -0,0 +1,35 @@ +// The crypto global from JavaScript: uuid's feature test +// (`crypto.randomUUID && crypto.randomUUID()`), the methods read as values, +// and getRandomValues through the dynamic object, including its +// missing-argument and non-integer-view errors. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +function v4(options) { + if (!options && crypto.randomUUID) { + return crypto.randomUUID(); + } + return "fallback"; +} +console.log(shape.test(v4()), v4({})); +console.log(shape.test(globalThis.crypto.randomUUID())); +console.log(typeof crypto.randomUUID, typeof crypto.getRandomValues, typeof crypto.subtle); +console.log(crypto.randomUUID.name, crypto.getRandomValues.name, crypto.getRandomValues.length); + +const native = typeof crypto !== "undefined" && crypto.randomUUID ? { randomUUID: crypto.randomUUID.bind(crypto) } : {}; +console.log(shape.test(native.randomUUID())); + +const rng = (n) => crypto.getRandomValues(new Uint8Array(n)); +console.log(rng(16).length, rng(16) instanceof Uint8Array); +const words = new Uint32Array(4); +console.log(crypto.getRandomValues(words) === words); + +function attempt(label, run) { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getRandomValues()); +attempt("float view", () => crypto.getRandomValues(new Float64Array(2))); +attempt("too long", () => crypto.getRandomValues(new Uint8Array(65537))); diff --git a/tests/corpus/7122-webcrypto-node-crypto.ts b/tests/corpus/7122-webcrypto-node-crypto.ts new file mode 100644 index 000000000..c0ada7939 --- /dev/null +++ b/tests/corpus/7122-webcrypto-node-crypto.ts @@ -0,0 +1,38 @@ +// WebCrypto reached through node:crypto: the webcrypto export (named and +// through the namespace), the module's own subtle export, and their +// identity with the crypto global. Each form runs the same HMAC and +// digest calls as the global. +import * as nodeCrypto from "node:crypto"; +import { subtle, webcrypto } from "node:crypto"; + +const encoder = new TextEncoder(); +const message = encoder.encode("node:crypto webcrypto"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +console.log(webcrypto === crypto, webcrypto === globalThis.crypto, nodeCrypto.webcrypto === crypto); +console.log(hex(await webcrypto.subtle.digest("SHA-256", message))); +console.log(hex(await subtle.digest("SHA-1", message))); +console.log(hex(await nodeCrypto.subtle.digest({ name: "SHA-384" }, message))); +console.log(hex(await nodeCrypto.webcrypto.subtle.digest("SHA-512", message))); + +const secret = encoder.encode("shared secret"); +const params = { name: "HMAC", hash: "SHA-256" }; +const signer = await webcrypto.subtle.importKey("raw", secret, params, false, ["sign"]); +const verifier = await subtle.importKey("raw", secret, params, false, ["verify"]); +const signature = await nodeCrypto.subtle.sign("HMAC", signer, message); +console.log(hex(signature)); +console.log(await nodeCrypto.webcrypto.subtle.verify("HMAC", verifier, signature, message)); +console.log(await subtle.verify("HMAC", verifier, signature, encoder.encode("other"))); +console.log(signer instanceof CryptoKey, signer.algorithm.name, verifier.usages.join(",")); + +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +console.log(shape.test(webcrypto.randomUUID()), shape.test(nodeCrypto.webcrypto.randomUUID())); +console.log(webcrypto.getRandomValues(new Uint32Array(4)).length); +try { + await subtle.digest("MD5", message); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} diff --git a/tests/corpus/7123-webcrypto-node-crypto.cjs b/tests/corpus/7123-webcrypto-node-crypto.cjs new file mode 100644 index 000000000..b11131d4b --- /dev/null +++ b/tests/corpus/7123-webcrypto-node-crypto.cjs @@ -0,0 +1,31 @@ +// node:crypto's WebCrypto from CommonJS: destructured webcrypto and +// subtle, the module object's members, the +// `globalThis.crypto ?? require("crypto").webcrypto` fallback libraries +// use to find a Crypto object, and crypto destructured from globalThis, +// all the same object. +const nodeCrypto = require("node:crypto"); +const { webcrypto, subtle } = require("crypto"); + +const encoder = new TextEncoder(); +const hex = (buffer) => Buffer.from(new Uint8Array(buffer)).toString("hex"); + +function cryptoObject() { + return globalThis.crypto ?? nodeCrypto.webcrypto; +} + +async function main() { + console.log(webcrypto === globalThis.crypto, cryptoObject() === crypto, typeof subtle); + console.log(hex(await webcrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + console.log(hex(await subtle.digest("SHA-1", encoder.encode("abc")))); + console.log(hex(await nodeCrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + const key = await nodeCrypto.webcrypto.subtle.importKey("raw", encoder.encode("k"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); + console.log(hex(await subtle.sign("HMAC", key, encoder.encode("abc")))); + const wc = cryptoObject(); + console.log(wc.randomUUID().length, wc.getRandomValues(new Uint8Array(8)).length); + console.log(typeof webcrypto.randomUUID, typeof nodeCrypto.webcrypto.getRandomValues); + const { crypto: destructured } = globalThis; + console.log(destructured === webcrypto, destructured.randomUUID().length); + console.log(hex(await destructured.subtle.digest("SHA-1", encoder.encode("abc")))); +} + +main(); diff --git a/tests/corpus/7124-webcrypto-secret-keys.ts b/tests/corpus/7124-webcrypto-secret-keys.ts new file mode 100644 index 000000000..8f1f5d601 --- /dev/null +++ b/tests/corpus/7124-webcrypto-secret-keys.ts @@ -0,0 +1,91 @@ +// crypto.crypto.subtle.generateKey, exportKey and importKey for secret keys: +// HMAC and AES keys generated at every length, exported as raw bytes and +// as JSON Web Keys, imported back from both forms, and PBKDF2 and HKDF +// base keys imported from raw bytes. Random key bytes are checked by +// length and round trip, not by value. +const encoder = new TextEncoder(); + +type Key = Awaited>; + +function describe(key: Key): string { + return JSON.stringify({ + type: key.type, + extractable: key.extractable, + algorithm: key.algorithm, + usages: key.usages, + }); +} + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +async function show(label: string, run: () => Promise): Promise { + try { + console.log(label, await run()); + } catch (e) { + const error = e as Error & { code?: string }; + console.log(label, "rejects", error.name, error.code, error.message); + } +} + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + await show(`hmac ${hash}`, async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash }, true, ["sign", "verify"]); + const raw = await crypto.subtle.exportKey("raw", key); + return `${describe(key)} ${raw.byteLength}`; + }); +} +await show("hmac length 100", async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash: { name: "SHA-512" }, length: 100 }, true, ["sign"]); + const raw = new Uint8Array(await crypto.subtle.exportKey("raw", key)); + return `${describe(key)} ${raw.length} ${raw[12]! & 0x0f}`; +}); +for (const name of ["AES-GCM", "AES-CBC", "AES-CTR", "AES-KW"]) { + for (const length of [128, 192, 256]) { + await show(`${name} ${length}`, async () => { + const usages: ("wrapKey" | "unwrapKey" | "encrypt" | "decrypt")[] = + name === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"]; + const key = await crypto.subtle.generateKey({ name, length }, true, usages); + const jwk = await crypto.subtle.exportKey("jwk", key); + return `${describe(key)} ${jwk.alg} ${jwk.kty} ${jwk.k!.length} ${JSON.stringify(jwk.key_ops)} ${jwk.ext}`; + }); + } +} + +// Fixed key bytes: the exported forms are deterministic. +const bytes = new Uint8Array(32); +for (let i = 0; i < bytes.length; i++) bytes[i] = i * 7 + 1; +const hmac = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-384" }, true, ["verify", "sign"]); +const hmacJwk = await crypto.subtle.exportKey("jwk", hmac); +console.log(JSON.stringify(hmacJwk), Object.keys(hmacJwk).join(",")); +console.log(hex(await crypto.subtle.exportKey("raw", hmac)) === Buffer.from(bytes).toString("hex")); +const reimported = await crypto.subtle.importKey("jwk", hmacJwk, { name: "HMAC", hash: "SHA-384" }, true, ["sign"]); +console.log(describe(reimported)); +const signature = await crypto.subtle.sign("HMAC", reimported, encoder.encode("payload")); +console.log(await crypto.subtle.verify("HMAC", hmac, signature, encoder.encode("payload"))); + +const short = await crypto.subtle.importKey("raw", bytes.subarray(0, 20), { name: "HMAC", hash: "SHA-1", length: 156 }, true, ["sign"]); +console.log(describe(short), JSON.stringify(await crypto.subtle.exportKey("jwk", short))); +console.log(hex(await crypto.subtle.exportKey("raw-secret", short))); + +for (const length of [16, 24, 32]) { + const key = await crypto.subtle.importKey("raw", bytes.subarray(0, length), "AES-GCM", true, ["encrypt"]); + const jwk = await crypto.subtle.exportKey("jwk", key); + console.log(describe(key), JSON.stringify(jwk)); + const back = await crypto.subtle.importKey("jwk", jwk, { name: "AES-GCM" }, false, ["encrypt"]); + console.log(describe(back)); +} +const cbc = await crypto.subtle.importKey("raw-secret", bytes.subarray(0, 16), "aes-cbc", true, ["decrypt", "unwrapKey"]); +console.log(describe(cbc), hex(await crypto.subtle.exportKey("raw", cbc))); +const wrap = await crypto.subtle.importKey("raw", bytes.subarray(0, 24), "AES-KW", true, ["unwrapKey", "wrapKey"]); +console.log(describe(wrap), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); + +const password = encoder.encode("correct horse battery staple"); +const pbkdf2 = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveBits", "deriveKey"]); +console.log(describe(pbkdf2)); +const hkdf = await crypto.subtle.importKey("raw-secret", bytes, { name: "hkdf" }, false, ["deriveKey"]); +console.log(describe(hkdf)); +const empty = await crypto.subtle.importKey("raw", new Uint8Array(0), "PBKDF2", false, ["deriveBits"]); +console.log(describe(empty)); +console.log(hmac instanceof CryptoKey, pbkdf2 instanceof CryptoKey); diff --git a/tests/corpus/7125-webcrypto-key-errors.mjs b/tests/corpus/7125-webcrypto-key-errors.mjs new file mode 100644 index 000000000..7f14ae0d7 --- /dev/null +++ b/tests/corpus/7125-webcrypto-key-errors.mjs @@ -0,0 +1,95 @@ +// Rejections of crypto.crypto.subtle.generateKey, exportKey and importKey for +// secret keys, in Node's order: argument conversion, algorithm +// normalization, usages, then the per-algorithm checks, and the JSON Web +// Key validation of oct keys. + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : JSON.stringify(value); + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +await attempt("generate too few", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true)); +await attempt("generate unknown", () => crypto.subtle.generateKey({ name: "FOO" }, true, ["sign"])); +await attempt("generate PBKDF2", () => crypto.subtle.generateKey({ name: "PBKDF2" }, false, ["deriveBits"])); +await attempt("generate no name", () => crypto.subtle.generateKey({ hash: "SHA-1" }, true, ["sign"])); +await attempt("generate hmac string", () => crypto.subtle.generateKey("HMAC", true, ["sign"])); +await attempt("generate hmac no hash", () => crypto.subtle.generateKey({ name: "HMAC" }, true, ["sign"])); +await attempt("generate hmac md5", () => crypto.subtle.generateKey({ name: "HMAC", hash: "MD5" }, true, ["sign"])); +await attempt("generate hmac bogus hash", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-999" }, true, ["sign"])); +await attempt("generate hmac length 0", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("generate hmac length NaN", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: NaN }, true, ["sign"])); +await attempt("generate hmac length -1", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: -1 }, true, ["sign"])); +await attempt("generate hmac encrypt", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["encrypt"])); +await attempt("generate hmac no usages", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, [])); +await attempt("generate bad usage", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["fly"])); +await attempt("generate usages string", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, "sign")); +await attempt("generate aes no length", () => crypto.subtle.generateKey({ name: "AES-GCM" }, true, ["encrypt"])); +await attempt("generate aes length 100", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 100 }, true, ["encrypt"])); +await attempt("generate aes length 70000", () => crypto.subtle.generateKey({ name: "AES-CBC", length: 70000 }, true, ["encrypt"])); +await attempt("generate aes sign", () => crypto.subtle.generateKey({ name: "AES-CTR", length: 128 }, true, ["sign"])); +await attempt("generate aes-kw encrypt", () => crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["encrypt"])); +await attempt("generate aes no usages", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, true, [])); +await attempt("generate aes string length", async () => (await crypto.subtle.generateKey({ name: "aes-gcm", length: "256" }, true, ["encrypt"])).algorithm); + +const hmac = await crypto.subtle.importKey("raw", new Uint8Array(20), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", false, ["encrypt"]); +const pbkdf2 = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]); +await attempt("export too few", () => crypto.subtle.exportKey("raw")); +await attempt("export bad format", () => crypto.subtle.exportKey("pem", hmac)); +await attempt("export not a key", () => crypto.subtle.exportKey("raw", { type: "secret" })); +await attempt("export spki", () => crypto.subtle.exportKey("spki", hmac)); +await attempt("export pkcs8", () => crypto.subtle.exportKey("pkcs8", hmac)); +await attempt("export raw-public", () => crypto.subtle.exportKey("raw-public", hmac)); +await attempt("export non-extractable", () => crypto.subtle.exportKey("raw", locked)); +await attempt("export pbkdf2", () => crypto.subtle.exportKey("raw", pbkdf2)); +await attempt("export pbkdf2 jwk", () => crypto.subtle.exportKey("jwk", pbkdf2)); + +const sixteen = new Uint8Array(16); +await attempt("import aes 15 bytes", () => crypto.subtle.importKey("raw", new Uint8Array(15), "AES-GCM", true, ["encrypt"])); +await attempt("import aes sign", () => crypto.subtle.importKey("raw", sixteen, "AES-GCM", true, ["sign"])); +await attempt("import aes-kw encrypt", () => crypto.subtle.importKey("raw", sixteen, "AES-KW", true, ["encrypt"])); +await attempt("import aes no usages", () => crypto.subtle.importKey("raw", sixteen, "AES-CBC", true, [])); +await attempt("import aes spki", () => crypto.subtle.importKey("spki", sixteen, "AES-CBC", true, ["encrypt"])); +await attempt("import aes length ignored", () => crypto.subtle.importKey("raw", sixteen, { name: "AES-CTR", length: 256 }, true, ["encrypt"])); +await attempt("import pbkdf2 extractable", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", true, ["deriveBits"])); +await attempt("import pbkdf2 sign", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, ["sign"])); +await attempt("import pbkdf2 no usages", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, [])); +await attempt("import hkdf jwk", () => crypto.subtle.importKey("jwk", { kty: "oct", k: "AAAA" }, "HKDF", false, ["deriveKey"])); +await attempt("import hkdf pkcs8", () => crypto.subtle.importKey("pkcs8", sixteen, "HKDF", false, ["deriveKey"])); +await attempt("sign with aes key", () => crypto.subtle.sign("HMAC", locked, sixteen)); + +const hs256 = { name: "HMAC", hash: "SHA-256" }; +const jwk = (data, alg = hs256, extractable = true, usages = ["sign"]) => crypto.subtle.importKey("jwk", data, alg, extractable, usages); +await attempt("jwk ok", () => jwk({ kty: "oct", k: "AAECAwQFBgc" })); +await attempt("jwk alg match", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS256" })); +await attempt("jwk alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS512" })); +await attempt("jwk no kty", () => jwk({ k: "AAECAwQFBgc" })); +await attempt("jwk kty RSA", () => jwk({ kty: "RSA", k: "AAECAwQFBgc" })); +await attempt("jwk no k", () => jwk({ kty: "oct" })); +await attempt("jwk k number", () => jwk({ kty: "oct", k: 5 })); +await attempt("jwk k empty", () => jwk({ kty: "oct", k: "" })); +await attempt("jwk k standard alphabet", () => jwk({ kty: "oct", k: "AA+/AA==" })); +await attempt("jwk k junk", () => jwk({ kty: "oct", k: "!!!!" })); +await attempt("jwk use enc", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "enc" })); +await attempt("jwk use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "sig" })); +await attempt("jwk key_ops superset", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "verify", "fly"] })); +await attempt("jwk key_ops missing", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["verify"] })); +await attempt("jwk key_ops duplicate", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "sign"] })); +await attempt("jwk key_ops string", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: "sign" })); +await attempt("jwk ext false", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false })); +await attempt("jwk ext false locked", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false }, hs256, false)); +await attempt("jwk not an object", () => jwk("key")); +await attempt("jwk null", () => jwk(null)); +await attempt("jwk kty symbol", () => jwk({ kty: Symbol("oct"), k: "AA" })); +await attempt("jwk hmac length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 60 })); +await attempt("jwk hmac length mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 128 })); +await attempt("jwk no usages", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, hs256, true, [])); +await attempt("jwk aes", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A128GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A256GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", use: "sig" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, "AES-CBC", true, ["encrypt"])); diff --git a/tests/corpus/7126-webcrypto-aes.ts b/tests/corpus/7126-webcrypto-aes.ts new file mode 100644 index 000000000..0e7be7548 --- /dev/null +++ b/tests/corpus/7126-webcrypto-aes.ts @@ -0,0 +1,77 @@ +// crypto.crypto.subtle.encrypt and decrypt with AES-GCM, AES-CBC and AES-CTR: +// fixed keys, IVs and inputs give Node's exact ciphertexts, every key +// length and GCM tag length round-trips, and CTR counters wrap within +// their low `length` bits. +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +function bytes(length: number, seed: number): Uint8Array { + const out = new Uint8Array(length); + for (let i = 0; i < length; i++) out[i] = (i * 31 + seed) & 0xff; + return out; +} + +const plaintext = encoder.encode("The quick brown fox jumps over the lazy dog"); +for (const length of [16, 24, 32]) { + const raw = bytes(length, length); + const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]); + const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", false, ["encrypt", "decrypt"]); + const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", false, ["encrypt", "decrypt"]); + const iv = bytes(12, 7); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, gcm, plaintext); + console.log("gcm", length, sealed.byteLength, hex(sealed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, gcm, sealed))); + const aad = encoder.encode("header"); + const tagged = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, plaintext); + console.log("gcm aad", length, hex(tagged)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, tagged))); + const cbcIv = bytes(16, 3); + const padded = await crypto.subtle.encrypt({ name: "AES-CBC", iv: cbcIv }, cbc, plaintext); + console.log("cbc", length, padded.byteLength, hex(padded)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CBC", iv: cbcIv }, cbc, padded))); + const counter = bytes(16, 9); + const streamed = await crypto.subtle.encrypt({ name: "AES-CTR", counter, length: 64 }, ctr, plaintext); + console.log("ctr", length, hex(streamed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CTR", counter, length: 64 }, ctr, streamed))); +} + +const key = await crypto.subtle.importKey("raw", bytes(16, 1), "AES-GCM", true, ["encrypt", "decrypt"]); +for (const tagLength of [32, 64, 96, 104, 112, 120, 128]) { + const iv = bytes(16, tagLength); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength }, key, plaintext.subarray(0, 5)); + const opened = await crypto.subtle.decrypt({ name: "AES-GCM", iv, tagLength }, key, sealed); + console.log("tag", tagLength, sealed.byteLength, hex(sealed), decoder.decode(opened)); +} +console.log("gcm empty", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(0)))); +console.log("gcm long iv", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: bytes(128, 5) }, key, plaintext.subarray(0, 3)))); + +const cbcKey = await crypto.subtle.importKey("raw", bytes(16, 2), "AES-CBC", false, ["encrypt", "decrypt"]); +for (const length of [0, 1, 15, 16, 17, 32]) { + const sealed = await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, bytes(length, 4)); + const opened = await crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, sealed); + console.log("cbc length", length, sealed.byteLength, opened.byteLength, hex(opened) === hex(bytes(length, 4).buffer as ArrayBuffer)); +} + +const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CTR", false, ["encrypt"]); +const allOnes = new Uint8Array(16).fill(0xff); +const zeros = new Uint8Array(64); +console.log("ctr wrap 4", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 4 }, ctrKey, zeros))); +console.log("ctr wrap 128", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 128 }, ctrKey, zeros))); +console.log("ctr wrap 2", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 2 }, ctrKey, zeros))); +console.log("ctr 1 bit, 2 blocks", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(16), length: 1 }, ctrKey, zeros.subarray(0, 32)))); +console.log("ctr partial", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: bytes(16, 6), length: 128 }, ctrKey, zeros.subarray(0, 21)))); + +const generated = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +const iv = crypto.getRandomValues(new Uint8Array(12)); +const message = encoder.encode("generated key round trip"); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, generated, message); +console.log(sealed.byteLength, decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, sealed))); +const exported = await crypto.subtle.exportKey("jwk", generated); +const reimported = await crypto.subtle.importKey("jwk", exported, "AES-GCM", false, ["decrypt"]); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, reimported, sealed))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, new DataView(sealed)))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.buffer as ArrayBuffer }, generated, Buffer.from(sealed)))); diff --git a/tests/corpus/7127-webcrypto-aes-errors.mjs b/tests/corpus/7127-webcrypto-aes-errors.mjs new file mode 100644 index 000000000..7ce51230a --- /dev/null +++ b/tests/corpus/7127-webcrypto-aes-errors.mjs @@ -0,0 +1,71 @@ +// Rejections of crypto.crypto.subtle.encrypt and decrypt in Node's order: +// argument conversion, the AesCbcParams, AesCtrParams and AeadParams +// dictionaries, the key checks, then the cipher's own OperationErrors +// (bad tags, bad padding, IV and counter limits). + +async function attempt(label, run) { + try { + const value = await run(); + console.log(label, "resolves", value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const raw = new Uint8Array(16).map((_, i) => i); +const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt", "decrypt"]); +const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", true, ["encrypt", "decrypt"]); +const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", true, ["encrypt", "decrypt"]); +const encryptOnly = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt"]); +const hmac = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); +const iv12 = new Uint8Array(12); +const iv16 = new Uint8Array(16); +const data = new TextEncoder().encode("hello world"); + +await attempt("too few", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm)); +await attempt("decrypt too few", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 })); +await attempt("not a key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, JSON.parse("{}"), data)); +await attempt("data string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, "hello")); +await attempt("unknown algorithm", () => crypto.subtle.encrypt({ name: "AES-XTS" }, gcm, data)); +await attempt("AES-KW", () => crypto.subtle.encrypt({ name: "AES-KW" }, gcm, data)); +await attempt("no name", () => crypto.subtle.encrypt({ iv: iv12 }, gcm, data)); +await attempt("string algorithm", () => crypto.subtle.encrypt("AES-GCM", gcm, data)); +await attempt("gcm no iv", () => crypto.subtle.encrypt({ name: "AES-GCM" }, gcm, data)); +await attempt("gcm iv string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: "abc" }, gcm, data)); +await attempt("gcm aad string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: "x" }, gcm, data)); +await attempt("gcm aad null", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: null }, gcm, data)); +await attempt("gcm tag 100", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 100 }, gcm, data)); +await attempt("gcm tag 300", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 300 }, gcm, data)); +await attempt("gcm tag NaN", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: NaN }, gcm, data)); +await attempt("gcm tag string", () => crypto.subtle.encrypt({ name: "aes-gcm", iv: iv12, tagLength: "96" }, gcm, data)); +await attempt("gcm iv 11 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(11) }, gcm, data)); +await attempt("gcm iv 129 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(129) }, gcm, data)); +await attempt("gcm iv empty", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(0) }, gcm, data)); +const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, data)); +const tampered = sealed.slice(); +tampered[0] ^= 1; +await attempt("gcm tampered", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, tampered)); +await attempt("gcm wrong aad", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, additionalData: data }, gcm, sealed)); +await attempt("gcm wrong tag length", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, tagLength: 96 }, gcm, sealed)); +await attempt("gcm shorter than tag", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(15))); +await attempt("gcm tag only", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(16))); +await attempt("cbc no iv", () => crypto.subtle.encrypt({ name: "AES-CBC" }, cbc, data)); +await attempt("cbc iv 12 bytes", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv12 }, cbc, data)); +await attempt("cbc decrypt 15 bytes", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(15))); +await attempt("cbc decrypt empty", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(0))); +const block = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, cbc, data)); +const badPad = block.slice(); +badPad[15] ^= 0x55; +await attempt("cbc bad padding", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, badPad)); +await attempt("cbc other iv", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16).fill(1) }, cbc, block)); +await attempt("ctr no counter", () => crypto.subtle.encrypt({ name: "AES-CTR", length: 64 }, ctr, data)); +await attempt("ctr no length", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16 }, ctr, data)); +await attempt("ctr length 0", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 0 }, ctr, data)); +await attempt("ctr length 129", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 129 }, ctr, data)); +await attempt("ctr length 300", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 300 }, ctr, data)); +await attempt("ctr counter 8 bytes", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(8), length: 64 }, ctr, data)); +await attempt("ctr 1 bit, 3 blocks", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(48))); +await attempt("ctr empty", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(0))); +await attempt("algorithm mismatch", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, gcm, data)); +await attempt("hmac key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, hmac, data)); +await attempt("missing usage", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, encryptOnly, sealed)); diff --git a/tests/corpus/7128-webcrypto-derive.ts b/tests/corpus/7128-webcrypto-derive.ts new file mode 100644 index 000000000..4badedb66 --- /dev/null +++ b/tests/corpus/7128-webcrypto-derive.ts @@ -0,0 +1,45 @@ +// crypto.crypto.subtle.deriveBits and deriveKey with PBKDF2 and HKDF: Node's +// exact derived bytes for every SHA digest, zero-length derivations, the +// SCRAM-style PBKDF2-then-HMAC chain, and derived AES and HMAC keys that +// encrypt and sign. +const encoder = new TextEncoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("pencil"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input keying material"), { name: "HKDF" }, false, ["deriveBits", "deriveKey"]); +const salt = encoder.encode("QSXCR+Q6sek8bf92"); +const info = encoder.encode("context"); + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + console.log("pbkdf2", hash, hex(await crypto.subtle.deriveBits({ name: "PBKDF2", hash, salt, iterations: 4096 }, password, 256))); + console.log("hkdf", hash, hex(await crypto.subtle.deriveBits({ name: "HKDF", hash: { name: hash }, salt, info }, ikm, 256))); +} +console.log("pbkdf2 0", (await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1 }, password, 0)).byteLength); +console.log("hkdf 0", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, 0)).byteLength); +console.log("hkdf empty salt and info", hex(await crypto.subtle.deriveBits({ name: "hkdf", hash: "SHA-256", salt: new Uint8Array(0), info: new Uint8Array(0) }, ikm, 128))); +console.log("hkdf max", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, 255 * 20 * 8)).byteLength); + +// SCRAM-SHA-256's client proof inputs, the way database drivers compute them. +const saltedPassword = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 4096 }, password, 256); +const salted = await crypto.subtle.importKey("raw", saltedPassword, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const clientKey = await crypto.subtle.sign("HMAC", salted, encoder.encode("Client Key")); +console.log("client key", hex(clientKey)); +console.log("stored key", hex(await crypto.subtle.digest("SHA-256", clientKey))); + +const aes = await crypto.subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000 }, password, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +console.log(JSON.stringify(aes.algorithm), aes.usages.join(","), aes.extractable); +console.log("aes key", hex(await crypto.subtle.exportKey("raw", aes))); +const iv = new Uint8Array(12); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aes, encoder.encode("derived")); +console.log(hex(sealed), new TextDecoder().decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aes, sealed))); + +const mac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, { name: "HMAC", hash: "SHA-384" }, false, ["sign", "verify"]); +console.log(JSON.stringify(mac.algorithm), mac.extractable); +console.log("mac", hex(await crypto.subtle.sign("HMAC", mac, encoder.encode("message")))); +const shortMac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, { name: "HMAC", hash: "SHA-1", length: 128 }, true, ["sign"]); +console.log(JSON.stringify(shortMac.algorithm), hex(await crypto.subtle.exportKey("raw", shortMac))); +const wrap = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-512", salt, info }, ikm, { name: "AES-KW", length: 128 }, true, ["wrapKey"]); +console.log(JSON.stringify(wrap.algorithm), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); diff --git a/tests/corpus/7129-webcrypto-derive-errors.mjs b/tests/corpus/7129-webcrypto-derive-errors.mjs new file mode 100644 index 000000000..83149b61b --- /dev/null +++ b/tests/corpus/7129-webcrypto-derive-errors.mjs @@ -0,0 +1,72 @@ +// Rejections of crypto.crypto.subtle.deriveBits and deriveKey in Node's order: +// argument conversion, the Pbkdf2Params and HkdfParams dictionaries, the +// base key's usages and algorithm, the length rules, and for deriveKey +// the derived key type's normalization and import. +const encoder = new TextEncoder(); + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : value; + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("password"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input key"), "HKDF", false, ["deriveBits", "deriveKey"]); +const bitsOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveBits"]); +const keyOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveKey"]); +const hmac = await crypto.subtle.importKey("raw", encoder.encode("x"), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); +const salt = encoder.encode("salt"); +const pbkdf2 = (extra = {}) => ({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000, ...extra }); +const hkdf = (extra = {}) => ({ name: "HKDF", hash: "SHA-256", salt, info: encoder.encode("info"), ...extra }); + +await attempt("bits too few", () => crypto.subtle.deriveBits(pbkdf2())); +await attempt("bits not a key", () => crypto.subtle.deriveBits(pbkdf2(), JSON.parse("{}"), 64)); +await attempt("bits length null", () => crypto.subtle.deriveBits(pbkdf2(), password, null)); +await attempt("bits length undefined", () => crypto.subtle.deriveBits(pbkdf2(), password, undefined)); +await attempt("bits length absent", () => crypto.subtle.deriveBits(pbkdf2(), password)); +await attempt("bits length 12", () => crypto.subtle.deriveBits(pbkdf2(), password, 12)); +await attempt("bits length -1", () => crypto.subtle.deriveBits(pbkdf2(), password, -1)); +await attempt("bits length NaN", () => crypto.subtle.deriveBits(pbkdf2(), password, NaN)); +await attempt("bits length string", () => crypto.subtle.deriveBits(pbkdf2(), password, "64")); +await attempt("bits unknown", () => crypto.subtle.deriveBits({ name: "SCRYPT" }, password, 64)); +await attempt("bits no name", () => crypto.subtle.deriveBits({ hash: "SHA-1" }, password, 64)); +await attempt("pbkdf2 iterations 0", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 0 }), password, 64)); +await attempt("pbkdf2 iterations 2^31", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 2 ** 31 }), password, 64)); +await attempt("pbkdf2 iterations -1", () => crypto.subtle.deriveBits(pbkdf2({ iterations: -1 }), password, 64)); +await attempt("pbkdf2 no iterations", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt }, password, 64)); +await attempt("pbkdf2 no salt", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", iterations: 1 }, password, 64)); +await attempt("pbkdf2 no hash", () => crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 1 }, password, 64)); +await attempt("pbkdf2 string", () => crypto.subtle.deriveBits("PBKDF2", password, 64)); +await attempt("pbkdf2 md5", () => crypto.subtle.deriveBits(pbkdf2({ hash: "MD5" }), password, 64)); +await attempt("pbkdf2 bogus sha", () => crypto.subtle.deriveBits(pbkdf2({ hash: "SHA-999" }), password, 64)); +await attempt("pbkdf2 salt string", () => crypto.subtle.deriveBits(pbkdf2({ salt: "salt" }), password, 64)); +await attempt("pbkdf2 hkdf key", () => crypto.subtle.deriveBits(pbkdf2(), ikm, 64)); +await attempt("pbkdf2 hmac key", () => crypto.subtle.deriveBits(pbkdf2(), hmac, 64)); +await attempt("pbkdf2 no deriveBits usage", () => crypto.subtle.deriveBits(pbkdf2(), keyOnly, 64)); +await attempt("hkdf no info", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt }, ikm, 64)); +await attempt("hkdf no salt", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", info: salt }, ikm, 64)); +await attempt("hkdf info 1025 bytes", () => crypto.subtle.deriveBits(hkdf({ info: new Uint8Array(1025) }), ikm, 64)); +await attempt("hkdf over maximum", () => crypto.subtle.deriveBits(hkdf({ hash: "SHA-1" }), ikm, 255 * 20 * 8 + 8)); +await attempt("hkdf length 4", () => crypto.subtle.deriveBits(hkdf(), ikm, 4)); +await attempt("hkdf null", () => crypto.subtle.deriveBits(hkdf(), ikm, null)); + +const aes = { name: "AES-GCM", length: 128 }; +await attempt("key too few", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true)); +await attempt("key not a key", () => crypto.subtle.deriveKey(pbkdf2(), "key", aes, true, ["encrypt"])); +await attempt("key no deriveKey usage", () => crypto.subtle.deriveKey(pbkdf2(), bitsOnly, aes, true, ["encrypt"])); +await attempt("key mismatch", () => crypto.subtle.deriveKey(hkdf(), password, aes, true, ["encrypt"])); +await attempt("key aes no length", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-GCM" }, true, ["encrypt"])); +await attempt("key aes length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-CBC", length: 100 }, true, ["encrypt"])); +await attempt("key aes sign usage", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, ["sign"])); +await attempt("key aes no usages", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, [])); +await attempt("key hmac no hash", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC" }, true, ["sign"])); +await attempt("key hmac length 0", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("key hmac length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 100 }, true, ["sign"])); +await attempt("key hkdf type", () => crypto.subtle.deriveKey(pbkdf2(), password, "HKDF", false, ["deriveBits"])); +await attempt("key unknown type", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "TWOFISH" }, true, ["encrypt"])); +await attempt("key pbkdf2 type", () => crypto.subtle.deriveKey(hkdf(), ikm, { name: "PBKDF2" }, false, ["deriveKey"])); +await attempt("key ok", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, false, ["encrypt"])); diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts index d162872fb..309e70c4e 100644 --- a/tests/diagnostics/webcrypto-refusals.ts +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -1,17 +1,8 @@ -// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, -// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' -// key format and crypto.subtle or its methods taken as values are refused -// by name; the bundled declarations omit the other SubtleCrypto methods -// (node-types/fenced.ts pins their fence under @types/node). -const fromJwk = await crypto.subtle.importKey( - "jwk", - { kty: "oct", k: "c2VjcmV0" }, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], -); -console.log(fromJwk.type); - +// The native WebCrypto surface is crypto.subtle's secret-key slice, called +// directly on crypto.subtle. crypto.subtle or its methods taken as values +// are refused by name; the bundled declarations omit the other +// SubtleCrypto methods (node-types/fenced.ts pins their fence under +// @types/node). const subtle = crypto.subtle; console.log(typeof subtle); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 16a74081f..dfd07d446 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -103,13 +103,13 @@ cipherStream.on("data", () => {}); import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* WebCrypto beyond the HMAC slice: @types/node declares the whole - * SubtleCrypto surface, and the methods outside importKey, sign, verify - * and digest fence by name. */ -async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +/* WebCrypto beyond the secret-key slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside the slice, such as + * wrapKey, fence by name. */ +async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); } -void encryptWithWebCrypto; +void wrapWithWebCrypto; /* Node's navigator global: typeof navigator and navigator.userAgent * compile; the other members fence by name. */ const cores = navigator.hardwareConcurrency; diff --git a/tests/fixtures/node-types/webcrypto-keys.ts b/tests/fixtures/node-types/webcrypto-keys.ts new file mode 100644 index 000000000..be16271d8 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-keys.ts @@ -0,0 +1,32 @@ +/* WebCrypto secret keys under @types/node: generateKey, exportKey and + * importKey for HMAC and AES keys, and PBKDF2 key imports, lower to the + * same native values as under the bundled declarations. @types/node + * names the dictionary, key and JSON Web Key types in node:crypto's + * webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const hmacParams: webcrypto.HmacKeyGenParams = { name: "HMAC", hash: "SHA-256", length: 256 }; + const hmac: webcrypto.CryptoKey = await crypto.subtle.generateKey(hmacParams, true, ["sign", "verify"]); + const hmacAlgorithm = hmac.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(hmac.type, hmacAlgorithm.name, hmacAlgorithm.length, hmacAlgorithm.hash.name, hmac.usages.join(",")); + const raw: ArrayBuffer = await crypto.subtle.exportKey("raw", hmac); + console.log(raw.byteLength); + + const aesParams: webcrypto.AesKeyGenParams = { name: "AES-GCM", length: 192 }; + const aes = await crypto.subtle.generateKey(aesParams, true, ["encrypt", "decrypt"]); + const aesAlgorithm = aes.algorithm as webcrypto.AesKeyAlgorithm; + console.log(aesAlgorithm.name, aesAlgorithm.length, aes.extractable); + const jwk: webcrypto.JsonWebKey = await crypto.subtle.exportKey("jwk", aes); + console.log(jwk.kty, jwk.alg, jwk.ext, jwk.key_ops?.join(","), jwk.k?.length); + const back = await crypto.subtle.importKey("jwk", jwk, "AES-GCM", false, ["decrypt"]); + console.log(back.algorithm.name, back.extractable, back.usages.join(",")); + + const fixed = new Uint8Array(16).fill(9); + const imported = await crypto.subtle.importKey("raw", fixed, { name: "AES-CBC" }, true, ["encrypt"]); + console.log(Buffer.from(await crypto.subtle.exportKey("raw", imported)).toString("hex")); + const password = await crypto.subtle.importKey("raw", new TextEncoder().encode("pw"), "PBKDF2", false, ["deriveBits"]); + console.log(password.algorithm.name, password.extractable, password.usages.join(",")); +} + +void main(); diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 0bd13963d..2f1f8cb2e 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -228,18 +228,18 @@ fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 104 | createECDH("sect163k1"); 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + 106 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet +fenced.ts:110:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet - 109 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + 109 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 110 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); | ^~~~~~~~~~~~~~~~~~~~~ 111 | } - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index 521edff3c..e46099f69 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -1,26 +1,17 @@ -webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet - 6 | const fromJwk = await crypto.subtle.importKey( - 7 | "jwk", - | ^~~~~ - 8 | { kty: "oct", k: "c2VjcmV0" }, + 5 | // @types/node). + 6 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 7 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - 14 | - 15 | const subtle = crypto.subtle; - | ^~~~~~~~~~~~~ - 16 | console.log(typeof subtle); - - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle - -webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - - 17 | - 18 | console.log(typeof crypto.subtle.sign); + 8 | + 9 | console.log(typeof crypto.subtle.sign); | ^~~~~~~~~~~~~~~~~~ - 19 | + 10 | - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 000000000..ce52548ca --- /dev/null +++ b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1 @@ +{ "plain": 4431, "san": 4365 } diff --git a/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 000000000..db431c2ca --- /dev/null +++ b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1 @@ +{ "plain": 5043, "san": 4705 } diff --git a/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 000000000..46469f2a9 --- /dev/null +++ b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1 @@ +{ "plain": 4805, "san": 6923 } diff --git a/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 000000000..682ba704a --- /dev/null +++ b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1 @@ +{ "plain": 4728, "san": 6725 } diff --git a/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 000000000..73f1ce847 --- /dev/null +++ b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 5860, "san": 9241 } diff --git a/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 000000000..ceff344eb --- /dev/null +++ b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 6582, "san": 9054 } diff --git a/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json new file mode 100644 index 000000000..b1680985c --- /dev/null +++ b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json @@ -0,0 +1 @@ +{ "plain": 4563, "san": 6500 } diff --git a/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 000000000..1ebb743b0 --- /dev/null +++ b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 4385, "san": 8939 } diff --git a/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json new file mode 100644 index 000000000..2888d22eb --- /dev/null +++ b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json @@ -0,0 +1 @@ +{ "plain": 5042, "san": 9143 } diff --git a/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 000000000..b2f70bf57 --- /dev/null +++ b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 5549, "san": 7441 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca..00ee40043 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1634,14 +1634,14 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot }); }); -// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and -// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice -// reject with a coded SC2020 Error instead of a Node-shaped success, and a -// JavaScript call with the 'jwk' format meets its named fence when it runs. +// WebCrypto's native slice is crypto.subtle's secret-key methods. +// Algorithms Node recognizes outside that slice reject with a coded SC2020 +// Error instead of a Node-shaped success, and a JavaScript call of a +// method outside it meets its named fence when it runs. // Node's own rejections (unknown names, bad usages) are corpus-pinned // (6292, 6294); these refusals have no Node twin, so they live here. describe("WebCrypto refusals (JS lane)", () => { - test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + test("recognized algorithms outside the secret-key slice reject with SC2020", async () => { const r = await compileAndRun( "webcrypto-unsupported-algorithms", `const bytes = new TextEncoder().encode("secret"); @@ -1662,10 +1662,10 @@ await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); await attempt("sha3 hmac", () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), ); -const format = JSON.parse('"jwk"'); -await attempt("computed jwk", () => - crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), -); +await attempt("ed25519 key", () => crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign"])); +const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]); +await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes)); +await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256)); `, "mjs", ); @@ -1676,22 +1676,25 @@ await attempt("computed jwk", () => "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + - "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + "ed25519 key Error SC2020 crypto.subtle.generateKey with the Ed25519 algorithm is not supported yet\n" + + "aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" + + "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n", ); }); - test("a literal 'jwk' import is a named fence where it runs", async () => { + test("a method outside the slice is a named fence where it runs", async () => { const r = await compileAndRun( - "webcrypto-jwk-literal", + "webcrypto-wrap-key", `console.log("before"); -await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]); +await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); `, "mjs", ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + /^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, ); }); }); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index dabb9b0bf..8532ad6e9 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1132,6 +1132,24 @@ test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto secret keys match Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-keys"); + const entry = join(nodeTypesDir, "webcrypto-keys.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-keys"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { const outDir = outDirFor("node-navigator"); const entry = join(nodeTypesDir, "navigator.ts"); From b2b216c3df6184fd964ad3aee3ee69c07e7ecd07 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:10:12 -0500 Subject: [PATCH 10/12] Extend a generic factory's anonymous local class returned through a constructor type A generic factory declared to return a constructor type now also extends when it returns an anonymous class expression (`return class { ... } as Struct`) or a `const` bound to one (`const Hold = class { ... }; return Hold`). The result's class value is the class expression's own constructor type; before, it was read through the class's name, so an anonymous class refused with SC1090 and SC2008. --- .../frontend/lowering/generic-functions.ts | 34 ++++++++++++++----- .../corpus/6912-extend-factory-local-class.ts | 32 +++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 0f549d889..e843b6909 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -496,10 +496,18 @@ function localClassResultInstance( const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass; const returnType = withInstanceScope(lowerer, info, inst, () => { if (!viaConstructorType) return lowerer.mapTypeOf(retTs); - const classSymbol = resultClass.name - ? lowerer.checker.getSymbolAtLocation(resultClass.name) - : undefined; - return classSymbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(classSymbol)) : null; + // The local class's own constructor type: a class expression's type is + // its constructor type, named or not (`return class { ... } as Struct`). + const classSymbol = + !ts.isClassExpression(resultClass) && resultClass.name + ? lowerer.checker.getSymbolAtLocation(resultClass.name) + : undefined; + const classType = ts.isClassExpression(resultClass) + ? lowerer.checker.getTypeAtLocation(resultClass) + : classSymbol + ? lowerer.checker.getTypeOfSymbol(classSymbol) + : undefined; + return classType ? lowerer.mapTypeOf(classType) : null; }); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); @@ -646,11 +654,19 @@ function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLike if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null; if (!ts.isIdentifier(expr)) return null; const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration; - return declaration && - ts.isClassDeclaration(declaration) && - declaredInside(declaration, info.decl) - ? declaration - : null; + if (!declaration || !declaredInside(declaration, info.decl)) return null; + if (ts.isClassDeclaration(declaration)) return declaration; + // A `const` bound to a class expression (`const C = class { ... }; return C`). + if ( + ts.isVariableDeclaration(declaration) && + ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const && + declaration.initializer + ) { + let init: ts.Expression = declaration.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isClassExpression(init)) return init; + } + return null; }; if (!ts.isBlock(body)) return classOf(body as ts.Expression); let found: ts.ClassLikeDeclaration | null = null; diff --git a/tests/corpus/6912-extend-factory-local-class.ts b/tests/corpus/6912-extend-factory-local-class.ts index 8848280e2..e33a59c77 100644 --- a/tests/corpus/6912-extend-factory-local-class.ts +++ b/tests/corpus/6912-extend-factory-local-class.ts @@ -65,3 +65,35 @@ function twice(n: number): number { return K.double() + K.quad(); } console.log(twice(3), twice(5)); + +// The factory returns an anonymous class expression, directly or through a +// `const` binding. +function anonymous(value: S): Struct { + return class { + static decode(_input: unknown): S { + return value; + } + } as unknown as Struct; +} +class Anonymous extends anonymous({ tag: "anon" }) {} +console.log(Anonymous.decode(null).tag); + +type Holder = (new (held: T) => { held: T }) & { readonly seed: () => T }; +function holder(initial: T): Holder { + const Hold = class { + held: T; + constructor(held: T) { + this.held = held; + } + static seed(): T { + return initial; + } + }; + return Hold; +} +class Boxed extends holder(7) { + twice(): number { + return this.held * 2; + } +} +console.log(new Boxed(4).twice(), Boxed.seed()); From 42dd9390c13551dd40383e3a1decbeeaa5de33af Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:10:30 -0500 Subject: [PATCH 11/12] Compute index temps of a typed-array loop counter as integers In `for (let i = 0; i < out.length; i++) { const j = i * 4; ... px[j + 1] }` the fast copy of the typed-array loop version computed `j` as a double and converted every index back. The version gives each affine index operand its range under the guard's bound on the counter, but kept the range an operand already had, and the counter of a loop to a length is only known to be below 2^53, so `i * 4` stayed a double multiply. The version now narrows the existing range to the guard's. A constant number local whose initializer has an exact integer form now keeps that integer in an i64 slot beside the double, and its reads carry both, so `px[j + 1]` indexes with integer arithmetic. Only optimized synchronous functions keep these integers, and every other declaration of the local drops its integer, since one declaration is emitted once per loop copy. --- .../backend/llvm/bytes-loop-versions.test.ts | 44 ++++++ .../src/backend/llvm/bytes-loop-versions.ts | 10 +- .../src/backend/llvm/emit-function.ts | 2 + .../src/backend/llvm/emit-statements.ts | 44 ++++++ packages/compiler/src/backend/llvm/emitter.ts | 11 ++ .../src/backend/llvm/expr-primitives.ts | 3 +- .../6996-typed-array-index-temps.ts.json | 6 + tests/corpus/6996-typed-array-index-temps.ts | 134 ++++++++++++++++++ .../6996-typed-array-index-temps.ts.json | 1 + 9 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json create mode 100644 tests/corpus/6996-typed-array-index-temps.ts create mode 100644 tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index 3fbc01cc5..28194c7b3 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -281,6 +281,50 @@ test("worker programs test each receiver's shared storage once in the guard", () expect(slow).toContain("store atomic double"); }); +test("index temps of a version's counter are integers in the fast copy", () => { + // for (let i = 0; i < out.length; i++) { const j = i * 4; out[i] = src[j] + src[j + 1] } + const length: IrExpr = { + kind: "bytesIntrinsic", + method: "length", + receiver: ref("out", f64s), + args: [], + type: F64, + loc, + }; + const loop = counted( + [ + { kind: "varDecl", localId: "at", init: bin("*", ref("i"), num(4)), loc }, + set( + ref("out", f64s), + ref("i"), + bin( + "+", + get(ref("src", f64s), ref("at")), + get(ref("src", f64s), bin("+", ref("at"), num(1))), + ), + ), + ], + length, + ); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn([loop], [local("at", F64)]), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(body).toContain("alloca i64 ; integer at"); + expect(fast).toMatch(/mul nsw i64 %\S+, 4/); + expect(fast).toMatch(/add nsw i64 %\S+, 1/); + expect(fast).not.toContain("fmul"); + expect(fast).not.toContain("fptosi"); + expect(fast).not.toContain("bytes.index"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index dcb735e61..8c7f668a0 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -427,7 +427,6 @@ export function matchBytesLoopVersion( used.add(group.check.receiver); for (const node of group.nodes) bounds.add(node); for (const operand of group.operands) { - if (facts.ranges.get(operand)) continue; const shape = shapes.get(operand)!; const a = shape.counter * counterLow, b = shape.counter * counterHigh; @@ -438,7 +437,14 @@ export function matchBytesLoopVersion( min += term.sign > 0 ? r.min : -r.max; max += term.sign > 0 ? r.max : -r.min; } - if (counterLow <= counterHigh) ranges.set(operand, { min, max }); + if (counterLow > counterHigh) continue; + // The guard's bound on the counter can narrow a range the operand + // already has (a loop to a length proves only `i < 2^53`), so that + // `i * 4` in the fast copy is exact integer arithmetic. + const known = facts.ranges.get(operand); + if (!known) ranges.set(operand, { min, max }); + else if (min > known.min || max < known.max) + ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) }); } } // Renumber the receivers the kept checks use. diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 5053c1b54..09e6551c3 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -128,6 +128,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.stableCaptureIds = fn.captures !== undefined ? emitter.lazyCaptures.stableCaptures(fn.name) : new Set(); emitter.integerLoopBindings.clear(); + emitter.integerTemps.clear(); + emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator; emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled; emitter.speculativeFunction = fn.name; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index d2e96994b..7f189c066 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -50,6 +50,7 @@ import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; +import { widenInteger } from "./integer-values.js"; import { SCALAR_UNION_RESULT } from "./int32-abi.js"; import { emitLiteralSwitch } from "./switch-dispatch.js"; import { @@ -267,6 +268,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { else emitter.toFrame(v); B.line(`store ${emitter.llType(b.type)} ${v.name}, ptr ${b.slot}`); emitter.storeIntegerView(s.localId, v, s.init); + storeIntegerTemp(emitter, s.localId, b, v); if (isRefCounted(b.type)) { emitter.scopes[emitter.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); emitter.noteLastUseMove(s, b); @@ -1544,6 +1546,48 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { * Abrupt completions inside tryBody/catchBody ride the finallyStack: * returns inline copies at the return site, while break/continue use the * shared region walk before branching to their resolved target. */ +/** A constant number local whose initializer has an exact integer form + * (such as `const j = i * 4` in a typed-array loop version) keeps that + * integer in an i64 slot beside the double, so reads such as `px[j + 1]` + * index with integer arithmetic instead of converting the double back. + * Every other declaration of the local drops the integer: the same + * declaration is emitted once per loop copy, and a copy must not read the + * integer another copy stored. */ +function storeIntegerTemp( + emitter: LlEmitter, + localId: string, + b: ReturnType, + v: LlValue, +): void { + const local = b.local; + if ( + !emitter.integerTempsEnabled || + b.kind !== "local" || + !local || + local.type.kind !== "f64" || + local.mutable || + local.tdz === true || + emitter.captureIds.has(localId) || + !v.integer + ) { + emitter.integerTemps.delete(localId); + return; + } + const B = emitter.B; + let slot = emitter.integerTemps.get(localId)?.slot; + if (slot === undefined) { + slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca i64 ; integer ${local.name}`); + } + B.line(`store i64 ${widenInteger(emitter, v.integer)}, ptr ${slot}`); + emitter.integerTemps.set(localId, { + slot, + type: "i64", + signed: true, + range: v.integer.range, + }); +} + export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" }): void { const B = emitter.B; const hasCatch = s.catchBody !== null; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c254d0907..e11622b9b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1081,6 +1081,17 @@ export class LlEmitter { string, { slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } } >(); + /** Constant number locals whose latest emitted declaration stored an + * exact integer beside the double, such as `const j = i * 4` in a + * typed-array loop version: an i64 slot and the integer's range. */ + integerTemps = new Map< + string, + { slot: string; type: "i64"; signed: true; range: IntegerRange } + >(); + /** Integer temps are kept only in optimized synchronous functions: an + * async or generator body resumes with its locals in its frame, which + * an entry alloca is not part of. */ + integerTempsEnabled = false; countedLoopsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 12e488b6d..354004e9e 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -59,7 +59,8 @@ export function emitLiteralExpr( throw new InternalCompilerError(`llvm emitter bug: bare unitLit '${e.unit}'`); case "varRef": { host.materializeSplitLocal(e.localId); - const integerSlot = host.integerLoopBindings.get(e.localId); + const integerSlot = + host.integerLoopBindings.get(e.localId) ?? host.integerTemps.get(e.localId); if (integerSlot !== undefined) { const integer = B.tmp(); const number = B.tmp(); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json new file mode 100644 index 000000000..b5f88af99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6996-typed-array-index-temps.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6996-typed-array-index-temps.ts b/tests/corpus/6996-typed-array-index-temps.ts new file mode 100644 index 000000000..a19561ef0 --- /dev/null +++ b/tests/corpus/6996-typed-array-index-temps.ts @@ -0,0 +1,134 @@ +// Constant index temps derived from a loop counter (`const j = i * 4`) in +// typed-array loops: as indices and as numbers, in loops whose bounds guard +// passes or fails, nested, captured by closures, read after an await or a +// yield, and in loops whose counter reaches large values. + +function gray(px: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) { + const j = i * 4; + out[i] = (px[j]! * 77 + px[j + 1]! * 150 + px[j + 2]! * 29) >> 8; + } +} + +function interleave(a: Float32Array, b: Float32Array, out: Float32Array): void { + for (let i = 0; i < a.length; i++) { + const at = 2 * i; + out[at] = a[i]!; + out[at + 1] = b[i]!; + } +} + +function strided(src: Int16Array, out: Int32Array): string { + const seen: number[] = []; + for (let i = 0; i < out.length; i++) { + const at = i * 3 + 2; + out[i] = src[at]! - src[at - 2]!; + if (i % 5 === 0) seen.push(at, at / 2, -at, at % 4); + } + return seen.join(","); +} + +function previous(src: Float64Array, out: Float64Array): string { + const edges: string[] = []; + for (let i = 0; i < out.length; i++) { + const before = i - 1; + out[i] = src[before]! + 1; + if (before < 1) edges.push(`${before}:${Object.is(before * 0, -0)}:${1 / (before * 0)}`); + } + return edges.join(" "); +} + +function rows(cells: Uint32Array, width: number, sums: Float64Array): void { + for (let r = 0; r < sums.length; r++) { + const base = r * width; + let total = 0; + for (let c = 0; c < width; c++) { + const at = base + c; + total += cells[at]!; + } + sums[r] = total; + } +} + +function captured(values: Uint8Array): number { + const readers: (() => number)[] = []; + for (let i = 0; i < values.length; i++) { + const at = i * 2; + readers.push(() => at + values[i]!); + } + let sum = 0; + for (const read of readers) sum += read(); + return sum; +} + +function wide(start: number, count: number): string { + const out: number[] = []; + for (let i = start; i < start + count; i++) { + const next = i + 1; + const twice = i * 2; + out.push(next, twice, next - i); + } + return out.join(","); +} + +async function afterAwait(values: Float64Array): Promise { + const out: number[] = []; + for (let i = 0; i < values.length; i++) { + const at = i * 2; + await null; + out.push(at, values[at] ?? -1); + } + return out.join(","); +} + +function* acrossYield(values: Int8Array): Generator { + for (let i = 0; i < values.length; i++) { + const at = i + 1; + yield values[i]!; + yield at * 10 + (values[at] ?? 0); + } +} + +const px = new Uint8Array(64); +for (let i = 0; i < px.length; i++) px[i] = (i * 41 + 7) & 255; +const g = new Uint8Array(16); +gray(px, g); +console.log("gray", g.join(",")); +const tall = new Uint8Array(20); +gray(px, tall); +console.log("gray past input", tall.join(",")); + +const fa = new Float32Array([1.5, -2.25, 3.125, 0.1]); +const fb = new Float32Array([10, 20, 30, 40]); +const pairs = new Float32Array(8); +interleave(fa, fb, pairs); +console.log("interleave", pairs.join(",")); +const short = new Float32Array(5); +interleave(fa, fb, short); +console.log("interleave short", short.join(",")); + +const src16 = new Int16Array(40); +for (let i = 0; i < src16.length; i++) src16[i] = (i * 9973) % 32768 - 16384; +const diff = new Int32Array(12); +console.log("strided", strided(src16, diff), diff.join(",")); +const far = new Int32Array(20); +console.log("strided past input", strided(src16, far), far.join(",")); + +const src64 = new Float64Array([0.5, 1.5, 2.5]); +const shifted = new Float64Array(4); +console.log("previous", previous(src64, shifted), shifted.join(",")); + +const cells = new Uint32Array(30); +for (let i = 0; i < cells.length; i++) cells[i] = (i * 2654435761) >>> 0; +const sums = new Float64Array(5); +rows(cells, 6, sums); +console.log("rows", sums.join(",")); +const partial = new Float64Array(6); +rows(cells, 6, partial); +console.log("rows past input", partial.join(",")); + +console.log("captured", captured(new Uint8Array([5, 6, 7]))); +console.log("wide", wide(2 ** 53 - 4, 3)); +console.log("wide negative", wide(-3, 4)); +console.log("across yield", [...acrossYield(new Int8Array([3, -4, 5]))].join(",")); +afterAwait(new Float64Array([0.25, 1.25, 2.25, 3.25, 4.25])).then((line) => console.log("after await", line)); diff --git a/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json b/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json new file mode 100644 index 000000000..4dc6b88cb --- /dev/null +++ b/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 4600 } From ec0574f44b2feb927745961916974b647121297e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:13:24 -0500 Subject: [PATCH 12/12] Read the byte offset of every untyped ArrayBuffer Buffer.from in one place All Buffer.from paths over an untyped ArrayBuffer or SharedArrayBuffer now go through one function that takes the byte offset explicitly, as Node's fromArrayBuffer does. The start-offset RangeError prints the offset as JavaScript does (-1.5, -1e+21, -Infinity) and builds its message without a fixed-size buffer, which a large negative offset overran. --- packages/runtime/src/scr_bytes_io.c | 77 ++++++++++--------- .../6868-buffer-from-untyped-array-buffer.cjs | 2 +- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index bd79d9089..137208938 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,20 +1299,48 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } -/* Buffer.from(arrayBuffer) over a checked value: a Buffer over all of the - * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. The - * calls that reach it have no second argument or a literal encoding name, - * which as a byte offset converts to NaN, offset 0; other second arguments - * take scr_buffer_from_dyn_offset_pooled, which applies the offset. */ -static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage) { - ScrBytes *out = scr_bytes_buffer_view(storage->v.handle.ptr, SCR_BYTES_U8, 0, false, 0); +/* Buffer.from(arrayBuffer, byteOffset) over a checked ArrayBuffer or + * SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer from the offset to + * the end of the buffer that shares its memory. The offset converts with + * ToNumber, where a missing offset (NULL) or undefined and NaN are 0; an + * offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS, and one at or below + * -1 V8's start-offset RangeError. NULL with the exception pending. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value) { + double offset = 0; + if (offset_value && offset_value->kind != SCR_DYN_UNDEF) { + if (!scr_dyn_number_coerce_js(offset_value, &offset)) return NULL; + if (isnan(offset)) offset = 0; + } + ScrBytes *bytes = storage->v.handle.ptr; + ScrBytes *owner = bytes->backing ? bytes->backing : bytes; + if ((double)scr_bytes_byte_len(owner) - offset < 0) { + static const char msg[] = "\"offset\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + double start = trunc(offset); + if (start < 0) { + /* The offset as JavaScript prints it (-1.5, -1e+21, -Infinity). */ + ScrStr *parts[3] = { + scr_str_new("Start offset ", 13), + scr_f64_to_scrstr(offset), + scr_str_new(" is outside the bounds of the buffer", 36), + }; + ScrStr *message = scr_str_concat_parts(parts, 3); + for (size_t i = 0; i < 3; i++) scr_str_release(parts[i]); + scr_throw_error(SCR_ERR_RANGE, message); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, false, 0); if (out) out->is_buffer = true; return out; } ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); + /* The second argument here is an encoding name, which as a byte offset + * converts to NaN: offset 0. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1356,8 +1384,9 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding if (value->kind == SCR_DYN_STR) return scr_buffer_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) return scr_buffer_from_bytes(value->v.bytes); - /* A view of an ArrayBuffer shares its memory, so it never comes from the pool. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); + /* A view of an ArrayBuffer shares its memory, so it never comes from the + * pool. The second argument here is an encoding name: offset 0. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; @@ -1384,9 +1413,8 @@ ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *enco /* Buffer.from(value, second) over checked values whose second argument is * not a literal encoding name: an ArrayBuffer or SharedArrayBuffer takes it - * as the byte offset, as Node's fromArrayBuffer does (undefined and NaN are - * 0, the rest of the buffer is the length, ERR_BUFFER_OUT_OF_BOUNDS past the - * end); other values convert as scr_buffer_from_dyn_ref_pooled with utf8. */ + * as the byte offset (scr_buffer_from_storage); other values convert as + * scr_buffer_from_dyn_ref_pooled with utf8. */ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second) { if (!scr_buffer_storage_is(value)) { ScrStr *utf8 = scr_str_new("utf8", 4); @@ -1394,28 +1422,7 @@ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *s scr_str_release(utf8); return out; } - double offset = 0; - if (second->kind != SCR_DYN_UNDEF) { - if (!scr_dyn_number_coerce_js(second, &offset)) return NULL; - if (isnan(offset)) offset = 0; - } - ScrBytes *storage = value->v.handle.ptr; - ScrBytes *owner = storage->backing ? storage->backing : storage; - if ((double)scr_bytes_byte_len(owner) - offset < 0) { - static const char msg[] = "\"offset\" is outside of buffer bounds"; - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); - return NULL; - } - double start = trunc(offset); - if (start < 0) { - char msg[96]; - int n = snprintf(msg, sizeof msg, "Start offset %.0f is outside the bounds of the buffer", start); - scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)n); - return NULL; - } - ScrBytes *out = scr_bytes_buffer_view(storage, SCR_BYTES_U8, start, false, 0); - if (out) out->is_buffer = true; - return out; + return scr_buffer_from_storage(value, second); } /* new Buffer(number, encoding) — the deprecated ctor's string arm with a diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs index e30b5873e..ff2c59a46 100644 --- a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -23,7 +23,7 @@ if (isMainThread) { const offsets = new Map(); offsets.set('storage', new ArrayBuffer(8)); const storage = offsets.get('storage'); - for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5]) { + for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5, -1.5, -1e21, -Number.MAX_VALUE, -Infinity]) { try { const view = Buffer.from(storage, offset); view.fill(offset === 2 ? 0x11 : 0);