diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f2a6d51d..8a726aee8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,7 +172,9 @@ jobs: SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }} # Match macOS, where Apple ASan has no LeakSanitizer: the RC audit owns # leak detection, including intentional abandonment. Plain shards also - # build some sanitized probes, so this applies to both flavors. + # build some sanitized probes, so this applies to both flavors. The + # san shards' case-sharded corpus turns LeakSanitizer on + # (scripts/ci-test-plan.mjs, tests/harness/lsan.supp). ASAN_OPTIONS: detect_leaks=0 # Within-file partition for the large case-driven harnesses # (differential, llvm-differential, npm, server, Test262, coverage, and diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 6fe670e1e..f8552b003 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -18,6 +18,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `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 | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `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 | +| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index 76d27fbc2..9a3f1bc2f 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -147,6 +147,13 @@ "20" ] }, + { + "name": "channel-messages", + "entry": "workloads/channel-messages.ts", + "args": [ + "200000" + ] + }, { "name": "atomics-contended", "entry": "workloads/atomics-contended.ts", diff --git a/benchmarks/runtime/workloads/channel-messages.ts b/benchmarks/runtime/workloads/channel-messages.ts new file mode 100644 index 000000000..5d11adee4 --- /dev/null +++ b/benchmarks/runtime/workloads/channel-messages.ts @@ -0,0 +1,21 @@ +// MessageChannel messages within one thread: the program posts the given +// number of messages, half of them numbers and half small objects, before +// the port delivers any, so all of them are queued at once. Argument: the +// number of messages. +import { MessageChannel } from "node:worker_threads"; + +const count = Number(process.argv[2] ?? "200000"); +const { port1, port2 } = new MessageChannel(); +let received = 0; +let sum = 0; +port1.on("message", (value: number | { id: number; name: string }) => { + sum += typeof value === "number" ? value : value.id + value.name.length; + if (++received === count) { + console.log(`${count} messages, sum ${sum}`); + port1.close(); + } +}); +for (let i = 0; i < count; i++) { + if (i % 2 === 0) port2.postMessage(i); + else port2.postMessage({ id: i, name: "item" }); +} diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index d261b6385..c64c5cebd 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -228,6 +228,10 @@ title: "Limitations" ../limitations/process-arguments-and-errors.mdx +### Module path globals + +../limitations/module-path-globals.mdx + ## Runtime code evaluation diff --git a/docs/content/limitations/builtin-apis/exit-status.mdx b/docs/content/limitations/builtin-apis/exit-status.mdx index d9bb779db..3cdf10496 100644 --- a/docs/content/limitations/builtin-apis/exit-status.mdx +++ b/docs/content/limitations/builtin-apis/exit-status.mdx @@ -1,3 +1,4 @@ - **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. `process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does. - Reads, resets, and assignments used as values are unsupported. + Reading `process.exitCode` answers the assigned code, or `undefined` before any assignment; `process.exit(code)` assigns it before the `'exit'` listeners run. + Resets (`process.exitCode = undefined`) and assignments used as values are unsupported. diff --git a/docs/content/limitations/builtin-apis/object-freezing.mdx b/docs/content/limitations/builtin-apis/object-freezing.mdx index 9784cd715..38f0155c0 100644 --- a/docs/content/limitations/builtin-apis/object-freezing.mdx +++ b/docs/content/limitations/builtin-apis/object-freezing.mdx @@ -1,3 +1,8 @@ - **Object freezing:** `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives. Freezing is shallow and preserves accessors. Aliased typed records, arrays, proxies, and other native objects cannot be frozen. + Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class. + A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code. + `Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs. + Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`. + A JavaScript class field that is first created after its instance became non-extensible is added instead of throwing. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index cdd5df47e..0ac0ca3a6 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -18,6 +18,10 @@ The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end. In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself. `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`. + `rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node. + In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported. + In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`. + A terminal interface's prompt that is an object or a symbol, `getPrompt()` while the prompt is not a string, and `getCursorPos()` with an object or symbol prompt are unsupported. `readline.cursorTo`, `moveCursor`, `clearLine`, and `clearScreenDown` write to `process.stdout`, `process.stderr`, a writable stream, or nothing for `null` and `undefined`; their callbacks take no parameters. The `readline/promises` `Readline` class writes to the same outputs. `readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences. diff --git a/docs/content/limitations/builtin-apis/test-mocks.mdx b/docs/content/limitations/builtin-apis/test-mocks.mdx index abfdb3657..903a873b9 100644 --- a/docs/content/limitations/builtin-apis/test-mocks.mdx +++ b/docs/content/limitations/builtin-apis/test-mocks.mdx @@ -3,7 +3,8 @@ Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. - `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`. + `mock.fn()` without an original returns `undefined` and records the arguments of each call, as in Node; with a function type whose return type does not include `undefined`, such as `mock.fn<() => number>()`, it throws `SC2020`. + `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`. - **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. diff --git a/docs/content/limitations/language-features/function-values.mdx b/docs/content/limitations/language-features/function-values.mdx index 88422c406..75229ff95 100644 --- a/docs/content/limitations/language-features/function-values.mdx +++ b/docs/content/limitations/language-features/function-values.mdx @@ -1,4 +1,4 @@ - **Function values:** Immutable aliases of overloaded program functions retain per-call overload resolution and identity. A function stored in a slot with a different signature remains the same function for equality, array searches, `length`, and own properties. - Typed rest-parameter functions can be stored and called indirectly. + Typed rest-parameter functions can be stored and called indirectly, including through a slot with fixed parameters, where the slot's arguments arrive in the rest array. Builtin function values have API-specific restrictions; see [Builtin function values](#builtin-function-values). diff --git a/docs/content/limitations/module-path-globals.mdx b/docs/content/limitations/module-path-globals.mdx new file mode 100644 index 000000000..358d96f95 --- /dev/null +++ b/docs/content/limitations/module-path-globals.mdx @@ -0,0 +1,4 @@ +`__dirname` and `__filename` are compile-time constants: the absolute path of the module's source directory and file at build time, the same strings as `import.meta.dirname` and `import.meta.filename`. +A `.ts` or `.tsx` file keeps them even when it uses `import` syntax, which Node.js would load as an ES module without these globals, because TypeScript projects are commonly shipped as CommonJS bundles where they exist. +In such a file `typeof __dirname` is `"string"`, where Node.js running the source directly reports `"undefined"`. +JavaScript ES modules (`.mjs`, or `.js` in a `"type": "module"` package) do not have them, as in Node.js. diff --git a/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx b/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx index e8b351679..be8b07571 100644 --- a/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx +++ b/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx @@ -1,3 +1,4 @@ - **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. - A function value with fixed parameters cannot be stored where such a variadic function type is expected. + A function value whose fixed parameters are all `unknown` or `any`, or that has none, can be stored where such a variadic function type without fixed parameters is expected: each call passes the arguments in order, `undefined` for the ones the caller left out. + Other function values with fixed parameters cannot be stored there. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index a02446908..2d10f73b4 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -55940,7 +55940,9 @@ "confidence": "high", "source": "compiler-feature:process.'disconnect'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -55982,7 +55984,9 @@ "confidence": "high", "source": "compiler-feature:process.'message'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -56365,7 +56369,9 @@ "confidence": "high", "source": "compiler-feature:process.process.disconnect", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -57013,7 +57019,9 @@ "confidence": "high", "source": "compiler-feature:process.process.send", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -57467,7 +57475,9 @@ "confidence": "high", "source": "compiler-feature:process.connected", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -57582,7 +57592,10 @@ "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", - "tests/corpus/2944-process-exit-code-wrap.ts" + "tests/corpus/2944-process-exit-code-wrap.ts", + "tests/corpus/6422-process-exitcode-read.ts", + "tests/corpus/6423-process-exitcode-default.ts", + "tests/corpus/6424-process-exit-keeps-exitcode.ts" ] } } @@ -58969,7 +58982,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59001,7 +59016,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59229,7 +59246,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59290,7 +59309,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59322,7 +59343,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59405,7 +59428,8 @@ "confidence": "high", "source": "compiler-dedicated:readline.createInterface", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] } } @@ -59544,7 +59568,8 @@ "confidence": "high", "source": "compiler-dedicated:rl.close", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] } } @@ -59574,7 +59599,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59604,7 +59631,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59634,7 +59663,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59664,7 +59695,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59694,7 +59727,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59724,7 +59759,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59754,7 +59791,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59784,7 +59823,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59904,7 +59945,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59934,7 +59977,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59964,7 +60009,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -60022,7 +60069,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -60052,7 +60101,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -66493,6 +66544,7 @@ "source": "compiler-feature:test.mock.fn", "tests": [ "tests/fixtures/node-test/cases/mock-fn.ts", + "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts" ] } @@ -76165,6 +76217,8 @@ "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", + "tests/corpus/6428-promisify-callback-functions.ts", + "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts" ] } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 3690ce077..637a83345 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -79409,6 +79409,11 @@ "tests/corpus/5210-process-memory-usage.ts", "tests/corpus/5211-process-identity.ts", "tests/corpus/5809-process-exit-and-rejections.cjs", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts", + "tests/corpus/6422-process-exitcode-read.ts", + "tests/corpus/6423-process-exitcode-default.ts", + "tests/corpus/6424-process-exit-keeps-exitcode.ts", "tests/corpus/990-process-basics.ts", "tests/corpus/node24-env-files-errors.ts", "tests/corpus/node24-env-files.ts", @@ -79486,7 +79491,9 @@ "status": "partial", "evidence": "compiler-feature:process.'disconnect'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -79525,7 +79532,9 @@ "status": "partial", "evidence": "compiler-feature:process.'message'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -79938,7 +79947,9 @@ "status": "partial", "evidence": "compiler-feature:process.process.disconnect", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -80583,7 +80594,9 @@ "status": "partial", "evidence": "compiler-feature:process.process.send", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -81002,7 +81015,9 @@ "status": "partial", "evidence": "compiler-feature:process.connected", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "chapter" @@ -81111,7 +81126,10 @@ "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", - "tests/corpus/2944-process-exit-code-wrap.ts" + "tests/corpus/2944-process-exit-code-wrap.ts", + "tests/corpus/6422-process-exitcode-read.ts", + "tests/corpus/6423-process-exitcode-default.ts", + "tests/corpus/6424-process-exit-keeps-exitcode.ts" ] }, "anchorSource": "chapter" @@ -82492,7 +82510,10 @@ "tests/corpus/5791-readline-keypress-interfaces.cjs", "tests/corpus/5792-readline-keypress-listeners.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] }, "anchorSource": "chapter" @@ -82546,7 +82567,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82577,7 +82600,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82797,7 +82822,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82879,7 +82906,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82910,7 +82939,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82990,7 +83021,8 @@ "status": "partial", "evidence": "compiler-dedicated:readline.createInterface", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] }, "anchorSource": "exact" @@ -83146,7 +83178,8 @@ "status": "partial", "evidence": "compiler-dedicated:rl.close", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] }, "anchorSource": "exact" @@ -83175,7 +83208,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83204,7 +83239,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83233,7 +83270,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83262,7 +83301,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83291,7 +83332,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83320,7 +83363,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83349,7 +83394,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83378,7 +83425,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83494,7 +83543,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83523,7 +83574,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83552,7 +83605,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83608,7 +83663,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83637,7 +83694,9 @@ "tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", - "tests/corpus/5299-readline-stdin.ts" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -91104,6 +91163,7 @@ "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/fixtures/node-test/cases/get-test-context.ts", + "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", @@ -92515,6 +92575,7 @@ "evidence": "compiler-feature:test.mock.fn", "tests": [ "tests/fixtures/node-test/cases/mock-fn.ts", + "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts" ] }, @@ -101196,6 +101257,7 @@ "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/5884-inspect-layout-options.ts", + "tests/corpus/6428-promisify-callback-functions.ts", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -101222,6 +101284,7 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/deep-equality.ts", "tests/fixtures/node-types/env-files.ts", + "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/fixtures/node-types/promisify-fs.ts", @@ -102880,6 +102943,8 @@ "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", + "tests/corpus/6428-promisify-callback-functions.ts", + "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts" ] }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 855d065f7..701b5aac1 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -148,7 +148,7 @@ "node-builtin.process.versions": ["tests/corpus/process-versions-storage.ts"], "node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"], "node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"], - "node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"], + "node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts", "tests/corpus/6422-process-exitcode-read.ts", "tests/corpus/6423-process-exitcode-default.ts", "tests/corpus/6424-process-exit-keeps-exitcode.ts"], "node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"], "node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"], "node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"], @@ -199,13 +199,13 @@ {"chapter": "events", "symbols": ["CustomEvent", "detail"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts"]}, {"chapter": "events", "symbols": ["events.errorMonitor"], "status": "partial", "evidence": ["tests/corpus/5134-events-error-monitor.ts", "tests/corpus/5136-events-error-monitor-string-name.ts", "tests/corpus/5137-events-error-monitor-computed-name.js", "tests/corpus/5138-events-error-monitor-shared-listener.ts"]}, {"chapter": "events", "symbols": ["events.addAbortListener"], "status": "partial", "evidence": ["tests/corpus/5133-abort-signal-event-target.ts"]}, - {"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs"]}, + {"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]}, {"chapter": "readline", "symbols": ["'history'", "rl.line"], "status": "partial", "evidence": ["tests/corpus/5780-readline-history-options.ts", "tests/corpus/5781-readline-history-options.cjs"]}, {"chapter": "readline", "symbols": ["'SIGINT'", "'SIGTSTP'", "'SIGCONT'", "line", "cursor", "completer"], "status": "partial", "evidence": ["tests/corpus/5782-readline-terminal-editing.ts", "tests/corpus/5783-readline-terminal-completion.cjs", "tests/corpus/5784-readline-terminal-promises.ts", "tests/corpus/5785-readline-terminal-keys.ts", "tests/corpus/5786-readline-terminal-history.cjs"]}, {"chapter": "readline", "symbols": ["readline.cursorTo", "readline.moveCursor", "readline.clearLine", "readline.clearScreenDown", "rl.getCursorPos"], "status": "partial", "evidence": ["tests/corpus/5787-readline-cursor-helpers.cjs", "tests/corpus/5789-readline-cursor-pos.cjs"]}, {"chapter": "readline", "symbols": ["readlinePromises.Readline", "rl.clearLine", "rl.clearScreenDown", "rl.commit", "rl.cursorTo", "rl.moveCursor", "rl.rollback"], "status": "partial", "evidence": ["tests/corpus/5788-readline-promises-readline.ts", "tests/corpus/5789-readline-cursor-pos.cjs"]}, {"chapter": "readline", "symbols": ["readline.emitKeypressEvents"], "status": "partial", "evidence": ["tests/corpus/5790-readline-keypress-events.ts", "tests/corpus/5791-readline-keypress-interfaces.cjs", "tests/corpus/5792-readline-keypress-listeners.ts"]}, - {"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts"]}, + {"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]}, {"chapter": "stream", "symbols": ["stream.addAbortSignal"], "status": "partial", "evidence": ["tests/corpus/5300-stream-add-abort-signal.ts", "tests/corpus/5301-stream-add-abort-signal-errors.cjs"]}, {"chapter": "stream", "symbols": ["readable.map", "readable.filter", "readable.forEach", "readable.toArray", "readable.some", "readable.find", "readable.every", "readable.flatMap", "readable.drop", "readable.take", "readable.reduce"], "status": "partial", "evidence": ["tests/corpus/5335-stream-operators-values.ts", "tests/corpus/5336-stream-operators-promise-timing.ts", "tests/corpus/5337-stream-operators-concurrency.ts", "tests/corpus/5338-stream-operators-checks.cjs"]}, {"chapter": "stream", "symbols": ["stream.Readable.from"], "status": "partial", "evidence": ["tests/corpus/5340-readable-from-options.ts", "tests/corpus/5341-readable-from-dynamic.cjs", "tests/corpus/5420-readable-from-generators.ts", "tests/corpus/5421-readable-from-generators.cjs", "tests/corpus/5610-readable-from-sets-and-views.ts", "tests/corpus/5611-readable-from-class-iterables.ts", "tests/corpus/5612-readable-from-iterables.cjs"]}, @@ -341,14 +341,14 @@ { "chapter": "fs", "signatures": ["`fsPromises.lstat(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] }, { "chapter": "fs", "signatures": ["`fs.globSync(pattern[, options])`", "`fs.glob(pattern[, options], callback)`", "`fsPromises.glob(pattern[, options])`"], "status": "partial", "evidence": ["tests/corpus/5448-fs-glob-patterns.ts", "tests/corpus/5449-fs-glob-options.ts", "tests/corpus/5450-fs-glob-validation.js", "tests/corpus/5451-fs-promises-glob.ts", "tests/corpus/5452-fs-glob-callback.ts", "tests/fixtures/node-types/fs-glob.ts"] }, { "chapter": "fs", "signatures": ["`fs.cpSync(src, dest[, options])`", "`fs.cp(src, dest[, options], callback)`", "`fsPromises.cp(src, dest[, options])`"], "status": "partial", "evidence": ["tests/corpus/5567-fs-cpsync.ts", "tests/corpus/5568-fs-cp-errors.js", "tests/corpus/5569-fs-cp-symlinks.ts", "tests/corpus/5570-fs-cp-async.ts"] }, - { "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] }, + { "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/6420-process-send-presence.ts", "tests/corpus/6421-process-send-presence-fork/main.ts"] }, { "chapter": "fs", "symbols": ["fs.Stats", "stats.isBlockDevice", "stats.isCharacterDevice", "stats.isDirectory", "stats.isFIFO", "stats.isFile", "stats.isSocket", "stats.isSymbolicLink"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] }, { "chapter": "fs", "parent": "Class: `fs.Stats`", "signatures": ["`mode` Type: {number|bigint}", "`nlink` Type: {number|bigint}", "`uid` Type: {number|bigint}", "`gid` Type: {number|bigint}", "`rdev` Type: {number|bigint}", "`size` Type: {number|bigint}", "`blksize` Type: {number|bigint}", "`blocks` Type: {number|bigint}", "`atimeMs` Type: {number|bigint}", "`mtimeMs` Type: {number|bigint}", "`birthtimeMs` Type: {number|bigint}", "`atimeNs` Type: {bigint}", "`mtimeNs` Type: {bigint}", "`ctimeNs` Type: {bigint}", "`birthtimeNs` Type: {bigint}", "`atime` Type: {Date}", "`mtime` Type: {Date}", "`ctime` Type: {Date}", "`birthtime` Type: {Date}"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] }, { "chapter": "fs", "signatures": ["`dev` Type: {number|bigint}", "`ino` Type: {number|bigint}", "`ctimeMs` Type: {number|bigint}"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] }, { "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "fs", "signatures": ["`fsPromises.realpath(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2940-fsp-realpath.ts"] }, { "chapter": "fs", "signatures": ["`fsPromises.readdir(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2942-fsp-readdir-dirent.ts"] }, - { "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, + { "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts", "tests/corpus/5876-readline-dispose-instanceof.ts"] }, { "chapter": "timers", "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "stream", "signatures": ["`readable[Symbol.asyncIterator]()`", "`readable.iterator([options])`"], "status": "partial", "evidence": ["tests/corpus/1746-stream-for-await.ts", "tests/corpus/2861-readable-iterator-close.ts"] }, { "chapter": "stream", "signatures": ["`writable.destroy([error])`"], "status": "partial", "evidence": ["tests/corpus/2897-child-stdin-destroy.ts"] }, @@ -370,7 +370,7 @@ { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.filePath", "passed", "attempt", "tags", "workerId"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-members.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `SuiteContext`", "symbols": ["context.name", "context.fullName", "context.filePath", "context.diagnostic", "passed", "attempt"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-context.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, - { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, @@ -378,7 +378,7 @@ { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, - { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, { "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] }, { "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] } @@ -416,7 +416,7 @@ { "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] }, { "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, { "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, - { "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] }, + { "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts", "tests/corpus/5878-readline-dynamic-streams.mjs"] }, { "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, { "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] } ] diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index a8f1f8a8b..436bcace3 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3372,6 +3372,13 @@ declare module "util" { info: import("node:crypto").BinaryLike, keylen: number, ) => Promise; + /* A callback-only function (`obj.close.bind(obj)`): before the targets + * whose leading parameters would otherwise capture it. Builtins that take + * arguments before their callback never match these. */ + export function promisify( + fn: (callback: (err: any, result: TResult) => void) => void, + ): () => Promise; + export function promisify(fn: (callback: (err?: any) => void) => void): () => Promise; export function promisify( fn: typeof import("node:crypto").randomFill, ): ( @@ -4006,7 +4013,10 @@ declare module "readline" { meta?: boolean; shift?: boolean; } - export interface Interface extends Disposable { + export class Interface implements Disposable { + /* createInterface creates interfaces; instanceof tells the two + * modules' classes apart. */ + protected constructor(); /* terminal says whether the interface edits lines as a terminal; * line and cursor are the terminal's (line is '' otherwise). history * is the history option itself (or the interface's own empty array); @@ -4121,7 +4131,10 @@ declare module "readline/promises" { /* The readline Interface with the promise question: it resolves with * the next line, rejects with an AbortError when options.signal aborts, * and with ERR_USE_AFTER_CLOSE after close(). */ - export interface Interface extends Disposable { + export class Interface implements Disposable { + /* createInterface creates interfaces; instanceof tells the two + * modules' classes apart. */ + protected constructor(); /* terminal says whether the interface edits lines as a terminal; * line and cursor are the terminal's (line is '' otherwise). history * is the history option itself (or the interface's own empty array); @@ -5238,6 +5251,8 @@ declare module "tls" { getProtocol(): string | null; /* OpenSSL's suite name, the IANA name and the suite's version */ getCipher(): CipherNameAndProtocol; + /* whether the handshake resumed a cached session */ + isSessionReused(): boolean; } export interface CipherNameAndProtocol { name: string; diff --git a/packages/compiler/src/backend/llvm/byte-numbers.ts b/packages/compiler/src/backend/llvm/byte-numbers.ts index c51d03744..9001e015d 100644 --- a/packages/compiler/src/backend/llvm/byte-numbers.ts +++ b/packages/compiler/src/backend/llvm/byte-numbers.ts @@ -4,6 +4,7 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { emitTruncF64 } from "./trunc.js"; +import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js"; /** Supported LLVM targets are little-endian. All memory accesses use align * 1: Buffer fields and DataView windows can start at arbitrary byte offsets. */ @@ -79,7 +80,7 @@ export function emitByteNumber( capacity = B.tmp(); room = B.tmp(); B.line(`${lengthPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`); - B.line(`${length} = load ${size}, ptr ${lengthPtr}`); + B.line(`${length} = load ${size}, ptr ${lengthPtr}${bytesHeaderAlias(host)}`); B.line(`${room} = icmp uge ${size} ${length}, ${spec.width}`); B.line(`${capacity} = sub ${size} ${length}, ${spec.width}`); if (integer) { @@ -221,14 +222,14 @@ export function emitByteNumber( } } const stored = endianBits(host, raw, spec.width, little); - B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1`); + B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1${bytesElementAlias(host)}`); if (!spec.dataView) { fastResult = B.tmp(); B.line(`${fastResult} = fadd double ${offset.name}, ${f64Lit(spec.width)}`); } } else { const loaded = B.tmp(); - B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1`); + B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1${bytesElementAlias(host)}`); const raw = endianBits(host, loaded, spec.width, little); fastResult = B.tmp(); if (spec.floating) { diff --git a/packages/compiler/src/backend/llvm/bytes-alias.ts b/packages/compiler/src/backend/llvm/bytes-alias.ts new file mode 100644 index 000000000..9aaeaa6c7 --- /dev/null +++ b/packages/compiler/src/backend/llvm/bytes-alias.ts @@ -0,0 +1,23 @@ +import type { LlvmEmitterContext } from "./expr-context.js"; + +/** Alias categories of typed-array storage. A ScrBytes header (length, + * data pointer, shared backing) is fixed at construction and only ever + * written by runtime code, never by emitted code. Element storage is one + * category for every element type: views of different types may share a + * buffer. Neither overlaps the other, a native field, array storage or a + * module global's slot, so element stores leave header loads and global + * receiver slots loop-invariant. Untagged accesses and calls still alias + * every category. */ +export function bytesHeaderAlias(host: LlvmEmitterContext): string { + return host.memoryAliasAttachment("bytes:header"); +} + +export function bytesElementAlias(host: LlvmEmitterContext): string { + return host.memoryAliasAttachment("bytes:elements"); +} + +/** A module global's slot is its own category for whole-slot loads of a + * scalar or typed-array binding. Stores stay untagged (and so alias it). */ +export function globalSlotAlias(host: LlvmEmitterContext, id: string, type: string): string { + return type === "bytes" || type === "f64" ? host.memoryAliasAttachment(`global:${id}`) : ""; +} diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts new file mode 100644 index 000000000..ddcfd667d --- /dev/null +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -0,0 +1,278 @@ +import { expect, test } from "vitest"; +import { + BOOL, + F64, + VOID, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { analyzeIntegerRanges } from "../../ir/integer-ranges.js"; +import { matchIntegerCountedForLoop } from "../../ir/integer-loops.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import { matchBytesLoopVersion, type BytesLoopVersion } from "./bytes-loop-versions.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "versions.ts", start: 0, end: 0 }; +const f64s: IrType = { kind: "bytes", elem: "f64" }; +const i32s: IrType = { kind: "bytes", elem: "i32" }; +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const ref = (localId: string, type: IrType = F64): IrExpr => ({ + kind: "varRef", + localId, + type, + loc, +}); +const bin = (op: string, left: IrExpr, right: IrExpr): IrExpr => + ({ kind: "bin", op, left, right, type: F64, loc }) as IrExpr; +const get = (receiver: IrExpr, index: IrExpr): IrExpr => ({ + kind: "bytesIntrinsic", + method: "get", + receiver, + args: [index], + invalidNaN: true, + type: F64, + loc, +}); +const set = (arr: IrExpr, index: IrExpr, value: IrExpr): IrStmt => ({ + kind: "bytesSet", + arr, + index, + value, + loc, +}); +const counted = ( + body: IrStmt[], + limit: IrExpr = ref("n"), + counter = "i", +): IrStmt & { kind: "for" } => ({ + kind: "for", + init: { kind: "varDecl", localId: counter, init: num(0), loc }, + cond: { kind: "bin", op: "<", left: ref(counter), right: limit, type: BOOL, loc }, + update: { kind: "assign", localId: counter, value: bin("+", ref(counter), num(1)), loc }, + body, + loc, +}); +const local = ( + id: string, + type: IrType, + mutable = false, + extra: Partial = {}, +): IrLocal => ({ + id, + name: id, + type, + mutable, + ...extra, +}); + +function fn(body: IrStmt[], extra: IrLocal[] = []): IrFunction { + return { + name: "kernel", + params: [ + { localId: "out", name: "out", type: f64s }, + { localId: "src", name: "src", type: f64s }, + { localId: "table", name: "table", type: i32s }, + { localId: "n", name: "n", type: F64 }, + { localId: "k", name: "k", type: F64 }, + ], + returnType: VOID, + locals: [ + local("out", f64s), + local("src", f64s), + local("table", i32s), + local("n", F64), + local("k", F64), + local("i", F64, true), + local("j", F64, true), + ...extra, + ], + body, + loc, + }; +} + +function match( + f: IrFunction, + loop: IrStmt & { kind: "for" }, + captured: string[] = [], +): BytesLoopVersion | null { + const ranges = analyzeIntegerRanges(f); + const locals = new Map(f.locals.map((l) => [l.id, l])); + return matchBytesLoopVersion(loop, matchIntegerCountedForLoop(loop, locals, ranges), { + locals, + invariantGlobals: new Set(), + captured: new Set(captured), + ranges, + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + }); +} + +test("a counted loop's affine accesses get one guard and exact operand ranges", () => { + const shifted = bin("+", ref("i"), num(1)); + const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]); + const version = match(fn([loop]), loop)!; + expect(version).not.toBeNull(); + expect(version.bounds.size).toBe(2); + expect(version.receivers.map((r) => r.ref.kind === "varRef" && r.ref.localId)).toEqual([ + "out", + "src", + ]); + expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([ + [1, 0, 0], + [1, 1, 1], + ]); + // `n` is unproven: the guard bounds the counter so `i + 1` stays exact. + expect(version.counted?.limitGuarded).toBe(true); + expect(version.counterBound).toBe(Number.MAX_SAFE_INTEGER - 1); + expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER }); +}); + +test("statically ranged indices compare one constant with the length", () => { + const masked = bin("&", bin("^", ref("i"), ref("k")), num(255)); + const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]); + const version = match(fn([loop]), loop)!; + expect(version.fixed.map((c) => [version.receivers[c.receiver]!.ref, c.end])).toEqual([ + [ref("out", f64s), 0], + [ref("table", i32s), 255], + ]); + expect(version.affine).toEqual([]); + expect(version.counted).toBeNull(); +}); + +test("scaled counters and body temps resolve to their receiver and index", () => { + const temps = [local("view", f64s), local("at", F64)]; + const at = bin("+", bin("*", ref("i"), num(4)), num(3)); + const loop = counted([ + { kind: "varDecl", localId: "view", init: ref("src", f64s), loc }, + { kind: "varDecl", localId: "at", init: at, loc }, + set(ref("view", f64s), ref("at"), num(1)), + // (The integer counted loop needs one direct counter index.) + set(ref("out", f64s), ref("i"), num(0)), + ]); + const version = match(fn([loop], temps), loop)!; + expect(version.receivers.map((r) => r.ref)).toEqual([ref("src", f64s), ref("out", f64s)]); + expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([ + [4, 3, 3], + [1, 0, 0], + ]); + expect(version.ranges.has(at)).toBe(true); +}); + +test("negative factors, rebinding, captures and outer loops are not versioned", () => { + // `i * -1` is -0 at zero, so it is not treated as an affine operand. + const negative = counted([set(ref("out", f64s), bin("*", ref("i"), num(-1)), num(1))]); + expect(match(fn([negative]), negative)).toBeNull(); + const rebound = counted([ + set(ref("out", f64s), ref("i"), num(1)), + { kind: "assign", localId: "out", value: ref("src", f64s), loc }, + ]); + expect(match(fn([rebound]), rebound)).toBeNull(); + const plain = counted([set(ref("out", f64s), ref("i"), num(1))]); + expect(match(fn([plain]), plain, ["out"])).toBeNull(); + const inner = counted([set(ref("out", f64s), ref("j"), num(1))], ref("n"), "j"); + const outer = counted([inner]); + expect(match(fn([outer]), outer)).toBeNull(); + expect(match(fn([outer]), inner)).not.toBeNull(); +}); + +test("emitted loops version the body and tag typed-array memory", () => { + 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", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const llvm = emitLlvmModule(mod); + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(llvm)![0]; + expect(body).toContain("bytes.version.fast"); + expect(body).toContain("bytes.version.slow"); + // The checked copy keeps its per-element tests; the guard reads lengths. + expect(body).toContain("bytes.index.invalid"); + expect(body).toMatch(/store double [^\n]*, align 1, !tbaa !\d+/); + expect(body).toMatch(/load double, ptr [^\n]*, align 1, !tbaa !\d+/); + expect(body).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+/); + expect(body).toMatch(/load ptr, ptr [^\n]*, !tbaa !\d+/); + const elements = /store double [^\n]*!tbaa !(\d+)/.exec(body)![1]; + const header = /load ptr, ptr [^\n]*!tbaa !(\d+)/.exec(body)![1]; + expect(elements).not.toBe(header); +}); + +test("Float16Array accesses version in elements and share the element tag", () => { + const f16s: IrType = { kind: "bytes", elem: "f16" }; + const withHalves = (body: IrStmt[]): IrFunction => { + const f = fn(body, [local("half", f16s)]); + return { ...f, params: [...f.params, { localId: "half", name: "half", type: f16s }] }; + }; + // half[i] = src[i + 1]; out[i] = half[i] * 2 + const loop = counted([ + set(ref("half", f16s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))), + set(ref("out", f64s), ref("i"), bin("*", get(ref("half", f16s), ref("i")), num(2))), + ]); + const kernel = withHalves([loop]); + const version = match(kernel, loop)!; + expect(version.bounds.size).toBe(4); + expect(version.receivers.map((r) => r.ref)).toEqual([ + ref("half", f16s), + ref("src", f64s), + ref("out", f64s), + ]); + // Element offsets against the element count, whatever the element size. + expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([ + [1, 0, 0], + [1, 1, 1], + [1, 0, 0], + ]); + + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + 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(fast).toContain("call double @scr_f16_to_f64"); + expect(fast).toContain("call zeroext i16 @scr_f16_from_f64"); + expect(fast).not.toContain("bytes.index"); + const halfStore = /store i16 [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1]; + const halfLoad = /load i16, ptr [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1]; + const doubleStore = /store double [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1]; + // Views of any element type may share a buffer: one element category. + expect(halfStore).toBe(doubleStore); + expect(halfLoad).toBe(doubleStore); + // The checked copy keeps its per-element tests. + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); +}); + +test("bodies compiled without optimization keep one checked loop", () => { + const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), ref("i")))]); + const boxes = Array.from({ length: 512 }, (_, k) => local(`box${k}`, F64, true, { boxed: true })); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn([loop], boxes), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + expect(body.split("\n", 1)[0]).toContain("optnone"); + expect(body).not.toContain("bytes.version"); + expect(body).toContain("bytes.index.invalid"); +}); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts new file mode 100644 index 000000000..df8977426 --- /dev/null +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -0,0 +1,578 @@ +import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; +import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { LlvmEmitterContext } from "./expr-context.js"; +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"; + +type Node = IrExpr | IrStmt; +type VarRef = IrExpr & { kind: "varRef" }; + +/** One loop-invariant integer operand of an affine index. */ +interface Term { + expr: IrExpr; + /** 1 or -1. */ + sign: number; +} + +interface KeyedTerm extends Term { + key: string; +} + +/** Every access of `receiver` at a static index range ends below `end + 1`. */ +interface FixedCheck { + receiver: number; + end: number; +} + +/** Every access of `receiver` at `scale * counter + Σ terms + offset` has an + * offset in [minOffset, maxEnd - width + 1]; `maxEnd` includes the width. */ +interface AffineCheck { + receiver: number; + scale: number; + terms: KeyedTerm[]; + minOffset: number; + maxEnd: number; +} + +export interface BytesLoopVersion { + /** Typed-array accesses that are in bounds whenever the guard holds. */ + bounds: ReadonlySet; + /** Root receiver bindings, read once before the loop. */ + receivers: { ref: VarRef; global: boolean }[]; + fixed: FixedCheck[]; + affine: AffineCheck[]; + /** The counted loop whose iteration space the affine checks cover. */ + counted: IntegerCountedForLoop | null; + /** The guard also requires every counter value within [-bound, bound]. */ + counterBound: number | null; + /** Integer ranges of affine index operands that hold in the unchecked + * loop (where the guard bounds the counter). */ + ranges: ReadonlyMap; +} + +export interface BytesLoopFacts { + /** The function's own locals (not module globals). */ + locals: ReadonlyMap; + /** Module globals whose slot no call can replace: stable constants and + * the private bindings of a guarded initializer. */ + invariantGlobals: ReadonlySet; + captured: ReadonlySet; + ranges: IntegerRanges; + /** Locals definitely initialized when the loop is entered. */ + initialized: ReadonlySet; + /** Accesses already proved in bounds without a guard. */ + proven: ReadonlySet; +} + +const MAX_COST = 400; +const MAX_FIXED_END = 2 ** 32; +const MAX_CHECKS = 8; +const MAX_OFFSET = 2 ** 31; +const SAFE = Number.MAX_SAFE_INTEGER; +const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]); + +/** Loop versioning for typed-array bounds. A typed array's length and data + * pointer are fixed at construction (ScrBytes has no resize, transfer or + * detach), so an access is in bounds for the whole loop when its receiver + * binding and index operands cannot change while it runs. One test before + * the loop then covers every iteration: + * + * - a statically ranged integer index `[lo, hi]` (lo >= 0) needs + * `hi < length`, such as `table[x & 0xff]` or a constant-trip counter; + * - an index `k * counter + invariant + c` of a counted loop needs its first + * and last values in bounds, such as `out[base + i]` for `i < n` or + * `view.getUint32(i * 4)`. + * + * 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 + * write and NaN-index behavior. */ +export function matchBytesLoopVersion( + loop: IrStmt & { kind: "for" }, + counted: IntegerCountedForLoop | null, + facts: BytesLoopFacts, +): BytesLoopVersion | null { + const written = new Set(); + const temps = new Map(); + let cost = 0; + const scanExpr = (e: IrExpr): boolean => { + if (++cost > MAX_COST) return false; + if (e.kind === "assignExpr" || e.kind === "incDec") written.add(e.localId); + return everyExprChild(e, scanExpr, scanStmt); + }; + const scanStmt = (s: IrStmt): boolean => { + if (++cost > MAX_COST) return false; + switch (s.kind) { + case "forOf": + written.add(s.localId); + break; + case "varDecl": { + if (temps.has(s.localId) || written.has(s.localId)) temps.delete(s.localId); + else if (s.init) temps.set(s.localId, s.init); + written.add(s.localId); + break; + } + case "assign": + written.add(s.localId); + break; + case "tryCatch": + if (s.catchLocalId) written.add(s.catchLocalId); + break; + default: + break; + } + return everyStmtChild(s, scanExpr, scanStmt); + }; + // The guard runs before the initializer, so a binding the initializer + // writes is not invariant either: `for (let i = (a = b, 0); ...)` must + // not keep the bounds the guard proved for the old `a`. + if ( + (loop.init !== null && !scanStmt(loop.init)) || + !loop.body.every(scanStmt) || + (loop.cond !== null && !scanExpr(loop.cond)) || + (loop.update !== null && !scanStmt(loop.update)) + ) + return null; + // A body temp stands for its initializer only when it is a constant + // binding assigned exactly once, by that declaration. + const declared = new Map(); + const countDecls = (s: IrStmt): boolean => { + if (s.kind === "varDecl") declared.set(s.localId, (declared.get(s.localId) ?? 0) + 1); + return everyStmtChild(s, countExprDecls, countDecls); + }; + const countExprDecls = (e: IrExpr): boolean => everyExprChild(e, countExprDecls, countDecls); + loop.body.every(countDecls); + const temp = (id: string): IrExpr | null => { + const local = facts.locals.get(id); + const init = temps.get(id); + if ( + !init || + !local || + local.mutable || + local.boxed || + local.tdz || + facts.captured.has(id) || + declared.get(id) !== 1 + ) + return null; + return init; + }; + const counter = counted?.localId ?? null; + // A binding keeps its loop-entry value for the whole loop. + const invariant = (id: string): boolean => { + if (written.has(id) || id === counter) return false; + const local = facts.locals.get(id); + if (local) + return !local.boxed && !local.tdz && !facts.captured.has(id) && facts.initialized.has(id); + return facts.invariantGlobals.has(id); + }; + const resolving = new Set(); + const root = (e: IrExpr): VarRef | null => { + if (e.kind !== "varRef" || e.type.kind !== "bytes") return null; + const init = temp(e.localId); + if (init) { + if (resolving.has(e.localId)) return null; + resolving.add(e.localId); + const result = root(init); + resolving.delete(e.localId); + return result; + } + return invariant(e.localId) ? e : null; + }; + // A pure integer operand evaluated again before the loop. + const invariantKey = (e: IrExpr): string | null => { + if (!facts.ranges.get(e)) return null; + if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null; + if (e.kind === "varRef") + return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null; + if ( + e.kind === "bin" && + INVARIANT_OPS.has(e.op) && + e.left.type.kind === "f64" && + e.right.type.kind === "f64" + ) { + const left = invariantKey(e.left), + right = invariantKey(e.right); + return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null; + } + return null; + }; + interface Affine { + /** The counter's coefficient; 0 when the value is loop-invariant. */ + counter: number; + terms: KeyedTerm[]; + offset: number; + } + // Every operand of an affine index is `counter * k + Σ terms + offset`. + // Invariant terms need proven ranges. Operands that involve the counter + // need none: the guard bounds the counter so that each of them is a safe + // integer, which makes the index's double arithmetic exact. + const shapes = new Map(); + const affine = (e: IrExpr): Affine | null => { + if (shapes.has(e)) return shapes.get(e)!; + const result = affineShape(e); + shapes.set(e, result); + return result; + }; + const affineShape = (e: IrExpr): Affine | null => { + if (e.kind === "numLit") + return Number.isSafeInteger(e.value) && Math.abs(e.value) <= MAX_OFFSET + ? { counter: 0, terms: [], offset: e.value } + : null; + if (e.kind === "varRef") { + if (e.localId === counter) return { counter: 1, terms: [], offset: 0 }; + const init = temp(e.localId); + if (init) { + if (resolving.has(e.localId)) return null; + resolving.add(e.localId); + const result = affine(init); + resolving.delete(e.localId); + return result; + } + } + if ( + e.kind === "bin" && + (e.op === "+" || e.op === "-") && + e.left.type.kind === "f64" && + e.right.type.kind === "f64" + ) { + const left = affine(e.left), + right = affine(e.right); + if (left && right && (left.counter === 0 || right.counter === 0)) { + const sign = e.op === "+" ? 1 : -1; + const offset = left.offset + sign * right.offset; + if (Math.abs(offset) > MAX_OFFSET) return null; + const terms = [ + ...left.terms, + ...right.terms.map((t) => ({ expr: t.expr, sign: sign * t.sign, key: t.key })), + ]; + const result = { counter: left.counter + sign * right.counter, terms, offset }; + if (result.counter !== 0 || facts.ranges.get(e)) return result; + } + } + // A positive constant multiple of the counter: `i * 4`, `4 * i`, + // `(i + 1) * 8`. (A negative factor turns a zero into -0.) + if (e.kind === "bin" && e.op === "*") { + const rightLiteral = e.right.kind === "numLit"; + const scaled = rightLiteral ? e.left : e.right; + const literal = rightLiteral ? e.right : e.left; + const factor = literal.kind === "numLit" ? literal.value : 0; + if ( + literal.kind === "numLit" && + scaled.type.kind === "f64" && + Number.isSafeInteger(factor) && + factor > 0 && + factor <= MAX_OFFSET + ) { + const inner = affine(scaled); + if (inner && inner.counter !== 0 && inner.terms.length === 0) { + const counterScale = inner.counter * factor, + offset = inner.offset * factor; + if (Math.abs(counterScale) <= MAX_OFFSET && Math.abs(offset) <= MAX_OFFSET) + return { counter: counterScale, terms: [], offset }; + } + } + } + const key = invariantKey(e); + return key !== null ? { counter: 0, terms: [{ expr: e, sign: 1, key }], offset: 0 } : null; + }; + // The operands of an index that involve the counter. + const chain = (e: IrExpr, out: Set): void => { + if (!shapes.get(e)?.counter) return; + out.add(e); + if (e.kind === "varRef") { + const init = temp(e.localId); + if (init) chain(init, out); + } else if (e.kind === "bin") { + chain(e.left, out); + chain(e.right, out); + } + }; + const termMagnitude = (terms: Term[]): number => + terms.reduce((sum, t) => { + const r = facts.ranges.get(t.expr)!; + return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); + }, 0); + + const receivers: { ref: VarRef; global: boolean }[] = []; + const receiverIndex = new Map(); + const fixed = new Map(); + const affineChecks = new Map< + string, + { check: AffineCheck; nodes: Node[]; operands: Set } + >(); + const receiverOf = (ref: VarRef): number => { + let index = receiverIndex.get(ref.localId); + if (index === undefined) { + index = receivers.length; + receiverIndex.set(ref.localId, index); + receivers.push({ ref, global: !facts.locals.has(ref.localId) }); + } + return index; + }; + const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { + if (facts.proven.has(node)) return; + const ref = root(array); + if (!ref) return; + const shape = counter !== null ? affine(index) : null; + if (shape && shape.counter !== 0) { + const operands = new Set(); + chain(index, operands); + const terms = [...shape.terms].sort((a, b) => + a.key < b.key ? -1 : a.key > b.key ? 1 : a.sign - b.sign, + ); + const receiver = receiverOf(ref); + const key = `${receiver}|${shape.counter}|${terms.map((t) => `${t.sign}${t.key}`).join(",")}`; + const end = shape.offset + width - 1; + const group = affineChecks.get(key); + if (group) { + group.check.minOffset = Math.min(group.check.minOffset, shape.offset); + group.check.maxEnd = Math.max(group.check.maxEnd, end); + group.nodes.push(node); + for (const operand of operands) group.operands.add(operand); + } else + affineChecks.set(key, { + check: { receiver, scale: shape.counter, terms, minOffset: shape.offset, maxEnd: end }, + nodes: [node], + operands, + }); + return; + } + const range = facts.ranges.get(index); + if (!range) return; + const end = range.max + width - 1; + // Only a length a buffer can have: an outer loop must not pay for a + // guard that bounds an inner loop's unbounded counter. + if (range.min < 0 || end >= MAX_FIXED_END) return; + const receiver = receiverOf(ref); + const group = fixed.get(receiver); + if (group) { + group.check.end = Math.max(group.check.end, end); + group.nodes.push(node); + } else fixed.set(receiver, { check: { receiver, end }, nodes: [node] }); + }; + const visitExpr = (e: IrExpr): boolean => { + if (e.kind === "bytesIntrinsic") { + const numeric = byteNumberAccess(e); + if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width); + else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1); + } + return everyExprChild(e, visitExpr, visitStmt); + }; + const visitStmt = (s: IrStmt): boolean => { + if (s.kind === "bytesSet") access(s, s.arr, s.index, 1); + return everyStmtChild(s, visitExpr, visitStmt); + }; + loop.body.every(visitStmt); + // Bound the counter so every counter operand stays a safe integer. + let bound = SAFE; + const affineGroups = [...affineChecks.values()].filter((group) => { + let limit = SAFE; + for (const operand of group.operands) { + const shape = shapes.get(operand)!; + const room = SAFE - termMagnitude(shape.terms) - Math.abs(shape.offset); + limit = Math.min(limit, Math.floor(room / Math.abs(shape.counter))); + } + if (limit < 0 || !counted) return false; + bound = Math.min(bound, limit); + return true; + }); + 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 bounds = new Set(); + const used = new Set(); + const ranges = new Map(); + const counterLow = Math.max(counted?.range.min ?? 0, -bound), + counterHigh = Math.min(counted?.range.max ?? 0, bound); + for (const group of fixedKept) { + used.add(group.check.receiver); + for (const node of group.nodes) bounds.add(node); + } + for (const group of affineKept) { + 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; + let min = Math.min(a, b) + shape.offset, + max = Math.max(a, b) + shape.offset; + for (const term of shape.terms) { + const r = facts.ranges.get(term.expr)!; + min += term.sign > 0 ? r.min : -r.max; + max += term.sign > 0 ? r.max : -r.min; + } + if (counterLow <= counterHigh) ranges.set(operand, { min, max }); + } + } + // Renumber the receivers the kept checks use. + const order = [...used].sort((a, b) => a - b); + const renumber = new Map(order.map((old, index) => [old, index])); + const needsBound = + affineKept.length > 0 && + counted !== null && + (counted.range.min < -bound || counted.range.max > bound); + return { + bounds, + receivers: order.map((index) => receivers[index]!), + fixed: fixedKept.map((g) => ({ + receiver: renumber.get(g.check.receiver)!, + end: g.check.end, + })), + affine: affineKept.map((g) => ({ + receiver: renumber.get(g.check.receiver)!, + scale: g.check.scale, + terms: g.check.terms, + minOffset: g.check.minOffset, + maxEnd: g.check.maxEnd, + })), + counted: affineKept.length > 0 ? counted : null, + counterBound: needsBound ? bound : null, + ranges, + }; +} + +/** Branches to `fast` when every access the version proves is in bounds for + * every iteration, 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, + version: BytesLoopVersion, + fast: string, + slow: string, +): void { + const B = host.B; + const next = (cond: string): void => { + if (cond === "true") return; + const ok = B.newLabel("bytes.version.check"); + B.condBr(cond, ok, slow); + B.startBlock(ok); + }; + const counted = version.counted; + if (counted?.guarded) next(emitCountedLoopSafety(host, counted)); + const receivers = version.receivers.map(({ ref, global }) => { + const value = host.emitReadReceiver(ref).name; + if (global) { + const set = B.tmp(); + B.line(`${set} = icmp ne ptr ${value}, null`); + next(set); + } + return value; + }); + const lengths = receivers.map((receiver) => { + const len = emitBytesHeaderLength(host, receiver); + if (host.sizeType === "i64") return len; + const wide = B.tmp(); + B.line(`${wide} = zext ${host.sizeType} ${len} to i64`); + return wide; + }); + const conditions: string[] = []; + for (const check of version.fixed) { + const ok = B.tmp(); + B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); + conditions.push(ok); + } + if (counted && version.affine.length > 0) { + // The first and last counter values, in ascending order. The last value + // of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ * + // step`. A loop that runs no iteration may compute anything here. + const startValue = host.emitExpr(counted.start); + const startInteger = exactInteger(host, startValue, counted.start); + let start: string; + if (startInteger) start = widenInteger(host, startInteger); + else { + // The counted-loop safety test above proved an exact integer. + start = B.tmp(); + B.line(`${start} = fptosi double ${startValue.name} to i64`); + } + const limit = emitCountedLoopLimit(host, counted); + const step = Math.abs(counted.step); + const edge = B.tmp(); + B.line(`${edge} = add i64 ${limit}, ${counted.step > 0 ? -1 : 1}`); + let last = edge; + if (step !== 1) { + const span = B.tmp(), + whole = B.tmp(), + covered = B.tmp(); + last = B.tmp(); + B.line( + counted.step > 0 + ? `${span} = sub i64 ${edge}, ${start}` + : `${span} = sub i64 ${start}, ${edge}`, + ); + B.line(`${whole} = sdiv i64 ${span}, ${step}`); + B.line(`${covered} = mul i64 ${whole}, ${step}`); + B.line(`${last} = ${counted.step > 0 ? "add" : "sub"} i64 ${start}, ${covered}`); + } + const low = counted.step > 0 ? start : last; + const high = counted.step > 0 ? last : start; + if (version.counterBound !== null) { + const above = B.tmp(), + below = B.tmp(); + B.line(`${above} = icmp sge i64 ${low}, ${-version.counterBound}`); + B.line(`${below} = icmp sle i64 ${high}, ${version.counterBound}`); + conditions.push(above, below); + } + for (const check of version.affine) { + let base = "0"; + for (const term of check.terms) { + const value = host.emitExpr(term.expr); + const integer = exactInteger(host, value, term.expr); + if (!integer) { + // Unreachable: every term has a proven range. Stay checked. + B.br(slow); + B.startBlock(B.newLabel("bytes.version.dead")); + return; + } + const wide = widenInteger(host, integer); + const sum = B.tmp(); + B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${wide}`); + base = sum; + } + // Products wrap only for a loop with no iteration: within the + // iteration space every scaled counter is a proven safe integer. + const scaledLow = scaleCounter(host, low, check.scale), + scaledHigh = scaleCounter(host, high, check.scale); + const first = B.tmp(), + final = B.tmp(), + nonNegative = B.tmp(), + below = B.tmp(), + both = B.tmp(); + B.line(`${first} = add i64 ${check.scale > 0 ? scaledLow : scaledHigh}, ${base}`); + B.line(`${final} = add i64 ${check.scale > 0 ? scaledHigh : scaledLow}, ${base}`); + const firstOffset = B.tmp(), + lastEnd = B.tmp(); + B.line(`${firstOffset} = add i64 ${first}, ${check.minOffset}`); + B.line(`${lastEnd} = add i64 ${final}, ${check.maxEnd}`); + B.line(`${nonNegative} = icmp sge i64 ${firstOffset}, 0`); + // Lengths are at most 2^53 elements: a signed comparison is exact. + B.line(`${below} = icmp slt i64 ${lastEnd}, ${lengths[check.receiver]}`); + B.line(`${both} = and i1 ${nonNegative}, ${below}`); + conditions.push(both); + } + } + let valid = "true"; + for (const cond of conditions) { + if (valid === "true") valid = cond; + else { + const all = B.tmp(); + B.line(`${all} = and i1 ${valid}, ${cond}`); + valid = all; + } + } + if (valid === "true") B.br(fast); + else B.condBr(valid, fast, slow); +} + +function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string { + if (scale === 1) return value; + const scaled = host.B.tmp(); + host.B.line(`${scaled} = mul i64 ${value}, ${scale}`); + return scaled; +} diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 03e0ed4f3..36fc63e1f 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -42,3 +42,54 @@ export function emitCountedLoopLimit( B.line(`${limit} = add nsw i64 ${integer}, ${ascending ? 1 : -1}`); return limit; } + +/** The run-time test of a guarded counted loop: an unproven limit is within + * the integer form's finite range and an unproven start is an exact safe + * integer. Returns `true` when nothing needs testing. */ +export function emitCountedLoopSafety( + host: LlvmEmitterContext, + loop: IntegerCountedForLoop, +): string { + const B = host.B; + let safe = "true"; + if (loop.limitGuarded) { + const bound = host.emitExpr(loop.limit); + safe = B.tmp(); + B.line( + `${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; + } + } + return safe; +} diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index cf7578c02..86b4f5be8 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -762,7 +762,9 @@ export class LlDyn { B.terminate(`ret i1 true`); B.startBlock(lNext); } - if (t.kind !== "dyn") { + // A union matches a capsule of another type through its arms below: a + // class capsule in a `C | undefined` field matches the `C` arm. + if (t.kind !== "dyn" && t.kind !== "union") { const view = this.viewFn(wrapped); this.host.declare(`declare void @scr_dyn_release_v(ptr)`); const kind = this.kindOf(B, "%d"); @@ -3263,8 +3265,9 @@ export class LlDyn { } B.startBlock(labels.get(DYN_KIND.BYTES)!); { - // Buffer-flavored values coerce utf8; plain Uint8Array joins - // its elements. + // Buffer-flavored values coerce utf8; typed arrays join their + // elements and a DataView answers "[object DataView]" (the + // runtime's conversion). const bufp = B.tmp(); const bufRaw = B.tmp(); const isBuf = B.tmp(); @@ -3286,10 +3289,9 @@ export class LlDyn { B.line(`call void @scr_str_release(ptr ${txt})`); B.br(done); B.startBlock(lJoin); - const bts = this.payloadOf(B, "%d", "ptr"); - host.declare(`declare ptr @scr_bytes_join(ptr, ptr)`); + host.declare(`declare ptr @scr_dyn_to_string(ptr, ptr)`); const joined = B.tmp(); - B.line(`${joined} = call ptr @scr_bytes_join(ptr ${bts}, ptr ${host.internLiteral(",")})`); + B.line(`${joined} = call ptr @scr_dyn_to_string(ptr %d, ptr null)`); this.putScrStr(B, "%b", joined); B.line(`call void @scr_str_release(ptr ${joined})`); B.br(done); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts new file mode 100644 index 000000000..9e50bea6d --- /dev/null +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -0,0 +1,1352 @@ +/* LlEmitter's function emission: function bodies, generator and async scaffolding, async awaits, WASI fulfillment and trampolines. */ +import { + type LlArgPackAndTrampolinePrologue, + type LlEmitter, + type LlScopeEntry, + SUSPENDS, + UNOPTIMIZED_BOX_SLOTS, + boxSlotCount, +} from "./emitter.js"; +import { type IrFunction, type IrType, isRefCounted, typeKey } from "../../ir/ir.js"; +import { BlockBuilder } from "./blocks.js"; +import { findPrivateSplitLocals } from "./split-loops.js"; +import { + conditionalLoopWrites, + initializerLocalCandidates, + withInitializerBindings, + withInitializerLocals, +} from "../../ir/initializer-bindings.js"; +import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; +import { constructorClass } from "../../ir/final-fields.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import { findLoopArrayBorrows } from "./loop-array-borrows.js"; +import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js"; +import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js"; +import { findMapReadLifetimes, findReadNumberMapWrites } from "./map-read-lifetimes.js"; +import { findMapEntryFusions } from "./map-entry-fusion.js"; +import { findLocalStackUnions } from "./stack-unions.js"; +import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js"; +import { findLocalUnionStorage } from "./local-union-storage.js"; +import { INT32_RANGE, analyzeIntegerRanges } from "../../ir/integer-ranges.js"; +import { findBytesBounds } from "./bytes-bounds.js"; +import { findInt32Locals } from "./int32-locals.js"; +import { findIntegerViews } from "./integer-views.js"; +import { + mangleArgPack, + mangleAsyncAdoptCall, + mangleAsyncAwaitCall, + mangleAsyncSpawn, + mangleBorrowedFunction, + mangleClassStruct, + mangleFunction, + mangleGenDrop, + mangleGenSourceClose, + mangleGenSpawn, + mangleGlobal, + mangleLocal, + mangleTrampoline, +} from "../mangle.js"; +import { InternalCompilerError } from "../../errors.js"; +import { classEnvironmentIndex } from "./classes.js"; +import { FN_ATTRS, boxNewCall, releaseSym, traceArg, vAdapters } from "./shapes.js"; +import type { LlValue } from "./expr-context.js"; +import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js"; +import { f64Lit } from "./common.js"; +import { RESULT_ENTRY_PREFIX } from "./record-results.js"; +import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; + +export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { + const B = new BlockBuilder(); + emitter.B = B; + emitter.currentFunctionName = fn.name; + // Fiber switches restore the active exception cell before control + // returns to a synchronous frame (emitPendingCheck). + B.excCellInvariant = fn.async !== true && fn.generator === undefined; + emitter.debugScope = emitter.debug?.function(fn) ?? null; + B.debugLocation = emitter.debug?.location(fn.loc, emitter.debugScope) ?? null; + emitter.frames = []; + emitter.scopes = []; + emitter.unwindCleanups.clear(); + emitter.jumpTargets = []; + emitter.currentLocals = new Map(fn.locals.map((l) => [l.id, l])); + emitter.lazyCaptureBoxes.clear(); + emitter.lazyStackBoxes.clear(); + // An unchanged captured parameter keeps its value in a plain slot; the + // binding is boxed only for closures (captureBox). + const lazyParameters = emitter.lazyCaptures.parameters(fn); + for (const id of lazyParameters) { + const { boxed: _boxed, ...plain } = emitter.currentLocals.get(id)!; + emitter.currentLocals.set(id, plain); + emitter.lazyCaptureBoxes.set(id, ""); + } + emitter.currentConstantCallbacks = + emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); + // Preserve concrete source bindings for debugger inspection. Suspended + // functions keep the established array lifetime across continuations. + emitter.streamingSplitsEnabled = emitter.debug === null && !fn.async && !fn.generator; + emitter.stackCallbacks.reset(emitter.streamingSplitsEnabled); + emitter.stackCaptures.reset(fn, emitter.streamingSplitsEnabled, lazyParameters); + emitter.privateSplitLocals = emitter.streamingSplitsEnabled + ? findPrivateSplitLocals(fn) + : new Map(); + emitter.storedSplits.clear(); + emitter.splitSpans.clear(); + emitter.stringSlices.clear(); + const initializerBindings = emitter.initializerBindings.get(fn.name) ?? []; + const numericFn = withInitializerBindings(fn, initializerBindings); + emitter.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l])); + emitter.unaliasedArrays = findUnaliasedLocalArrays(fn); + emitter.borrowedParameters.clear(); + emitter.referenceWrites?.enter(fn); + emitter.reboundOwnerSlots.clear(); + emitter.projectedParameters.clear(); + const projectedParameterIndexes = emitter.callLifetimes.parameters.get(fn.name); + if (projectedParameterIndexes) + for (const index of projectedParameterIndexes) + emitter.projectedParameters.add(fn.params[index]!.localId); + emitter.stableCallBindings = emitter.callLifetimes.bindings.get(fn.name) ?? new Set(); + const borrowedParameterIndexes = emitter.callLifetimes.borrowed.get(fn.name); + if (borrowedParameterIndexes) { + // Rebound walk parameters borrow too, but only unchanged ones may + // serve as stable owners for call arguments and aliases. + for (const index of borrowedParameterIndexes) + if ( + !emitter.walkBorrowsByFunction.get(fn.name)?.has(fn.params[index]!.localId) && + !emitter.reboundByFunction.get(fn.name)?.has(fn.params[index]!.localId) + ) + emitter.borrowedParameters.add(fn.params[index]!.localId); + } + // A generator frame owns its unchanged parameters across suspensions. + const generatorParameters = emitter.callLifetimes.generatorParameters.get(fn.name); + if (generatorParameters !== undefined) + for (const id of generatorParameters) emitter.borrowedParameters.add(id); + emitter.captureIds = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + emitter.stableCaptureIds = + fn.captures !== undefined ? emitter.lazyCaptures.stableCaptures(fn.name) : new Set(); + emitter.integerLoopBindings.clear(); + emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; + emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled; + emitter.speculativeFunction = fn.name; + emitter.speculation = null; + emitter.speculationResume = null; + emitter.finalFieldBorrows = + !fn.async && + !fn.generator && + constructorClass(fn.name) === null && + emitter.finalFields.size > 0; + emitter.byteWindowsEnabled = emitter.countedLoopsEnabled; + emitter.bytesLoopVersionsEnabled = emitter.countedLoopsEnabled; + emitter.bytesLoopEntries = null; + emitter.bytesLoopFunction = fn; + emitter.denseLoopsEnabled = emitter.countedLoopsEnabled; + emitter.denseLoop = null; + emitter.denseLoopEntries = + emitter.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") + ? findInitializedByteLoopBindings(fn) + : new Map(); + emitter.byteWindowEntries = + emitter.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") + ? findInitializedByteLoopBindings(fn) + : new Map(); + emitter.integerViews.clear(); + emitter.fieldPointerTags.clear(); + emitter.integerArrayBindings.clear(); + emitter.loopArrayBorrows = + emitter.debug === null + ? findLoopArrayBorrows( + fn, + emitter.callLifetimes, + emitter.referenceEffects, + emitter.optionalArrayReads, + emitter.referenceWrites, + emitter.fnByName, + ) + : new Set(); + emitter.localArrayReads = findLocalArrayReads( + fn, + emitter.fnByName, + emitter.unionsById, + emitter.referenceEffects.functions, + emitter.callLifetimes, + emitter.optionalArrayReads, + emitter.loopArrayBorrows, + emitter.debug === null ? emitter.referenceEffects : undefined, + ); + emitter.callArrayReads = findCallArrayReads( + fn, + emitter.optionalArrayReads, + emitter.referenceEffects.functions, + emitter.callLifetimes, + emitter.debug === null ? emitter.referenceEffects : undefined, + ); + emitter.scalarStringSlices = + emitter.debug === null ? findScalarStringSlices(fn, emitter.callLifetimes) : new Map(); + emitter.scratchStringSlices = + emitter.debug === null + ? findScratchStringSlices( + fn, + emitter.callLifetimes, + emitter.scalarStringSlices, + emitter.lentStringParameters, + ) + : new Set(); + emitter.mapReadLifetimes = findMapReadLifetimes( + fn, + emitter.boxedUnionsById, + emitter.callLifetimes, + ); + emitter.readNumberMapWrites = findReadNumberMapWrites(fn); + emitter.mapEntryFusion = emitter.debug === null ? findMapEntryFusions(fn) : null; + emitter.localStackUnions = findLocalStackUnions( + fn, + emitter.callLifetimes, + emitter.boxedUnionsById, + ); + emitter.localStackObjects = emitter.stackObjects + ? findLocalStackObjects(emitter, fn, emitter.callLifetimes) + : new Map(); + emitter.reassignedStackObjects = emitter.stackObjects + ? findReassignedStackObjects(emitter, fn, emitter.callLifetimes) + : new Set(); + emitter.stackObjectSlots = new Map(); + emitter.stackObjectFieldPtrs = new Map(); + emitter.localUnionStorageProofs = findLocalUnionStorage( + fn, + emitter.callLifetimes, + emitter.unionSources, + ); + emitter.localUnionStorage = new Map(); + emitter.walkBorrows = emitter.walkBorrowsByFunction.get(fn.name) ?? new Set(); + const slotFacts = emitter.int32Slots.facts(fn.name); + emitter.integerRanges = analyzeIntegerRanges(numericFn, slotFacts); + emitter.int32Elements = slotFacts.elements ?? null; + emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); + // 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 + // such a parameter (copyStableCaptures): the source already holds the + // double, which an i32 copy would convert and widen again on every call. + // Initializer bindings proven int32 move into an i32 local of their + // initializer: no other code observes the global, so the loop-carried + // value never round-trips through the double slot. + const initializerLocals = initializerLocalCandidates(initializerBindings); + const int32Locals = + emitter.debug === null + ? findInt32Locals( + withInitializerLocals(fn, initializerLocals), + emitter.integerRanges, + emitter.int32Slots.facts(fn.name), + new Set([...emitter.lazyCaptureBoxes.keys(), ...emitter.doubleCopies(fn)]), + ) + : null; + emitter.int32Locals = int32Locals?.locals ?? new Map(); + emitter.int32LocalWrites = int32Locals?.values ?? new Set(); + const localizedGlobals = new Set(); + for (const global of initializerLocals) { + if (!emitter.int32Locals.has(global.id)) continue; + localizedGlobals.add(global.id); + emitter.currentLocals.set(global.id, { + id: global.id, + name: global.name, + type: global.type, + mutable: global.mutable, + }); + } + emitter.chainSlots.clear(); + emitter.finallyStack = []; + emitter.tryStack = []; + emitter.loopDepth = 0; + emitter.currentReturnType = fn.returnType; + const int32Abi = emitter.int32Abis.get(fn.name); + emitter.currentInt32Result = int32Abi?.result === true; + emitter.currentUnionResult = int32Abi?.union === true; + emitter.currentGenerator = fn.generator ?? null; + emitter.currentAsyncGenerator = fn.async === true && fn.generator !== undefined; + emitter.currentBorrowedReturn = emitter.borrowedReturns.has(fn.name); + emitter.lastUseMoves.clear(); + emitter.varRefCounts = null; + emitter.currentFunctionBody = fn.body; + emitter.inConstructor = emitter.constructorFunctions.has(fn.name); + emitter.currentCoro = null; + emitter.logArgSlots = 0; + + if (emitter.coroLowered(fn)) { + emitter.usesCoroutines = true; + emitter.declare(`declare token @llvm.coro.id(i32, ptr, ptr, ptr)`); + emitter.declare(`declare ${emitter.sizeType} @llvm.coro.size.${emitter.sizeType}()`); + emitter.declare(`declare ptr @llvm.coro.begin(token, ptr)`); + emitter.declare(`declare token @llvm.coro.save(ptr)`); + emitter.declare(`declare i8 @llvm.coro.suspend(token, i1)`); + emitter.declare(`declare ptr @llvm.coro.free(token, ptr)`); + emitter.declare(`declare void @llvm.coro.end(ptr, i1, token)`); + // Native frames come from the runtime's small-block allocator. + const frameAlloc = emitter.wasi ? "malloc" : "scr_rt_calloc"; + emitter.declare(`declare ptr @${frameAlloc}(${emitter.sizeType})`); + emitter.declare(`declare void @${emitter.wasi ? "free" : "scr_rt_free"}(ptr)`); + const started = emitter.wasi ? "scr_wasi_coro_started" : "scr_coro_started"; + emitter.declare(`declare void @${started}(ptr)`); + emitter.declare(`declare ptr @scr_fiber_self()`); + const id = B.tmp(); + const size = B.tmp(); + const mem = B.tmp(); + const handle = B.tmp(); + const self = B.tmp(); + B.line(`${id} = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)`); + B.line(`${size} = call ${emitter.sizeType} @llvm.coro.size.${emitter.sizeType}()`); + B.line(`${mem} = call ptr @${frameAlloc}(${emitter.sizeType} ${size})`); + B.line(`${handle} = call ptr @llvm.coro.begin(token ${id}, ptr ${mem})`); + B.line(`call void @${started}(ptr ${handle})`); + B.line(`${self} = call ptr @scr_fiber_self()`); + // A native synchronous body owns its generator for the whole run (the + // ScrGen owns the frame), so its OUT slot address is fixed. + let out: string | null = null; + const yieldKind = fn.generator?.yieldT.kind; + if ( + !emitter.wasi && + fn.async !== true && + (yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool") + ) { + emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`); + const gen = B.tmp(); + B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`); + out = emitter.genOutSlot(gen); + } + emitter.currentCoro = { + kind: fn.generator !== undefined ? "generator" : "async", + id, + handle, + self, + finalLabel: B.newLabel("coro.final"), + cleanupLabel: B.newLabel("coro.cleanup"), + suspendLabel: B.newLabel("coro.suspend"), + out, + }; + } + + const paramIds = new Set(fn.params.map((p) => p.localId)); + if (emitter.debug === null) { + for (const id of findIntegerViews(numericFn)) { + // An i32 slot is its own view. + if (emitter.int32Locals.has(id)) continue; + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i32 ; integer view ${emitter.numericLocals.get(id)!.name}`, + ); + emitter.integerViews.set(id, slot); + } + } + for (const global of initializerBindings) { + if (!emitter.integerViews.has(global.id)) continue; + const value = B.tmp(); + B.line(`${value} = load double, ptr @${mangleGlobal(global.id)}`); + emitter.storeIntegerView(global.id, { name: value, type: global.type }); + } + // A number global only its guarded, synchronous initializer reads or + // writes (findInitializerBindings) is invisible to every other function, + // and reentry returns before reaching it, so a local slot can stand in + // for it: LLVM keeps it in a register across calls and loops that would + // otherwise force a store and reload per iteration. The global's + // storage is never read again, so nothing writes the slot back. Bindings + // proven int32 already live in an i32 local (localizedGlobals above); + // this covers the remaining doubles. + emitter.privateGlobalSlots.clear(); + if (emitter.debug === null && emitter.currentCoro === null && !fn.async && !fn.generator) { + const conditional = initializerLocals.length ? conditionalLoopWrites(fn.body) : null; + for (const global of initializerLocals) { + if (localizedGlobals.has(global.id)) continue; + // A conditional update in a loop stays a memory update (see + // conditionalLoopWrites). + if (conditional?.has(global.id)) continue; + const slot = B.slot(); + const value = B.tmp(); + B.entryAllocas.push(`${slot} = alloca double ; module binding ${global.name}`); + B.line(`${value} = load double, ptr @${mangleGlobal(global.id)}`); + B.line(`store double ${value}, ptr ${slot}`); + emitter.privateGlobalSlots.set(global.id, slot); + } + } + for (const local of emitter.currentLocals.values()) { + // Boxed locals' slots hold their capture BOX (a ptr); captured + // (env-borrowed) locals bind the incoming box below. A caught-typed + // local is a catch binding: its slot holds the ScrCaught snapshot + // box the catch prologue takes (scr_exc_take). + const slotTy = + local.boxed || emitter.captureIds.has(local.id) || local.type.kind === "caught" + ? "ptr" + : emitter.int32Locals.has(local.id) + ? "i32" + : emitter.llType(local.type); + B.entryAllocas.push(`%${mangleLocal(local.id)} = alloca ${slotTy} ; ${local.name}`); + emitter.emitDebugLocal( + local, + `%${mangleLocal(local.id)}`, + fn.params.findIndex((p) => p.localId === local.id) + 1, + emitter.captureIds.has(local.id), + ); + // A localized initializer binding starts at its global's zero value. + if (localizedGlobals.has(local.id)) B.line(`store i32 0, ptr %${mangleLocal(local.id)}`); + // Refcounted/boxed locals start NULL (the C prologue's `= NULL`): + // scope-exit releases run whether or not an assign ever did. + if (paramIds.has(local.id) || emitter.captureIds.has(local.id)) continue; + if (local.boxed || isRefCounted(local.type)) { + B.line(`store ptr null, ptr %${mangleLocal(local.id)}`); + } + } + // A body holding thousands of capture boxes compiles unoptimized, its + // box pointers kept in their slots (see boxSlotCount). + const unoptimized = emitter.currentCoro === null && boxSlotCount(fn) >= UNOPTIMIZED_BOX_SLOTS; + if (unoptimized) { + const slots = new Set(); + for (const local of emitter.currentLocals.values()) + if (local.boxed || emitter.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`); + B.volatileSlots = slots; + // Without optimization a versioned copy only adds code. + emitter.bytesLoopVersionsEnabled = false; + } + // Captured bindings come in through the environment — borrowed for the + // whole call (the closure owns them): bound here, never released here. + (fn.captures ?? []).forEach((c, i) => { + const caps = B.tmp(); + const p = B.tmp(); + const box = B.tmp(); + B.line(`${caps} = getelementptr inbounds %ScrClosure, ptr %sc_env, i64 1 ; caps`); + B.line(`${p} = getelementptr inbounds ptr, ptr ${caps}, ${emitter.sizeType} ${i} ; caps[${i}]`); + B.line(`${box} = load ptr, ptr ${p}`); + B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; captured ${c.name}`); + }); + (fn.classCaptures ?? []).forEach((c) => { + const self = fn.params[0]!; + if (self.type.kind !== "object") + throw new InternalCompilerError("class captures require an instance receiver"); + const classSlot = B.tmp(); + const classValue = B.tmp(); + const caps = B.tmp(); + const slot = B.tmp(); + const box = B.tmp(); + B.line( + `${classSlot} = getelementptr inbounds %${mangleClassStruct(self.type.className)}, ptr %p_${mangleLocal(self.localId)}, i64 0, i32 ${classEnvironmentIndex(emitter.classMeta.get(self.type.className)!)}`, + ); + B.line(`${classValue} = load ptr, ptr ${classSlot}`); + B.line(`${caps} = getelementptr inbounds %ScrClassObj, ptr ${classValue}, i64 1`); + B.line(`${slot} = getelementptr inbounds ptr, ptr ${caps}, ${emitter.sizeType} ${c.slot}`); + B.line(`${box} = load ptr, ptr ${slot}`); + B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; class capture ${c.name}`); + }); + // Params spill into their slots; the function scope owns refcounted + // params (callees own their params — callers passed +1). Boxed params + // allocate the shared binding and move the raw value in. + const fnScope: LlScopeEntry[] = []; + for (const id of emitter.lazyCaptureBoxes.keys()) { + const cache = B.slot(); + B.entryAllocas.push(`${cache} = alloca ptr ; lazy box ${emitter.currentLocals.get(id)!.name}`); + B.line(`store ptr null, ptr ${cache}`); + fnScope.push({ + slot: cache, + type: emitter.currentLocals.get(id)!.type, + boxed: true, + lazy: true, + }); + emitter.lazyCaptureBoxes.set(id, cache); + // Environments that finish during the call can share an immortal + // frame box. Its payload owner is released with the function scope. + const stack = emitter.stackCaptures.emit(emitter, id, emitter.currentLocals.get(id)!.type); + if (stack) { + emitter.lazyStackBoxes.set(id, stack.box); + if (stack.owner) fnScope.push(stack.owner); + } + } + const borrowed = emitter.callLifetimes.borrowed.get(fn.name); + for (const [index, p] of fn.params.entries()) { + const local = emitter.currentLocals.get(p.localId)!; + const slot = `%${mangleLocal(p.localId)}`; + if (local.boxed) { + const box = B.tmp(); + B.line(`${box} = ${boxNewCall(emitter.shapeHost, p.type)} ; ${p.name} (boxed param)`); + emitter.boxSet(box, p.type, `%p_${mangleLocal(p.localId)}`); + B.line(`store ptr ${box}, ptr ${slot}`); + fnScope.push({ slot, type: p.type, boxed: true }); + continue; + } + const incoming: LlValue = { name: `%p_${mangleLocal(p.localId)}`, type: p.type }; + if (int32Abi?.params[index]) { + const bits = `%p_${mangleLocal(p.localId)}${INT32_ABI_SUFFIX}`; + incoming.uint32 = bits; + incoming.integer = { name: bits, type: "i32", signed: true, range: INT32_RANGE }; + } + if (emitter.storeInt32Local(p.localId, incoming)) continue; + B.line(`store ${emitter.llType(p.type)} %p_${mangleLocal(p.localId)}, ptr ${slot}`); + emitter.storeIntegerView(p.localId, incoming); + if (isRefCounted(p.type) && !borrowed?.has(index)) { + fnScope.push({ slot, type: p.type }); + emitter.noteParameterMove(fn, p.localId, slot); + } + if (emitter.reboundByFunction.get(fn.name)?.has(p.localId) && borrowed?.has(index)) { + // The caller's borrow stays in the parameter slot; values the body + // assigns are owned here until rebinding or exit releases them. + const owner = `${slot}.owner`; + B.entryAllocas.push(`${owner} = alloca ptr`); + B.line(`store ptr null, ptr ${owner}`); + fnScope.push({ slot: owner, type: p.type }); + emitter.reboundOwnerSlots.set(p.localId, owner); + } + } + emitter.scopes.push(fnScope); + // Stackful native fibers and native coroutine frames retain these frames + // while suspended. The active exception context owns the chain, so + // concurrent fibers isolate it. + if ( + emitter.stackTraces && + !emitter.wasi && + fn.sourceName !== undefined && + !emitter.unframedFunctions.has(fn.name) + ) { + B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`); + const frame = emitter.cstr(` at ${fn.sourceName} (${fn.loc.file})`); + if ( + B.excCellInvariant && + emitter.mod.lib === undefined && + emitter.mod.workers !== true && + emitter.sizeType === "i64" + ) { + // Executables link the active-cell pointer as a plain global, and a + // synchronous body sees one cell from entry to every return: push + // and pop the frame inline (ScrExcCell.stack is at byte 56 on + // 64-bit targets) instead of calling the runtime twice per call. + emitter.declare(`@scr_exc_active = external global ptr`); + B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`); + B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`); + B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`); + B.line(`store ptr %source_frame.previous, ptr %source_frame`); + B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`); + B.line(`store ptr ${frame}, ptr %source_frame.text`); + B.line(`store ptr %source_frame, ptr %source_frame.chain`); + B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`; + } else { + emitter.declare(`declare void @scr_stack_enter(ptr, ptr)`); + emitter.declare(`declare void @scr_stack_leave(ptr)`); + B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`); + B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`; + } + } + if (emitter.stackChecks?.has(fn.name)) emitter.emitStackCheck(); + if (emitter.workerEntryPolls?.has(fn.name)) emitter.emitPendingCheck(undefined, true); + emitter.emitStmts(fn.body, true); + // Implicit exit of a void function: release the function scope unless + // the body already terminated its final block (return, or a throw + // whose unwind released everything down to depth 0). + if (fn.returnType.kind === "void" && !B.isTerminated()) { + emitter.releaseScope(emitter.scopes[0]!); + // Completing by falling off the end has nothing to await. + if (emitter.currentAsyncGenerator) emitter.emitAsyncGenSettled(); + if (emitter.currentCoro !== null) { + emitter.emitWasiFulfill(null); + B.terminate(`br label %${emitter.currentCoro.finalLabel}`); + } else { + B.terminate("ret void"); + } + } + emitter.scopes.pop(); + + const coro = emitter.currentCoro; + if (coro !== null) { + B.startBlock(coro.finalLabel); + // Every exit of a coroutine body joins here: its source frame (part + // of the coroutine frame) leaves the generator's own chain. + const epilogue = B.returnEpilogue; + B.returnEpilogue = null; + if (epilogue !== null) B.line(epilogue); + if (fn.captures !== undefined) { + emitter.declare(`declare void @scr_closure_release(ptr)`); + B.line(`call void @scr_closure_release(ptr %sc_env)`); + } + if (coro.kind === "generator") { + const finish = emitter.wasi ? "scr_wasi_gen_finish" : "scr_gen_coro_finish"; + emitter.declare(`declare void @${finish}(ptr)`); + B.line(`call void @${finish}(ptr ${coro.self})`); + } else { + const finish = emitter.wasi ? "scr_wasi_async_finish" : "scr_coro_async_finish"; + emitter.declare(`declare void @${finish}(ptr)`); + B.line(`call void @${finish}(ptr ${coro.self})`); + } + const finalState = B.tmp(); + B.line(`${finalState} = call i8 @llvm.coro.suspend(token none, i1 true)`); + B.terminate( + `switch i8 ${finalState}, label %${coro.suspendLabel} [ i8 1, label %${coro.cleanupLabel} ]`, + ); + B.startBlock(coro.cleanupLabel); + const frame = B.tmp(); + B.line(`${frame} = call ptr @llvm.coro.free(token ${coro.id}, ptr ${coro.handle})`); + B.line(`call void @${emitter.wasi ? "free" : "scr_rt_free"}(ptr ${frame})`); + B.br(coro.suspendLabel); + B.startBlock(coro.suspendLabel); + B.line(`call void @llvm.coro.end(ptr ${coro.handle}, i1 false, token none)`); + const ret = emitter.llType(fn.returnType); + if (ret === "void") B.terminate(`ret void`); + else if (ret === "double") B.terminate(`ret double ${f64Lit(0)}`); + else if (ret === "i1") B.terminate(`ret i1 false`); + else B.terminate(`ret ptr null`); + } + + // Shared blocks have multiple source locations. Keep the location on + // each incoming exception check instead of assigning one to the cleanup. + B.debugLocation = null; + for (const cleanup of emitter.unwindCleanups.values()) { + B.startBlock(cleanup.label); + emitter.releaseScope(cleanup.entries); + B.terminate(cleanup.terminator); + } + + if (emitter.logArgSlots > 0) { + B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`); + } + const params = fn.params.map((p, index) => + int32Abi?.params[index] + ? `i32 %p_${mangleLocal(p.localId)}${INT32_ABI_SUFFIX}` + : `${emitter.llType(p.type)} %p_${mangleLocal(p.localId)}`, + ); + // The i32 entry widens its i32 parameters once; the proof makes the + // double exact, so readers of the incoming number see the same value. + if (int32Abi) + fn.params.forEach((p, index) => { + if (!int32Abi.params[index]) return; + const name = `%p_${mangleLocal(p.localId)}`; + B.entryAllocas.push(`${name} = sitofp i32 ${name}${INT32_ABI_SUFFIX} to double`); + }); + // Lifted functions receive their closure first (the callValue ABI). + if (fn.captures !== undefined) params.unshift("ptr %sc_env"); + const ret = int32Abi?.result + ? "i32" + : int32Abi?.union + ? SCALAR_UNION_RESULT + : emitter.llType(fn.returnType); + let attrs = coro !== null ? "#1" : FN_ATTRS; + // The shared indexed-read helper (lowerSafeIndexRead) is a dense bounds + // test, a state test and a load behind cold runtime fallbacks, called + // from every `a[i]` of one element type. Without the hint LLVM keeps the + // hot instances out of line once they have many callers: every read pays + // a call, and an integer index converts to a double and back across it. + if (fn.name.startsWith("%arr.idxOr.")) attrs = `inlinehint ${attrs}`; + // Interface dispatch is a few key tests and direct calls ahead of the + // checked dynamic call. Keeping that fallback out of line leaves the + // tests small enough to inline at each call site. + if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`; + else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`; + if (fn.name.startsWith(RESULT_ENTRY_PREFIX)) attrs = `inlinehint ${attrs}`; + if (unoptimized && attrs === FN_ATTRS) attrs = `noinline optnone ${attrs}`; + const debug = emitter.debugScope === null ? "" : ` !dbg ${emitter.debugScope}`; + const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name); + emitter.privateGlobalSlots.clear(); + let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`; + if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi); + if (emitter.integerClones.members.has(fn.name)) { + body += "\n" + emitIntegerClone(fn, emitter, emitter.stackChecks?.has(fn.name) === true); + if (emitter.integerClones.dispatched.has(fn.name)) + body += + "\n" + + emitIntegerDispatcher( + fn, + emitter, + `${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}`, + int32Abi?.params ?? [], + int32Abi?.result === true, + ); + } + if (!borrowed) return body; + const slot = emitter.implSlotBorrowed(fn.name); + const virtual = + slot.size > 0 && slot.size !== borrowed.size + ? "\n" + emitter.emitOwnedCallAdapter(fn, borrowed, slot) + : ""; + return body + "\n" + emitter.emitOwnedCallAdapter(fn, borrowed) + virtual; +} + +/** Per-generator-function machinery — the async scaffolding's lazy + * sibling (async.ts's emitGenScaffolding): the same argument pack, + * a fiber trampoline whose epilogue stores the COMPLETION value (or + * consumes the GENRET sentinel, promoting the parked .return value), a + * spawn wrapper that only ALLOCATES the suspended fiber, and the + * never-started teardown that drops the packed (+1) arguments. */ +export function emitGenScaffolding(emitter: LlEmitter): string[] { + const out: string[] = []; + for (const fn of emitter.mod.functions) { + if (fn.generator === undefined) continue; + const { definitions, pack, lifted, fieldTys, ret, tr, spawnParams, argPackLines } = + emitter.emitArgPackAndTrampolinePrologue(fn); + for (const line of definitions) out.push(line); + emitter.declare(`declare zeroext i1 @scr_exc_genret_pending()`); + emitter.declare(`declare void @scr_exc_clear()`); + emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`); + emitter.declare(`declare void @scr_gen_ret_to_out(ptr)`); + emitter.declare(`declare ptr @scr_gen_new(ptr, ptr, ptr)`); + emitter.needOom(); + const coroBody = emitter.coroLowered(fn); + if (lifted && !coroBody) { + emitter.declare(`declare void @scr_closure_release(ptr)`); + } + // Normal completion stores the (typed) return value; void completes + // with the NONE slot — JS's undefined done-value. A GENRET unwind + // consumes the sentinel and promotes the parked .return value; a + // real exception stays pending (the consumer-side resume moves it). + if (coroBody) { + tr.push(` ret void`, `}`, ``); + } else { + tr.push( + ` %g = call ptr @scr_gen_of_fiber(ptr %self)`, + ...emitter.pendingTestLines("%pend"), + ` br i1 %pend, label %thrown, label %clean`, + `clean:`, + ); + switch (ret.kind) { + case "void": + tr.push(` br label %done ; void body: the done value is undefined (NONE)`); + break; + case "f64": + case "date": + emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); + tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`); + break; + case "bool": + emitter.declare(`declare void @scr_gen_out_bool(ptr, i1 zeroext)`); + tr.push(` call void @scr_gen_out_bool(ptr %g, i1 %r)`, ` br label %done`); + break; + default: { + const v = vAdapters(emitter.shapeHost, ret); + emitter.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`); + tr.push( + ` call void @scr_gen_out_ref(ptr %g, ptr %r, ptr ${v.release})`, + ` br label %done`, + ); + } + } + tr.push( + `thrown:`, + ` %genret = call zeroext i1 @scr_exc_genret_pending()`, + ` br i1 %genret, label %promote, label %dropdummy`, + `promote:`, + ` call void @scr_exc_clear()`, + ` call void @scr_gen_ret_to_out(ptr %g)`, + ` br label %dropdummy`, + `dropdummy:`, + ); + if (ret.kind !== "void" && isRefCounted(ret)) { + tr.push( + ` call void ${releaseSym(emitter.shapeHost, ret)}(ptr %r) ; unwound: the never-read dummy`, + ); + } + tr.push(` br label %done`, `done:`, ` ret void`, `}`, ``); + } + for (const line of tr) out.push(line); + + let settleAsync: string | null = null; + if (fn.async) { + emitter.declare(`declare ptr @scr_async_gen_new(ptr, ptr, ptr, ptr)`); + const genT: IrType & { kind: "generator" } = { + kind: "generator", + async: true, + yieldT: fn.generator.yieldT, + retT: ret, + nextT: fn.generator.nextT, + }; + const resultT = fn.generator.resultType; + const build = emitter.genResultThunkFor(genT, resultT); + settleAsync = `${build}_async`; + const settleKey = `ags:${typeKey(genT)}`; + if (!emitter.resolveThunks.has(settleKey)) { + emitter.resolveThunks.set(settleKey, settleAsync); + const adapters = vAdapters(emitter.shapeHost, resultT); + emitter.resolveThunkDefs.push( + `define internal void @${settleAsync}(ptr %g, ptr %p) ${FN_ATTRS} {`, + `entry:`, + ` %r = call ptr @${build}(ptr %g)`, + ` call void @scr_promise_fulfill_ref(ptr %p, ptr %r, ptr ${adapters.retain}, ptr ${adapters.release}, ptr ${traceArg(emitter.shapeHost, resultT)})`, + ` ret void`, + `}`, + ``, + ); + } + emitter.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + } + + // The never-started teardown: drop the packed (+1) arguments. + const dr: string[] = [ + `define internal void @${mangleGenDrop(fn.name)}(ptr %ap) ${FN_ATTRS} {`, + `entry:`, + ]; + fieldTys.forEach((ty, i) => { + const pType = lifted && i === 0 ? null : fn.params[lifted ? i - 1 : i]!.type; + const refcounted = pType === null || isRefCounted(pType); + if (!refcounted) return; + dr.push( + ` %dp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`, + ` %dv${i} = load ptr, ptr %dp${i}`, + ); + if (pType === null) { + emitter.declare(`declare void @scr_closure_release(ptr)`); + dr.push(` call void @scr_closure_release(ptr %dv${i})`); + } else { + dr.push(` call void ${releaseSym(emitter.shapeHost, pType)}(ptr %dv${i})`); + } + }); + dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``); + for (const line of dr) out.push(line); + + // An iterator helper's return() before its first next() closes the + // source generator in its first argument slot. + const closesSource = fn.generator.closesSource === true; + if (fn.generator.tag !== undefined) emitter.declare(`declare void @scr_gen_set_tag(ptr, ptr)`); + if (fn.generator.inheritsTag === true) + emitter.declare(`declare void @scr_gen_copy_tag(ptr, ptr)`); + if (closesSource) { + const field = lifted ? 1 : 0; + emitter.declare(`declare void @scr_gen_close(ptr)`); + emitter.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`); + out.push( + `define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`, + `entry:`, + ` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`, + ` %sv = load ptr, ptr %sp`, + ` call void @scr_gen_close(ptr %sv)`, + ` ret void`, + `}`, + ``, + ); + } + + // Spawn wrapper: pack the args (+1 moves in), allocate the + // SUSPENDED fiber — nothing runs until the first .next(). + if (lifted) { + emitter.declare(`declare ptr @scr_closure_retain_v(ptr)`); + } + const sp: string[] = [ + `define internal ptr @${mangleGenSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; gen spawn ${fn.name}`, + `entry:`, + ...argPackLines, + ]; + const genNew = coroBody && !emitter.wasi ? "scr_gen_new_stackless" : "scr_gen_new"; + if (genNew !== "scr_gen_new") emitter.declare(`declare ptr @${genNew}(ptr, ptr, ptr)`); + sp.push( + settleAsync === null + ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` + : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, + ...(fn.generator.tag !== undefined + ? [` call void @scr_gen_set_tag(ptr %gg, ptr ${emitter.cstr(fn.generator.tag)})`] + : []), + ...(fn.generator.inheritsTag === true + ? [` call void @scr_gen_copy_tag(ptr %gg, ptr %a${lifted ? 1 : 0})`] + : []), + ...(closesSource + ? [ + ` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`, + ] + : []), + ` ret ptr %gg`, + `}`, + ``, + ); + for (const line of sp) out.push(line); + } + return out; +} + +/** Per-async-function machinery — async.ts's scaffolding, .ll + * flavored: an argument-pack struct type, a fiber trampoline (unpacks, + * frees the pack, runs the ordinary compiled body, settles the + * promise — fulfilling on clean return, leaving a pending exception + * for the runtime to reject with), and a spawn wrapper call sites and + * closures enter through (packs the args +1, scr_async_spawn runs the + * fiber eagerly to its first suspension and returns the promise). */ +export function emitAsyncScaffolding(emitter: LlEmitter): string[] { + const out: string[] = []; + if (emitter.wasi) { + emitter.declare(`declare void @llvm.coro.resume(ptr)`); + emitter.declare(`declare void @llvm.coro.destroy(ptr)`); + out.push( + `define void @scr_wasi_coro_resume(ptr %handle) ${FN_ATTRS} {`, + `entry:`, + ` call void @llvm.coro.resume(ptr %handle)`, + ` ret void`, + `}`, + `define void @scr_wasi_coro_destroy(ptr %handle) ${FN_ATTRS} {`, + `entry:`, + ` call void @llvm.coro.destroy(ptr %handle)`, + ` ret void`, + `}`, + ``, + ); + } + const fiberless = emitter.fiberlessAsyncFunctions(); + for (const fn of emitter.mod.functions) { + if (fn.async !== true || fn.generator !== undefined) continue; + if (fiberless.has(fn.name) && emitter.awaitCallees.has(fn.name)) { + for (const line of emitter.emitAsyncAwaitCall(fn, null)) out.push(line); + } + if (fiberless.has(fn.name) && emitter.adoptCallees.has(fn.name)) { + for (const line of emitter.emitAsyncAwaitCall(fn, null, true)) out.push(line); + } + if (emitter.inlineAwaitCallees.has(fn.name)) { + for (const line of emitter.emitAsyncInlineAwaitCall(fn)) out.push(line); + } + if (fiberless.has(fn.name)) { + const fieldTys = [ + ...(fn.captures !== undefined ? ["ptr"] : []), + ...fn.params.map((p) => emitter.llType(p.type)), + ]; + const params = fieldTys.map((ty, i) => `${ty} %a${i}`); + for (const line of emitter.emitFiberlessAsyncSpawn(fn, params)) out.push(line); + continue; + } + const { definitions, ret, tr, spawnParams, argPackLines } = + emitter.emitArgPackAndTrampolinePrologue(fn); + for (const line of definitions) out.push(line); + if (emitter.awaitCallees.has(fn.name)) { + for (const line of emitter.emitAsyncAwaitCall(fn, argPackLines)) out.push(line); + } + if (emitter.adoptCallees.has(fn.name)) { + for (const line of emitter.emitAsyncAwaitCall(fn, argPackLines, true)) out.push(line); + } + const stackless = emitter.stacklessAsync.has(fn.name); + const spawn = stackless ? "scr_async_spawn_stackless" : "scr_async_spawn"; + emitter.declare(`declare ptr @scr_fiber_promise(ptr)`); + emitter.declare(`declare ptr @${spawn}(ptr, ptr)`); + emitter.needOom(); + if (emitter.wasi || stackless) { + // The coroutine body settles its own promise and owns the retained + // lifted environment until final suspension. Its initial call + // returns here at the first suspend (or final suspend). + tr.push(` ret void`, `}`, ``); + } else { + if (fn.captures !== undefined) { + emitter.declare(`declare void @scr_closure_release(ptr)`); + } + tr.push( + ...emitter.pendingTestLines("%pend"), + ` br i1 %pend, label %thrown, label %clean`, + `clean:`, + ` %pr = call ptr @scr_fiber_promise(ptr %self)`, + ); + for (const line of emitter.asyncFulfillLines(ret, "%pr")) tr.push(line); + tr.push(` ret void`, `thrown:`); + if (ret.kind !== "void" && isRefCounted(ret)) { + // An escaping throw means %r is the never-read dummy (NULL). + tr.push(` call void ${releaseSym(emitter.shapeHost, ret)}(ptr %r)`); + } + tr.push(` ret void`, `}`, ``); + } + for (const line of tr) out.push(line); + + // Spawn wrapper: pack the args (+1 moves in), spawn the fiber. + const cache = fn.asyncCacheGlobal !== undefined ? mangleGlobal(fn.asyncCacheGlobal) : null; + const cycleCache = + fn.asyncCycleCacheGlobal !== undefined ? mangleGlobal(fn.asyncCycleCacheGlobal) : null; + if (cache !== null || cycleCache !== null) { + emitter.declare(`declare ptr @scr_promise_retain_v(ptr)`); + emitter.declare(`declare void @scr_promise_release(ptr)`); + } + if (cache !== null) { + emitter.declare(`declare void @scr_promise_mark_handled(ptr)`); + } + if (fn.captures !== undefined) { + emitter.declare(`declare ptr @scr_closure_retain_v(ptr)`); + } + const sp: string[] = [ + `define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; spawn ${fn.name}`, + `entry:`, + ...(cache !== null + ? [ + ` %cached = load ptr, ptr @${cache}`, + ` %cache_hit = icmp ne ptr %cached, null`, + ` br i1 %cache_hit, label %cached_return, label %cache_miss`, + `cached_return:`, + ` %cached_owned = call ptr @scr_promise_retain_v(ptr %cached)`, + ` ret ptr %cached_owned`, + `cache_miss:`, + ] + : []), + ...argPackLines, + ]; + sp.push( + ` %p = call ptr @${spawn}(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`, + ...(cache !== null + ? [ + // The module loader owns this evaluation promise + // immediately. A later sibling can throw before the + // aggregate dependency wait is built, but this rejection + // must never become an unrelated unhandled rejection. + ` call void @scr_promise_mark_handled(ptr %p)`, + ] + : []), + ...(cache !== null + ? [ + ` %cache_owned = call ptr @scr_promise_retain_v(ptr %p)`, + // The eager spawn may have re-entered this guarded module + // through an admitted async cycle and installed a temporary + // cache entry. Drop that owned slot before replacing it + // with the outer evaluation promise. + ` %replaced_cache = load ptr, ptr @${cache}`, + ` call void @scr_promise_release(ptr %replaced_cache)`, + ` store ptr %cache_owned, ptr @${cache}`, + ] + : []), + ...(cycleCache !== null + ? [ + // Eager recursive spawns publish from the inside out. The + // runtime-requested outermost member writes last and is the + // SCC's actual evaluation root. + ` %cycle_cache_owned = call ptr @scr_promise_retain_v(ptr %p)`, + ` %replaced_cycle_cache = load ptr, ptr @${cycleCache}`, + ` call void @scr_promise_release(ptr %replaced_cycle_cache)`, + ` store ptr %cycle_cache_owned, ptr @${cycleCache}`, + ] + : []), + ` ret ptr %p`, + `}`, + ``, + ); + for (const line of sp) out.push(line); + } + for (const line of emitter.emitGenScaffolding()) out.push(line); + return out; +} + +/** The await-call entry of an async function (`sc_aw_*`): one direct + * `await f(...)` as a call returning the awaited value, so the call site + * needs no promise of its own. A fiberless body runs on the caller's + * stack as in its spawn wrapper; a clean result then takes the await's + * one microtask turn (scr_await_hop) and is returned without ever + * materializing the promise, which nothing else could observe. A throw + * builds the rejected promise exactly as the spawn wrapper would and + * awaits it, so the rejection reaches the awaiter on the same turn. Other + * bodies pack their arguments as the spawn wrapper does and enter + * scr_async_spawn_await_*, which parks the caller on the promise as soon + * as the body first suspends. The adopting entry (`sc_ad_*`, for + * `return f(...)` in an async function) adds the turn of the then job + * that adopts the returned promise, before the await proper. */ +export function emitAsyncAwaitCall( + emitter: LlEmitter, + fn: IrFunction, + argPackLines: string[] | null, + adopt: boolean, +): string[] { + const ret = fn.returnType; + const retTy = emitter.llType(ret); + const params = fn.params.map((p, i) => `${emitter.llType(p.type)} %a${i}`); + const awaitLine = (dest: string, promise: string): string => { + switch (ret.kind) { + case "void": + emitter.declare(`declare void @scr_await_void(ptr)`); + return ` call void @scr_await_void(ptr ${promise})`; + case "f64": + case "date": + emitter.declare(`declare double @scr_await_f64(ptr)`); + return ` ${dest} = call double @scr_await_f64(ptr ${promise})`; + case "bool": + emitter.declare(`declare zeroext i1 @scr_await_bool(ptr)`); + return ` ${dest} = call zeroext i1 @scr_await_bool(ptr ${promise})`; + case "string": + emitter.declare(`declare ptr @scr_await_str(ptr)`); + return ` ${dest} = call ptr @scr_await_str(ptr ${promise})`; + default: + emitter.declare(`declare ptr @scr_await_ref(ptr)`); + return ` ${dest} = call ptr @scr_await_ref(ptr ${promise})`; + } + }; + const retLine = (v: string): string => (retTy === "void" ? ` ret void` : ` ret ${retTy} ${v}`); + const head = adopt + ? `define internal ${retTy} @${mangleAsyncAdoptCall(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; adopt ${fn.name}` + : `define internal ${retTy} @${mangleAsyncAwaitCall(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; await ${fn.name}`; + const args = params.join(", "); + if (adopt && argPackLines !== null) { + // A body that may suspend settles later than the adoption's then job + // runs: materialize its promise, take that turn, then await it. + emitter.declare(`declare void @scr_await_hop()`); + emitter.declare(`declare void @scr_promise_release(ptr)`); + return [ + head, + `entry:`, + ` %p = call ptr @${mangleAsyncSpawn(fn.name)}(${args})`, + ` call void @scr_await_hop()`, + awaitLine("%v", "%p"), + ` call void @scr_promise_release(ptr %p)`, + retLine("%v"), + `}`, + ``, + ]; + } + if (argPackLines !== null && emitter.stacklessAsync.has(fn.name)) { + // A stackless body's eager prefix already runs on the awaiter's + // stack, which then awaits the promise like the two separate steps. + emitter.declare(`declare void @scr_promise_release(ptr)`); + return [ + head, + `entry:`, + ` %p = call ptr @${mangleAsyncSpawn(fn.name)}(${args})`, + awaitLine("%v", "%p"), + ` call void @scr_promise_release(ptr %p)`, + retLine("%v"), + `}`, + ``, + ]; + } + if (argPackLines !== null) { + const kind = + ret.kind === "void" + ? "void" + : ret.kind === "f64" || ret.kind === "date" + ? "f64" + : ret.kind === "bool" + ? "bool" + : ret.kind === "string" + ? "str" + : "ref"; + const cret = kind === "bool" ? "zeroext i1" : retTy; + emitter.declare(`declare ${cret} @scr_async_spawn_await_${kind}(ptr, ptr)`); + emitter.needOom(); + const call = `call ${cret} @scr_async_spawn_await_${kind}(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`; + return [ + head, + `entry:`, + ...argPackLines, + kind === "void" ? ` ${call}` : ` %v = ${call}`, + retLine("%v"), + `}`, + ``, + ]; + } + emitter.declare(`declare void @scr_async_inline_enter(ptr)`); + emitter.declare(`declare void @scr_async_inline_exit(ptr)`); + emitter.declare(`declare void @scr_await_hop()`); + emitter.declare(`declare ptr @scr_promise_new()`); + emitter.declare(`declare void @scr_promise_reject_pending(ptr)`); + emitter.declare(`declare void @scr_promise_release(ptr)`); + const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`; + const out = [ + head, + `entry:`, + ` %frame = alloca { ptr, ptr }`, + ` call void @scr_async_inline_enter(ptr %frame)`, + retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`, + ...emitter.pendingTestLines("%pend"), + ` call void @scr_async_inline_exit(ptr %frame)`, + // Adopting: the then job's turn comes before the settled promise's + // own reaction, whichever way the body completed. + ...(adopt ? [` call void @scr_await_hop()`] : []), + ` br i1 %pend, label %thrown, label %clean`, + `clean:`, + ` call void @scr_await_hop()`, + retLine("%r"), + `thrown:`, + ]; + if (ret.kind !== "void" && isRefCounted(ret)) { + // An escaping throw means %r is the never-read dummy (NULL). + out.push(` call void ${releaseSym(emitter.shapeHost, ret)}(ptr %r)`); + } + out.push( + ` %p = call ptr @scr_promise_new()`, + ` call void @scr_promise_reject_pending(ptr %p)`, + awaitLine("%v", "%p"), + ` call void @scr_promise_release(ptr %p)`, + retLine("%v"), + `}`, + ``, + ); + return out; +} + +/** Move a clean async return value into the current fiber's promise. */ +export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { + const coro = emitter.currentCoro; + if (coro === null) + throw new InternalCompilerError("llvm emitter bug: wasm fulfillment outside async body"); + const ret = emitter.currentReturnType; + if (coro.kind === "generator") { + emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`); + const gen = emitter.B.tmp(); + emitter.B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${coro.self})`); + switch (ret.kind) { + case "void": + return; + case "f64": + case "date": + emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); + emitter.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`); + return; + case "bool": + emitter.declare(`declare void @scr_gen_out_bool(ptr, i1 zeroext)`); + emitter.B.line(`call void @scr_gen_out_bool(ptr ${gen}, i1 ${v!.name})`); + return; + default: + emitter.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`); + emitter.B.line( + `call void @scr_gen_out_ref(ptr ${gen}, ptr ${v!.name}, ptr ${vAdapters(emitter.shapeHost, ret).release})`, + ); + return; + } + } + emitter.declare(`declare ptr @scr_fiber_promise(ptr)`); + const pr = emitter.B.tmp(); + emitter.B.line(`${pr} = call ptr @scr_fiber_promise(ptr ${coro.self})`); + switch (ret.kind) { + case "void": + emitter.declare(`declare void @scr_promise_fulfill_void(ptr)`); + emitter.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`); + break; + case "f64": + case "date": + emitter.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); + emitter.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`); + break; + case "bool": + emitter.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`); + emitter.B.line(`call void @scr_promise_fulfill_bool(ptr ${pr}, i1 ${v!.name})`); + break; + case "string": + emitter.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`); + emitter.B.line(`call void @scr_promise_fulfill_str(ptr ${pr}, ptr ${v!.name}) ; moves in`); + break; + case "dyn": + emitter.declare(`declare void @scr_promise_resolve_dyn(ptr, ptr)`); + emitter.B.line(`call void @scr_promise_resolve_dyn(ptr ${pr}, ptr ${v!.name}) ; moves in`); + break; + default: { + const rc = vAdapters(emitter.shapeHost, ret); + emitter.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + emitter.B.line( + `call void @scr_promise_fulfill_ref(ptr ${pr}, ptr ${v!.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(emitter.shapeHost, ret)})`, + ); + } + } +} + +/** The argument-pack ABI and trampoline prefix shared by async functions + * and generators. Their promise/generator completion and spawn tails stay + * with the callers below. */ +export function emitArgPackAndTrampolinePrologue( + emitter: LlEmitter, + fn: IrFunction, +): LlArgPackAndTrampolinePrologue { + const pack = mangleArgPack(fn.name); + const lifted = fn.captures !== undefined; + const fieldTys = [...(lifted ? ["ptr"] : []), ...fn.params.map((p) => emitter.llType(p.type))]; + const definitions = [`%${pack} = type { ${fieldTys.join(", ") || "i8"} } ; ${fn.name} args`]; + const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${emitter.sizeType})`; + + // Async argument packs live from the spawn to the trampoline's first + // instructions, so they take the runtime's small-block allocator. + // Generator packs keep malloc: the runtime frees never-started ones. + const smallPack = fn.generator === undefined; + const packAlloc = smallPack ? "scr_rt_calloc" : "malloc"; + const packFree = smallPack ? "scr_rt_free" : "free"; + emitter.declare(`declare void @${packFree}(ptr)`); + emitter.declare(`declare ptr @${packAlloc}(${emitter.sizeType})`); + + const tr: string[] = [ + `define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`, + `entry:`, + ]; + const loads: string[] = []; + fieldTys.forEach((ty, i) => { + tr.push( + ` %fp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`, + ` %a${i} = load ${ty}, ptr %fp${i}`, + ); + loads.push(`${ty} %a${i}`); + }); + tr.push(` call void @${packFree}(ptr %ap)`); + const ret = fn.returnType; + const retTy = emitter.llType(ret); + const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${loads.join(", ")})`; + tr.push(retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`); + if (lifted && !emitter.coroLowered(fn)) { + tr.push(` call void @scr_closure_release(ptr %a0)`); + } + + const spawnParams = fieldTys.map((ty, i) => `${ty} %a${i}`); + const argPackLines = [ + ` %ap = call ptr @${packAlloc}(${emitter.sizeType} ${sizeOf})`, + ` %isnull = icmp eq ptr %ap, null`, + ` br i1 %isnull, label %oom, label %ok`, + `oom:`, + ` call void @sc_oom()`, + ` unreachable`, + `ok:`, + ]; + if (lifted) { + // scr_closure_retain is a header static inline — the `_v` twin is + // the exported symbol. + argPackLines.push(` %env = call ptr @scr_closure_retain_v(ptr %a0)`); + } + fieldTys.forEach((ty, i) => { + const src = lifted && i === 0 ? "%env" : `%a${i}`; + argPackLines.push( + ` %sp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`, + ` store ${ty} ${src}, ptr %sp${i}`, + ); + }); + return { definitions, pack, lifted, fieldTys, ret, tr, spawnParams, argPackLines }; +} + +/** The module functions whose bodies may park their execution context: + * their emitted text mentions a suspension entry point (SUSPENDS), or + * they mention such a function (transitively). Null where async bodies + * must keep fibers regardless: wasm32 (its coroutine lowering owns async + * bodies), worker programs, and programs that use node:test subtests, + * whose `t.test()` awaits on the CURRENT fiber even from synchronous + * code. */ +export function parkingFunctions(emitter: LlEmitter): Set | null { + if (emitter.wasi || emitter.mod.workers === true) return null; + for (const decl of emitter.decls) if (/@scr_test_sub\b/.test(decl)) return null; + for (const text of emitter.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null; + // Reverse mention edges, then propagate "may park" to every mentioner. + const mentionedBy = new Map>(); + const parks = new Set(); + const work: string[] = []; + for (const [name, text] of emitter.fnDefText) { + if (SUSPENDS.test(text)) { + parks.add(name); + work.push(name); + } + for (const callee of emitter.mentionedFunctions(text)) { + if (callee === name) continue; + let callers = mentionedBy.get(callee); + if (callers === undefined) mentionedBy.set(callee, (callers = new Set())); + callers.add(name); + } + } + while (work.length > 0) { + const callers = mentionedBy.get(work.pop()!); + if (callers === undefined) continue; + for (const caller of callers) { + if (parks.has(caller)) continue; + parks.add(caller); + work.push(caller); + } + } + return parks; +} + +/** Async functions whose calls can skip the fiber (scr_async_inline_*). + * A body qualifies only when nothing it runs can park its execution + * context: its emitted text, and the text of every module function it + * mentions (transitively), contains no suspension entry point. Such a + * body always completes before the call returns, which is exactly the + * behavior of a fiber that never suspends. Excluded conservatively: + * wasm32 (its coroutine lowering owns async bodies), module-evaluation + * bodies (their promise caches), + * `dyn` results (resolution may adopt a thenable), and every program + * that uses node:test subtests — `t.test()` runs a subtest inline and + * awaits it on the CURRENT fiber even from synchronous code. Worker + * programs keep fibers too: their fiber epilogue owns context + * termination (a stopping worker rejects and observes the promise), which + * the fiberless wrapper does not reproduce. A missed suspension still + * cannot reorder execution undetected: the runtime traps when a context parks while a + * fiberless body is on its stack. */ +export function fiberlessAsyncFunctions(emitter: LlEmitter): Set { + const parks = emitter.parkingFunctions(); + const eligible = parks === null ? new Set() : emitter.neverParkingAsync(parks); + for (const name of emitter.inlineAwaitCallees) + if (!eligible.has(name)) + throw new InternalCompilerError(`llvm emitter bug: inline await of suspending ${name}`); + return eligible; +} diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts new file mode 100644 index 000000000..7d60205e5 --- /dev/null +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -0,0 +1,2245 @@ +/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */ +import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; +import { + mangleAsyncSpawn, + mangleClassObj, + mangleFnClosure, + mangleFunction, + mangleGlobal, + mangleVtInstance, +} from "../mangle.js"; +import { + type IrGlobal, + type IrType, + RUNTIME_ERROR_CLASSES, + RUNTIME_EVENT_CLASS, + RUNTIME_EVENT_TARGET_CLASS, + ffiCallbackType, + isFfiContextParam, + isRefCounted, + typeKey, +} from "../../ir/ir.js"; +import { + FN_ATTRS, + limitInlineDynReleases, + releaseSym, + typedRefConstructor, + vAdapters, +} from "./shapes.js"; +import { emitLlvmLayouts } from "./layouts.js"; +import { moduleRuntimeFeatures } from "../../ir/runtime-features.js"; +import { emitArraySearchHelpers } from "./array-search.js"; +import { emitClassMembershipHelper } from "./classes.js"; +import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js"; +import { + MAP_ITER_DECL, + NUMBER_MAP_ENTRY_DECL, + STRING_MAP_ENTRY_DECL, +} from "./map-number-lookup.js"; +import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; +import { threadSlotsBlockDefinitions } from "./thread-slots.js"; +import { + ABSENT_FIELD_PAYLOAD, + exactIndexLines, + f64Lit, + ffiNativeParamLl, + ffiNativeReturnLl, + ffiNativeTypeLl, +} from "./common.js"; +import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; +import { hasLoneSurrogate, wtf8Bytes } from "../literals.js"; +import { undefinedArmTag } from "../../ir/analysis.js"; +import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js"; +import { literalEqualityDefinition } from "./string-literal-equality.js"; +import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; +import { emitLibraryIdentityLines } from "../library-identity-markers.js"; +import { InternalCompilerError } from "../../errors.js"; + +export function emitModuleParts(emitter: LlEmitter): string[] { + // Function bodies first (the literal/unit/fn-value tables fill as they + // emit), then the file assembles around them — the runtime ABI’s order. + const fnDefs: string[] = []; + for (const fn of emitter.mod.functions) { + const def = emitter.emitFunction(fn); + fnDefs.push(def); + emitter.fnDefText.set(fn.name, def); + } + emitter.selectStacklessAsync(fnDefs); + const errorViews: string[] = []; + for (const property of ["name", "message"]) + if (emitter.mod.functions.some((fn) => fn.name === `%error.${property}.read`)) { + emitter.declare(`declare void @scr_error_install_${property}_reader(ptr)`); + errorViews.push( + ` call void @scr_error_install_${property}_reader(ptr @${mangleFunction(`%error.${property}.read`)})`, + ); + } + for (const meta of emitter.classMeta.values()) { + if (meta.def.runtime || meta.root.def.name !== "%Error") continue; + const type: IrType = { kind: "object", className: meta.def.name }; + const adapter = emitter.liveDynRefAdapter(type); + const rc = vAdapters(emitter.shapeHost, type); + const key = typeKey(type); + const name = `sc_error_view_${errorViews.length}`; + emitter.resolveThunkDefs.push( + `define internal ptr @${name}(ptr %v) ${FN_ATTRS} {`, + `entry:`, + ` %d = call ptr ${typedRefConstructor(emitter.shapeHost, type)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${emitter.cstr(key)}, ${emitter.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`, + ` ret ptr %d`, + `}`, + ``, + ); + emitter.declare(`declare void @scr_error_register_dyn(ptr, ptr)`); + errorViews.push( + ` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`, + ); + } + const layouts = emitLlvmLayouts( + emitter.shapeHost, + emitter.mod, + emitter.classMeta, + emitter.classObjs, + emitter.fnByName, + (t) => emitter.llType(t), + emitter.nullableFields, + emitter.int32Slots, + ); + const shapes = layouts.records; + const classShapes = layouts.classes; + const classObjDefs = layouts.classObjects; + const wrappers = emitter.emitFnValueDefs(); + const asyncDefs = emitter.emitAsyncScaffolding(); + const ffiCallbacks = emitter.emitFfiCallbackDefs(); + const hasNoInlineRecordClone = [...emitter.recordCloneShapes].some( + (shapeId) => (emitter.recordsById.get(shapeId)?.fields.length ?? 0) >= 16, + ); + + // Module globals, FIRST OCCURRENCE per id: a class-expression static + // instantiated through several mixin applications registers one global + // id several times (2041-mixin-values) — C's tentative definitions + // accept the duplicates without an error, LLVM rejects redefinition, so the + // storage AND the release emit once per id here. + const seenGlobalIds = new Set(); + const globals = (emitter.mod.globals ?? []).filter((g) => { + if (seenGlobalIds.has(g.id)) return false; + seenGlobalIds.add(g.id); + return true; + }); + // Refcounted globals release before main returns — the runtime ABI’s + // sc_release_globals, keeping the RC audit's live count exact. Built + // before the declaration table flushes (it adds the release symbols). + // Two spellings with distinct temp names: the normal exit and the + // uncaught-exception exit are separate blocks of the same function. + // Interned function-value closures are IMMORTAL (rc == SIZE_MAX), so + // an own-property table Object.defineProperties hung on one would + // outlive the RC audit — release it with the globals (the runtime ABI’s + // sc_release_globals tail). Only when the dispatch unit is even + // linked (defineProps is the only writer). + const runtimeFeatures = moduleRuntimeFeatures(emitter.mod); + const fnValueProps = [...emitter.fnValues]; + if (fnValueProps.length > 0) emitter.declare(`declare void @scr_box_release(ptr)`); + if (emitter.classObjs.size > 0) + emitter.declare(`declare void @scr_classobj_clear_properties(ptr)`); + const globalReleaseLines = (prefix: string): string[] => { + const lines: string[] = []; + globals.forEach((g, i) => { + if (!isRefCounted(g.type)) return; + lines.push( + ` %${prefix}${i} = load ptr, ptr @${mangleGlobal(g.id)}`, + ` call void ${releaseSym(emitter.shapeHost, g.type)}(ptr %${prefix}${i}) ; ${g.name}`, + ); + }); + fnValueProps.forEach((name, i) => { + // props sits at %ScrClosure field 3; release is NULL-tolerant — + // cleared so a second release path stays idempotent. + lines.push( + ` %${prefix}fp${i} = load ptr, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`, + ` call void @scr_box_release(ptr %${prefix}fp${i}) ; ${name}.props`, + ` store ptr null, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`, + ); + }); + for (const name of emitter.classObjs.keys()) + lines.push(` call void @scr_classobj_clear_properties(ptr @${mangleClassObj(name)})`); + return lines; + }; + // Exit listeners can read MODULE GLOBALS directly, so they must run + // BEFORE the global releases (the runtime ABI’s runExitListeners + // ordering — the atexit half becomes an idempotent no-op). + const usesEvents = runtimeFeatures.processEvents; + const usesChildProcess = runtimeFeatures.childProcess; + const usesFsWatch = runtimeFeatures.fsWatch; + // Stream-surface programs fill the loop's stream hook (the deferred + // next-tick emissions) and the emitter's post-registration flow kick + // before %main — scr_stream.c links only when the line is emitted + // (native/executable.ts gates on the same predicate). + const usesStream = runtimeFeatures.stream; + // Net-surface programs fill the loop's net hooks (and the netSocket + // handle-dispatch ops for the checked-dynamic boundary); http-surface + // programs additionally stamp the httpReq/httpRes ops — the C main's + // install lines, gated on the same predicates native/executable.ts links by. + const usesNet = runtimeFeatures.net; + const usesDgram = runtimeFeatures.dgram; + const usesHttp2 = runtimeFeatures.http2; + const usesHttp = runtimeFeatures.http; + // Fetch-referencing programs register native fetch before main. + // Runtime selection compiles scr_fetch.c on the same predicate. + const usesFetch = runtimeFeatures.fetch; + const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa; + // The process verdict has the same precedence as the C reference + // emitter: node:test owns the final status when present; otherwise an + // embedded process.exitCode owns it; ordinary programs return zero. + const usesNodeTest = runtimeFeatures.nodeTest; + // Declared NOW — the extern block flushes before main assembles. + if (usesEvents) emitter.declare(`declare void @scr_events_install()`); + if (usesChildProcess) emitter.declare(`declare void @scr_child_dyn_install()`); + const childStreamBoxes = + usesChildProcess && + usesStream && + emitter.classMeta.has("%Readable") && + emitter.classMeta.has("%Writable") + ? [ + emitter.dyn.toDynHelper({ kind: "object", className: "%Readable" }), + emitter.dyn.toDynHelper({ kind: "object", className: "%Writable" }), + ] + : null; + // worker.stdout and worker.stderr box the stream unit's Readable the + // same way, and worker.stdin its Writable when the program has one + // (scr_worker_stdio_install, scr_worker_events.c). + const workerStdioBox = + emitter.mod.workers === true && usesStream && emitter.classMeta.has("%Readable") + ? (childStreamBoxes?.[0] ?? + emitter.dyn.toDynHelper({ kind: "object", className: "%Readable" })) + : null; + const workerStdinBox = + emitter.mod.workers === true && usesStream && emitter.classMeta.has("%Writable") + ? (childStreamBoxes?.[1] ?? + emitter.dyn.toDynHelper({ kind: "object", className: "%Writable" })) + : null; + if (childStreamBoxes || workerStdioBox || workerStdinBox) { + emitter.declare( + `@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`, + ); + } + if (childStreamBoxes) + emitter.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`); + if (workerStdioBox || workerStdinBox) + emitter.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`); + if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`); + if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`); + if (usesStream) emitter.declare(`declare void @scr_stream_install()`); + if (usesNet) { + emitter.declare(`declare void @scr_net_install()`); + emitter.declare(`declare void @scr_net_dyn_install()`); + } + if (usesDgram) emitter.declare(`declare void @scr_dgram_install()`); + if (usesHttp2) emitter.declare(`declare void @scr_http2_dyn_install()`); + if (usesHttp) emitter.declare(`declare void @scr_http_dyn_install()`); + if (usesFetch) emitter.declare(`declare void @scr_fetch_install()`); + + if (usesNodeTest) { + emitter.declare(`declare i32 @scr_test_exit_code()`); + emitter.declare(`declare void @scr_test_init()`); + } + if (snapshotsTlsCa) { + emitter.declare(`declare void @scr_tls_ca_install()`); + } + // Inline exit listeners run when something they must beat exists: + // the refcounted-global releases, or the retained-FFI atexit ledger + // sweep (a listener may legitimately release or pump a registration, + // and only the inline call orders ahead of every atexit handler — + // the runtime ABI’s runExitListeners stance). Plain event programs + // with neither keep the atexit path, so their listener timing is + // unchanged. + const hasRefGlobals = globals.some((g) => isRefCounted(g.type)) || fnValueProps.length > 0; + const inlineExitListeners = + usesEvents && (emitter.mod.workers || hasRefGlobals || emitter.ffiHasRetainedCallback); + emitter.declare(`declare i32 @scr_exit_code_hint_get()`); + if (inlineExitListeners) { + emitter.declare(`declare void @scr_run_exit_listeners(double)`); + emitter.declare(`declare i32 @scr_exit_code_hint_get()`); + } + const exitListenerLines = (prefix: string): string[] => { + if (!inlineExitListeners) return []; + return [ + ` %${prefix}h = call i32 @scr_exit_code_hint_get()`, + ` %${prefix}hd = sitofp i32 %${prefix}h to double`, + ` call void @scr_run_exit_listeners(double %${prefix}hd)`, + ]; + }; + // The entry's exits release the module globals only where the thread + // frees its heap when its script ends (scr_exit_frees: workers, and + // builds that check for leaks); the main thread of other builds leaves + // it to the operating system. + const exitReleaseLines = (prefix: string): string[] => { + const releases = globalReleaseLines(prefix); + if (releases.length === 0) return releases; + emitter.declare(`declare zeroext i1 @scr_exit_frees()`); + return [ + ` %${prefix}.frees = call zeroext i1 @scr_exit_frees()`, + ` br i1 %${prefix}.frees, label %${prefix}.release, label %${prefix}.kept`, + `${prefix}.release:`, + ...releases, + ` br label %${prefix}.kept`, + `${prefix}.kept:`, + ]; + }; + const globalReleases = exitReleaseLines("g"); + const asyncEntry = emitter.fnByName.get(emitter.mod.entry)?.async === true; + const entryMayThrow = emitter.mayThrow.has(emitter.mod.entry); + // The event loop runs when timers appeared OR any async/generator + // function exists (the C main's hasAsync || hasGenerators || + // usesTimers gate). Generator programs run + // the loop too: its exit accounting notes still-suspended generator + // fibers as abandoned, so the RC audit downgrades exactly like the + // async loop-exhaustion story. + const runsLoop = + emitter.usesTimers || + emitter.ffiHasForeignCallback || + emitter.mod.functions.some((f) => f.async === true || f.generator !== undefined); + const uncaughtReleases = entryMayThrow && !asyncEntry ? exitReleaseLines("gu") : []; + const loopReleasesU = runsLoop ? exitReleaseLines("gl") : []; + const loopReleasesR = runsLoop ? exitReleaseLines("gr") : []; + const topRejectReleases = asyncEntry ? exitReleaseLines("gt") : []; + const topPendingReleases = asyncEntry ? exitReleaseLines("gp") : []; + const loopReportedReleases = runsLoop ? exitReleaseLines("gq") : []; + // main's epilogues read the flag / the loop entry points — declared + // HERE, before the extern block flushes. They run once, so they keep + // the out-of-line scr_exc_pending call; per-call checks inline the + // test (pendingTestLines). + if (entryMayThrow || runsLoop) emitter.declare(`declare zeroext i1 @scr_exc_pending()`); + if (runsLoop) { + emitter.declare(`declare zeroext i1 @scr_loop_run(ptr)`); + if (emitter.mod.commonJsEntry === true) + emitter.declare(`declare void @scr_loop_commonjs_entry()`); + emitter.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`); + emitter.declare(`declare void @scr_discard_unhandled_rejections()`); + } + if (asyncEntry) { + emitter.declare(`declare i32 @scr_promise_finish_top_level(ptr)`); + emitter.declare(`declare void @scr_promise_rethrow_top_level(ptr)`); + emitter.declare(`declare void @scr_promise_release(ptr)`); + emitter.declare(`declare void @scr_exit_code_note(i32)`); + } + const topPendingExitLines = (): string[] => { + if (!asyncEntry) return []; + const lines: string[] = []; + // finish_top_level noted 13 unless the program set process.exitCode. + // A failed node:test run outranks either before exit listeners run. + if (usesNodeTest) { + lines.push( + ` %tla_program_exit = call i32 @scr_test_exit_code()`, + ` %tla_program_exit_zero = icmp eq i32 %tla_program_exit, 0`, + ` %tla_hint = call i32 @scr_exit_code_hint_get()`, + ` %tla_exit_status = select i1 %tla_program_exit_zero, i32 %tla_hint, i32 %tla_program_exit`, + ` call void @scr_exit_code_note(i32 %tla_exit_status)`, + ); + } + for (const line of exitListenerLines("xp")) lines.push(line); + for (const line of topPendingReleases) lines.push(line); + // An exit listener may set process.exitCode again, as in Node. + lines.push(` %tla_exit_code = call i32 @scr_exit_code_hint_get()`, ` ret i32 %tla_exit_code`); + return lines; + }; + // LIBRARY mode: the runtime entry points the generated library + // symbols delegate to — declared before the extern block flushes. + if (emitter.mod.lib !== undefined) { + if (emitter.wasi) { + emitter.declare(`declare ptr @malloc(${emitter.sizeType})`); + emitter.declare(`declare void @free(ptr)`); + } + emitter.declare(`declare void @scr_library_entry(i1 zeroext, ptr)`); + emitter.declare(`declare void @scr_library_reset()`); + emitter.declare(`declare void @scr_library_check_exc()`); + emitter.declare(`declare void @scr_library_set_sink(ptr, ptr)`); + emitter.declare(`declare void @scr_library_callback_entry_guard(ptr)`); + emitter.declare(`declare void @scr_library_arena_reset()`); + emitter.declare(`declare void @scr_library_collect()`); + if ((emitter.mod.lib.callbacks?.length ?? 0) > 0) { + // Host-callback channels: the registration define's dispatch + // (strcmp over the declared names + the runtime slot store). + // The call-site fetch pair (scr_library_cb_require/_ctx) is + // declared at ffiCall emission like every body-driven runtime + // symbol. + emitter.declare(`declare void @scr_library_cb_set(${emitter.sizeType}, ptr, ptr)`); + emitter.declare(`declare i32 @strcmp(ptr, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.params.includes("string"))) { + emitter.declare(`declare ptr @scr_library_str_in(ptr, ${emitter.sizeType})`); + } + if (emitter.mod.lib.exports.some((e) => e.params.includes("bytes"))) { + emitter.declare(`declare ptr @scr_library_bytes_in(ptr, ${emitter.sizeType}, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.params.includes("i64"))) { + emitter.declare(`declare double @scr_library_i64_in(i64, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.params.includes("u64"))) { + emitter.declare(`declare double @scr_library_u64_in(i64, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.returns === "string")) { + emitter.declare(`declare void @scr_library_str_out(ptr, ptr, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.returns === "bytes")) { + emitter.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.params.includes("cstring"))) { + emitter.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`); + } + if (emitter.mod.lib.exports.some((e) => e.returns === "cstring")) { + emitter.declare(`declare ptr @scr_library_cstr_out(ptr)`); + } + } + // Helpers assemble BEFORE the declaration table flushes (they add + // write/abort declarations). + const helpers = [ + ...emitter.helperDefs(), + ...emitArraySearchHelpers(emitter.arraySearchKinds, emitter.sizeType, (line) => + emitter.declare(line), + ), + ...emitClassMembershipHelper(emitter.classMeta, emitter.sizeType), + ...(emitter.publishWalkersCache?.defs() ?? []), + ...emitter.publishedClassNamesDefs(), + ...(emitter.regions?.moduleDefs() ?? []), + ]; + if (emitter.constantNumericTables.size > 0) + emitter.declare(`declare double @scr_arr_get_number(ptr, double)`); + // Native matchers of the regex literals inside the compiled subset. + const regexMatchers = new Map(); + for (const [key, re] of emitter.regexInstances) { + const code = compileRegexMatcher( + `${re.sym}_match`, + re.pattern, + re.flags, + emitter.sizeType, + !emitter.wasi, + ); + if (code === null) continue; + regexMatchers.set(key, code); + for (const decl of code.decls) emitter.declare(decl); + } + + const out: string[] = [ + `; Generated by scriptc (LLVM backend) from ${emitter.mod.sourceFile}. Do not edit.`, + ``, + // Type shapes shared with the runtime's C ABI. ScrStr is the header + // prefix only (the flexible-array tail is concrete per literal); + // ScrLogArg is { i32 tag; 8-byte union } — i64 at offset 8 matches + // the C layout (4 bytes padding after the tag). ScrUnion/ScrClosure + // mirror scr_runtime.h field-for-field (tag reads, slot peeks, the + // fn pointer, and the caps[] tail all address through them). + `%ScrStr = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType} }`, + `%ScrLogArg = type { i32, i64 }`, + `%ScrVt = type { ${emitter.sizeType}, ${emitter.sizeType}, ptr }`, + `%ScrUnion = type { ${emitter.sizeType}, i32, ptr, ptr, ptr, i64 }`, + `%ScrClosure = type { ${emitter.sizeType}, ptr, ${emitter.sizeType}, ptr, i32, i32, ptr, ptr }`, + `%ScrFfiTable = type { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, i8, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`, + `%ScrRegex = type { ${emitter.sizeType}, ptr, ptr, ptr, double, ptr, ptr, ptr }`, + // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem, + // cyc, spare, filled, ops, data, present, ext } (field numbers in + // SCR_ARR_FIELD), where filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor + // (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse, + // property and metadata storage (NULL until first used). Live dynamic + // stream commits swap its dense, presence and out-of-line storage while + // preserving the target object's identity. + `%ScrArr = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, i8, i8, i16, i32, ptr, ptr, ptr, ptr }`, + `%ScrArrOps = type { ptr, ptr, ptr }`, + // The ScrMap prefix { rc, key_kind, val_kind, traced } — the inline + // RC fast paths test the traced byte (shapes.ts). + ...(emitter.rcHelpers === null ? [] : [`%ScrMapRc = type { ${emitter.sizeType}, i8, i8, i8 }`]), + // The complete ScrMap and its { key, val, hash } entry, read by the + // inline number-key lookup (sc_map_entry_f64) and the inline + // iteration steps (sc_map_iter_entry): field 8 is the i32 entry + // count, field 10 the i64 absent-key filter, entries is field 11, + // nbuckets and buckets fields 13 and 14, the direct index and its i32 + // length fields 15 and 16. + ...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) || + emitter.decls.has(STRING_MAP_ENTRY_DECL) || + emitter.decls.has(MAP_ITER_DECL) + ? [ + `%ScrMapIx = type { ${emitter.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`, + ] + : []), + // String-keyed reference reads load the value slot of the entry + // scr_map_entry_str returns (map-number-lookup.ts). + ...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) || + emitter.decls.has(STRING_MAP_ENTRY_DECL) || + emitter.decls.has(MAP_ITER_DECL) + ? [`%ScrMapEntry = type { i64, i64, i64 }`] + : []), + // The runtime error prefix { rc, vt, name, message, code, cause } and the + // class-object shape { rc, pre, post, ctor, name } — field reads on + // builtin errors and classval loads GEP through these. + `%ScrError = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`, + `%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`, + // The runtime emitter prefix { rc, vt, reg, cls } — user subclasses + // embed it (classes.ts), and bare-emitter GEPs address through it. + `%ScrEmitter = type { ${emitter.sizeType}, ptr, ptr, ptr }`, + // The runtime stream layout { rc, vt, reg, cls, st } — one struct + // for all five stream classes; stream subclasses embed it. + `%ScrStream = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr }`, + // The EventTarget and Event subclass prefixes { rc, vt, state, cls }. + ...(emitter.classMeta.has(RUNTIME_EVENT_TARGET_CLASS) + ? [`%ScrEventTargetObj = type { ${emitter.sizeType}, ptr, ptr, ptr }`] + : []), + ...(emitter.classMeta.has(RUNTIME_EVENT_CLASS) + ? [`%ScrEventObj = type { ${emitter.sizeType}, ptr, ptr, ptr }`] + : []), + // The catch-binding snapshot box { rc, kind, f64, b, payload, + // retain_fn, release_fn, trace_fn } — caughtTest kind reads and + // caughtNarrow payload extraction GEP through it (offsets match + // scr_runtime.h's natural alignment: kind at 8, f64 at 16, b at 24, + // payload at 32). + `%ScrCaught = type { ${emitter.sizeType}, i32, double, i8, ptr, ptr, ptr, ptr }`, + // ScrBytes { rc, len, elem(i32+pad), data, backing, brands, shared }. Indexed + // typed-array access GEPs through this directly: the IR type already + // fixes elem, so the hot path needs neither a runtime kind load nor + // the generic scr_bytes_get/set call. + `%ScrBytes = type { ${emitter.sizeType}, ${emitter.sizeType}, i32, ptr, ptr, i8, i8, i8, i8, ptr }`, + // The capture box { rc, kind, obj_retain, obj_release, obj_trace, + // slot } — TDZ reads peek the payload slot (offset 40) directly. + `%ScrBox = type { ${emitter.sizeType}, i32, ptr, ptr, ptr, i64 }`, + // The stack buffer of the emitted JSON serializers { data, len, cap }. + `%ScrJsonBuf = type { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ${emitter.sizeType}, ${emitter.sizeType} }`, + // The dynCheck error-path spine { parent, key, index } — the emitted + // builders stack-allocate one per recursion level (dyn.ts). + `%ScrDynPath = type { ptr, ptr, ${emitter.sizeType} }`, + `%ScrIslandModule = type { ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, i32, ptr, ${emitter.sizeType}, ${emitter.sizeType} }`, + `%ScrIslandEdge = type { ptr, ptr, ptr, i32 }`, + // A generator's prefix { rc, receiver, fiber, state, out } and its + // value slot { kind, f64, b, payload, release_fn } (scr_async.c pins + // both): number and boolean yields and drives address OUT directly. + ...(emitter.usesGenSlots + ? [ + `%ScrGenSlot = type { i32, double, i8, ptr, ptr }`, + `%ScrGenHead = type { ${emitter.sizeType}, ptr, ptr, i32, %ScrGenSlot }`, + ] + : []), + ]; + for (const line of shapes.typeDefs) out.push(line); + for (const line of classShapes.typeDefs) out.push(line); + // Thread-instanced library state (abi.instance_per_thread): the + // program TU's mutable globals — module globals, run-once guards, the + // lazily-compiled regex literal caches — and the runtime globals its + // init stamps live in thread-local storage, matching the runtime + // objects compiled with -DSCR_THREAD_INSTANCES. Immutable interned + // data (string literals, unit arms, template arrays, vtables) stays + // shared. + const tl = + emitter.mod.workers === true || emitter.mod.lib?.threadInstances === true + ? "thread_local " + : ""; + out.push( + ``, + `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, + `declare void @scr_init()`, + `declare void @scr_lib_init(i32, ptr)`, + ...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), + ...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), + ...(emitter.runtimeAbiMarker && emitter.mod.lib === undefined + ? [`declare void @${RUNTIME_ABI_MARKER}()`] + : []), + // Overrides the runtime's weak default (scr_lib.c). + ...(emitter.detachedExit && emitter.mod.lib === undefined + ? [`@scr_detached_exit = constant i8 1`] + : []), + ); + for (const d of emitter.decls) out.push(d); + out.push(``); + const flushedDecls = new Set(emitter.decls); + { + const tl = + emitter.mod.workers === true || emitter.mod.lib?.threadInstances === true + ? "thread_local " + : ""; + out.push(...threadSlotsBlockDefinitions(emitter.threadSlotsBlocks, tl !== "")); + for (let site = 0; site < emitter.dynReadIcSites; site++) + out.push( + `@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr } zeroinitializer`, + ); + } + for (const table of emitter.constantNumericTables.values()) { + const n = table.values.length; + let tableTmp = 0; + const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`); + out.push( + `@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`, + `define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`, + `entry:`, + ` %initialized = icmp ne ptr %a, null`, + ...tableIndex.lines.map((line) => ` ${line}`), + ` %safe = and i1 %initialized, ${tableIndex.ok}`, + ` br i1 %safe, label %read, label %fallback`, + `read:`, + ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`, + ` %value = load double, ptr %slot`, + ` ret double %value`, + `fallback:`, + ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, + ` ret double %generic`, + `}`, + ``, + ); + } + for (const [text, lit] of emitter.literals) { + // Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } — + // the runtime ABI’s static table, retain/release skip rc == SIZE_MAX. + // 64-bit (little-endian) targets split the capacity word: the high + // half carries the precomputed Map key hash (SCR_STR_HASH_CACHE). + // Bit 31 marks all-ASCII literals (SCR_STR_ASCII_BIT) on every target. + const capWord = + emitter.sizeType === "i64" + ? literalCapWord(wtf8Bytes(text)) + : literalCapWord32(wtf8Bytes(text)); + out.push( + `@${lit.sym} = internal global { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, [${lit.len + 1} x i8] } ` + + `{ ${emitter.sizeType} -1, ${emitter.sizeType} ${lit.len}, ${emitter.sizeType} ${capWord}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`, + ); + } + if (emitter.literals.size > 0) out.push(``); + for (const lit of emitter.bigintLiterals.values()) { + // Immortal ScrBigInt: { rc = SIZE_MAX, len, sign, cap, limbs } with + // pointer-sized little-endian limbs (scr_bigint.c); zero keeps one + // zero limb so limb 0 is always readable. + const size = emitter.sizeType; + const limbBits = size === "i64" ? 64n : 32n; + const mask = (1n << limbBits) - 1n; + const limbs: string[] = []; + for (let m = lit.value < 0n ? -lit.value : lit.value; m > 0n; m = m >> limbBits) { + limbs.push(`${size} ${m & mask}`); + } + const len = limbs.length; + if (len === 0) limbs.push(`${size} 0`); + const sign = lit.value < 0n ? -1 : lit.value > 0n ? 1 : 0; + out.push( + `@${lit.sym} = internal global { ${size}, ${size}, i32, i32, [${limbs.length} x ${size}] } ` + + `{ ${size} -1, ${size} ${len}, i32 ${sign}, i32 ${limbs.length}, [${limbs.length} x ${size}] [${limbs.join(", ")}] }`, + ); + } + if (emitter.bigintLiterals.size > 0) out.push(``); + for (const [key, sym] of emitter.unitInstances) { + // One immortal instance per unit-armed (union, tag): tag set, payload + // slot and RC entry points zero — retain/release/collector all skip + // rc == SIZE_MAX, so these never join the RC audit or a trace walk. + const [unionId, tag] = key.split(":"); + out.push( + `@${sym} = internal global %ScrUnion { ${emitter.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 0 } ; ${unionId} unit arm`, + ); + } + if (emitter.unitInstances.size > 0) out.push(``); + for (const [unionId, sym] of emitter.retypeGuards) { + // Thread-local like module globals: a thread's cast caches hold only + // its own boxes. Never written, the flag folds to false. + out.push(`@${sym} = internal ${tl}global i8 0 ; ${unionId} boxes in a cast cache`); + } + if (emitter.retypeGuards.size > 0) out.push(``); + for (const [unionId, sym] of emitter.absentInstances) { + // One immortal ABSENT field-slot instance per undefined-armed union: + // the undefined arm's tag with payload 1 (ordinary unit instances + // carry 0). Every tag reader sees undefined; recordHas and the + // field reads compare tag and payload, never the address, so the + // per-object copies of separately compiled units agree. + const tag = undefinedArmTag({ kind: "union", unionId }, emitter.unionsById); + out.push( + `@${sym} = internal global %ScrUnion { ${emitter.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 ${ABSENT_FIELD_PAYLOAD} } ; ${unionId} absent field`, + ); + } + if (emitter.absentInstances.size > 0) out.push(``); + if (emitter.needsUnionNone) + out.push( + `@sc_union_none = internal constant %ScrUnion zeroinitializer ; NULL union dummy read by inline stores`, + ``, + ); + if (emitter.nullableUnions.usesNullSentinel) { + // `null` in every `C | null | undefined` union (NULL is undefined): + // one module-scope immortal object like the ABSENT sentinel. + const S = emitter.sizeType; + out.push( + `${NULLABLE_NULL} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable null arm`, + ``, + ); + } + if (emitter.needsNullableAbsent) { + // The ABSENT field-slot state of every nullable-pointer union: one + // module-scope immortal object (rc == SIZE_MAX), distinct from NULL + // (undefined) and from every instance. RC entry points and the + // collector skip it like any immortal; field reads compare its address. + const S = emitter.sizeType; + out.push( + `${NULLABLE_ABSENT} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable absent field`, + ``, + ); + } + for (const [key, re] of emitter.regexInstances) { + // One immortal ScrRegex per (pattern, flags) literal, pointing at + // the interned source/flags strings. The bc and translated-program + // slots start null (lazy compile, cached by the runtime) — a mutable + // global, not constant. The last slot is the build-time matcher. + const matcher = regexMatchers.has(key) ? `@${re.sym}_match` : "null"; + out.push( + `@${re.sym} = internal ${tl}global %ScrRegex { ${emitter.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null, double 0.0, ptr null, ptr null, ptr ${matcher} } ; ${key.replace(/\n/g, "\\n")}`, + ); + } + if (emitter.regexInstances.size > 0) out.push(``); + for (const code of regexMatchers.values()) out.push(...code.lines, ``); + for (const [, inst] of emitter.templateStringsInstances) { + // One immortal ScrArr per tagged-template site: a [N x ptr] data + // global of interned cooked-string literals, and the ScrArr header + // over it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, + // no REF entry points). Every dense slot is present; reads retain + // immortal strings — a no-op. + const n = inst.slots.length; + const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`; + out.push( + `@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, + `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, + `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, ${emitter.sizeType} ${n}, ${emitter.sizeType} ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`, + ); + } + if (emitter.templateStringsInstances.size > 0) out.push(``); + for (const [text, c] of emitter.cstrs) { + // NUL-terminated byte-array constants for scr_jb_puts and indentation. + out.push(`@${c.sym} = internal constant [${c.len + 1} x i8] c"${llStrBytes(text)}"`); + } + if (emitter.cstrs.size > 0) out.push(``); + for (const [body, sym] of emitter.mapOpsConsts) { + out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); + } + if (emitter.mapOpsConsts.size > 0) out.push(``); + + for (const line of ffiCallbacks.globals) out.push(line); + if (ffiCallbacks.globals.length > 0) out.push(``); + for (const g of globals) { + const ty = emitter.llType(g.type); + const zero = ty === "double" ? f64Lit(0) : ty === "ptr" ? "null" : "false"; + const debug = emitter.debug?.global(g); + out.push( + `@${mangleGlobal(g.id)} = internal ${tl}global ${ty} ${zero}${debug ? `, !dbg ${debug}` : ""} ; ${g.name}`, + ); + } + if (globals.length > 0) out.push(``); + for (const line of helpers) out.push(line); + for (const line of ffiCallbacks.defs) out.push(line); + for (const line of shapes.defs) out.push(line); + for (const line of classShapes.defs) out.push(line); + for (const line of classObjDefs) out.push(line); + for (const line of emitter.walkers.defs) out.push(line); + for (const line of emitter.dyn.defs) out.push(line); + for (const line of wrappers) out.push(line); + for (const line of asyncDefs) out.push(line); + for (const line of emitter.resolveThunkDefs) out.push(line); + out.push(fnDefs[0] ?? ""); + for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!); + out.push(""); + + // main(): scr_init, the program-dependent error-vt interval stamps, + // scr_lib_init(argc, argv), then the entry function. An uncaught + // exception escaping top-level code prints and exits 1 (Node). + // The exception epilogue is emitted when the entry may throw. + const stamps: string[] = [...errorViews]; + for (const iv of emitter.errorIntervals) { + const fields: [number, number][] = [ + [0, iv.pre], + [1, iv.post], + ]; + for (const [field, value] of fields) { + stamps.push( + ` store ${emitter.sizeType} ${value}, ptr getelementptr inbounds ([${RUNTIME_ERROR_CLASSES.size} x %ScrVt], ptr @scr_error_vts, i64 0, i64 ${iv.kind}, i32 ${field})${field === 1 ? ` ; ${iv.lib}` : ""}`, + ); + } + } + if (emitter.emitterInterval !== null) { + // The runtime emitter vtable's interval (emitterVtStampLines): bare + // EventEmitter instances answer instanceof and dispatch dynamic + // teardown under THIS module's preorder numbering. + out.push(`@scr_emitter_vt = external ${tl}global %ScrVt`, ``); + stamps.push( + ` store ${emitter.sizeType} ${emitter.emitterInterval.pre}, ptr getelementptr inbounds (%ScrVt, ptr @scr_emitter_vt, i64 0, i32 0)`, + ` store ${emitter.sizeType} ${emitter.emitterInterval.post}, ptr getelementptr inbounds (%ScrVt, ptr @scr_emitter_vt, i64 0, i32 1) ; EventEmitter`, + ); + } + for (const iv of emitter.streamIntervals) { + // The runtime stream vtables' intervals (streamVtStampLines) — the + // emitter story: instanceof and dynamic teardown dispatch through + // them. + out.push(`@${iv.vt} = external ${tl}global %ScrVt`); + stamps.push( + ` store ${emitter.sizeType} ${iv.pre}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 0)`, + ` store ${emitter.sizeType} ${iv.post}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 1) ; ${iv.lib}`, + ); + } + if (emitter.streamIntervals.length > 0) out.push(``); + if ([...emitter.literals.keys(), ...emitter.cstrs.keys()].some(hasLoneSurrogate)) { + // The runtime skips surrogate work until a lone surrogate may exist + // (scr_wtf8_any); its own producers note one, and literals do here. + out.push(`declare void @scr_wtf8_note()`, ``); + stamps.push(` call void @scr_wtf8_note()`); + } + if (emitter.errorIntervals.length > 0 && emitter.tracedShapes.has("object:%Error")) { + // The cycle fixpoint marked the Error hierarchy (a user subclass + // holds cycle-capable fields — capability is hierarchy-uniform), so + // the runtime's own error allocations need collector headers too. + // Declared inline: the extern block already flushed (LLVM is + // order-free, the helper-defs precedent). + out.push(`declare void @scr_error_set_traced()`, ``); + stamps.push(` call void @scr_error_set_traced()`); + } + if ((entryMayThrow || runsLoop) && emitter.mod.lib === undefined) { + // Declared inline: the extern block already flushed (LLVM is + // order-free — the scr_error_set_traced precedent). Only the + // printer emits here (nothing else declares it); scr_exc_pending + // and the loop entry points rode the Set before the flush. + out.push( + `declare void @scr_exc_print_uncaught()`, + `declare zeroext i1 @scr_exc_handle_uncaught(i1 zeroext)`, + ``, + ); + } + if (emitter.mod.lib !== undefined) { + // LIBRARY mode: no @main — the profile-declared external + // symbols specified by the library IR instead. + for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line); + out.push(...emitter.inlineRcTail(flushedDecls)); + out.push(`attributes #0 = { sanitize_address }`); + if (emitter.usesCoroutines) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`); + if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`); + out.push(``); + out.push(...emitter.fieldAliasMetadata()); + if (emitter.debug !== null) out.push(emitter.debug.render()); + return out; + } + out.push( + `define i32 @${emitter.mod.workers ? "sc_context_entry" : emitter.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`, + `entry:`, + ...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), + ...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []), + ` call void @scr_init()`, + // Each context (the main thread, every worker) installs its stack + // guard before running compiled code that checks it. + ...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), + ...stamps, + // Event-surface programs (signal/exit listeners) fill the loop's + // nullable event hooks before %main — scr_events.c links only when + // this line is emitted (native/executable.ts gates on the same predicate). + ...(usesEvents ? [` call void @scr_events_install()`] : []), + ...(usesChildProcess ? [` call void @scr_child_dyn_install()`] : []), + ...(childStreamBoxes + ? [ + ` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`, + ] + : []), + ...(workerStdioBox || workerStdinBox + ? [ + ` call void @scr_worker_stdio_install(ptr @scr_stream_child_ops, ptr ${workerStdioBox ? `@${workerStdioBox}` : "null"}, ptr ${workerStdinBox ? `@${workerStdinBox}` : "null"})`, + ] + : []), + // fs.watch programs fill the loop's watch hooks the same way — + // scr_watch.c links only when this line is emitted. + ...(usesFsWatch ? [` call void @scr_watch_install()`] : []), + ...(emitter.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []), + ...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []), + ...(usesFetch ? [` call void @scr_fetch_install()`] : []), + ...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []), + ...(usesHttp ? [` call void @scr_http_dyn_install()`] : []), + ...(usesDgram ? [` call void @scr_dgram_install()`] : []), + ...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []), + ...(usesStream ? [` call void @scr_stream_install()`] : []), + ` call void @scr_lib_init(i32 %argc, ptr %argv)`, + // node:test takes Node's --test-update-snapshots out of argv first. + ...(usesNodeTest ? [` call void @scr_test_init()`] : []), + ...(asyncEntry + ? [` %top = call ptr @${mangleAsyncSpawn(emitter.mod.entry)}()`] + : [` call void @${mangleFunction(emitter.mod.entry)}()`]), + // Uncaught exception from top-level code: Node exits 1. + ...(entryMayThrow && !asyncEntry + ? [ + ` %exc = call zeroext i1 @scr_exc_pending()`, + ` br i1 %exc, label %uncaught, label %ok`, + `uncaught:`, + ` %handled = call zeroext i1 @scr_exc_handle_uncaught(i1 false)`, + ` br i1 %handled, label %ok, label %fatal`, + `fatal:`, + ` call void @scr_exc_print_uncaught()`, + ...exitListenerLines("xu"), + ...uncaughtReleases, + ` %failure_code_0 = call i32 @scr_exit_code_hint_get()`, + ` ret i32 %failure_code_0`, + `ok:`, + ] + : []), + // The event loop runs to exhaustion (microtasks before timers). A + // throw escaping a timer callback and unhandled promise rejections + // both exit 1, like Node — the C main's loop block exactly. + ...(runsLoop + ? [ + ...(emitter.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []), + ` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`, + ` %lexc = call zeroext i1 @scr_exc_pending()`, + ` br i1 %lexc, label %luncaught, label %lok`, + `luncaught:`, + ` call void @scr_exc_print_uncaught()`, + ...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []), + ...exitListenerLines("xl"), + ...loopReleasesU, + ` %failure_code_1 = call i32 @scr_exit_code_hint_get()`, + ` ret i32 %failure_code_1`, + `lok:`, + ` br i1 %loop_rejection, label %lreported, label %lclean`, + `lreported:`, + ` call void @scr_discard_unhandled_rejections()`, + ...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []), + ...exitListenerLines("xq"), + ...loopReportedReleases, + ` %failure_code_2 = call i32 @scr_exit_code_hint_get()`, + ` ret i32 %failure_code_2`, + `lclean:`, + ...(asyncEntry + ? [ + ` %tla_status = call i32 @scr_promise_finish_top_level(ptr %top)`, + ` %tla_rejected = icmp eq i32 %tla_status, 1`, + ` br i1 %tla_rejected, label %tla_fail, label %tla_not_rejected`, + `tla_fail:`, + // The loop already delivered every earlier-checkpoint + // rejection. Drop same-checkpoint competitors before + // surfacing the fatal module verdict. + ` call void @scr_discard_unhandled_rejections()`, + ` call void @scr_promise_rethrow_top_level(ptr %top)`, + ` call void @scr_promise_release(ptr %top)`, + ` call void @scr_exc_print_uncaught()`, + ...exitListenerLines("xt"), + ...topRejectReleases, + ` %failure_code_3 = call i32 @scr_exit_code_hint_get()`, + ` ret i32 %failure_code_3`, + `tla_not_rejected:`, + ` call void @scr_promise_release(ptr %top)`, + ] + : []), + ` %rej = call zeroext i1 @scr_report_unhandled_rejections()`, + ` br i1 %rej, label %lrej, label %lrok`, + `lrej:`, + ...exitListenerLines("xr"), + ...loopReleasesR, + ` %failure_code_4 = call i32 @scr_exit_code_hint_get()`, + ` ret i32 %failure_code_4`, + `lrok:`, + ...(asyncEntry + ? [ + ` %tla_pending = icmp eq i32 %tla_status, 13`, + ` br i1 %tla_pending, label %tla_stuck, label %tla_ok`, + `tla_stuck:`, + ...topPendingExitLines(), + `tla_ok:`, + ] + : []), + ] + : []), + ...exitListenerLines("xn"), + ...globalReleases, + ...(usesNodeTest + ? [` %test_exit = call i32 @scr_test_exit_code()`, ` ret i32 %test_exit`] + : [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]), + `}`, + ``, + ...emitter.inlineRcTail(flushedDecls), + // sanitize_address is inert under the plain pipeline; the sanitized + // lane's -fsanitize=address link activates instrumentation over the + // emitted functions too (the runtime TUs get theirs from clang). + `attributes #0 = { sanitize_address }`, + ...(emitter.usesCoroutines ? [`attributes #1 = { sanitize_address presplitcoroutine }`] : []), + ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []), + ``, + ); + if (emitter.mod.workers) { + out.push( + `declare void @scr_context_cleanup()`, + `declare i32 @scr_worker_argc()`, + `declare ptr @scr_worker_argv()`, + `define i32 @sc_worker_entry(i32 %root) ${FN_ATTRS} {`, + `entry:`, + ` %argc = call i32 @scr_worker_argc()`, + ` %argv = call ptr @scr_worker_argv()`, + ` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`, + ` ret i32 %result`, + `}`, + `define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`, + `entry:`, + ` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`, + ` call void @scr_context_cleanup()`, + ` ret i32 %result`, + `}`, + ); + } + out.push(...emitter.fieldAliasMetadata()); + if (emitter.debug !== null) out.push(emitter.debug.render()); + return out; +} + +/** The shared abort helpers (emitted only when referenced): the OOM + * abort of untraced shape allocation and the invalid-union-tag abort — + * both print the runtime ABI’s exact message on fd 2 and abort. */ +export function helperDefs(emitter: LlEmitter): string[] { + const defs: string[] = []; + const msgHelper = (fnName: string, msgSym: string, msg: string): void => { + // The message routes through the runtime's trap funnel: executable + // builds expand to the historical bytes-on-stderr + abort; library + // builds route to the registered panic sink (scr_runtime.h). + const bytes = Buffer.byteLength(msg, "utf8"); + emitter.declare(`declare void @scr_trap(ptr)`); + defs.push( + `@${msgSym} = internal constant [${bytes + 1} x i8] c"${llStrBytes(msg)}"`, + `define internal void @${fnName}() ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_trap(ptr @${msgSym})`, + ` unreachable`, + `}`, + ``, + ); + }; + if (emitter.needsOom) msgHelper("sc_oom", "sc_oom_msg", "scriptc: out of memory\n"); + if (emitter.needsBadTag) { + msgHelper("sc_bad_tag", "sc_bad_tag_msg", "scriptc: internal error: invalid union tag\n"); + } + if (emitter.needsBadKey) { + // The keyed-read miss on a result type that cannot say `undefined`: + // trap like an array OOB read instead of corrupting a typed slot + // (SEMANTICS.md; the C helper's message additionally interpolates + // the runtime key — a trap-path debugging nicety, never reachable + // by a program whose behavior matches Node). + msgHelper( + "sc_bad_key", + "sc_bad_key_msg", + "scriptc: TypeError: record has no key (typed slot — no undefined is representable)\n", + ); + } + if (emitter.needsRetainBox) { + // scr_box_retain is a static inline (increment-unless-immortal); + // emitted once with internal linkage, like the record retains. + defs.push( + `define internal ptr @sc_retain_box(ptr %b) ${FN_ATTRS} {`, + `entry:`, + ` %rc = load ${emitter.sizeType}, ptr %b`, + ` %imm = icmp eq ${emitter.sizeType} %rc, -1`, + ` br i1 %imm, label %done, label %inc`, + `inc:`, + ` %n = add ${emitter.sizeType} %rc, 1`, + ` store ${emitter.sizeType} %n, ptr %b`, + ` br label %done`, + `done:`, + ` ret ptr %b`, + `}`, + ``, + ); + } + // Number-key Map/Set lookups probe the runtime's direct index inline: + // while ndense is nonzero every live key is an integer below it, so an + // in-range integral key reads its slot (entry index + 1, 0 = absent) and + // any other key is absent. Maps without the index (ndense 0) hash the + // key in sc_map_entry_f64_hashed, which scans the entries of a small map + // or probes a compact bucket table as scr_map_find_f64 does, and calls + // the runtime lookup for a wide table. It stays out of line so that the + // direct-index path stays small at every call site. Both only read + // memory, so LLVM merges lookups of one key when nothing is stored in + // between (a has() followed by get(), for example). + if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL)) { + const sz = emitter.sizeType; + let mapTmp = 0; + const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`); + defs.push( + `define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`, + `entry:`, + ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, + ...(sz === "i64" + ? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`] + : [` %nd = load i32, ptr %ndp`]), + // A non-integral key inside the table is as absent as one outside it. + ...mapIndex.lines.map((line) => ` ${line}`), + ` br i1 ${mapIndex.ok}, label %slot, label %outside`, + `slot:`, + sz === "i64" + ? ` %i = add i64 ${mapIndex.wide}, 0` + : ` %i = trunc i64 ${mapIndex.wide} to ${sz}`, + ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`, + ` %dense = load ptr, ptr %dp`, + ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, + ` %s = load i32, ptr %sp`, + ` %empty = icmp eq i32 %s, 0`, + ` br i1 %empty, label %miss, label %hit`, + `hit:`, + ` %e32 = sub i32 %s, 1`, + sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`, + ` ret ptr %found`, + `miss:`, + ` ret ptr null`, + `outside:`, + ` %dense.any = icmp ne ${sz} %nd, 0`, + ` br i1 %dense.any, label %miss, label %slow`, + `slow:`, + ` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`, + ` ret ptr %hashed`, + `}`, + ``, + // Hashing (scr_map_find_f64): the SameValueZero key word, its + // scr_key_mix hash (zero remapped to one) and the absent-key filter. + `define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`, + `entry:`, + ` %nan = fcmp uno double %k, %k`, + ` %zero = fcmp oeq double %k, 0.0`, + ` %kz = select i1 %zero, double 0.0, double %k`, + ` %kzb = bitcast double %kz to i64`, + ` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`, + ` %x1s = lshr i64 %kb, 30`, + ` %x1 = xor i64 %kb, %x1s`, + ` %x2 = mul i64 %x1, -4658895280553007687`, + ` %x3s = lshr i64 %x2, 27`, + ` %x3 = xor i64 %x2, %x3s`, + ` %x4 = mul i64 %x3, -7723592293110705685`, + ` %x5s = lshr i64 %x4, 31`, + ` %x5 = xor i64 %x4, %x5s`, + ` %hz = icmp eq i64 %x5, 0`, + ` %h = select i1 %hz, i64 1, i64 %x5`, + ` %f1s = lshr i64 %h, 44`, + ` %f1a = and i64 %f1s, 63`, + ` %f1 = shl i64 1, %f1a`, + ` %f2s = lshr i64 %h, 50`, + ` %f2a = and i64 %f2s, 63`, + ` %f2 = shl i64 1, %f2a`, + ` %fb = or i64 %f1, %f2`, + ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, + ` %filter = load i64, ptr %fp`, + ` %fm = and i64 %filter, %fb`, + ` %fin = icmp eq i64 %fm, %fb`, + ` br i1 %fin, label %table, label %miss`, + `table:`, + ` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`, + ...(sz === "i64" + ? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`] + : [` %nb = load i32, ptr %nbp`]), + ` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %ents = load ptr, ptr %eps`, + ` %linear = icmp eq ${sz} %nb, 0`, + ` br i1 %linear, label %scan, label %buckets`, + // Small maps keep no bucket table: scan the cached entry hashes. + `scan:`, + ` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ...(sz === "i64" + ? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`] + : [` %ne = load i32, ptr %nep`]), + ` br label %scan.loop`, + `scan.loop:`, + ` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`, + ` %sdone = icmp eq ${sz} %si, %ne`, + ` br i1 %sdone, label %miss, label %scan.entry`, + `scan.entry:`, + ` %se = getelementptr inbounds %ScrMapEntry, ptr %ents, ${sz} %si`, + ` %seh = getelementptr inbounds %ScrMapEntry, ptr %se, i32 0, i32 2`, + ` %sh = load i64, ptr %seh`, + ` %sk = load i64, ptr %se`, + ` %shq = icmp eq i64 %sh, %h`, + ` %skq = icmp eq i64 %sk, %kb`, + ` %sq = and i1 %shq, %skq`, + ` br i1 %sq, label %scan.found, label %scan.next`, + `scan.found:`, + ` ret ptr %se`, + `scan.next:`, + ` %si.next = add ${sz} %si, 1`, + ` br label %scan.loop`, + // Compact tables tag each 24-bit entry index (+1) with the hash's + // top byte; wider tables take the runtime lookup. + `buckets:`, + ` %wide = icmp ugt ${sz} %nb, 16777216`, + ` br i1 %wide, label %runtime, label %probe`, + `probe:`, + ` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`, + ` %bk = load ptr, ptr %bp`, + ` %mask = sub ${sz} %nb, 1`, + ` %ht = lshr i64 %h, 32`, + ` %ht32 = trunc i64 %ht to i32`, + ` %tag = and i32 %ht32, -16777216`, + sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`, + ` %i0 = and ${sz} %hi, %mask`, + ` br label %probe.loop`, + `probe.loop:`, + ` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`, + ` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`, + ` %b = load i32, ptr %bep`, + ` %bempty = icmp eq i32 %b, 0`, + ` br i1 %bempty, label %miss, label %probe.tag`, + `probe.tag:`, + ` %bt = and i32 %b, -16777216`, + ` %btq = icmp eq i32 %bt, %tag`, + ` br i1 %btq, label %probe.entry, label %probe.next`, + `probe.entry:`, + ` %bix = and i32 %b, 16777215`, + ` %bix1 = sub i32 %bix, 1`, + sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`, + ` %pe = getelementptr inbounds %ScrMapEntry, ptr %ents, ${sz} %bi`, + ` %peh = getelementptr inbounds %ScrMapEntry, ptr %pe, i32 0, i32 2`, + ` %ph = load i64, ptr %peh`, + ` %pk = load i64, ptr %pe`, + ` %phq = icmp eq i64 %ph, %h`, + ` %pkq = icmp eq i64 %pk, %kb`, + ` %pq = and i1 %phq, %pkq`, + ` br i1 %pq, label %probe.found, label %probe.next`, + `probe.found:`, + ` ret ptr %pe`, + `probe.next:`, + ` %pi.inc = add ${sz} %pi, 1`, + ` %pi.next = and ${sz} %pi.inc, %mask`, + ` br label %probe.loop`, + `miss:`, + ` ret ptr null`, + `runtime:`, + ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`, + ` ret ptr %r`, + `}`, + ``, + ); + } + // Map/Set iteration steps read the entries array inline: an index is + // live when it is below the entry count and its entry's cached hash is + // nonzero (a tombstone's is zero), as scr_map_iter_live decides. The + // entry count is read again at every step, so entries appended during + // the iteration are visited. + if (emitter.decls.has(MAP_ITER_DECL)) { + const sz = emitter.sizeType; + defs.push( + `define internal ptr @sc_map_iter_entry(ptr %m, double %i) ${FN_ATTRS} {`, + `entry:`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` %lo = fcmp oge double %i, 0.0`, + ` %hi = fcmp olt double %i, %nf`, + ` %in = and i1 %lo, %hi`, + ` br i1 %in, label %load, label %none`, + `load:`, + ` %ix = fptoui double %i to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %e = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %ix`, + ` %hp = getelementptr inbounds %ScrMapEntry, ptr %e, i32 0, i32 2`, + ` %h = load i64, ptr %hp`, + ` %live = icmp ne i64 %h, 0`, + ` br i1 %live, label %yes, label %none`, + `yes:`, + ` ret ptr %e`, + `none:`, + ` ret ptr null`, + `}`, + ``, + `define internal double @sc_map_iter_count(ptr %m) ${FN_ATTRS} {`, + `entry:`, + ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ` %n = load i32, ptr %np`, + ` %nf = uitofp i32 %n to double`, + ` ret double %nf`, + `}`, + ``, + ); + } + // String-key Map/Set lookups test the map's absent-key filter inline + // (scr_map.c scr_map_filter_bits): a key whose two filter bits are not + // both set is absent, so most misses on small and medium maps return + // without a call. On 64-bit targets the key's cached hash h is the high + // half of its capacity word, the runtime's widened hash is (h << 32) | h, + // and the filter reads its bits 44..55, which are the capacity word's + // own. A key without a cached hash, and every possible hit, take the + // runtime lookup. + if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) { + defs.push( + `define internal ptr @sc_map_entry_str(ptr %m, ptr %k) ${FN_ATTRS} {`, + `entry:`, + ...(emitter.sizeType === "i64" + ? [ + ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, + ` %filter = load i64, ptr %fp`, + ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, + ` %cap = load i64, ptr %capp`, + ` %cached = icmp uge i64 %cap, 4294967296`, + ` br i1 %cached, label %test, label %slow`, + `test:`, + ` %s1 = lshr i64 %cap, 44`, + ` %i1 = and i64 %s1, 63`, + ` %b1 = shl i64 1, %i1`, + ` %s2 = lshr i64 %cap, 50`, + ` %i2 = and i64 %s2, 63`, + ` %b2 = shl i64 1, %i2`, + ` %bits = or i64 %b1, %b2`, + ` %have = and i64 %filter, %bits`, + ` %maybe = icmp eq i64 %have, %bits`, + ` br i1 %maybe, label %slow, label %absent`, + `absent:`, + ` ret ptr null`, + ] + : [` br label %slow`]), + `slow:`, + ` %r = call ptr @scr_map_entry_str(ptr %m, ptr %k)`, + ` ret ptr %r`, + `}`, + ``, + ); + } + // String comparisons enter through small internal wrappers that LLVM + // inlines at each site: interned literals and shared values compare by + // identity, and equality rejects different byte lengths, before any + // runtime call (scr_str_eq/scr_str_cmp keep the complete semantics). + if (emitter.decls.has(`declare zeroext i1 @scr_str_eq(ptr, ptr)`)) { + defs.push( + `define internal zeroext i1 @sc_str_eq(ptr %a, ptr %b) ${FN_ATTRS} {`, + `entry:`, + ` %same = icmp eq ptr %a, %b`, + ` br i1 %same, label %yes, label %lengths`, + `lengths:`, + ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, + ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, + ` %alen = load ${emitter.sizeType}, ptr %alen.p`, + ` %blen = load ${emitter.sizeType}, ptr %blen.p`, + ` %samelen = icmp eq ${emitter.sizeType} %alen, %blen`, + ` br i1 %samelen, label %bytes, label %no`, + `bytes:`, + ` %eq = call zeroext i1 @scr_str_eq(ptr %a, ptr %b)`, + ` ret i1 %eq`, + `yes:`, + ` ret i1 true`, + `no:`, + ` ret i1 false`, + `}`, + ``, + ); + } + for (const [text, name] of emitter.literalEqualities) { + defs.push( + ...literalEqualityDefinition( + name, + wtf8Bytes(text), + emitter.internLiteral(text), + emitter.sizeType, + FN_ATTRS, + ), + ``, + ); + } + // String length and charCodeAt read proven-ASCII strings inline: bit 31 + // of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on + // little-endian targets) makes UTF-16 units bytes. Unproven strings and + // anything but an in-range index take the runtime's UTF-16 mapping. + // A scan's receiver reads inline too where runtime state is ordinary + // global storage (ScrStrScan; SCR_TL makes it thread-local otherwise). + const scanInline = emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true; + let scanDeclared = false; + for (const helper of STRING_READ_HELPERS) { + if (!emitter.decls.has(helper.decl)) continue; + if (scanInline && !scanDeclared) { + defs.push(stringScanDecl(emitter.sizeType), ``); + scanDeclared = true; + } + defs.push(...helper.define(emitter.sizeType, FN_ATTRS, scanInline), ``); + } + // Ordering decides inline from the first differing bytes. The bytes + // before them are equal, so both strings are at the same position of a + // character: either both bytes continue characters with the same lead + // byte, or both start a character. Byte order is the UTF-16 order for + // two continuation bytes, and for two lead bytes when one starts a + // character below U+D000, whose UTF-16 unit is below every unit of a + // character with a lead byte of at least 0xED. Between lead bytes of at + // least 0xED, a supplementary character (0xF0..0xF4, a high surrogate + // unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of + // 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime. + // Differing words without a byte of at least 0xED decide in byte + // order; other differing words decide by their first differing byte, + // and strings shorter than a word by their first byte. Otherwise a + // proven-ASCII string on either side orders by bytes (scr_str_cmp), and + // other pairs call scr_str_cmp_u16_bytes, which only reads the two + // strings, so LLVM can merge repeated comparisons of one pair. Callers + // only test the sign of the result. + if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { + const fn = "scr_str_cmp_u16"; + emitter.declare("declare i64 @llvm.bswap.i64(i64)"); + emitter.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)"); + emitter.declare("declare i8 @llvm.umin.i8(i8, i8)"); + emitter.declare("declare i32 @scr_str_cmp(ptr, ptr)"); + emitter.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)"); + defs.push( + `define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`, + `entry:`, + ` %same = icmp eq ptr %a, %b`, + ` br i1 %same, label %equal, label %lengths`, + `lengths:`, + ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, + ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, + ` %alen = load ${emitter.sizeType}, ptr %alen.p`, + ` %blen = load ${emitter.sizeType}, ptr %blen.p`, + ` %aempty = icmp eq ${emitter.sizeType} %alen, 0`, + ` %bempty = icmp eq ${emitter.sizeType} %blen, 0`, + ` %empty = or i1 %aempty, %bempty`, + ` br i1 %empty, label %order, label %words`, + // Both at least 8 bytes: the first word of each. + `words:`, + ` %along = icmp uge ${emitter.sizeType} %alen, 8`, + ` %blong = icmp uge ${emitter.sizeType} %blen, 8`, + ` %long = and i1 %along, %blong`, + ` br i1 %long, label %word, label %first`, + `word:`, + ` %awp = getelementptr inbounds %ScrStr, ptr %a, i32 1`, + ` %bwp = getelementptr inbounds %ScrStr, ptr %b, i32 1`, + ` %aw = load i64, ptr %awp, align 1`, + ` %bw = load i64, ptr %bwp, align 1`, + ` %wdiffer = icmp ne i64 %aw, %bw`, + ` br i1 %wdiffer, label %differ, label %wsame`, + // Equal first words and one length of at most 16 bytes: the word + // ending at that length covers the rest, and any difference lies + // past the equal first word. + `wsame:`, + ` %samelen = icmp eq ${emitter.sizeType} %alen, %blen`, + ` %short = icmp ule ${emitter.sizeType} %alen, 16`, + ` %tailable = and i1 %samelen, %short`, + ` br i1 %tailable, label %tail, label %order`, + `tail:`, + ` %toff = sub ${emitter.sizeType} %alen, 8`, + ` %atp = getelementptr inbounds i8, ptr %awp, ${emitter.sizeType} %toff`, + ` %btp = getelementptr inbounds i8, ptr %bwp, ${emitter.sizeType} %toff`, + ` %at = load i64, ptr %atp, align 1`, + ` %bt = load i64, ptr %btp, align 1`, + ` %tdiffer = icmp ne i64 %at, %bt`, + ` br i1 %tdiffer, label %differ, label %equal`, + `first:`, + ` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`, + ` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`, + ` %x = load i8, ptr %adata`, + ` %y = load i8, ptr %bdata`, + ` %fdiffer = icmp ne i8 %x, %y`, + ` br i1 %fdiffer, label %bytes, label %order`, + // Two differing words. When neither has a byte of at least 0xED + // (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are + // set), byte-swapped little-endian words compare in byte order. + // Otherwise the lowest set byte of their xor is the first differing + // byte. + `differ:`, + ` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`, + ` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`, + ` %xl = and i64 %dx, 9187201950435737471`, + ` %yl = and i64 %dy, 9187201950435737471`, + ` %xa = add i64 %xl, 1374463283923456787`, + ` %ya = add i64 %yl, 1374463283923456787`, + ` %xh = and i64 %dx, %xa`, + ` %yh = and i64 %dy, %ya`, + ` %hh = or i64 %xh, %yh`, + ` %whigh = and i64 %hh, -9187201950435737472`, + ` %dlow = icmp eq i64 %whigh, 0`, + ` br i1 %dlow, label %wordorder, label %locate`, + `wordorder:`, + ` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`, + ` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`, + ` %wbelow = icmp ult i64 %xsw, %ysw`, + ` %wsign = select i1 %wbelow, i32 -1, i32 1`, + ` ret i32 %wsign`, + `locate:`, + ` %dxor = xor i64 %dx, %dy`, + ` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`, + ` %dshift = and i64 %dtz, 56`, + ` %dxs = lshr i64 %dx, %dshift`, + ` %dys = lshr i64 %dy, %dshift`, + ` %dxb = trunc i64 %dxs to i8`, + ` %dyb = trunc i64 %dys to i8`, + ` br label %bytes`, + // The first differing bytes. + `bytes:`, + ` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`, + ` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`, + ` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`, + ` %blow = icmp ult i8 %bmin, -19`, + ` br i1 %blow, label %byte, label %high`, + // Both at least 0xED. A lone surrogate (0xED) takes the runtime; + // supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF + // once 0x10 is subtracted from them. + `high:`, + ` %xed = icmp eq i8 %bx, -19`, + ` %yed = icmp eq i8 %by, -19`, + ` %ed = or i1 %xed, %yed`, + ` br i1 %ed, label %order, label %planes`, + `planes:`, + ` %xsupp = icmp uge i8 %bx, -16`, + ` %ysupp = icmp uge i8 %by, -16`, + ` %xlow = add i8 %bx, -16`, + ` %ylow = add i8 %by, -16`, + ` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`, + ` %yk = select i1 %ysupp, i8 %ylow, i8 %by`, + ` %kbelow = icmp ult i8 %xk, %yk`, + ` %ksign = select i1 %kbelow, i32 -1, i32 1`, + ` ret i32 %ksign`, + `byte:`, + ` %below = icmp ult i8 %bx, %by`, + ` %sign = select i1 %below, i32 -1, i32 1`, + ` ret i32 %sign`, + `order:`, + ` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`, + ` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`, + ` %acw = load i32, ptr %acw.p`, + ` %bcw = load i32, ptr %bcw.p`, + ` %cws = or i32 %acw, %bcw`, + ` %anyascii = icmp slt i32 %cws, 0`, + ` br i1 %anyascii, label %memorder, label %units`, + `memorder:`, + ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, + ` ret i32 %m`, + `units:`, + ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, + ` ret i32 %c`, + `equal:`, + ` ret i32 0`, + `}`, + ``, + ); + } + // The declarations these helpers added must land in the extern block, + // which already flushed — append here instead (LLVM is order-free). + return defs.length > 0 ? [...defs] : defs; +} + +/** LIBRARY mode: the profile-declared external definitions — the + * export-map wrappers plus init / sink-registration / reset / collect. + * Plain `define` (not `define internal`) — the exact linkage distinction + * that separates the executable lane's @main from everything else. The + * bodies delegate every runtime half to scr_library.c, mirroring the C + * emission line for line, so the two lanes are identical by + * construction. */ +export function emitLibDefs( + emitter: LlEmitter, + globals: IrGlobal[], + globalReleaseLines: (prefix: string) => string[], + stamps: string[], +): string[] { + const lib = emitter.mod.lib!; + const autoReset = lib.resultResetSymbol === null; + const out: string[] = [``, `; ── library-mode entries (profile: ${lib.profileName}) ──`, ``]; + // Every entry's prologue records its external symbol in the funnel's + // current-entry slot (structured trap-teaching field 2); the symbols + // live as internal constants, one per entry. + const symConst = (sym: string): string => `@sc_lib_sym_${sym}`; + const emitSymConst = (sym: string): void => { + out.push( + `${symConst(sym)} = internal constant [${Buffer.byteLength(sym, "utf8") + 1} x i8] c"${llStrBytes(sym)}"`, + ); + }; + emitSymConst(lib.initSymbol); + emitSymConst(lib.sinkRegisterSymbol); + if (lib.callbackRegisterSymbol !== null && lib.callbackRegisterSymbol !== undefined) + emitSymConst(lib.callbackRegisterSymbol); + if (lib.resultResetSymbol !== null) emitSymConst(lib.resultResetSymbol); + if (lib.collectSymbol !== null) emitSymConst(lib.collectSymbol); + for (const e of lib.exports) emitSymConst(e.symbol); + out.push(``); + if (emitter.wasi) { + for (const symbol of ["scriptc_alloc", "scriptc_free"]) emitSymConst(symbol); + out.push( + `define ptr @scriptc_alloc(i32 %size) ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_alloc")})`, + ` %p = call ptr @malloc(i32 %size)`, + ` ret ptr %p`, + `}`, + ``, + `define void @scriptc_free(ptr %p) ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_free")})`, + ` call void @free(ptr %p)`, + ` ret void`, + `}`, + ``, + ); + } + // The runtime detected-trap overlay table (scr_runtime.h declares it, + // the library trap funnel consults it): flat code/teaching/remediation + // triples, one per runtime trap code (SC4013–SC4019) the profile + // declares text for. The funnel assembles the structured sink message. + // The empty table still defines the symbols the funnel links against. + const ovlCells: string[] = []; + lib.trapOverlays.forEach((o, i) => { + const cell = (name: string, text: string | undefined): void => { + if (text === undefined) { + ovlCells.push("ptr null"); + return; + } + const sym = `@sc_lib_ovl_${i}_${name}`; + out.push( + `${sym} = internal constant [${Buffer.byteLength(text, "utf8") + 1} x i8] c"${llStrBytes(text)}"`, + ); + ovlCells.push(`ptr ${sym}`); + }; + cell("code", o.code); + cell("teach", o.teaching); + cell("rem", o.remediation); + }); + out.push( + ovlCells.length === 0 + ? `@scr_library_trap_overlays = constant [1 x ptr] zeroinitializer` + : `@scr_library_trap_overlays = constant [${ovlCells.length} x ptr] [${ovlCells.join(", ")}]`, + `@scr_library_trap_overlays_len = constant ${emitter.sizeType} ${lib.trapOverlays.length}`, + ``, + ); + // The init entry: full deterministic reset-and-reevaluate. Program + // globals release and zero first (run-once guards included), then the + // runtime session reset, the error-vt interval stamps verbatim from + // the executable main, %main itself, and the escaped-exception check. + const zeroStores = globals.map((g) => { + const ty = emitter.llType(g.type); + const zero = ty === "double" ? f64Lit(0) : ty === "ptr" ? "null" : "false"; + return ` store ${ty} ${zero}, ptr @${mangleGlobal(g.id)} ; ${g.name}`; + }); + out.push( + `define void @${lib.initSymbol}() ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_entry(i1 zeroext true, ptr ${symConst(lib.initSymbol)}) ; init always resets the result arena`, + ...globalReleaseLines("ci"), + ...zeroStores, + ` call void @scr_library_reset()`, + ...stamps, + ` call void @${mangleFunction(emitter.mod.entry)}()`, + ` call void @scr_library_check_exc()`, + ` ret void`, + `}`, + ``, + `define void @${lib.sinkRegisterSymbol}(ptr %fn, ptr %ctx) ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.sinkRegisterSymbol)})`, + ` call void @scr_library_set_sink(ptr %fn, ptr %ctx)`, + ` ret void`, + `}`, + ``, + ); + if (lib.callbacks !== undefined && lib.callbacks.length > 0) { + // Host-callback channels: the per-channel name constants (the + // registration dispatch's strcmp operands), the per-channel + // unregistered-call trap constants (the ffiCall sites' + // scr_library_cb_require operands), and the registration function: + // a pure store dispatch + // (the sink registration's rule — no entry prologue, no poison + // guard) whose first operation rejects callback-time re-entry + // (SC4026). An unknown or NULL name is a defined -1, never a store. + for (const cb of lib.callbacks) { + out.push( + `@sc_lib_cb_name_${cb.slot} = internal constant [${Buffer.byteLength(cb.name, "utf8") + 1} x i8] c"${llStrBytes(cb.name)}"`, + `@sc_lib_cb_trap_${cb.slot} = internal constant [${Buffer.byteLength(cb.unregisteredTrap, "utf8") + 1} x i8] c"${llStrBytes(cb.unregisteredTrap)}"`, + ); + } + out.push( + ``, + `define i32 @${lib.callbackRegisterSymbol}(ptr %name, ptr %fn, ptr %ctx) ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.callbackRegisterSymbol!)})`, + ` %isnull = icmp eq ptr %name, null`, + ` br i1 %isnull, label %miss, label %try0`, + ); + lib.callbacks.forEach((cb, i) => { + const next = i + 1 < lib.callbacks!.length ? `try${i + 1}` : "miss"; + out.push( + `try${i}: ; channel '${cb.name}'`, + ` %cmp${i} = call i32 @strcmp(ptr %name, ptr @sc_lib_cb_name_${cb.slot})`, + ` %eq${i} = icmp eq i32 %cmp${i}, 0`, + ` br i1 %eq${i}, label %set${i}, label %${next}`, + `set${i}:`, + ` call void @scr_library_cb_set(${emitter.sizeType} ${cb.slot}, ptr %fn, ptr %ctx)`, + ` ret i32 0`, + ); + }); + out.push(`miss:`, ` ret i32 -1`, `}`, ``); + } + if (lib.identity !== undefined && emitter.emitLibraryIdentity) { + // Profile-declared identity getters (the ask-2 sidecar's boot-time + // pairing fence): pure data returns with NO entry prologue — exempt + // from the poisoned guard and every runtime touch (ratified), so a + // host can read them before init and after a trap. The u64 rides + // i64 two's-complement (LLVM integer constants are signed). + for (const line of emitLibraryIdentityLines(lib.identity, FN_ATTRS)) out.push(line); + } + if (lib.resultResetSymbol !== null) { + out.push( + `define void @${lib.resultResetSymbol}() ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst(lib.resultResetSymbol)})`, + ` call void @scr_library_arena_reset()`, + ` ret void`, + `}`, + ``, + ); + } + if (lib.collectSymbol !== null) { + out.push( + `define void @${lib.collectSymbol}() ${FN_ATTRS} {`, + `entry:`, + ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst(lib.collectSymbol)})`, + ` call void @scr_library_collect() ; arena reset + a full cycle collection`, + ` ret void`, + `}`, + ``, + ); + } + for (const e of lib.exports) { + const params: string[] = []; + const body: string[] = [ + ` call void @scr_library_entry(i1 zeroext ${autoReset ? "true" : "false"}, ptr ${symConst(e.symbol)})`, + ]; + const args: string[] = []; + if (e.inboundBytesTrap !== undefined) { + // The bytes-in helper's trap message: the compiler-assembled + // structured trap-teaching form (0x01 text 0x1F SC4012 0x1F symbol + // [0x1F remediation]) consumed by the runtime sink. + const trapBytes = Buffer.byteLength(e.inboundBytesTrap, "utf8"); + out.push( + `@sc_lib_bytes_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundBytesTrap)}"`, + ``, + ); + } + if (e.inboundIntTrap !== undefined) { + // The i64/u64-in helpers' host-contract trap message (ask 4): an + // inbound integer past ±(2^53−1) cannot ride f64 exactly. Same + // assembled structured form, same SC4012 code, same + // emission-invariance argument as the bytes trap. + const trapBytes = Buffer.byteLength(e.inboundIntTrap, "utf8"); + out.push( + `@sc_lib_int_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundIntTrap)}"`, + ``, + ); + } + if (e.inboundCstringTrap !== undefined) { + // The cstring-in helper's host-contract trap message (a NULL + // `const char *`): same assembled structured form and SC4012 code. + const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8"); + out.push( + `@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`, + ``, + ); + } + e.params.forEach((cls, i) => { + switch (cls) { + case "f64": + params.push(`double %a${i}`); + args.push(`double %a${i}`); + break; + case "bool": + params.push(`i8 %a${i}`); + body.push(` %c${i} = icmp ne i8 %a${i}, 0`); + args.push(`i1 %c${i}`); + break; + case "u8": + params.push(`i8 %a${i}`); + body.push(` %c${i} = uitofp i8 %a${i} to double`); + args.push(`double %c${i}`); + break; + case "u32": + params.push(`i32 %a${i}`); + body.push(` %c${i} = uitofp i32 %a${i} to double`); + args.push(`double %c${i}`); + break; + case "i32": + params.push(`i32 %a${i}`); + body.push(` %c${i} = sitofp i32 %a${i} to double`); + args.push(`double %c${i}`); + break; + case "i64": + // Inbound declared-integer edge (ask 4): the helper converts + // exactly or delivers the host-contract trap (past ±(2^53−1) + // the value cannot ride f64 without rounding). + params.push(`i64 %a${i}`); + body.push( + ` %c${i} = call double @scr_library_i64_in(i64 %a${i}, ptr @sc_lib_int_trap_${e.symbol})`, + ); + args.push(`double %c${i}`); + break; + case "u64": + params.push(`i64 %a${i}`); + body.push( + ` %c${i} = call double @scr_library_u64_in(i64 %a${i}, ptr @sc_lib_int_trap_${e.symbol})`, + ); + args.push(`double %c${i}`); + break; + case "string": + params.push(`ptr %a${i}_ptr`, `${emitter.sizeType} %a${i}_len`); + body.push( + ` %c${i} = call ptr @scr_library_str_in(ptr %a${i}_ptr, ${emitter.sizeType} %a${i}_len)`, + ); + args.push(`ptr %c${i}`); + break; + case "bytes": + params.push(`ptr %a${i}_ptr`, `${emitter.sizeType} %a${i}_len`); + body.push( + ` %c${i} = call ptr @scr_library_bytes_in(ptr %a${i}_ptr, ${emitter.sizeType} %a${i}_len, ptr @sc_lib_bytes_trap_${e.symbol})`, + ); + args.push(`ptr %c${i}`); + break; + case "cstring": + params.push(`ptr %a${i}`); + body.push( + ` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`, + ); + args.push(`ptr %c${i}`); + break; + } + }); + if (e.returns === "string" || e.returns === "bytes") { + params.push(`ptr %out`, `ptr %out_len`); + } + const target = `@${mangleFunction(e.fnName)}`; + const callArgs = args.join(", "); + let retType = "void"; + switch (e.returns) { + case "void": + body.push( + ` call void ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` ret void`, + ); + break; + case "f64": + retType = "double"; + body.push( + ` %r = call double ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` ret double %r`, + ); + break; + case "i64": + case "u64": + // The outbound declared-integer edge (ask 4): every value + // reaching this return was PROVEN whole and inside the class's + // range at compile time, so the fp-to-int conversion is exact + // by construction — the crossing carries the mathematically + // exact integer the f64 held. + retType = "i64"; + body.push( + ` %r = call double ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` %z = ${e.returns === "i64" ? "fptosi" : "fptoui"} double %r to i64`, + ` ret i64 %z`, + ); + break; + case "bool": + retType = "i8"; + body.push( + ` %r = call i1 ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` %z = zext i1 %r to i8`, + ` ret i8 %z`, + ); + break; + case "string": + body.push( + ` %r = call ptr ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` call void @scr_library_str_out(ptr %r, ptr %out, ptr %out_len)`, + ` ret void`, + ); + break; + case "bytes": + body.push( + ` %r = call ptr ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` call void @scr_library_bytes_out(ptr %r, ptr %out, ptr %out_len)`, + ` ret void`, + ); + break; + case "cstring": + // The result's arena copy, returned as `const char *`. + retType = "ptr"; + body.push( + ` %r = call ptr ${target}(${callArgs})`, + ` call void @scr_library_check_exc()`, + ` %z = call ptr @scr_library_cstr_out(ptr %r)`, + ` ret ptr %z`, + ); + break; + } + out.push( + `define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`, + `entry:`, + ...body, + `}`, + ``, + ); + } + return out; +} + +/** C-callable scalar callback trampolines. A callback with an explicit + * context entry receives the closure at that exact ABI position. A raw + * callback loads it from a call-scoped TLS slot installed around the + * outer native call. */ +export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; defs: string[] } { + const globals: string[] = []; + const defs: string[] = []; + if (emitter.ffiCallbackAdapters.size === 0) return { globals, defs }; + emitter.declare(`declare void @scr_trap(ptr)`); + const expired = emitter.cstr( + "scriptc: native callback invoked outside its call-scoped lifetime\n", + ); + const released = emitter.cstr("scriptc: native callback invoked outside its retained lifetime\n"); + for (const adapter of emitter.ffiCallbackAdapters.values()) { + const cb = adapter.callback; + if (adapter.tls !== null) + globals.push(`@${adapter.tls} = internal thread_local global ptr null`); + if (adapter.global !== null) + globals.push(`@${adapter.global} = internal thread_local global ptr null`); + if (adapter.table !== null) { + globals.push( + `@${adapter.table} = internal ${emitter.mod.workers ? "thread_local " : ""}global %ScrFfiTable zeroinitializer`, + ); + } + const params = cb.params.flatMap((param, i): string[] => { + if (isFfiContextParam(param)) return [`ptr %ctx`]; + if (param === "string" || param === "bytes") { + return [`ptr %a${i}`, `${emitter.sizeType} %a${i}_len`]; + } + return [`${ffiNativeParamLl(param, emitter.ffiExtendNarrowIntegers)} %a${i}`]; + }); + if (cb.invoke === "foreign") { + if ( + adapter.table === null || + !cb.params.some(isFfiContextParam) || + cb.returns !== "void" || + cb.params.some((param) => param === "i64" || param === "u64" || param === "pointer") + ) { + throw new InternalCompilerError( + "llvm emitter bug: invalid foreign FFI callback descriptor", + ); + } + const dispatch = `${adapter.symbol}_dispatch`; + const scriptArgs: string[] = []; + const dispatchBody: string[] = [ + `define internal void @${dispatch}(ptr %cb, ptr %call) ${FN_ATTRS} {`, + `entry:`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ]; + for (let i = 0; i < cb.params.length; i++) { + const param = cb.params[i]!; + if (isFfiContextParam(param)) continue; + switch (param) { + case "f64": + case "f32": + case "i8": + case "u16": + case "i16": + emitter.declare(`declare double @scr_ffi_call_get_f64(ptr, ${emitter.sizeType})`); + dispatchBody.push( + ` %s${i} = call double @scr_ffi_call_get_f64(ptr %call, ${emitter.sizeType} ${i})`, + ); + scriptArgs.push(`double %s${i}`); + break; + case "bool": + emitter.declare(`declare zeroext i1 @scr_ffi_call_get_bool(ptr, ${emitter.sizeType})`); + dispatchBody.push( + ` %s${i} = call zeroext i1 @scr_ffi_call_get_bool(ptr %call, ${emitter.sizeType} ${i})`, + ); + scriptArgs.push(`i1 %s${i}`); + break; + case "u8": + case "u32": + case "i32": + emitter.declare(`declare double @scr_ffi_call_get_${param}(ptr, ${emitter.sizeType})`); + dispatchBody.push( + ` %s${i} = call double @scr_ffi_call_get_${param}(ptr %call, ${emitter.sizeType} ${i})`, + ); + scriptArgs.push(`double %s${i}`); + break; + case "cstring": + case "string": + case "bytes": { + emitter.declare(`declare ptr @scr_ffi_call_get_data(ptr, ${emitter.sizeType})`); + emitter.declare( + `declare ${emitter.sizeType} @scr_ffi_call_get_len(ptr, ${emitter.sizeType})`, + ); + dispatchBody.push( + ` %data${i} = call ptr @scr_ffi_call_get_data(ptr %call, ${emitter.sizeType} ${i})`, + ` %len${i} = call ${emitter.sizeType} @scr_ffi_call_get_len(ptr %call, ${emitter.sizeType} ${i})`, + ); + if (param === "bytes") { + emitter.declare(`declare ptr @scr_bytes_from_data(ptr, ${emitter.sizeType})`); + dispatchBody.push( + ` %s${i} = call ptr @scr_bytes_from_data(ptr %data${i}, ${emitter.sizeType} %len${i})`, + ); + } else { + emitter.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${emitter.sizeType})`); + dispatchBody.push( + ` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %data${i}, ${emitter.sizeType} %len${i})`, + ); + } + scriptArgs.push(`ptr %s${i}`); + break; + } + } + } + dispatchBody.push( + ` call void %fn(${[`ptr %cb`, ...scriptArgs].join(", ")})`, + ` ret void`, + `}`, + ``, + ); + for (const line of dispatchBody) defs.push(line); + + emitter.declare(`declare ptr @scr_ffi_call_new(ptr, ptr, ptr, ${emitter.sizeType})`); + emitter.declare(`declare void @scr_ffi_post(ptr)`); + defs.push( + `define internal void @${adapter.symbol}(${params.join(", ")}) ${FN_ATTRS} {`, + `entry:`, + ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, + ` %call = call ptr @scr_ffi_call_new(ptr @${adapter.table}, ptr %cb, ptr @${dispatch}, ${emitter.sizeType} ${cb.params.length})`, + ); + for (let i = 0; i < cb.params.length; i++) { + const param = cb.params[i]!; + if (isFfiContextParam(param)) continue; + if (param === "cstring") { + emitter.declare(`declare void @scr_ffi_call_copy_cstring(ptr, ${emitter.sizeType}, ptr)`); + defs.push( + ` call void @scr_ffi_call_copy_cstring(ptr %call, ${emitter.sizeType} ${i}, ptr %a${i})`, + ); + } else if (param === "string" || param === "bytes") { + emitter.declare( + `declare void @scr_ffi_call_copy_${param}(ptr, ${emitter.sizeType}, ptr, ${emitter.sizeType})`, + ); + defs.push( + ` call void @scr_ffi_call_copy_${param}(ptr %call, ${emitter.sizeType} ${i}, ptr %a${i}, ${emitter.sizeType} %a${i}_len)`, + ); + } else if (param === "f32" || param === "i8" || param === "u16" || param === "i16") { + const nativeTy = ffiNativeTypeLl(param); + const op = param === "f32" ? "fpext" : param === "u16" ? "uitofp" : "sitofp"; + emitter.declare(`declare void @scr_ffi_call_set_f64(ptr, ${emitter.sizeType}, double)`); + defs.push( + ` %wide${i} = ${op} ${nativeTy} %a${i} to double`, + ` call void @scr_ffi_call_set_f64(ptr %call, ${emitter.sizeType} ${i}, double %wide${i})`, + ); + } else { + const nativeTy = ffiNativeTypeLl(param); + emitter.declare( + `declare void @scr_ffi_call_set_${param}(ptr, ${emitter.sizeType}, ${nativeTy})`, + ); + defs.push( + ` call void @scr_ffi_call_set_${param}(ptr %call, ${emitter.sizeType} ${i}, ${nativeTy} %a${i})`, + ); + } + } + defs.push(` call void @scr_ffi_post(ptr %call)`, ` ret void`, `}`, ``); + continue; + } + defs.push( + `define internal ${ffiNativeReturnLl(cb.returns, emitter.ffiExtendNarrowIntegers)} @${adapter.symbol}(${params.join(", ")}) ${FN_ATTRS} {`, + `entry:`, + adapter.tls !== null + ? ` %cb = load ptr, ptr @${adapter.tls}` + : adapter.global !== null + ? ` %cb = load ptr, ptr @${adapter.global}` + : ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, + ` %missing = icmp eq ptr %cb, null`, + ` br i1 %missing, label %expired, label %ready`, + `expired:`, + ` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`, + ` unreachable`, + `ready:`, + ...emitter.pendingTestLines("%pending"), + ` br i1 %pending, label %skip, label %invoke`, + `skip:`, + ` ret ${ffiCallbackDummyLl(cb)}`, + `invoke:`, + ); + // Validate every native pointer before allocating any copy-in value. + // A later bad slot therefore cannot leak an earlier materialization. + for (let i = 0; i < cb.params.length; i++) { + const param = cb.params[i]!; + if (param !== "cstring" && param !== "string" && param !== "bytes") continue; + const invalid = `%invalid${i}`; + defs.push(` %null${i} = icmp eq ptr %a${i}, null`); + if (param === "cstring") { + defs.push(` ${invalid} = or i1 %null${i}, false`); + } else { + defs.push( + ` %nonempty${i} = icmp ne ${emitter.sizeType} %a${i}_len, 0`, + ` ${invalid} = and i1 %null${i}, %nonempty${i}`, + ); + } + const message = + param === "cstring" + ? "scriptc: native callback passed a NULL cstring\n" + : `scriptc: native callback passed a NULL ${param} span with nonzero length\n`; + defs.push( + ` br i1 ${invalid}, label %invalid_param${i}, label %param_ok${i}`, + `invalid_param${i}:`, + ` call void @scr_trap(ptr ${emitter.cstr(message)})`, + ` unreachable`, + `param_ok${i}:`, + ); + } + defs.push( + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ); + const scriptArgs: string[] = []; + for (let i = 0; i < cb.params.length; i++) { + const param = cb.params[i]!; + if (isFfiContextParam(param)) continue; + switch (param) { + case "i64": + case "u64": + case "pointer": { + const ty = ffiNativeTypeLl(param); + emitter.declare(`declare ptr @scr_bigint_from_${param}(${ty})`); + defs.push(` %s${i} = call ptr @scr_bigint_from_${param}(${ty} %a${i})`); + scriptArgs.push(`ptr %s${i}`); + break; + } + case "f64": + scriptArgs.push(`double %a${i}`); + break; + case "f32": + case "i8": + case "u16": + case "i16": { + const nativeTy = ffiNativeTypeLl(param); + const op = param === "f32" ? "fpext" : param === "u16" ? "uitofp" : "sitofp"; + defs.push(` %s${i} = ${op} ${nativeTy} %a${i} to double`); + scriptArgs.push(`double %s${i}`); + break; + } + case "bool": + defs.push(` %s${i} = icmp ne i8 %a${i}, 0`); + scriptArgs.push(`i1 %s${i}`); + break; + case "u8": + defs.push(` %s${i} = uitofp i8 %a${i} to double`); + scriptArgs.push(`double %s${i}`); + break; + case "u32": + defs.push(` %s${i} = uitofp i32 %a${i} to double`); + scriptArgs.push(`double %s${i}`); + break; + case "i32": + defs.push(` %s${i} = sitofp i32 %a${i} to double`); + scriptArgs.push(`double %s${i}`); + break; + case "cstring": + emitter.declare(`declare ${emitter.sizeType} @strlen(ptr)`); + emitter.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${emitter.sizeType})`); + defs.push( + ` %len${i} = call ${emitter.sizeType} @strlen(ptr %a${i})`, + ` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %a${i}, ${emitter.sizeType} %len${i})`, + ); + scriptArgs.push(`ptr %s${i}`); + break; + case "string": + emitter.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${emitter.sizeType})`); + defs.push( + ` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %a${i}, ${emitter.sizeType} %a${i}_len)`, + ); + scriptArgs.push(`ptr %s${i}`); + break; + case "bytes": + emitter.declare(`declare ptr @scr_bytes_from_data(ptr, ${emitter.sizeType})`); + defs.push( + ` %s${i} = call ptr @scr_bytes_from_data(ptr %a${i}, ${emitter.sizeType} %a${i}_len)`, + ); + scriptArgs.push(`ptr %s${i}`); + break; + } + } + const ft = ffiCallbackType(cb); + const internalRet = emitter.llType(ft.ret); + const callArgs = [`ptr %cb`, ...scriptArgs].join(", "); + if (cb.lifetime === "retained") { + emitter.declare(`declare ptr @scr_closure_retain_v(ptr)`); + emitter.declare(`declare void @scr_closure_release_v(ptr)`); + defs.push(` %invoke_pin = call ptr @scr_closure_retain_v(ptr %cb)`); + } + if (cb.returns === "void") { + defs.push( + ` call void %fn(${callArgs})`, + ...(cb.lifetime === "retained" + ? [` call void @scr_closure_release_v(ptr %invoke_pin)`] + : []), + ` ret void`, + `}`, + ``, + ); + continue; + } + defs.push(` %result = call ${internalRet} %fn(${callArgs})`); + if (cb.lifetime === "retained") { + defs.push(` call void @scr_closure_release_v(ptr %invoke_pin)`); + } + switch (cb.returns) { + case "i64": + case "u64": + case "pointer": { + const ty = ffiNativeTypeLl(cb.returns); + emitter.declare(`declare ${ty} @scr_bigint_to_${cb.returns}(ptr)`); + emitter.declare(`declare void @scr_bigint_release(ptr)`); + defs.push( + ` %out = call ${ty} @scr_bigint_to_${cb.returns}(ptr %result)`, + ` call void @scr_bigint_release(ptr %result)`, + ` ret ${ty} %out`, + ); + break; + } + case "f64": + defs.push(` ret double %result`); + break; + case "f32": + defs.push(` %out = fptrunc double %result to float`, ` ret float %out`); + break; + case "bool": + defs.push(` %out = zext i1 %result to i8`, ` ret i8 %out`); + break; + case "u8": + case "i8": + case "u16": + case "i16": + case "u32": + emitter.declare(`declare double @scr_bit_ushr(double, double)`); + defs.push( + ` %coerced = call double @scr_bit_ushr(double %result, double ${f64Lit(0)})`, + ` %wide = fptoui double %coerced to i32`, + ); + if (cb.returns !== "u32") { + const nativeTy = ffiNativeTypeLl(cb.returns); + defs.push(` %out = trunc i32 %wide to ${nativeTy}`, ` ret ${nativeTy} %out`); + } else { + defs.push(` ret i32 %wide`); + } + break; + case "i32": + emitter.declare(`declare double @scr_bit_or(double, double)`); + defs.push( + ` %coerced = call double @scr_bit_or(double %result, double ${f64Lit(0)})`, + ` %out = fptosi double %coerced to i32`, + ` ret i32 %out`, + ); + break; + } + defs.push(`}`, ``); + } + return { globals, defs }; +} + +export function emitParts(emitter: LlEmitter): string[] { + const parts = [ + ...limitInlineDynReleases(emitter.emitModuleParts()), + ...emitter.disposeObserverGates(), + ]; + if (!emitter.executableTls) return parts; + // A worker executable's thread-locals all live in the executable's own + // TLS block: the program's internal ones, and the runtime's external + // ones, because the runtime is always linked statically into the + // executable. So on ELF they can all use the local-exec model: LLVM's + // default general-dynamic sequence (relaxed by the linker to `mov %fs:0` + // plus an add), and even initial-exec (relaxed to `mov $offset` plus an + // indexed access), become one %fs-relative operand LLVM can fold into + // loads and compares. Darwin's thread-local variables have one model. + return parts.map((part) => + part + .replace( + /^(@[-$._A-Za-z0-9]+ = internal )thread_local global /gm, + "$1thread_local(localexec) global ", + ) + .replace( + /^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, + "$1thread_local(localexec) global ", + ), + ); +} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts new file mode 100644 index 000000000..a5da56f55 --- /dev/null +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -0,0 +1,2083 @@ +/* LlEmitter's statement emission: the statement switch, try/catch, switch, loop copies, speculative writes and string self-concatenation. */ +import { + type LlEmitter, + type LlScopeEntry, + adoptingAwaitOf, + MAX_BYTES_LOOP_VERSION_DEPTH, +} from "./emitter.js"; +import { CAUGHT, F64, type IrExpr, type IrStmt, STRING, isRefCounted } from "../../ir/ir.js"; +import { emitStringSliceSnapshot } from "./string-slices.js"; +import { + canDeferSplitPiece, + emitSplitCursor, + emitSplitNext, + emitSplitScratch, + emitSplitSnapshot, + emitSplitSpanNext, + loadSplitSnapshot, + storeSplitSnapshot, + stringSplit, +} from "./split-loops.js"; +import { + type StackObjectField, + type StackObjectSource, + emitStackObject, + stackObjectFields, + stackObjectStruct, +} from "./stack-objects.js"; +import { emitStackUnion } from "./stack-unions.js"; +import { emitBorrowedArrayRead, emitLocalArrayRead } from "./local-array-reads.js"; +import { emitStackMapRead } from "./map-read-lifetimes.js"; +import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js"; +import { boxAccess, boxNewCall, elemAccess } from "./shapes.js"; +import { dynHoleRead } from "./loop-array-borrows.js"; +import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; +import { emitDiscardedSplice } from "./expr-containers.js"; +import { InternalCompilerError } from "../../errors.js"; +import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js"; +import { emitBorrowedInput } from "./borrowed-inputs.js"; +import { f64Lit, llvmCommentText, sizeToDouble } from "./common.js"; +import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js"; +import { + matchIntegerArrayForLoop, + matchIntegerBytesForLoop, + matchIntegerCountedForLoop, +} 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 { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; +import { mangleLocal } from "../mangle.js"; +import { LlvmUnsupportedError } from "./unsupported.js"; +import type { LlValue } from "./expr-context.js"; +import { SCALAR_UNION_RESULT } from "./int32-abi.js"; +import { emitLiteralSwitch } from "./switch-dispatch.js"; +import { + SPECULATIVE_RANGE, + emitSafeIntegerCheck, + emitSpeculativeInteger, + planSpeculativeLoop, + speculativeReads, + speculativeWriteTarget, +} from "./speculative-integers.js"; +import { + emitConcatInputs, + emitMixedConcat, + numberPart, + stringParts, +} from "./string-construction.js"; +import { emitStringInputs } from "./string-lifetimes.js"; + +export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { + const B = emitter.B; + emitter.frames.push([]); + switch (s.kind) { + case "varDecl": { + if (s.init && emitter.int32Locals.has(s.localId)) { + emitter.storeInt32Local(s.localId, emitter.emitExpr(s.init), s.init); + break; + } + const b = emitter.binding(s.localId); + if (emitter.walkBorrows.has(s.localId) && b.kind === "local") { + // A borrowed walk pointer: no retain now, no release at scope exit. + const v = s.init === null ? "null" : emitter.emitReadReceiver(s.init).name; + B.line(`store ptr ${v}, ptr ${b.slot}`); + break; + } + const slice = emitter.scalarStringSlices.get(s.localId); + if (slice) { + const snapshot = emitStringSliceSnapshot(emitter, slice); + emitter.stringSlices.set(s.localId, snapshot); + emitter.scopes[emitter.scopes.length - 1]!.push({ slot: snapshot.source, type: STRING }); + break; + } + const split = emitter.privateSplitLocals.get(s.localId); + if (split) { + const snapshot = storeSplitSnapshot(emitter, emitSplitSnapshot(emitter, split)); + emitter.storedSplits.set(s.localId, snapshot); + emitter.scopes[emitter.scopes.length - 1]!.push( + { slot: snapshot.sourceSlot, type: STRING }, + { slot: snapshot.separatorSlot, type: STRING }, + ); + break; + } + const stackObject = emitter.localStackObjects.get(s.localId); + if (stackObject) { + const value = emitStackObject(emitter, stackObject, (fields) => + emitter.ownScopeFields(fields), + ); + B.line(`store ptr ${value.name}, ptr ${b.slot}`); + break; + } + if (s.init && emitter.reassignedStackObjects.has(s.localId)) { + const slot = emitter.stackObjectSlot(s.localId, s.init.type); + emitStackObject( + emitter, + s.init as StackObjectSource, + (fields) => emitter.ownScopeFields(fields), + slot, + ); + B.line(`store ptr ${slot}, ptr ${b.slot}`); + break; + } + const localUnion = emitter.localStackUnions.get(s.localId); + if (localUnion) { + const union = emitStackUnion(emitter, localUnion); + B.line(`store ptr ${union.value.name}, ptr ${b.slot}`); + if (union.payload) { + emitter.toFrame(union.payload); + emitter.scopes[emitter.scopes.length - 1]!.push({ + slot: union.payloadSlot, + type: union.payload.type, + }); + } + break; + } + const localRead = emitter.localArrayReads.get(s.localId); + if (localRead) { + const owner = emitLocalArrayRead( + emitter, + !localRead.borrow && emitter.unwrittenLocalArrayRead(s, localRead) + ? { ...localRead, borrow: true } + : localRead, + b.slot, + ); + if (owner) emitter.scopes[emitter.scopes.length - 1]!.push(owner); + break; + } + const mapRead = emitter.mapReadLifetimes.locals.get(s.localId); + if (mapRead) { + const result = emitStackMapRead(emitter, mapRead); + B.line(`store ptr ${result.value.name}, ptr ${b.slot}`); + if (result.owner) emitter.scopes[emitter.scopes.length - 1]!.push(result.owner); + break; + } + const localStorage = emitter.localUnionStorageProofs.get(s.localId); + if (localStorage && s.init) { + const storage = allocateLocalUnion(emitter, localStorage); + emitter.localUnionStorage.set(s.localId, storage); + storeLocalUnion(emitter, storage, s.init); + B.line(`store ptr ${storage.box}, ptr ${b.slot}`); + if (storage.owner && storage.ownerType) + emitter.scopes[emitter.scopes.length - 1]!.push({ + slot: storage.owner, + type: storage.ownerType, + }); + break; + } + if (b.kind === "boxed") { + const stack = emitter.stackCaptures.emit(emitter, s.localId, b.type); + if (stack) { + B.line(`store ptr ${stack.box}, ptr ${b.slot}`); + if (stack.owner) emitter.scopes[emitter.scopes.length - 1]!.push(stack.owner); + if (s.init) { + const value = emitter.emitExpr(s.init); + if (isRefCounted(value.type)) emitter.toFrame(value); + emitter.boxSet(stack.box, b.type, value.name); + } + break; + } + // Box FIRST, then evaluate the initializer: a named function + // expression's closure captures this box during init evaluation. + // A SCALAR TDZ box rides an ARR-kind box: the value lives in a + // one-element array cell, so the empty (NULL) slot stays the + // not-yet-initialized sentinel — a raw scalar slot has no spare + // bit pattern to spend on it (stmts.ts's varDecl). + const boxNew = + b.local!.tdz === true && boxAccess(b.type) !== "ref" + ? (emitter.declare(`declare ptr @scr_box_new(i32)`), `call ptr @scr_box_new(i32 3)`) + : boxNewCall(emitter.shapeHost, b.type); + const box = B.tmp(); + B.line(`${box} = ${boxNew} ; let ${b.local!.name} (boxed)`); + B.line(`store ptr ${box}, ptr ${b.slot}`); + emitter.scopes[emitter.scopes.length - 1]!.push({ + slot: b.slot, + type: b.type, + boxed: true, + }); + if (s.init === null) { + // An empty TDZ box whose payload can legitimately be NULL + // starts at its distinct sentinel (tdzEmptySentinel). + const sentinel = b.local!.tdz === true ? emitter.tdzEmptySentinel(b.type) : null; + if (sentinel !== null) B.line(`store ptr ${sentinel}, ptr ${emitter.boxSlot(box)}`); + break; + } + const v = emitter.emitExpr(s.init); + if (isRefCounted(v.type)) emitter.toHeap(v); // the box takes ownership + if (b.local!.tdz === true && boxAccess(b.type) !== "ref") { + emitter.tdzScalarInit(box, b.type, v.name); + } else { + emitter.boxSet(box, b.type, v.name); + } + break; + } + if (s.init === null) { + // Declared, uninitialized (`let x: number;`): reset the slot — + // inside a loop the previous iteration's scope exit released the + // old value and left a stale pointer (NULL-tolerant releases). + if (isRefCounted(b.type)) { + B.line(`store ptr null, ptr ${b.slot}`); + emitter.scopes[emitter.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); + } + break; + } + if (emitter.loopArrayBorrows.has(s) && s.init.kind === "arrayGet") { + emitBorrowedArrayRead(emitter, s.init, b.slot); + break; + } + const holeRead = emitter.loopArrayBorrows.has(s) ? dynHoleRead(s.init) : null; + if (holeRead && s.init.kind === "ternary") { + // A hole reads as the immortal undefined value, which needs no owner. + const present = emitter.emitExpr(s.init.cond); + const filled = B.newLabel("borrow.hole.filled"), + missing = B.newLabel("borrow.hole.missing"), + done = B.newLabel("borrow.hole.done"); + B.condBr(present.name, filled, missing); + B.startBlock(filled); + emitBorrowedArrayRead(emitter, holeRead, b.slot); + B.br(done); + B.startBlock(missing); + emitter.declare("declare ptr @scr_dyn_undefined()"); + const undef = B.tmp(); + B.line(`${undef} = call ptr @scr_dyn_undefined()`); + B.line(`store ptr ${undef}, ptr ${b.slot}`); + B.br(done); + B.startBlock(done); + break; + } + // A stable lexical alias cannot outlive its unchanged local owner. + // Whole-value uses still retain their own copies, so returning or + // storing the alias preserves the ordinary heap representation. + // Boxed captures, reassigned sources and projections need ownership. + if ( + b.kind === "local" && + b.local && + isRefCounted(b.type) && + emitter.stableCallBindings.has(s.localId) && + (emitter.canBorrowCallArgument(s.init) || emitter.unwrittenAlias(s)) + ) { + const v = emitter.emitReadReceiver(s.init); + B.line(`store ptr ${v.name}, ptr ${b.slot}`); + break; + } + const v = emitter.emitExpr(s.init); + if (b.kind === "global") emitter.toHeap(v); + else emitter.toFrame(v); + B.line(`store ${emitter.llType(b.type)} ${v.name}, ptr ${b.slot}`); + emitter.storeIntegerView(s.localId, v, s.init); + if (isRefCounted(b.type)) { + emitter.scopes[emitter.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); + emitter.noteLastUseMove(s, b); + } + break; + } + case "assign": { + if (emitter.int32Locals.has(s.localId)) { + emitter.storeInt32Local(s.localId, emitter.emitExpr(s.value), s.value); + break; + } + const owner = emitter.reboundOwnerSlots.get(s.localId); + if (owner !== undefined && !emitter.walkBorrows.has(s.localId)) { + // A rebound borrowed parameter: the new value moves into the owner + // slot, replacing (and releasing) the previous owned value. This + // precedes the in-place string append, which would release the + // caller's borrow. + const param = emitter.binding(s.localId); + const slot = param.slot; + const v = emitter.emitExpr(s.value); + emitter.toFrame(v); + const old = B.tmp(); + B.line(`${old} = load ptr, ptr ${owner}`); + emitter.releaseFrame(old, param.type); + B.line(`store ptr ${v.name}, ptr ${slot}`); + B.line(`store ptr ${v.name}, ptr ${owner}`); + break; + } + if (emitter.reassignedStackObjects.has(s.localId)) { + // The new value may read the old one: build it apart (its + // reference fields owned by this statement until it completes), + // then copy it over the binding's slot. The binding takes over the + // new fields; the old ones are released after the slot no longer + // holds them. + const struct = stackObjectStruct(s.value.type); + let owned: readonly StackObjectField[] = []; + const value = emitStackObject(emitter, s.value as StackObjectSource, (fields) => { + owned = fields; + for (const field of fields) emitter.ownSlot(field.ptr, field.type); + }); + const target = emitter.stackObjectSlot(s.localId, s.value.type); + const old = stackObjectFields(emitter, s.value.type, target).map((field) => { + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${field.ptr}`); + return { name: t, type: field.type }; + }); + const bits = B.tmp(); + B.line(`${bits} = load ${struct}, ptr ${value.name}`); + B.line(`store ${struct} ${bits}, ptr ${target}`); + emitter.disownSlots(owned); + for (const field of old) emitter.releaseFrame(field.name, field.type); + break; + } + const localStorage = emitter.localUnionStorage.get(s.localId); + if (localStorage) { + storeLocalUnion(emitter, localStorage, s.value); + break; + } + const self = matchStringSelfConcat(s.localId, s.value); + if (self) { + emitter.emitStringSelfConcatAssign(s.localId, self.left, self.suffix, false); + break; + } + const b = emitter.binding(s.localId); + if (emitter.walkBorrows.has(s.localId) && b.kind === "local") { + // Rebinding a borrowed walk pointer releases nothing. + B.line(`store ptr ${emitter.emitReadReceiver(s.value).name}, ptr ${b.slot}`); + break; + } + const v = emitter.emitExpr(s.value); + if (b.kind === "global" && !s.initializes) emitter.checkGlobalTdz(s.localId); + if (b.kind === "boxed") { + emitter.writeBindingBox(emitter.loadBox(b.slot), b.local!, v.name, s.initializes); + // The RHS remains frame-owned until a possible TDZ throw passes. + if (isRefCounted(v.type)) emitter.toHeap(v); + break; + } + if (b.kind === "global") emitter.toHeap(v); + else emitter.toFrame(v); + if (isRefCounted(b.type)) { + const old = B.tmp(); + B.line(`${old} = load ptr, ptr ${b.slot}`); + if (b.kind === "global") emitter.releaseHeap(old, b.type); + else emitter.releaseFrame(old, b.type); + } + B.line(`store ${emitter.llType(b.type)} ${v.name}, ptr ${b.slot}`); + emitter.storeIntegerView(s.localId, v, s.value); + if (b.kind === "global" && emitter.immortalGlobals.has(s.localId)) { + // An Immutable module constant (contracts/check.ts proved it is + // fresh, publishable and never written): its graph becomes + // immortal, so reads skip reference counting and the collector + // never visits it. + const lines = emitter.publishWalkers.emitPublish(v.name, v.type, s.loc); + for (const line of lines) B.line(line.trimStart()); + // Reads of this global skip retains from here on, which is only + // sound once the graph really is immortal. Node.js never fails + // here, so a refusal means the contract proof was wrong: stop + // rather than keep running with a mortal graph read as immortal. + if (lines.length > 0) { + emitter.declare(`declare zeroext i1 @scr_exc_pending()`); + emitter.declare(`declare void @scr_trap(ptr)`); + const refused = B.tmp(); + B.line(`${refused} = call zeroext i1 @scr_exc_pending()`); + const stop = B.newLabel("immutable.refused"); + const done = B.newLabel("immutable.published"); + B.condBr(refused, stop, done); + B.startBlock(stop); + B.line( + `call void @scr_trap(ptr ${emitter.cstr("scriptc: internal error: an Immutable value could not be published\n")})`, + ); + B.terminate("unreachable"); + B.startBlock(done); + } + } + break; + } + case "exprStmt": + // A statement-position splice never observes its removed elements. + if (!emitDiscardedSplice(emitter, s.expr)) emitter.emitDiscarded(s.expr); + break; + case "arraySet": { + // Evaluation order matches JS: array, index, then value. Ownership + // of a refcounted value moves into the array (the runtime releases + // the replaced element itself). A local receiver that the index and + // value cannot rebind is read in place, as for field stores; the + // released element cannot reach that binding. + if (emitter.emitArraySelfConcat(s)) break; + const arr = emitter.emitStableReceiver(s.arr, [s.index, s.value]); + const idx = emitter.emitExpr(s.index); + const v = emitter.emitExpr(s.value); + if (s.arr.type.kind !== "array") + throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); + const acc = elemAccess(s.arr.type.elem); + emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); + if (acc === "ref") emitter.toHeap(v); + emitDenseArraySet( + emitter, + arr.name, + idx, + s.index, + acc, + s.arr.type.elem, + v.name, + emitter.provenDenseIndex(s.arr, s.index), + ); + break; + } + case "arraySetLength": { + // Pop's `length = 0` store on an empty array only matters when the + // array can be frozen (published); its operand is a local. + if ( + s.pop === true && + s.length.kind === "numLit" && + s.length.value === 0 && + emitter.publishedTypes?.containerGuarded(s.arr.type) !== true + ) + break; + const arr = emitBorrowedInput(emitter, s.arr); + const length = emitter.emitExpr(s.length); + if (s.arr.type.kind !== "array") + throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array"); + emitter.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3); + emitter.declare(`declare void @scr_arr_set_len(ptr, double)`); + // Assigning the current length (a reused work array reset to where + // it already is) changes nothing: the runtime's validation accepts + // exactly the value equal to the length (-0 included, NaN never), + // and no element is released. Everything else keeps the call. + const lenPtr = B.tmp(), + len = B.tmp(), + lenNumber = B.tmp(), + same = B.tmp(); + const resize = B.newLabel("arr.len.set"), + done = B.newLabel("arr.len.done"); + B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`); + emitter.markMemoryPointer(lenPtr, "array:header"); + B.line( + `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}${emitter.arrayLengthRange}`, + ); + B.line( + `${lenNumber} = ${sizeToDouble(emitter.sizeType)} ${emitter.sizeType} ${len} to double`, + ); + B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`); + B.condBr(same, done, resize); + B.startBlock(resize); + if (s.pop === true) { + // A discarded pop's `length - 1` on a packed array removes the + // last element inline, then releases it. + const call = B.newLabel("arr.len.call"), + pop = B.newLabel("arr.len.pop"); + const lastNumber = B.tmp(), + dropsLast = B.tmp(); + B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`); + B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`); + B.condBr(dropsLast, pop, call); + B.startBlock(pop); + const elem = s.arr.type.elem; + const acc = elemAccess(elem); + const raw = emitDenseArrayPop(emitter, arr.name, acc, call, call); + if (acc === "ref" && isRefCounted(elem)) { + const old = B.tmp(); + B.line(`${old} = inttoptr i64 ${raw} to ptr`); + emitter.releaseHeap(old, elem); + } + B.br(done); + B.startBlock(call); + } + B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`); + emitter.emitPendingCheck(); + B.br(done); + B.startBlock(done); + break; + } + case "arraySetUndefined": + case "arrayDelete": { + const arr = emitBorrowedInput(emitter, s.arr); + const idx = emitter.emitExpr(s.index); + if (s.arr.type.kind !== "array") + throw new InternalCompilerError(`llvm emitter bug: ${s.kind} on non-array`); + emitter.emitPublishedArrayGuard( + arr.name, + s.arr.type, + s.kind === "arrayDelete" ? 5 : 0, + idx.name, + ); + const fn = s.kind === "arraySetUndefined" ? "scr_arr_set_undefined" : "scr_arr_delete"; + emitter.declare( + `declare ${s.kind === "arrayDelete" ? "zeroext i1" : "void"} @${fn}(ptr, double)`, + ); + if (s.kind === "arrayDelete") + B.line(`call zeroext i1 @${fn}(ptr ${arr.name}, double ${idx.name})`); + else B.line(`call void @${fn}(ptr ${arr.name}, double ${idx.name})`); + break; + } + case "bytesSet": { + // Typed-array element write: same evaluation order as arraySet; + // the value is a scalar (the kind-specific inline path coerces + // JS-exactly), so no ownership moves. Invalid writes are ignored. + // IrBytesElem is static, so never rediscover it through the + // generic runtime switch in a hot loop. + const arr = emitter.emitStableReceiver(s.arr, [s.index, s.value]); + const idx = emitter.emitExpr(s.index); + const v = emitter.emitExpr(s.value); + if (s.arr.type.kind !== "bytes") + throw new InternalCompilerError("llvm emitter bug: bytesSet on non-bytes"); + emitter.emitBytesSet( + s.arr.type.elem, + arr.name, + idx, + v, + s.index, + s.value, + emitter.bytesBounds.has(s), + ); + break; + } + case "fieldSet": + case "recordSet": { + // Evaluation order: obj, then value. New value moved in; the old + // value is released AFTER the field is overwritten (unlink-then- + // release — a release can trigger a cycle collection, which must + // never see a heap edge whose count was already given up). + // Classes and records share the struct layout, so one emission. + const nullable = + s.kind === "fieldSet" ? emitter.nullableFields.get(s.className, s.field) : null; + if (nullable && s.kind === "fieldSet") { + const obj = emitter.emitStableReceiver(s.obj, [s.value]); + // (The guard precedes the value here: the nullable store + // evaluates it itself.) + emitter.emitStoreGuard(obj.name, s); + emitter.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value); + break; + } + if (s.kind === "fieldSet" && emitter.int32Slots.isField(s.className, s.field)) { + const obj = emitter.emitStableReceiver(s.obj, [s.value]); + const v = emitter.emitExpr(s.value); + emitter.emitStoreGuard(obj.name, s); + const { ptr } = emitter.classFieldPtr(obj.name, s.className, s.field); + emitter.storeInt32Field(ptr, v, s.value); + break; + } + if (emitter.emitFieldSelfConcat(s)) break; + const obj = emitter.emitStableReceiver(s.obj, [s.value]); + const v = emitter.emitFieldValue( + s.value, + s.kind === "recordSet" + ? emitter.recordFieldInline(s.shapeId, s.field) + : emitter.inlineUnions.classField(s.className, s.field), + ); + if (s.kind === "fieldSet") emitter.emitStoreGuard(obj.name, s); + else if (emitter.publishedTypes?.recordGuarded(s.shapeId) === true) + emitter.emitPublishedFieldGuard(obj.name, null, s.field); + const { ptr, type, inline } = + s.kind === "fieldSet" + ? emitter.classFieldPtr(obj.name, s.className, s.field) + : emitter.recordFieldPtr(obj.name, s.shapeId, s.field); + // A result-slot entry fills zeroed caller storage, each field once. + if ( + s.kind === "recordSet" && + s.obj.kind === "varRef" && + s.obj.localId === RESULT_DST_ID && + emitter.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX) + ) + emitter.initField(ptr, type, inline, v); + else emitter.replaceField(ptr, type, inline, v); + if ( + s.kind === "fieldSet" && + s.field === "name" && + emitter.classMeta.get(s.className)?.root.def.name === "%Error" + ) { + const present = emitter.classFieldPtr(obj.name, s.className, "%namePresent").ptr; + const enumerable = emitter.classFieldPtr(obj.name, s.className, "%nameEnumerable").ptr; + const wasPresent = B.tmp(), + wasEnumerable = B.tmp(), + isPresent = B.tmp(), + nextEnumerable = B.tmp(); + B.line(`${wasPresent} = load i8, ptr ${present}`); + B.line(`${wasEnumerable} = load i8, ptr ${enumerable}`); + B.line(`${isPresent} = icmp ne i8 ${wasPresent}, 0`); + B.line(`${nextEnumerable} = select i1 ${isPresent}, i8 ${wasEnumerable}, i8 1`); + B.line(`store i8 1, ptr ${present}`); + B.line(`store i8 ${nextEnumerable}, ptr ${enumerable}`); + } + break; + } + case "recordKeyDelete": { + // `delete obj[k]` on a pure index-signature shape: a Map delete on + // the overflow (key and value released; absent keys no-op). + const obj = emitter.emitExpr(s.obj); + const key = emitter.emitExpr(s.key); + if (emitter.publishedTypes?.recordGuarded(s.shapeId) === true) { + // Node: "Cannot delete property 'k' of #" (key known at + // run time only; the guard reports the frozen record generically). + emitter.emitPublishedFieldGuard(obj.name, null, "[key]"); + } + const ovf = emitter.recordOvfPtr(obj.name, s.shapeId); + emitter.declare(`declare zeroext i1 @scr_map_delete_str(ptr, ptr)`); + const t = B.tmp(); + B.line(`${t} = call zeroext i1 @scr_map_delete_str(ptr ${ovf}, ptr ${key.name})`); + break; + } + case "recordKeySet": { + // Dynamic-keyed record write (the C per-shape helper, inline): + // declared keys write through (same-typed — dyn-valued shapes, + // whose writes validate and can throw, stay refused), undeclared + // keys insert/replace in the overflow map. Evaluation order: obj, + // key, value; the write OWNS the value (+1 moves in). + const obj = emitter.emitExpr(s.obj); + const key = emitter.emitExpr(s.key); + const v = emitter.emitExpr(s.value); + if (emitter.publishedTypes?.recordGuarded(s.shapeId) === true) + emitter.emitPublishedFieldGuard(obj.name, null, "[key]"); + if (isRefCounted(v.type)) emitter.toHeap(v); + const shape = emitter.recordShape(s.shapeId); + // Signature-free shapes dispatch over their (one-typed) declared + // fields and TRAP on a miss (scr_record_key_miss — JS would add + // the property, which a monomorphic struct cannot); overflow + // shapes keep the map insert tail. + const iv = shape.indexValue ?? shape.fields[0]?.type; + if (!iv) + throw new InternalCompilerError( + `llvm emitter bug: keyed write on field-free non-overflow shape ${s.shapeId}`, + ); + const vAcc = iv.kind === "f64" ? "f64" : iv.kind === "bool" ? "bool" : "ref"; + if (s.overflowOnly === true) { + // A LITERAL key naming no declared field: a plain overflow + // insert — no field chain. + const ovf = emitter.recordOvfPtr(obj.name, s.shapeId); + emitter.mapSet(ovf, "str", vAcc, key.name, v.name); + break; + } + const join = B.newLabel("rks.j"); + if (iv.kind === "dyn" && shape.indexValue) { + // A dyn-valued shape (the C recordKeySetHelper's dyn arm, + // inline): declared keys VALIDATE the dyn value against the + // field's type first (dynCheck — a mismatched write throws the + // catchable TypeError and leaves the field untouched; JS would + // store anything, the documented divergence); undeclared keys + // insert the dyn value into the overflow map as-is. + emitter.declare(`declare void @scr_dyn_release(ptr)`); + for (const f of shape.fields) { + const hit = B.tmp(); + B.line( + `${hit} = call zeroext i1 ${emitter.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`, + ); + const lh = B.newLabel("rks.h"); + const ln = B.newLabel("rks.n"); + B.condBr(hit, lh, ln); + B.startBlock(lh); + const pathSlot = B.slot(); + B.entryAllocas.push(`${pathSlot} = alloca %ScrDynPath`); + const pp = B.tmp(); + const kp = B.tmp(); + const ip = B.tmp(); + B.line(`${pp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 0`); + B.line(`store ptr null, ptr ${pp}`); + B.line(`${kp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 1`); + B.line(`store ptr ${emitter.cstr(f.name)}, ptr ${kp}`); + B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`); + B.line(`store ${emitter.sizeType} 0, ptr ${ip}`); + const helper = emitter.dyn.dynCheckHelper(f.type); + const fty = emitter.llType(f.type); + const nv = B.tmp(); + B.line( + `${nv} = call ${fty === "i1" ? "zeroext i1" : fty} @${helper}(ptr ${v.name}, ptr ${pathSlot})`, + ); + B.line(`call void @scr_dyn_release(ptr ${v.name})`); + // Mismatched write: TypeError pending, field untouched — the + // statement-level check below unwinds. + const pend = B.tmp(); + for (const line of emitter.pendingTestLines(pend)) B.line(line.trimStart()); + const lw = B.newLabel("rks.w"); + B.condBr(pend, join, lw); + B.startBlock(lw); + const { ptr, type, inline } = emitter.recordFieldPtr(obj.name, s.shapeId, f.name); + if (inline) { + // The slot copies the checked value's bits; nv is released below. + emitter.storeField(ptr, type, nv, true); + emitter.releaseFrame(nv, type); + } else if (isRefCounted(type)) { + const old = B.tmp(); + B.line(`${old} = load ptr, ptr ${ptr}`); + emitter.storeField(ptr, type, nv, false); + emitter.releaseHeap(old, type); + } else { + emitter.storeField(ptr, type, nv, false); + } + B.br(join); + B.startBlock(ln); + } + const ovf = emitter.recordOvfPtr(obj.name, s.shapeId); + emitter.mapSet(ovf, "str", vAcc, key.name, v.name); + B.br(join); + B.startBlock(join); + // MAY THROW exactly when a dyn value can validate against a + // declared field (stmts.ts's condition). + if (shape.fields.length > 0) emitter.emitPendingCheck(); + break; + } + for (const f of shape.fields) { + // typeEquals(f.type, iv) — the frontend fences everything else. + const hit = B.tmp(); + B.line( + `${hit} = call zeroext i1 ${emitter.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`, + ); + const lh = B.newLabel("rks.h"); + const ln = B.newLabel("rks.n"); + B.condBr(hit, lh, ln); + B.startBlock(lh); + const { ptr, type, inline } = emitter.recordFieldPtr(obj.name, s.shapeId, f.name); + if (inline) { + // The write owns the moved-in value; the slot copies its bits. + emitter.storeField(ptr, type, v.name, true); + emitter.releaseHeap(v.name, type); + } else if (isRefCounted(type)) { + const old = B.tmp(); + B.line(`${old} = load ptr, ptr ${ptr}`); + emitter.storeField(ptr, type, v.name, false); + emitter.releaseHeap(old, type); + } else { + emitter.storeField(ptr, type, v.name, false); + } + B.br(join); + B.startBlock(ln); + } + if (!shape.indexValue) { + // The MISS on a fixed shape: release the moved-in value, throw + // the catchable TypeError naming the key (scr_record_key_miss — + // JS would add the property, the documented divergence). + if (isRefCounted(iv)) emitter.releaseHeap(v.name, iv); + emitter.declare(`declare void @scr_record_key_miss(ptr)`); + B.line(`call void @scr_record_key_miss(ptr ${key.name})`); + B.br(join); + B.startBlock(join); + emitter.emitPendingCheck(); + break; + } + const ovf = emitter.recordOvfPtr(obj.name, s.shapeId); + emitter.mapSet(ovf, "str", vAcc, key.name, v.name); + B.br(join); + B.startBlock(join); + break; + } + case "block": { + if (s.labels === undefined) { + emitter.emitBlock(s.body); + break; + } + // A labeled block: `break lbl` inside branches to the end label. + const le = B.newLabel("blk.e"); + emitter.jumpTargets.push({ + kind: "block", + brkLabel: le, + contLabel: null, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + emitter.emitBlock(s.body); + emitter.jumpTargets.pop(); + B.br(le); + B.startBlock(le); + break; + } + case "if": { + const cond = emitter.emitCondition(s.cond); + const trueLabel = B.newLabel("if.t"); + const joinLabel = B.newLabel("if.j"); + const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel; + B.condBr(cond, trueLabel, falseLabel); + B.startBlock(trueLabel); + emitter.emitBlock(s.then); + B.br(joinLabel); + if (s.else_) { + B.startBlock(falseLabel); + emitter.emitBlock(s.else_); + B.br(joinLabel); + } + B.startBlock(joinLabel); + break; + } + case "while": { + const workerBudget = emitter.workerLoopBudget(s); + const le = B.newLabel("loop.e"); + emitter.emitLoopCopies(s, true, le, (exit) => { + const lc = B.newLabel("loop.c"); + const lb = B.newLabel("loop.b"); + const lp = workerBudget !== null ? B.newLabel("loop.p") : lc; + B.br(lc); + B.startBlock(lc); + B.condBr(emitter.emitCondition(s.cond), lb, exit); + B.startBlock(lb); + emitter.jumpTargets.push({ + kind: "loop", + brkLabel: exit, + contLabel: lp, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + emitter.emitBlock(s.body); + emitter.jumpTargets.pop(); + B.br(lp); + if (lp !== lc) { + B.startBlock(lp); + emitter.emitWorkerLoopCheck(workerBudget); + B.br(lc); + } + }); + B.startBlock(le); + break; + } + case "doWhile": { + const workerBudget = emitter.workerLoopBudget(s); + // Body first (runs at least once); continue jumps to the CONDITION. + const le = B.newLabel("loop.e"); + emitter.emitLoopCopies(s, true, le, (exit) => { + const lb = B.newLabel("loop.b"); + const lc = B.newLabel("loop.c"); + B.br(lb); + B.startBlock(lb); + emitter.jumpTargets.push({ + kind: "loop", + brkLabel: exit, + contLabel: lc, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + emitter.emitBlock(s.body); + emitter.jumpTargets.pop(); + B.br(lc); + B.startBlock(lc); + emitter.emitWorkerLoopCheck(workerBudget); + B.condBr(emitter.emitCondition(s.cond), lb, exit); + }); + B.startBlock(le); + break; + } + case "for": { + // The init's scope wraps the whole loop (break/continue must NOT + // release it — scopeDepth captured after the push, C parity). + const integerArrayLoop = + emitter.debug === null + ? matchIntegerArrayForLoop(s, emitter.currentLocals, emitter.integerArrayBindings) + : null; + const integerLoop = + integerArrayLoop ?? + (emitter.debug === null ? matchIntegerBytesForLoop(s, emitter.numericLocals) : null); + const countedLoop = + !integerLoop && emitter.countedLoopsEnabled + ? matchIntegerCountedForLoop( + s, + emitter.numericLocals, + emitter.integerRanges, + emitter.unaliasedArrays, + ) + : null; + const window = + countedLoop && emitter.byteWindowsEnabled + ? matchByteWindow( + s, + countedLoop, + emitter.numericLocals, + emitter.captureIds, + emitter.integerRanges, + emitter.byteWindowEntries.get(s) ?? new Set(), + ) + : null; + const denseShape = !emitter.denseLoopsEnabled + ? null + : integerArrayLoop + ? { + counter: integerArrayLoop.localId, + length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" }, + range: { min: 0, max: 4294967294 }, + } + : countedLoop && !countedLoop.guarded + ? countedDenseShape(s, countedLoop) + : null; + const dense = + denseShape && !window + ? matchDenseArrayLoop( + s, + denseShape, + emitter.numericLocals, + emitter.unaliasedArrays, + emitter.denseLoopEntries.get(s) ?? new Set(), + ) + : null; + if (dense) { + const valid = emitDenseLoopGuard(emitter, dense); + const fast = B.newLabel("dense.loop.fast"), + slow = B.newLabel("dense.loop.slow"), + done = B.newLabel("dense.loop.done"); + B.condBr(valid, fast, slow); + emitter.denseLoopsEnabled = false; + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.denseLoop = dense; + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.denseLoop = null; + emitter.denseLoopsEnabled = true; + } + B.startBlock(done); + break; + } + if (window) { + const valid = emitByteWindowGuard(emitter, window); + const fast = B.newLabel("bytes.window.fast"), + slow = B.newLabel("bytes.window.slow"), + done = B.newLabel("bytes.window.done"); + B.condBr(valid, fast, slow); + const originalRanges = emitter.integerRanges, + originalBounds = emitter.bytesBounds; + emitter.byteWindowsEnabled = false; + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.integerRanges = window.ranges; + emitter.bytesBounds = new Set([...originalBounds, ...window.bounds]); + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.integerRanges = originalRanges; + emitter.bytesBounds = originalBounds; + emitter.byteWindowsEnabled = true; + } + B.startBlock(done); + break; + } + const version = + emitter.bytesLoopVersionsEnabled && + emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !emitter.bytesLoopVersioning.has(s) + ? emitter.matchBytesLoopVersion(s, countedLoop) + : null; + if (version) { + const fast = B.newLabel("bytes.version.fast"), + slow = B.newLabel("bytes.version.slow"), + done = B.newLabel("bytes.version.done"); + emitBytesLoopVersionGuard(emitter, version, fast, slow); + const originalBounds = emitter.bytesBounds, + originalRanges = emitter.integerRanges; + emitter.bytesLoopVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.ranges.size > 0) + emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); + if (version.counted?.guarded) emitter.provenCountedGuards.add(s); + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.bytesBounds = originalBounds; + emitter.integerRanges = originalRanges; + emitter.provenCountedGuards.delete(s); + emitter.bytesLoopVersioning.delete(s); + } + B.startBlock(done); + break; + } + let countedJoin: string | null = null; + 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"); + B.condBr(safe, fast, slow); + B.startBlock(slow); + emitter.countedLoopsEnabled = false; + emitter.emitStmt(s); + emitter.countedLoopsEnabled = true; + B.br(countedJoin); + B.startBlock(fast); + } + emitter.scopes.push([]); + let integerSlot: string | null = null; + let countedLimit: string | null = null; + if (integerLoop) { + integerSlot = B.slot(); + B.entryAllocas.push( + `${integerSlot} = alloca ${emitter.sizeType} ; integer induction ${emitter.currentLocals.get(integerLoop.localId)!.name}`, + ); + let start = "0"; + if (integerArrayLoop && s.init?.kind === "varDecl" && s.init.init) { + const value = emitter.emitExpr(s.init.init); + start = B.tmp(); + // The start is 0 or an enclosing array index plus one (at most + // 2^32 - 1): on 64-bit targets the signed conversion is exact + // and avoids x86-64's unsigned 64-bit conversion sequence. + const conversion = emitter.sizeType === "i64" ? "fptosi" : "fptoui"; + B.line(`${start} = ${conversion} double ${value.name} to ${emitter.sizeType}`); + emitter.integerArrayBindings.add(integerLoop.localId); + } + B.line(`store ${emitter.sizeType} ${start}, ptr ${integerSlot}`); + emitter.integerLoopBindings.set(integerLoop.localId, { + slot: integerSlot, + type: emitter.sizeType, + range: + emitter.denseLoop?.counter === integerLoop.localId + ? emitter.denseLoop.range + : { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 }, + }); + } else if (countedLoop) { + integerSlot = B.slot(); + B.entryAllocas.push( + `${integerSlot} = alloca i64 ; integer induction ${emitter.currentLocals.get(countedLoop.localId)!.name}`, + ); + const startNumber = emitter.emitExpr(countedLoop.start); + const start = B.tmp(); + B.line(`${start} = fptosi double ${startNumber.name} to i64`); + B.line(`store i64 ${start}, ptr ${integerSlot}`); + emitter.integerLoopBindings.set(countedLoop.localId, { + slot: integerSlot, + type: "i64", + signed: true, + range: + emitter.denseLoop?.counter === countedLoop.localId + ? emitter.denseLoop.range + : countedLoop.range, + }); + countedLimit = emitCountedLoopLimit(emitter, countedLoop); + } 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 + // before the first condition. + const initializers = s.init.kind === "block" ? s.init.body : [s.init]; + for (const initializer of initializers) emitter.emitStmt(initializer); + } + const le = B.newLabel("loop.e"); + // JS `for (let i ...)`: each iteration gets a FRESH binding holding + // a copy of the previous one (closures made in iteration k keep + // seeing iteration k's value) — only observable, and only emitted, + // when a let variable is captured (boxed). Freshen before the first + // condition and again in the continue-target block before updating. + const initializers = s.init?.kind === "block" ? s.init.body : s.init ? [s.init] : []; + const capturedLets = initializers.flatMap((init) => { + const local = init.kind === "varDecl" ? emitter.currentLocals.get(init.localId) : undefined; + return local?.boxed && local.mutable ? [local] : []; + }); + const freshenBindings = (): void => { + for (const local of capturedLets) { + const slot = `%${mangleLocal(local.id)}`; + const fresh = B.tmp(); + const old = B.tmp(); + B.line( + `${fresh} = ${boxNewCall(emitter.shapeHost, local.type)} ; per-iteration ${local.name}`, + ); + B.line(`${old} = load ptr, ptr ${slot}`); + const val = emitter.boxGet(old, local.type); + emitter.boxSet(fresh, local.type, val); + emitter.declare(`declare void @scr_box_release(ptr)`); + B.line(`call void @scr_box_release(ptr ${old})`); + B.line(`store ptr ${fresh}, ptr ${slot}`); + } + }; + const workerBudget = emitter.workerLoopBudget(s); + freshenBindings(); + emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => { + const lc = B.newLabel("loop.c"); + const lb = B.newLabel("loop.b"); + const lu = + s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc; + B.br(lc); + B.startBlock(lc); + if (integerLoop && integerSlot) { + const receiver = emitter.emitStableReceiver(integerLoop.limitReceiver, []); + const lenPtr = B.tmp(); + const len = B.tmp(); + const index = B.tmp(); + const inBounds = B.tmp(); + B.line( + `${lenPtr} = getelementptr inbounds %${integerArrayLoop ? "ScrArr" : "ScrBytes"}, ptr ${receiver.name}, i64 0, i32 1`, + ); + emitter.markMemoryPointer(lenPtr, integerArrayLoop ? "array:header" : "bytes:header"); + B.line( + `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`, + ); + B.line(`${index} = load ${emitter.sizeType}, ptr ${integerSlot}`); + B.line(`${inBounds} = icmp ult ${emitter.sizeType} ${index}, ${len}`); + B.condBr(inBounds, lb, exit); + } else if (countedLoop && integerSlot && countedLimit) { + const index = B.tmp(); + const inBounds = B.tmp(); + B.line(`${index} = load i64, ptr ${integerSlot}`); + B.line( + `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, + ); + B.condBr(inBounds, lb, exit); + } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); + else B.br(lb); + B.startBlock(lb); + emitter.jumpTargets.push({ + kind: "loop", + brkLabel: exit, + contLabel: lu, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + emitter.emitBlock(s.body); + emitter.jumpTargets.pop(); + B.br(lu); + if (lu !== lc) { + B.startBlock(lu); + emitter.emitWorkerLoopCheck(workerBudget); + freshenBindings(); + if (integerLoop && integerSlot) { + const old = B.tmp(); + const next = B.tmp(); + B.line(`${old} = load ${emitter.sizeType}, ptr ${integerSlot}`); + B.line(`${next} = add nuw ${emitter.sizeType} ${old}, 1`); + B.line(`store ${emitter.sizeType} ${next}, ptr ${integerSlot}`); + } else if (countedLoop && integerSlot) { + const old = B.tmp(); + const next = B.tmp(); + 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 (s.update) emitter.emitStmt(s.update); + B.br(lc); + } + }); + B.startBlock(le); + emitter.releaseScope(emitter.scopes.pop()!); + if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); + if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); + if (countedJoin) { + B.br(countedJoin); + B.startBlock(countedJoin); + } + break; + } + case "forOf": { + // Ascending index loop; the length is re-read every iteration + // (JS-exact — pushes inside the body extend the iteration). The + // iterable temp lives in this statement's frame, so it is released + // when the whole loop ends (and by `return`'s frame sweep). + if (s.iterable.type.kind !== "array") + throw new LlvmUnsupportedError(`forOf:${s.iterable.type.kind}`, s.loc); + const elem = s.iterable.type.elem; + const stored = + s.iterable.kind === "varRef" ? emitter.storedSplits.get(s.iterable.localId) : undefined; + const direct = emitter.streamingSplitsEnabled ? stringSplit(s.iterable) : null; + const snapshot = stored + ? loadSplitSnapshot(emitter, stored) + : direct + ? emitSplitSnapshot(emitter, direct) + : null; + const arr = snapshot ? null : emitter.emitExpr(s.iterable); + const cursor = snapshot ? emitSplitCursor(emitter, snapshot) : null; + const scratch = snapshot ? emitSplitScratch(emitter) : null; + const deferred = snapshot && canDeferSplitPiece(s, emitter.currentLocals.get(s.localId)); + const spanLength = deferred ? B.slot() : null; + if (spanLength) B.entryAllocas.push(`${spanLength} = alloca ${emitter.sizeType}`); + const idxSlot = snapshot ? null : B.slot(); + if (idxSlot) { + B.entryAllocas.push(`${idxSlot} = alloca double`); + B.line(`store double ${f64Lit(0)}, ptr ${idxSlot}`); + } + const lc = B.newLabel("fof.c"); + const lb = B.newLabel("fof.b"); + const lu = B.newLabel("fof.u"); + const le = B.newLabel("fof.e"); + const workerBudget = emitter.workerLoopBudget(s); + B.br(lc); + B.startBlock(lc); + const inBounds = B.tmp(); + let cur: string; + if (snapshot && cursor && scratch) { + cur = spanLength + ? emitSplitSpanNext(emitter, snapshot, cursor, spanLength) + : emitSplitNext(emitter, snapshot, cursor, scratch); + B.line(`${inBounds} = icmp ne ptr ${cur}, null`); + } else { + const i = B.tmp(), + lenPtr = B.tmp(), + rawLen = B.tmp(), + len = B.tmp(); + B.line(`${i} = load double, ptr ${idxSlot}`); + // The inline form of scr_arr_len, like the `length` intrinsic. + B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr!.name}, i32 0, i32 1`); + emitter.markMemoryPointer(lenPtr, "array:header"); + B.line( + `${rawLen} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}${emitter.arrayLengthRange}`, + ); + B.line( + `${len} = ${sizeToDouble(emitter.sizeType)} ${emitter.sizeType} ${rawLen} to double`, + ); + B.line(`${inBounds} = fcmp olt double ${i}, ${len}`); + cur = i; + } + B.condBr(inBounds, lb, le); + B.startBlock(lb); + emitter.jumpTargets.push({ + kind: "loop", + brkLabel: le, + contLabel: lu, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + // The loop variable is a fresh const per iteration: its scope opens + // here, holds the (for ref elements: owned +1) current element, and + // releases it at the end of each iteration. + emitter.scopes.push([]); + const localInfo = emitter.currentLocals.get(s.localId); + const slot = `%${mangleLocal(s.localId)}`; + const borrowedElement = !snapshot && emitter.loopArrayBorrows.has(s); + if (!snapshot) { + const acc = elemAccess(elem); + const getter = borrowedElement ? "scr_arr_borrow_ref" : `scr_arr_get_${acc}`; + cur = emitDenseArrayGet( + emitter, + arr!.name, + { name: cur, type: { kind: "f64" } }, + undefined, + acc, + elem, + getter, + !borrowedElement, + ); + } + if (spanLength) { + const length = B.tmp(); + B.line(`${length} = load ${emitter.sizeType}, ptr ${spanLength}`); + emitter.splitSpans.set(s.localId, { bytes: cur, length, scratch: scratch! }); + cur = "null"; + } + if (localInfo?.boxed) { + // Captured loop variable: a fresh box per iteration, matching the + // fresh const binding. The box takes ownership of a ref element's + // +1 and is released with the iteration's scope. + const box = B.tmp(); + B.line(`${box} = ${boxNewCall(emitter.shapeHost, elem)} ; per-iteration ${localInfo.name}`); + emitter.boxSet(box, elem, cur); + B.line(`store ptr ${box}, ptr ${slot}`); + emitter.scopes[emitter.scopes.length - 1]!.push({ slot, type: elem, boxed: true }); + } else { + B.line(`store ${emitter.llType(elem)} ${cur}, ptr ${slot}`); + if (isRefCounted(elem) && !borrowedElement) + emitter.scopes[emitter.scopes.length - 1]!.push({ slot, type: elem }); + } + emitter.emitStmts(s.body); + if (spanLength) emitter.splitSpans.delete(s.localId); + const endedWithJump = endsWithJump(s.body); + const scope = emitter.scopes.pop()!; + if (!endedWithJump) emitter.releaseScope(scope); + emitter.jumpTargets.pop(); + B.br(lu); + B.startBlock(lu); + emitter.emitWorkerLoopCheck(workerBudget); + if (idxSlot) { + const i2 = B.tmp(), + i3 = B.tmp(); + B.line(`${i2} = load double, ptr ${idxSlot}`); + B.line(`${i3} = fadd double ${i2}, ${f64Lit(1)}`); + B.line(`store double ${i3}, ptr ${idxSlot}`); + } + B.br(lc); + B.startBlock(le); + break; + } + case "switch": + emitter.emitSwitch(s); + break; + case "break": { + // Unlabeled: the innermost loop OR switch (labeled blocks are + // skipped); labeled: the entry carrying the label. + let target: (typeof emitter.jumpTargets)[number] | undefined; + for (let i = emitter.jumpTargets.length - 1; i >= 0; i--) { + const t = emitter.jumpTargets[i]!; + if (s.label !== undefined ? t.labels?.includes(s.label) : t.kind !== "block") { + target = t; + break; + } + } + if (!target) throw new InternalCompilerError("llvm emitter bug: break target not found"); + emitter.emitFinallysForJump(target.finallyDepth, target.frameDepth, target.scopeDepth); + if (!B.isTerminated()) B.terminate(`br label %${target.brkLabel}`); + break; + } + case "continue": { + // Unlabeled: the innermost loop; labeled: the loop carrying the + // label (tsc + the validator guarantee it IS a loop). + let target: (typeof emitter.jumpTargets)[number] | undefined; + for (let i = emitter.jumpTargets.length - 1; i >= 0; i--) { + const t = emitter.jumpTargets[i]!; + if (t.kind === "loop" && (s.label === undefined || t.labels?.includes(s.label))) { + target = t; + break; + } + } + if (!target || target.contLabel === null) + throw new InternalCompilerError("llvm emitter bug: continue target not found"); + emitter.emitFinallysForJump(target.finallyDepth, target.frameDepth, target.scopeDepth); + if (!B.isTerminated()) B.terminate(`br label %${target.contLabel}`); + break; + } + case "return": { + // The value computes FIRST (an SSA temp — finally mutations of + // returned locals cannot change it, Node-exact), then every + // crossed finally runs innermost-first with the frames/scopes/ + // tryStack it sees truncated to its region (its releases already + // ran; a throw inside a copy propagates OUT of the completing + // try, past its own catch), then the function-level releases and + // the actual ret. The parked value owns a synthetic slot-backed + // scope entry during each copy so a throwing finally releases it. + const adopted = + emitter.finallyStack.length > 0 || emitter.tryStack.length > 0 + ? adoptingAwaitOf(s.value) + : null; + if (adopted !== null) { + emitter.emitAdoptingReturn(s, adopted); + break; + } + let v: LlValue | null = null; + const moved = + emitter.finallyStack.length === 0 && !emitter.currentUnionResult + ? emitter.movableReturn(s.value) + : null; + if (s.value !== null && emitter.currentUnionResult) { + // The tag and payload bits leave in registers; a heap box the + // value needed is released with its frame below. + const box = emitter.emitUnionProjection(s.value); + v = { name: emitter.scalarUnionPair(box.name, s.value.type), type: s.value.type }; + } else if (moved !== null) { + // Returning an owned local hands its reference to the caller: no + // retain now and no release of the local below. + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${moved.entry.slot}`); + v = { name: t, type: s.value!.type }; + moved.scope.splice(moved.scope.indexOf(moved.entry), 1); + emitter.releaseForJump(0, 0); + moved.scope.splice(moved.index, 0, moved.entry); + } else if (s.value !== null && emitter.currentBorrowedReturn) { + // A borrowed-return body hands back its walk without a reference + // (it has no try statements, so no finally can intervene). An + // optional element read lends the element its array keeps alive. + const read = emitter.optionalArrayReads.get(s.value); + if (read) { + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ptr`); + emitLocalArrayRead(emitter, { ...read, borrow: true }, slot); + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${slot}`); + v = { name: t, type: s.value.type }; + } else v = emitter.emitReadReceiver(s.value); + } else if (s.value !== null) { + v = emitter.emitExpr(s.value); + if (emitter.currentCoro !== null) emitter.toHeap(v); + else emitter.toFrame(v); + } + // An async generator's `return v` awaits v before any crossed + // finally runs. Past finally regions that turn is taken here and + // the request settles at completion; otherwise the completion's + // own turn is the await (nothing runs in between). + const genSettles = + emitter.currentAsyncGenerator && (s.awaited === true || emitter.finallyStack.length > 0); + // A completion turn that awaits x replaces a return() request's + // already-taken await. + if (emitter.currentAsyncGenerator && !genSettles) { + emitter.declare(`declare void @scr_async_gen_return_awaits()`); + B.line(`call void @scr_async_gen_return_awaits()`); + } + if (emitter.currentAsyncGenerator && s.awaited !== true && emitter.finallyStack.length > 0) { + if (emitter.wasi) emitter.emitWasiSuspend(null); + else { + emitter.declare(`declare void @scr_await_hop()`); + B.line(`call void @scr_await_hop()`); + } + } + if (emitter.finallyStack.length > 0) { + let pretSlot: string | null = null; + if (v !== null && isRefCounted(v.type) && !emitter.currentUnionResult) { + pretSlot = B.slot(); + B.entryAllocas.push(`${pretSlot} = alloca ptr ; pending return (through finally)`); + B.line(`store ptr ${v.name}, ptr ${pretSlot}`); + } + const savedFrames = emitter.frames; + const savedScopes = emitter.scopes; + const savedFinally = emitter.finallyStack; + const savedTry = emitter.tryStack; + for (let i = savedFinally.length - 1; i >= 0 && !B.isTerminated(); i--) { + const fin = savedFinally[i]!; + emitter.releaseForJump(fin.frameDepth, fin.scopeDepth); + emitter.frames = emitter.frames.slice(0, fin.frameDepth); + emitter.scopes = emitter.scopes.slice(0, fin.scopeDepth); + emitter.finallyStack = savedFinally.slice(0, i); + emitter.tryStack = savedTry.slice(0, fin.tryDepth); + if (pretSlot !== null) emitter.scopes.push([{ slot: pretSlot, type: v!.type }]); + emitter.emitBlock(fin.body); + if (pretSlot !== null) emitter.scopes.pop(); + } + if (!B.isTerminated()) emitter.releaseForJump(0, 0); + emitter.frames = savedFrames; + emitter.scopes = savedScopes; + emitter.finallyStack = savedFinally; + emitter.tryStack = savedTry; + } else if (moved === null) { + emitter.releaseForJump(0, 0); + } + if (!B.isTerminated()) { + if (genSettles) emitter.emitAsyncGenSettled(); + if (emitter.currentCoro !== null) { + emitter.emitWasiFulfill(v); + B.terminate(`br label %${emitter.currentCoro.finalLabel}`); + } else if (v === null) B.terminate("ret void"); + else if (emitter.currentInt32Result) + B.terminate(`ret i32 ${emitter.int32Argument(v, s.value!)}`); + else if (emitter.currentUnionResult) B.terminate(`ret ${SCALAR_UNION_RESULT} ${v.name}`); + else B.terminate(`ret ${emitter.llType(s.value!.type)} ${v.name}`); + } + break; + } + case "throw": { + // Evaluate, move ownership into the runtime's exception cell, then + // unwind unconditionally (the innermost try handler, or out of the + // function) — the same release path as return/break/continue. + const v = emitter.emitExpr(s.value); + if (isRefCounted(s.value.type)) emitter.toHeap(v); // the cell takes ownership + emitter.emitThrowValue({ name: v.name, type: s.value.type }); + emitter.emitUnwind(); + break; + } + case "rethrow": { + // Re-raise the saved snapshot (payload retained — the binding + // local releases with its scope) and unwind like `throw`. + const c = emitter.emitExpr({ kind: "varRef", localId: s.localId, type: CAUGHT, loc: s.loc }); + emitter.declare(`declare void @scr_rethrow(ptr)`); + B.line(`call void @scr_rethrow(ptr ${c.name})`); + emitter.emitUnwind(); + break; + } + case "runtimeFence": { + // The deferred JS compile fence: throw a catchable Error naming + // the construct (message) with the SC code stamped on `code`, + // then unwind exactly like `throw`. SCR_ERR_ERROR = 0. + const bytes = Buffer.byteLength(s.message, "utf8"); + emitter.declare(`declare void @scr_throw_error_msg_code(i32, ptr, ${emitter.sizeType}, ptr)`); + B.line( + `call void @scr_throw_error_msg_code(i32 0, ptr ${emitter.cstr(s.message)}, ${emitter.sizeType} ${bytes}, ptr ${emitter.cstr(s.code)})`, + ); + emitter.emitUnwind(); + break; + } + case "tryCatch": + emitter.emitTryCatch(s); + break; + default: { + // Statement coverage is now total (bytesSet closed the set) — + // keep throwing LlvmUnsupportedError for any future IR statement kind. + const rest: never = s; + const k = (rest as IrStmt).kind; + throw new LlvmUnsupportedError(`stmt:${k}`, (rest as IrStmt).loc); + } + } + const frame = emitter.frames.pop()!; + // return/throw already released their frames on the jump path; the + // fall-through releases after them would be dead double-release code. + if ( + s.kind !== "return" && + s.kind !== "throw" && + s.kind !== "rethrow" && + s.kind !== "runtimeFence" + ) { + emitter.releaseTemps(frame); + } +} + +/** try/catch/finally via pending-flag unwinding — stmts.ts's + * emitTryCatch, block-flavored. Entering a try emits NO code: the try + * context is compile-time state (tryStack) redirecting unwinds inside + * the region to a label here. Shape: + * + * { try body } unwinds inside release frames/scopes down to + * this statement's depths, then br the handler + * br after (normal completion skips the handler) + * try.c: (emitted only when some unwind targets it) + * binding = scr_exc_take() (or scr_exc_clear() when bindingless) + * { catch body } + * after: { finally body } normal path + * br try.e + * try.fx: exception path: the pending exception is + * stash = scr_exc_take() STASHED across the finally body so the + * { finally body } body's own pending checks answer for + * scr_rethrow(stash) themselves; a throw inside REPLACES the + * stash (it unwinds through the synthetic + * try.e: scope entry) — JS's semantics exactly + * + * 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. */ +export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" }): void { + const B = emitter.B; + const hasCatch = s.catchBody !== null; + const hasFinally = s.finallyBody !== null; + const catchLabel = B.newLabel("try.c"); + const finExcLabel = B.newLabel("try.fx"); + const endLabel = B.newLabel("try.e"); + const afterTryLabel = hasFinally ? B.newLabel("try.f") : endLabel; + + const handler = { + label: hasCatch ? catchLabel : finExcLabel, + used: false, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + }; + if (hasFinally) { + emitter.finallyStack.push({ + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + tryDepth: emitter.tryStack.length, + body: s.finallyBody!, + }); + } + emitter.tryStack.push(handler); + emitter.emitBlock(s.tryBody); + emitter.tryStack.pop(); + B.br(afterTryLabel); // no-op when the try body already terminated + + // Exceptions raised in the CATCH body unwind to the exception-path + // finally (pending stays set through it) when one exists. + const excHandler = { + label: finExcLabel, + used: !hasCatch && handler.used, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + }; + + if (hasCatch && handler.used) { + B.startBlock(catchLabel); + if (hasFinally) emitter.tryStack.push(excHandler); + if (emitter.currentGenerator !== null) { + // Generator bodies: a pending GENRET sentinel (.return(v) + // injected at a yield) is a RETURN completion, not a throw — + // catch must not take it. Re-unwind past this handler (finally + // still runs — the unwind targets the exception-path finally or + // the enclosing context; the depths here equal the handler's). + emitter.declare(`declare zeroext i1 @scr_exc_genret_pending()`); + const gr = B.tmp(); + B.line(`${gr} = call zeroext i1 @scr_exc_genret_pending()`); + const lg = B.newLabel("try.gr"); + const lk = B.newLabel("try.gk"); + B.condBr(gr, lg, lk); + B.startBlock(lg); + emitter.emitUnwind(); + B.startBlock(lk); + } + if (s.catchLocalId !== null) { + // catch (e): the exception MOVES into the binding's snapshot box, + // owned by the catch body's scope (released on every exit — + // normal fall-through, jumps out, and unwinds from the body). + const slot = `%${mangleLocal(s.catchLocalId)}`; + emitter.declare(`declare ptr @scr_exc_take()`); + emitter.emitBlock(s.catchBody!, (scope) => { + const c = B.tmp(); + B.line(`${c} = call ptr @scr_exc_take() ; catch binding`); + if (emitter.currentLocals.get(s.catchLocalId!)?.boxed) { + const box = B.tmp(); + B.line(`${box} = ${boxNewCall(emitter.shapeHost, CAUGHT)} ; captured exception`); + emitter.boxSet(box, CAUGHT, c); + B.line(`store ptr ${box}, ptr ${slot}`); + scope.push({ slot, type: CAUGHT, boxed: true }); + } else { + B.line(`store ptr ${c}, ptr ${slot}`); + scope.push({ slot, type: CAUGHT }); + } + }); + } else { + emitter.declare(`declare void @scr_exc_clear()`); + B.line(`call void @scr_exc_clear() ; catch takes the exception`); + emitter.emitBlock(s.catchBody!); + } + if (hasFinally) emitter.tryStack.pop(); + B.br(afterTryLabel); // the catch's normal completion + } + if (hasFinally) emitter.finallyStack.pop(); + + if (hasFinally) { + B.startBlock(afterTryLabel); + emitter.emitBlock(s.finallyBody!); // normal path + B.br(endLabel); + if (excHandler.used) { + // The pending exception is STASHED across the finally body (a + // ScrCaught snapshot, re-raised after) so the body runs with a + // CLEAN cell — see stmts.ts's exception-path copy. The + // stash rides an alloca slot so a throw inside the body unwinds + // through the synthetic scope entry (replace semantics). + B.startBlock(finExcLabel); + emitter.declare(`declare ptr @scr_exc_take()`); + emitter.declare(`declare void @scr_rethrow(ptr)`); + emitter.declare(`declare void @scr_caught_release(ptr)`); + const stash = B.tmp(); + const stashSlot = B.slot(); + B.entryAllocas.push(`${stashSlot} = alloca ptr ; finally exception stash`); + B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`); + B.line(`store ptr ${stash}, ptr ${stashSlot}`); + emitter.scopes.push([{ slot: stashSlot, type: CAUGHT }]); + const suppressHandler = + s.suppressFinallyErrors || s.dropFinallyErrors + ? { + label: B.newLabel("try.fs"), + used: false, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + } + : null; + if (suppressHandler) emitter.tryStack.push(suppressHandler); + emitter.emitBlock(s.finallyBody!); + if (suppressHandler) emitter.tryStack.pop(); + emitter.scopes.pop(); // normal completion keeps the stash for the re-raise + B.line(`call void @scr_rethrow(ptr ${stash})`); + B.line(`call void @scr_caught_release(ptr ${stash})`); + emitter.emitUnwind(); + if (suppressHandler?.used) { + B.startBlock(suppressHandler.label); + const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress"; + emitter.declare(`declare void @${cleanup}(ptr)`); + B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`); + emitter.emitUnwind(); + } + } + B.startBlock(endLabel); + } else { + B.startBlock(endLabel); + } +} + +/** JS-exact switch: lazily evaluated, arbitrary-expression case tests in + * source order, bodies falling through in source order until a break — + * Literal-only tests can use native dispatch; other tests keep a chain + * of conditional branches. All case bodies + * share ONE scope; because dispatch can jump PAST a varDecl into a later + * case, refcounted/boxed case-body locals are NULL-reset up front and + * the scope-exit releases rely on NULL tolerance. */ +export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void { + const unitSwitch = charAtUnitSwitch(s); + if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch); + const B = emitter.B; + const discKind = s.disc.type.kind; + if (discKind !== "f64" && discKind !== "string" && discKind !== "bool") { + throw new LlvmUnsupportedError(`switch:${discKind}`, s.loc); + } + // The disc temp lives in the whole statement's frame: for a string + // discriminant it stays alive across every test and body, released + // when the switch statement ends (break lands past this statement's + // frame release — releaseForJump keeps the target's own frame). + const disc = emitter.emitExpr(s.disc); + for (const c of s.cases) { + for (const stmt of c.body) { + if (stmt.kind !== "varDecl") continue; + const local = emitter.currentLocals.get(stmt.localId)!; + if (local.boxed || isRefCounted(local.type)) { + B.line(`store ptr null, ptr %${mangleLocal(local.id)} ; case-scoped ${local.name}`); + } + } + } + const end = B.newLabel("sw.e"); + const caseLabels = s.cases.map(() => B.newLabel("sw.c")); + const defaultIdx = s.cases.findIndex((c) => c.test === null); + const fallback = defaultIdx >= 0 ? caseLabels[defaultIdx]! : end; + const nullableString = nullableStringSwitch(emitter, s); + if (nullableString !== null) { + // `undefined` matches no string literal; any other value is the + // string itself and dispatches as a string switch. + const value = emitter.emitExpr(nullableString.value); + const present = B.tmp(); + const dispatch = B.newLabel("sw.string"); + B.line(`${present} = icmp ne ptr ${value.name}, null`); + B.condBr(present, dispatch, fallback); + B.startBlock(dispatch); + const text: LlValue = { name: value.name, type: STRING }; + if (!emitLiteralSwitch(emitter, text, undefined, nullableString.cases, caseLabels, fallback)) { + nullableString.cases.forEach((c, i) => { + if (c.test?.kind !== "strLit") return; + const hit = B.tmp(); + B.line( + `${hit} = call zeroext i1 ${emitter.literalEquals(c.test.value)}(ptr ${value.name})`, + ); + const next = B.newLabel("sw.t"); + B.condBr(hit, caseLabels[i]!, next); + B.startBlock(next); + }); + B.br(fallback); + } + } else if (!emitLiteralSwitch(emitter, disc, s.disc, s.cases, caseLabels, fallback)) { + s.cases.forEach((c, i) => { + if (c.test === null) return; + // Lazy source-order test evaluation (a test after the match never + // runs). Each test's temps release right after its comparison. + emitter.frames.push([]); + const t = emitter.emitExpr(c.test); + const hit = B.tmp(); + if (c.test.kind === "strLit") { + B.line(`${hit} = call zeroext i1 ${emitter.literalEquals(c.test.value)}(ptr ${disc.name})`); + } else if (c.test.type.kind === "string") { + emitter.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`); + B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${disc.name}, ptr ${t.name})`); + } else if (c.test.type.kind === "bool") { + B.line(`${hit} = icmp eq i1 ${disc.name}, ${t.name}`); + } else { + B.line(`${hit} = fcmp oeq double ${disc.name}, ${t.name}`); + } + emitter.releaseTemps(emitter.frames.pop()!); + const next = B.newLabel("sw.t"); + B.condBr(hit, caseLabels[i]!, next); + B.startBlock(next); + }); + B.br(fallback); + } + + emitter.jumpTargets.push({ + kind: "switch", + brkLabel: end, + contLabel: null, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + const scope: LlScopeEntry[] = []; + emitter.scopes.push(scope); + s.cases.forEach((c, i) => { + B.br(caseLabels[i]!); // the previous body's natural fall-through + B.startBlock(caseLabels[i]!); + emitter.emitStmts(c.body); + }); + emitter.jumpTargets.pop(); + emitter.scopes.pop(); + // Natural fall-off of the last body releases the shared scope; a jump + // already released it before jumping. + const lastBody = s.cases[s.cases.length - 1]?.body; + if (!lastBody || !endsWithJump(lastBody)) emitter.releaseScope(scope); + B.br(end); + B.startBlock(end); +} + +/** Emit a loop's condition, body and update through `emitCopy`, twice + * when speculative-integers.ts plans a fast integer copy. Both copies + * share the loop's setup and exit; the ordinary copy comes first so the + * fast copy is only emitted when every resume point exists. */ +export function emitLoopCopies( + emitter: LlEmitter, + loop: Extract, + allowed: boolean, + exit: string, + emitCopy: (exit: string) => void, +): void { + const plan = + allowed && + emitter.speculativeLoopsEnabled && + emitter.speculation === null && + emitter.speculationResume === null + ? planSpeculativeLoop( + loop, + (id) => emitter.speculativeLocal(id), + (id) => emitter.integerLoopBindings.has(id) || emitter.int32Locals.has(id), + (e) => { + const range = emitter.integerRanges.get(e); + return ( + range !== undefined && + range !== null && + range.min >= -Number.MAX_SAFE_INTEGER && + range.max <= Number.MAX_SAFE_INTEGER + ); + }, + (id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id), + ) + : null; + if (!plan) { + emitCopy(exit); + return; + } + const B = emitter.B; + const entries = new Map(); + let ok = "true"; + for (const [id, live] of plan.locals) { + if (!live) continue; + const value = B.tmp(); + B.line(`${value} = load double, ptr ${emitter.binding(id).slot}`); + const [integer, exact] = emitSafeIntegerCheck(emitter, value); + entries.set(id, integer); + if (ok === "true") ok = exact; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${ok}, ${exact}`); + ok = both; + } + } + const fast = B.newLabel("spec.fast"), + slow = B.newLabel("spec.slow"); + B.condBr(ok, fast, slow); + const resume = new Map(); + for (const s of plan.writes) resume.set(s, { label: B.newLabel("spec.resume"), placed: false }); + B.startBlock(slow); + emitter.speculationResume = resume; + try { + emitCopy(exit); + } finally { + emitter.speculationResume = null; + } + B.startBlock(fast); + if (![...resume.values()].every((r) => r.placed)) { + B.br(slow); + return; + } + for (const id of plan.locals.keys()) { + const shadow = B.slot(); + B.entryAllocas.push( + `${shadow} = alloca i64 ; speculative integer ${emitter.currentLocals.get(id)?.name ?? id}`, + ); + const entry = entries.get(id); + if (entry !== undefined) { + // The same double, now visibly an integer conversion: the double + // phis of the fast copy fold into integer phis. + const exact = B.tmp(); + B.line(`store i64 ${entry}, ptr ${shadow}`); + B.line(`${exact} = sitofp i64 ${entry} to double`); + B.line(`store double ${exact}, ptr ${emitter.binding(id).slot}`); + } else B.line(`store i64 0, ptr ${shadow}`); + emitter.integerLoopBindings.set(id, { + slot: shadow, + type: "i64", + signed: true, + range: SPECULATIVE_RANGE, + }); + } + // A global's store stays in the loop (calls may read it). When nothing + // in the loop can observe it, its double is written once on leaving the + // copy instead, so the loop keeps it in a register as the double copy + // would. + const deferred = new Set(); + // Worker loops also leave through their cancellation poll. + if (plan.observerFree && !emitter.mod.workers) + for (const s of plan.writes) { + const id = speculativeWriteTarget(s)!.localId; + if (!emitter.currentLocals.has(id)) deferred.add(id); + } + emitter.speculation = { + writes: plan.writes, + resume: new Map([...resume].map(([s, r]) => [s, r.label])), + deferred, + }; + try { + if (deferred.size === 0) emitCopy(exit); + else { + const leave = B.newLabel("spec.leave"); + emitCopy(leave); + B.startBlock(leave); + emitter.writeBackSpeculativeGlobals(deferred); + B.br(exit); + } + } finally { + emitter.speculation = null; + for (const id of plan.locals.keys()) emitter.integerLoopBindings.delete(id); + } +} + +/** A speculated write in the fast copy: compute the value as a safe + * integer, store it to the shadow and its exact double to the local. + * Any failed check re-executes the statement in the ordinary copy. */ +export function emitSpeculativeWrite(emitter: LlEmitter, s: IrStmt, resume: string): void { + const B = emitter.B; + const target = speculativeWriteTarget(s)!; + const binding = emitter.integerLoopBindings.get(target.localId)!; + const failed = B.newLabel("spec.bail"); + let bails = false; + emitter.declare("declare i1 @llvm.expect.i1(i1, i1)"); + const bail = (condition: string): void => { + if (condition === "false") return; + bails = true; + const expected = B.tmp(); + B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${condition}, i1 false)`); + const next = B.newLabel("spec.ok"); + B.condBr(expected, failed, next); + B.startBlock(next); + }; + // Heap reads come first, before any check, and release their temporaries + // at once: a bailing path then owns nothing, and reads have no effect + // whose reordering before the arithmetic could be observed. + const reads = new Map(); + if (target.value !== null) { + emitter.frames.push([]); + for (const read of speculativeReads(target.value)) reads.set(read, emitter.emitExpr(read)); + emitter.releaseTemps(emitter.frames.pop()!); + } + emitter.frames.push([]); + let result: string; + if (target.value === null) { + const old = B.tmp(); + result = B.tmp(); + B.line(`${old} = load i64, ptr ${binding.slot}`); + B.line(`${result} = ${target.op === "+" ? "add" : "sub"} nsw i64 ${old}, 1`); + const biased = B.tmp(), + outside = B.tmp(); + B.line(`${biased} = add i64 ${result}, ${Number.MAX_SAFE_INTEGER}`); + B.line(`${outside} = icmp ugt i64 ${biased}, ${2 * Number.MAX_SAFE_INTEGER}`); + bail(outside); + } else result = emitSpeculativeInteger(emitter, target.value, bail, reads); + // Speculable values read only numbers past this point. + if (emitter.frames.pop()!.length > 0) + throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary"); + B.line(`store i64 ${result}, ptr ${binding.slot}`); + const deferred = emitter.speculation!.deferred; + if (!deferred.has(target.localId)) { + const number = B.tmp(); + B.line(`${number} = sitofp i64 ${result} to double`); + B.line(`store double ${number}, ptr ${emitter.binding(target.localId).slot}`); + } + if (!bails) return; + const done = B.newLabel("spec.done"); + B.br(done); + B.startBlock(failed); + emitter.writeBackSpeculativeGlobals(deferred); + B.br(resume); + B.startBlock(done); +} + +/** + * Lower canonical `target = target + suffix` after evaluating the old + * left side and suffix in JavaScript order. The snapshot stays owned by + * the statement frame while the destination relinquishes its CURRENT + * value, making the snapshot unique unless a real observable alias exists. + */ +export function emitStringSelfConcatAssign( + emitter: LlEmitter, + localId: string, + left: IrExpr, + suffix: IrExpr, + retainForYield: boolean, +): LlValue { + const moved = emitter.emitMovedSelfConcat(localId, left, suffix, retainForYield); + if (moved) return moved; + const snapshot = emitter.emitExpr(left); + // A suffix of several parts, or a number, appends each operand to the + // snapshot directly (in place once the binding lets go of it) instead + // of first building the suffix as a separate string. + const parts = stringParts(suffix); + const mixed = parts.length > 1 || numberPart(parts[0]!) !== null; + const charCode = mixed ? null : emitter.emitSingleCharCode(suffix); + const inputs = mixed ? emitConcatInputs(emitter, parts) : []; + // The concat only reads the suffix; relinquishing the binding below + // runs no code that could rebind a borrowed local. + const right = mixed || charCode !== null ? null : emitStringInputs(emitter, [suffix])[0]!; + const b = emitter.binding(localId); + const B = emitter.B; + if (b.kind === "boxed") { + // set_ref(NULL) unlinks then releases the binding's post-suffix value. + emitter.boxSet(emitter.loadBox(b.slot), b.type, "null"); + } else { + const old = B.tmp(); + B.line(`${old} = load ptr, ptr ${b.slot}`); + B.line(`store ptr null, ptr ${b.slot}`); + if (b.kind === "global") emitter.releaseHeap(old, b.type); + else emitter.releaseFrame(old, b.type); + } + let raw: string; + if (charCode !== null) { + // The snapshot moves into the consuming character append. + emitter.toFrame(snapshot); + emitter.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`); + raw = B.tmp(); + B.line( + `${raw} = call ptr @scr_str_concat_move_char_code(ptr ${snapshot.name}, double ${charCode.name})`, + ); + } else if (right === null) { + raw = emitMixedConcat(emitter, snapshot.name, inputs); + } else { + emitter.declare(`declare ptr @scr_str_concat(ptr, ptr)`); + raw = B.tmp(); + B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`); + } + const result = emitter.own({ name: raw, type: left.type }); + if (retainForYield) { + const stored = + b.kind === "local" + ? emitter.retainFrame(result.name, result.type) + : emitter.retainHeap(result.name, result.type); + if (b.kind === "boxed") emitter.boxSet(emitter.loadBox(b.slot), b.type, stored); + else B.line(`store ptr ${stored}, ptr ${b.slot}`); + } else { + if (b.kind === "local") emitter.toFrame(result); + else emitter.toHeap(result); + if (b.kind === "boxed") emitter.boxSet(emitter.loadBox(b.slot), b.type, result.name); + else B.line(`store ptr ${result.name}, ptr ${b.slot}`); + } + return result; +} + +/** `switch (x)` on a `string | undefined` value whose case tests are all + * string literals: the frontend's `switch (true)` over `x === "lit"` union + * equalities against one switch local. The local and the cases with plain + * string literal tests. */ +function nullableStringSwitch( + emitter: LlEmitter, + s: IrStmt & { kind: "switch" }, +): { value: IrExpr; cases: (IrStmt & { kind: "switch" })["cases"] } | null { + if (s.disc.kind !== "boolLit" || !s.disc.value) return null; + let value: (IrExpr & { kind: "varRef" }) | null = null; + const cases: (IrStmt & { kind: "switch" })["cases"] = []; + for (const c of s.cases) { + const test = c.test; + if (test === null) { + cases.push(c); + continue; + } + if ( + test.kind !== "unionEq" || + test.negated || + test.sameValue || + test.left.kind !== "varRef" || + test.right.kind !== "unionWrap" || + test.right.value.kind !== "strLit" + ) + return null; + const nullable = emitter.nullableUnions.get(test.unionId); + if ( + nullable === null || + nullable.repr !== "single" || + nullable.nullTag !== -1 || + nullable.arm.kind !== "string" || + test.right.tag !== nullable.refTag + ) + return null; + if (value !== null && value.localId !== test.left.localId) return null; + value = test.left; + cases.push({ ...c, test: test.right.value }); + } + return value === null ? null : { value, cases }; +} + +/** `switch (s.charAt(i))` (or `switch (s[i])`) whose case tests are all + * one-unit string literals, as a switch on `s.charCodeAt(i)` with the units + * as cases: no character string is built. An index out of range answers + * NaN, which matches no case, as "" matches no one-unit literal. */ +function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null { + const disc = s.disc; + if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1) + return null; + if (!s.cases.some((c) => c.test !== null)) return null; + if ( + !s.cases.every( + (c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1), + ) + ) + return null; + return { + ...s, + disc: { ...disc, method: "charCodeAt", type: F64 }, + cases: s.cases.map((c) => + c.test?.kind === "strLit" + ? { + ...c, + test: { kind: "numLit", value: c.test.value.charCodeAt(0), type: F64, loc: c.test.loc }, + } + : c, + ), + }; +} diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index e86b447d9..f7b9f2517 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,9 +1,7 @@ import { jsTruthy } from "./jsval.js"; -import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js"; +import { type ArraySearchKind } from "./array-search.js"; import { emitStringInputs } from "./string-lifetimes.js"; -import { emitLlvmLayouts } from "./layouts.js"; -import { emitThreadSlotsIntrinsic, threadSlotsBlockDefinitions } from "./thread-slots.js"; -import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js"; +import { emitThreadSlotsIntrinsic } from "./thread-slots.js"; import { CYC_BUF, CYC_HDR, @@ -13,18 +11,16 @@ import { WEAK_KEYS_GATE, } from "./alloc.js"; import { youngNurseryRequested } from "../young-nursery.js"; -import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; +import { llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; import { createHash } from "node:crypto"; import { endsWithJump, matchFieldSelfConcat, - matchStringSelfConcat, readsArraySlot, streamTypedRefEligible, undefinedArmTag, } from "../../ir/analysis.js"; -import { emitLibraryIdentityLines } from "../library-identity-markers.js"; import { loopTestsEveryIteration } from "../loop-polls.js"; import type { IrBytesElem, @@ -42,10 +38,7 @@ import type { SrcLoc, } from "../../ir/ir.js"; import { - CAUGHT, - ffiCallbackType, isDynTypedRefType, - isFfiContextParam, isRefCounted, isUnitType, POINTER_KINDS, @@ -56,77 +49,37 @@ import { RUNTIME_STREAM_CLASSES, typeEquals, typeKey, - STRING, VOID, classJsName, } from "../../ir/ir.js"; import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; +import { type DenseArrayLoop } from "./dense-array-loops.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import { matchBytesLoopVersion } from "./bytes-loop-versions.js"; +import { globalSlotAlias } from "./bytes-alias.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import { - matchIntegerArrayForLoop, - matchIntegerBytesForLoop, - matchIntegerCountedForLoop, -} from "../../ir/integer-loops.js"; -import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; -import { - countedDenseShape, - emitDenseLoopGuard, - matchDenseArrayLoop, - type DenseArrayLoop, -} from "./dense-array-loops.js"; -import { emitCountedLoopLimit } from "./counted-loops.js"; -import { - emitSafeIntegerCheck, - emitSpeculativeInteger, - planSpeculativeLoop, - speculativeReads, - speculativeWriteTarget, - SPECULATIVE_RANGE, -} from "./speculative-integers.js"; -import { emitLiteralSwitch } from "./switch-dispatch.js"; -import { findBytesBounds } from "./bytes-bounds.js"; -import { - emitByteWindowGuard, - findInitializedByteLoopBindings, - matchByteWindow, -} from "./byte-windows.js"; -import { - canDeferSplitPiece, - emitSplitSpanNext, materializeSplitPiece, type SplitSpan, - emitSplitCursor, - emitSplitNext, - emitSplitScratch, - emitSplitSnapshot, - findPrivateSplitLocals, - loadSplitSnapshot, - storeSplitSnapshot, - stringSplit, type StoredSplitSnapshot, type StringSplit, } from "./split-loops.js"; import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; -import { RESULT_DST_ID, RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js"; +import { addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; -import { - analyzeIntegerRanges, - INT32_RANGE, - type IntegerRange, - type IntegerRanges, -} from "../../ir/integer-ranges.js"; +import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; import { resolveSelfCaptures } from "../../ir/self-captures.js"; import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js"; -import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js"; +import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js"; import { foldConstantGlobals } from "../../ir/constant-globals.js"; import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js"; import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js"; import { hoistFinalFieldReads } from "../../ir/field-values.js"; -import { findIntegerViews } from "./integer-views.js"; import { findInt32Abis, INT32_ABI_SUFFIX, @@ -134,22 +87,10 @@ import { SCALAR_UNION_RESULT, type Int32Abi, } from "./int32-abi.js"; -import { findInt32Locals, type Int32Local } from "./int32-locals.js"; -import { - emitIntegerClone, - emitIntegerDispatcher, - findIntegerClones, - NO_INTEGER_CLONES, - type IntegerClones, -} from "./integer-clones.js"; +import { type Int32Local } from "./int32-locals.js"; +import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js"; import { exactInteger, narrowInteger } from "./integer-values.js"; -import { - conditionalLoopWrites, - findInitializerBindings, - initializerLocalCandidates, - withInitializerBindings, - withInitializerLocals, -} from "../../ir/initializer-bindings.js"; +import { findInitializerBindings } from "../../ir/initializer-bindings.js"; import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js"; import { allocateFfiCallbackAdapters, @@ -157,47 +98,28 @@ import { hasRetainedFfiCallback, type FfiCallbackAdapter, } from "../ffi-callbacks.js"; -import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; import { computeMayThrow } from "../may-throw.js"; import { - mangleArgPack, - mangleAsyncAdoptCall, - mangleAsyncAwaitCall, mangleAsyncInlineAwaitCall, mangleAsyncSpawn, mangleBorrowedFunction, mangleClassObj, - mangleClassStruct, mangleFnClosure, mangleFunction, - mangleGenDrop, - mangleGenSourceClose, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, - mangleTrampoline, mangleWrapper, mangleBorrowingWrapper, - mangleVtInstance, } from "../mangle.js"; import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js"; -import { canStackUnion, emitStackUnion, findLocalStackUnions } from "./stack-unions.js"; import { - emitStackObject, - findLocalStackObjects, - findReassignedStackObjects, - stackObjectFields, stackObjectStruct, type StackObjectField, type StackObjectSource, } from "./stack-objects.js"; import { - allocateLocalUnion, - emitScalarOptional, - findLocalUnionStorage, - isScalarOptionalSource, - storeLocalUnion, type LocalUnionStorageProof, type LocalUnionStorage, type UnionSources, @@ -212,63 +134,30 @@ import { storeInlineUnion, unionBoxBits, } from "./inline-unions.js"; -import { - emitLocalArrayRead, - emitProjectedArrayRead, - emitBorrowedArrayRead, - findLocalArrayReads, - findCallArrayReads, - OptionalArrayReads, - type LocalArrayRead, -} from "./local-array-reads.js"; -import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js"; -import { - emitDenseArrayGet, - emitDenseArrayPop, - emitDenseArraySet, - emitDenseSlotUnlink, -} from "./dense-array-access.js"; -import { - emitStackMapRead, - findMapReadLifetimes, - findReadNumberMapWrites, - matchMapRead, - type MapReadLifetimes, -} from "./map-read-lifetimes.js"; -import { findMapEntryFusions, type MapEntryFusion } from "./map-entry-fusion.js"; +import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js"; +import { CheckedNarrows } from "./checked-narrows.js"; +import { emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js"; +import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js"; +import { type MapEntryFusion } from "./map-entry-fusion.js"; import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js"; -import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js"; -import { analyzeWalks, findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js"; +import { emitBorrowedInputs } from "./borrowed-inputs.js"; +import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js"; import { lastUseRead } from "./local-extents.js"; import { findReboundParameters } from "./rebound-parameters.js"; import { ReferenceEffects } from "./reference-effects.js"; import { ReferenceWrites, type Writes } from "./reference-writes.js"; import { LlvmDebugInfo } from "./debug-info.js"; import { StackCallbacks } from "./stack-callbacks.js"; -import { dynHoleRead, findLoopArrayBorrows } from "./loop-array-borrows.js"; import { findAppendedArrayLiterals } from "./appended-array-literals.js"; import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js"; import { - findScalarStringSlices, - findScratchStringSlices, findLentStringParameters, - emitStringSliceSnapshot, type LentStringParameters, type StringSliceSnapshot, } from "./string-slices.js"; import { LazyCaptures } from "./lazy-captures.js"; import { StackCaptures } from "./stack-captures.js"; -import { - ABSENT_FIELD_PAYLOAD, - emitFieldAbsentTest, - exactIndexLines, - f64Lit, - ffiNativeTypeLl, - ffiNativeParamLl, - ffiNativeReturnLl, - llvmCommentText, - sizeToDouble, -} from "./common.js"; +import { emitFieldAbsentTest, f64Lit } from "./common.js"; import { ffiExtendsNarrowIntegers } from "../targets.js"; import { emitLiteralExpr, @@ -278,7 +167,7 @@ import { emitRecordExpr, } from "./expr-primitives.js"; import { emitControlExpr } from "./expr-control.js"; -import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js"; +import { emitCallExpr } from "./expr-calls.js"; import { emitDynamicExpr } from "./expr-dynamic.js"; import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js"; import { PublishWalkers } from "./publish.js"; @@ -321,7 +210,6 @@ import { emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, - emitDiscardedSplice, wrapNullable, emitMapNew, mapSet, @@ -371,8 +259,6 @@ import { import { buildClassGraph, classMembershipIntervals, - emitClassMembershipHelper, - classEnvironmentIndex, classFieldIndex, classStructSym, programLifetimeClass, @@ -385,13 +271,6 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import { LlWalkers } from "./walkers.js"; import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js"; import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js"; -import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; -import { - MAP_ITER_DECL, - NUMBER_MAP_ENTRY_DECL, - STRING_MAP_ENTRY_DECL, -} from "./map-number-lookup.js"; -import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; import { emitConcatInputs, emitMixedConcat, @@ -404,30 +283,60 @@ import { boxNewCall, boxReleaseSym, computeTraced, - elemAccess, emitInlineRcHelpers, emitNullableRetainWrappers, emitShapeUnionHelpers, FN_ATTRS, inlineRcDecls, type InlineRcMode, - limitInlineDynReleases, llFieldType, mapFlagsOffset, type MapKeyAccess, releaseSym, retainSym, traceArg, - typedRefConstructor, vAdapters, } from "./shapes.js"; import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js"; -import { INLINE_LITERAL_BYTES, literalEqualityDefinition } from "./string-literal-equality.js"; -import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js"; +import { INLINE_LITERAL_BYTES } from "./string-literal-equality.js"; +import { + emitFfiCallbackDefs, + emitLibDefs, + emitModuleParts, + emitParts, + helperDefs, +} from "./emit-module.js"; +import { + emitLoopCopies, + emitSpeculativeWrite, + emitStmtBody, + emitStringSelfConcatAssign, + emitSwitch, + emitTryCatch, +} from "./emit-statements.js"; +import { + emitArgPackAndTrampolinePrologue, + emitAsyncAwaitCall, + emitAsyncScaffolding, + emitFunction, + emitGenScaffolding, + emitWasiFulfill, + fiberlessAsyncFunctions, + parkingFunctions, +} from "./emit-function.js"; +import { + analyzeBorrowedReturns, + canBorrowCallArgument, + canBorrowReceiver, + canStackReceiver, + emitReadReceiver, + fieldAliasMetadata, + isStackUnionSource, +} from "./receivers.js"; export { LlvmUnsupportedError } from "./unsupported.js"; -interface LlArgPackAndTrampolinePrologue { +export interface LlArgPackAndTrampolinePrologue { definitions: string[]; pack: string; lifted: boolean; @@ -438,7 +347,7 @@ interface LlArgPackAndTrampolinePrologue { argPackLines: string[]; } -function ffiCallbackDummyLl(callback: IrFfiCallbackParam["callback"]): string { +export function ffiCallbackDummyLl(callback: IrFfiCallbackParam["callback"]): string { switch (callback.returns) { case "i64": case "u64": @@ -464,7 +373,7 @@ function ffiCallbackDummyLl(callback: IrFfiCallbackParam["callback"]): string { } } -interface LlScopeEntry { +export interface LlScopeEntry { slot: string; type: IrType; boxed?: boolean; @@ -568,7 +477,7 @@ export function emitLlvmModuleSource( return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); } -function llStrBytes(text: string): string { +export function llStrBytes(text: string): string { return llBytes(wtf8Bytes(text)); } @@ -576,7 +485,7 @@ const NO_BORROWED: ReadonlySet = new Set(); /** Runtime entry points (and await-call entries) that may park the running * execution context, as they appear in emitted function text. */ -const SUSPENDS = +export const SUSPENDS = /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)|sc_a[wd]_)/; /** The suspensions of a fiber body's own awaits, which its stackless * coroutine form suspends itself at instead. */ @@ -587,7 +496,7 @@ const COROUTINE_SUSPENDS = /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_(?!self\b|promise\b))|sc_a[wd]_)/; /** Captured bindings (boxes a body declares or receives) from which a * function compiles without LLVM optimization (see boxSlotCount). */ -const UNOPTIMIZED_BOX_SLOTS = 512; +export const UNOPTIMIZED_BOX_SLOTS = 512; /** ScrExcKind tags of a generator slot holding a number or a boolean. */ const SCR_EXC_F64 = 1; const SCR_EXC_BOOL = 2; @@ -605,7 +514,7 @@ const SCR_EXC_BOOL = 2; * optnone bodies, as the native helper's does. Each use reloads its box * pointer and code generation stays linear. Semantics, boxes and * environments are unchanged; only that body's machine code is slower. */ -function boxSlotCount(fn: IrFunction): number { +export function boxSlotCount(fn: IrFunction): number { const captured = new Set(); for (const capture of fn.captures ?? []) captured.add(capture.localId); for (const capture of fn.classCaptures ?? []) captured.add(capture.localId); @@ -616,6 +525,9 @@ function boxSlotCount(fn: IrFunction): number { /** Whether any node of `e` names the local (reads, writes, declarations, * captures). */ +/** Typed-array loop versions nest at most this deep (each doubles a body). */ +export const MAX_BYTES_LOOP_VERSION_DEPTH = 3; + function mentionsLocal(e: IrExpr, localId: string): boolean { const exprNames = (node: IrExpr): boolean => { if (node.kind === "varRef" || node.kind === "incDec" || node.kind === "assignExpr") @@ -649,26 +561,26 @@ function mentionsLocal(e: IrExpr, localId: string): boolean { export class LlEmitter { localArrayReads = new Map(); - private loopArrayBorrows: ReadonlySet = new Set(); - private scalarStringSlices = new Map(); + loopArrayBorrows: ReadonlySet = new Set(); + scalarStringSlices = new Map(); readonly stringSlices = new Map(); /** Slices written into stack buffers (findScratchStringSlices). */ scratchStringSlices: ReadonlySet = new Set(); /** String parameters a caller may lend scratch slices to. */ - private lentStringParameters: LentStringParameters = new Map(); - private localStackUnions = new Map(); - private localStackObjects = new Map(); - private reassignedStackObjects = new Set(); + lentStringParameters: LentStringParameters = new Map(); + localStackUnions = new Map(); + localStackObjects = new Map(); + reassignedStackObjects = new Set(); /** The binding slot of each reassigned stack-object local. */ - private stackObjectSlots = new Map(); + stackObjectSlots = new Map(); /** Entry-block pointers to the reference fields of each frame object * slot (stack-objects.ts stackObjectFields). */ stackObjectFieldPtrs = new Map(); - private localUnionStorageProofs = new Map(); - private localUnionStorage = new Map(); + localUnionStorageProofs = new Map(); + localUnionStorage = new Map(); integerArrayBindings = new Set(); - private readonly fieldAliasTags = new Map(); - private readonly fieldPointerTags = new Map(); + readonly fieldAliasTags = new Map(); + readonly fieldPointerTags = new Map(); /** Native class fields with different property names cannot occupy the * same slot of one object. Inherited views of the same property share a @@ -726,22 +638,13 @@ export class LlEmitter { const tag = this.fieldPointerTags.get(ptr); return tag === undefined ? "" : `, !tbaa !${tag}`; } - - private fieldAliasMetadata(): string[] { - if (this.fieldAliasTags.size === 0) return []; - const lines = ['!0 = !{!"scriptc native fields"}']; - for (const tag of this.fieldAliasTags.values()) { - lines.push( - `!${tag - 1} = !{!"field.${tag}", !0, i64 0}`, - `!${tag} = !{!${tag - 1}, !${tag - 1}, i64 0}`, - ); - } - return lines; + fieldAliasMetadata(): string[] { + return fieldAliasMetadata(this); } - private readonly debug: LlvmDebugInfo | null; + readonly debug: LlvmDebugInfo | null; /** Projection-only scalar objects may live in frame storage (stack-objects.ts). */ readonly stackObjects: boolean; - private debugScope: string | null = null; + debugScope: string | null = null; readonly sizeType: "i32" | "i64"; readonly ffiExtendNarrowIntegers: boolean; readonly cycleBufOffset: number; @@ -760,48 +663,48 @@ export class LlEmitter { * generators' conditions; library and Windows builds keep fibers. */ private stacklessAsyncAllowed = false; /** The async functions selectStacklessAsync lowered as coroutines. */ - private readonly stacklessAsync = new Set(); + readonly stacklessAsync = new Set(); /** Some function in the module is lowered as a coroutine. */ - private usesCoroutines = false; + usesCoroutines = false; /** The target lacks a rounding instruction at its baseline CPU (x86-64 * before SSE4.1), so `llvm.trunc` would lower to a libm call: emit * truncation inline through an integer round trip instead. */ readonly inlineTrunc: boolean; /** ELF worker executables give thread-locals the executable TLS models. */ - private readonly executableTls: boolean; + readonly executableTls: boolean; /** Source loops enclosing the statement being emitted in the current * function body. */ - private loopDepth = 0; + loopDepth = 0; /** Darwin worker executables read the stack guard from a pthread * thread-specific-data slot off the thread pointer (scr_runtime.h's * SCR_STACK_GUARD_TSD): the architecture names the access sequence. */ private readonly stackGuardTsd: "aarch64" | "x86_64" | null; - private readonly emitLibraryIdentity: boolean; - private readonly runtimeAbiMarker: boolean; - private readonly detachedExit: boolean; + readonly emitLibraryIdentity: boolean; + readonly runtimeAbiMarker: boolean; + readonly detachedExit: boolean; /** Interned string literals: UTF-8 text → { symbol, byte length } — * first-use order, the runtime ABI’s determinism discipline. */ - private readonly literals = new Map(); + readonly literals = new Map(); /** Literal equality helpers: literal text → helper symbol (literalEquals). */ - private readonly literalEqualities = new Map(); + readonly literalEqualities = new Map(); /** Interned bigint literals: hex magnitude with sign → { symbol, value }. */ - private readonly bigintLiterals = new Map(); + readonly bigintLiterals = new Map(); /** Symbols of interned constant data, named by content (contentSymbol). */ private readonly contentSymbols = new Set(); /** Interned unit-armed union instances: "unionId:tag" → symbol — one * immortal (rc == SIZE_MAX) static per (union, unit tag). * RC entry points and the collector skip immortals. */ - private readonly unitInstances = new Map(); + readonly unitInstances = new Map(); /** unionId → symbol of the union's immortal ABSENT field-slot instance. */ - private readonly absentInstances = new Map(); + readonly absentInstances = new Map(); /** The zeroed box an inline store reads for a NULL union dummy. */ - private needsUnionNone = false; + needsUnionNone = false; /** unionId → symbol of a flag set once a checked-dynamic cast caches one * of the union's boxes. A cached box is refreshed in place, possibly with * a tag that another union numbers apart, so it is never shared. */ - private readonly retypeGuards = new Map(); + readonly retypeGuards = new Map(); /** The nullable-union ABSENT sentinel was referenced (absentInstanceRef). */ - private needsNullableAbsent = false; + needsNullableAbsent = false; private readonly immortalValues = new Set(); /** Regex literal templates: "/" → { symbol, interned * source/flags literal refs } — one immortal ScrRegex template per distinct @@ -819,9 +722,9 @@ export class LlEmitter { readonly templateStringsInstances = new Map(); /** Interned NUL-terminated C-string constants (scr_jb_puts labels, the * stringify indent text): UTF-8 text → { symbol, byte length }. */ - private readonly cstrs = new Map(); + readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ - private readonly mapOpsConsts = new Map(); + readonly mapOpsConsts = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ @@ -832,17 +735,17 @@ export class LlEmitter { /** Explicit callback boundary for standalone layout and walker stages. */ readonly shapeHost: DynHost; /** External declarations, in first-use order. */ - private readonly decls = new Set(); + readonly decls = new Set(); /** Declared functions referenced as values: each needs an env-signature * wrapper + an interned immortal closure (so `f === f` holds). */ readonly fnValues = new Set(); - private needsOom = false; + needsOom = false; needsBadTag = false; needsBadKey = false; - private needsRetainBox = false; + needsRetainBox = false; /** Inline RC fast-path helpers requested by call sites (shapes.ts); null * when inline RC is off. */ - private readonly rcHelpers: Set | null; + readonly rcHelpers: Set | null; private readonly rcMode: InlineRcMode | undefined; private readonly rcHelpersNoinline: boolean; @@ -851,11 +754,11 @@ export class LlEmitter { appendedArrayLiterals: ReadonlySet = new Set(); /** Emitted LLVM text of every IR function body, by function name: the * fiberless-async analysis scans it for suspension entry points. */ - private readonly fnDefText = new Map(); + readonly fnDefText = new Map(); readonly referenceEffects: ReferenceEffects; /** Field-sensitive reference writes (null in debug builds). */ referenceWrites: ReferenceWrites | null = null; - private readonly borrowedReturnKeyCache = new Map(); + readonly borrowedReturnKeyCache = new Map(); /** The statement list being emitted and the current index, for blocks * whose bindings end with the list (emitBlock and function bodies). */ private stmtCursor: { stmts: IrStmt[]; index: number } | null = null; @@ -873,63 +776,63 @@ export class LlEmitter { currentConstantCallbacks: ReadonlyMap = new Map(); readonly stackCallbacks: StackCallbacks; readonly receiverReaders: AmbientReceiverReaders; - private readonly stackCaptures: StackCaptures; - private readonly lazyCaptures: LazyCaptures; + readonly stackCaptures: StackCaptures; + readonly lazyCaptures: LazyCaptures; /** Unchanged captured parameters of the current function: each maps to * the slot caching its box, NULL until the first closure needs it. */ - private readonly lazyCaptureBoxes = new Map(); - private readonly lazyStackBoxes = new Map(); - private borrowedParameters = new Set(); + readonly lazyCaptureBoxes = new Map(); + readonly lazyStackBoxes = new Map(); + borrowedParameters = new Set(); /** Captures of the current closure body that its environment holds * unchanged for the closure's whole life (LazyCaptures.stableCaptures). */ - private stableCaptureIds: ReadonlySet = new Set(); + stableCaptureIds: ReadonlySet = new Set(); /** Parameters proven projection-only: callers may pass stack boxes. */ - private projectedParameters = new Set(); - private stableCallBindings: ReadonlySet = new Set(); + projectedParameters = new Set(); + stableCallBindings: ReadonlySet = new Set(); /** Locals and rebound parameters holding borrowed pointers for their * whole lifetime (walk-borrows.ts), per function and for the current one. */ - private readonly walkBorrowsByFunction = new Map>(); - private walkBorrows: ReadonlySet = new Set(); + readonly walkBorrowsByFunction = new Map>(); + walkBorrows: ReadonlySet = new Set(); /** Functions whose borrowing body returns its result without a reference * (analyzeBorrowedReturns), and whether the current body is one. */ readonly borrowedReturns = new Set(); /** Rebound parameters that keep the borrowed convention with an owner * slot (rebound-parameters.ts), per function, and the current owner slots. */ - private readonly reboundByFunction = new Map>(); - private readonly reboundOwnerSlots = new Map(); - private currentBorrowedReturn = false; + readonly reboundByFunction = new Map>(); + readonly reboundOwnerSlots = new Map(); + currentBorrowedReturn = false; /** Manifest-bound native imports, used by ffiCall emission. */ readonly ffiByName = new Map(); /** C-ABI callback trampolines and (for raw/no-userdata callbacks) their * distinct call-scoped TLS closure slots. */ - private readonly ffiCallbackAdapters: Map; + readonly ffiCallbackAdapters: Map; /** Module-level constant consulted per ffiCall: with a retained * descriptor anywhere in the manifest, every native call is a * pending-exception checkpoint (may-throw derives the same fact from * the same helper). */ readonly ffiHasRetainedCallback: boolean; - private readonly ffiHasForeignCallback: boolean; + readonly ffiHasForeignCallback: boolean; private readonly globalTypes = new Map(); /** Module constants that keep one value once initialized * (immutability.ts stableGlobals). */ - private stableGlobals: ReadonlySet = new Set(); + stableGlobals: ReadonlySet = new Set(); private readonly tdzGlobals = new Map(); readonly constantNumericTables: ReadonlyMap; /** Constant-key reads on checked-dynamic objects, each with its own * zero-initialized ScrDynReadIc (scr_runtime.h). */ - private dynReadIcSites = 0; + dynReadIcSites = 0; /** ThreadSlots blocks by family name (thread-slots.ts). */ - private readonly threadSlotsBlocks = new Map(); + readonly threadSlotsBlocks = new Map(); /** May-throw analysis (computeMayThrow): pending * checks are emitted only after calls that can actually raise. */ readonly mayThrow: Set; readonly indirectMayThrow: boolean; /** Worker programs: the functions that poll for termination on entry * (workerTerminationPolls); null outside worker programs. */ - private readonly workerEntryPolls: ReadonlySet | null; + readonly workerEntryPolls: ReadonlySet | null; /** Functions that check the native stack guard on entry * (stackCheckedFunctions); null where the runtime has no guard. */ - private readonly stackChecks: ReadonlySet | null; + readonly stackChecks: ReadonlySet | null; /** Static types whose values can be published (@scriptc/threads): their * stores carry the frozen-object guard. Null without publish calls. */ readonly publishedTypes: PublishedTypes | null; @@ -938,8 +841,8 @@ export class LlEmitter { private readonly immutableFields: ImmutableFields | null; /** Constructor function names: their bodies may still write their own * `this`'s construction-only fields. */ - private readonly constructorFunctions: ReadonlySet; - private inConstructor = false; + readonly constructorFunctions: ReadonlySet; + inConstructor = false; /** Method names with at least one may-throw implementation — the * virtualCall pending check's key. */ readonly mayThrowMethods = new Set(); @@ -1015,30 +918,30 @@ export class LlEmitter { /** Class objects (classes as first-class values): className → the * interned .name literal ref — registered during body emission, the * statics and construct thunks assemble around the bodies. */ - private readonly classObjs = new Map(); + readonly classObjs = new Map(); private readonly unionFieldReadGroups = new Map(); /** Preorder intervals of the runtime error classes under THIS module's * class-forest numbering (main() stamps scr_error_vts with them, exactly * like the runtime ABI’s errorVtStampLines). */ - private readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = []; + readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = []; /** The runtime emitter vtable's preorder interval, when the program * touches node:events (the class def rides the module exactly then) — * main() stamps scr_emitter_vt with it (emitterVtStampLines, ported). */ - private emitterInterval: { pre: number; post: number } | null = null; + emitterInterval: { pre: number; post: number } | null = null; /** The runtime stream vtables' preorder intervals (streamVtStampLines, * ported): the defs ride every emitter-touching module (the frontend * collects the whole emitter-rooted tree), and scr_stream.c links on * the same predicate, so the stamps always have their globals. */ - private readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = []; + readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = []; // ── per-function state (reset in emitFunction) ───────────────────────── B = new BlockBuilder(); frames: LlValue[][] = []; - private scopes: LlScopeEntry[][] = []; + scopes: LlScopeEntry[][] = []; /** Scope slots dominate every throw site. Share their exceptional cleanup * for identical live bindings and handlers; statement temporaries stay at * their own throw sites because their SSA values are path-dependent. */ - private readonly unwindCleanups = new Map< + readonly unwindCleanups = new Map< string, { label: string; entries: LlScopeEntry[]; terminator: string } >(); @@ -1049,7 +952,7 @@ export class LlEmitter { * continue binds to the innermost LOOP; a labeled jump binds to the * entry whose `labels` contains its label. `contLabel` is null exactly * for blocks and switches. */ - private jumpTargets: { + jumpTargets: { kind: "loop" | "block" | "switch"; brkLabel: string; contLabel: string | null; @@ -1058,12 +961,12 @@ export class LlEmitter { scopeDepth: number; finallyDepth: number; }[] = []; - private currentLocals = new Map(); - private privateSplitLocals = new Map(); + currentLocals = new Map(); + privateSplitLocals = new Map(); readonly splitSpans = new Map(); - private storedSplits = new Map(); - private streamingSplitsEnabled = false; - private readonly initializerBindings: ReturnType; + storedSplits = new Map(); + streamingSplitsEnabled = false; + readonly initializerBindings: ReturnType; /** Whole-program int32 storage and parameter/result facts. */ readonly int32Slots: Int32Slots; /** The program has no runtime store into a checked-dynamic value @@ -1087,56 +990,67 @@ export class LlEmitter { /** ProgramLifetime hierarchy roots (contract types): their instances are * immortal from construction (classes.ts programLifetimeClass). */ readonly programLifetime: ReadonlySet; - private numericLocals = new Map(); + numericLocals = new Map(); /** Local arrays of the current function that no other binding can hold * (ir/array-length-stability.ts). */ - private unaliasedArrays: ReadonlySet = new Set(); + unaliasedArrays: ReadonlySet = new Set(); /** Dense array loops (dense-array-loops.ts) run in this function. */ - private denseLoopsEnabled = false; - private denseLoopEntries: ReturnType = new Map(); + denseLoopsEnabled = false; + denseLoopEntries: ReturnType = new Map(); /** The fast copy of a dense array loop being emitted. */ - private denseLoop: DenseArrayLoop | null = null; - private captureIds = new Set(); + denseLoop: DenseArrayLoop | null = null; + captureIds = new Set(); /** Active induction bindings retain their width independently of size_t. */ integerLoopBindings = new Map< string, { slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } } >(); - private countedLoopsEnabled = false; + countedLoopsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ - private speculativeLoopsEnabled = false; + speculativeLoopsEnabled = false; /** The fast copy of a speculative integer loop being emitted: its * integer-computed writes and where each resumes in the ordinary copy. */ - private speculation: { + speculation: { writes: Set; resume: Map; /** Globals whose double is written back only when the copy is left. */ deferred: Set; } | null = null; /** The ordinary copy being emitted: resume labels still to place. */ - private speculationResume: Map | null = null; + speculationResume: Map | null = null; /** The function being emitted, for speculative globals. */ - private speculativeFunction = ""; + speculativeFunction = ""; /** Every function that writes each number global (computed on demand). */ private globalWriters: Map> | null = null; /** Whole-program final reference fields (ir/final-fields.ts). */ - private finalFields!: FinalFields; + finalFields!: FinalFields; /** The current function may borrow final fields of borrowable objects: * not a constructor (where they change), not async or a generator. */ - private finalFieldBorrows = false; + finalFieldBorrows = false; integerRanges: IntegerRanges = new Map(); /** Number arrays of the current function whose every present element is * an int32 (Int32[] contracts), or null when the module has none. */ int32Elements: ((arr: IrExpr) => boolean) | null = null; /** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */ wrappingSums = new Set(); - private byteWindowsEnabled = true; - private byteWindowEntries: ReturnType = new Map(); + byteWindowsEnabled = true; + byteWindowEntries: ReturnType = new Map(); + /** Typed-array loop versioning (bytes-loop-versions.ts) for the current + * function: off in debug and suspending functions and inside a copy. */ + bytesLoopVersionsEnabled = false; + /** Loops being versioned around the statement being emitted: a loop is + * never versioned inside its own copies, and versions nest boundedly. */ + readonly bytesLoopVersioning = new Set(); + bytesLoopEntries: ReturnType | null = null; + bytesLoopFunction: IrFunction | null = null; + /** Counted loops whose integer-form safety test an enclosing version + * guard already passed. */ + readonly provenCountedGuards = new Set(); bytesBounds: 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. */ - private privateGlobalSlots = new Map(); + privateGlobalSlots = new Map(); /** Number locals of the current function stored only as an i32 * (int32-locals.ts); their slots are `alloca i32`. */ int32Locals = new Map(); @@ -1151,7 +1065,7 @@ export class LlEmitter { * (past its own catch), so the copies emit under the truncated stack. * break/continue use the same region snapshots through * emitFinallysForJump. */ - private finallyStack: { + finallyStack: { frameDepth: number; scopeDepth: number; tryDepth: number; @@ -1162,15 +1076,15 @@ export class LlEmitter { * releases frames/scopes down to the recorded depths and branches to * `label` (the catch, or the exception-path finally) instead of * returning out of the function. Entering a try emits no code. */ - private tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = []; + tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = []; /** Return type of the function being emitted — the unwind path returns * a dummy of this type (never read: callers check the flag first). */ currentReturnType: IrType = VOID; /** The current body returns its number result as i32. */ - private currentInt32Result = false; + currentInt32Result = false; /** The function's i32 entry returns its scalar union result as tag and * payload bits (Int32Abi.union). */ - private currentUnionResult = false; + currentUnionResult = false; /** Active only while emitting a body lowered with LLVM's * switched-coroutine intrinsics (wasm32 async bodies and generators, * native synchronous generators; see coroLowered). */ @@ -1195,13 +1109,13 @@ export class LlEmitter { * catch prologue emits the GENRET sentinel re-unwind exactly here. */ currentGenerator: { yieldT: IrType; nextT: IrType } | null = null; /** The function being emitted is an async generator body. */ - private currentAsyncGenerator = false; + currentAsyncGenerator = false; /** Active optional-chain bind slots, by chain id (chainRecv reads). */ readonly chainSlots = new Map(); logArgSlots = 0; - private readonly stackTraces: boolean; + readonly stackTraces: boolean; /** Stack-trace mode: functions no capture can include (no frame push). */ - private readonly unframedFunctions: ReadonlySet; + readonly unframedFunctions: ReadonlySet; constructor( readonly mod: IrModule, @@ -1594,7 +1508,7 @@ export class LlEmitter { return this.virtualBorrows.entry(implFn); } - private implSlotBorrowed(implFn: string): ReadonlySet { + implSlotBorrowed(implFn: string): ReadonlySet { return this.virtualBorrows.implSlotBorrowed(implFn); } @@ -1631,398 +1545,22 @@ export class LlEmitter { throw new InternalCompilerError(`llvm emitter bug: no callback adapter for ${binding}:${id}`); return adapter; } - - /** C-callable scalar callback trampolines. A callback with an explicit - * context entry receives the closure at that exact ABI position. A raw - * callback loads it from a call-scoped TLS slot installed around the - * outer native call. */ - private emitFfiCallbackDefs(): { globals: string[]; defs: string[] } { - const globals: string[] = []; - const defs: string[] = []; - if (this.ffiCallbackAdapters.size === 0) return { globals, defs }; - this.declare(`declare void @scr_trap(ptr)`); - const expired = this.cstr( - "scriptc: native callback invoked outside its call-scoped lifetime\n", - ); - const released = this.cstr("scriptc: native callback invoked outside its retained lifetime\n"); - for (const adapter of this.ffiCallbackAdapters.values()) { - const cb = adapter.callback; - if (adapter.tls !== null) - globals.push(`@${adapter.tls} = internal thread_local global ptr null`); - if (adapter.global !== null) - globals.push(`@${adapter.global} = internal thread_local global ptr null`); - if (adapter.table !== null) { - globals.push( - `@${adapter.table} = internal ${this.mod.workers ? "thread_local " : ""}global %ScrFfiTable zeroinitializer`, - ); - } - const params = cb.params.flatMap((param, i): string[] => { - if (isFfiContextParam(param)) return [`ptr %ctx`]; - if (param === "string" || param === "bytes") { - return [`ptr %a${i}`, `${this.sizeType} %a${i}_len`]; - } - return [`${ffiNativeParamLl(param, this.ffiExtendNarrowIntegers)} %a${i}`]; - }); - if (cb.invoke === "foreign") { - if ( - adapter.table === null || - !cb.params.some(isFfiContextParam) || - cb.returns !== "void" || - cb.params.some((param) => param === "i64" || param === "u64" || param === "pointer") - ) { - throw new InternalCompilerError( - "llvm emitter bug: invalid foreign FFI callback descriptor", - ); - } - const dispatch = `${adapter.symbol}_dispatch`; - const scriptArgs: string[] = []; - const dispatchBody: string[] = [ - `define internal void @${dispatch}(ptr %cb, ptr %call) ${FN_ATTRS} {`, - `entry:`, - ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, - ` %fn = load ptr, ptr %fnp`, - ]; - for (let i = 0; i < cb.params.length; i++) { - const param = cb.params[i]!; - if (isFfiContextParam(param)) continue; - switch (param) { - case "f64": - case "f32": - case "i8": - case "u16": - case "i16": - this.declare(`declare double @scr_ffi_call_get_f64(ptr, ${this.sizeType})`); - dispatchBody.push( - ` %s${i} = call double @scr_ffi_call_get_f64(ptr %call, ${this.sizeType} ${i})`, - ); - scriptArgs.push(`double %s${i}`); - break; - case "bool": - this.declare(`declare zeroext i1 @scr_ffi_call_get_bool(ptr, ${this.sizeType})`); - dispatchBody.push( - ` %s${i} = call zeroext i1 @scr_ffi_call_get_bool(ptr %call, ${this.sizeType} ${i})`, - ); - scriptArgs.push(`i1 %s${i}`); - break; - case "u8": - case "u32": - case "i32": - this.declare(`declare double @scr_ffi_call_get_${param}(ptr, ${this.sizeType})`); - dispatchBody.push( - ` %s${i} = call double @scr_ffi_call_get_${param}(ptr %call, ${this.sizeType} ${i})`, - ); - scriptArgs.push(`double %s${i}`); - break; - case "cstring": - case "string": - case "bytes": { - this.declare(`declare ptr @scr_ffi_call_get_data(ptr, ${this.sizeType})`); - this.declare(`declare ${this.sizeType} @scr_ffi_call_get_len(ptr, ${this.sizeType})`); - dispatchBody.push( - ` %data${i} = call ptr @scr_ffi_call_get_data(ptr %call, ${this.sizeType} ${i})`, - ` %len${i} = call ${this.sizeType} @scr_ffi_call_get_len(ptr %call, ${this.sizeType} ${i})`, - ); - if (param === "bytes") { - this.declare(`declare ptr @scr_bytes_from_data(ptr, ${this.sizeType})`); - dispatchBody.push( - ` %s${i} = call ptr @scr_bytes_from_data(ptr %data${i}, ${this.sizeType} %len${i})`, - ); - } else { - this.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${this.sizeType})`); - dispatchBody.push( - ` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %data${i}, ${this.sizeType} %len${i})`, - ); - } - scriptArgs.push(`ptr %s${i}`); - break; - } - } - } - dispatchBody.push( - ` call void %fn(${[`ptr %cb`, ...scriptArgs].join(", ")})`, - ` ret void`, - `}`, - ``, - ); - for (const line of dispatchBody) defs.push(line); - - this.declare(`declare ptr @scr_ffi_call_new(ptr, ptr, ptr, ${this.sizeType})`); - this.declare(`declare void @scr_ffi_post(ptr)`); - defs.push( - `define internal void @${adapter.symbol}(${params.join(", ")}) ${FN_ATTRS} {`, - `entry:`, - ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, - ` %call = call ptr @scr_ffi_call_new(ptr @${adapter.table}, ptr %cb, ptr @${dispatch}, ${this.sizeType} ${cb.params.length})`, - ); - for (let i = 0; i < cb.params.length; i++) { - const param = cb.params[i]!; - if (isFfiContextParam(param)) continue; - if (param === "cstring") { - this.declare(`declare void @scr_ffi_call_copy_cstring(ptr, ${this.sizeType}, ptr)`); - defs.push( - ` call void @scr_ffi_call_copy_cstring(ptr %call, ${this.sizeType} ${i}, ptr %a${i})`, - ); - } else if (param === "string" || param === "bytes") { - this.declare( - `declare void @scr_ffi_call_copy_${param}(ptr, ${this.sizeType}, ptr, ${this.sizeType})`, - ); - defs.push( - ` call void @scr_ffi_call_copy_${param}(ptr %call, ${this.sizeType} ${i}, ptr %a${i}, ${this.sizeType} %a${i}_len)`, - ); - } else if (param === "f32" || param === "i8" || param === "u16" || param === "i16") { - const nativeTy = ffiNativeTypeLl(param); - const op = param === "f32" ? "fpext" : param === "u16" ? "uitofp" : "sitofp"; - this.declare(`declare void @scr_ffi_call_set_f64(ptr, ${this.sizeType}, double)`); - defs.push( - ` %wide${i} = ${op} ${nativeTy} %a${i} to double`, - ` call void @scr_ffi_call_set_f64(ptr %call, ${this.sizeType} ${i}, double %wide${i})`, - ); - } else { - const nativeTy = ffiNativeTypeLl(param); - this.declare( - `declare void @scr_ffi_call_set_${param}(ptr, ${this.sizeType}, ${nativeTy})`, - ); - defs.push( - ` call void @scr_ffi_call_set_${param}(ptr %call, ${this.sizeType} ${i}, ${nativeTy} %a${i})`, - ); - } - } - defs.push(` call void @scr_ffi_post(ptr %call)`, ` ret void`, `}`, ``); - continue; - } - defs.push( - `define internal ${ffiNativeReturnLl(cb.returns, this.ffiExtendNarrowIntegers)} @${adapter.symbol}(${params.join(", ")}) ${FN_ATTRS} {`, - `entry:`, - adapter.tls !== null - ? ` %cb = load ptr, ptr @${adapter.tls}` - : adapter.global !== null - ? ` %cb = load ptr, ptr @${adapter.global}` - : ` %cb = getelementptr inbounds i8, ptr %ctx, i64 0`, - ` %missing = icmp eq ptr %cb, null`, - ` br i1 %missing, label %expired, label %ready`, - `expired:`, - ` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`, - ` unreachable`, - `ready:`, - ...this.pendingTestLines("%pending"), - ` br i1 %pending, label %skip, label %invoke`, - `skip:`, - ` ret ${ffiCallbackDummyLl(cb)}`, - `invoke:`, - ); - // Validate every native pointer before allocating any copy-in value. - // A later bad slot therefore cannot leak an earlier materialization. - for (let i = 0; i < cb.params.length; i++) { - const param = cb.params[i]!; - if (param !== "cstring" && param !== "string" && param !== "bytes") continue; - const invalid = `%invalid${i}`; - defs.push(` %null${i} = icmp eq ptr %a${i}, null`); - if (param === "cstring") { - defs.push(` ${invalid} = or i1 %null${i}, false`); - } else { - defs.push( - ` %nonempty${i} = icmp ne ${this.sizeType} %a${i}_len, 0`, - ` ${invalid} = and i1 %null${i}, %nonempty${i}`, - ); - } - const message = - param === "cstring" - ? "scriptc: native callback passed a NULL cstring\n" - : `scriptc: native callback passed a NULL ${param} span with nonzero length\n`; - defs.push( - ` br i1 ${invalid}, label %invalid_param${i}, label %param_ok${i}`, - `invalid_param${i}:`, - ` call void @scr_trap(ptr ${this.cstr(message)})`, - ` unreachable`, - `param_ok${i}:`, - ); - } - defs.push( - ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, - ` %fn = load ptr, ptr %fnp`, - ); - const scriptArgs: string[] = []; - for (let i = 0; i < cb.params.length; i++) { - const param = cb.params[i]!; - if (isFfiContextParam(param)) continue; - switch (param) { - case "i64": - case "u64": - case "pointer": { - const ty = ffiNativeTypeLl(param); - this.declare(`declare ptr @scr_bigint_from_${param}(${ty})`); - defs.push(` %s${i} = call ptr @scr_bigint_from_${param}(${ty} %a${i})`); - scriptArgs.push(`ptr %s${i}`); - break; - } - case "f64": - scriptArgs.push(`double %a${i}`); - break; - case "f32": - case "i8": - case "u16": - case "i16": { - const nativeTy = ffiNativeTypeLl(param); - const op = param === "f32" ? "fpext" : param === "u16" ? "uitofp" : "sitofp"; - defs.push(` %s${i} = ${op} ${nativeTy} %a${i} to double`); - scriptArgs.push(`double %s${i}`); - break; - } - case "bool": - defs.push(` %s${i} = icmp ne i8 %a${i}, 0`); - scriptArgs.push(`i1 %s${i}`); - break; - case "u8": - defs.push(` %s${i} = uitofp i8 %a${i} to double`); - scriptArgs.push(`double %s${i}`); - break; - case "u32": - defs.push(` %s${i} = uitofp i32 %a${i} to double`); - scriptArgs.push(`double %s${i}`); - break; - case "i32": - defs.push(` %s${i} = sitofp i32 %a${i} to double`); - scriptArgs.push(`double %s${i}`); - break; - case "cstring": - this.declare(`declare ${this.sizeType} @strlen(ptr)`); - this.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${this.sizeType})`); - defs.push( - ` %len${i} = call ${this.sizeType} @strlen(ptr %a${i})`, - ` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %a${i}, ${this.sizeType} %len${i})`, - ); - scriptArgs.push(`ptr %s${i}`); - break; - case "string": - this.declare(`declare ptr @scr_str_from_utf8_lossy(ptr, ${this.sizeType})`); - defs.push( - ` %s${i} = call ptr @scr_str_from_utf8_lossy(ptr %a${i}, ${this.sizeType} %a${i}_len)`, - ); - scriptArgs.push(`ptr %s${i}`); - break; - case "bytes": - this.declare(`declare ptr @scr_bytes_from_data(ptr, ${this.sizeType})`); - defs.push( - ` %s${i} = call ptr @scr_bytes_from_data(ptr %a${i}, ${this.sizeType} %a${i}_len)`, - ); - scriptArgs.push(`ptr %s${i}`); - break; - } - } - const ft = ffiCallbackType(cb); - const internalRet = this.llType(ft.ret); - const callArgs = [`ptr %cb`, ...scriptArgs].join(", "); - if (cb.lifetime === "retained") { - this.declare(`declare ptr @scr_closure_retain_v(ptr)`); - this.declare(`declare void @scr_closure_release_v(ptr)`); - defs.push(` %invoke_pin = call ptr @scr_closure_retain_v(ptr %cb)`); - } - if (cb.returns === "void") { - defs.push( - ` call void %fn(${callArgs})`, - ...(cb.lifetime === "retained" - ? [` call void @scr_closure_release_v(ptr %invoke_pin)`] - : []), - ` ret void`, - `}`, - ``, - ); - continue; - } - defs.push(` %result = call ${internalRet} %fn(${callArgs})`); - if (cb.lifetime === "retained") { - defs.push(` call void @scr_closure_release_v(ptr %invoke_pin)`); - } - switch (cb.returns) { - case "i64": - case "u64": - case "pointer": { - const ty = ffiNativeTypeLl(cb.returns); - this.declare(`declare ${ty} @scr_bigint_to_${cb.returns}(ptr)`); - this.declare(`declare void @scr_bigint_release(ptr)`); - defs.push( - ` %out = call ${ty} @scr_bigint_to_${cb.returns}(ptr %result)`, - ` call void @scr_bigint_release(ptr %result)`, - ` ret ${ty} %out`, - ); - break; - } - case "f64": - defs.push(` ret double %result`); - break; - case "f32": - defs.push(` %out = fptrunc double %result to float`, ` ret float %out`); - break; - case "bool": - defs.push(` %out = zext i1 %result to i8`, ` ret i8 %out`); - break; - case "u8": - case "i8": - case "u16": - case "i16": - case "u32": - this.declare(`declare double @scr_bit_ushr(double, double)`); - defs.push( - ` %coerced = call double @scr_bit_ushr(double %result, double ${f64Lit(0)})`, - ` %wide = fptoui double %coerced to i32`, - ); - if (cb.returns !== "u32") { - const nativeTy = ffiNativeTypeLl(cb.returns); - defs.push(` %out = trunc i32 %wide to ${nativeTy}`, ` ret ${nativeTy} %out`); - } else { - defs.push(` ret i32 %wide`); - } - break; - case "i32": - this.declare(`declare double @scr_bit_or(double, double)`); - defs.push( - ` %coerced = call double @scr_bit_or(double %result, double ${f64Lit(0)})`, - ` %out = fptosi double %coerced to i32`, - ` ret i32 %out`, - ); - break; - } - defs.push(`}`, ``); - } - return { globals, defs }; + emitFfiCallbackDefs(): { globals: string[]; defs: string[] } { + return emitFfiCallbackDefs(this); } emit(): string { return this.emitParts().join("\n"); } - emitParts(): string[] { - const parts = [ - ...limitInlineDynReleases(this.emitModuleParts()), - ...this.disposeObserverGates(), - ]; - if (!this.executableTls) return parts; - // A worker executable's thread-locals all live in the executable's own - // TLS block: the program's internal ones, and the runtime's external - // ones, because the runtime is always linked statically into the - // executable. So on ELF they can all use the local-exec model: LLVM's - // default general-dynamic sequence (relaxed by the linker to `mov %fs:0` - // plus an add), and even initial-exec (relaxed to `mov $offset` plus an - // indexed access), become one %fs-relative operand LLVM can fold into - // loads and compares. Darwin's thread-local variables have one model. - return parts.map((part) => - part - .replace( - /^(@[-$._A-Za-z0-9]+ = internal )thread_local global /gm, - "$1thread_local(localexec) global ", - ) - .replace( - /^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm, - "$1thread_local(localexec) global ", - ), - ); + return emitParts(this); } /** The dispose-observer gates the emitted frees and record writers test * (alloc.ts): defined after every helper is emitted, because dynCheck * builders, JSON schemas and weak collection constructors are requested * lazily. */ - private disposeObserverGates(): string[] { + disposeObserverGates(): string[] { const weak = [...this.decls].some( (d) => d.includes("@scr_weak_map_new(") || d.includes("@scr_weak_set_new("), ); @@ -2032,1812 +1570,25 @@ export class LlEmitter { ``, ]; } - - private emitModuleParts(): string[] { - // Function bodies first (the literal/unit/fn-value tables fill as they - // emit), then the file assembles around them — the runtime ABI’s order. - const fnDefs: string[] = []; - for (const fn of this.mod.functions) { - const def = this.emitFunction(fn); - fnDefs.push(def); - this.fnDefText.set(fn.name, def); - } - this.selectStacklessAsync(fnDefs); - const errorViews: string[] = []; - for (const property of ["name", "message"]) - if (this.mod.functions.some((fn) => fn.name === `%error.${property}.read`)) { - this.declare(`declare void @scr_error_install_${property}_reader(ptr)`); - errorViews.push( - ` call void @scr_error_install_${property}_reader(ptr @${mangleFunction(`%error.${property}.read`)})`, - ); - } - for (const meta of this.classMeta.values()) { - if (meta.def.runtime || meta.root.def.name !== "%Error") continue; - const type: IrType = { kind: "object", className: meta.def.name }; - const adapter = this.liveDynRefAdapter(type); - const rc = vAdapters(this.shapeHost, type); - const key = typeKey(type); - const name = `sc_error_view_${errorViews.length}`; - this.resolveThunkDefs.push( - `define internal ptr @${name}(ptr %v) ${FN_ATTRS} {`, - `entry:`, - ` %d = call ptr ${typedRefConstructor(this.shapeHost, type)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${this.cstr(key)}, ${this.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`, - ` ret ptr %d`, - `}`, - ``, - ); - this.declare(`declare void @scr_error_register_dyn(ptr, ptr)`); - errorViews.push( - ` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`, - ); - } - const layouts = emitLlvmLayouts( - this.shapeHost, - this.mod, - this.classMeta, - this.classObjs, - this.fnByName, - (t) => this.llType(t), - this.nullableFields, - this.int32Slots, - ); - const shapes = layouts.records; - const classShapes = layouts.classes; - const classObjDefs = layouts.classObjects; - const wrappers = this.emitFnValueDefs(); - const asyncDefs = this.emitAsyncScaffolding(); - const ffiCallbacks = this.emitFfiCallbackDefs(); - const hasNoInlineRecordClone = [...this.recordCloneShapes].some( - (shapeId) => (this.recordsById.get(shapeId)?.fields.length ?? 0) >= 16, - ); - - // Module globals, FIRST OCCURRENCE per id: a class-expression static - // instantiated through several mixin applications registers one global - // id several times (2041-mixin-values) — C's tentative definitions - // accept the duplicates without an error, LLVM rejects redefinition, so the - // storage AND the release emit once per id here. - const seenGlobalIds = new Set(); - const globals = (this.mod.globals ?? []).filter((g) => { - if (seenGlobalIds.has(g.id)) return false; - seenGlobalIds.add(g.id); - return true; - }); - // Refcounted globals release before main returns — the runtime ABI’s - // sc_release_globals, keeping the RC audit's live count exact. Built - // before the declaration table flushes (it adds the release symbols). - // Two spellings with distinct temp names: the normal exit and the - // uncaught-exception exit are separate blocks of the same function. - // Interned function-value closures are IMMORTAL (rc == SIZE_MAX), so - // an own-property table Object.defineProperties hung on one would - // outlive the RC audit — release it with the globals (the runtime ABI’s - // sc_release_globals tail). Only when the dispatch unit is even - // linked (defineProps is the only writer). - const runtimeFeatures = moduleRuntimeFeatures(this.mod); - const fnValueProps = [...this.fnValues]; - if (fnValueProps.length > 0) this.declare(`declare void @scr_box_release(ptr)`); - if (this.classObjs.size > 0) this.declare(`declare void @scr_classobj_clear_properties(ptr)`); - const globalReleaseLines = (prefix: string): string[] => { - const lines: string[] = []; - globals.forEach((g, i) => { - if (!isRefCounted(g.type)) return; - lines.push( - ` %${prefix}${i} = load ptr, ptr @${mangleGlobal(g.id)}`, - ` call void ${releaseSym(this.shapeHost, g.type)}(ptr %${prefix}${i}) ; ${g.name}`, - ); - }); - fnValueProps.forEach((name, i) => { - // props sits at %ScrClosure field 3; release is NULL-tolerant — - // cleared so a second release path stays idempotent. - lines.push( - ` %${prefix}fp${i} = load ptr, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`, - ` call void @scr_box_release(ptr %${prefix}fp${i}) ; ${name}.props`, - ` store ptr null, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`, - ); - }); - for (const name of this.classObjs.keys()) - lines.push(` call void @scr_classobj_clear_properties(ptr @${mangleClassObj(name)})`); - return lines; - }; - // Exit listeners can read MODULE GLOBALS directly, so they must run - // BEFORE the global releases (the runtime ABI’s runExitListeners - // ordering — the atexit half becomes an idempotent no-op). - const usesEvents = runtimeFeatures.processEvents; - const usesChildProcess = runtimeFeatures.childProcess; - const usesFsWatch = runtimeFeatures.fsWatch; - // Stream-surface programs fill the loop's stream hook (the deferred - // next-tick emissions) and the emitter's post-registration flow kick - // before %main — scr_stream.c links only when the line is emitted - // (native/executable.ts gates on the same predicate). - const usesStream = runtimeFeatures.stream; - // Net-surface programs fill the loop's net hooks (and the netSocket - // handle-dispatch ops for the checked-dynamic boundary); http-surface - // programs additionally stamp the httpReq/httpRes ops — the C main's - // install lines, gated on the same predicates native/executable.ts links by. - const usesNet = runtimeFeatures.net; - const usesDgram = runtimeFeatures.dgram; - const usesHttp2 = runtimeFeatures.http2; - const usesHttp = runtimeFeatures.http; - // Fetch-referencing programs register native fetch before main. - // Runtime selection compiles scr_fetch.c on the same predicate. - const usesFetch = runtimeFeatures.fetch; - const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa; - // The process verdict has the same precedence as the C reference - // emitter: node:test owns the final status when present; otherwise an - // embedded process.exitCode owns it; ordinary programs return zero. - const usesNodeTest = runtimeFeatures.nodeTest; - // Declared NOW — the extern block flushes before main assembles. - if (usesEvents) this.declare(`declare void @scr_events_install()`); - if (usesChildProcess) this.declare(`declare void @scr_child_dyn_install()`); - const childStreamBoxes = - usesChildProcess && - usesStream && - this.classMeta.has("%Readable") && - this.classMeta.has("%Writable") - ? [ - this.dyn.toDynHelper({ kind: "object", className: "%Readable" }), - this.dyn.toDynHelper({ kind: "object", className: "%Writable" }), - ] - : null; - // worker.stdout and worker.stderr box the stream unit's Readable the - // same way, and worker.stdin its Writable when the program has one - // (scr_worker_stdio_install, scr_worker_events.c). - const workerStdioBox = - this.mod.workers === true && usesStream && this.classMeta.has("%Readable") - ? (childStreamBoxes?.[0] ?? - this.dyn.toDynHelper({ kind: "object", className: "%Readable" })) - : null; - const workerStdinBox = - this.mod.workers === true && usesStream && this.classMeta.has("%Writable") - ? (childStreamBoxes?.[1] ?? - this.dyn.toDynHelper({ kind: "object", className: "%Writable" })) - : null; - if (childStreamBoxes || workerStdioBox || workerStdinBox) { - this.declare( - `@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`, - ); - } - if (childStreamBoxes) - this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`); - if (workerStdioBox || workerStdinBox) - this.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`); - if (usesFsWatch) this.declare(`declare void @scr_watch_install()`); - if (this.ffiHasForeignCallback) this.declare(`declare void @scr_ffi_install()`); - if (usesStream) this.declare(`declare void @scr_stream_install()`); - if (usesNet) { - this.declare(`declare void @scr_net_install()`); - this.declare(`declare void @scr_net_dyn_install()`); - } - if (usesDgram) this.declare(`declare void @scr_dgram_install()`); - if (usesHttp2) this.declare(`declare void @scr_http2_dyn_install()`); - if (usesHttp) this.declare(`declare void @scr_http_dyn_install()`); - if (usesFetch) this.declare(`declare void @scr_fetch_install()`); - - if (usesNodeTest) { - this.declare(`declare i32 @scr_test_exit_code()`); - this.declare(`declare void @scr_test_init()`); - } - if (snapshotsTlsCa) { - this.declare(`declare void @scr_tls_ca_install()`); - } - // Inline exit listeners run when something they must beat exists: - // the refcounted-global releases, or the retained-FFI atexit ledger - // sweep (a listener may legitimately release or pump a registration, - // and only the inline call orders ahead of every atexit handler — - // the runtime ABI’s runExitListeners stance). Plain event programs - // with neither keep the atexit path, so their listener timing is - // unchanged. - const hasRefGlobals = globals.some((g) => isRefCounted(g.type)) || fnValueProps.length > 0; - const inlineExitListeners = - usesEvents && (this.mod.workers || hasRefGlobals || this.ffiHasRetainedCallback); - this.declare(`declare i32 @scr_exit_code_hint_get()`); - if (inlineExitListeners) { - this.declare(`declare void @scr_run_exit_listeners(double)`); - this.declare(`declare i32 @scr_exit_code_hint_get()`); - } - const exitListenerLines = (prefix: string): string[] => { - if (!inlineExitListeners) return []; - return [ - ` %${prefix}h = call i32 @scr_exit_code_hint_get()`, - ` %${prefix}hd = sitofp i32 %${prefix}h to double`, - ` call void @scr_run_exit_listeners(double %${prefix}hd)`, - ]; - }; - // The entry's exits release the module globals only where the thread - // frees its heap when its script ends (scr_exit_frees: workers, and - // builds that check for leaks); the main thread of other builds leaves - // it to the operating system. - const exitReleaseLines = (prefix: string): string[] => { - const releases = globalReleaseLines(prefix); - if (releases.length === 0) return releases; - this.declare(`declare zeroext i1 @scr_exit_frees()`); - return [ - ` %${prefix}.frees = call zeroext i1 @scr_exit_frees()`, - ` br i1 %${prefix}.frees, label %${prefix}.release, label %${prefix}.kept`, - `${prefix}.release:`, - ...releases, - ` br label %${prefix}.kept`, - `${prefix}.kept:`, - ]; - }; - const globalReleases = exitReleaseLines("g"); - const asyncEntry = this.fnByName.get(this.mod.entry)?.async === true; - const entryMayThrow = this.mayThrow.has(this.mod.entry); - // The event loop runs when timers appeared OR any async/generator - // function exists (the C main's hasAsync || hasGenerators || - // usesTimers gate). Generator programs run - // the loop too: its exit accounting notes still-suspended generator - // fibers as abandoned, so the RC audit downgrades exactly like the - // async loop-exhaustion story. - const runsLoop = - this.usesTimers || - this.ffiHasForeignCallback || - this.mod.functions.some((f) => f.async === true || f.generator !== undefined); - const uncaughtReleases = entryMayThrow && !asyncEntry ? exitReleaseLines("gu") : []; - const loopReleasesU = runsLoop ? exitReleaseLines("gl") : []; - const loopReleasesR = runsLoop ? exitReleaseLines("gr") : []; - const topRejectReleases = asyncEntry ? exitReleaseLines("gt") : []; - const topPendingReleases = asyncEntry ? exitReleaseLines("gp") : []; - const loopReportedReleases = runsLoop ? exitReleaseLines("gq") : []; - // main's epilogues read the flag / the loop entry points — declared - // HERE, before the extern block flushes. They run once, so they keep - // the out-of-line scr_exc_pending call; per-call checks inline the - // test (pendingTestLines). - if (entryMayThrow || runsLoop) this.declare(`declare zeroext i1 @scr_exc_pending()`); - if (runsLoop) { - this.declare(`declare zeroext i1 @scr_loop_run(ptr)`); - if (this.mod.commonJsEntry === true) this.declare(`declare void @scr_loop_commonjs_entry()`); - this.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`); - this.declare(`declare void @scr_discard_unhandled_rejections()`); - } - if (asyncEntry) { - this.declare(`declare i32 @scr_promise_finish_top_level(ptr)`); - this.declare(`declare void @scr_promise_rethrow_top_level(ptr)`); - this.declare(`declare void @scr_promise_release(ptr)`); - this.declare(`declare void @scr_exit_code_note(i32)`); - } - const topPendingExitLines = (): string[] => { - if (!asyncEntry) return []; - const lines: string[] = []; - // finish_top_level noted 13 unless the program set process.exitCode. - // A failed node:test run outranks either before exit listeners run. - if (usesNodeTest) { - lines.push( - ` %tla_program_exit = call i32 @scr_test_exit_code()`, - ` %tla_program_exit_zero = icmp eq i32 %tla_program_exit, 0`, - ` %tla_hint = call i32 @scr_exit_code_hint_get()`, - ` %tla_exit_status = select i1 %tla_program_exit_zero, i32 %tla_hint, i32 %tla_program_exit`, - ` call void @scr_exit_code_note(i32 %tla_exit_status)`, - ); - } - for (const line of exitListenerLines("xp")) lines.push(line); - for (const line of topPendingReleases) lines.push(line); - // An exit listener may set process.exitCode again, as in Node. - lines.push( - ` %tla_exit_code = call i32 @scr_exit_code_hint_get()`, - ` ret i32 %tla_exit_code`, - ); - return lines; - }; - // LIBRARY mode: the runtime entry points the generated library - // symbols delegate to — declared before the extern block flushes. - if (this.mod.lib !== undefined) { - if (this.wasi) { - this.declare(`declare ptr @malloc(${this.sizeType})`); - this.declare(`declare void @free(ptr)`); - } - this.declare(`declare void @scr_library_entry(i1 zeroext, ptr)`); - this.declare(`declare void @scr_library_reset()`); - this.declare(`declare void @scr_library_check_exc()`); - this.declare(`declare void @scr_library_set_sink(ptr, ptr)`); - this.declare(`declare void @scr_library_callback_entry_guard(ptr)`); - this.declare(`declare void @scr_library_arena_reset()`); - this.declare(`declare void @scr_library_collect()`); - if ((this.mod.lib.callbacks?.length ?? 0) > 0) { - // Host-callback channels: the registration define's dispatch - // (strcmp over the declared names + the runtime slot store). - // The call-site fetch pair (scr_library_cb_require/_ctx) is - // declared at ffiCall emission like every body-driven runtime - // symbol. - this.declare(`declare void @scr_library_cb_set(${this.sizeType}, ptr, ptr)`); - this.declare(`declare i32 @strcmp(ptr, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.params.includes("string"))) { - this.declare(`declare ptr @scr_library_str_in(ptr, ${this.sizeType})`); - } - if (this.mod.lib.exports.some((e) => e.params.includes("bytes"))) { - this.declare(`declare ptr @scr_library_bytes_in(ptr, ${this.sizeType}, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.params.includes("i64"))) { - this.declare(`declare double @scr_library_i64_in(i64, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.params.includes("u64"))) { - this.declare(`declare double @scr_library_u64_in(i64, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.returns === "string")) { - this.declare(`declare void @scr_library_str_out(ptr, ptr, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.returns === "bytes")) { - this.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.params.includes("cstring"))) { - this.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`); - } - if (this.mod.lib.exports.some((e) => e.returns === "cstring")) { - this.declare(`declare ptr @scr_library_cstr_out(ptr)`); - } - } - // Helpers assemble BEFORE the declaration table flushes (they add - // write/abort declarations). - const helpers = [ - ...this.helperDefs(), - ...emitArraySearchHelpers(this.arraySearchKinds, this.sizeType, (line) => this.declare(line)), - ...emitClassMembershipHelper(this.classMeta, this.sizeType), - ...(this.publishWalkersCache?.defs() ?? []), - ...this.publishedClassNamesDefs(), - ...(this.regions?.moduleDefs() ?? []), - ]; - if (this.constantNumericTables.size > 0) - this.declare(`declare double @scr_arr_get_number(ptr, double)`); - // Native matchers of the regex literals inside the compiled subset. - const regexMatchers = new Map(); - for (const [key, re] of this.regexInstances) { - const code = compileRegexMatcher( - `${re.sym}_match`, - re.pattern, - re.flags, - this.sizeType, - !this.wasi, - ); - if (code === null) continue; - regexMatchers.set(key, code); - for (const decl of code.decls) this.declare(decl); - } - - const out: string[] = [ - `; Generated by scriptc (LLVM backend) from ${this.mod.sourceFile}. Do not edit.`, - ``, - // Type shapes shared with the runtime's C ABI. ScrStr is the header - // prefix only (the flexible-array tail is concrete per literal); - // ScrLogArg is { i32 tag; 8-byte union } — i64 at offset 8 matches - // the C layout (4 bytes padding after the tag). ScrUnion/ScrClosure - // mirror scr_runtime.h field-for-field (tag reads, slot peeks, the - // fn pointer, and the caps[] tail all address through them). - `%ScrStr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType} }`, - `%ScrLogArg = type { i32, i64 }`, - `%ScrVt = type { ${this.sizeType}, ${this.sizeType}, ptr }`, - `%ScrUnion = type { ${this.sizeType}, i32, ptr, ptr, ptr, i64 }`, - `%ScrClosure = type { ${this.sizeType}, ptr, ${this.sizeType}, ptr, i32, i32, ptr, ptr }`, - `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`, - `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr, ptr }`, - // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem, - // cyc, spare, filled, ops, data, present, ext } (field numbers in - // SCR_ARR_FIELD), where filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor - // (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse, - // property and metadata storage (NULL until first used). Live dynamic - // stream commits swap its dense, presence and out-of-line storage while - // preserving the target object's identity. - `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i8, i8, i16, i32, ptr, ptr, ptr, ptr }`, - `%ScrArrOps = type { ptr, ptr, ptr }`, - // The ScrMap prefix { rc, key_kind, val_kind, traced } — the inline - // RC fast paths test the traced byte (shapes.ts). - ...(this.rcHelpers === null ? [] : [`%ScrMapRc = type { ${this.sizeType}, i8, i8, i8 }`]), - // The complete ScrMap and its { key, val, hash } entry, read by the - // inline number-key lookup (sc_map_entry_f64) and the inline - // iteration steps (sc_map_iter_entry): field 8 is the i32 entry - // count, field 10 the i64 absent-key filter, entries is field 11, - // nbuckets and buckets fields 13 and 14, the direct index and its i32 - // length fields 15 and 16. - ...(this.decls.has(NUMBER_MAP_ENTRY_DECL) || - this.decls.has(STRING_MAP_ENTRY_DECL) || - this.decls.has(MAP_ITER_DECL) - ? [ - `%ScrMapIx = type { ${this.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`, - ] - : []), - // String-keyed reference reads load the value slot of the entry - // scr_map_entry_str returns (map-number-lookup.ts). - ...(this.decls.has(NUMBER_MAP_ENTRY_DECL) || - this.decls.has(STRING_MAP_ENTRY_DECL) || - this.decls.has(MAP_ITER_DECL) - ? [`%ScrMapEntry = type { i64, i64, i64 }`] - : []), - // The runtime error prefix { rc, vt, name, message, code, cause } and the - // class-object shape { rc, pre, post, ctor, name } — field reads on - // builtin errors and classval loads GEP through these. - `%ScrError = type { ${this.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`, - `%ScrClassObj = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`, - // The runtime emitter prefix { rc, vt, reg, cls } — user subclasses - // embed it (classes.ts), and bare-emitter GEPs address through it. - `%ScrEmitter = type { ${this.sizeType}, ptr, ptr, ptr }`, - // The runtime stream layout { rc, vt, reg, cls, st } — one struct - // for all five stream classes; stream subclasses embed it. - `%ScrStream = type { ${this.sizeType}, ptr, ptr, ptr, ptr }`, - // The EventTarget and Event subclass prefixes { rc, vt, state, cls }. - ...(this.classMeta.has(RUNTIME_EVENT_TARGET_CLASS) - ? [`%ScrEventTargetObj = type { ${this.sizeType}, ptr, ptr, ptr }`] - : []), - ...(this.classMeta.has(RUNTIME_EVENT_CLASS) - ? [`%ScrEventObj = type { ${this.sizeType}, ptr, ptr, ptr }`] - : []), - // The catch-binding snapshot box { rc, kind, f64, b, payload, - // retain_fn, release_fn, trace_fn } — caughtTest kind reads and - // caughtNarrow payload extraction GEP through it (offsets match - // scr_runtime.h's natural alignment: kind at 8, f64 at 16, b at 24, - // payload at 32). - `%ScrCaught = type { ${this.sizeType}, i32, double, i8, ptr, ptr, ptr, ptr }`, - // ScrBytes { rc, len, elem(i32+pad), data, backing, brands, shared }. Indexed - // typed-array access GEPs through this directly: the IR type already - // fixes elem, so the hot path needs neither a runtime kind load nor - // the generic scr_bytes_get/set call. - `%ScrBytes = type { ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, i8, i8, i8, i8, ptr }`, - // The capture box { rc, kind, obj_retain, obj_release, obj_trace, - // slot } — TDZ reads peek the payload slot (offset 40) directly. - `%ScrBox = type { ${this.sizeType}, i32, ptr, ptr, ptr, i64 }`, - // The stack buffer of the emitted JSON serializers { data, len, cap }. - `%ScrJsonBuf = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ${this.sizeType} }`, - // The dynCheck error-path spine { parent, key, index } — the emitted - // builders stack-allocate one per recursion level (dyn.ts). - `%ScrDynPath = type { ptr, ptr, ${this.sizeType} }`, - `%ScrIslandModule = type { ptr, ptr, ${this.sizeType}, ${this.sizeType}, i32, ptr, ${this.sizeType}, ${this.sizeType} }`, - `%ScrIslandEdge = type { ptr, ptr, ptr, i32 }`, - // A generator's prefix { rc, receiver, fiber, state, out } and its - // value slot { kind, f64, b, payload, release_fn } (scr_async.c pins - // both): number and boolean yields and drives address OUT directly. - ...(this.usesGenSlots - ? [ - `%ScrGenSlot = type { i32, double, i8, ptr, ptr }`, - `%ScrGenHead = type { ${this.sizeType}, ptr, ptr, i32, %ScrGenSlot }`, - ] - : []), - ]; - for (const line of shapes.typeDefs) out.push(line); - for (const line of classShapes.typeDefs) out.push(line); - // Thread-instanced library state (abi.instance_per_thread): the - // program TU's mutable globals — module globals, run-once guards, the - // lazily-compiled regex literal caches — and the runtime globals its - // init stamps live in thread-local storage, matching the runtime - // objects compiled with -DSCR_THREAD_INSTANCES. Immutable interned - // data (string literals, unit arms, template arrays, vtables) stays - // shared. - const tl = - this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""; - out.push( - ``, - `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, - `declare void @scr_init()`, - `declare void @scr_lib_init(i32, ptr)`, - ...(this.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), - ...(this.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), - ...(this.runtimeAbiMarker && this.mod.lib === undefined - ? [`declare void @${RUNTIME_ABI_MARKER}()`] - : []), - // Overrides the runtime's weak default (scr_lib.c). - ...(this.detachedExit && this.mod.lib === undefined - ? [`@scr_detached_exit = constant i8 1`] - : []), - ); - for (const d of this.decls) out.push(d); - out.push(``); - const flushedDecls = new Set(this.decls); - { - const tl = - this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""; - out.push(...threadSlotsBlockDefinitions(this.threadSlotsBlocks, tl !== "")); - for (let site = 0; site < this.dynReadIcSites; site++) - out.push( - `@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${this.sizeType}, ${this.sizeType}, ptr } zeroinitializer`, - ); - } - for (const table of this.constantNumericTables.values()) { - const n = table.values.length; - let tableTmp = 0; - const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`); - out.push( - `@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`, - `define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`, - `entry:`, - ` %initialized = icmp ne ptr %a, null`, - ...tableIndex.lines.map((line) => ` ${line}`), - ` %safe = and i1 %initialized, ${tableIndex.ok}`, - ` br i1 %safe, label %read, label %fallback`, - `read:`, - ` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`, - ` %value = load double, ptr %slot`, - ` ret double %value`, - `fallback:`, - ` %generic = call double @scr_arr_get_number(ptr %a, double %i)`, - ` ret double %generic`, - `}`, - ``, - ); - } - for (const [text, lit] of this.literals) { - // Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } — - // the runtime ABI’s static table, retain/release skip rc == SIZE_MAX. - // 64-bit (little-endian) targets split the capacity word: the high - // half carries the precomputed Map key hash (SCR_STR_HASH_CACHE). - // Bit 31 marks all-ASCII literals (SCR_STR_ASCII_BIT) on every target. - const capWord = - this.sizeType === "i64" - ? literalCapWord(wtf8Bytes(text)) - : literalCapWord32(wtf8Bytes(text)); - out.push( - `@${lit.sym} = internal global { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, [${lit.len + 1} x i8] } ` + - `{ ${this.sizeType} -1, ${this.sizeType} ${lit.len}, ${this.sizeType} ${capWord}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`, - ); - } - if (this.literals.size > 0) out.push(``); - for (const lit of this.bigintLiterals.values()) { - // Immortal ScrBigInt: { rc = SIZE_MAX, len, sign, cap, limbs } with - // pointer-sized little-endian limbs (scr_bigint.c); zero keeps one - // zero limb so limb 0 is always readable. - const size = this.sizeType; - const limbBits = size === "i64" ? 64n : 32n; - const mask = (1n << limbBits) - 1n; - const limbs: string[] = []; - for (let m = lit.value < 0n ? -lit.value : lit.value; m > 0n; m = m >> limbBits) { - limbs.push(`${size} ${m & mask}`); - } - const len = limbs.length; - if (len === 0) limbs.push(`${size} 0`); - const sign = lit.value < 0n ? -1 : lit.value > 0n ? 1 : 0; - out.push( - `@${lit.sym} = internal global { ${size}, ${size}, i32, i32, [${limbs.length} x ${size}] } ` + - `{ ${size} -1, ${size} ${len}, i32 ${sign}, i32 ${limbs.length}, [${limbs.length} x ${size}] [${limbs.join(", ")}] }`, - ); - } - if (this.bigintLiterals.size > 0) out.push(``); - for (const [key, sym] of this.unitInstances) { - // One immortal instance per unit-armed (union, tag): tag set, payload - // slot and RC entry points zero — retain/release/collector all skip - // rc == SIZE_MAX, so these never join the RC audit or a trace walk. - const [unionId, tag] = key.split(":"); - out.push( - `@${sym} = internal global %ScrUnion { ${this.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 0 } ; ${unionId} unit arm`, - ); - } - if (this.unitInstances.size > 0) out.push(``); - for (const [unionId, sym] of this.retypeGuards) { - // Thread-local like module globals: a thread's cast caches hold only - // its own boxes. Never written, the flag folds to false. - out.push(`@${sym} = internal ${tl}global i8 0 ; ${unionId} boxes in a cast cache`); - } - if (this.retypeGuards.size > 0) out.push(``); - for (const [unionId, sym] of this.absentInstances) { - // One immortal ABSENT field-slot instance per undefined-armed union: - // the undefined arm's tag with payload 1 (ordinary unit instances - // carry 0). Every tag reader sees undefined; recordHas and the - // field reads compare tag and payload, never the address, so the - // per-object copies of separately compiled units agree. - const tag = undefinedArmTag({ kind: "union", unionId }, this.unionsById); - out.push( - `@${sym} = internal global %ScrUnion { ${this.sizeType} -1, i32 ${tag}, ptr null, ptr null, ptr null, i64 ${ABSENT_FIELD_PAYLOAD} } ; ${unionId} absent field`, - ); - } - if (this.absentInstances.size > 0) out.push(``); - if (this.needsUnionNone) - out.push( - `@sc_union_none = internal constant %ScrUnion zeroinitializer ; NULL union dummy read by inline stores`, - ``, - ); - if (this.nullableUnions.usesNullSentinel) { - // `null` in every `C | null | undefined` union (NULL is undefined): - // one module-scope immortal object like the ABSENT sentinel. - const S = this.sizeType; - out.push( - `${NULLABLE_NULL} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable null arm`, - ``, - ); - } - if (this.needsNullableAbsent) { - // The ABSENT field-slot state of every nullable-pointer union: one - // module-scope immortal object (rc == SIZE_MAX), distinct from NULL - // (undefined) and from every instance. RC entry points and the - // collector skip it like any immortal; field reads compare its address. - const S = this.sizeType; - out.push( - `${NULLABLE_ABSENT} = internal global { ${S}, ${S}, ${S}, ${S} } { ${S} -1, ${S} 0, ${S} 0, ${S} 0 } ; nullable absent field`, - ``, - ); - } - for (const [key, re] of this.regexInstances) { - // One immortal ScrRegex per (pattern, flags) literal, pointing at - // the interned source/flags strings. The bc and translated-program - // slots start null (lazy compile, cached by the runtime) — a mutable - // global, not constant. The last slot is the build-time matcher. - const matcher = regexMatchers.has(key) ? `@${re.sym}_match` : "null"; - out.push( - `@${re.sym} = internal ${tl}global %ScrRegex { ${this.sizeType} -1, ptr ${re.src}, ptr ${re.fl}, ptr null, double 0.0, ptr null, ptr null, ptr ${matcher} } ; ${key.replace(/\n/g, "\\n")}`, - ); - } - if (this.regexInstances.size > 0) out.push(``); - for (const code of regexMatchers.values()) out.push(...code.lines, ``); - for (const [, inst] of this.templateStringsInstances) { - // One immortal ScrArr per tagged-template site: a [N x ptr] data - // global of interned cooked-string literals, and the ScrArr header - // over it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, - // no REF entry points). Every dense slot is present; reads retain - // immortal strings — a no-op. - const n = inst.slots.length; - const present = - n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`; - out.push( - `@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, - `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, - `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`, - ); - } - if (this.templateStringsInstances.size > 0) out.push(``); - for (const [text, c] of this.cstrs) { - // NUL-terminated byte-array constants for scr_jb_puts and indentation. - out.push(`@${c.sym} = internal constant [${c.len + 1} x i8] c"${llStrBytes(text)}"`); - } - if (this.cstrs.size > 0) out.push(``); - for (const [body, sym] of this.mapOpsConsts) { - out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); - } - if (this.mapOpsConsts.size > 0) out.push(``); - - for (const line of ffiCallbacks.globals) out.push(line); - if (ffiCallbacks.globals.length > 0) out.push(``); - for (const g of globals) { - const ty = this.llType(g.type); - const zero = ty === "double" ? f64Lit(0) : ty === "ptr" ? "null" : "false"; - const debug = this.debug?.global(g); - out.push( - `@${mangleGlobal(g.id)} = internal ${tl}global ${ty} ${zero}${debug ? `, !dbg ${debug}` : ""} ; ${g.name}`, - ); - } - if (globals.length > 0) out.push(``); - for (const line of helpers) out.push(line); - for (const line of ffiCallbacks.defs) out.push(line); - for (const line of shapes.defs) out.push(line); - for (const line of classShapes.defs) out.push(line); - for (const line of classObjDefs) out.push(line); - for (const line of this.walkers.defs) out.push(line); - for (const line of this.dyn.defs) out.push(line); - for (const line of wrappers) out.push(line); - for (const line of asyncDefs) out.push(line); - for (const line of this.resolveThunkDefs) out.push(line); - out.push(fnDefs[0] ?? ""); - for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!); - out.push(""); - - // main(): scr_init, the program-dependent error-vt interval stamps, - // scr_lib_init(argc, argv), then the entry function. An uncaught - // exception escaping top-level code prints and exits 1 (Node). - // The exception epilogue is emitted when the entry may throw. - const stamps: string[] = [...errorViews]; - for (const iv of this.errorIntervals) { - const fields: [number, number][] = [ - [0, iv.pre], - [1, iv.post], - ]; - for (const [field, value] of fields) { - stamps.push( - ` store ${this.sizeType} ${value}, ptr getelementptr inbounds ([${RUNTIME_ERROR_CLASSES.size} x %ScrVt], ptr @scr_error_vts, i64 0, i64 ${iv.kind}, i32 ${field})${field === 1 ? ` ; ${iv.lib}` : ""}`, - ); - } - } - if (this.emitterInterval !== null) { - // The runtime emitter vtable's interval (emitterVtStampLines): bare - // EventEmitter instances answer instanceof and dispatch dynamic - // teardown under THIS module's preorder numbering. - out.push(`@scr_emitter_vt = external ${tl}global %ScrVt`, ``); - stamps.push( - ` store ${this.sizeType} ${this.emitterInterval.pre}, ptr getelementptr inbounds (%ScrVt, ptr @scr_emitter_vt, i64 0, i32 0)`, - ` store ${this.sizeType} ${this.emitterInterval.post}, ptr getelementptr inbounds (%ScrVt, ptr @scr_emitter_vt, i64 0, i32 1) ; EventEmitter`, - ); - } - for (const iv of this.streamIntervals) { - // The runtime stream vtables' intervals (streamVtStampLines) — the - // emitter story: instanceof and dynamic teardown dispatch through - // them. - out.push(`@${iv.vt} = external ${tl}global %ScrVt`); - stamps.push( - ` store ${this.sizeType} ${iv.pre}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 0)`, - ` store ${this.sizeType} ${iv.post}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 1) ; ${iv.lib}`, - ); - } - if (this.streamIntervals.length > 0) out.push(``); - if ([...this.literals.keys(), ...this.cstrs.keys()].some(hasLoneSurrogate)) { - // The runtime skips surrogate work until a lone surrogate may exist - // (scr_wtf8_any); its own producers note one, and literals do here. - out.push(`declare void @scr_wtf8_note()`, ``); - stamps.push(` call void @scr_wtf8_note()`); - } - if (this.errorIntervals.length > 0 && this.tracedShapes.has("object:%Error")) { - // The cycle fixpoint marked the Error hierarchy (a user subclass - // holds cycle-capable fields — capability is hierarchy-uniform), so - // the runtime's own error allocations need collector headers too. - // Declared inline: the extern block already flushed (LLVM is - // order-free, the helper-defs precedent). - out.push(`declare void @scr_error_set_traced()`, ``); - stamps.push(` call void @scr_error_set_traced()`); - } - if ((entryMayThrow || runsLoop) && this.mod.lib === undefined) { - // Declared inline: the extern block already flushed (LLVM is - // order-free — the scr_error_set_traced precedent). Only the - // printer emits here (nothing else declares it); scr_exc_pending - // and the loop entry points rode the Set before the flush. - out.push( - `declare void @scr_exc_print_uncaught()`, - `declare zeroext i1 @scr_exc_handle_uncaught(i1 zeroext)`, - ``, - ); - } - if (this.mod.lib !== undefined) { - // LIBRARY mode: no @main — the profile-declared external - // symbols specified by the library IR instead. - for (const line of this.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line); - out.push(...this.inlineRcTail(flushedDecls)); - out.push(`attributes #0 = { sanitize_address }`); - if (this.usesCoroutines) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`); - if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`); - out.push(``); - out.push(...this.fieldAliasMetadata()); - if (this.debug !== null) out.push(this.debug.render()); - return out; - } - out.push( - `define i32 @${this.mod.workers ? "sc_context_entry" : this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`, - `entry:`, - ...(this.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), - ...(this.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []), - ` call void @scr_init()`, - // Each context (the main thread, every worker) installs its stack - // guard before running compiled code that checks it. - ...(this.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), - ...stamps, - // Event-surface programs (signal/exit listeners) fill the loop's - // nullable event hooks before %main — scr_events.c links only when - // this line is emitted (native/executable.ts gates on the same predicate). - ...(usesEvents ? [` call void @scr_events_install()`] : []), - ...(usesChildProcess ? [` call void @scr_child_dyn_install()`] : []), - ...(childStreamBoxes - ? [ - ` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`, - ] - : []), - ...(workerStdioBox || workerStdinBox - ? [ - ` call void @scr_worker_stdio_install(ptr @scr_stream_child_ops, ptr ${workerStdioBox ? `@${workerStdioBox}` : "null"}, ptr ${workerStdinBox ? `@${workerStdinBox}` : "null"})`, - ] - : []), - // fs.watch programs fill the loop's watch hooks the same way — - // scr_watch.c links only when this line is emitted. - ...(usesFsWatch ? [` call void @scr_watch_install()`] : []), - ...(this.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []), - ...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []), - ...(usesFetch ? [` call void @scr_fetch_install()`] : []), - ...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []), - ...(usesHttp ? [` call void @scr_http_dyn_install()`] : []), - ...(usesDgram ? [` call void @scr_dgram_install()`] : []), - ...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []), - ...(usesStream ? [` call void @scr_stream_install()`] : []), - ` call void @scr_lib_init(i32 %argc, ptr %argv)`, - // node:test takes Node's --test-update-snapshots out of argv first. - ...(usesNodeTest ? [` call void @scr_test_init()`] : []), - ...(asyncEntry - ? [` %top = call ptr @${mangleAsyncSpawn(this.mod.entry)}()`] - : [` call void @${mangleFunction(this.mod.entry)}()`]), - // Uncaught exception from top-level code: Node exits 1. - ...(entryMayThrow && !asyncEntry - ? [ - ` %exc = call zeroext i1 @scr_exc_pending()`, - ` br i1 %exc, label %uncaught, label %ok`, - `uncaught:`, - ` %handled = call zeroext i1 @scr_exc_handle_uncaught(i1 false)`, - ` br i1 %handled, label %ok, label %fatal`, - `fatal:`, - ` call void @scr_exc_print_uncaught()`, - ...exitListenerLines("xu"), - ...uncaughtReleases, - ` %failure_code_0 = call i32 @scr_exit_code_hint_get()`, - ` ret i32 %failure_code_0`, - `ok:`, - ] - : []), - // The event loop runs to exhaustion (microtasks before timers). A - // throw escaping a timer callback and unhandled promise rejections - // both exit 1, like Node — the C main's loop block exactly. - ...(runsLoop - ? [ - ...(this.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []), - ` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`, - ` %lexc = call zeroext i1 @scr_exc_pending()`, - ` br i1 %lexc, label %luncaught, label %lok`, - `luncaught:`, - ` call void @scr_exc_print_uncaught()`, - ...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []), - ...exitListenerLines("xl"), - ...loopReleasesU, - ` %failure_code_1 = call i32 @scr_exit_code_hint_get()`, - ` ret i32 %failure_code_1`, - `lok:`, - ` br i1 %loop_rejection, label %lreported, label %lclean`, - `lreported:`, - ` call void @scr_discard_unhandled_rejections()`, - ...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []), - ...exitListenerLines("xq"), - ...loopReportedReleases, - ` %failure_code_2 = call i32 @scr_exit_code_hint_get()`, - ` ret i32 %failure_code_2`, - `lclean:`, - ...(asyncEntry - ? [ - ` %tla_status = call i32 @scr_promise_finish_top_level(ptr %top)`, - ` %tla_rejected = icmp eq i32 %tla_status, 1`, - ` br i1 %tla_rejected, label %tla_fail, label %tla_not_rejected`, - `tla_fail:`, - // The loop already delivered every earlier-checkpoint - // rejection. Drop same-checkpoint competitors before - // surfacing the fatal module verdict. - ` call void @scr_discard_unhandled_rejections()`, - ` call void @scr_promise_rethrow_top_level(ptr %top)`, - ` call void @scr_promise_release(ptr %top)`, - ` call void @scr_exc_print_uncaught()`, - ...exitListenerLines("xt"), - ...topRejectReleases, - ` %failure_code_3 = call i32 @scr_exit_code_hint_get()`, - ` ret i32 %failure_code_3`, - `tla_not_rejected:`, - ` call void @scr_promise_release(ptr %top)`, - ] - : []), - ` %rej = call zeroext i1 @scr_report_unhandled_rejections()`, - ` br i1 %rej, label %lrej, label %lrok`, - `lrej:`, - ...exitListenerLines("xr"), - ...loopReleasesR, - ` %failure_code_4 = call i32 @scr_exit_code_hint_get()`, - ` ret i32 %failure_code_4`, - `lrok:`, - ...(asyncEntry - ? [ - ` %tla_pending = icmp eq i32 %tla_status, 13`, - ` br i1 %tla_pending, label %tla_stuck, label %tla_ok`, - `tla_stuck:`, - ...topPendingExitLines(), - `tla_ok:`, - ] - : []), - ] - : []), - ...exitListenerLines("xn"), - ...globalReleases, - ...(usesNodeTest - ? [` %test_exit = call i32 @scr_test_exit_code()`, ` ret i32 %test_exit`] - : [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]), - `}`, - ``, - ...this.inlineRcTail(flushedDecls), - // sanitize_address is inert under the plain pipeline; the sanitized - // lane's -fsanitize=address link activates instrumentation over the - // emitted functions too (the runtime TUs get theirs from clang). - `attributes #0 = { sanitize_address }`, - ...(this.usesCoroutines ? [`attributes #1 = { sanitize_address presplitcoroutine }`] : []), - ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []), - ``, - ); - if (this.mod.workers) { - out.push( - `declare void @scr_context_cleanup()`, - `declare i32 @scr_worker_argc()`, - `declare ptr @scr_worker_argv()`, - `define i32 @sc_worker_entry(i32 %root) ${FN_ATTRS} {`, - `entry:`, - ` %argc = call i32 @scr_worker_argc()`, - ` %argv = call ptr @scr_worker_argv()`, - ` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`, - ` ret i32 %result`, - `}`, - `define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`, - `entry:`, - ` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`, - ` call void @scr_context_cleanup()`, - ` ret i32 %result`, - `}`, - ); - } - out.push(...this.fieldAliasMetadata()); - if (this.debug !== null) out.push(this.debug.render()); - return out; + emitModuleParts(): string[] { + return emitModuleParts(this); } - - /** LIBRARY mode: the profile-declared external definitions — the - * export-map wrappers plus init / sink-registration / reset / collect. - * Plain `define` (not `define internal`) — the exact linkage distinction - * that separates the executable lane's @main from everything else. The - * bodies delegate every runtime half to scr_library.c, mirroring the C - * emission line for line, so the two lanes are identical by - * construction. */ - private emitLibDefs( + emitLibDefs( globals: IrGlobal[], globalReleaseLines: (prefix: string) => string[], stamps: string[], ): string[] { - const lib = this.mod.lib!; - const autoReset = lib.resultResetSymbol === null; - const out: string[] = [``, `; ── library-mode entries (profile: ${lib.profileName}) ──`, ``]; - // Every entry's prologue records its external symbol in the funnel's - // current-entry slot (structured trap-teaching field 2); the symbols - // live as internal constants, one per entry. - const symConst = (sym: string): string => `@sc_lib_sym_${sym}`; - const emitSymConst = (sym: string): void => { - out.push( - `${symConst(sym)} = internal constant [${Buffer.byteLength(sym, "utf8") + 1} x i8] c"${llStrBytes(sym)}"`, - ); - }; - emitSymConst(lib.initSymbol); - emitSymConst(lib.sinkRegisterSymbol); - if (lib.callbackRegisterSymbol !== null && lib.callbackRegisterSymbol !== undefined) - emitSymConst(lib.callbackRegisterSymbol); - if (lib.resultResetSymbol !== null) emitSymConst(lib.resultResetSymbol); - if (lib.collectSymbol !== null) emitSymConst(lib.collectSymbol); - for (const e of lib.exports) emitSymConst(e.symbol); - out.push(``); - if (this.wasi) { - for (const symbol of ["scriptc_alloc", "scriptc_free"]) emitSymConst(symbol); - out.push( - `define ptr @scriptc_alloc(i32 %size) ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_alloc")})`, - ` %p = call ptr @malloc(i32 %size)`, - ` ret ptr %p`, - `}`, - ``, - `define void @scriptc_free(ptr %p) ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_free")})`, - ` call void @free(ptr %p)`, - ` ret void`, - `}`, - ``, - ); - } - // The runtime detected-trap overlay table (scr_runtime.h declares it, - // the library trap funnel consults it): flat code/teaching/remediation - // triples, one per runtime trap code (SC4013–SC4019) the profile - // declares text for. The funnel assembles the structured sink message. - // The empty table still defines the symbols the funnel links against. - const ovlCells: string[] = []; - lib.trapOverlays.forEach((o, i) => { - const cell = (name: string, text: string | undefined): void => { - if (text === undefined) { - ovlCells.push("ptr null"); - return; - } - const sym = `@sc_lib_ovl_${i}_${name}`; - out.push( - `${sym} = internal constant [${Buffer.byteLength(text, "utf8") + 1} x i8] c"${llStrBytes(text)}"`, - ); - ovlCells.push(`ptr ${sym}`); - }; - cell("code", o.code); - cell("teach", o.teaching); - cell("rem", o.remediation); - }); - out.push( - ovlCells.length === 0 - ? `@scr_library_trap_overlays = constant [1 x ptr] zeroinitializer` - : `@scr_library_trap_overlays = constant [${ovlCells.length} x ptr] [${ovlCells.join(", ")}]`, - `@scr_library_trap_overlays_len = constant ${this.sizeType} ${lib.trapOverlays.length}`, - ``, - ); - // The init entry: full deterministic reset-and-reevaluate. Program - // globals release and zero first (run-once guards included), then the - // runtime session reset, the error-vt interval stamps verbatim from - // the executable main, %main itself, and the escaped-exception check. - const zeroStores = globals.map((g) => { - const ty = this.llType(g.type); - const zero = ty === "double" ? f64Lit(0) : ty === "ptr" ? "null" : "false"; - return ` store ${ty} ${zero}, ptr @${mangleGlobal(g.id)} ; ${g.name}`; - }); - out.push( - `define void @${lib.initSymbol}() ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_entry(i1 zeroext true, ptr ${symConst(lib.initSymbol)}) ; init always resets the result arena`, - ...globalReleaseLines("ci"), - ...zeroStores, - ` call void @scr_library_reset()`, - ...stamps, - ` call void @${mangleFunction(this.mod.entry)}()`, - ` call void @scr_library_check_exc()`, - ` ret void`, - `}`, - ``, - `define void @${lib.sinkRegisterSymbol}(ptr %fn, ptr %ctx) ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.sinkRegisterSymbol)})`, - ` call void @scr_library_set_sink(ptr %fn, ptr %ctx)`, - ` ret void`, - `}`, - ``, - ); - if (lib.callbacks !== undefined && lib.callbacks.length > 0) { - // Host-callback channels: the per-channel name constants (the - // registration dispatch's strcmp operands), the per-channel - // unregistered-call trap constants (the ffiCall sites' - // scr_library_cb_require operands), and the registration function: - // a pure store dispatch - // (the sink registration's rule — no entry prologue, no poison - // guard) whose first operation rejects callback-time re-entry - // (SC4026). An unknown or NULL name is a defined -1, never a store. - for (const cb of lib.callbacks) { - out.push( - `@sc_lib_cb_name_${cb.slot} = internal constant [${Buffer.byteLength(cb.name, "utf8") + 1} x i8] c"${llStrBytes(cb.name)}"`, - `@sc_lib_cb_trap_${cb.slot} = internal constant [${Buffer.byteLength(cb.unregisteredTrap, "utf8") + 1} x i8] c"${llStrBytes(cb.unregisteredTrap)}"`, - ); - } - out.push( - ``, - `define i32 @${lib.callbackRegisterSymbol}(ptr %name, ptr %fn, ptr %ctx) ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.callbackRegisterSymbol!)})`, - ` %isnull = icmp eq ptr %name, null`, - ` br i1 %isnull, label %miss, label %try0`, - ); - lib.callbacks.forEach((cb, i) => { - const next = i + 1 < lib.callbacks!.length ? `try${i + 1}` : "miss"; - out.push( - `try${i}: ; channel '${cb.name}'`, - ` %cmp${i} = call i32 @strcmp(ptr %name, ptr @sc_lib_cb_name_${cb.slot})`, - ` %eq${i} = icmp eq i32 %cmp${i}, 0`, - ` br i1 %eq${i}, label %set${i}, label %${next}`, - `set${i}:`, - ` call void @scr_library_cb_set(${this.sizeType} ${cb.slot}, ptr %fn, ptr %ctx)`, - ` ret i32 0`, - ); - }); - out.push(`miss:`, ` ret i32 -1`, `}`, ``); - } - if (lib.identity !== undefined && this.emitLibraryIdentity) { - // Profile-declared identity getters (the ask-2 sidecar's boot-time - // pairing fence): pure data returns with NO entry prologue — exempt - // from the poisoned guard and every runtime touch (ratified), so a - // host can read them before init and after a trap. The u64 rides - // i64 two's-complement (LLVM integer constants are signed). - for (const line of emitLibraryIdentityLines(lib.identity, FN_ATTRS)) out.push(line); - } - if (lib.resultResetSymbol !== null) { - out.push( - `define void @${lib.resultResetSymbol}() ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst(lib.resultResetSymbol)})`, - ` call void @scr_library_arena_reset()`, - ` ret void`, - `}`, - ``, - ); - } - if (lib.collectSymbol !== null) { - out.push( - `define void @${lib.collectSymbol}() ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_library_entry(i1 zeroext false, ptr ${symConst(lib.collectSymbol)})`, - ` call void @scr_library_collect() ; arena reset + a full cycle collection`, - ` ret void`, - `}`, - ``, - ); - } - for (const e of lib.exports) { - const params: string[] = []; - const body: string[] = [ - ` call void @scr_library_entry(i1 zeroext ${autoReset ? "true" : "false"}, ptr ${symConst(e.symbol)})`, - ]; - const args: string[] = []; - if (e.inboundBytesTrap !== undefined) { - // The bytes-in helper's trap message: the compiler-assembled - // structured trap-teaching form (0x01 text 0x1F SC4012 0x1F symbol - // [0x1F remediation]) consumed by the runtime sink. - const trapBytes = Buffer.byteLength(e.inboundBytesTrap, "utf8"); - out.push( - `@sc_lib_bytes_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundBytesTrap)}"`, - ``, - ); - } - if (e.inboundIntTrap !== undefined) { - // The i64/u64-in helpers' host-contract trap message (ask 4): an - // inbound integer past ±(2^53−1) cannot ride f64 exactly. Same - // assembled structured form, same SC4012 code, same - // emission-invariance argument as the bytes trap. - const trapBytes = Buffer.byteLength(e.inboundIntTrap, "utf8"); - out.push( - `@sc_lib_int_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundIntTrap)}"`, - ``, - ); - } - if (e.inboundCstringTrap !== undefined) { - // The cstring-in helper's host-contract trap message (a NULL - // `const char *`): same assembled structured form and SC4012 code. - const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8"); - out.push( - `@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`, - ``, - ); - } - e.params.forEach((cls, i) => { - switch (cls) { - case "f64": - params.push(`double %a${i}`); - args.push(`double %a${i}`); - break; - case "bool": - params.push(`i8 %a${i}`); - body.push(` %c${i} = icmp ne i8 %a${i}, 0`); - args.push(`i1 %c${i}`); - break; - case "u8": - params.push(`i8 %a${i}`); - body.push(` %c${i} = uitofp i8 %a${i} to double`); - args.push(`double %c${i}`); - break; - case "u32": - params.push(`i32 %a${i}`); - body.push(` %c${i} = uitofp i32 %a${i} to double`); - args.push(`double %c${i}`); - break; - case "i32": - params.push(`i32 %a${i}`); - body.push(` %c${i} = sitofp i32 %a${i} to double`); - args.push(`double %c${i}`); - break; - case "i64": - // Inbound declared-integer edge (ask 4): the helper converts - // exactly or delivers the host-contract trap (past ±(2^53−1) - // the value cannot ride f64 without rounding). - params.push(`i64 %a${i}`); - body.push( - ` %c${i} = call double @scr_library_i64_in(i64 %a${i}, ptr @sc_lib_int_trap_${e.symbol})`, - ); - args.push(`double %c${i}`); - break; - case "u64": - params.push(`i64 %a${i}`); - body.push( - ` %c${i} = call double @scr_library_u64_in(i64 %a${i}, ptr @sc_lib_int_trap_${e.symbol})`, - ); - args.push(`double %c${i}`); - break; - case "string": - params.push(`ptr %a${i}_ptr`, `${this.sizeType} %a${i}_len`); - body.push( - ` %c${i} = call ptr @scr_library_str_in(ptr %a${i}_ptr, ${this.sizeType} %a${i}_len)`, - ); - args.push(`ptr %c${i}`); - break; - case "bytes": - params.push(`ptr %a${i}_ptr`, `${this.sizeType} %a${i}_len`); - body.push( - ` %c${i} = call ptr @scr_library_bytes_in(ptr %a${i}_ptr, ${this.sizeType} %a${i}_len, ptr @sc_lib_bytes_trap_${e.symbol})`, - ); - args.push(`ptr %c${i}`); - break; - case "cstring": - params.push(`ptr %a${i}`); - body.push( - ` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`, - ); - args.push(`ptr %c${i}`); - break; - } - }); - if (e.returns === "string" || e.returns === "bytes") { - params.push(`ptr %out`, `ptr %out_len`); - } - const target = `@${mangleFunction(e.fnName)}`; - const callArgs = args.join(", "); - let retType = "void"; - switch (e.returns) { - case "void": - body.push( - ` call void ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` ret void`, - ); - break; - case "f64": - retType = "double"; - body.push( - ` %r = call double ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` ret double %r`, - ); - break; - case "i64": - case "u64": - // The outbound declared-integer edge (ask 4): every value - // reaching this return was PROVEN whole and inside the class's - // range at compile time, so the fp-to-int conversion is exact - // by construction — the crossing carries the mathematically - // exact integer the f64 held. - retType = "i64"; - body.push( - ` %r = call double ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` %z = ${e.returns === "i64" ? "fptosi" : "fptoui"} double %r to i64`, - ` ret i64 %z`, - ); - break; - case "bool": - retType = "i8"; - body.push( - ` %r = call i1 ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` %z = zext i1 %r to i8`, - ` ret i8 %z`, - ); - break; - case "string": - body.push( - ` %r = call ptr ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` call void @scr_library_str_out(ptr %r, ptr %out, ptr %out_len)`, - ` ret void`, - ); - break; - case "bytes": - body.push( - ` %r = call ptr ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` call void @scr_library_bytes_out(ptr %r, ptr %out, ptr %out_len)`, - ` ret void`, - ); - break; - case "cstring": - // The result's arena copy, returned as `const char *`. - retType = "ptr"; - body.push( - ` %r = call ptr ${target}(${callArgs})`, - ` call void @scr_library_check_exc()`, - ` %z = call ptr @scr_library_cstr_out(ptr %r)`, - ` ret ptr %z`, - ); - break; - } - out.push( - `define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`, - `entry:`, - ...body, - `}`, - ``, - ); - } - return out; + return emitLibDefs(this, globals, globalReleaseLines, stamps); } - - /** The shared abort helpers (emitted only when referenced): the OOM - * abort of untraced shape allocation and the invalid-union-tag abort — - * both print the runtime ABI’s exact message on fd 2 and abort. */ - private helperDefs(): string[] { - const defs: string[] = []; - const msgHelper = (fnName: string, msgSym: string, msg: string): void => { - // The message routes through the runtime's trap funnel: executable - // builds expand to the historical bytes-on-stderr + abort; library - // builds route to the registered panic sink (scr_runtime.h). - const bytes = Buffer.byteLength(msg, "utf8"); - this.declare(`declare void @scr_trap(ptr)`); - defs.push( - `@${msgSym} = internal constant [${bytes + 1} x i8] c"${llStrBytes(msg)}"`, - `define internal void @${fnName}() ${FN_ATTRS} {`, - `entry:`, - ` call void @scr_trap(ptr @${msgSym})`, - ` unreachable`, - `}`, - ``, - ); - }; - if (this.needsOom) msgHelper("sc_oom", "sc_oom_msg", "scriptc: out of memory\n"); - if (this.needsBadTag) { - msgHelper("sc_bad_tag", "sc_bad_tag_msg", "scriptc: internal error: invalid union tag\n"); - } - if (this.needsBadKey) { - // The keyed-read miss on a result type that cannot say `undefined`: - // trap like an array OOB read instead of corrupting a typed slot - // (SEMANTICS.md; the C helper's message additionally interpolates - // the runtime key — a trap-path debugging nicety, never reachable - // by a program whose behavior matches Node). - msgHelper( - "sc_bad_key", - "sc_bad_key_msg", - "scriptc: TypeError: record has no key (typed slot — no undefined is representable)\n", - ); - } - if (this.needsRetainBox) { - // scr_box_retain is a static inline (increment-unless-immortal); - // emitted once with internal linkage, like the record retains. - defs.push( - `define internal ptr @sc_retain_box(ptr %b) ${FN_ATTRS} {`, - `entry:`, - ` %rc = load ${this.sizeType}, ptr %b`, - ` %imm = icmp eq ${this.sizeType} %rc, -1`, - ` br i1 %imm, label %done, label %inc`, - `inc:`, - ` %n = add ${this.sizeType} %rc, 1`, - ` store ${this.sizeType} %n, ptr %b`, - ` br label %done`, - `done:`, - ` ret ptr %b`, - `}`, - ``, - ); - } - // Number-key Map/Set lookups probe the runtime's direct index inline: - // while ndense is nonzero every live key is an integer below it, so an - // in-range integral key reads its slot (entry index + 1, 0 = absent) and - // any other key is absent. Maps without the index (ndense 0) hash the - // key in sc_map_entry_f64_hashed, which scans the entries of a small map - // or probes a compact bucket table as scr_map_find_f64 does, and calls - // the runtime lookup for a wide table. It stays out of line so that the - // direct-index path stays small at every call site. Both only read - // memory, so LLVM merges lookups of one key when nothing is stored in - // between (a has() followed by get(), for example). - if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) { - const sz = this.sizeType; - let mapTmp = 0; - const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`); - defs.push( - `define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`, - `entry:`, - ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, - ...(sz === "i64" - ? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`] - : [` %nd = load i32, ptr %ndp`]), - // A non-integral key inside the table is as absent as one outside it. - ...mapIndex.lines.map((line) => ` ${line}`), - ` br i1 ${mapIndex.ok}, label %slot, label %outside`, - `slot:`, - sz === "i64" - ? ` %i = add i64 ${mapIndex.wide}, 0` - : ` %i = trunc i64 ${mapIndex.wide} to ${sz}`, - ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`, - ` %dense = load ptr, ptr %dp`, - ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, - ` %s = load i32, ptr %sp`, - ` %empty = icmp eq i32 %s, 0`, - ` br i1 %empty, label %miss, label %hit`, - `hit:`, - ` %e32 = sub i32 %s, 1`, - sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, - ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, - ` %entries = load ptr, ptr %ep`, - ` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`, - ` ret ptr %found`, - `miss:`, - ` ret ptr null`, - `outside:`, - ` %dense.any = icmp ne ${sz} %nd, 0`, - ` br i1 %dense.any, label %miss, label %slow`, - `slow:`, - ` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`, - ` ret ptr %hashed`, - `}`, - ``, - // Hashing (scr_map_find_f64): the SameValueZero key word, its - // scr_key_mix hash (zero remapped to one) and the absent-key filter. - `define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`, - `entry:`, - ` %nan = fcmp uno double %k, %k`, - ` %zero = fcmp oeq double %k, 0.0`, - ` %kz = select i1 %zero, double 0.0, double %k`, - ` %kzb = bitcast double %kz to i64`, - ` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`, - ` %x1s = lshr i64 %kb, 30`, - ` %x1 = xor i64 %kb, %x1s`, - ` %x2 = mul i64 %x1, -4658895280553007687`, - ` %x3s = lshr i64 %x2, 27`, - ` %x3 = xor i64 %x2, %x3s`, - ` %x4 = mul i64 %x3, -7723592293110705685`, - ` %x5s = lshr i64 %x4, 31`, - ` %x5 = xor i64 %x4, %x5s`, - ` %hz = icmp eq i64 %x5, 0`, - ` %h = select i1 %hz, i64 1, i64 %x5`, - ` %f1s = lshr i64 %h, 44`, - ` %f1a = and i64 %f1s, 63`, - ` %f1 = shl i64 1, %f1a`, - ` %f2s = lshr i64 %h, 50`, - ` %f2a = and i64 %f2s, 63`, - ` %f2 = shl i64 1, %f2a`, - ` %fb = or i64 %f1, %f2`, - ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, - ` %filter = load i64, ptr %fp`, - ` %fm = and i64 %filter, %fb`, - ` %fin = icmp eq i64 %fm, %fb`, - ` br i1 %fin, label %table, label %miss`, - `table:`, - ` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`, - ...(sz === "i64" - ? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`] - : [` %nb = load i32, ptr %nbp`]), - ` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, - ` %ents = load ptr, ptr %eps`, - ` %linear = icmp eq ${sz} %nb, 0`, - ` br i1 %linear, label %scan, label %buckets`, - // Small maps keep no bucket table: scan the cached entry hashes. - `scan:`, - ` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, - ...(sz === "i64" - ? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`] - : [` %ne = load i32, ptr %nep`]), - ` br label %scan.loop`, - `scan.loop:`, - ` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`, - ` %sdone = icmp eq ${sz} %si, %ne`, - ` br i1 %sdone, label %miss, label %scan.entry`, - `scan.entry:`, - ` %se = getelementptr inbounds %ScrMapEntry, ptr %ents, ${sz} %si`, - ` %seh = getelementptr inbounds %ScrMapEntry, ptr %se, i32 0, i32 2`, - ` %sh = load i64, ptr %seh`, - ` %sk = load i64, ptr %se`, - ` %shq = icmp eq i64 %sh, %h`, - ` %skq = icmp eq i64 %sk, %kb`, - ` %sq = and i1 %shq, %skq`, - ` br i1 %sq, label %scan.found, label %scan.next`, - `scan.found:`, - ` ret ptr %se`, - `scan.next:`, - ` %si.next = add ${sz} %si, 1`, - ` br label %scan.loop`, - // Compact tables tag each 24-bit entry index (+1) with the hash's - // top byte; wider tables take the runtime lookup. - `buckets:`, - ` %wide = icmp ugt ${sz} %nb, 16777216`, - ` br i1 %wide, label %runtime, label %probe`, - `probe:`, - ` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`, - ` %bk = load ptr, ptr %bp`, - ` %mask = sub ${sz} %nb, 1`, - ` %ht = lshr i64 %h, 32`, - ` %ht32 = trunc i64 %ht to i32`, - ` %tag = and i32 %ht32, -16777216`, - sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`, - ` %i0 = and ${sz} %hi, %mask`, - ` br label %probe.loop`, - `probe.loop:`, - ` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`, - ` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`, - ` %b = load i32, ptr %bep`, - ` %bempty = icmp eq i32 %b, 0`, - ` br i1 %bempty, label %miss, label %probe.tag`, - `probe.tag:`, - ` %bt = and i32 %b, -16777216`, - ` %btq = icmp eq i32 %bt, %tag`, - ` br i1 %btq, label %probe.entry, label %probe.next`, - `probe.entry:`, - ` %bix = and i32 %b, 16777215`, - ` %bix1 = sub i32 %bix, 1`, - sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`, - ` %pe = getelementptr inbounds %ScrMapEntry, ptr %ents, ${sz} %bi`, - ` %peh = getelementptr inbounds %ScrMapEntry, ptr %pe, i32 0, i32 2`, - ` %ph = load i64, ptr %peh`, - ` %pk = load i64, ptr %pe`, - ` %phq = icmp eq i64 %ph, %h`, - ` %pkq = icmp eq i64 %pk, %kb`, - ` %pq = and i1 %phq, %pkq`, - ` br i1 %pq, label %probe.found, label %probe.next`, - `probe.found:`, - ` ret ptr %pe`, - `probe.next:`, - ` %pi.inc = add ${sz} %pi, 1`, - ` %pi.next = and ${sz} %pi.inc, %mask`, - ` br label %probe.loop`, - `miss:`, - ` ret ptr null`, - `runtime:`, - ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`, - ` ret ptr %r`, - `}`, - ``, - ); - } - // Map/Set iteration steps read the entries array inline: an index is - // live when it is below the entry count and its entry's cached hash is - // nonzero (a tombstone's is zero), as scr_map_iter_live decides. The - // entry count is read again at every step, so entries appended during - // the iteration are visited. - if (this.decls.has(MAP_ITER_DECL)) { - const sz = this.sizeType; - defs.push( - `define internal ptr @sc_map_iter_entry(ptr %m, double %i) ${FN_ATTRS} {`, - `entry:`, - ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, - ` %n = load i32, ptr %np`, - ` %nf = uitofp i32 %n to double`, - ` %lo = fcmp oge double %i, 0.0`, - ` %hi = fcmp olt double %i, %nf`, - ` %in = and i1 %lo, %hi`, - ` br i1 %in, label %load, label %none`, - `load:`, - ` %ix = fptoui double %i to ${sz}`, - ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, - ` %entries = load ptr, ptr %ep`, - ` %e = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %ix`, - ` %hp = getelementptr inbounds %ScrMapEntry, ptr %e, i32 0, i32 2`, - ` %h = load i64, ptr %hp`, - ` %live = icmp ne i64 %h, 0`, - ` br i1 %live, label %yes, label %none`, - `yes:`, - ` ret ptr %e`, - `none:`, - ` ret ptr null`, - `}`, - ``, - `define internal double @sc_map_iter_count(ptr %m) ${FN_ATTRS} {`, - `entry:`, - ` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, - ` %n = load i32, ptr %np`, - ` %nf = uitofp i32 %n to double`, - ` ret double %nf`, - `}`, - ``, - ); - } - // String-key Map/Set lookups test the map's absent-key filter inline - // (scr_map.c scr_map_filter_bits): a key whose two filter bits are not - // both set is absent, so most misses on small and medium maps return - // without a call. On 64-bit targets the key's cached hash h is the high - // half of its capacity word, the runtime's widened hash is (h << 32) | h, - // and the filter reads its bits 44..55, which are the capacity word's - // own. A key without a cached hash, and every possible hit, take the - // runtime lookup. - if (this.decls.has(STRING_MAP_ENTRY_DECL)) { - defs.push( - `define internal ptr @sc_map_entry_str(ptr %m, ptr %k) ${FN_ATTRS} {`, - `entry:`, - ...(this.sizeType === "i64" - ? [ - ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, - ` %filter = load i64, ptr %fp`, - ` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`, - ` %cap = load i64, ptr %capp`, - ` %cached = icmp uge i64 %cap, 4294967296`, - ` br i1 %cached, label %test, label %slow`, - `test:`, - ` %s1 = lshr i64 %cap, 44`, - ` %i1 = and i64 %s1, 63`, - ` %b1 = shl i64 1, %i1`, - ` %s2 = lshr i64 %cap, 50`, - ` %i2 = and i64 %s2, 63`, - ` %b2 = shl i64 1, %i2`, - ` %bits = or i64 %b1, %b2`, - ` %have = and i64 %filter, %bits`, - ` %maybe = icmp eq i64 %have, %bits`, - ` br i1 %maybe, label %slow, label %absent`, - `absent:`, - ` ret ptr null`, - ] - : [` br label %slow`]), - `slow:`, - ` %r = call ptr @scr_map_entry_str(ptr %m, ptr %k)`, - ` ret ptr %r`, - `}`, - ``, - ); - } - // String comparisons enter through small internal wrappers that LLVM - // inlines at each site: interned literals and shared values compare by - // identity, and equality rejects different byte lengths, before any - // runtime call (scr_str_eq/scr_str_cmp keep the complete semantics). - if (this.decls.has(`declare zeroext i1 @scr_str_eq(ptr, ptr)`)) { - defs.push( - `define internal zeroext i1 @sc_str_eq(ptr %a, ptr %b) ${FN_ATTRS} {`, - `entry:`, - ` %same = icmp eq ptr %a, %b`, - ` br i1 %same, label %yes, label %lengths`, - `lengths:`, - ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, - ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, - ` %alen = load ${this.sizeType}, ptr %alen.p`, - ` %blen = load ${this.sizeType}, ptr %blen.p`, - ` %samelen = icmp eq ${this.sizeType} %alen, %blen`, - ` br i1 %samelen, label %bytes, label %no`, - `bytes:`, - ` %eq = call zeroext i1 @scr_str_eq(ptr %a, ptr %b)`, - ` ret i1 %eq`, - `yes:`, - ` ret i1 true`, - `no:`, - ` ret i1 false`, - `}`, - ``, - ); - } - for (const [text, name] of this.literalEqualities) { - defs.push( - ...literalEqualityDefinition( - name, - wtf8Bytes(text), - this.internLiteral(text), - this.sizeType, - FN_ATTRS, - ), - ``, - ); - } - // String length and charCodeAt read proven-ASCII strings inline: bit 31 - // of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on - // little-endian targets) makes UTF-16 units bytes. Unproven strings and - // anything but an in-range index take the runtime's UTF-16 mapping. - // A scan's receiver reads inline too where runtime state is ordinary - // global storage (ScrStrScan; SCR_TL makes it thread-local otherwise). - const scanInline = this.mod.workers !== true && this.mod.lib?.threadInstances !== true; - let scanDeclared = false; - for (const helper of STRING_READ_HELPERS) { - if (!this.decls.has(helper.decl)) continue; - if (scanInline && !scanDeclared) { - defs.push(stringScanDecl(this.sizeType), ``); - scanDeclared = true; - } - defs.push(...helper.define(this.sizeType, FN_ATTRS, scanInline), ``); - } - // Ordering decides inline from the first differing bytes. The bytes - // before them are equal, so both strings are at the same position of a - // character: either both bytes continue characters with the same lead - // byte, or both start a character. Byte order is the UTF-16 order for - // two continuation bytes, and for two lead bytes when one starts a - // character below U+D000, whose UTF-16 unit is below every unit of a - // character with a lead byte of at least 0xED. Between lead bytes of at - // least 0xED, a supplementary character (0xF0..0xF4, a high surrogate - // unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of - // 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime. - // Differing words without a byte of at least 0xED decide in byte - // order; other differing words decide by their first differing byte, - // and strings shorter than a word by their first byte. Otherwise a - // proven-ASCII string on either side orders by bytes (scr_str_cmp), and - // other pairs call scr_str_cmp_u16_bytes, which only reads the two - // strings, so LLVM can merge repeated comparisons of one pair. Callers - // only test the sign of the result. - if (this.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { - const fn = "scr_str_cmp_u16"; - this.declare("declare i64 @llvm.bswap.i64(i64)"); - this.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)"); - this.declare("declare i8 @llvm.umin.i8(i8, i8)"); - this.declare("declare i32 @scr_str_cmp(ptr, ptr)"); - this.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)"); - defs.push( - `define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`, - `entry:`, - ` %same = icmp eq ptr %a, %b`, - ` br i1 %same, label %equal, label %lengths`, - `lengths:`, - ` %alen.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 1`, - ` %blen.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 1`, - ` %alen = load ${this.sizeType}, ptr %alen.p`, - ` %blen = load ${this.sizeType}, ptr %blen.p`, - ` %aempty = icmp eq ${this.sizeType} %alen, 0`, - ` %bempty = icmp eq ${this.sizeType} %blen, 0`, - ` %empty = or i1 %aempty, %bempty`, - ` br i1 %empty, label %order, label %words`, - // Both at least 8 bytes: the first word of each. - `words:`, - ` %along = icmp uge ${this.sizeType} %alen, 8`, - ` %blong = icmp uge ${this.sizeType} %blen, 8`, - ` %long = and i1 %along, %blong`, - ` br i1 %long, label %word, label %first`, - `word:`, - ` %awp = getelementptr inbounds %ScrStr, ptr %a, i32 1`, - ` %bwp = getelementptr inbounds %ScrStr, ptr %b, i32 1`, - ` %aw = load i64, ptr %awp, align 1`, - ` %bw = load i64, ptr %bwp, align 1`, - ` %wdiffer = icmp ne i64 %aw, %bw`, - ` br i1 %wdiffer, label %differ, label %wsame`, - // Equal first words and one length of at most 16 bytes: the word - // ending at that length covers the rest, and any difference lies - // past the equal first word. - `wsame:`, - ` %samelen = icmp eq ${this.sizeType} %alen, %blen`, - ` %short = icmp ule ${this.sizeType} %alen, 16`, - ` %tailable = and i1 %samelen, %short`, - ` br i1 %tailable, label %tail, label %order`, - `tail:`, - ` %toff = sub ${this.sizeType} %alen, 8`, - ` %atp = getelementptr inbounds i8, ptr %awp, ${this.sizeType} %toff`, - ` %btp = getelementptr inbounds i8, ptr %bwp, ${this.sizeType} %toff`, - ` %at = load i64, ptr %atp, align 1`, - ` %bt = load i64, ptr %btp, align 1`, - ` %tdiffer = icmp ne i64 %at, %bt`, - ` br i1 %tdiffer, label %differ, label %equal`, - `first:`, - ` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`, - ` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`, - ` %x = load i8, ptr %adata`, - ` %y = load i8, ptr %bdata`, - ` %fdiffer = icmp ne i8 %x, %y`, - ` br i1 %fdiffer, label %bytes, label %order`, - // Two differing words. When neither has a byte of at least 0xED - // (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are - // set), byte-swapped little-endian words compare in byte order. - // Otherwise the lowest set byte of their xor is the first differing - // byte. - `differ:`, - ` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`, - ` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`, - ` %xl = and i64 %dx, 9187201950435737471`, - ` %yl = and i64 %dy, 9187201950435737471`, - ` %xa = add i64 %xl, 1374463283923456787`, - ` %ya = add i64 %yl, 1374463283923456787`, - ` %xh = and i64 %dx, %xa`, - ` %yh = and i64 %dy, %ya`, - ` %hh = or i64 %xh, %yh`, - ` %whigh = and i64 %hh, -9187201950435737472`, - ` %dlow = icmp eq i64 %whigh, 0`, - ` br i1 %dlow, label %wordorder, label %locate`, - `wordorder:`, - ` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`, - ` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`, - ` %wbelow = icmp ult i64 %xsw, %ysw`, - ` %wsign = select i1 %wbelow, i32 -1, i32 1`, - ` ret i32 %wsign`, - `locate:`, - ` %dxor = xor i64 %dx, %dy`, - ` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`, - ` %dshift = and i64 %dtz, 56`, - ` %dxs = lshr i64 %dx, %dshift`, - ` %dys = lshr i64 %dy, %dshift`, - ` %dxb = trunc i64 %dxs to i8`, - ` %dyb = trunc i64 %dys to i8`, - ` br label %bytes`, - // The first differing bytes. - `bytes:`, - ` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`, - ` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`, - ` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`, - ` %blow = icmp ult i8 %bmin, -19`, - ` br i1 %blow, label %byte, label %high`, - // Both at least 0xED. A lone surrogate (0xED) takes the runtime; - // supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF - // once 0x10 is subtracted from them. - `high:`, - ` %xed = icmp eq i8 %bx, -19`, - ` %yed = icmp eq i8 %by, -19`, - ` %ed = or i1 %xed, %yed`, - ` br i1 %ed, label %order, label %planes`, - `planes:`, - ` %xsupp = icmp uge i8 %bx, -16`, - ` %ysupp = icmp uge i8 %by, -16`, - ` %xlow = add i8 %bx, -16`, - ` %ylow = add i8 %by, -16`, - ` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`, - ` %yk = select i1 %ysupp, i8 %ylow, i8 %by`, - ` %kbelow = icmp ult i8 %xk, %yk`, - ` %ksign = select i1 %kbelow, i32 -1, i32 1`, - ` ret i32 %ksign`, - `byte:`, - ` %below = icmp ult i8 %bx, %by`, - ` %sign = select i1 %below, i32 -1, i32 1`, - ` ret i32 %sign`, - `order:`, - ` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`, - ` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`, - ` %acw = load i32, ptr %acw.p`, - ` %bcw = load i32, ptr %bcw.p`, - ` %cws = or i32 %acw, %bcw`, - ` %anyascii = icmp slt i32 %cws, 0`, - ` br i1 %anyascii, label %memorder, label %units`, - `memorder:`, - ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, - ` ret i32 %m`, - `units:`, - ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, - ` ret i32 %c`, - `equal:`, - ` ret i32 0`, - `}`, - ``, - ); - } - // The declarations these helpers added must land in the extern block, - // which already flushed — append here instead (LLVM is order-free). - return defs.length > 0 ? [...defs] : defs; + helperDefs(): string[] { + return helperDefs(this); } /** The inline RC helpers every call site requested, emitted last so * requests from main/library epilogues are included. Slow-path * declarations a late request added after the extern block flushed land * here too. */ - private inlineRcTail(flushedDecls: ReadonlySet): string[] { + inlineRcTail(flushedDecls: ReadonlySet): string[] { const late = inlineRcDecls(this.shapeHost).filter((d) => !flushedDecls.has(d)); // NULL-skipping retain wrappers of nullable unions; a runtime inner // retain requested after the extern block flushed is declared here. @@ -3857,7 +1608,7 @@ export class LlEmitter { /** Env-signature wrappers + interned immortal closures for declared * functions used as values (the runtime ABI’s sc_w_/sc_fc_ pair): every * mention of `f` yields the same pointer, so `f === f` holds. */ - private emitFnValueDefs(): string[] { + emitFnValueDefs(): string[] { const out: string[] = []; for (const name of this.fnValues) { const fn = this.fnByName.get(name)!; @@ -3893,74 +1644,12 @@ export class LlEmitter { } return out; } - - /** The argument-pack ABI and trampoline prefix shared by async functions - * and generators. Their promise/generator completion and spawn tails stay - * with the callers below. */ - private emitArgPackAndTrampolinePrologue(fn: IrFunction): LlArgPackAndTrampolinePrologue { - const pack = mangleArgPack(fn.name); - const lifted = fn.captures !== undefined; - const fieldTys = [...(lifted ? ["ptr"] : []), ...fn.params.map((p) => this.llType(p.type))]; - const definitions = [`%${pack} = type { ${fieldTys.join(", ") || "i8"} } ; ${fn.name} args`]; - const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`; - - // Async argument packs live from the spawn to the trampoline's first - // instructions, so they take the runtime's small-block allocator. - // Generator packs keep malloc: the runtime frees never-started ones. - const smallPack = fn.generator === undefined; - const packAlloc = smallPack ? "scr_rt_calloc" : "malloc"; - const packFree = smallPack ? "scr_rt_free" : "free"; - this.declare(`declare void @${packFree}(ptr)`); - this.declare(`declare ptr @${packAlloc}(${this.sizeType})`); - - const tr: string[] = [ - `define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`, - `entry:`, - ]; - const loads: string[] = []; - fieldTys.forEach((ty, i) => { - tr.push( - ` %fp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`, - ` %a${i} = load ${ty}, ptr %fp${i}`, - ); - loads.push(`${ty} %a${i}`); - }); - tr.push(` call void @${packFree}(ptr %ap)`); - const ret = fn.returnType; - const retTy = this.llType(ret); - const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${loads.join(", ")})`; - tr.push(retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`); - if (lifted && !this.coroLowered(fn)) { - tr.push(` call void @scr_closure_release(ptr %a0)`); - } - - const spawnParams = fieldTys.map((ty, i) => `${ty} %a${i}`); - const argPackLines = [ - ` %ap = call ptr @${packAlloc}(${this.sizeType} ${sizeOf})`, - ` %isnull = icmp eq ptr %ap, null`, - ` br i1 %isnull, label %oom, label %ok`, - `oom:`, - ` call void @sc_oom()`, - ` unreachable`, - `ok:`, - ]; - if (lifted) { - // scr_closure_retain is a header static inline — the `_v` twin is - // the exported symbol. - argPackLines.push(` %env = call ptr @scr_closure_retain_v(ptr %a0)`); - } - fieldTys.forEach((ty, i) => { - const src = lifted && i === 0 ? "%env" : `%a${i}`; - argPackLines.push( - ` %sp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`, - ` store ${ty} ${src}, ptr %sp${i}`, - ); - }); - return { definitions, pack, lifted, fieldTys, ret, tr, spawnParams, argPackLines }; + emitArgPackAndTrampolinePrologue(fn: IrFunction): LlArgPackAndTrampolinePrologue { + return emitArgPackAndTrampolinePrologue(this, fn); } /** Settles `promise` from a clean async body result `%r` (moved in). */ - private asyncFulfillLines(ret: IrType, promise: string): string[] { + asyncFulfillLines(ret: IrType, promise: string): string[] { switch (ret.kind) { case "void": this.declare(`declare void @scr_promise_fulfill_void(ptr)`); @@ -3987,34 +1676,12 @@ export class LlEmitter { } } } - - /** Async functions whose calls can skip the fiber (scr_async_inline_*). - * A body qualifies only when nothing it runs can park its execution - * context: its emitted text, and the text of every module function it - * mentions (transitively), contains no suspension entry point. Such a - * body always completes before the call returns, which is exactly the - * behavior of a fiber that never suspends. Excluded conservatively: - * wasm32 (its coroutine lowering owns async bodies), module-evaluation - * bodies (their promise caches), - * `dyn` results (resolution may adopt a thenable), and every program - * that uses node:test subtests — `t.test()` runs a subtest inline and - * awaits it on the CURRENT fiber even from synchronous code. Worker - * programs keep fibers too: their fiber epilogue owns context - * termination (a stopping worker rejects and observes the promise), which - * the fiberless wrapper does not reproduce. A missed suspension still - * cannot reorder execution undetected: the runtime traps when a context parks while a - * fiberless body is on its stack. */ - private fiberlessAsyncFunctions(): Set { - const parks = this.parkingFunctions(); - const eligible = parks === null ? new Set() : this.neverParkingAsync(parks); - for (const name of this.inlineAwaitCallees) - if (!eligible.has(name)) - throw new InternalCompilerError(`llvm emitter bug: inline await of suspending ${name}`); - return eligible; + fiberlessAsyncFunctions(): Set { + return fiberlessAsyncFunctions(this); } /** The async functions `parks` leaves fiberless (fiberlessAsyncFunctions). */ - private neverParkingAsync(parks: ReadonlySet): Set { + neverParkingAsync(parks: ReadonlySet): Set { const eligible = new Set(); for (const fn of this.mod.functions) { if (fn.async !== true || fn.generator !== undefined) continue; @@ -4025,50 +1692,14 @@ export class LlEmitter { } return eligible; } - - /** The module functions whose bodies may park their execution context: - * their emitted text mentions a suspension entry point (SUSPENDS), or - * they mention such a function (transitively). Null where async bodies - * must keep fibers regardless: wasm32 (its coroutine lowering owns async - * bodies), worker programs, and programs that use node:test subtests, - * whose `t.test()` awaits on the CURRENT fiber even from synchronous - * code. */ - private parkingFunctions(): Set | null { - if (this.wasi || this.mod.workers === true) return null; - for (const decl of this.decls) if (/@scr_test_sub\b/.test(decl)) return null; - for (const text of this.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null; - // Reverse mention edges, then propagate "may park" to every mentioner. - const mentionedBy = new Map>(); - const parks = new Set(); - const work: string[] = []; - for (const [name, text] of this.fnDefText) { - if (SUSPENDS.test(text)) { - parks.add(name); - work.push(name); - } - for (const callee of this.mentionedFunctions(text)) { - if (callee === name) continue; - let callers = mentionedBy.get(callee); - if (callers === undefined) mentionedBy.set(callee, (callers = new Set())); - callers.add(name); - } - } - while (work.length > 0) { - const callers = mentionedBy.get(work.pop()!); - if (callers === undefined) continue; - for (const caller of callers) { - if (parks.has(caller)) continue; - parks.add(caller); - work.push(caller); - } - } - return parks; + parkingFunctions(): Set | null { + return parkingFunctions(this); } private mangledFunctions: Map | null = null; /** The module functions whose ordinary or borrowed body `text` mentions. */ - private *mentionedFunctions(text: string): Generator { + *mentionedFunctions(text: string): Generator { if (this.mangledFunctions === null) { this.mangledFunctions = new Map(); for (const fn of this.mod.functions) { @@ -4095,7 +1726,7 @@ export class LlEmitter { * otherwise the fiber form stays. Bodies that never suspend stay * fiberless, and module evaluation bodies and `dyn` results keep fibers * as they do there. The runtime traps if a stackless body ever parks. */ - private selectStacklessAsync(fnDefs: string[]): void { + selectStacklessAsync(fnDefs: string[]): void { if (!this.stacklessAsyncAllowed) return; const parks = this.parkingFunctions(); if (parks === null) return; @@ -4129,7 +1760,7 @@ export class LlEmitter { * caller's stack between scr_async_inline_enter/leave and settles the * fresh promise from its outcome — the fiber trampoline's epilogue, * without the fiber. */ - private emitFiberlessAsyncSpawn(fn: IrFunction, spawnParams: string[]): string[] { + emitFiberlessAsyncSpawn(fn: IrFunction, spawnParams: string[]): string[] { const ret = fn.returnType; const retTy = this.llType(ret); const lifted = fn.captures !== undefined; @@ -4194,7 +1825,7 @@ export class LlEmitter { * body on the caller's stack between scr_async_inline_enter/exit, as in * its spawn wrapper, returning the result and leaving a throw pending for * the awaiting coroutine (emitCoroInlineAwait). */ - private emitAsyncInlineAwaitCall(fn: IrFunction): string[] { + emitAsyncInlineAwaitCall(fn: IrFunction): string[] { const retTy = this.llType(fn.returnType); const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`); const args = params.join(", "); @@ -4249,506 +1880,23 @@ export class LlEmitter { this.emitPendingCheck(); return value; } - - /** The await-call entry of an async function (`sc_aw_*`): one direct - * `await f(...)` as a call returning the awaited value, so the call site - * needs no promise of its own. A fiberless body runs on the caller's - * stack as in its spawn wrapper; a clean result then takes the await's - * one microtask turn (scr_await_hop) and is returned without ever - * materializing the promise, which nothing else could observe. A throw - * builds the rejected promise exactly as the spawn wrapper would and - * awaits it, so the rejection reaches the awaiter on the same turn. Other - * bodies pack their arguments as the spawn wrapper does and enter - * scr_async_spawn_await_*, which parks the caller on the promise as soon - * as the body first suspends. The adopting entry (`sc_ad_*`, for - * `return f(...)` in an async function) adds the turn of the then job - * that adopts the returned promise, before the await proper. */ - private emitAsyncAwaitCall( - fn: IrFunction, - argPackLines: string[] | null, - adopt = false, - ): string[] { - const ret = fn.returnType; - const retTy = this.llType(ret); - const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`); - const awaitLine = (dest: string, promise: string): string => { - switch (ret.kind) { - case "void": - this.declare(`declare void @scr_await_void(ptr)`); - return ` call void @scr_await_void(ptr ${promise})`; - case "f64": - case "date": - this.declare(`declare double @scr_await_f64(ptr)`); - return ` ${dest} = call double @scr_await_f64(ptr ${promise})`; - case "bool": - this.declare(`declare zeroext i1 @scr_await_bool(ptr)`); - return ` ${dest} = call zeroext i1 @scr_await_bool(ptr ${promise})`; - case "string": - this.declare(`declare ptr @scr_await_str(ptr)`); - return ` ${dest} = call ptr @scr_await_str(ptr ${promise})`; - default: - this.declare(`declare ptr @scr_await_ref(ptr)`); - return ` ${dest} = call ptr @scr_await_ref(ptr ${promise})`; - } - }; - const retLine = (v: string): string => - retTy === "void" ? ` ret void` : ` ret ${retTy} ${v}`; - const head = adopt - ? `define internal ${retTy} @${mangleAsyncAdoptCall(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; adopt ${fn.name}` - : `define internal ${retTy} @${mangleAsyncAwaitCall(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; await ${fn.name}`; - const args = params.join(", "); - if (adopt && argPackLines !== null) { - // A body that may suspend settles later than the adoption's then job - // runs: materialize its promise, take that turn, then await it. - this.declare(`declare void @scr_await_hop()`); - this.declare(`declare void @scr_promise_release(ptr)`); - return [ - head, - `entry:`, - ` %p = call ptr @${mangleAsyncSpawn(fn.name)}(${args})`, - ` call void @scr_await_hop()`, - awaitLine("%v", "%p"), - ` call void @scr_promise_release(ptr %p)`, - retLine("%v"), - `}`, - ``, - ]; - } - if (argPackLines !== null && this.stacklessAsync.has(fn.name)) { - // A stackless body's eager prefix already runs on the awaiter's - // stack, which then awaits the promise like the two separate steps. - this.declare(`declare void @scr_promise_release(ptr)`); - return [ - head, - `entry:`, - ` %p = call ptr @${mangleAsyncSpawn(fn.name)}(${args})`, - awaitLine("%v", "%p"), - ` call void @scr_promise_release(ptr %p)`, - retLine("%v"), - `}`, - ``, - ]; - } - if (argPackLines !== null) { - const kind = - ret.kind === "void" - ? "void" - : ret.kind === "f64" || ret.kind === "date" - ? "f64" - : ret.kind === "bool" - ? "bool" - : ret.kind === "string" - ? "str" - : "ref"; - const cret = kind === "bool" ? "zeroext i1" : retTy; - this.declare(`declare ${cret} @scr_async_spawn_await_${kind}(ptr, ptr)`); - this.needOom(); - const call = `call ${cret} @scr_async_spawn_await_${kind}(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`; - return [ - head, - `entry:`, - ...argPackLines, - kind === "void" ? ` ${call}` : ` %v = ${call}`, - retLine("%v"), - `}`, - ``, - ]; - } - this.declare(`declare void @scr_async_inline_enter(ptr)`); - this.declare(`declare void @scr_async_inline_exit(ptr)`); - this.declare(`declare void @scr_await_hop()`); - this.declare(`declare ptr @scr_promise_new()`); - this.declare(`declare void @scr_promise_reject_pending(ptr)`); - this.declare(`declare void @scr_promise_release(ptr)`); - const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`; - const out = [ - head, - `entry:`, - ` %frame = alloca { ptr, ptr }`, - ` call void @scr_async_inline_enter(ptr %frame)`, - retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`, - ...this.pendingTestLines("%pend"), - ` call void @scr_async_inline_exit(ptr %frame)`, - // Adopting: the then job's turn comes before the settled promise's - // own reaction, whichever way the body completed. - ...(adopt ? [` call void @scr_await_hop()`] : []), - ` br i1 %pend, label %thrown, label %clean`, - `clean:`, - ` call void @scr_await_hop()`, - retLine("%r"), - `thrown:`, - ]; - if (ret.kind !== "void" && isRefCounted(ret)) { - // An escaping throw means %r is the never-read dummy (NULL). - out.push(` call void ${releaseSym(this.shapeHost, ret)}(ptr %r)`); - } - out.push( - ` %p = call ptr @scr_promise_new()`, - ` call void @scr_promise_reject_pending(ptr %p)`, - awaitLine("%v", "%p"), - ` call void @scr_promise_release(ptr %p)`, - retLine("%v"), - `}`, - ``, - ); - return out; + emitAsyncAwaitCall(fn: IrFunction, argPackLines: string[] | null, adopt = false): string[] { + return emitAsyncAwaitCall(this, fn, argPackLines, adopt); } - - /** Per-async-function machinery — async.ts's scaffolding, .ll - * flavored: an argument-pack struct type, a fiber trampoline (unpacks, - * frees the pack, runs the ordinary compiled body, settles the - * promise — fulfilling on clean return, leaving a pending exception - * for the runtime to reject with), and a spawn wrapper call sites and - * closures enter through (packs the args +1, scr_async_spawn runs the - * fiber eagerly to its first suspension and returns the promise). */ - private emitAsyncScaffolding(): string[] { - const out: string[] = []; - if (this.wasi) { - this.declare(`declare void @llvm.coro.resume(ptr)`); - this.declare(`declare void @llvm.coro.destroy(ptr)`); - out.push( - `define void @scr_wasi_coro_resume(ptr %handle) ${FN_ATTRS} {`, - `entry:`, - ` call void @llvm.coro.resume(ptr %handle)`, - ` ret void`, - `}`, - `define void @scr_wasi_coro_destroy(ptr %handle) ${FN_ATTRS} {`, - `entry:`, - ` call void @llvm.coro.destroy(ptr %handle)`, - ` ret void`, - `}`, - ``, - ); - } - const fiberless = this.fiberlessAsyncFunctions(); - for (const fn of this.mod.functions) { - if (fn.async !== true || fn.generator !== undefined) continue; - if (fiberless.has(fn.name) && this.awaitCallees.has(fn.name)) { - for (const line of this.emitAsyncAwaitCall(fn, null)) out.push(line); - } - if (fiberless.has(fn.name) && this.adoptCallees.has(fn.name)) { - for (const line of this.emitAsyncAwaitCall(fn, null, true)) out.push(line); - } - if (this.inlineAwaitCallees.has(fn.name)) { - for (const line of this.emitAsyncInlineAwaitCall(fn)) out.push(line); - } - if (fiberless.has(fn.name)) { - const fieldTys = [ - ...(fn.captures !== undefined ? ["ptr"] : []), - ...fn.params.map((p) => this.llType(p.type)), - ]; - const params = fieldTys.map((ty, i) => `${ty} %a${i}`); - for (const line of this.emitFiberlessAsyncSpawn(fn, params)) out.push(line); - continue; - } - const { definitions, ret, tr, spawnParams, argPackLines } = - this.emitArgPackAndTrampolinePrologue(fn); - for (const line of definitions) out.push(line); - if (this.awaitCallees.has(fn.name)) { - for (const line of this.emitAsyncAwaitCall(fn, argPackLines)) out.push(line); - } - if (this.adoptCallees.has(fn.name)) { - for (const line of this.emitAsyncAwaitCall(fn, argPackLines, true)) out.push(line); - } - const stackless = this.stacklessAsync.has(fn.name); - const spawn = stackless ? "scr_async_spawn_stackless" : "scr_async_spawn"; - this.declare(`declare ptr @scr_fiber_promise(ptr)`); - this.declare(`declare ptr @${spawn}(ptr, ptr)`); - this.needOom(); - if (this.wasi || stackless) { - // The coroutine body settles its own promise and owns the retained - // lifted environment until final suspension. Its initial call - // returns here at the first suspend (or final suspend). - tr.push(` ret void`, `}`, ``); - } else { - if (fn.captures !== undefined) { - this.declare(`declare void @scr_closure_release(ptr)`); - } - tr.push( - ...this.pendingTestLines("%pend"), - ` br i1 %pend, label %thrown, label %clean`, - `clean:`, - ` %pr = call ptr @scr_fiber_promise(ptr %self)`, - ); - for (const line of this.asyncFulfillLines(ret, "%pr")) tr.push(line); - tr.push(` ret void`, `thrown:`); - if (ret.kind !== "void" && isRefCounted(ret)) { - // An escaping throw means %r is the never-read dummy (NULL). - tr.push(` call void ${releaseSym(this.shapeHost, ret)}(ptr %r)`); - } - tr.push(` ret void`, `}`, ``); - } - for (const line of tr) out.push(line); - - // Spawn wrapper: pack the args (+1 moves in), spawn the fiber. - const cache = fn.asyncCacheGlobal !== undefined ? mangleGlobal(fn.asyncCacheGlobal) : null; - const cycleCache = - fn.asyncCycleCacheGlobal !== undefined ? mangleGlobal(fn.asyncCycleCacheGlobal) : null; - if (cache !== null || cycleCache !== null) { - this.declare(`declare ptr @scr_promise_retain_v(ptr)`); - this.declare(`declare void @scr_promise_release(ptr)`); - } - if (cache !== null) { - this.declare(`declare void @scr_promise_mark_handled(ptr)`); - } - if (fn.captures !== undefined) { - this.declare(`declare ptr @scr_closure_retain_v(ptr)`); - } - const sp: string[] = [ - `define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; spawn ${fn.name}`, - `entry:`, - ...(cache !== null - ? [ - ` %cached = load ptr, ptr @${cache}`, - ` %cache_hit = icmp ne ptr %cached, null`, - ` br i1 %cache_hit, label %cached_return, label %cache_miss`, - `cached_return:`, - ` %cached_owned = call ptr @scr_promise_retain_v(ptr %cached)`, - ` ret ptr %cached_owned`, - `cache_miss:`, - ] - : []), - ...argPackLines, - ]; - sp.push( - ` %p = call ptr @${spawn}(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`, - ...(cache !== null - ? [ - // The module loader owns this evaluation promise - // immediately. A later sibling can throw before the - // aggregate dependency wait is built, but this rejection - // must never become an unrelated unhandled rejection. - ` call void @scr_promise_mark_handled(ptr %p)`, - ] - : []), - ...(cache !== null - ? [ - ` %cache_owned = call ptr @scr_promise_retain_v(ptr %p)`, - // The eager spawn may have re-entered this guarded module - // through an admitted async cycle and installed a temporary - // cache entry. Drop that owned slot before replacing it - // with the outer evaluation promise. - ` %replaced_cache = load ptr, ptr @${cache}`, - ` call void @scr_promise_release(ptr %replaced_cache)`, - ` store ptr %cache_owned, ptr @${cache}`, - ] - : []), - ...(cycleCache !== null - ? [ - // Eager recursive spawns publish from the inside out. The - // runtime-requested outermost member writes last and is the - // SCC's actual evaluation root. - ` %cycle_cache_owned = call ptr @scr_promise_retain_v(ptr %p)`, - ` %replaced_cycle_cache = load ptr, ptr @${cycleCache}`, - ` call void @scr_promise_release(ptr %replaced_cycle_cache)`, - ` store ptr %cycle_cache_owned, ptr @${cycleCache}`, - ] - : []), - ` ret ptr %p`, - `}`, - ``, - ); - for (const line of sp) out.push(line); - } - for (const line of this.emitGenScaffolding()) out.push(line); - return out; + emitAsyncScaffolding(): string[] { + return emitAsyncScaffolding(this); } /** Whether `fn`'s body is emitted as an LLVM switched coroutine: every * wasm32 async or generator body, native synchronous generators, and the * native async bodies selectStacklessAsync chose. */ - private coroLowered(fn: IrFunction): boolean { + coroLowered(fn: IrFunction): boolean { if (this.wasi) return fn.async === true || fn.generator !== undefined; if (fn.async === true) return fn.generator === undefined && this.stacklessAsync.has(fn.name); return this.stacklessGenerators && fn.generator !== undefined; } - - /** Per-generator-function machinery — the async scaffolding's lazy - * sibling (async.ts's emitGenScaffolding): the same argument pack, - * a fiber trampoline whose epilogue stores the COMPLETION value (or - * consumes the GENRET sentinel, promoting the parked .return value), a - * spawn wrapper that only ALLOCATES the suspended fiber, and the - * never-started teardown that drops the packed (+1) arguments. */ - private emitGenScaffolding(): string[] { - const out: string[] = []; - for (const fn of this.mod.functions) { - if (fn.generator === undefined) continue; - const { definitions, pack, lifted, fieldTys, ret, tr, spawnParams, argPackLines } = - this.emitArgPackAndTrampolinePrologue(fn); - for (const line of definitions) out.push(line); - this.declare(`declare zeroext i1 @scr_exc_genret_pending()`); - this.declare(`declare void @scr_exc_clear()`); - this.declare(`declare ptr @scr_gen_of_fiber(ptr)`); - this.declare(`declare void @scr_gen_ret_to_out(ptr)`); - this.declare(`declare ptr @scr_gen_new(ptr, ptr, ptr)`); - this.needOom(); - const coroBody = this.coroLowered(fn); - if (lifted && !coroBody) { - this.declare(`declare void @scr_closure_release(ptr)`); - } - // Normal completion stores the (typed) return value; void completes - // with the NONE slot — JS's undefined done-value. A GENRET unwind - // consumes the sentinel and promotes the parked .return value; a - // real exception stays pending (the consumer-side resume moves it). - if (coroBody) { - tr.push(` ret void`, `}`, ``); - } else { - tr.push( - ` %g = call ptr @scr_gen_of_fiber(ptr %self)`, - ...this.pendingTestLines("%pend"), - ` br i1 %pend, label %thrown, label %clean`, - `clean:`, - ); - switch (ret.kind) { - case "void": - tr.push(` br label %done ; void body: the done value is undefined (NONE)`); - break; - case "f64": - case "date": - this.declare(`declare void @scr_gen_out_f64(ptr, double)`); - tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`); - break; - case "bool": - this.declare(`declare void @scr_gen_out_bool(ptr, i1 zeroext)`); - tr.push(` call void @scr_gen_out_bool(ptr %g, i1 %r)`, ` br label %done`); - break; - default: { - const v = vAdapters(this.shapeHost, ret); - this.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`); - tr.push( - ` call void @scr_gen_out_ref(ptr %g, ptr %r, ptr ${v.release})`, - ` br label %done`, - ); - } - } - tr.push( - `thrown:`, - ` %genret = call zeroext i1 @scr_exc_genret_pending()`, - ` br i1 %genret, label %promote, label %dropdummy`, - `promote:`, - ` call void @scr_exc_clear()`, - ` call void @scr_gen_ret_to_out(ptr %g)`, - ` br label %dropdummy`, - `dropdummy:`, - ); - if (ret.kind !== "void" && isRefCounted(ret)) { - tr.push( - ` call void ${releaseSym(this.shapeHost, ret)}(ptr %r) ; unwound: the never-read dummy`, - ); - } - tr.push(` br label %done`, `done:`, ` ret void`, `}`, ``); - } - for (const line of tr) out.push(line); - - let settleAsync: string | null = null; - if (fn.async) { - this.declare(`declare ptr @scr_async_gen_new(ptr, ptr, ptr, ptr)`); - const genT: IrType & { kind: "generator" } = { - kind: "generator", - async: true, - yieldT: fn.generator.yieldT, - retT: ret, - nextT: fn.generator.nextT, - }; - const resultT = fn.generator.resultType; - const build = this.genResultThunkFor(genT, resultT); - settleAsync = `${build}_async`; - const settleKey = `ags:${typeKey(genT)}`; - if (!this.resolveThunks.has(settleKey)) { - this.resolveThunks.set(settleKey, settleAsync); - const adapters = vAdapters(this.shapeHost, resultT); - this.resolveThunkDefs.push( - `define internal void @${settleAsync}(ptr %g, ptr %p) ${FN_ATTRS} {`, - `entry:`, - ` %r = call ptr @${build}(ptr %g)`, - ` call void @scr_promise_fulfill_ref(ptr %p, ptr %r, ptr ${adapters.retain}, ptr ${adapters.release}, ptr ${traceArg(this.shapeHost, resultT)})`, - ` ret void`, - `}`, - ``, - ); - } - this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); - } - - // The never-started teardown: drop the packed (+1) arguments. - const dr: string[] = [ - `define internal void @${mangleGenDrop(fn.name)}(ptr %ap) ${FN_ATTRS} {`, - `entry:`, - ]; - fieldTys.forEach((ty, i) => { - const pType = lifted && i === 0 ? null : fn.params[lifted ? i - 1 : i]!.type; - const refcounted = pType === null || isRefCounted(pType); - if (!refcounted) return; - dr.push( - ` %dp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`, - ` %dv${i} = load ptr, ptr %dp${i}`, - ); - if (pType === null) { - this.declare(`declare void @scr_closure_release(ptr)`); - dr.push(` call void @scr_closure_release(ptr %dv${i})`); - } else { - dr.push(` call void ${releaseSym(this.shapeHost, pType)}(ptr %dv${i})`); - } - }); - dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``); - for (const line of dr) out.push(line); - - // An iterator helper's return() before its first next() closes the - // source generator in its first argument slot. - const closesSource = fn.generator.closesSource === true; - if (fn.generator.tag !== undefined) this.declare(`declare void @scr_gen_set_tag(ptr, ptr)`); - if (fn.generator.inheritsTag === true) - this.declare(`declare void @scr_gen_copy_tag(ptr, ptr)`); - if (closesSource) { - const field = lifted ? 1 : 0; - this.declare(`declare void @scr_gen_close(ptr)`); - this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`); - out.push( - `define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`, - `entry:`, - ` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`, - ` %sv = load ptr, ptr %sp`, - ` call void @scr_gen_close(ptr %sv)`, - ` ret void`, - `}`, - ``, - ); - } - - // Spawn wrapper: pack the args (+1 moves in), allocate the - // SUSPENDED fiber — nothing runs until the first .next(). - if (lifted) { - this.declare(`declare ptr @scr_closure_retain_v(ptr)`); - } - const sp: string[] = [ - `define internal ptr @${mangleGenSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; gen spawn ${fn.name}`, - `entry:`, - ...argPackLines, - ]; - const genNew = coroBody && !this.wasi ? "scr_gen_new_stackless" : "scr_gen_new"; - if (genNew !== "scr_gen_new") this.declare(`declare ptr @${genNew}(ptr, ptr, ptr)`); - sp.push( - settleAsync === null - ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` - : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, - ...(fn.generator.tag !== undefined - ? [` call void @scr_gen_set_tag(ptr %gg, ptr ${this.cstr(fn.generator.tag)})`] - : []), - ...(fn.generator.inheritsTag === true - ? [` call void @scr_gen_copy_tag(ptr %gg, ptr %a${lifted ? 1 : 0})`] - : []), - ...(closesSource - ? [ - ` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`, - ] - : []), - ` ret ptr %gg`, - `}`, - ``, - ); - for (const line of sp) out.push(line); - } - return out; + emitGenScaffolding(): string[] { + return emitGenScaffolding(this); } // ── plumbing (frame and scope ownership) ────── @@ -5107,7 +2255,7 @@ export class LlEmitter { } } - private releaseScope(scope: LlScopeEntry[]): void { + releaseScope(scope: LlScopeEntry[]): void { for (const e of scope) { const t = this.B.tmp(); this.B.line(`${t} = load ptr, ptr ${e.slot}`); @@ -5136,7 +2284,7 @@ export class LlEmitter { * frames and entered scopes down to the given depths, innermost first — * everything whose normal fall-through releases the jump bypasses. * Release every scope crossed by the control-flow edge. */ - private releaseForJump(frameDepth: number, scopeDepth: number): void { + releaseForJump(frameDepth: number, scopeDepth: number): void { for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!); for (let i = this.scopes.length - 1; i >= scopeDepth; i--) this.releaseScope(this.scopes[i]!); } @@ -5144,7 +2292,7 @@ export class LlEmitter { /** Run the finally regions an abrupt loop/block jump crosses, then * release to the already-resolved target. This is the return path's * completion walk without a parked result value. */ - private emitFinallysForJump(finallyDepth: number, frameDepth: number, scopeDepth: number): void { + emitFinallysForJump(finallyDepth: number, frameDepth: number, scopeDepth: number): void { if (this.finallyStack.length <= finallyDepth) { this.releaseForJump(frameDepth, scopeDepth); return; @@ -5178,7 +2326,7 @@ export class LlEmitter { * result of the function's own return type: with nothing to release, the * unwind returns it as the dummy, so a call in tail position keeps a single * return and stays eligible for tail-call and recursion elimination. */ - private emitUnwind(passThrough?: string): void { + emitUnwind(passThrough?: string): void { if (this.mod.workers && this.tryStack.length > 0) { const stop = this.B.newLabel("context.stop"); const ordinary = this.B.newLabel("context.throw"); @@ -5414,7 +2562,7 @@ export class LlEmitter { /** A recursive function's entry check: below the running stack's guard, * throw Node's RangeError and unwind like any other pending exception. */ - private emitStackCheck(): void { + emitStackCheck(): void { const B = this.B; this.declare(`declare ptr @llvm.stacksave.p0()`); this.declare(`declare i1 @llvm.expect.i1(i1, i1)`); @@ -5501,7 +2649,7 @@ export class LlEmitter { } } - private workerLoopBudget( + workerLoopBudget( loop: Extract, ): string | null { if (!this.mod.workers) return null; @@ -5513,128 +2661,16 @@ export class LlEmitter { this.B.line(`store i32 0, ptr ${slot}`); return slot; } - - /** Emit a loop's condition, body and update through `emitCopy`, twice - * when speculative-integers.ts plans a fast integer copy. Both copies - * share the loop's setup and exit; the ordinary copy comes first so the - * fast copy is only emitted when every resume point exists. */ - private emitLoopCopies( + emitLoopCopies( loop: Extract, allowed: boolean, exit: string, emitCopy: (exit: string) => void, ): void { - const plan = - allowed && - this.speculativeLoopsEnabled && - this.speculation === null && - this.speculationResume === null - ? planSpeculativeLoop( - loop, - (id) => this.speculativeLocal(id), - (id) => this.integerLoopBindings.has(id) || this.int32Locals.has(id), - (e) => { - const range = this.integerRanges.get(e); - return ( - range !== undefined && - range !== null && - range.min >= -Number.MAX_SAFE_INTEGER && - range.max <= Number.MAX_SAFE_INTEGER - ); - }, - (id) => this.speculativeLocal(id) || this.speculativeInvariantGlobal(id), - ) - : null; - if (!plan) { - emitCopy(exit); - return; - } - const B = this.B; - const entries = new Map(); - let ok = "true"; - for (const [id, live] of plan.locals) { - if (!live) continue; - const value = B.tmp(); - B.line(`${value} = load double, ptr ${this.binding(id).slot}`); - const [integer, exact] = emitSafeIntegerCheck(this, value); - entries.set(id, integer); - if (ok === "true") ok = exact; - else { - const both = B.tmp(); - B.line(`${both} = and i1 ${ok}, ${exact}`); - ok = both; - } - } - const fast = B.newLabel("spec.fast"), - slow = B.newLabel("spec.slow"); - B.condBr(ok, fast, slow); - const resume = new Map(); - for (const s of plan.writes) resume.set(s, { label: B.newLabel("spec.resume"), placed: false }); - B.startBlock(slow); - this.speculationResume = resume; - try { - emitCopy(exit); - } finally { - this.speculationResume = null; - } - B.startBlock(fast); - if (![...resume.values()].every((r) => r.placed)) { - B.br(slow); - return; - } - for (const id of plan.locals.keys()) { - const shadow = B.slot(); - B.entryAllocas.push( - `${shadow} = alloca i64 ; speculative integer ${this.currentLocals.get(id)?.name ?? id}`, - ); - const entry = entries.get(id); - if (entry !== undefined) { - // The same double, now visibly an integer conversion: the double - // phis of the fast copy fold into integer phis. - const exact = B.tmp(); - B.line(`store i64 ${entry}, ptr ${shadow}`); - B.line(`${exact} = sitofp i64 ${entry} to double`); - B.line(`store double ${exact}, ptr ${this.binding(id).slot}`); - } else B.line(`store i64 0, ptr ${shadow}`); - this.integerLoopBindings.set(id, { - slot: shadow, - type: "i64", - signed: true, - range: SPECULATIVE_RANGE, - }); - } - // A global's store stays in the loop (calls may read it). When nothing - // in the loop can observe it, its double is written once on leaving the - // copy instead, so the loop keeps it in a register as the double copy - // would. - const deferred = new Set(); - // Worker loops also leave through their cancellation poll. - if (plan.observerFree && !this.mod.workers) - for (const s of plan.writes) { - const id = speculativeWriteTarget(s)!.localId; - if (!this.currentLocals.has(id)) deferred.add(id); - } - this.speculation = { - writes: plan.writes, - resume: new Map([...resume].map(([s, r]) => [s, r.label])), - deferred, - }; - try { - if (deferred.size === 0) emitCopy(exit); - else { - const leave = B.newLabel("spec.leave"); - emitCopy(leave); - B.startBlock(leave); - this.writeBackSpeculativeGlobals(deferred); - B.br(exit); - } - } finally { - this.speculation = null; - for (const id of plan.locals.keys()) this.integerLoopBindings.delete(id); - } + return emitLoopCopies(this, loop, allowed, exit, emitCopy); } - private writeBackSpeculativeGlobals(deferred: Set): void { + writeBackSpeculativeGlobals(deferred: Set): void { const B = this.B; for (const id of deferred) { const integer = B.tmp(), @@ -5647,7 +2683,7 @@ export class LlEmitter { /** A number local whose only storage is its own double slot, so an i64 * shadow beside it can stand for it within one loop. */ - private speculativeLocal(id: string): boolean { + speculativeLocal(id: string): boolean { const local = this.currentLocals.get(id); if (!local) return this.speculativeGlobal(id); if (local.type.kind !== "f64" || local.boxed || local.tdz) return false; @@ -5679,7 +2715,7 @@ export class LlEmitter { /** A module-level number a loop of any function only reads, written by * nothing but top-level code: an init function never runs during a loop * of another function, so the entry value stays its value. */ - private speculativeInvariantGlobal(id: string): boolean { + speculativeInvariantGlobal(id: string): boolean { const writers = this.numberGlobalWriters(id); return writers !== null && [...writers].every((name) => name.startsWith("%init.")); } @@ -5713,65 +2749,8 @@ export class LlEmitter { } return this.globalWriters.get(id) ?? new Set(); } - - /** A speculated write in the fast copy: compute the value as a safe - * integer, store it to the shadow and its exact double to the local. - * Any failed check re-executes the statement in the ordinary copy. */ private emitSpeculativeWrite(s: IrStmt, resume: string): void { - const B = this.B; - const target = speculativeWriteTarget(s)!; - const binding = this.integerLoopBindings.get(target.localId)!; - const failed = B.newLabel("spec.bail"); - let bails = false; - this.declare("declare i1 @llvm.expect.i1(i1, i1)"); - const bail = (condition: string): void => { - if (condition === "false") return; - bails = true; - const expected = B.tmp(); - B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${condition}, i1 false)`); - const next = B.newLabel("spec.ok"); - B.condBr(expected, failed, next); - B.startBlock(next); - }; - // Heap reads come first, before any check, and release their temporaries - // at once: a bailing path then owns nothing, and reads have no effect - // whose reordering before the arithmetic could be observed. - const reads = new Map(); - if (target.value !== null) { - this.frames.push([]); - for (const read of speculativeReads(target.value)) reads.set(read, this.emitExpr(read)); - this.releaseTemps(this.frames.pop()!); - } - this.frames.push([]); - let result: string; - if (target.value === null) { - const old = B.tmp(); - result = B.tmp(); - B.line(`${old} = load i64, ptr ${binding.slot}`); - B.line(`${result} = ${target.op === "+" ? "add" : "sub"} nsw i64 ${old}, 1`); - const biased = B.tmp(), - outside = B.tmp(); - B.line(`${biased} = add i64 ${result}, ${Number.MAX_SAFE_INTEGER}`); - B.line(`${outside} = icmp ugt i64 ${biased}, ${2 * Number.MAX_SAFE_INTEGER}`); - bail(outside); - } else result = emitSpeculativeInteger(this, target.value, bail, reads); - // Speculable values read only numbers past this point. - if (this.frames.pop()!.length > 0) - throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary"); - B.line(`store i64 ${result}, ptr ${binding.slot}`); - const deferred = this.speculation!.deferred; - if (!deferred.has(target.localId)) { - const number = B.tmp(); - B.line(`${number} = sitofp i64 ${result} to double`); - B.line(`store double ${number}, ptr ${this.binding(target.localId).slot}`); - } - if (!bails) return; - const done = B.newLabel("spec.done"); - B.br(done); - B.startBlock(failed); - this.writeBackSpeculativeGlobals(deferred); - B.br(resume); - B.startBlock(done); + return emitSpeculativeWrite(this, s, resume); } /** The amortized cancellation poll, emitted on the loop's back edge (the @@ -5779,7 +2758,7 @@ export class LlEmitter { * exit ahead of the body; LLVM then cannot rotate the loop, so loads that * the body repeats on every iteration (a typed array's length, for one) * stay in the loop. */ - private emitWorkerLoopCheck(slot: string | null): void { + emitWorkerLoopCheck(slot: string | null): void { if (slot === null) return; const B = this.B; const budget = B.tmp(), @@ -6385,6 +3364,34 @@ export class LlEmitter { /** A binding's storage: a module global, a plain local slot, or a boxed * local (the slot holds the capture BOX; access goes through it). */ + matchBytesLoopVersion( + s: IrStmt & { kind: "for" }, + counted: IntegerCountedForLoop | null, + ): ReturnType { + const fn = this.bytesLoopFunction; + if (!fn) return null; + this.bytesLoopEntries ??= findInitializedByteLoopBindings(fn); + const initialized = this.bytesLoopEntries.get(s); + if (!initialized) return null; + const invariantGlobals = new Set(this.stableGlobals); + for (const global of this.initializerBindings.get(fn.name) ?? []) + invariantGlobals.add(global.id); + return matchBytesLoopVersion(s, counted, { + locals: this.currentLocals, + invariantGlobals, + captured: this.captureIds, + ranges: this.integerRanges, + initialized, + proven: this.bytesBounds, + }); + } + + /** The alias tag of a whole-slot load of a binding (module globals only). */ + slotAlias(id: string): string { + const binding = this.binding(id); + return binding.kind === "global" ? globalSlotAlias(this, id, binding.type.kind) : ""; + } + binding(id: string): { kind: "global" | "local" | "boxed"; slot: string; @@ -6409,7 +3416,7 @@ export class LlEmitter { }; } - private emitDebugLocal(local: IrLocal, slot: string, arg = 0, captured = false): void { + emitDebugLocal(local: IrLocal, slot: string, arg = 0, captured = false): void { const debug = this.debug?.local(local, this.debugScope, arg, captured); if (!debug) return; this.declare("declare void @llvm.dbg.declare(metadata, metadata, metadata)"); @@ -6426,7 +3433,7 @@ export class LlEmitter { } /** The payload slot of a capture box (ScrBox.slot, field 5). */ - private boxSlot(box: string): string { + boxSlot(box: string): string { const slot = this.B.tmp(); this.B.line(`${slot} = getelementptr inbounds %ScrBox, ptr ${box}, i32 0, i32 5`); return slot; @@ -6505,7 +3512,7 @@ export class LlEmitter { /** The initializing write of a scalar TDZ box: mint the one-element * array cell holding the value and move it into the ARR-kind box * (set_ref releases nothing — the slot was the empty sentinel). */ - private tdzScalarInit(box: string, t: IrType, value: string): void { + tdzScalarInit(box: string, t: IrType, value: string): void { const B = this.B; const acc = boxAccess(t); const cell = B.tmp(); @@ -6521,7 +3528,7 @@ export class LlEmitter { * read back as the temporal dead zone; such boxes start at the module's * immortal NULLABLE_ABSENT sentinel instead, which no value of the * binding can hold and every RC entry point and the collector skip. */ - private tdzEmptySentinel(t: IrType): string | null { + tdzEmptySentinel(t: IrType): string | null { if (!this.nullableUnions.of(t)) return null; this.needsNullableAbsent = true; this.immortalValues.add(NULLABLE_ABSENT); @@ -6805,75 +3812,13 @@ export class LlEmitter { ); this.B.startBlock(resume); } - - /** Move a clean async return value into the current fiber's promise. */ - private emitWasiFulfill(v: LlValue | null): void { - const coro = this.currentCoro; - if (coro === null) - throw new InternalCompilerError("llvm emitter bug: wasm fulfillment outside async body"); - const ret = this.currentReturnType; - if (coro.kind === "generator") { - this.declare(`declare ptr @scr_gen_of_fiber(ptr)`); - const gen = this.B.tmp(); - this.B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${coro.self})`); - switch (ret.kind) { - case "void": - return; - case "f64": - case "date": - this.declare(`declare void @scr_gen_out_f64(ptr, double)`); - this.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`); - return; - case "bool": - this.declare(`declare void @scr_gen_out_bool(ptr, i1 zeroext)`); - this.B.line(`call void @scr_gen_out_bool(ptr ${gen}, i1 ${v!.name})`); - return; - default: - this.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`); - this.B.line( - `call void @scr_gen_out_ref(ptr ${gen}, ptr ${v!.name}, ptr ${vAdapters(this.shapeHost, ret).release})`, - ); - return; - } - } - this.declare(`declare ptr @scr_fiber_promise(ptr)`); - const pr = this.B.tmp(); - this.B.line(`${pr} = call ptr @scr_fiber_promise(ptr ${coro.self})`); - switch (ret.kind) { - case "void": - this.declare(`declare void @scr_promise_fulfill_void(ptr)`); - this.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`); - break; - case "f64": - case "date": - this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); - this.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`); - break; - case "bool": - this.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`); - this.B.line(`call void @scr_promise_fulfill_bool(ptr ${pr}, i1 ${v!.name})`); - break; - case "string": - this.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`); - this.B.line(`call void @scr_promise_fulfill_str(ptr ${pr}, ptr ${v!.name}) ; moves in`); - break; - case "dyn": - this.declare(`declare void @scr_promise_resolve_dyn(ptr, ptr)`); - this.B.line(`call void @scr_promise_resolve_dyn(ptr ${pr}, ptr ${v!.name}) ; moves in`); - break; - default: { - const rc = vAdapters(this.shapeHost, ret); - this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); - this.B.line( - `call void @scr_promise_fulfill_ref(ptr ${pr}, ptr ${v!.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this.shapeHost, ret)})`, - ); - } - } + emitWasiFulfill(v: LlValue | null): void { + return emitWasiFulfill(this, v); } /** Number locals declared as a copy of a capture or of a lazily boxed * parameter, at the top of the body. */ - private doubleCopies(fn: IrFunction): string[] { + doubleCopies(fn: IrFunction): string[] { const copies: string[] = []; for (const s of fn.body) { if (s.kind !== "varDecl") break; @@ -6883,596 +3828,14 @@ export class LlEmitter { } return copies; } - - private emitFunction(fn: IrFunction): string { - const B = new BlockBuilder(); - this.B = B; - this.currentFunctionName = fn.name; - // Fiber switches restore the active exception cell before control - // returns to a synchronous frame (emitPendingCheck). - B.excCellInvariant = fn.async !== true && fn.generator === undefined; - this.debugScope = this.debug?.function(fn) ?? null; - B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null; - this.frames = []; - this.scopes = []; - this.unwindCleanups.clear(); - this.jumpTargets = []; - this.currentLocals = new Map(fn.locals.map((l) => [l.id, l])); - this.lazyCaptureBoxes.clear(); - this.lazyStackBoxes.clear(); - // An unchanged captured parameter keeps its value in a plain slot; the - // binding is boxed only for closures (captureBox). - const lazyParameters = this.lazyCaptures.parameters(fn); - for (const id of lazyParameters) { - const { boxed: _boxed, ...plain } = this.currentLocals.get(id)!; - this.currentLocals.set(id, plain); - this.lazyCaptureBoxes.set(id, ""); - } - this.currentConstantCallbacks = - this.debug === null ? (this.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); - // Preserve concrete source bindings for debugger inspection. Suspended - // functions keep the established array lifetime across continuations. - this.streamingSplitsEnabled = this.debug === null && !fn.async && !fn.generator; - this.stackCallbacks.reset(this.streamingSplitsEnabled); - this.stackCaptures.reset(fn, this.streamingSplitsEnabled, lazyParameters); - this.privateSplitLocals = this.streamingSplitsEnabled ? findPrivateSplitLocals(fn) : new Map(); - this.storedSplits.clear(); - this.splitSpans.clear(); - this.stringSlices.clear(); - const initializerBindings = this.initializerBindings.get(fn.name) ?? []; - const numericFn = withInitializerBindings(fn, initializerBindings); - this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l])); - this.unaliasedArrays = findUnaliasedLocalArrays(fn); - this.borrowedParameters.clear(); - this.referenceWrites?.enter(fn); - this.reboundOwnerSlots.clear(); - this.projectedParameters.clear(); - const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name); - if (projectedParameterIndexes) - for (const index of projectedParameterIndexes) - this.projectedParameters.add(fn.params[index]!.localId); - this.stableCallBindings = this.callLifetimes.bindings.get(fn.name) ?? new Set(); - const borrowedParameterIndexes = this.callLifetimes.borrowed.get(fn.name); - if (borrowedParameterIndexes) { - // Rebound walk parameters borrow too, but only unchanged ones may - // serve as stable owners for call arguments and aliases. - for (const index of borrowedParameterIndexes) - if ( - !this.walkBorrowsByFunction.get(fn.name)?.has(fn.params[index]!.localId) && - !this.reboundByFunction.get(fn.name)?.has(fn.params[index]!.localId) - ) - this.borrowedParameters.add(fn.params[index]!.localId); - } - // A generator frame owns its unchanged parameters across suspensions. - const generatorParameters = this.callLifetimes.generatorParameters.get(fn.name); - if (generatorParameters !== undefined) - for (const id of generatorParameters) this.borrowedParameters.add(id); - this.captureIds = new Set( - [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), - ); - this.stableCaptureIds = - fn.captures !== undefined ? this.lazyCaptures.stableCaptures(fn.name) : new Set(); - this.integerLoopBindings.clear(); - this.countedLoopsEnabled = this.debug === null && !fn.async && !fn.generator; - this.speculativeLoopsEnabled = this.countedLoopsEnabled; - this.speculativeFunction = fn.name; - this.speculation = null; - this.speculationResume = null; - this.finalFieldBorrows = - !fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0; - this.byteWindowsEnabled = this.countedLoopsEnabled; - this.denseLoopsEnabled = this.countedLoopsEnabled; - this.denseLoop = null; - this.denseLoopEntries = - this.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") - ? findInitializedByteLoopBindings(fn) - : new Map(); - this.byteWindowEntries = - this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") - ? findInitializedByteLoopBindings(fn) - : new Map(); - this.integerViews.clear(); - this.fieldPointerTags.clear(); - this.integerArrayBindings.clear(); - this.loopArrayBorrows = - this.debug === null - ? findLoopArrayBorrows( - fn, - this.callLifetimes, - this.referenceEffects, - this.optionalArrayReads, - this.referenceWrites, - this.fnByName, - ) - : new Set(); - this.localArrayReads = findLocalArrayReads( - fn, - this.fnByName, - this.unionsById, - this.referenceEffects.functions, - this.callLifetimes, - this.optionalArrayReads, - this.loopArrayBorrows, - this.debug === null ? this.referenceEffects : undefined, - ); - this.callArrayReads = findCallArrayReads( - fn, - this.optionalArrayReads, - this.referenceEffects.functions, - this.callLifetimes, - this.debug === null ? this.referenceEffects : undefined, - ); - this.scalarStringSlices = - this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map(); - this.scratchStringSlices = - this.debug === null - ? findScratchStringSlices( - fn, - this.callLifetimes, - this.scalarStringSlices, - this.lentStringParameters, - ) - : new Set(); - this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes); - this.readNumberMapWrites = findReadNumberMapWrites(fn); - this.mapEntryFusion = this.debug === null ? findMapEntryFusions(fn) : null; - this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById); - this.localStackObjects = this.stackObjects - ? findLocalStackObjects(this, fn, this.callLifetimes) - : new Map(); - this.reassignedStackObjects = this.stackObjects - ? findReassignedStackObjects(this, fn, this.callLifetimes) - : new Set(); - this.stackObjectSlots = new Map(); - this.stackObjectFieldPtrs = new Map(); - this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources); - this.localUnionStorage = new Map(); - this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set(); - const slotFacts = this.int32Slots.facts(fn.name); - this.integerRanges = analyzeIntegerRanges(numericFn, slotFacts); - this.int32Elements = slotFacts.elements ?? null; - this.bytesBounds = findBytesBounds(numericFn, this.integerRanges); - // 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 - // such a parameter (copyStableCaptures): the source already holds the - // double, which an i32 copy would convert and widen again on every call. - // Initializer bindings proven int32 move into an i32 local of their - // initializer: no other code observes the global, so the loop-carried - // value never round-trips through the double slot. - const initializerLocals = initializerLocalCandidates(initializerBindings); - const int32Locals = - this.debug === null - ? findInt32Locals( - withInitializerLocals(fn, initializerLocals), - this.integerRanges, - this.int32Slots.facts(fn.name), - new Set([...this.lazyCaptureBoxes.keys(), ...this.doubleCopies(fn)]), - ) - : null; - this.int32Locals = int32Locals?.locals ?? new Map(); - this.int32LocalWrites = int32Locals?.values ?? new Set(); - const localizedGlobals = new Set(); - for (const global of initializerLocals) { - if (!this.int32Locals.has(global.id)) continue; - localizedGlobals.add(global.id); - this.currentLocals.set(global.id, { - id: global.id, - name: global.name, - type: global.type, - mutable: global.mutable, - }); - } - this.chainSlots.clear(); - this.finallyStack = []; - this.tryStack = []; - this.loopDepth = 0; - this.currentReturnType = fn.returnType; - const int32Abi = this.int32Abis.get(fn.name); - this.currentInt32Result = int32Abi?.result === true; - this.currentUnionResult = int32Abi?.union === true; - this.currentGenerator = fn.generator ?? null; - this.currentAsyncGenerator = fn.async === true && fn.generator !== undefined; - this.currentBorrowedReturn = this.borrowedReturns.has(fn.name); - this.lastUseMoves.clear(); - this.varRefCounts = null; - this.currentFunctionBody = fn.body; - this.inConstructor = this.constructorFunctions.has(fn.name); - this.currentCoro = null; - this.logArgSlots = 0; - - if (this.coroLowered(fn)) { - this.usesCoroutines = true; - this.declare(`declare token @llvm.coro.id(i32, ptr, ptr, ptr)`); - this.declare(`declare ${this.sizeType} @llvm.coro.size.${this.sizeType}()`); - this.declare(`declare ptr @llvm.coro.begin(token, ptr)`); - this.declare(`declare token @llvm.coro.save(ptr)`); - this.declare(`declare i8 @llvm.coro.suspend(token, i1)`); - this.declare(`declare ptr @llvm.coro.free(token, ptr)`); - this.declare(`declare void @llvm.coro.end(ptr, i1, token)`); - // Native frames come from the runtime's small-block allocator. - const frameAlloc = this.wasi ? "malloc" : "scr_rt_calloc"; - this.declare(`declare ptr @${frameAlloc}(${this.sizeType})`); - this.declare(`declare void @${this.wasi ? "free" : "scr_rt_free"}(ptr)`); - const started = this.wasi ? "scr_wasi_coro_started" : "scr_coro_started"; - this.declare(`declare void @${started}(ptr)`); - this.declare(`declare ptr @scr_fiber_self()`); - const id = B.tmp(); - const size = B.tmp(); - const mem = B.tmp(); - const handle = B.tmp(); - const self = B.tmp(); - B.line(`${id} = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)`); - B.line(`${size} = call ${this.sizeType} @llvm.coro.size.${this.sizeType}()`); - B.line(`${mem} = call ptr @${frameAlloc}(${this.sizeType} ${size})`); - B.line(`${handle} = call ptr @llvm.coro.begin(token ${id}, ptr ${mem})`); - B.line(`call void @${started}(ptr ${handle})`); - B.line(`${self} = call ptr @scr_fiber_self()`); - // A native synchronous body owns its generator for the whole run (the - // ScrGen owns the frame), so its OUT slot address is fixed. - let out: string | null = null; - const yieldKind = fn.generator?.yieldT.kind; - if ( - !this.wasi && - fn.async !== true && - (yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool") - ) { - this.declare(`declare ptr @scr_gen_of_fiber(ptr)`); - const gen = B.tmp(); - B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`); - out = this.genOutSlot(gen); - } - this.currentCoro = { - kind: fn.generator !== undefined ? "generator" : "async", - id, - handle, - self, - finalLabel: B.newLabel("coro.final"), - cleanupLabel: B.newLabel("coro.cleanup"), - suspendLabel: B.newLabel("coro.suspend"), - out, - }; - } - - const paramIds = new Set(fn.params.map((p) => p.localId)); - if (this.debug === null) { - for (const id of findIntegerViews(numericFn)) { - // An i32 slot is its own view. - if (this.int32Locals.has(id)) continue; - const slot = B.slot(); - B.entryAllocas.push( - `${slot} = alloca i32 ; integer view ${this.numericLocals.get(id)!.name}`, - ); - this.integerViews.set(id, slot); - } - } - for (const global of initializerBindings) { - if (!this.integerViews.has(global.id)) continue; - const value = B.tmp(); - B.line(`${value} = load double, ptr @${mangleGlobal(global.id)}`); - this.storeIntegerView(global.id, { name: value, type: global.type }); - } - // A number global only its guarded, synchronous initializer reads or - // writes (findInitializerBindings) is invisible to every other function, - // and reentry returns before reaching it, so a local slot can stand in - // for it: LLVM keeps it in a register across calls and loops that would - // otherwise force a store and reload per iteration. The global's - // storage is never read again, so nothing writes the slot back. Bindings - // proven int32 already live in an i32 local (localizedGlobals above); - // this covers the remaining doubles. - this.privateGlobalSlots.clear(); - if (this.debug === null && this.currentCoro === null && !fn.async && !fn.generator) { - const conditional = initializerLocals.length ? conditionalLoopWrites(fn.body) : null; - for (const global of initializerLocals) { - if (localizedGlobals.has(global.id)) continue; - // A conditional update in a loop stays a memory update (see - // conditionalLoopWrites). - if (conditional?.has(global.id)) continue; - const slot = B.slot(); - const value = B.tmp(); - B.entryAllocas.push(`${slot} = alloca double ; module binding ${global.name}`); - B.line(`${value} = load double, ptr @${mangleGlobal(global.id)}`); - B.line(`store double ${value}, ptr ${slot}`); - this.privateGlobalSlots.set(global.id, slot); - } - } - for (const local of this.currentLocals.values()) { - // Boxed locals' slots hold their capture BOX (a ptr); captured - // (env-borrowed) locals bind the incoming box below. A caught-typed - // local is a catch binding: its slot holds the ScrCaught snapshot - // box the catch prologue takes (scr_exc_take). - const slotTy = - local.boxed || this.captureIds.has(local.id) || local.type.kind === "caught" - ? "ptr" - : this.int32Locals.has(local.id) - ? "i32" - : this.llType(local.type); - B.entryAllocas.push(`%${mangleLocal(local.id)} = alloca ${slotTy} ; ${local.name}`); - this.emitDebugLocal( - local, - `%${mangleLocal(local.id)}`, - fn.params.findIndex((p) => p.localId === local.id) + 1, - this.captureIds.has(local.id), - ); - // A localized initializer binding starts at its global's zero value. - if (localizedGlobals.has(local.id)) B.line(`store i32 0, ptr %${mangleLocal(local.id)}`); - // Refcounted/boxed locals start NULL (the C prologue's `= NULL`): - // scope-exit releases run whether or not an assign ever did. - if (paramIds.has(local.id) || this.captureIds.has(local.id)) continue; - if (local.boxed || isRefCounted(local.type)) { - B.line(`store ptr null, ptr %${mangleLocal(local.id)}`); - } - } - // A body holding thousands of capture boxes compiles unoptimized, its - // box pointers kept in their slots (see boxSlotCount). - const unoptimized = this.currentCoro === null && boxSlotCount(fn) >= UNOPTIMIZED_BOX_SLOTS; - if (unoptimized) { - const slots = new Set(); - for (const local of this.currentLocals.values()) - if (local.boxed || this.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`); - B.volatileSlots = slots; - } - // Captured bindings come in through the environment — borrowed for the - // whole call (the closure owns them): bound here, never released here. - (fn.captures ?? []).forEach((c, i) => { - const caps = B.tmp(); - const p = B.tmp(); - const box = B.tmp(); - B.line(`${caps} = getelementptr inbounds %ScrClosure, ptr %sc_env, i64 1 ; caps`); - B.line(`${p} = getelementptr inbounds ptr, ptr ${caps}, ${this.sizeType} ${i} ; caps[${i}]`); - B.line(`${box} = load ptr, ptr ${p}`); - B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; captured ${c.name}`); - }); - (fn.classCaptures ?? []).forEach((c) => { - const self = fn.params[0]!; - if (self.type.kind !== "object") - throw new InternalCompilerError("class captures require an instance receiver"); - const classSlot = B.tmp(); - const classValue = B.tmp(); - const caps = B.tmp(); - const slot = B.tmp(); - const box = B.tmp(); - B.line( - `${classSlot} = getelementptr inbounds %${mangleClassStruct(self.type.className)}, ptr %p_${mangleLocal(self.localId)}, i64 0, i32 ${classEnvironmentIndex(this.classMeta.get(self.type.className)!)}`, - ); - B.line(`${classValue} = load ptr, ptr ${classSlot}`); - B.line(`${caps} = getelementptr inbounds %ScrClassObj, ptr ${classValue}, i64 1`); - B.line(`${slot} = getelementptr inbounds ptr, ptr ${caps}, ${this.sizeType} ${c.slot}`); - B.line(`${box} = load ptr, ptr ${slot}`); - B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; class capture ${c.name}`); - }); - // Params spill into their slots; the function scope owns refcounted - // params (callees own their params — callers passed +1). Boxed params - // allocate the shared binding and move the raw value in. - const fnScope: LlScopeEntry[] = []; - for (const id of this.lazyCaptureBoxes.keys()) { - const cache = B.slot(); - B.entryAllocas.push(`${cache} = alloca ptr ; lazy box ${this.currentLocals.get(id)!.name}`); - B.line(`store ptr null, ptr ${cache}`); - fnScope.push({ - slot: cache, - type: this.currentLocals.get(id)!.type, - boxed: true, - lazy: true, - }); - this.lazyCaptureBoxes.set(id, cache); - // Environments that finish during the call can share an immortal - // frame box. Its payload owner is released with the function scope. - const stack = this.stackCaptures.emit(this, id, this.currentLocals.get(id)!.type); - if (stack) { - this.lazyStackBoxes.set(id, stack.box); - if (stack.owner) fnScope.push(stack.owner); - } - } - const borrowed = this.callLifetimes.borrowed.get(fn.name); - for (const [index, p] of fn.params.entries()) { - const local = this.currentLocals.get(p.localId)!; - const slot = `%${mangleLocal(p.localId)}`; - if (local.boxed) { - const box = B.tmp(); - B.line(`${box} = ${boxNewCall(this.shapeHost, p.type)} ; ${p.name} (boxed param)`); - this.boxSet(box, p.type, `%p_${mangleLocal(p.localId)}`); - B.line(`store ptr ${box}, ptr ${slot}`); - fnScope.push({ slot, type: p.type, boxed: true }); - continue; - } - const incoming: LlValue = { name: `%p_${mangleLocal(p.localId)}`, type: p.type }; - if (int32Abi?.params[index]) { - const bits = `%p_${mangleLocal(p.localId)}${INT32_ABI_SUFFIX}`; - incoming.uint32 = bits; - incoming.integer = { name: bits, type: "i32", signed: true, range: INT32_RANGE }; - } - if (this.storeInt32Local(p.localId, incoming)) continue; - B.line(`store ${this.llType(p.type)} %p_${mangleLocal(p.localId)}, ptr ${slot}`); - this.storeIntegerView(p.localId, incoming); - if (isRefCounted(p.type) && !borrowed?.has(index)) { - fnScope.push({ slot, type: p.type }); - this.noteParameterMove(fn, p.localId, slot); - } - if (this.reboundByFunction.get(fn.name)?.has(p.localId) && borrowed?.has(index)) { - // The caller's borrow stays in the parameter slot; values the body - // assigns are owned here until rebinding or exit releases them. - const owner = `${slot}.owner`; - B.entryAllocas.push(`${owner} = alloca ptr`); - B.line(`store ptr null, ptr ${owner}`); - fnScope.push({ slot: owner, type: p.type }); - this.reboundOwnerSlots.set(p.localId, owner); - } - } - this.scopes.push(fnScope); - // Stackful native fibers and native coroutine frames retain these frames - // while suspended. The active exception context owns the chain, so - // concurrent fibers isolate it. - if ( - this.stackTraces && - !this.wasi && - fn.sourceName !== undefined && - !this.unframedFunctions.has(fn.name) - ) { - B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`); - const frame = this.cstr(` at ${fn.sourceName} (${fn.loc.file})`); - if ( - B.excCellInvariant && - this.mod.lib === undefined && - this.mod.workers !== true && - this.sizeType === "i64" - ) { - // Executables link the active-cell pointer as a plain global, and a - // synchronous body sees one cell from entry to every return: push - // and pop the frame inline (ScrExcCell.stack is at byte 56 on - // 64-bit targets) instead of calling the runtime twice per call. - this.declare(`@scr_exc_active = external global ptr`); - B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`); - B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`); - B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`); - B.line(`store ptr %source_frame.previous, ptr %source_frame`); - B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`); - B.line(`store ptr ${frame}, ptr %source_frame.text`); - B.line(`store ptr %source_frame, ptr %source_frame.chain`); - B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`; - } else { - this.declare(`declare void @scr_stack_enter(ptr, ptr)`); - this.declare(`declare void @scr_stack_leave(ptr)`); - B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`); - B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`; - } - } - if (this.stackChecks?.has(fn.name)) this.emitStackCheck(); - if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true); - this.emitStmts(fn.body, true); - // Implicit exit of a void function: release the function scope unless - // the body already terminated its final block (return, or a throw - // whose unwind released everything down to depth 0). - if (fn.returnType.kind === "void" && !B.isTerminated()) { - this.releaseScope(this.scopes[0]!); - // Completing by falling off the end has nothing to await. - if (this.currentAsyncGenerator) this.emitAsyncGenSettled(); - if (this.currentCoro !== null) { - this.emitWasiFulfill(null); - B.terminate(`br label %${this.currentCoro.finalLabel}`); - } else { - B.terminate("ret void"); - } - } - this.scopes.pop(); - - const coro = this.currentCoro; - if (coro !== null) { - B.startBlock(coro.finalLabel); - // Every exit of a coroutine body joins here: its source frame (part - // of the coroutine frame) leaves the generator's own chain. - const epilogue = B.returnEpilogue; - B.returnEpilogue = null; - if (epilogue !== null) B.line(epilogue); - if (fn.captures !== undefined) { - this.declare(`declare void @scr_closure_release(ptr)`); - B.line(`call void @scr_closure_release(ptr %sc_env)`); - } - if (coro.kind === "generator") { - const finish = this.wasi ? "scr_wasi_gen_finish" : "scr_gen_coro_finish"; - this.declare(`declare void @${finish}(ptr)`); - B.line(`call void @${finish}(ptr ${coro.self})`); - } else { - const finish = this.wasi ? "scr_wasi_async_finish" : "scr_coro_async_finish"; - this.declare(`declare void @${finish}(ptr)`); - B.line(`call void @${finish}(ptr ${coro.self})`); - } - const finalState = B.tmp(); - B.line(`${finalState} = call i8 @llvm.coro.suspend(token none, i1 true)`); - B.terminate( - `switch i8 ${finalState}, label %${coro.suspendLabel} [ i8 1, label %${coro.cleanupLabel} ]`, - ); - B.startBlock(coro.cleanupLabel); - const frame = B.tmp(); - B.line(`${frame} = call ptr @llvm.coro.free(token ${coro.id}, ptr ${coro.handle})`); - B.line(`call void @${this.wasi ? "free" : "scr_rt_free"}(ptr ${frame})`); - B.br(coro.suspendLabel); - B.startBlock(coro.suspendLabel); - B.line(`call void @llvm.coro.end(ptr ${coro.handle}, i1 false, token none)`); - const ret = this.llType(fn.returnType); - if (ret === "void") B.terminate(`ret void`); - else if (ret === "double") B.terminate(`ret double ${f64Lit(0)}`); - else if (ret === "i1") B.terminate(`ret i1 false`); - else B.terminate(`ret ptr null`); - } - - // Shared blocks have multiple source locations. Keep the location on - // each incoming exception check instead of assigning one to the cleanup. - B.debugLocation = null; - for (const cleanup of this.unwindCleanups.values()) { - B.startBlock(cleanup.label); - this.releaseScope(cleanup.entries); - B.terminate(cleanup.terminator); - } - - if (this.logArgSlots > 0) { - B.entryAllocas.push(`%logargs = alloca [${this.logArgSlots} x %ScrLogArg]`); - } - const params = fn.params.map((p, index) => - int32Abi?.params[index] - ? `i32 %p_${mangleLocal(p.localId)}${INT32_ABI_SUFFIX}` - : `${this.llType(p.type)} %p_${mangleLocal(p.localId)}`, - ); - // The i32 entry widens its i32 parameters once; the proof makes the - // double exact, so readers of the incoming number see the same value. - if (int32Abi) - fn.params.forEach((p, index) => { - if (!int32Abi.params[index]) return; - const name = `%p_${mangleLocal(p.localId)}`; - B.entryAllocas.push(`${name} = sitofp i32 ${name}${INT32_ABI_SUFFIX} to double`); - }); - // Lifted functions receive their closure first (the callValue ABI). - if (fn.captures !== undefined) params.unshift("ptr %sc_env"); - const ret = int32Abi?.result - ? "i32" - : int32Abi?.union - ? SCALAR_UNION_RESULT - : this.llType(fn.returnType); - let attrs = coro !== null ? "#1" : FN_ATTRS; - // The shared indexed-read helper (lowerSafeIndexRead) is a dense bounds - // test, a state test and a load behind cold runtime fallbacks, called - // from every `a[i]` of one element type. Without the hint LLVM keeps the - // hot instances out of line once they have many callers: every read pays - // a call, and an integer index converts to a double and back across it. - if (fn.name.startsWith("%arr.idxOr.")) attrs = `inlinehint ${attrs}`; - // Interface dispatch is a few key tests and direct calls ahead of the - // checked dynamic call. Keeping that fallback out of line leaves the - // tests small enough to inline at each call site. - if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`; - else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`; - if (fn.name.startsWith(RESULT_ENTRY_PREFIX)) attrs = `inlinehint ${attrs}`; - if (unoptimized && attrs === FN_ATTRS) attrs = `noinline optnone ${attrs}`; - const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`; - const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name); - this.privateGlobalSlots.clear(); - let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`; - if (int32Abi) body += "\n" + this.emitInt32AbiWrapper(fn, symbol, int32Abi); - if (this.integerClones.members.has(fn.name)) { - body += "\n" + emitIntegerClone(fn, this, this.stackChecks?.has(fn.name) === true); - if (this.integerClones.dispatched.has(fn.name)) - body += - "\n" + - emitIntegerDispatcher( - fn, - this, - `${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}`, - int32Abi?.params ?? [], - int32Abi?.result === true, - ); - } - if (!borrowed) return body; - const slot = this.implSlotBorrowed(fn.name); - const virtual = - slot.size > 0 && slot.size !== borrowed.size - ? "\n" + this.emitOwnedCallAdapter(fn, borrowed, slot) - : ""; - return body + "\n" + this.emitOwnedCallAdapter(fn, borrowed) + virtual; + emitFunction(fn: IrFunction): string { + return emitFunction(this, fn); } /** The double-ABI entry of a function with an i32 entry: every caller * that is not a direct call (closures, dispatch, adapters, the runtime) * enters here. A pending exception passes through to its caller. */ - private emitInt32AbiWrapper(fn: IrFunction, symbol: string, abi: Int32Abi): string { + emitInt32AbiWrapper(fn: IrFunction, symbol: string, abi: Int32Abi): string { const lines: string[] = []; const params: string[] = []; const args: string[] = []; @@ -7632,7 +3995,7 @@ export class LlEmitter { /** With `kept`, the virtual adapter of a borrowing slot instead: the * slot's parameters stay borrowed, the body's other borrowed ones are * released here. */ - private emitOwnedCallAdapter( + emitOwnedCallAdapter( fn: IrFunction, borrowed: ReadonlySet, kept: ReadonlySet = NO_BORROWED, @@ -7672,86 +4035,13 @@ export class LlEmitter { kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name); return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`; } - - /** A local stable binding keeps its value alive while later arguments - * and the callee execute. A proven local union box cannot escape or be - * rebound during a call; its independent payload owner keeps it alive. - * Other writable bindings need an owned snapshot. */ canBorrowCallArgument(value: IrExpr): boolean { - if (value.kind === "strLit") return true; - // An exact capsule owns its instance while the capsule's binding holds it. - if (value.kind === "dynCheck" && value.exact) - return value.value.kind === "varRef" && this.canBorrowCallArgument(value.value); - // An inline union field has no box to lend. - if (this.inlineFieldGet(value)) return false; - // Class casts reinterpret the same pointer (prefix layouts). - if ( - (value.kind === "upcast" || value.kind === "downcast") && - value.value.type.kind === "object" - ) - return this.canBorrowCallArgument(value.value); - if (value.kind === "seqExpr") { - const guarded = guardedValue(value); - return guarded !== null && this.canBorrowCallArgument(guarded); - } - // A nullable-union wrap reinterprets its payload pointer; an immortal - // unit constant needs no owner at all. - const wrap = this.borrowableNullableWrap(value); - if (wrap === "unit") return true; - if (wrap === "ref" && value.kind === "unionWrap") - return this.canBorrowCallArgument(value.value); - // Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose - // failing arm always throws; the successful arm projects the same - // stable owner. The throwing arm's typed dummy owns nothing. - if (value.kind === "unionNarrow") return this.canBorrowCallArgument(value.value); - if (value.kind === "libCall") return value.fn === "error.nodeThrow"; - // A final field never changes outside constructors: while its object is - // borrowable, so is the field's value. Nullable-pointer fields have no - // box to lend and keep their owning path. - if ( - value.kind === "fieldGet" && - this.finalFieldBorrows && - isRefCounted(value.type) && - this.finalFields.isFinal(value.className, value.field) && - !this.nullableFieldGet(value) - ) - return this.canBorrowReceiver(value.obj) && this.canBorrowCallArgument(value.obj); - if (value.kind === "ternary") - return ( - this.canBorrowReceiver(value.then) && - this.canBorrowReceiver(value.else_) && - this.canBorrowCallArgument(value.then) && - this.canBorrowCallArgument(value.else_) - ); - // A construction-only field of a stable owner keeps its value until the - // owner dies: no later operand or callee can replace and release it. - if ((value.kind === "fieldGet" || value.kind === "recordGet") && this.immutableRead(value)) - return this.canBorrowCallArgument(value.obj); - // An optional chain's bound receiver stays unchanged until its body - // finishes (see emitReadReceiver). - if (value.kind === "chainRecv") return this.chainSlots.has(value.id); - if (value.kind !== "varRef") return false; - const binding = this.binding(value.localId); - // A module constant owns its value for the rest of the program. - if (binding.kind === "global") return this.stableGlobals.has(value.localId); - // So does the environment for a capture nothing ever rebinds. - if (binding.kind === "boxed") - return this.stableCaptureIds.has(value.localId) && this.canBorrowReceiver(value); - return ( - binding.kind === "local" && - binding.local !== undefined && - (!binding.local.mutable || - this.stableCallBindings.has(value.localId) || - this.borrowedParameters.has(value.localId) || - this.localUnionStorage.has(value.localId)) && - !binding.local.boxed && - !binding.local.tdz - ); + return canBorrowCallArgument(this, value); } /** A plain-pointer read of a construction-only field outside any * constructor (ImmutableFields). */ - private immutableRead(e: ExprOf<"fieldGet" | "recordGet">): boolean { + immutableRead(e: ExprOf<"fieldGet" | "recordGet">): boolean { if (this.inConstructor || this.immutableFields === null) return false; const t = e.type.kind; if (t !== "object" && t !== "record" && t !== "array" && t !== "string") return false; @@ -7775,7 +4065,7 @@ export class LlEmitter { * rest of its block (its whole lifetime) writes none of the projection's * locations: the source edge then keeps the value alive, and uses that * escape still retain their own copies. */ - private unwrittenAlias(s: IrStmt & { kind: "varDecl" }): boolean { + unwrittenAlias(s: IrStmt & { kind: "varDecl" }): boolean { const cursor = this.stmtCursor; const rw = this.referenceWrites; if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s) @@ -7794,7 +4084,7 @@ export class LlEmitter { * element when the rest of the block writes no array element and none of * the locations keeping the array alive (which must itself be read * without a frame owner). */ - private unwrittenLocalArrayRead(s: IrStmt & { kind: "varDecl" }, read: LocalArrayRead): boolean { + unwrittenLocalArrayRead(s: IrStmt & { kind: "varDecl" }, read: LocalArrayRead): boolean { const cursor = this.stmtCursor; const rw = this.referenceWrites; if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s) @@ -7824,7 +4114,7 @@ export class LlEmitter { * finally copies, like any pending return value, and the adopting await * runs afterwards with no handler in scope: a rejection settles the * function's promise instead of reaching its own catch clauses. */ - private emitAdoptingReturn(s: IrStmt & { kind: "return" }, adopted: ExprOf<"awaitExpr">): void { + emitAdoptingReturn(s: IrStmt & { kind: "return" }, adopted: ExprOf<"awaitExpr">): void { const B = this.B; const promise = this.emitExpr(adopted.value); this.toFrame(promise); @@ -7880,19 +4170,19 @@ export class LlEmitter { /** An async generator completing without a further turn: its request * settles as the body returns (scr_async_gen_hop_done). */ - private emitAsyncGenSettled(): void { + emitAsyncGenSettled(): void { this.declare(`declare void @scr_async_gen_hop_done()`); this.B.line(`call void @scr_async_gen_hop_done()`); } /** Owned-binding reads that take over the binding's reference, by read * node (the binding's slot). */ - private readonly lastUseMoves = new Map(); + readonly lastUseMoves = new Map(); /** How often each varRef node object occurs in the current function: * lowering may share one node between several reads, and a move keyed by * node identity must fire for exactly one of them. */ - private varRefCounts: Map | null = null; - private currentFunctionBody: IrStmt[] = []; + varRefCounts: Map | null = null; + currentFunctionBody: IrStmt[] = []; private varRefCount(e: IrExpr): number { if (this.varRefCounts === null) { @@ -7922,10 +4212,7 @@ export class LlEmitter { /** An owned local declared at the statement cursor whose last statement * reads it once (lastUseRead): that read moves the reference. */ - private noteLastUseMove( - s: IrStmt & { kind: "varDecl" }, - b: ReturnType, - ): void { + noteLastUseMove(s: IrStmt & { kind: "varDecl" }, b: ReturnType): void { const cursor = this.stmtCursor; if ( !this.lastUseMovesAllowed() || @@ -7947,7 +4234,7 @@ export class LlEmitter { * parameter's slot (they need it until the function returns), so bodies * with walks keep their parameters, and locals rebound from the * parameter count as derived from it. */ - private noteParameterMove(fn: IrFunction, localId: string, slot: string): void { + noteParameterMove(fn: IrFunction, localId: string, slot: string): void { if ( !this.lastUseMovesAllowed() || fn.async || @@ -7972,7 +4259,7 @@ export class LlEmitter { /** `return x` of a local whose slot is an owned scope entry of the same * type: the return can take over the local's reference. */ - private movableReturn( + movableReturn( value: IrExpr | null, ): { scope: LlScopeEntry[]; entry: LlScopeEntry; index: number } | null { if (value === null || value.kind !== "varRef" || !isRefCounted(value.type)) return null; @@ -7994,7 +4281,7 @@ export class LlEmitter { /** A walk host for a body that may write the heap: each walk must read * only locations the whole body (with its callees) never writes; * rebinding the walk locals themselves releases nothing. */ - private unwrittenWalkHost(fn: IrFunction, host: WalkBorrowHost): WalkBorrowHost { + unwrittenWalkHost(fn: IrFunction, host: WalkBorrowHost): WalkBorrowHost { return { ...host, unwritten: (value, candidates) => @@ -8094,7 +4381,7 @@ export class LlEmitter { // ── statements ────────────────────────────────────────────────────────── - private emitStmts(stmts: IrStmt[], scoped = false): void { + emitStmts(stmts: IrStmt[], scoped = false): void { const saved = this.stmtCursor; const cursor = scoped ? { stmts, index: 0 } : null; try { @@ -8117,7 +4404,7 @@ export class LlEmitter { * scope opens, before the statements — the catch-binding hook: it may * emit prelude lines and register entries the scope owns (released on * every exit, jumps and unwinds included). */ - private emitBlock(stmts: IrStmt[], setup?: (scope: LlScopeEntry[]) => void): void { + emitBlock(stmts: IrStmt[], setup?: (scope: LlScopeEntry[]) => void): void { const scope: LlScopeEntry[] = []; this.scopes.push(scope); setup?.(scope); @@ -8151,1674 +4438,21 @@ export class LlEmitter { if (loop) this.loopDepth--; } } - private emitStmtBody(s: IrStmt): void { - const B = this.B; - this.frames.push([]); - switch (s.kind) { - case "varDecl": { - if (s.init && this.int32Locals.has(s.localId)) { - this.storeInt32Local(s.localId, this.emitExpr(s.init), s.init); - break; - } - const b = this.binding(s.localId); - if (this.walkBorrows.has(s.localId) && b.kind === "local") { - // A borrowed walk pointer: no retain now, no release at scope exit. - const v = s.init === null ? "null" : this.emitReadReceiver(s.init).name; - B.line(`store ptr ${v}, ptr ${b.slot}`); - break; - } - const slice = this.scalarStringSlices.get(s.localId); - if (slice) { - const snapshot = emitStringSliceSnapshot(this, slice); - this.stringSlices.set(s.localId, snapshot); - this.scopes[this.scopes.length - 1]!.push({ slot: snapshot.source, type: STRING }); - break; - } - const split = this.privateSplitLocals.get(s.localId); - if (split) { - const snapshot = storeSplitSnapshot(this, emitSplitSnapshot(this, split)); - this.storedSplits.set(s.localId, snapshot); - this.scopes[this.scopes.length - 1]!.push( - { slot: snapshot.sourceSlot, type: STRING }, - { slot: snapshot.separatorSlot, type: STRING }, - ); - break; - } - const stackObject = this.localStackObjects.get(s.localId); - if (stackObject) { - const value = emitStackObject(this, stackObject, (fields) => this.ownScopeFields(fields)); - B.line(`store ptr ${value.name}, ptr ${b.slot}`); - break; - } - if (s.init && this.reassignedStackObjects.has(s.localId)) { - const slot = this.stackObjectSlot(s.localId, s.init.type); - emitStackObject( - this, - s.init as StackObjectSource, - (fields) => this.ownScopeFields(fields), - slot, - ); - B.line(`store ptr ${slot}, ptr ${b.slot}`); - break; - } - const localUnion = this.localStackUnions.get(s.localId); - if (localUnion) { - const union = emitStackUnion(this, localUnion); - B.line(`store ptr ${union.value.name}, ptr ${b.slot}`); - if (union.payload) { - this.toFrame(union.payload); - this.scopes[this.scopes.length - 1]!.push({ - slot: union.payloadSlot, - type: union.payload.type, - }); - } - break; - } - const localRead = this.localArrayReads.get(s.localId); - if (localRead) { - const owner = emitLocalArrayRead( - this, - !localRead.borrow && this.unwrittenLocalArrayRead(s, localRead) - ? { ...localRead, borrow: true } - : localRead, - b.slot, - ); - if (owner) this.scopes[this.scopes.length - 1]!.push(owner); - break; - } - const mapRead = this.mapReadLifetimes.locals.get(s.localId); - if (mapRead) { - const result = emitStackMapRead(this, mapRead); - B.line(`store ptr ${result.value.name}, ptr ${b.slot}`); - if (result.owner) this.scopes[this.scopes.length - 1]!.push(result.owner); - break; - } - const localStorage = this.localUnionStorageProofs.get(s.localId); - if (localStorage && s.init) { - const storage = allocateLocalUnion(this, localStorage); - this.localUnionStorage.set(s.localId, storage); - storeLocalUnion(this, storage, s.init); - B.line(`store ptr ${storage.box}, ptr ${b.slot}`); - if (storage.owner && storage.ownerType) - this.scopes[this.scopes.length - 1]!.push({ - slot: storage.owner, - type: storage.ownerType, - }); - break; - } - if (b.kind === "boxed") { - const stack = this.stackCaptures.emit(this, s.localId, b.type); - if (stack) { - B.line(`store ptr ${stack.box}, ptr ${b.slot}`); - if (stack.owner) this.scopes[this.scopes.length - 1]!.push(stack.owner); - if (s.init) { - const value = this.emitExpr(s.init); - if (isRefCounted(value.type)) this.toFrame(value); - this.boxSet(stack.box, b.type, value.name); - } - break; - } - // Box FIRST, then evaluate the initializer: a named function - // expression's closure captures this box during init evaluation. - // A SCALAR TDZ box rides an ARR-kind box: the value lives in a - // one-element array cell, so the empty (NULL) slot stays the - // not-yet-initialized sentinel — a raw scalar slot has no spare - // bit pattern to spend on it (stmts.ts's varDecl). - const boxNew = - b.local!.tdz === true && boxAccess(b.type) !== "ref" - ? (this.declare(`declare ptr @scr_box_new(i32)`), `call ptr @scr_box_new(i32 3)`) - : boxNewCall(this.shapeHost, b.type); - const box = B.tmp(); - B.line(`${box} = ${boxNew} ; let ${b.local!.name} (boxed)`); - B.line(`store ptr ${box}, ptr ${b.slot}`); - this.scopes[this.scopes.length - 1]!.push({ slot: b.slot, type: b.type, boxed: true }); - if (s.init === null) { - // An empty TDZ box whose payload can legitimately be NULL - // starts at its distinct sentinel (tdzEmptySentinel). - const sentinel = b.local!.tdz === true ? this.tdzEmptySentinel(b.type) : null; - if (sentinel !== null) B.line(`store ptr ${sentinel}, ptr ${this.boxSlot(box)}`); - break; - } - const v = this.emitExpr(s.init); - if (isRefCounted(v.type)) this.toHeap(v); // the box takes ownership - if (b.local!.tdz === true && boxAccess(b.type) !== "ref") { - this.tdzScalarInit(box, b.type, v.name); - } else { - this.boxSet(box, b.type, v.name); - } - break; - } - if (s.init === null) { - // Declared, uninitialized (`let x: number;`): reset the slot — - // inside a loop the previous iteration's scope exit released the - // old value and left a stale pointer (NULL-tolerant releases). - if (isRefCounted(b.type)) { - B.line(`store ptr null, ptr ${b.slot}`); - this.scopes[this.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); - } - break; - } - if (this.loopArrayBorrows.has(s) && s.init.kind === "arrayGet") { - emitBorrowedArrayRead(this, s.init, b.slot); - break; - } - const holeRead = this.loopArrayBorrows.has(s) ? dynHoleRead(s.init) : null; - if (holeRead && s.init.kind === "ternary") { - // A hole reads as the immortal undefined value, which needs no owner. - const present = this.emitExpr(s.init.cond); - const filled = B.newLabel("borrow.hole.filled"), - missing = B.newLabel("borrow.hole.missing"), - done = B.newLabel("borrow.hole.done"); - B.condBr(present.name, filled, missing); - B.startBlock(filled); - emitBorrowedArrayRead(this, holeRead, b.slot); - B.br(done); - B.startBlock(missing); - this.declare("declare ptr @scr_dyn_undefined()"); - const undef = B.tmp(); - B.line(`${undef} = call ptr @scr_dyn_undefined()`); - B.line(`store ptr ${undef}, ptr ${b.slot}`); - B.br(done); - B.startBlock(done); - break; - } - // A stable lexical alias cannot outlive its unchanged local owner. - // Whole-value uses still retain their own copies, so returning or - // storing the alias preserves the ordinary heap representation. - // Boxed captures, reassigned sources and projections need ownership. - if ( - b.kind === "local" && - b.local && - isRefCounted(b.type) && - this.stableCallBindings.has(s.localId) && - (this.canBorrowCallArgument(s.init) || this.unwrittenAlias(s)) - ) { - const v = this.emitReadReceiver(s.init); - B.line(`store ptr ${v.name}, ptr ${b.slot}`); - break; - } - const v = this.emitExpr(s.init); - if (b.kind === "global") this.toHeap(v); - else this.toFrame(v); - B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`); - this.storeIntegerView(s.localId, v, s.init); - if (isRefCounted(b.type)) { - this.scopes[this.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); - this.noteLastUseMove(s, b); - } - break; - } - case "assign": { - if (this.int32Locals.has(s.localId)) { - this.storeInt32Local(s.localId, this.emitExpr(s.value), s.value); - break; - } - const owner = this.reboundOwnerSlots.get(s.localId); - if (owner !== undefined && !this.walkBorrows.has(s.localId)) { - // A rebound borrowed parameter: the new value moves into the owner - // slot, replacing (and releasing) the previous owned value. This - // precedes the in-place string append, which would release the - // caller's borrow. - const param = this.binding(s.localId); - const slot = param.slot; - const v = this.emitExpr(s.value); - this.toFrame(v); - const old = B.tmp(); - B.line(`${old} = load ptr, ptr ${owner}`); - this.releaseFrame(old, param.type); - B.line(`store ptr ${v.name}, ptr ${slot}`); - B.line(`store ptr ${v.name}, ptr ${owner}`); - break; - } - if (this.reassignedStackObjects.has(s.localId)) { - // The new value may read the old one: build it apart (its - // reference fields owned by this statement until it completes), - // then copy it over the binding's slot. The binding takes over the - // new fields; the old ones are released after the slot no longer - // holds them. - const struct = stackObjectStruct(s.value.type); - let owned: readonly StackObjectField[] = []; - const value = emitStackObject(this, s.value as StackObjectSource, (fields) => { - owned = fields; - for (const field of fields) this.ownSlot(field.ptr, field.type); - }); - const target = this.stackObjectSlot(s.localId, s.value.type); - const old = stackObjectFields(this, s.value.type, target).map((field) => { - const t = B.tmp(); - B.line(`${t} = load ptr, ptr ${field.ptr}`); - return { name: t, type: field.type }; - }); - const bits = B.tmp(); - B.line(`${bits} = load ${struct}, ptr ${value.name}`); - B.line(`store ${struct} ${bits}, ptr ${target}`); - this.disownSlots(owned); - for (const field of old) this.releaseFrame(field.name, field.type); - break; - } - const localStorage = this.localUnionStorage.get(s.localId); - if (localStorage) { - storeLocalUnion(this, localStorage, s.value); - break; - } - const self = matchStringSelfConcat(s.localId, s.value); - if (self) { - this.emitStringSelfConcatAssign(s.localId, self.left, self.suffix, false); - break; - } - const b = this.binding(s.localId); - if (this.walkBorrows.has(s.localId) && b.kind === "local") { - // Rebinding a borrowed walk pointer releases nothing. - B.line(`store ptr ${this.emitReadReceiver(s.value).name}, ptr ${b.slot}`); - break; - } - const v = this.emitExpr(s.value); - if (b.kind === "global" && !s.initializes) this.checkGlobalTdz(s.localId); - if (b.kind === "boxed") { - this.writeBindingBox(this.loadBox(b.slot), b.local!, v.name, s.initializes); - // The RHS remains frame-owned until a possible TDZ throw passes. - if (isRefCounted(v.type)) this.toHeap(v); - break; - } - if (b.kind === "global") this.toHeap(v); - else this.toFrame(v); - if (isRefCounted(b.type)) { - const old = B.tmp(); - B.line(`${old} = load ptr, ptr ${b.slot}`); - if (b.kind === "global") this.releaseHeap(old, b.type); - else this.releaseFrame(old, b.type); - } - B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`); - this.storeIntegerView(s.localId, v, s.value); - if (b.kind === "global" && this.immortalGlobals.has(s.localId)) { - // An Immutable module constant (contracts/check.ts proved it is - // fresh, publishable and never written): its graph becomes - // immortal, so reads skip reference counting and the collector - // never visits it. - const lines = this.publishWalkers.emitPublish(v.name, v.type, s.loc); - for (const line of lines) B.line(line.trimStart()); - // Reads of this global skip retains from here on, which is only - // sound once the graph really is immortal. Node.js never fails - // here, so a refusal means the contract proof was wrong: stop - // rather than keep running with a mortal graph read as immortal. - if (lines.length > 0) { - this.declare(`declare zeroext i1 @scr_exc_pending()`); - this.declare(`declare void @scr_trap(ptr)`); - const refused = B.tmp(); - B.line(`${refused} = call zeroext i1 @scr_exc_pending()`); - const stop = B.newLabel("immutable.refused"); - const done = B.newLabel("immutable.published"); - B.condBr(refused, stop, done); - B.startBlock(stop); - B.line( - `call void @scr_trap(ptr ${this.cstr("scriptc: internal error: an Immutable value could not be published\n")})`, - ); - B.terminate("unreachable"); - B.startBlock(done); - } - } - break; - } - case "exprStmt": - // A statement-position splice never observes its removed elements. - if (!emitDiscardedSplice(this, s.expr)) this.emitDiscarded(s.expr); - break; - case "arraySet": { - // Evaluation order matches JS: array, index, then value. Ownership - // of a refcounted value moves into the array (the runtime releases - // the replaced element itself). A local receiver that the index and - // value cannot rebind is read in place, as for field stores; the - // released element cannot reach that binding. - if (this.emitArraySelfConcat(s)) break; - const arr = this.emitStableReceiver(s.arr, [s.index, s.value]); - const idx = this.emitExpr(s.index); - const v = this.emitExpr(s.value); - if (s.arr.type.kind !== "array") - throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); - const acc = elemAccess(s.arr.type.elem); - this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); - if (acc === "ref") this.toHeap(v); - emitDenseArraySet( - this, - arr.name, - idx, - s.index, - acc, - s.arr.type.elem, - v.name, - this.provenDenseIndex(s.arr, s.index), - ); - break; - } - case "arraySetLength": { - // Pop's `length = 0` store on an empty array only matters when the - // array can be frozen (published); its operand is a local. - if ( - s.pop === true && - s.length.kind === "numLit" && - s.length.value === 0 && - this.publishedTypes?.containerGuarded(s.arr.type) !== true - ) - break; - const arr = emitBorrowedInput(this, s.arr); - const length = this.emitExpr(s.length); - if (s.arr.type.kind !== "array") - throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array"); - this.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3); - this.declare(`declare void @scr_arr_set_len(ptr, double)`); - // Assigning the current length (a reused work array reset to where - // it already is) changes nothing: the runtime's validation accepts - // exactly the value equal to the length (-0 included, NaN never), - // and no element is released. Everything else keeps the call. - const lenPtr = B.tmp(), - len = B.tmp(), - lenNumber = B.tmp(), - same = B.tmp(); - const resize = B.newLabel("arr.len.set"), - done = B.newLabel("arr.len.done"); - B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`); - this.markMemoryPointer(lenPtr, "array:header"); - B.line( - `${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}${this.arrayLengthRange}`, - ); - B.line(`${lenNumber} = ${sizeToDouble(this.sizeType)} ${this.sizeType} ${len} to double`); - B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`); - B.condBr(same, done, resize); - B.startBlock(resize); - if (s.pop === true) { - // A discarded pop's `length - 1` on a packed array removes the - // last element inline, then releases it. - const call = B.newLabel("arr.len.call"), - pop = B.newLabel("arr.len.pop"); - const lastNumber = B.tmp(), - dropsLast = B.tmp(); - B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`); - B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`); - B.condBr(dropsLast, pop, call); - B.startBlock(pop); - const elem = s.arr.type.elem; - const acc = elemAccess(elem); - const raw = emitDenseArrayPop(this, arr.name, acc, call, call); - if (acc === "ref" && isRefCounted(elem)) { - const old = B.tmp(); - B.line(`${old} = inttoptr i64 ${raw} to ptr`); - this.releaseHeap(old, elem); - } - B.br(done); - B.startBlock(call); - } - B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`); - this.emitPendingCheck(); - B.br(done); - B.startBlock(done); - break; - } - case "arraySetUndefined": - case "arrayDelete": { - const arr = emitBorrowedInput(this, s.arr); - const idx = this.emitExpr(s.index); - if (s.arr.type.kind !== "array") - throw new InternalCompilerError(`llvm emitter bug: ${s.kind} on non-array`); - this.emitPublishedArrayGuard( - arr.name, - s.arr.type, - s.kind === "arrayDelete" ? 5 : 0, - idx.name, - ); - const fn = s.kind === "arraySetUndefined" ? "scr_arr_set_undefined" : "scr_arr_delete"; - this.declare( - `declare ${s.kind === "arrayDelete" ? "zeroext i1" : "void"} @${fn}(ptr, double)`, - ); - if (s.kind === "arrayDelete") - B.line(`call zeroext i1 @${fn}(ptr ${arr.name}, double ${idx.name})`); - else B.line(`call void @${fn}(ptr ${arr.name}, double ${idx.name})`); - break; - } - case "bytesSet": { - // Typed-array element write: same evaluation order as arraySet; - // the value is a scalar (the kind-specific inline path coerces - // JS-exactly), so no ownership moves. Invalid writes are ignored. - // IrBytesElem is static, so never rediscover it through the - // generic runtime switch in a hot loop. - const arr = this.emitStableReceiver(s.arr, [s.index, s.value]); - const idx = this.emitExpr(s.index); - const v = this.emitExpr(s.value); - if (s.arr.type.kind !== "bytes") - throw new InternalCompilerError("llvm emitter bug: bytesSet on non-bytes"); - this.emitBytesSet( - s.arr.type.elem, - arr.name, - idx, - v, - s.index, - s.value, - this.bytesBounds.has(s), - ); - break; - } - case "fieldSet": - case "recordSet": { - // Evaluation order: obj, then value. New value moved in; the old - // value is released AFTER the field is overwritten (unlink-then- - // release — a release can trigger a cycle collection, which must - // never see a heap edge whose count was already given up). - // Classes and records share the struct layout, so one emission. - const nullable = - s.kind === "fieldSet" ? this.nullableFields.get(s.className, s.field) : null; - if (nullable && s.kind === "fieldSet") { - const obj = this.emitStableReceiver(s.obj, [s.value]); - // (The guard precedes the value here: the nullable store - // evaluates it itself.) - this.emitStoreGuard(obj.name, s); - this.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value); - break; - } - if (s.kind === "fieldSet" && this.int32Slots.isField(s.className, s.field)) { - const obj = this.emitStableReceiver(s.obj, [s.value]); - const v = this.emitExpr(s.value); - this.emitStoreGuard(obj.name, s); - const { ptr } = this.classFieldPtr(obj.name, s.className, s.field); - this.storeInt32Field(ptr, v, s.value); - break; - } - if (this.emitFieldSelfConcat(s)) break; - const obj = this.emitStableReceiver(s.obj, [s.value]); - const v = this.emitFieldValue( - s.value, - s.kind === "recordSet" - ? this.recordFieldInline(s.shapeId, s.field) - : this.inlineUnions.classField(s.className, s.field), - ); - if (s.kind === "fieldSet") this.emitStoreGuard(obj.name, s); - else if (this.publishedTypes?.recordGuarded(s.shapeId) === true) - this.emitPublishedFieldGuard(obj.name, null, s.field); - const { ptr, type, inline } = - s.kind === "fieldSet" - ? this.classFieldPtr(obj.name, s.className, s.field) - : this.recordFieldPtr(obj.name, s.shapeId, s.field); - // A result-slot entry fills zeroed caller storage, each field once. - if ( - s.kind === "recordSet" && - s.obj.kind === "varRef" && - s.obj.localId === RESULT_DST_ID && - this.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX) - ) - this.initField(ptr, type, inline, v); - else this.replaceField(ptr, type, inline, v); - if ( - s.kind === "fieldSet" && - s.field === "name" && - this.classMeta.get(s.className)?.root.def.name === "%Error" - ) { - const present = this.classFieldPtr(obj.name, s.className, "%namePresent").ptr; - const enumerable = this.classFieldPtr(obj.name, s.className, "%nameEnumerable").ptr; - const wasPresent = B.tmp(), - wasEnumerable = B.tmp(), - isPresent = B.tmp(), - nextEnumerable = B.tmp(); - B.line(`${wasPresent} = load i8, ptr ${present}`); - B.line(`${wasEnumerable} = load i8, ptr ${enumerable}`); - B.line(`${isPresent} = icmp ne i8 ${wasPresent}, 0`); - B.line(`${nextEnumerable} = select i1 ${isPresent}, i8 ${wasEnumerable}, i8 1`); - B.line(`store i8 1, ptr ${present}`); - B.line(`store i8 ${nextEnumerable}, ptr ${enumerable}`); - } - break; - } - case "recordKeyDelete": { - // `delete obj[k]` on a pure index-signature shape: a Map delete on - // the overflow (key and value released; absent keys no-op). - const obj = this.emitExpr(s.obj); - const key = this.emitExpr(s.key); - if (this.publishedTypes?.recordGuarded(s.shapeId) === true) { - // Node: "Cannot delete property 'k' of #" (key known at - // run time only; the guard reports the frozen record generically). - this.emitPublishedFieldGuard(obj.name, null, "[key]"); - } - const ovf = this.recordOvfPtr(obj.name, s.shapeId); - this.declare(`declare zeroext i1 @scr_map_delete_str(ptr, ptr)`); - const t = B.tmp(); - B.line(`${t} = call zeroext i1 @scr_map_delete_str(ptr ${ovf}, ptr ${key.name})`); - break; - } - case "recordKeySet": { - // Dynamic-keyed record write (the C per-shape helper, inline): - // declared keys write through (same-typed — dyn-valued shapes, - // whose writes validate and can throw, stay refused), undeclared - // keys insert/replace in the overflow map. Evaluation order: obj, - // key, value; the write OWNS the value (+1 moves in). - const obj = this.emitExpr(s.obj); - const key = this.emitExpr(s.key); - const v = this.emitExpr(s.value); - if (this.publishedTypes?.recordGuarded(s.shapeId) === true) - this.emitPublishedFieldGuard(obj.name, null, "[key]"); - if (isRefCounted(v.type)) this.toHeap(v); - const shape = this.recordShape(s.shapeId); - // Signature-free shapes dispatch over their (one-typed) declared - // fields and TRAP on a miss (scr_record_key_miss — JS would add - // the property, which a monomorphic struct cannot); overflow - // shapes keep the map insert tail. - const iv = shape.indexValue ?? shape.fields[0]?.type; - if (!iv) - throw new InternalCompilerError( - `llvm emitter bug: keyed write on field-free non-overflow shape ${s.shapeId}`, - ); - const vAcc = iv.kind === "f64" ? "f64" : iv.kind === "bool" ? "bool" : "ref"; - if (s.overflowOnly === true) { - // A LITERAL key naming no declared field: a plain overflow - // insert — no field chain. - const ovf = this.recordOvfPtr(obj.name, s.shapeId); - this.mapSet(ovf, "str", vAcc, key.name, v.name); - break; - } - const join = B.newLabel("rks.j"); - if (iv.kind === "dyn" && shape.indexValue) { - // A dyn-valued shape (the C recordKeySetHelper's dyn arm, - // inline): declared keys VALIDATE the dyn value against the - // field's type first (dynCheck — a mismatched write throws the - // catchable TypeError and leaves the field untouched; JS would - // store anything, the documented divergence); undeclared keys - // insert the dyn value into the overflow map as-is. - this.declare(`declare void @scr_dyn_release(ptr)`); - for (const f of shape.fields) { - const hit = B.tmp(); - B.line( - `${hit} = call zeroext i1 ${this.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`, - ); - const lh = B.newLabel("rks.h"); - const ln = B.newLabel("rks.n"); - B.condBr(hit, lh, ln); - B.startBlock(lh); - const pathSlot = B.slot(); - B.entryAllocas.push(`${pathSlot} = alloca %ScrDynPath`); - const pp = B.tmp(); - const kp = B.tmp(); - const ip = B.tmp(); - B.line(`${pp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 0`); - B.line(`store ptr null, ptr ${pp}`); - B.line(`${kp} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 1`); - B.line(`store ptr ${this.cstr(f.name)}, ptr ${kp}`); - B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`); - B.line(`store ${this.sizeType} 0, ptr ${ip}`); - const helper = this.dyn.dynCheckHelper(f.type); - const fty = this.llType(f.type); - const nv = B.tmp(); - B.line( - `${nv} = call ${fty === "i1" ? "zeroext i1" : fty} @${helper}(ptr ${v.name}, ptr ${pathSlot})`, - ); - B.line(`call void @scr_dyn_release(ptr ${v.name})`); - // Mismatched write: TypeError pending, field untouched — the - // statement-level check below unwinds. - const pend = B.tmp(); - for (const line of this.pendingTestLines(pend)) B.line(line.trimStart()); - const lw = B.newLabel("rks.w"); - B.condBr(pend, join, lw); - B.startBlock(lw); - const { ptr, type, inline } = this.recordFieldPtr(obj.name, s.shapeId, f.name); - if (inline) { - // The slot copies the checked value's bits; nv is released below. - this.storeField(ptr, type, nv, true); - this.releaseFrame(nv, type); - } else if (isRefCounted(type)) { - const old = B.tmp(); - B.line(`${old} = load ptr, ptr ${ptr}`); - this.storeField(ptr, type, nv, false); - this.releaseHeap(old, type); - } else { - this.storeField(ptr, type, nv, false); - } - B.br(join); - B.startBlock(ln); - } - const ovf = this.recordOvfPtr(obj.name, s.shapeId); - this.mapSet(ovf, "str", vAcc, key.name, v.name); - B.br(join); - B.startBlock(join); - // MAY THROW exactly when a dyn value can validate against a - // declared field (stmts.ts's condition). - if (shape.fields.length > 0) this.emitPendingCheck(); - break; - } - for (const f of shape.fields) { - // typeEquals(f.type, iv) — the frontend fences everything else. - const hit = B.tmp(); - B.line( - `${hit} = call zeroext i1 ${this.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`, - ); - const lh = B.newLabel("rks.h"); - const ln = B.newLabel("rks.n"); - B.condBr(hit, lh, ln); - B.startBlock(lh); - const { ptr, type, inline } = this.recordFieldPtr(obj.name, s.shapeId, f.name); - if (inline) { - // The write owns the moved-in value; the slot copies its bits. - this.storeField(ptr, type, v.name, true); - this.releaseHeap(v.name, type); - } else if (isRefCounted(type)) { - const old = B.tmp(); - B.line(`${old} = load ptr, ptr ${ptr}`); - this.storeField(ptr, type, v.name, false); - this.releaseHeap(old, type); - } else { - this.storeField(ptr, type, v.name, false); - } - B.br(join); - B.startBlock(ln); - } - if (!shape.indexValue) { - // The MISS on a fixed shape: release the moved-in value, throw - // the catchable TypeError naming the key (scr_record_key_miss — - // JS would add the property, the documented divergence). - if (isRefCounted(iv)) this.releaseHeap(v.name, iv); - this.declare(`declare void @scr_record_key_miss(ptr)`); - B.line(`call void @scr_record_key_miss(ptr ${key.name})`); - B.br(join); - B.startBlock(join); - this.emitPendingCheck(); - break; - } - const ovf = this.recordOvfPtr(obj.name, s.shapeId); - this.mapSet(ovf, "str", vAcc, key.name, v.name); - B.br(join); - B.startBlock(join); - break; - } - case "block": { - if (s.labels === undefined) { - this.emitBlock(s.body); - break; - } - // A labeled block: `break lbl` inside branches to the end label. - const le = B.newLabel("blk.e"); - this.jumpTargets.push({ - kind: "block", - brkLabel: le, - contLabel: null, - labels: s.labels, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - finallyDepth: this.finallyStack.length, - }); - this.emitBlock(s.body); - this.jumpTargets.pop(); - B.br(le); - B.startBlock(le); - break; - } - case "if": { - const cond = this.emitCondition(s.cond); - const trueLabel = B.newLabel("if.t"); - const joinLabel = B.newLabel("if.j"); - const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel; - B.condBr(cond, trueLabel, falseLabel); - B.startBlock(trueLabel); - this.emitBlock(s.then); - B.br(joinLabel); - if (s.else_) { - B.startBlock(falseLabel); - this.emitBlock(s.else_); - B.br(joinLabel); - } - B.startBlock(joinLabel); - break; - } - case "while": { - const workerBudget = this.workerLoopBudget(s); - const le = B.newLabel("loop.e"); - this.emitLoopCopies(s, true, le, (exit) => { - const lc = B.newLabel("loop.c"); - const lb = B.newLabel("loop.b"); - const lp = workerBudget !== null ? B.newLabel("loop.p") : lc; - B.br(lc); - B.startBlock(lc); - B.condBr(this.emitCondition(s.cond), lb, exit); - B.startBlock(lb); - this.jumpTargets.push({ - kind: "loop", - brkLabel: exit, - contLabel: lp, - labels: s.labels, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - finallyDepth: this.finallyStack.length, - }); - this.emitBlock(s.body); - this.jumpTargets.pop(); - B.br(lp); - if (lp !== lc) { - B.startBlock(lp); - this.emitWorkerLoopCheck(workerBudget); - B.br(lc); - } - }); - B.startBlock(le); - break; - } - case "doWhile": { - const workerBudget = this.workerLoopBudget(s); - // Body first (runs at least once); continue jumps to the CONDITION. - const le = B.newLabel("loop.e"); - this.emitLoopCopies(s, true, le, (exit) => { - const lb = B.newLabel("loop.b"); - const lc = B.newLabel("loop.c"); - B.br(lb); - B.startBlock(lb); - this.jumpTargets.push({ - kind: "loop", - brkLabel: exit, - contLabel: lc, - labels: s.labels, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - finallyDepth: this.finallyStack.length, - }); - this.emitBlock(s.body); - this.jumpTargets.pop(); - B.br(lc); - B.startBlock(lc); - this.emitWorkerLoopCheck(workerBudget); - B.condBr(this.emitCondition(s.cond), lb, exit); - }); - B.startBlock(le); - break; - } - case "for": { - // The init's scope wraps the whole loop (break/continue must NOT - // release it — scopeDepth captured after the push, C parity). - const integerArrayLoop = - this.debug === null - ? matchIntegerArrayForLoop(s, this.currentLocals, this.integerArrayBindings) - : null; - const integerLoop = - integerArrayLoop ?? - (this.debug === null ? matchIntegerBytesForLoop(s, this.numericLocals) : null); - const countedLoop = - !integerLoop && this.countedLoopsEnabled - ? matchIntegerCountedForLoop( - s, - this.numericLocals, - this.integerRanges, - this.unaliasedArrays, - ) - : null; - const window = - countedLoop && this.byteWindowsEnabled - ? matchByteWindow( - s, - countedLoop, - this.numericLocals, - this.captureIds, - this.integerRanges, - this.byteWindowEntries.get(s) ?? new Set(), - ) - : null; - const denseShape = !this.denseLoopsEnabled - ? null - : integerArrayLoop - ? { - counter: integerArrayLoop.localId, - length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" }, - range: { min: 0, max: 4294967294 }, - } - : countedLoop && !countedLoop.guarded - ? countedDenseShape(s, countedLoop) - : null; - const dense = - denseShape && !window - ? matchDenseArrayLoop( - s, - denseShape, - this.numericLocals, - this.unaliasedArrays, - this.denseLoopEntries.get(s) ?? new Set(), - ) - : null; - if (dense) { - const valid = emitDenseLoopGuard(this, dense); - const fast = B.newLabel("dense.loop.fast"), - slow = B.newLabel("dense.loop.slow"), - done = B.newLabel("dense.loop.done"); - B.condBr(valid, fast, slow); - this.denseLoopsEnabled = false; - try { - B.startBlock(slow); - this.emitStmt(s); - B.br(done); - B.startBlock(fast); - this.denseLoop = dense; - this.emitStmt(s); - B.br(done); - } finally { - this.denseLoop = null; - this.denseLoopsEnabled = true; - } - B.startBlock(done); - break; - } - if (window) { - const valid = emitByteWindowGuard(this, window); - const fast = B.newLabel("bytes.window.fast"), - slow = B.newLabel("bytes.window.slow"), - done = B.newLabel("bytes.window.done"); - B.condBr(valid, fast, slow); - const originalRanges = this.integerRanges, - originalBounds = this.bytesBounds; - this.byteWindowsEnabled = false; - try { - B.startBlock(slow); - this.emitStmt(s); - B.br(done); - B.startBlock(fast); - this.integerRanges = window.ranges; - this.bytesBounds = new Set([...originalBounds, ...window.bounds]); - this.emitStmt(s); - B.br(done); - } finally { - this.integerRanges = originalRanges; - this.bytesBounds = originalBounds; - this.byteWindowsEnabled = true; - } - B.startBlock(done); - break; - } - let countedJoin: string | null = null; - if (countedLoop?.guarded) { - let safe = "true"; - if (countedLoop.limitGuarded) { - const bound = this.emitExpr(countedLoop.limit); - safe = B.tmp(); - B.line( - `${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`, - ); - } - if (countedLoop.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 = this.emitExpr(countedLoop.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; - } - } - const fast = B.newLabel("counted.fast"); - const slow = B.newLabel("counted.slow"); - countedJoin = B.newLabel("counted.done"); - B.condBr(safe, fast, slow); - B.startBlock(slow); - this.countedLoopsEnabled = false; - this.emitStmt(s); - this.countedLoopsEnabled = true; - B.br(countedJoin); - B.startBlock(fast); - } - this.scopes.push([]); - let integerSlot: string | null = null; - let countedLimit: string | null = null; - if (integerLoop) { - integerSlot = B.slot(); - B.entryAllocas.push( - `${integerSlot} = alloca ${this.sizeType} ; integer induction ${this.currentLocals.get(integerLoop.localId)!.name}`, - ); - let start = "0"; - if (integerArrayLoop && s.init?.kind === "varDecl" && s.init.init) { - const value = this.emitExpr(s.init.init); - start = B.tmp(); - // The start is 0 or an enclosing array index plus one (at most - // 2^32 - 1): on 64-bit targets the signed conversion is exact - // and avoids x86-64's unsigned 64-bit conversion sequence. - const conversion = this.sizeType === "i64" ? "fptosi" : "fptoui"; - B.line(`${start} = ${conversion} double ${value.name} to ${this.sizeType}`); - this.integerArrayBindings.add(integerLoop.localId); - } - B.line(`store ${this.sizeType} ${start}, ptr ${integerSlot}`); - this.integerLoopBindings.set(integerLoop.localId, { - slot: integerSlot, - type: this.sizeType, - range: - this.denseLoop?.counter === integerLoop.localId - ? this.denseLoop.range - : { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 }, - }); - } else if (countedLoop) { - integerSlot = B.slot(); - B.entryAllocas.push( - `${integerSlot} = alloca i64 ; integer induction ${this.currentLocals.get(countedLoop.localId)!.name}`, - ); - const startNumber = this.emitExpr(countedLoop.start); - const start = B.tmp(); - B.line(`${start} = fptosi double ${startNumber.name} to i64`); - B.line(`store i64 ${start}, ptr ${integerSlot}`); - this.integerLoopBindings.set(countedLoop.localId, { - slot: integerSlot, - type: "i64", - signed: true, - range: - this.denseLoop?.counter === countedLoop.localId - ? this.denseLoop.range - : countedLoop.range, - }); - countedLimit = emitCountedLoopLimit(this, countedLoop); - } 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 - // before the first condition. - const initializers = s.init.kind === "block" ? s.init.body : [s.init]; - for (const initializer of initializers) this.emitStmt(initializer); - } - const le = B.newLabel("loop.e"); - // JS `for (let i ...)`: each iteration gets a FRESH binding holding - // a copy of the previous one (closures made in iteration k keep - // seeing iteration k's value) — only observable, and only emitted, - // when a let variable is captured (boxed). Freshen before the first - // condition and again in the continue-target block before updating. - const initializers = s.init?.kind === "block" ? s.init.body : s.init ? [s.init] : []; - const capturedLets = initializers.flatMap((init) => { - const local = init.kind === "varDecl" ? this.currentLocals.get(init.localId) : undefined; - return local?.boxed && local.mutable ? [local] : []; - }); - const freshenBindings = (): void => { - for (const local of capturedLets) { - const slot = `%${mangleLocal(local.id)}`; - const fresh = B.tmp(); - const old = B.tmp(); - B.line( - `${fresh} = ${boxNewCall(this.shapeHost, local.type)} ; per-iteration ${local.name}`, - ); - B.line(`${old} = load ptr, ptr ${slot}`); - const val = this.boxGet(old, local.type); - this.boxSet(fresh, local.type, val); - this.declare(`declare void @scr_box_release(ptr)`); - B.line(`call void @scr_box_release(ptr ${old})`); - B.line(`store ptr ${fresh}, ptr ${slot}`); - } - }; - const workerBudget = this.workerLoopBudget(s); - freshenBindings(); - this.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => { - const lc = B.newLabel("loop.c"); - const lb = B.newLabel("loop.b"); - const lu = - s.update || capturedLets.length > 0 || workerBudget !== null - ? B.newLabel("loop.u") - : lc; - B.br(lc); - B.startBlock(lc); - if (integerLoop && integerSlot) { - const receiver = this.emitStableReceiver(integerLoop.limitReceiver, []); - const lenPtr = B.tmp(); - const len = B.tmp(); - const index = B.tmp(); - const inBounds = B.tmp(); - B.line( - `${lenPtr} = getelementptr inbounds %${integerArrayLoop ? "ScrArr" : "ScrBytes"}, ptr ${receiver.name}, i64 0, i32 1`, - ); - if (integerArrayLoop) this.markMemoryPointer(lenPtr, "array:header"); - B.line( - `${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`, - ); - B.line(`${index} = load ${this.sizeType}, ptr ${integerSlot}`); - B.line(`${inBounds} = icmp ult ${this.sizeType} ${index}, ${len}`); - B.condBr(inBounds, lb, exit); - } else if (countedLoop && integerSlot && countedLimit) { - const index = B.tmp(); - const inBounds = B.tmp(); - B.line(`${index} = load i64, ptr ${integerSlot}`); - B.line( - `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, - ); - B.condBr(inBounds, lb, exit); - } else if (s.cond) B.condBr(this.emitCondition(s.cond), lb, exit); - else B.br(lb); - B.startBlock(lb); - this.jumpTargets.push({ - kind: "loop", - brkLabel: exit, - contLabel: lu, - labels: s.labels, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - finallyDepth: this.finallyStack.length, - }); - this.emitBlock(s.body); - this.jumpTargets.pop(); - B.br(lu); - if (lu !== lc) { - B.startBlock(lu); - this.emitWorkerLoopCheck(workerBudget); - freshenBindings(); - if (integerLoop && integerSlot) { - const old = B.tmp(); - const next = B.tmp(); - B.line(`${old} = load ${this.sizeType}, ptr ${integerSlot}`); - B.line(`${next} = add nuw ${this.sizeType} ${old}, 1`); - B.line(`store ${this.sizeType} ${next}, ptr ${integerSlot}`); - } else if (countedLoop && integerSlot) { - const old = B.tmp(); - const next = B.tmp(); - 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 (s.update) this.emitStmt(s.update); - B.br(lc); - } - }); - B.startBlock(le); - this.releaseScope(this.scopes.pop()!); - if (integerLoop) this.integerLoopBindings.delete(integerLoop.localId); - if (countedLoop) this.integerLoopBindings.delete(countedLoop.localId); - if (integerArrayLoop) this.integerArrayBindings.delete(integerArrayLoop.localId); - if (countedJoin) { - B.br(countedJoin); - B.startBlock(countedJoin); - } - break; - } - case "forOf": { - // Ascending index loop; the length is re-read every iteration - // (JS-exact — pushes inside the body extend the iteration). The - // iterable temp lives in this statement's frame, so it is released - // when the whole loop ends (and by `return`'s frame sweep). - if (s.iterable.type.kind !== "array") - throw new LlvmUnsupportedError(`forOf:${s.iterable.type.kind}`, s.loc); - const elem = s.iterable.type.elem; - const stored = - s.iterable.kind === "varRef" ? this.storedSplits.get(s.iterable.localId) : undefined; - const direct = this.streamingSplitsEnabled ? stringSplit(s.iterable) : null; - const snapshot = stored - ? loadSplitSnapshot(this, stored) - : direct - ? emitSplitSnapshot(this, direct) - : null; - const arr = snapshot ? null : this.emitExpr(s.iterable); - const cursor = snapshot ? emitSplitCursor(this, snapshot) : null; - const scratch = snapshot ? emitSplitScratch(this) : null; - const deferred = snapshot && canDeferSplitPiece(s, this.currentLocals.get(s.localId)); - const spanLength = deferred ? B.slot() : null; - if (spanLength) B.entryAllocas.push(`${spanLength} = alloca ${this.sizeType}`); - const idxSlot = snapshot ? null : B.slot(); - if (idxSlot) { - B.entryAllocas.push(`${idxSlot} = alloca double`); - B.line(`store double ${f64Lit(0)}, ptr ${idxSlot}`); - } - const lc = B.newLabel("fof.c"); - const lb = B.newLabel("fof.b"); - const lu = B.newLabel("fof.u"); - const le = B.newLabel("fof.e"); - const workerBudget = this.workerLoopBudget(s); - B.br(lc); - B.startBlock(lc); - const inBounds = B.tmp(); - let cur: string; - if (snapshot && cursor && scratch) { - cur = spanLength - ? emitSplitSpanNext(this, snapshot, cursor, spanLength) - : emitSplitNext(this, snapshot, cursor, scratch); - B.line(`${inBounds} = icmp ne ptr ${cur}, null`); - } else { - const i = B.tmp(), - lenPtr = B.tmp(), - rawLen = B.tmp(), - len = B.tmp(); - B.line(`${i} = load double, ptr ${idxSlot}`); - // The inline form of scr_arr_len, like the `length` intrinsic. - B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr!.name}, i32 0, i32 1`); - this.markMemoryPointer(lenPtr, "array:header"); - B.line( - `${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}${this.arrayLengthRange}`, - ); - B.line(`${len} = ${sizeToDouble(this.sizeType)} ${this.sizeType} ${rawLen} to double`); - B.line(`${inBounds} = fcmp olt double ${i}, ${len}`); - cur = i; - } - B.condBr(inBounds, lb, le); - B.startBlock(lb); - this.jumpTargets.push({ - kind: "loop", - brkLabel: le, - contLabel: lu, - labels: s.labels, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - finallyDepth: this.finallyStack.length, - }); - // The loop variable is a fresh const per iteration: its scope opens - // here, holds the (for ref elements: owned +1) current element, and - // releases it at the end of each iteration. - this.scopes.push([]); - const localInfo = this.currentLocals.get(s.localId); - const slot = `%${mangleLocal(s.localId)}`; - const borrowedElement = !snapshot && this.loopArrayBorrows.has(s); - if (!snapshot) { - const acc = elemAccess(elem); - const getter = borrowedElement ? "scr_arr_borrow_ref" : `scr_arr_get_${acc}`; - cur = emitDenseArrayGet( - this, - arr!.name, - { name: cur, type: { kind: "f64" } }, - undefined, - acc, - elem, - getter, - !borrowedElement, - ); - } - if (spanLength) { - const length = B.tmp(); - B.line(`${length} = load ${this.sizeType}, ptr ${spanLength}`); - this.splitSpans.set(s.localId, { bytes: cur, length, scratch: scratch! }); - cur = "null"; - } - if (localInfo?.boxed) { - // Captured loop variable: a fresh box per iteration, matching the - // fresh const binding. The box takes ownership of a ref element's - // +1 and is released with the iteration's scope. - const box = B.tmp(); - B.line(`${box} = ${boxNewCall(this.shapeHost, elem)} ; per-iteration ${localInfo.name}`); - this.boxSet(box, elem, cur); - B.line(`store ptr ${box}, ptr ${slot}`); - this.scopes[this.scopes.length - 1]!.push({ slot, type: elem, boxed: true }); - } else { - B.line(`store ${this.llType(elem)} ${cur}, ptr ${slot}`); - if (isRefCounted(elem) && !borrowedElement) - this.scopes[this.scopes.length - 1]!.push({ slot, type: elem }); - } - this.emitStmts(s.body); - if (spanLength) this.splitSpans.delete(s.localId); - const endedWithJump = endsWithJump(s.body); - const scope = this.scopes.pop()!; - if (!endedWithJump) this.releaseScope(scope); - this.jumpTargets.pop(); - B.br(lu); - B.startBlock(lu); - this.emitWorkerLoopCheck(workerBudget); - if (idxSlot) { - const i2 = B.tmp(), - i3 = B.tmp(); - B.line(`${i2} = load double, ptr ${idxSlot}`); - B.line(`${i3} = fadd double ${i2}, ${f64Lit(1)}`); - B.line(`store double ${i3}, ptr ${idxSlot}`); - } - B.br(lc); - B.startBlock(le); - break; - } - case "switch": - this.emitSwitch(s); - break; - case "break": { - // Unlabeled: the innermost loop OR switch (labeled blocks are - // skipped); labeled: the entry carrying the label. - let target: (typeof this.jumpTargets)[number] | undefined; - for (let i = this.jumpTargets.length - 1; i >= 0; i--) { - const t = this.jumpTargets[i]!; - if (s.label !== undefined ? t.labels?.includes(s.label) : t.kind !== "block") { - target = t; - break; - } - } - if (!target) throw new InternalCompilerError("llvm emitter bug: break target not found"); - this.emitFinallysForJump(target.finallyDepth, target.frameDepth, target.scopeDepth); - if (!B.isTerminated()) B.terminate(`br label %${target.brkLabel}`); - break; - } - case "continue": { - // Unlabeled: the innermost loop; labeled: the loop carrying the - // label (tsc + the validator guarantee it IS a loop). - let target: (typeof this.jumpTargets)[number] | undefined; - for (let i = this.jumpTargets.length - 1; i >= 0; i--) { - const t = this.jumpTargets[i]!; - if (t.kind === "loop" && (s.label === undefined || t.labels?.includes(s.label))) { - target = t; - break; - } - } - if (!target || target.contLabel === null) - throw new InternalCompilerError("llvm emitter bug: continue target not found"); - this.emitFinallysForJump(target.finallyDepth, target.frameDepth, target.scopeDepth); - if (!B.isTerminated()) B.terminate(`br label %${target.contLabel}`); - break; - } - case "return": { - // The value computes FIRST (an SSA temp — finally mutations of - // returned locals cannot change it, Node-exact), then every - // crossed finally runs innermost-first with the frames/scopes/ - // tryStack it sees truncated to its region (its releases already - // ran; a throw inside a copy propagates OUT of the completing - // try, past its own catch), then the function-level releases and - // the actual ret. The parked value owns a synthetic slot-backed - // scope entry during each copy so a throwing finally releases it. - const adopted = - this.finallyStack.length > 0 || this.tryStack.length > 0 - ? adoptingAwaitOf(s.value) - : null; - if (adopted !== null) { - this.emitAdoptingReturn(s, adopted); - break; - } - let v: LlValue | null = null; - const moved = - this.finallyStack.length === 0 && !this.currentUnionResult - ? this.movableReturn(s.value) - : null; - if (s.value !== null && this.currentUnionResult) { - // The tag and payload bits leave in registers; a heap box the - // value needed is released with its frame below. - const box = this.emitUnionProjection(s.value); - v = { name: this.scalarUnionPair(box.name, s.value.type), type: s.value.type }; - } else if (moved !== null) { - // Returning an owned local hands its reference to the caller: no - // retain now and no release of the local below. - const t = B.tmp(); - B.line(`${t} = load ptr, ptr ${moved.entry.slot}`); - v = { name: t, type: s.value!.type }; - moved.scope.splice(moved.scope.indexOf(moved.entry), 1); - this.releaseForJump(0, 0); - moved.scope.splice(moved.index, 0, moved.entry); - } else if (s.value !== null && this.currentBorrowedReturn) { - // A borrowed-return body hands back its walk without a reference - // (it has no try statements, so no finally can intervene). An - // optional element read lends the element its array keeps alive. - const read = this.optionalArrayReads.get(s.value); - if (read) { - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ptr`); - emitLocalArrayRead(this, { ...read, borrow: true }, slot); - const t = B.tmp(); - B.line(`${t} = load ptr, ptr ${slot}`); - v = { name: t, type: s.value.type }; - } else v = this.emitReadReceiver(s.value); - } else if (s.value !== null) { - v = this.emitExpr(s.value); - if (this.currentCoro !== null) this.toHeap(v); - else this.toFrame(v); - } - // An async generator's `return v` awaits v before any crossed - // finally runs. Past finally regions that turn is taken here and - // the request settles at completion; otherwise the completion's - // own turn is the await (nothing runs in between). - const genSettles = - this.currentAsyncGenerator && (s.awaited === true || this.finallyStack.length > 0); - // A completion turn that awaits x replaces a return() request's - // already-taken await. - if (this.currentAsyncGenerator && !genSettles) { - this.declare(`declare void @scr_async_gen_return_awaits()`); - B.line(`call void @scr_async_gen_return_awaits()`); - } - if (this.currentAsyncGenerator && s.awaited !== true && this.finallyStack.length > 0) { - if (this.wasi) this.emitWasiSuspend(null); - else { - this.declare(`declare void @scr_await_hop()`); - B.line(`call void @scr_await_hop()`); - } - } - if (this.finallyStack.length > 0) { - let pretSlot: string | null = null; - if (v !== null && isRefCounted(v.type) && !this.currentUnionResult) { - pretSlot = B.slot(); - B.entryAllocas.push(`${pretSlot} = alloca ptr ; pending return (through finally)`); - B.line(`store ptr ${v.name}, ptr ${pretSlot}`); - } - const savedFrames = this.frames; - const savedScopes = this.scopes; - const savedFinally = this.finallyStack; - const savedTry = this.tryStack; - for (let i = savedFinally.length - 1; i >= 0 && !B.isTerminated(); i--) { - const fin = savedFinally[i]!; - this.releaseForJump(fin.frameDepth, fin.scopeDepth); - this.frames = this.frames.slice(0, fin.frameDepth); - this.scopes = this.scopes.slice(0, fin.scopeDepth); - this.finallyStack = savedFinally.slice(0, i); - this.tryStack = savedTry.slice(0, fin.tryDepth); - if (pretSlot !== null) this.scopes.push([{ slot: pretSlot, type: v!.type }]); - this.emitBlock(fin.body); - if (pretSlot !== null) this.scopes.pop(); - } - if (!B.isTerminated()) this.releaseForJump(0, 0); - this.frames = savedFrames; - this.scopes = savedScopes; - this.finallyStack = savedFinally; - this.tryStack = savedTry; - } else if (moved === null) { - this.releaseForJump(0, 0); - } - if (!B.isTerminated()) { - if (genSettles) this.emitAsyncGenSettled(); - if (this.currentCoro !== null) { - this.emitWasiFulfill(v); - B.terminate(`br label %${this.currentCoro.finalLabel}`); - } else if (v === null) B.terminate("ret void"); - else if (this.currentInt32Result) - B.terminate(`ret i32 ${this.int32Argument(v, s.value!)}`); - else if (this.currentUnionResult) B.terminate(`ret ${SCALAR_UNION_RESULT} ${v.name}`); - else B.terminate(`ret ${this.llType(s.value!.type)} ${v.name}`); - } - break; - } - case "throw": { - // Evaluate, move ownership into the runtime's exception cell, then - // unwind unconditionally (the innermost try handler, or out of the - // function) — the same release path as return/break/continue. - const v = this.emitExpr(s.value); - if (isRefCounted(s.value.type)) this.toHeap(v); // the cell takes ownership - this.emitThrowValue({ name: v.name, type: s.value.type }); - this.emitUnwind(); - break; - } - case "rethrow": { - // Re-raise the saved snapshot (payload retained — the binding - // local releases with its scope) and unwind like `throw`. - const c = this.emitExpr({ kind: "varRef", localId: s.localId, type: CAUGHT, loc: s.loc }); - this.declare(`declare void @scr_rethrow(ptr)`); - B.line(`call void @scr_rethrow(ptr ${c.name})`); - this.emitUnwind(); - break; - } - case "runtimeFence": { - // The deferred JS compile fence: throw a catchable Error naming - // the construct (message) with the SC code stamped on `code`, - // then unwind exactly like `throw`. SCR_ERR_ERROR = 0. - const bytes = Buffer.byteLength(s.message, "utf8"); - this.declare(`declare void @scr_throw_error_msg_code(i32, ptr, ${this.sizeType}, ptr)`); - B.line( - `call void @scr_throw_error_msg_code(i32 0, ptr ${this.cstr(s.message)}, ${this.sizeType} ${bytes}, ptr ${this.cstr(s.code)})`, - ); - this.emitUnwind(); - break; - } - case "tryCatch": - this.emitTryCatch(s); - break; - default: { - // Statement coverage is now total (bytesSet closed the set) — - // keep throwing LlvmUnsupportedError for any future IR statement kind. - const rest: never = s; - const k = (rest as IrStmt).kind; - throw new LlvmUnsupportedError(`stmt:${k}`, (rest as IrStmt).loc); - } - } - const frame = this.frames.pop()!; - // return/throw already released their frames on the jump path; the - // fall-through releases after them would be dead double-release code. - if ( - s.kind !== "return" && - s.kind !== "throw" && - s.kind !== "rethrow" && - s.kind !== "runtimeFence" - ) { - this.releaseTemps(frame); - } + return emitStmtBody(this, s); } - - /** try/catch/finally via pending-flag unwinding — stmts.ts's - * emitTryCatch, block-flavored. Entering a try emits NO code: the try - * context is compile-time state (tryStack) redirecting unwinds inside - * the region to a label here. Shape: - * - * { try body } unwinds inside release frames/scopes down to - * this statement's depths, then br the handler - * br after (normal completion skips the handler) - * try.c: (emitted only when some unwind targets it) - * binding = scr_exc_take() (or scr_exc_clear() when bindingless) - * { catch body } - * after: { finally body } normal path - * br try.e - * try.fx: exception path: the pending exception is - * stash = scr_exc_take() STASHED across the finally body so the - * { finally body } body's own pending checks answer for - * scr_rethrow(stash) themselves; a throw inside REPLACES the - * stash (it unwinds through the synthetic - * try.e: scope entry) — JS's semantics exactly - * - * 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. */ - private emitTryCatch(s: IrStmt & { kind: "tryCatch" }): void { - const B = this.B; - const hasCatch = s.catchBody !== null; - const hasFinally = s.finallyBody !== null; - const catchLabel = B.newLabel("try.c"); - const finExcLabel = B.newLabel("try.fx"); - const endLabel = B.newLabel("try.e"); - const afterTryLabel = hasFinally ? B.newLabel("try.f") : endLabel; - - const handler = { - label: hasCatch ? catchLabel : finExcLabel, - used: false, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - }; - if (hasFinally) { - this.finallyStack.push({ - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - tryDepth: this.tryStack.length, - body: s.finallyBody!, - }); - } - this.tryStack.push(handler); - this.emitBlock(s.tryBody); - this.tryStack.pop(); - B.br(afterTryLabel); // no-op when the try body already terminated - - // Exceptions raised in the CATCH body unwind to the exception-path - // finally (pending stays set through it) when one exists. - const excHandler = { - label: finExcLabel, - used: !hasCatch && handler.used, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - }; - - if (hasCatch && handler.used) { - B.startBlock(catchLabel); - if (hasFinally) this.tryStack.push(excHandler); - if (this.currentGenerator !== null) { - // Generator bodies: a pending GENRET sentinel (.return(v) - // injected at a yield) is a RETURN completion, not a throw — - // catch must not take it. Re-unwind past this handler (finally - // still runs — the unwind targets the exception-path finally or - // the enclosing context; the depths here equal the handler's). - this.declare(`declare zeroext i1 @scr_exc_genret_pending()`); - const gr = B.tmp(); - B.line(`${gr} = call zeroext i1 @scr_exc_genret_pending()`); - const lg = B.newLabel("try.gr"); - const lk = B.newLabel("try.gk"); - B.condBr(gr, lg, lk); - B.startBlock(lg); - this.emitUnwind(); - B.startBlock(lk); - } - if (s.catchLocalId !== null) { - // catch (e): the exception MOVES into the binding's snapshot box, - // owned by the catch body's scope (released on every exit — - // normal fall-through, jumps out, and unwinds from the body). - const slot = `%${mangleLocal(s.catchLocalId)}`; - this.declare(`declare ptr @scr_exc_take()`); - this.emitBlock(s.catchBody!, (scope) => { - const c = B.tmp(); - B.line(`${c} = call ptr @scr_exc_take() ; catch binding`); - if (this.currentLocals.get(s.catchLocalId!)?.boxed) { - const box = B.tmp(); - B.line(`${box} = ${boxNewCall(this.shapeHost, CAUGHT)} ; captured exception`); - this.boxSet(box, CAUGHT, c); - B.line(`store ptr ${box}, ptr ${slot}`); - scope.push({ slot, type: CAUGHT, boxed: true }); - } else { - B.line(`store ptr ${c}, ptr ${slot}`); - scope.push({ slot, type: CAUGHT }); - } - }); - } else { - this.declare(`declare void @scr_exc_clear()`); - B.line(`call void @scr_exc_clear() ; catch takes the exception`); - this.emitBlock(s.catchBody!); - } - if (hasFinally) this.tryStack.pop(); - B.br(afterTryLabel); // the catch's normal completion - } - if (hasFinally) this.finallyStack.pop(); - - if (hasFinally) { - B.startBlock(afterTryLabel); - this.emitBlock(s.finallyBody!); // normal path - B.br(endLabel); - if (excHandler.used) { - // The pending exception is STASHED across the finally body (a - // ScrCaught snapshot, re-raised after) so the body runs with a - // CLEAN cell — see stmts.ts's exception-path copy. The - // stash rides an alloca slot so a throw inside the body unwinds - // through the synthetic scope entry (replace semantics). - B.startBlock(finExcLabel); - this.declare(`declare ptr @scr_exc_take()`); - this.declare(`declare void @scr_rethrow(ptr)`); - this.declare(`declare void @scr_caught_release(ptr)`); - const stash = B.tmp(); - const stashSlot = B.slot(); - B.entryAllocas.push(`${stashSlot} = alloca ptr ; finally exception stash`); - B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`); - B.line(`store ptr ${stash}, ptr ${stashSlot}`); - this.scopes.push([{ slot: stashSlot, type: CAUGHT }]); - const suppressHandler = - s.suppressFinallyErrors || s.dropFinallyErrors - ? { - label: B.newLabel("try.fs"), - used: false, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - } - : null; - if (suppressHandler) this.tryStack.push(suppressHandler); - this.emitBlock(s.finallyBody!); - if (suppressHandler) this.tryStack.pop(); - this.scopes.pop(); // normal completion keeps the stash for the re-raise - B.line(`call void @scr_rethrow(ptr ${stash})`); - B.line(`call void @scr_caught_release(ptr ${stash})`); - this.emitUnwind(); - if (suppressHandler?.used) { - B.startBlock(suppressHandler.label); - const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress"; - this.declare(`declare void @${cleanup}(ptr)`); - B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`); - this.emitUnwind(); - } - } - B.startBlock(endLabel); - } else { - B.startBlock(endLabel); - } + emitTryCatch(s: IrStmt & { kind: "tryCatch" }): void { + return emitTryCatch(this, s); } - - /** JS-exact switch: lazily evaluated, arbitrary-expression case tests in - * source order, bodies falling through in source order until a break — - * Literal-only tests can use native dispatch; other tests keep a chain - * of conditional branches. All case bodies - * share ONE scope; because dispatch can jump PAST a varDecl into a later - * case, refcounted/boxed case-body locals are NULL-reset up front and - * the scope-exit releases rely on NULL tolerance. */ - private emitSwitch(s: IrStmt & { kind: "switch" }): void { - const B = this.B; - const discKind = s.disc.type.kind; - if (discKind !== "f64" && discKind !== "string" && discKind !== "bool") { - throw new LlvmUnsupportedError(`switch:${discKind}`, s.loc); - } - // The disc temp lives in the whole statement's frame: for a string - // discriminant it stays alive across every test and body, released - // when the switch statement ends (break lands past this statement's - // frame release — releaseForJump keeps the target's own frame). - const disc = this.emitExpr(s.disc); - for (const c of s.cases) { - for (const stmt of c.body) { - if (stmt.kind !== "varDecl") continue; - const local = this.currentLocals.get(stmt.localId)!; - if (local.boxed || isRefCounted(local.type)) { - B.line(`store ptr null, ptr %${mangleLocal(local.id)} ; case-scoped ${local.name}`); - } - } - } - const end = B.newLabel("sw.e"); - const caseLabels = s.cases.map(() => B.newLabel("sw.c")); - const defaultIdx = s.cases.findIndex((c) => c.test === null); - const fallback = defaultIdx >= 0 ? caseLabels[defaultIdx]! : end; - if (!emitLiteralSwitch(this, disc, s.disc, s.cases, caseLabels, fallback)) { - s.cases.forEach((c, i) => { - if (c.test === null) return; - // Lazy source-order test evaluation (a test after the match never - // runs). Each test's temps release right after its comparison. - this.frames.push([]); - const t = this.emitExpr(c.test); - const hit = B.tmp(); - if (c.test.kind === "strLit") { - B.line(`${hit} = call zeroext i1 ${this.literalEquals(c.test.value)}(ptr ${disc.name})`); - } else if (c.test.type.kind === "string") { - this.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`); - B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${disc.name}, ptr ${t.name})`); - } else if (c.test.type.kind === "bool") { - B.line(`${hit} = icmp eq i1 ${disc.name}, ${t.name}`); - } else { - B.line(`${hit} = fcmp oeq double ${disc.name}, ${t.name}`); - } - this.releaseTemps(this.frames.pop()!); - const next = B.newLabel("sw.t"); - B.condBr(hit, caseLabels[i]!, next); - B.startBlock(next); - }); - B.br(fallback); - } - - this.jumpTargets.push({ - kind: "switch", - brkLabel: end, - contLabel: null, - labels: s.labels, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - finallyDepth: this.finallyStack.length, - }); - const scope: LlScopeEntry[] = []; - this.scopes.push(scope); - s.cases.forEach((c, i) => { - B.br(caseLabels[i]!); // the previous body's natural fall-through - B.startBlock(caseLabels[i]!); - this.emitStmts(c.body); - }); - this.jumpTargets.pop(); - this.scopes.pop(); - // Natural fall-off of the last body releases the shared scope; a jump - // already released it before jumping. - const lastBody = s.cases[s.cases.length - 1]?.body; - if (!lastBody || !endsWithJump(lastBody)) this.releaseScope(scope); - B.br(end); - B.startBlock(end); + emitSwitch(s: IrStmt & { kind: "switch" }): void { + return emitSwitch(this, s); } /** Evaluates a condition (IR conds are bool-typed) and releases its * temps BEFORE the branch — safe because the result is a scalar i1, and * required in loop-condition blocks (their temps must not survive into * later blocks across the back edge). */ - private emitCondition(cond: IrExpr): string { + emitCondition(cond: IrExpr): string { const v = this.emitExpr(cond); const frame = this.currentFrame(); this.releaseTemps(frame); @@ -9901,7 +4535,7 @@ export class LlEmitter { return this.canBorrowCallArgument(value); } - private emitDiscarded(e: IrExpr): void { + emitDiscarded(e: IrExpr): void { // A statement-position yield never reads the resumed value: it stays // in the IN slot until the next resume replaces it. if (e.kind === "yieldExpr" && e.captureCompletion === undefined && e.type.kind !== "void") { @@ -9956,81 +4590,19 @@ export class LlEmitter { ): LlValue { return emitStringExpr(this, e); } - - /** - * Lower canonical `target = target + suffix` after evaluating the old - * left side and suffix in JavaScript order. The snapshot stays owned by - * the statement frame while the destination relinquishes its CURRENT - * value, making the snapshot unique unless a real observable alias exists. - */ emitStringSelfConcatAssign( localId: string, left: IrExpr, suffix: IrExpr, retainForYield: boolean, ): LlValue { - const moved = this.emitMovedSelfConcat(localId, left, suffix, retainForYield); - if (moved) return moved; - const snapshot = this.emitExpr(left); - // A suffix of several parts, or a number, appends each operand to the - // snapshot directly (in place once the binding lets go of it) instead - // of first building the suffix as a separate string. - const parts = stringParts(suffix); - const mixed = parts.length > 1 || numberPart(parts[0]!) !== null; - const charCode = mixed ? null : this.emitSingleCharCode(suffix); - const inputs = mixed ? emitConcatInputs(this, parts) : []; - // The concat only reads the suffix; relinquishing the binding below - // runs no code that could rebind a borrowed local. - const right = mixed || charCode !== null ? null : emitStringInputs(this, [suffix])[0]!; - const b = this.binding(localId); - const B = this.B; - if (b.kind === "boxed") { - // set_ref(NULL) unlinks then releases the binding's post-suffix value. - this.boxSet(this.loadBox(b.slot), b.type, "null"); - } else { - const old = B.tmp(); - B.line(`${old} = load ptr, ptr ${b.slot}`); - B.line(`store ptr null, ptr ${b.slot}`); - if (b.kind === "global") this.releaseHeap(old, b.type); - else this.releaseFrame(old, b.type); - } - let raw: string; - if (charCode !== null) { - // The snapshot moves into the consuming character append. - this.toFrame(snapshot); - this.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`); - raw = B.tmp(); - B.line( - `${raw} = call ptr @scr_str_concat_move_char_code(ptr ${snapshot.name}, double ${charCode.name})`, - ); - } else if (right === null) { - raw = emitMixedConcat(this, snapshot.name, inputs); - } else { - this.declare(`declare ptr @scr_str_concat(ptr, ptr)`); - raw = B.tmp(); - B.line(`${raw} = call ptr @scr_str_concat(ptr ${snapshot.name}, ptr ${right.name})`); - } - const result = this.own({ name: raw, type: left.type }); - if (retainForYield) { - const stored = - b.kind === "local" - ? this.retainFrame(result.name, result.type) - : this.retainHeap(result.name, result.type); - if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, stored); - else B.line(`store ptr ${stored}, ptr ${b.slot}`); - } else { - if (b.kind === "local") this.toFrame(result); - else this.toHeap(result); - if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, result.name); - else B.line(`store ptr ${result.name}, ptr ${b.slot}`); - } - return result; + return emitStringSelfConcatAssign(this, localId, left, suffix, retainForYield); } /** The code of a one-argument `String.fromCharCode(c)` suffix, emitted, so * `s += String.fromCharCode(c)` appends the character without building * the one-character string; null for any other suffix. */ - private emitSingleCharCode(suffix: IrExpr): LlValue | null { + emitSingleCharCode(suffix: IrExpr): LlValue | null { if (suffix.kind !== "libCall" || suffix.fn !== "string.fromCharCode") return null; const packed = suffix.args[0]; if (packed?.kind !== "arrayLit" || packed.elems.length !== 1 || packed.spreads?.length) @@ -10047,7 +4619,7 @@ export class LlEmitter { * copying the whole string on every append. Returns false (emitting * nothing) for every other store. */ - private emitFieldSelfConcat(s: Extract): boolean { + emitFieldSelfConcat(s: Extract): boolean { const suffix = matchFieldSelfConcat(s); if (!suffix || s.value.kind !== "strConcat") return false; let fieldType: IrType | undefined; @@ -10114,7 +4686,7 @@ export class LlEmitter { * the snapshot, the array lets go of it (emitDenseSlotUnlink) so the * concat can append in place, and the result is stored as usual. */ - private emitArraySelfConcat(s: Extract): boolean { + emitArraySelfConcat(s: Extract): boolean { const value = s.value; if ( value.kind !== "strConcat" || @@ -10165,7 +4737,7 @@ export class LlEmitter { * expressions): reading the binding after the suffix gives the value the * left side had, so it moves into a consuming concat, and the result takes * its place. Null when the general snapshot path is needed. */ - private emitMovedSelfConcat( + emitMovedSelfConcat( localId: string, left: IrExpr, suffix: IrExpr, @@ -10434,7 +5006,7 @@ export class LlEmitter { /** Lifetime regions in modules with lifetimeRegions (llvm/regions.ts). * Created with the class graph; null elsewhere. */ - private regions: LifetimeRegions | null = null; + regions: LifetimeRegions | null = null; /** LifetimeRegion's runtime calls: create answers the region id, enter * and leave bracket run(), release condemns. */ @@ -10465,10 +5037,10 @@ export class LlEmitter { return { name: "", type: e.type }; } - private publishWalkersCache: PublishWalkers | null = null; + publishWalkersCache: PublishWalkers | null = null; /** Per-type publication walkers, created on the first publish. */ - private get publishWalkers(): PublishWalkers { + get publishWalkers(): PublishWalkers { if (this.publishWalkersCache !== null) return this.publishWalkersCache; const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre); this.publishWalkersCache = new PublishWalkers({ @@ -10553,7 +5125,7 @@ export class LlEmitter { private publishedNamesSym: string | null = null; /** The class-name table the hierarchy field guards index by preorder. */ - private publishedClassNamesDefs(): string[] { + publishedClassNamesDefs(): string[] { if (this.publishedNamesSym === null) return []; const metas = [...this.classMeta.values()]; const size = metas.reduce((max, m) => Math.max(max, m.pre + 1), 0); @@ -10567,7 +5139,7 @@ export class LlEmitter { /** The frozen-object guard of a class field store, unless the store * provably targets a still-private `this` (PublishedTypes.privateStores). */ - private emitStoreGuard(obj: string, s: Extract): void { + emitStoreGuard(obj: string, s: Extract): void { if (this.publishedTypes?.privateStores.has(s) === true) return; this.emitPublishedFieldGuard(obj, s.className, s.field); } @@ -10659,153 +5231,12 @@ export class LlEmitter { ): LlValue { return emitAsyncExpr(this, e); } - - /** Read a receiver whose value is consumed immediately by a field or tag - * load. No user code may run between this read and its consumer. Locals - * and their projections already have an owner; other expressions keep - * their ordinary statement-frame ownership. The caller must retain any - * reference result before evaluating another expression. */ emitReadReceiver(e: IrExpr): LlValue { - if (!isRefCounted(e.type)) return this.emitExpr(e); - if (e.kind === "strLit") return { name: this.internLiteral(e.value), type: e.type }; - const guarded = guardedValue(e); - if (guarded !== null) { - // Throwing guards run in place, exactly as the sequence emits them. - for (const s of (e as IrExpr & { kind: "seqExpr" }).stmts) this.emitStmt(s); - return this.emitReadReceiver(guarded); - } - if (e.kind === "varRef") { - this.materializeSplitLocal(e.localId); - const binding = this.binding(e.localId); - if (binding.kind === "boxed") { - // A projection consumes this pointer before any user code runs. - // Keep TDZ checks and caught-value conversion on the owned path. - if (!this.canBorrowReceiver(e)) return this.emitExpr(e); - const box = this.loadBox(binding.slot); - const payload = this.B.tmp(); - const value = this.B.tmp(); - this.B.line(`${payload} = getelementptr inbounds %ScrBox, ptr ${box}, i32 0, i32 5`); - this.B.line(`${value} = load ptr, ptr ${payload}`); - return { name: value, type: e.type }; - } - if (binding.kind === "global") this.checkGlobalTdz(e.localId); - const value = this.B.tmp(); - this.B.line(`${value} = load ptr, ptr ${binding.slot}`); - return { name: value, type: e.type }; - } - if (e.kind === "dynCheck" && e.exact) return this.emitExactCapsuleObject(e.value, e.type); - if (e.kind === "chainRecv") { - // The chain's bind slot holds its receiver unchanged until the body - // finishes, owned by the slot itself or by the chain's receiver. - const bound = this.chainSlots.get(e.id); - if (!bound) - throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`); - const value = this.B.tmp(); - this.B.line(`${value} = load ${this.llType(bound.type)}, ptr ${bound.name}`); - return { name: value, type: e.type }; - } - if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) { - // The array keeps the element alive until the consumer; a missing - // element still traps exactly like the owned read. - const slot = this.B.slot(); - this.B.entryAllocas.push(`${slot} = alloca ptr`); - emitBorrowedArrayRead(this, e, slot); - const value = this.B.tmp(); - this.B.line(`${value} = load ptr, ptr ${slot}`); - return { name: value, type: e.type }; - } - const wrap = this.borrowableNullableWrap(e); - if (wrap === "unit" && e.kind === "unionWrap") - return { name: this.unitInstanceRef(e.unionId, e.tag), type: e.type }; - if (wrap === "ref" && e.kind === "unionWrap") - return { name: this.emitReadReceiver(e.value).name, type: e.type }; - const read = matchMapRead(e, this.boxedUnionsById); - if (read) { - const result = emitStackMapRead(this, read); - if (result.owner) this.ownSlot(result.owner.slot, result.owner.type); - return result.value; - } - // A borrowed-return helper (such as an optional element read) lends its - // result like any borrowed-return call. - if (e.kind === "call" && this.borrowedReturns.has(e.callee) && this.canBorrowReceiver(e)) - return emitBorrowedResultCall(this, e); - const arrayRead = this.optionalArrayReads.get(e); - if (arrayRead) return emitProjectedArrayRead(this, arrayRead); - const narrow = this.checkedNarrows.get(e); - const narrowed = narrow ? emitCheckedNarrowReceiver(this, narrow) : null; - if (narrowed) return narrowed; - if (canStackUnion(e, this.boxedUnionsById)) return emitStackUnion(this, e).value; - if (e.kind === "unionNarrow") { - const union = this.emitUnionProjection(e.value); - emitUnionNarrowGuard(this, union.name, e.unionId, e.tag); - return { name: this.unionPeek(union.name, e.unionId), type: e.type }; - } - if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") { - // Prefix layouts: a class cast reads the operand's own pointer; a - // downcast verifies its target class (narrow-guards.ts). - const receiver = this.emitReadReceiver(e.value); - if (e.kind === "downcast" && e.type.kind === "object") - emitDowncastGuard(this, receiver.name, e.value.type.className, e.type.className); - return { name: receiver.name, type: e.type }; - } - // A nullable-pointer field has no box to borrow. Projections use - // emitUnionProjection; other borrowers get an owned heap box. - if (this.nullableFieldGet(e)) return this.emitExpr(e); - if (e.kind === "recordGet" || e.kind === "fieldGet") { - const receiver = this.emitReadReceiver(e.obj); - if (e.kind === "recordGet") { - // An inline union field answers a private stack box. - const { ptr, type, inline } = this.recordFieldPtr(receiver.name, e.shapeId, e.field); - return { name: this.loadRecordField(ptr, type, inline), type: e.type }; - } - const { ptr, type, inline } = this.classFieldPtr(receiver.name, e.className, e.field); - if (this.int32Slots.isField(e.className, e.field)) return this.loadInt32Field(ptr, e.type); - return { name: this.loadField(ptr, type, inline), type: e.type }; - } - if (e.kind === "ternary" && this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_)) { - // Checked projections lower to a ternary with an always-throwing - // arm. Keep that check at the use, but borrow the successful arm. - const B = this.B; - const cond = this.emitExpr(e.cond); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ptr`); - const yes = B.newLabel("read.then"), - no = B.newLabel("read.else"), - join = B.newLabel("read.join"); - B.condBr(cond.name, yes, no); - for (const [label, value] of [ - [yes, e.then], - [no, e.else_], - ] as const) { - B.startBlock(label); - this.frames.push([]); - const result = this.emitReadReceiver(value); - B.line(`store ptr ${result.name}, ptr ${slot}`); - this.releaseTemps(this.frames.pop()!); - B.br(join); - } - B.startBlock(join); - const value = B.tmp(); - B.line(`${value} = load ptr, ptr ${slot}`); - return { name: value, type: e.type }; - } - // Scalar optional values and register results answer a private stack - // box. A ternary of borrowable arms above keeps lending its arm. - if ( - (e.kind === "optChain" || - e.kind === "nullish" || - e.kind === "ternary" || - e.kind === "arrIntrinsic") && - isScalarOptionalSource(e, this.unionSources) - ) - return emitScalarOptional(this, e); - if (e.kind === "call" && this.scalarResultCall(e)) return emitCallExpr(this, e, false, true); - if (e.kind === "call" && this.canBorrowReceiver(e)) return emitBorrowedResultCall(this, e); - return this.emitExpr(e); + return emitReadReceiver(this, e); } /** The frame slot a reassigned stack-object local keeps for its value. */ - private stackObjectSlot(localId: string, type: IrType): string { + stackObjectSlot(localId: string, type: IrType): string { let slot = this.stackObjectSlots.get(localId); if (slot === undefined) { slot = this.B.slot(); @@ -10814,24 +5245,11 @@ export class LlEmitter { } return slot; } - - /** A private stack union source with an independently owned payload. */ canStackReceiver(e: IrExpr): boolean { - return this.isStackUnionSource(e); + return canStackReceiver(this, e); } - - /** True for union sources that emitReadReceiver can produce as a private - * stack box with an independently owned payload (no heap union). */ isStackUnionSource(e: IrExpr): boolean { - if (e.type.kind !== "union") return false; - return ( - canStackUnion(e, this.boxedUnionsById) || - matchMapRead(e, this.boxedUnionsById) !== null || - this.optionalArrayReads.get(e) !== null || - isScalarOptionalSource(e, this.unionSources) || - this.scalarResultCall(e) || - this.inlineFieldGet(e) - ); + return isStackUnionSource(this, e); } /** A read of a record or class field that stores its scalar union @@ -11007,77 +5425,13 @@ export class LlEmitter { this.B.line(`${object} = load ptr, ptr ${slot}`); return { name: object, type }; } - canBorrowReceiver(e: IrExpr): boolean { - switch (e.kind) { - case "chainRecv": - return this.chainSlots.has(e.id); - case "varRef": { - const binding = this.binding(e.localId); - return ( - binding.kind !== "boxed" || - (!binding.local!.tdz && - binding.type.kind === e.type.kind && - e.type.kind !== "caught" && - isRefCounted(e.type)) - ); - } - case "unionNarrow": - // Class casts reinterpret the same pointer (prefix layouts). - case "downcast": - case "upcast": - return this.canBorrowReceiver(e.value); - // An inline union field has no box to lend: projections read it - // through emitReadReceiver's private stack box. - case "fieldGet": - case "recordGet": - return !this.inlineFieldGet(e) && this.canBorrowReceiver(e.obj); - case "ternary": - return this.canBorrowReceiver(e.then) && this.canBorrowReceiver(e.else_); - case "seqExpr": { - const guarded = guardedValue(e); - return guarded !== null && this.canBorrowReceiver(guarded); - } - // The runtime always sets a pending exception; its unreachable - // typed dummy is null and owns no receiver. - case "libCall": - return e.fn === "error.nodeThrow"; - case "unionWrap": { - const wrap = this.borrowableNullableWrap(e); - return wrap === "unit" || (wrap === "ref" && this.canBorrowReceiver(e.value)); - } - // A borrowed-return result stays reachable from its borrowed - // arguments until the consumer runs. Each argument is lent without a - // snapshot, so neither the callee nor a later argument may replace - // what it was read from: a callee that preserves every edge cannot, - // and any other one must leave the argument's locations unwritten. - case "call": { - if (!this.borrowedReturns.has(e.callee)) return false; - if (!e.args.every((arg) => this.referenceEffects.preserves(arg))) return false; - const preserving = this.referenceEffects.functions.has(e.callee); - if (preserving) - return e.args.every((arg) => !isRefCounted(arg.type) || this.canBorrowReceiver(arg)); - if (this.referenceWrites === null) return false; - const lent = borrowableInputs(this, e.args, false, this.referenceWrites.summary(e.callee)); - return e.args.every((arg, i) => !isRefCounted(arg.type) || lent[i] === true); - } - // A required element read: the (borrowed) array owns the element - // while an edge-preserving index computes. - case "arrayGet": - return ( - e.arr.type.kind === "array" && - isRefCounted(e.arr.type.elem) && - this.canBorrowReceiver(e.arr) && - this.referenceEffects.preserves(e.index) - ); - default: - return false; - } + return canBorrowReceiver(this, e); } /** A reference held as one plain pointer that emitReadReceiver can read - * without owning: class instances, records, arrays, strings and - * nullable-pointer unions. Tagged union boxes, closures and dynamic + * without owning: class instances, records, arrays, strings, typed arrays + * and nullable-pointer unions. Tagged union boxes, closures and dynamic * values keep ordinary ownership. */ plainReference(type: IrType): boolean { switch (type.kind) { @@ -11085,6 +5439,7 @@ export class LlEmitter { case "record": case "array": case "string": + case "bytes": return true; case "union": return this.nullableUnions.has(type.unionId); @@ -11092,65 +5447,8 @@ export class LlEmitter { return false; } } - - /** Functions returning a walk of their borrowed parameters return it - * without a reference (+0) from their borrowing body; direct callers - * either consume it as a receiver/walk or retain it at once, and the owned - * adapter retains before releasing its parameters. Candidates preserve - * every heap edge (so the result stays reachable from the arguments), - * borrow every reference parameter and return plain pointers. The set - * grows to a fixpoint because a call to a borrowed-return function with - * walk arguments is itself a walk; walk facts are then recomputed with - * the final set, admitting only parameters whose convention borrows. */ private analyzeBorrowedReturns(): void { - const pointer = (type: IrType): boolean => this.plainReference(type); - const borrowedIds = (fn: IrFunction): Set => { - const indexes = this.callLifetimes.borrowed.get(fn.name); - return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId)); - }; - const hostFor = (fn: IrFunction): WalkBorrowHost => { - const allowed = borrowedIds(fn); - const host = { - pointerLocal: (local: IrLocal) => pointer(local.type), - borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e), - borrowedReturn: (callee: string) => this.borrowedReturns.has(callee), - parameterAllowed: (id: string) => allowed.has(id), - }; - return this.referenceEffects.functions.has(fn.name) ? host : this.unwrittenWalkHost(fn, host); - }; - // With ReferenceWrites, a body that writes none of its walks' locations - // qualifies like one that preserves every edge. - const preserving = [...this.fnByName.values()].filter( - (fn) => this.referenceEffects.functions.has(fn.name) || this.referenceWrites !== null, - ); - const candidates = preserving.filter((fn) => { - if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures) return false; - // A borrowing vtable slot dispatches to the borrowing body (or its - // virtual adapter) directly, and virtual callers own the result. - if (this.implSlotBorrowed(fn.name).size > 0) return false; - const indexes = this.callLifetimes.borrowed.get(fn.name); - return ( - indexes !== undefined && - indexes.size > 0 && - fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i)) - ); - }); - for (let changed = true; changed;) { - changed = false; - for (const fn of candidates) { - if (this.borrowedReturns.has(fn.name)) continue; - if (!analyzeWalks(fn, hostFor(fn)).returnsWalk) continue; - this.borrowedReturns.add(fn.name); - // Nested borrowed returns extend the locations a caller depends on. - this.borrowedReturnKeyCache.clear(); - changed = true; - } - } - if (this.borrowedReturns.size === 0) return; - for (const fn of preserving) { - const walks = analyzeWalks(fn, hostFor(fn)).locals; - if (walks.size > 0) this.walkBorrowsByFunction.set(fn.name, walks); - } + return analyzeBorrowedReturns(this); } /** emitReadReceiver produces this node's pointer without acquiring a @@ -11462,7 +5760,7 @@ export class LlEmitter { this.B.line(`store i32 ${integer}, ptr ${slot}`); } - private emitBytesSet( + emitBytesSet( elem: IrBytesElem, receiver: string, index: LlValue, @@ -11601,7 +5899,7 @@ export class LlEmitter { /** The adopting await a `return` value resolves through, if any: the value * itself or the operand of its conversion into the declared result. */ -function adoptingAwaitOf(value: IrExpr | null): ExprOf<"awaitExpr"> | null { +export function adoptingAwaitOf(value: IrExpr | null): ExprOf<"awaitExpr"> | null { if (value === null) return null; let found: ExprOf<"awaitExpr"> | null = null; everyExpr(value, { diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index ac025df09..485b7a6b3 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -8,6 +8,7 @@ import { exactInteger, widenInteger } from "./integer-values.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { emitByteNumber } from "./byte-numbers.js"; import { integerOrNaNRange } from "./wrapping-sums.js"; +import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js"; const BYTES_NUM_KIND: Record = { u8: { kind: 0, le: false }, @@ -93,10 +94,7 @@ export function emitBytesIndex( B.line(`${narrow} = trunc i64 ${wide} to ${host.sizeType}`); return narrow; } - const lenPtr = B.tmp(); - const len = B.tmp(); - B.line(`${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`); - B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}`); + const len = emitBytesHeaderLength(host, receiver); if (integer) { const wideIndex = widenInteger(host, integer); const wideLength = host.sizeType === "i64" ? len : B.tmp(); @@ -145,11 +143,20 @@ export function emitBytesIndex( return idx; } +/** The element count of a typed array, in the target's size type. */ +export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string { + const p = host.B.tmp(); + const len = host.B.tmp(); + host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`); + host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`); + return len; +} + export function emitBytesData(host: LlvmEmitterContext, receiver: string): string { const p = host.B.tmp(); const data = host.B.tmp(); host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 3`); - host.B.line(`${data} = load ptr, ptr ${p}`); + host.B.line(`${data} = load ptr, ptr ${p}${bytesHeaderAlias(host)}`); return data; } @@ -160,10 +167,7 @@ export function emitBytesLength( bytes: boolean, ): LlValue { const B = host.B; - const p = B.tmp(); - const len = B.tmp(); - B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`); - B.line(`${len} = load ${host.sizeType}, ptr ${p}`); + const len = emitBytesHeaderLength(host, receiver); const size = BYTES_ELEMENT_SIZE[elem]; const count = bytes && size !== 1 ? B.tmp() : len; if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`); @@ -187,7 +191,7 @@ function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): strin 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}`); + 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"), @@ -257,9 +261,15 @@ function emitBytesLoad( inBounds: boolean, invalid: string | undefined, ): LlValue { + if (host.mod.workers) { + const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds); + return emitSharedOrLocalLoad(host, elem, receiver, idx); + } + // The header is immutable, so the data pointer loads beside the length, + // ahead of the bounds branch: an unconditional load LICM can hoist. + const data = host.emitBytesData(receiver); const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds); - if (host.mod.workers) return emitSharedOrLocalLoad(host, elem, receiver, idx); - return emitBytesLoadAt(host, elem, receiver, idx); + return emitBytesLoadAt(host, elem, data, idx); } /** Worker programs: the element at a checked index of a view that may be @@ -285,7 +295,7 @@ function emitSharedOrLocalLoad( const sharedDone = B.newLabel("bytes.shared.done"); B.br(sharedDone); B.startBlock(local); - const own = emitBytesLoadAt(host, elem, receiver, idx); + const own = emitBytesLoadAt(host, elem, host.emitBytesData(receiver), idx); const localDone = B.newLabel("bytes.local.done"); B.br(localDone); B.startBlock(localDone); @@ -306,15 +316,15 @@ function emitSharedOrLocalLoad( return { name: out, type: F64, uint32: u32 }; } -/** The element at a checked index of a thread-owned view. */ +/** The element at a checked index of a thread-owned view, given its data + * pointer. */ function emitBytesLoadAt( host: LlvmEmitterContext, elem: IrBytesElem, - receiver: string, + data: string, idx: string, ): LlValue { const B = host.B; - const data = host.emitBytesData(receiver); const p = B.tmp(); if (elem === "f16") { // Half-precision bits widen exactly; the runtime converts them. @@ -322,7 +332,7 @@ function emitBytesLoadAt( const raw = B.tmp(); const out = B.tmp(); B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`${raw} = load i16, ptr ${p}, align 1`); + B.line(`${raw} = load i16, ptr ${p}, align 1${bytesElementAlias(host)}`); B.line(`${out} = call double @scr_f16_to_f64(i16 zeroext ${raw})`); return { name: out, type: F64 }; } @@ -333,7 +343,7 @@ function emitBytesLoadAt( const wide = B.tmp(); const out = B.tmp(); B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`${raw} = load i${bits}, ptr ${p}, align 1`); + B.line(`${raw} = load i${bits}, ptr ${p}, align 1${bytesElementAlias(host)}`); B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`); B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`); return { name: out, type: F64, uint32: wide }; @@ -342,20 +352,20 @@ function emitBytesLoadAt( const raw = B.tmp(); const out = B.tmp(); B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`${raw} = load float, ptr ${p}, align 1`); + B.line(`${raw} = load float, ptr ${p}, align 1${bytesElementAlias(host)}`); B.line(`${out} = fpext float ${raw} to double`); return { name: out, type: F64 }; } if (elem === "f64") { const out = B.tmp(); B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`${out} = load double, ptr ${p}, align 1`); + B.line(`${out} = load double, ptr ${p}, align 1${bytesElementAlias(host)}`); return { name: out, type: F64 }; } const raw = B.tmp(); const out = B.tmp(); B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`${raw} = load i32, ptr ${p}, align 1`); + B.line(`${raw} = load i32, ptr ${p}, align 1${bytesElementAlias(host)}`); B.line(`${out} = ${elem === "i32" ? "sitofp" : "uitofp"} i32 ${raw} to double`); return { name: out, type: F64, uint32: raw }; } @@ -465,6 +475,7 @@ export function emitBytesSet( 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) { host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`); @@ -487,7 +498,6 @@ export function emitBytesSet( elem === "f16" || elem === "f32" || elem === "f64" ? null : host.emitToUint32(value.name, expr, value.uint32); - const data = host.emitBytesData(receiver); const p = B.tmp(); if (elem === "f16") { // One round-to-nearest-even step from the double, in the runtime. @@ -495,24 +505,24 @@ export function emitBytesSet( const narrowed = B.tmp(); B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`); B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store i16 ${narrowed}, ptr ${p}, align 1`); + B.line(`store i16 ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`); } else if (BYTES_ELEMENT_SIZE[elem] < 4) { const bits = BYTES_ELEMENT_SIZE[elem] * 8; const byte = B.tmp(); B.line(`${byte} = trunc i32 ${stored!} to i${bits}`); B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store i${bits} ${byte}, ptr ${p}, align 1`); + B.line(`store i${bits} ${byte}, ptr ${p}, align 1${bytesElementAlias(host)}`); } else if (elem === "f32") { const narrowed = B.tmp(); B.line(`${narrowed} = fptrunc double ${value.name} to float`); B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store float ${narrowed}, ptr ${p}, align 1`); + B.line(`store float ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`); } else if (elem === "f64") { B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store double ${value.name}, ptr ${p}, align 1`); + B.line(`store double ${value.name}, ptr ${p}, align 1${bytesElementAlias(host)}`); } else { B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store i32 ${stored!}, ptr ${p}, align 1`); + B.line(`store i32 ${stored!}, ptr ${p}, align 1${bytesElementAlias(host)}`); } B.br(done); B.startBlock(done); diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index d71df333a..db1dca1af 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -1,6 +1,13 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; -import { type IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js"; +import { + type IrType, + UNDEFINED_T, + isRefCounted, + isUnitType, + typeEquals, + typeKey, +} from "../../ir/ir.js"; import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js"; import { FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -68,6 +75,14 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp `define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`, `entry:`, ]; + if (from.kind === "void") { + // A Promise entry fulfills with undefined: the result's + // undefined arm, whatever the entry's payload slot holds. + const tag = tagOf(UNDEFINED_T); + d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``); + host.resolveThunkDefs.push(...d); + return sym; + } if (from.kind !== "union") { // One arm wrap, straight off the payload accessors. const tag = tagOf(from); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index a04f46f7b..0ca76e2d5 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -528,7 +528,7 @@ export function emitStableReceiver( if (b.kind !== "boxed") { if (b.kind === "global") host.checkGlobalTdz(receiver.localId); const value = host.B.tmp(); - host.B.line(`${value} = load ptr, ptr ${b.slot}`); + host.B.line(`${value} = load ptr, ptr ${b.slot}${host.slotAlias(receiver.localId)}`); return { name: value, type: receiver.type }; } } diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 35e759110..0d2cd90e8 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -1,7 +1,7 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js"; -import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; +import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; import { mangleRecordClone, mangleRecordNew } from "../mangle.js"; import { arrNewCall, elemAccess } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -116,7 +116,7 @@ export function emitLiteralExpr( return host.own({ name: host.adoptHeap(v, e.type), type: e.type }); } const t = B.tmp(); - B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}`); + B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}${host.slotAlias(e.localId)}`); const integerView = host.integerViews.get(e.localId); if (integerView !== undefined) { // Snapshot both representations before later operands can write @@ -558,6 +558,28 @@ export function regexLiteralTemplate( return `@${re.sym}`; } +/** A string equality between `s.charAt(i)` and a literal of exactly one + * UTF-16 code unit: the charAt call and the unit. */ +function charAtUnitTest( + e: ExprOf<"strEq">, +): { charAt: ExprOf<"strIntrinsic">; code: number } | null { + let text: string; + let other: IrExpr; + if (e.right.kind === "strLit") { + text = e.right.value; + other = e.left; + } else if (e.left.kind === "strLit") { + text = e.left.value; + other = e.right; + } else { + return null; + } + if (text.length !== 1) return null; + if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1) + return null; + return { charAt: other, code: text.charCodeAt(0) }; +} + export function emitStringExpr( host: LlvmEmitterContext, e: ExprOf< @@ -600,6 +622,26 @@ export function emitStringExpr( return host.own({ name: t, type: e.type }); } case "strEq": { + // `s.charAt(i) === "c"` (and `s[i]`, which lowers to charAt) for a + // one-unit literal compares the code unit: charCodeAt converts the + // index the same way, and an index out of range answers NaN, which + // equals no unit, as "" equals no one-unit literal. + const unit = charAtUnitTest(e); + if (unit !== null) { + const code = host.emitExpr({ + kind: "strIntrinsic", + method: "charCodeAt", + receiver: unit.charAt.receiver, + args: unit.charAt.args, + type: F64, + loc: unit.charAt.loc, + }); + const t = B.tmp(); + B.line( + `${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`, + ); + return { name: t, type: e.type }; + } const [l, r] = emitStringInputs(host, [e.left, e.right]); const eq = B.tmp(); // A literal side compares its length and bytes inline. diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 8a53b5fa8..134c74b92 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -5,10 +5,12 @@ import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; import { type IrRecordShape, type IrType, + CLASS_ATTRIBUTES, DYN_CLASS_PROPERTIES, RUNTIME_STREAM_CLASSES, SYMBOL_T, classDynViewSupported, + classFieldAttributesName, isClassOwnEnumerableFieldName, isDirentShape, isDynTypedRefType, @@ -766,6 +768,7 @@ export function streamTypedRefMaterializeAdapter( host.declare(`declare ptr @scr_stream_dyn_view(ptr)`); B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`); } else B.line(`${out} = call ptr @scr_dyn_new_obj()`); + const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES); if (meta.def.tracksOwnFields) { host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); @@ -835,9 +838,27 @@ export function streamTypedRefMaterializeAdapter( const nullable = host.nullableFields.get(t.className, field.name); if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + // Attributes copied from the bag do not stop the view taking the value. + const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; + if (withAttributes) + host.declare(`declare void @scr_dyn_obj_set_field(ptr, ptr, ${host.sizeType}, ptr)`); B.line( - `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, + `call void @${setter}(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, ); + // A field redefined through Object.defineProperty keeps its attributes. + const attributesField = classFieldAttributesName(field.name); + if (meta.def.fields.some((f) => f.name === attributesField)) { + host.declare(`declare void @scr_dyn_class_field_attributes(ptr, ptr, double)`); + const slot = B.tmp(), + bits = B.tmp(); + B.line( + `${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, attributesField).index}`, + ); + B.line(`${bits} = load double, ptr ${slot}`); + B.line( + `call void @scr_dyn_class_field_attributes(ptr ${out}, ptr ${host.internLiteral(field.name)}, double ${bits})`, + ); + } if (nullable) { host.declare(`declare void @scr_union_release(ptr)`); B.line(`call void @scr_union_release(ptr ${fieldValue})`); @@ -920,6 +941,17 @@ export function streamTypedRefMaterializeAdapter( B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`); B.line(`call void @scr_dyn_release_v(ptr ${copied})`); } + // A frozen, sealed or non-extensible instance's view says so. + if (withAttributes) { + host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`); + const slot = B.tmp(), + bits = B.tmp(); + B.line( + `${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`, + ); + B.line(`${bits} = load double, ptr ${slot}`); + B.line(`call void @scr_dyn_class_view_attributes(ptr ${out}, double ${bits})`); + } B.terminate(`ret ptr ${out}`); } else if (t.kind === "record") { const shape = host.recordsById.get(t.shapeId); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 31d7ca5c2..16759a592 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -83,12 +83,19 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue } // A signal that is not an AbortSignal throws ERR_INVALID_ARG_TYPE. if (mode & 8) host.emitPendingCheck(); + // readline/promises interfaces are instances of that module's class. + if (mode & 32) { + host.declare(`declare void @scr_rl_mark_promises(double)`); + B.line(`call void @scr_rl_mark_promises(double ${t})`); + } return { name: t, type: e.type }; } - if (e.fn === "rl.question") { + if (e.fn === "rl.question" || e.fn === "rl.questionDyn") { // The answer callback MOVES into the interface's registry; throws - // Node's use-after-close error (the may-throw seed). + // Node's use-after-close error (the may-throw seed), and with a + // checked-dynamic query that is not a string, ERR_INVALID_ARG_TYPE. host.usesTimers = true; + const fn = e.fn === "rl.question" ? "scr_rl_question" : "scr_rl_question_dyn"; const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func"); @@ -101,29 +108,37 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue ? "scr_rl_answer_thunk_dyn" : "scr_rl_answer_thunk_str"; host.declare(`declare void @${adapter}(ptr, ptr)`); - host.declare(`declare void @scr_rl_question(double, ptr, ptr, ptr)`); + host.declare(`declare void @${fn}(double, ptr, ptr, ptr)`); B.line( - `call void @scr_rl_question(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`, + `call void @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`, ); host.emitPendingCheck(); return { name: "", type: e.type }; } - if (e.fn === "rl.questionAsync" || e.fn === "rl.questionAsyncSignal") { - // The promise question never throws: closed interfaces and aborted - // signals reject the answered promise (+1). + if ( + e.fn === "rl.questionAsync" || + e.fn === "rl.questionAsyncSignal" || + e.fn === "rl.questionAsyncDyn" || + e.fn === "rl.questionAsyncSignalDyn" + ) { + // The promise question never throws: closed interfaces, aborted + // signals and queries that are not strings reject the answered + // promise (+1). host.usesTimers = true; const args = e.args.map((a) => host.emitExpr(a)); + const dyn = e.fn === "rl.questionAsyncDyn" || e.fn === "rl.questionAsyncSignalDyn"; + const fn = dyn ? "scr_rl_question_promise_dyn" : "scr_rl_question_promise"; let signal = "null"; let add = "null"; - if (e.fn === "rl.questionAsyncSignal") { + if (e.fn === "rl.questionAsyncSignal" || e.fn === "rl.questionAsyncSignalDyn") { signal = args[2]!.name; host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); add = "@scr_events_add_abort_listener"; } - host.declare(`declare ptr @scr_rl_question_promise(double, ptr, ptr, ptr)`); + host.declare(`declare ptr @${fn}(double, ptr, ptr, ptr)`); const t = B.tmp(); B.line( - `${t} = call ptr @scr_rl_question_promise(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`, + `${t} = call ptr @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`, ); return host.own({ name: t, type: e.type }); } diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 4798b1f09..358b21c4f 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -1097,6 +1097,30 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line(`${t} = call ptr @scr_str_from_char_code_one(double ${code.name})`); return host.own({ name: t, type: e.type }); } + if ( + e.fn === "string.fromCharCode" && + packed.kind === "arrayLit" && + packed.elems.length <= 16 && + !packed.spreads?.length + ) { + // A short argument list passes its codes in a stack array. + const codes = packed.elems.map((elem) => host.emitExpr(elem)); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca [${Math.max(codes.length, 1)} x double]`); + codes.forEach((code, i) => { + const p = B.tmp(); + B.line( + `${p} = getelementptr inbounds [${Math.max(codes.length, 1)} x double], ptr ${slot}, i64 0, i64 ${i}`, + ); + B.line(`store double ${code.name}, ptr ${p}`); + }); + host.declare(`declare ptr @scr_str_from_char_codes(ptr, ${host.sizeType})`); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_str_from_char_codes(ptr ${slot}, ${host.sizeType} ${codes.length})`, + ); + return host.own({ name: t, type: e.type }); + } // One packed f64[] (the frontend built it) or one bytes value (the // spread-typed-array form); +1 string. const base = diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 2d91386b3..294128107 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -294,9 +294,12 @@ export const LIB_FN_SYMS: Record = { "process.rusage": "scr_process_rusage", "process.activeResources": "scr_active_resources", "process.exiting": "scr_process_exiting", + "process.ipcOpen": "scr_process_ipc_open", "process.exit": "scr_process_exit", "process.setExitCode": "scr_process_set_exit_code", "process.currentExitCode": "scr_process_exit_code_or_zero", + "process.setExitCodeTracked": "scr_process_set_exit_code_tracked", + "process.hasExitCode": "scr_process_has_exit_code", "process.envSet": "scr_env_set", "process.envUnset": "scr_env_unset", "process.envPairs": "scr_env_pairs", @@ -827,6 +830,7 @@ export const LIB_FN_SYMS: Record = { "dyn.sameValue": "scr_dyn_same_value", "error.toString": "scr_error_to_string", "error.stack": "scr_error_stack", + "error.setStack": "scr_error_set_stack", "error.stackLimitGet": "scr_stack_limit_get", "error.stackLimitSet": "scr_stack_limit_set", "class.name": "scr_classobj_name", @@ -926,6 +930,12 @@ export const LIB_FN_SYMS: Record = { "dyn.objectTag": "scr_dyn_object_tag", "dyn.freeze": "scr_dyn_freeze", "dyn.isFrozen": "scr_dyn_is_frozen", + "class.defineField": "scr_class_define_field", + "class.definedBits": "scr_class_defined_bits", + "class.definedValue": "scr_class_defined_value", + "class.descriptorHasValue": "scr_class_descriptor_has_value", + "class.throwReadOnly": "scr_class_throw_read_only", + "class.throwNotExtensible": "scr_class_throw_not_extensible", "dyn.nativeSetNew": "scr_dyn_native_set_new", "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", @@ -994,6 +1004,7 @@ export const LIB_FN_SYMS: Record = { // family have non-generic shapes (adapter/variadic) — special-cased in // emitLibCall. "emitter.new": "scr_emitter_new", + "emitter.className": "scr_emitter_class_name", "emitter.ctor": "scr_emitter_init", "emitter.off": "scr_emitter_off", "emitter.offDyn": "scr_emitter_off_dyn", @@ -1039,6 +1050,7 @@ export const LIB_FN_SYMS: Record = { "rl.prompt": "scr_rl_prompt", "rl.promptWith": "scr_rl_prompt_with", "rl.setPrompt": "scr_rl_set_prompt", + "rl.setPromptDyn": "scr_rl_set_prompt_dyn", "rl.getPrompt": "scr_rl_get_prompt", "rl.write": "scr_rl_write", "rl.writeKey": "scr_rl_write_key", @@ -1054,6 +1066,9 @@ export const LIB_FN_SYMS: Record = { "rl.terminal": "scr_rl_terminal", "rl.line": "scr_rl_line", "rl.isTerminal": "scr_rl_is_terminal", + "rl.isPromises": "scr_rl_is_promises", + "rl.inputFromDyn": "scr_stream_rl_from_dyn", + "rl.outputFromDyn": "scr_stream_rl_from_dyn", "rl.cursor": "scr_rl_cursor", "rl.seq": "scr_rl_seq", "rl.moves": "scr_rl_moves", diff --git a/packages/compiler/src/backend/llvm/receivers.ts b/packages/compiler/src/backend/llvm/receivers.ts new file mode 100644 index 000000000..50b9485a1 --- /dev/null +++ b/packages/compiler/src/backend/llvm/receivers.ts @@ -0,0 +1,409 @@ +/* LlEmitter's receivers: receiver reads, and the analyses that decide when calls, receivers and returns borrow. */ +import type { LlEmitter } from "./emitter.js"; +import { + type IrExpr, + type IrFunction, + type IrLocal, + type IrType, + isRefCounted, +} from "../../ir/ir.js"; +import type { LlValue } from "./expr-context.js"; +import { guardedValue } from "./borrowed-receivers.js"; +import { InternalCompilerError } from "../../errors.js"; +import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js"; +import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js"; +import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js"; +import { emitCheckedNarrowReceiver } from "./checked-narrows.js"; +import { canStackUnion, emitStackUnion } from "./stack-unions.js"; +import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js"; +import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js"; +import { borrowableInputs } from "./borrowed-inputs.js"; +import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js"; + +/** Read a receiver whose value is consumed immediately by a field or tag + * load. No user code may run between this read and its consumer. Locals + * and their projections already have an owner; other expressions keep + * their ordinary statement-frame ownership. The caller must retain any + * reference result before evaluating another expression. */ +export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue { + if (!isRefCounted(e.type)) return emitter.emitExpr(e); + if (e.kind === "strLit") return { name: emitter.internLiteral(e.value), type: e.type }; + const guarded = guardedValue(e); + if (guarded !== null) { + // Throwing guards run in place, exactly as the sequence emits them. + for (const s of (e as IrExpr & { kind: "seqExpr" }).stmts) emitter.emitStmt(s); + return emitter.emitReadReceiver(guarded); + } + if (e.kind === "varRef") { + emitter.materializeSplitLocal(e.localId); + const binding = emitter.binding(e.localId); + if (binding.kind === "boxed") { + // A projection consumes this pointer before any user code runs. + // Keep TDZ checks and caught-value conversion on the owned path. + if (!emitter.canBorrowReceiver(e)) return emitter.emitExpr(e); + const box = emitter.loadBox(binding.slot); + const payload = emitter.B.tmp(); + const value = emitter.B.tmp(); + emitter.B.line(`${payload} = getelementptr inbounds %ScrBox, ptr ${box}, i32 0, i32 5`); + emitter.B.line(`${value} = load ptr, ptr ${payload}`); + return { name: value, type: e.type }; + } + if (binding.kind === "global") emitter.checkGlobalTdz(e.localId); + const value = emitter.B.tmp(); + emitter.B.line(`${value} = load ptr, ptr ${binding.slot}${emitter.slotAlias(e.localId)}`); + return { name: value, type: e.type }; + } + if (e.kind === "dynCheck" && e.exact) return emitter.emitExactCapsuleObject(e.value, e.type); + if (e.kind === "chainRecv") { + // The chain's bind slot holds its receiver unchanged until the body + // finishes, owned by the slot itself or by the chain's receiver. + const bound = emitter.chainSlots.get(e.id); + if (!bound) + throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`); + const value = emitter.B.tmp(); + emitter.B.line(`${value} = load ${emitter.llType(bound.type)}, ptr ${bound.name}`); + return { name: value, type: e.type }; + } + if (e.kind === "arrayGet" && emitter.canBorrowReceiver(e)) { + // The array keeps the element alive until the consumer; a missing + // element still traps exactly like the owned read. + const slot = emitter.B.slot(); + emitter.B.entryAllocas.push(`${slot} = alloca ptr`); + emitBorrowedArrayRead(emitter, e, slot); + const value = emitter.B.tmp(); + emitter.B.line(`${value} = load ptr, ptr ${slot}`); + return { name: value, type: e.type }; + } + const wrap = emitter.borrowableNullableWrap(e); + if (wrap === "unit" && e.kind === "unionWrap") + return { name: emitter.unitInstanceRef(e.unionId, e.tag), type: e.type }; + if (wrap === "ref" && e.kind === "unionWrap") + return { name: emitter.emitReadReceiver(e.value).name, type: e.type }; + const read = matchMapRead(e, emitter.boxedUnionsById); + if (read) { + const result = emitStackMapRead(emitter, read); + if (result.owner) emitter.ownSlot(result.owner.slot, result.owner.type); + return result.value; + } + // A borrowed-return helper (such as an optional element read) lends its + // result like any borrowed-return call. + if (e.kind === "call" && emitter.borrowedReturns.has(e.callee) && emitter.canBorrowReceiver(e)) + return emitBorrowedResultCall(emitter, e); + const arrayRead = emitter.optionalArrayReads.get(e); + if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead); + const narrow = emitter.checkedNarrows.get(e); + const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null; + if (narrowed) return narrowed; + if (canStackUnion(e, emitter.boxedUnionsById)) return emitStackUnion(emitter, e).value; + if (e.kind === "unionNarrow") { + const union = emitter.emitUnionProjection(e.value); + emitUnionNarrowGuard(emitter, union.name, e.unionId, e.tag); + return { name: emitter.unionPeek(union.name, e.unionId), type: e.type }; + } + if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") { + // Prefix layouts: a class cast reads the operand's own pointer; a + // downcast verifies its target class (narrow-guards.ts). + const receiver = emitter.emitReadReceiver(e.value); + if (e.kind === "downcast" && e.type.kind === "object") + emitDowncastGuard(emitter, receiver.name, e.value.type.className, e.type.className); + return { name: receiver.name, type: e.type }; + } + // A nullable-pointer field has no box to borrow. Projections use + // emitUnionProjection; other borrowers get an owned heap box. + if (emitter.nullableFieldGet(e)) return emitter.emitExpr(e); + if (e.kind === "recordGet" || e.kind === "fieldGet") { + const receiver = emitter.emitReadReceiver(e.obj); + if (e.kind === "recordGet") { + // An inline union field answers a private stack box. + const { ptr, type, inline } = emitter.recordFieldPtr(receiver.name, e.shapeId, e.field); + return { name: emitter.loadRecordField(ptr, type, inline), type: e.type }; + } + const { ptr, type, inline } = emitter.classFieldPtr(receiver.name, e.className, e.field); + if (emitter.int32Slots.isField(e.className, e.field)) + return emitter.loadInt32Field(ptr, e.type); + return { name: emitter.loadField(ptr, type, inline), type: e.type }; + } + if ( + e.kind === "ternary" && + emitter.canBorrowReceiver(e.then) && + emitter.canBorrowReceiver(e.else_) + ) { + // Checked projections lower to a ternary with an always-throwing + // arm. Keep that check at the use, but borrow the successful arm. + const B = emitter.B; + const cond = emitter.emitExpr(e.cond); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ptr`); + const yes = B.newLabel("read.then"), + no = B.newLabel("read.else"), + join = B.newLabel("read.join"); + B.condBr(cond.name, yes, no); + for (const [label, value] of [ + [yes, e.then], + [no, e.else_], + ] as const) { + B.startBlock(label); + emitter.frames.push([]); + const result = emitter.emitReadReceiver(value); + B.line(`store ptr ${result.name}, ptr ${slot}`); + emitter.releaseTemps(emitter.frames.pop()!); + B.br(join); + } + B.startBlock(join); + const value = B.tmp(); + B.line(`${value} = load ptr, ptr ${slot}`); + return { name: value, type: e.type }; + } + // Scalar optional values and register results answer a private stack + // box. A ternary of borrowable arms above keeps lending its arm. + if ( + (e.kind === "optChain" || + e.kind === "nullish" || + e.kind === "ternary" || + e.kind === "arrIntrinsic") && + isScalarOptionalSource(e, emitter.unionSources) + ) + return emitScalarOptional(emitter, e); + if (e.kind === "call" && emitter.scalarResultCall(e)) + return emitCallExpr(emitter, e, false, true); + if (e.kind === "call" && emitter.canBorrowReceiver(e)) return emitBorrowedResultCall(emitter, e); + return emitter.emitExpr(e); +} + +/** A local stable binding keeps its value alive while later arguments + * and the callee execute. A proven local union box cannot escape or be + * rebound during a call; its independent payload owner keeps it alive. + * Other writable bindings need an owned snapshot. */ +export function canBorrowCallArgument(emitter: LlEmitter, value: IrExpr): boolean { + if (value.kind === "strLit") return true; + // An exact capsule owns its instance while the capsule's binding holds it. + if (value.kind === "dynCheck" && value.exact) + return value.value.kind === "varRef" && emitter.canBorrowCallArgument(value.value); + // An inline union field has no box to lend. + if (emitter.inlineFieldGet(value)) return false; + // Class casts reinterpret the same pointer (prefix layouts). + if ((value.kind === "upcast" || value.kind === "downcast") && value.value.type.kind === "object") + return emitter.canBorrowCallArgument(value.value); + if (value.kind === "seqExpr") { + const guarded = guardedValue(value); + return guarded !== null && emitter.canBorrowCallArgument(guarded); + } + // A nullable-union wrap reinterprets its payload pointer; an immortal + // unit constant needs no owner at all. + const wrap = emitter.borrowableNullableWrap(value); + if (wrap === "unit") return true; + if (wrap === "ref" && value.kind === "unionWrap") + return emitter.canBorrowCallArgument(value.value); + // Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose + // failing arm always throws; the successful arm projects the same + // stable owner. The throwing arm's typed dummy owns nothing. + if (value.kind === "unionNarrow") return emitter.canBorrowCallArgument(value.value); + if (value.kind === "libCall") return value.fn === "error.nodeThrow"; + // A final field never changes outside constructors: while its object is + // borrowable, so is the field's value. Nullable-pointer fields have no + // box to lend and keep their owning path. + if ( + value.kind === "fieldGet" && + emitter.finalFieldBorrows && + isRefCounted(value.type) && + emitter.finalFields.isFinal(value.className, value.field) && + !emitter.nullableFieldGet(value) + ) + return emitter.canBorrowReceiver(value.obj) && emitter.canBorrowCallArgument(value.obj); + if (value.kind === "ternary") + return ( + emitter.canBorrowReceiver(value.then) && + emitter.canBorrowReceiver(value.else_) && + emitter.canBorrowCallArgument(value.then) && + emitter.canBorrowCallArgument(value.else_) + ); + // A construction-only field of a stable owner keeps its value until the + // owner dies: no later operand or callee can replace and release it. + if ((value.kind === "fieldGet" || value.kind === "recordGet") && emitter.immutableRead(value)) + return emitter.canBorrowCallArgument(value.obj); + // An optional chain's bound receiver stays unchanged until its body + // finishes (see emitReadReceiver). + if (value.kind === "chainRecv") return emitter.chainSlots.has(value.id); + if (value.kind !== "varRef") return false; + const binding = emitter.binding(value.localId); + // A module constant owns its value for the rest of the program. + if (binding.kind === "global") return emitter.stableGlobals.has(value.localId); + // So does the environment for a capture nothing ever rebinds. + if (binding.kind === "boxed") + return emitter.stableCaptureIds.has(value.localId) && emitter.canBorrowReceiver(value); + return ( + binding.kind === "local" && + binding.local !== undefined && + (!binding.local.mutable || + emitter.stableCallBindings.has(value.localId) || + emitter.borrowedParameters.has(value.localId) || + emitter.localUnionStorage.has(value.localId)) && + !binding.local.boxed && + !binding.local.tdz + ); +} + +export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean { + switch (e.kind) { + case "chainRecv": + return emitter.chainSlots.has(e.id); + case "varRef": { + const binding = emitter.binding(e.localId); + return ( + binding.kind !== "boxed" || + (!binding.local!.tdz && + binding.type.kind === e.type.kind && + e.type.kind !== "caught" && + isRefCounted(e.type)) + ); + } + case "unionNarrow": + // Class casts reinterpret the same pointer (prefix layouts). + case "downcast": + case "upcast": + return emitter.canBorrowReceiver(e.value); + // An inline union field has no box to lend: projections read it + // through emitReadReceiver's private stack box. + case "fieldGet": + case "recordGet": + return !emitter.inlineFieldGet(e) && emitter.canBorrowReceiver(e.obj); + case "ternary": + return emitter.canBorrowReceiver(e.then) && emitter.canBorrowReceiver(e.else_); + case "seqExpr": { + const guarded = guardedValue(e); + return guarded !== null && emitter.canBorrowReceiver(guarded); + } + // The runtime always sets a pending exception; its unreachable + // typed dummy is null and owns no receiver. + case "libCall": + return e.fn === "error.nodeThrow"; + case "unionWrap": { + const wrap = emitter.borrowableNullableWrap(e); + return wrap === "unit" || (wrap === "ref" && emitter.canBorrowReceiver(e.value)); + } + // A borrowed-return result stays reachable from its borrowed + // arguments until the consumer runs. Each argument is lent without a + // snapshot, so neither the callee nor a later argument may replace + // what it was read from: a callee that preserves every edge cannot, + // and any other one must leave the argument's locations unwritten. + case "call": { + if (!emitter.borrowedReturns.has(e.callee)) return false; + if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false; + const preserving = emitter.referenceEffects.functions.has(e.callee); + if (preserving) + return e.args.every((arg) => !isRefCounted(arg.type) || emitter.canBorrowReceiver(arg)); + if (emitter.referenceWrites === null) return false; + const lent = borrowableInputs( + emitter, + e.args, + false, + emitter.referenceWrites.summary(e.callee), + ); + return e.args.every((arg, i) => !isRefCounted(arg.type) || lent[i] === true); + } + // A required element read: the (borrowed) array owns the element + // while an edge-preserving index computes. + case "arrayGet": + return ( + e.arr.type.kind === "array" && + isRefCounted(e.arr.type.elem) && + emitter.canBorrowReceiver(e.arr) && + emitter.referenceEffects.preserves(e.index) + ); + default: + return false; + } +} + +/** Functions returning a walk of their borrowed parameters return it + * without a reference (+0) from their borrowing body; direct callers + * either consume it as a receiver/walk or retain it at once, and the owned + * adapter retains before releasing its parameters. Candidates preserve + * every heap edge (so the result stays reachable from the arguments), + * borrow every reference parameter and return plain pointers. The set + * grows to a fixpoint because a call to a borrowed-return function with + * walk arguments is itself a walk; walk facts are then recomputed with + * the final set, admitting only parameters whose convention borrows. */ +export function analyzeBorrowedReturns(emitter: LlEmitter): void { + const pointer = (type: IrType): boolean => emitter.plainReference(type); + const borrowedIds = (fn: IrFunction): Set => { + const indexes = emitter.callLifetimes.borrowed.get(fn.name); + return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId)); + }; + const hostFor = (fn: IrFunction): WalkBorrowHost => { + const allowed = borrowedIds(fn); + const host = { + pointerLocal: (local: IrLocal) => pointer(local.type), + borrowsWithoutOwning: (e: IrExpr) => emitter.borrowsWithoutOwning(e), + borrowedReturn: (callee: string) => emitter.borrowedReturns.has(callee), + parameterAllowed: (id: string) => allowed.has(id), + }; + return emitter.referenceEffects.functions.has(fn.name) + ? host + : emitter.unwrittenWalkHost(fn, host); + }; + // With ReferenceWrites, a body that writes none of its walks' locations + // qualifies like one that preserves every edge. + const preserving = [...emitter.fnByName.values()].filter( + (fn) => emitter.referenceEffects.functions.has(fn.name) || emitter.referenceWrites !== null, + ); + const candidates = preserving.filter((fn) => { + if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures) return false; + // A borrowing vtable slot dispatches to the borrowing body (or its + // virtual adapter) directly, and virtual callers own the result. + if (emitter.implSlotBorrowed(fn.name).size > 0) return false; + const indexes = emitter.callLifetimes.borrowed.get(fn.name); + return ( + indexes !== undefined && + indexes.size > 0 && + fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i)) + ); + }); + for (let changed = true; changed;) { + changed = false; + for (const fn of candidates) { + if (emitter.borrowedReturns.has(fn.name)) continue; + if (!analyzeWalks(fn, hostFor(fn)).returnsWalk) continue; + emitter.borrowedReturns.add(fn.name); + // Nested borrowed returns extend the locations a caller depends on. + emitter.borrowedReturnKeyCache.clear(); + changed = true; + } + } + if (emitter.borrowedReturns.size === 0) return; + for (const fn of preserving) { + const walks = analyzeWalks(fn, hostFor(fn)).locals; + if (walks.size > 0) emitter.walkBorrowsByFunction.set(fn.name, walks); + } +} + +export function fieldAliasMetadata(emitter: LlEmitter): string[] { + if (emitter.fieldAliasTags.size === 0) return []; + const lines = ['!0 = !{!"scriptc native fields"}']; + for (const tag of emitter.fieldAliasTags.values()) { + lines.push( + `!${tag - 1} = !{!"field.${tag}", !0, i64 0}`, + `!${tag} = !{!${tag - 1}, !${tag - 1}, i64 0}`, + ); + } + return lines; +} + +/** A private stack union source with an independently owned payload. */ +export function canStackReceiver(emitter: LlEmitter, e: IrExpr): boolean { + return emitter.isStackUnionSource(e); +} + +/** True for union sources that emitReadReceiver can produce as a private + * stack box with an independently owned payload (no heap union). */ +export function isStackUnionSource(emitter: LlEmitter, e: IrExpr): boolean { + if (e.type.kind !== "union") return false; + return ( + canStackUnion(e, emitter.boxedUnionsById) || + matchMapRead(e, emitter.boxedUnionsById) !== null || + emitter.optionalArrayReads.get(e) !== null || + isScalarOptionalSource(e, emitter.unionSources) || + emitter.scalarResultCall(e) || + emitter.inlineFieldGet(e) + ); +} diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index e632795c9..5ebb4796e 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -98,6 +98,218 @@ import { CacheInputsChangedError } from "./session.js"; import { ensureRuntimeObjects, stageRuntimeObjects, clearCcCaches } from "./runtime-objects.js"; import { localizeLibraryObjects } from "./object-merge.js"; +/** What the runtime-unit table reads: the build options, the target driver, + * the units other options imply, and the vendor inputs resolved for this + * build. */ +interface RuntimeUnitContext { + opts: CcOptions; + driver: ReturnType; + rt: (path: string) => string; + rtDir: string; + regex: boolean; + nativeFetch: boolean; + net: boolean; + http: boolean; + tls: boolean; + tlsCa: boolean; + cipher: boolean; + lreObjects: readonly string[]; + zlibObjects: readonly string[]; + tlsArchive: string | null; +} + +/** An optional runtime input: runtime sources compiled when `when` holds, + * or command-line arguments computed from the build. */ +interface RuntimeUnit { + when: (u: RuntimeUnitContext) => boolean | undefined; + sources: readonly string[]; + args?: (u: RuntimeUnitContext) => readonly string[]; +} + +const unit = (when: RuntimeUnit["when"], ...sources: string[]): RuntimeUnit => ({ when, sources }); +const unitArgs = (args: (u: RuntimeUnitContext) => readonly string[]): RuntimeUnit => ({ + when: () => true, + sources: [], + args, +}); + +/** The optional runtime inputs, in link order. The order is part of the + * executable: a new unit goes next to the units it references. */ +const RUNTIME_UNITS: readonly RuntimeUnit[] = [ + unit( + (u) => u.opts.workers, + "scr_worker.c", + "scr_worker_events.c", + "scr_mailbox.c", + "scr_message.c", + ), + unit((u) => u.opts.copying, "scr_copying.c"), + unit((u) => u.opts.fileHandle, "scr_file_handle.c"), + // win32 targets compile the libc-shim TU (stpcpy, arc4random_buf, + // gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares + // them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036, + // GetUserNameA), iphlpapi (GetAdaptersAddresses behind + // os.networkInterfaces), and ws2_32 (inet_ntop there; the socket + // units ride the same import). Never present on the default path, so + // the historical line cannot change. + unitArgs((u) => + targetPlatform(u.driver) === "win32" + ? [u.rt(join(u.rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"] + : [], + ), + // musl deliberately has no libc-identification predefine; resolveCc's + // SCR_MUSL flag and this target-selected TU travel together. + unit((u) => isMuslTarget(u.driver), "scr_musl.c"), + // scr_glob.c (path.matchesGlob) compiles its segments through the + // regex unit and scr_idna.c (node:url's IDNA) normalizes through it, + // so both ride the same switch; section GC drops them from binaries + // that never use them. + unitArgs((u) => + u.regex + ? [ + "-I", + vendorEngineDir(), + u.rt(join(u.rtDir, "scr_regex.c")), + u.rt(join(u.rtDir, "scr_glob.c")), + u.rt(join(u.rtDir, "scr_idna.c")), + ...u.lreObjects, + ] + : [], + ), + unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"), + unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"), + unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"), + unit((u) => u.opts.dc, "scr_dc.c"), + unit((u) => u.opts.dynTier, "scr_js_value.c"), + unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), + unit( + (u) => + u.opts.dynAsync || + u.opts.dynInvoke || + u.opts.workers || + u.opts.dc || + u.opts.fileHandle || + u.nativeFetch, + "scr_async_dyn.c", + ), + // The performance timeline (every perf_hooks user-timing call selects + // dynAsync, whose unit carries the timeline's buffer warning). + unit((u) => u.opts.dynAsync, "scr_perf.c"), + // Blob and File (every blob.* call selects dynAsync, whose unit boxes + // their reads as dyn promises). + unit((u) => u.opts.dynAsync, "scr_blob.c"), + // The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system + // libz on the default host-clang build, vendored objects on Zig builds) + // also serves the native fetch's gzip decoder — spread exactly once. + unitArgs((u) => + u.opts.zlib + ? isZigDriver(u.driver) + ? ["-I", vendorZlibDir(), u.rt(join(u.rtDir, "scr_zlib.c")), ...u.zlibObjects] + : [u.rt(join(u.rtDir, "scr_zlib.c"))] + : u.nativeFetch + ? isZigDriver(u.driver) + ? ["-I", vendorZlibDir(), ...u.zlibObjects] + : [] + : [], + ), + unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"), + // readline's terminal mode: rl.terminal sets the regex unit it + // measures with. + unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"), + unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"), + // The checked-dynamic HANDLE support unit (listener gate + runtime + // adapter closures): every referencing unit is one of the emitter or + // net families (http implies net), so handle-free binaries keep + // their exact size class. + unit((u) => u.opts.emitter || u.opts.workers || u.net, "scr_dyn_handle.c"), + unit((u) => u.opts.symbol, "scr_symbol.c"), + unit((u) => u.opts.assert && u.opts.bigint, "scr_bigint_assert.c"), + unit((u) => u.opts.qs, "scr_qs.c"), + // The legacy url unit references the regex, querystring and dyn-async + // units; the url.parse family's IR sets all three. + unit((u) => u.regex && u.opts.qs && u.opts.dynAsync, "scr_url_legacy.c"), + unit((u) => u.opts.parseArgs, "scr_util.c"), + unit((u) => u.opts.parseArgs && u.opts.symbol, "scr_util_compare.c"), + unit((u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync, "scr_util_style.c"), + unit( + (u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex, + "scr_util_callable.c", + ), + unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"), + // The zlib stream classes reference both the zlib engine and the + // stream unit. + unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"), + unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"), + // FileHandle streams reference both the stream unit and FileHandle. + unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"), + // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD, + // epoll on Linux, WSAPoll on Windows — each TU is empty off its + // platform, so all three link whenever a poller-using unit does and + // the others cost nothing (ws2_32 rides the unconditional win32 libs + // above). + unit((u) => u.net || u.opts.dgram, "scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c"), + unit((u) => u.net, "scr_net.c"), + unit((u) => u.http, "scr_http.c"), + unit((u) => u.opts.http2, "scr_http2.c"), + unit((u) => u.opts.dgram, "scr_dgram.c"), + unit((u) => u.opts.watch, "scr_watch.c"), + 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 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"), + unitArgs((u) => + u.tlsArchive + ? [ + "-I", + join(vendorTlsDir(), "include"), + ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), + ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), + u.tlsArchive, + // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). + // The unconditional win32 libs above do not carry it. + // Never present on the default path, so the historical TLS + // link line cannot change. + ...(targetPlatform(u.driver) === "win32" ? ["-lbcrypt"] : []), + ] + : [], + ), + // scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries. + // This is independent of the mbedTLS archive: getCACertificates-only + // programs need crypt32 too, while TLS programs imply the CA unit. + unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])), + // Fetch uses the native network, TLS, and HTTP runtime. + unit((u) => u.nativeFetch, "scr_fetch.c"), +]; + +/** The arguments of the optional runtime inputs, in link order. */ +function runtimeUnitArgs(u: RuntimeUnitContext): string[] { + return RUNTIME_UNITS.flatMap((entry) => + entry.args + ? [...entry.args(u)] + : entry.when(u) + ? entry.sources.map((source) => u.rt(join(u.rtDir, source))) + : [], + ); +} + +/** The runtime's optional compile-time definitions, in command-line order. + * Both the program build and the runtime-object flags use this list. */ +const RUNTIME_DEFINES: readonly { + define: string; + when: (opts: CcOptions) => boolean | undefined; +}[] = [ + { define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy }, + { define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace }, + { define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended }, + { define: "SCR_YIN", when: (opts) => opts.youngNursery }, +]; + +function runtimeDefineArgs(opts: CcOptions): string[] { + return RUNTIME_DEFINES.filter((d) => d.when(opts)).map((d) => `-D${d.define}`); +} + export async function compileCInternal( opts: CcOptions, cacheWarmOnly: boolean, @@ -444,10 +656,7 @@ export async function compileCInternal( ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"] : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, - ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), - ...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []), - ...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []), - ...(opts.youngNursery ? ["-DSCR_YIN"] : []), + ...runtimeDefineArgs(opts), "-fno-math-errno", // The runtime object model uses type-punned C: a hierarchy // upcast is a raw pointer cast, so one object's header (rc, vt) and @@ -465,146 +674,22 @@ export async function compileCInternal( "-I", rtDir, ...runtimeSources.map((f) => rt(join(rtDir, f))), - ...(opts.workers - ? ["scr_worker.c", "scr_worker_events.c", "scr_mailbox.c", "scr_message.c"].map((f) => - rt(join(rtDir, f)), - ) - : []), - ...(opts.copying ? [rt(join(rtDir, "scr_copying.c"))] : []), - ...(opts.fileHandle ? [rt(join(rtDir, "scr_file_handle.c"))] : []), - // win32 targets compile the libc-shim TU (stpcpy, arc4random_buf, - // gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares - // them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036, - // GetUserNameA), iphlpapi (GetAdaptersAddresses behind - // os.networkInterfaces), and ws2_32 (inet_ntop there; the socket - // units ride the same import). Never present on the default path, so - // the historical line cannot change. - ...(targetPlatform(driver) === "win32" - ? [rt(join(rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"] - : []), - // musl deliberately has no libc-identification predefine; resolveCc's - // SCR_MUSL flag and this target-selected TU travel together. - ...(isMuslTarget(driver) ? [rt(join(rtDir, "scr_musl.c"))] : []), - // scr_glob.c (path.matchesGlob) compiles its segments through the - // regex unit and scr_idna.c (node:url's IDNA) normalizes through it, - // so both ride the same switch; section GC drops them from binaries - // that never use them. - ...(regex - ? [ - "-I", - vendorEngineDir(), - rt(join(rtDir, "scr_regex.c")), - rt(join(rtDir, "scr_glob.c")), - rt(join(rtDir, "scr_idna.c")), - ...lreObjects, - ] - : []), - ...(opts.assert || regex || opts.symbol ? [rt(join(rtDir, "scr_assert.c"))] : []), - ...(opts.inspect - ? [rt(join(rtDir, "scr_inspect.c")), rt(join(rtDir, "scr_console_native.c"))] - : []), - ...(opts.dynInvoke || opts.workers || nativeFetch ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []), - ...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []), - ...(opts.dynTier === true ? [rt(join(rtDir, "scr_js_value.c"))] : []), - ...(opts.eventTarget || nativeFetch ? [rt(join(rtDir, "scr_event_target.c"))] : []), - ...(opts.dynAsync || opts.dynInvoke || opts.workers || opts.dc || opts.fileHandle || nativeFetch - ? [rt(join(rtDir, "scr_async_dyn.c"))] - : []), - // The performance timeline (every perf_hooks user-timing call selects - // dynAsync, whose unit carries the timeline's buffer warning). - ...(opts.dynAsync ? [rt(join(rtDir, "scr_perf.c"))] : []), - // Blob and File (every blob.* call selects dynAsync, whose unit boxes - // their reads as dyn promises). - ...(opts.dynAsync ? [rt(join(rtDir, "scr_blob.c"))] : []), - // The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system - // libz on the default host-clang build, vendored objects on Zig builds) - // also serves the native fetch's gzip decoder — spread exactly once. - ...(opts.zlib - ? isZigDriver(driver) - ? ["-I", vendorZlibDir(), rt(join(rtDir, "scr_zlib.c")), ...zlibObjects] - : [rt(join(rtDir, "scr_zlib.c"))] - : nativeFetch - ? isZigDriver(driver) - ? ["-I", vendorZlibDir(), ...zlibObjects] - : [] - : []), - - // through the native installation hooks — the emitted main calls its - // installer exactly then. - ...(opts.events ? [rt(join(rtDir, "scr_events.c")), rt(join(rtDir, "scr_readline.c"))] : []), - // readline's terminal mode: rl.terminal sets the regex unit it - // measures with. - ...(opts.events && regex ? [rt(join(rtDir, "scr_readline_tty.c"))] : []), - ...(opts.emitter || opts.workers ? [rt(join(rtDir, "scr_events_emitter.c"))] : []), - // The checked-dynamic HANDLE support unit (listener gate + runtime - // adapter closures): every referencing unit is one of the emitter or - // net families (http implies net), so handle-free binaries keep - // their exact size class. - ...(opts.emitter || opts.workers || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []), - ...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []), - ...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []), - ...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []), - // The legacy url unit references the regex, querystring and dyn-async - // units; the url.parse family's IR sets all three. - ...(regex && opts.qs && opts.dynAsync ? [rt(join(rtDir, "scr_url_legacy.c"))] : []), - ...(opts.parseArgs ? [rt(join(rtDir, "scr_util.c"))] : []), - ...(opts.parseArgs && opts.symbol ? [rt(join(rtDir, "scr_util_compare.c"))] : []), - ...(opts.parseArgs && opts.inspect && opts.dynAsync - ? [rt(join(rtDir, "scr_util_style.c"))] - : []), - ...(opts.parseArgs && opts.inspect && opts.dynAsync && opts.dynInvoke && regex - ? [rt(join(rtDir, "scr_util_callable.c"))] - : []), - ...(opts.stream ? [rt(join(rtDir, "scr_stream.c")), rt(join(rtDir, "scr_fs_stream.c"))] : []), - // The zlib stream classes reference both the zlib engine and the - // stream unit. - ...(opts.zlib && opts.stream ? [rt(join(rtDir, "scr_zlib_stream.c"))] : []), - ...(opts.stream && opts.dynAsync ? [rt(join(rtDir, "scr_stream_ops.c"))] : []), - // FileHandle streams reference both the stream unit and FileHandle. - ...(opts.stream && opts.fileHandle ? [rt(join(rtDir, "scr_fs_stream_fh.c"))] : []), - // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD, - // epoll on Linux, WSAPoll on Windows — each TU is empty off its - // platform, so all three link whenever a poller-using unit does and - // the others cost nothing (ws2_32 rides the unconditional win32 libs - // above). - ...(net || opts.dgram - ? [ - rt(join(rtDir, "scr_loop_kqueue.c")), - rt(join(rtDir, "scr_loop_epoll.c")), - rt(join(rtDir, "scr_loop_wsapoll.c")), - ] - : []), - ...(net ? [rt(join(rtDir, "scr_net.c"))] : []), - ...(http ? [rt(join(rtDir, "scr_http.c"))] : []), - ...((opts.http2 ?? false) ? [rt(join(rtDir, "scr_http2.c"))] : []), - ...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []), - ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []), - ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []), - ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []), - ...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []), - // The CA-store unit rides its own gate OR the tls one: scr_tls.c - // references its default-set override unconditionally. - ...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []), - ...(tlsArchive - ? [ - "-I", - join(vendorTlsDir(), "include"), - ...(tls ? [rt(join(rtDir, "scr_tls.c"))] : []), - ...(cipher ? [rt(join(rtDir, "scr_cipher.c"))] : []), - tlsArchive, - // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). - // The unconditional win32 libs above do not carry it. - // Never present on the default path, so the historical TLS - // link line cannot change. - ...(targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), - ] - : []), - // scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries. - // This is independent of the mbedTLS archive: getCACertificates-only - // programs need crypt32 too, while TLS programs imply the CA unit. - ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), - // Fetch uses the native network, TLS, and HTTP runtime. - ...(nativeFetch ? [rt(join(rtDir, "scr_fetch.c"))] : []), + ...runtimeUnitArgs({ + opts, + driver, + rt, + rtDir, + regex, + nativeFetch, + net, + http, + tls, + tlsCa, + cipher, + lreObjects, + zlibObjects, + tlsArchive, + }), // A .ll program TU (the LLVM backend) deliberately carries no target // triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET // triple exactly as it does for .c) — silence the -Woverride-module @@ -647,10 +732,7 @@ export async function compileCInternal( ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"] : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, - ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), - ...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []), - ...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []), - ...(opts.youngNursery ? ["-DSCR_YIN"] : []), + ...runtimeDefineArgs(opts), "-fno-math-errno", "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs "-Wno-deprecated-declarations", diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts index a95f0140f..0e1178ce9 100644 --- a/packages/compiler/src/backend/stack-observations.test.ts +++ b/packages/compiler/src/backend/stack-observations.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "vitest"; import { + DYN, F64, STRING, VOID, @@ -49,6 +50,40 @@ test("a reachable stack read is observed", () => { expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true); }); +test("a stack read on a checked-dynamic value is observed", () => { + const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN }; + const read = (key: string): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { + loc, + kind: "dynKeyGet", + value, + key: { loc, kind: "strLit", value: key, type: STRING }, + type: DYN, + }, + }); + expect(observesStacks(mod([fn("%main", [read("stack")])]))).toBe(true); + expect(observesStacks(mod([fn("%main", [read("message")])]))).toBe(false); +}); + +test("a computed key reads the stack when the program spells it", () => { + const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN }; + const key: IrExpr = { loc, kind: "varRef", localId: "k", type: STRING }; + const computed: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "dynKeyGet", value, key, type: DYN }, + }; + const spelled: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "strLit", value: "stack", type: STRING }, + }; + expect(observesStacks(mod([fn("%main", [computed, spelled])]))).toBe(true); + expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false); +}); + test("programs without stack reads are not", () => { expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false); }); diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index 7debf0751..cc6d109e4 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -13,12 +13,34 @@ import { writesPublished } from "./publication.js"; /** Lib calls that read a captured stack trace. */ const STACK_READS: ReadonlySet = new Set(["error.stack"]); -function readsStack(expr: IrExpr): boolean { - return expr.kind === "libCall" && STACK_READS.has(expr.fn); +/** A typed `error.stack` read, or a read on a checked-dynamic value that + * can reach an error's own `stack` accessor: the constant key `stack`, or + * a computed key when `computedKeys` (the program spells "stack", so a + * computed key can name it). A key the program assembles from other + * strings or from data it reads keeps the static answer. */ +function readsStack(expr: IrExpr, computedKeys: boolean): boolean { + if (expr.kind === "libCall") return STACK_READS.has(expr.fn); + if (expr.kind !== "dynKeyGet") return false; + if (expr.key.kind === "strLit") return expr.key.value === "stack"; + return computedKeys; } -function containsStackRead(fn: IrFunction): boolean { - return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) }); +/** Whether the module contains the string literal "stack". */ +function spellsStack(mod: IrModule): boolean { + return mod.functions.some( + (fn) => + !everyStmtList(fn.body, { + stmt: () => true, + expr: (expr) => !(expr.kind === "strLit" && expr.value === "stack"), + }), + ); +} + +function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean { + return !everyStmtList(fn.body, { + stmt: () => true, + expr: (expr) => !readsStack(expr, computedKeys), + }); } /** Visit every expression of `body` that execution can reach: statements @@ -43,7 +65,8 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void { * and those reads must not cost every call of the program a frame. * Programs without any stack read answer false without the walk. */ export function observesStacks(mod: IrModule): boolean { - if (!mod.functions.some(containsStackRead)) return false; + const computedKeys = spellsStack(mod); + if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false; const byName = new Map(mod.functions.map((fn) => [fn.name, fn])); const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); const pending: string[] = [...moduleFunctionReferences(mod)]; @@ -67,7 +90,7 @@ export function observesStacks(mod: IrModule): boolean { const fn = byName.get(name); if (fn === undefined) continue; visitLiveExprs(fn.body, (expr) => { - if (readsStack(expr)) observed = true; + if (readsStack(expr, computedKeys)) observed = true; if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee); const referenced = exprFunctionReference(expr); if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced); diff --git a/packages/compiler/src/frontend/helper-tokens.test.ts b/packages/compiler/src/frontend/helper-tokens.test.ts index c6355cef9..522b39545 100644 --- a/packages/compiler/src/frontend/helper-tokens.test.ts +++ b/packages/compiler/src/frontend/helper-tokens.test.ts @@ -27,6 +27,8 @@ test.each([ ['"default"', '"default;s3:foo;"'], ['"default"', '"default\\0"'], ["mod", "mod other"], + ["cb[0]", "cb[1]"], + ["cb[1]", "cb[10]"], ])("retains a semantic difference between %s and %s", (a, b) => { expect(helperTokens(a)).not.toBe(helperTokens(b)); }); @@ -44,8 +46,12 @@ test.each([ "'\\8'", "`template`", "/regexp/", - "0", "0x10", + "07", + "1.5", + "1e3", + "1n", + "1_000", "a / b", "\\u0061", "café", diff --git a/packages/compiler/src/frontend/helper-tokens.ts b/packages/compiler/src/frontend/helper-tokens.ts index d7a968755..908cc0d22 100644 --- a/packages/compiler/src/frontend/helper-tokens.ts +++ b/packages/compiler/src/frontend/helper-tokens.ts @@ -1,8 +1,8 @@ /** Token fingerprints for the fixed bundler helpers we recognize. This is * deliberately not a JavaScript parser: regexes, templates, escaped names, - * numbers, and unknown punctuation refuse the helper. None occur in the - * canonical helpers. Every accepted token is represented in the result; - * comments and whitespace alone may disappear. */ + * numbers other than plain decimal integers, and unknown punctuation refuse + * the helper. None occur in the canonical helpers. Every accepted token is + * represented in the result; comments and whitespace alone may disappear. */ const operators = [ ">>>=", "===", @@ -157,6 +157,15 @@ export function helperTokens(text: string): string | null { result += fingerprint("s", token.value); i = token.end; previous = "string"; + } else if (/[0-9]/.test(ch)) { + // Plain decimal integers only (esbuild's `cb[keys(cb)[0]]`): a + // legacy octal, fraction, exponent, radix prefix, BigInt suffix or + // numeric separator refuses rather than aliasing another value. + const start = i++; + while (i < text.length && /[0-9]/.test(text.charAt(i))) i++; + if (/[A-Za-z0-9_$.\\]/.test(text.charAt(i)) || (ch === "0" && i - start > 1)) return null; + previous = text.slice(start, i); + result += fingerprint("n", previous); } else if (/[A-Za-z_$]/.test(ch)) { const start = i++; while (i < text.length && /[A-Za-z0-9_$]/.test(text.charAt(i))) i++; diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index 304a21cfa..fa2e92568 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -164,8 +164,28 @@ export function markFrontendInputsUnstable(): void { (synchronousTracker ?? activeTracker.getStore())?.markUnstable(); } +/** Compiler-synthesized module sources (a bundle's CommonJS chunks split + * into sibling modules, npm-static-bundled-chunks.ts). They answer like + * regular files but are never recorded: each derives from the content of + * a recorded bundle read, which already keys every cache that sees it. */ +const virtualSources = new Map(); + +export function setVirtualSource(path: string, text: string): void { + virtualSources.set(resolve(path), text); +} + +export function isVirtualSource(path: string): boolean { + return virtualSources.has(resolve(path)); +} + +export function clearVirtualSources(): void { + virtualSources.clear(); +} + export function trackedReadFile(path: string): string | null { path = resolve(path); + const virtual = virtualSources.get(path); + if (virtual !== undefined) return virtual; try { const text = readFileSync(path, "utf8"); record({ op: "file", path, digest: frontendSourceDigest(text) }); @@ -178,6 +198,7 @@ export function trackedReadFile(path: string): string | null { export function trackedFileExists(path: string): boolean { path = resolve(path); + if (virtualSources.has(path)) return true; const kind = pathKind(path); record({ op: "kind", path, kind }); return kind === "file"; @@ -185,6 +206,7 @@ export function trackedFileExists(path: string): boolean { export function trackedDirectoryExists(path: string): boolean { path = resolve(path); + if (virtualSources.has(path)) return false; const kind = pathKind(path); record({ op: "kind", path, kind }); return kind === "directory"; @@ -192,6 +214,7 @@ export function trackedDirectoryExists(path: string): boolean { export function trackedExists(path: string): boolean { path = resolve(path); + if (virtualSources.has(path)) return true; const kind = pathKind(path); record({ op: "kind", path, kind }); return kind !== "missing"; @@ -199,6 +222,7 @@ export function trackedExists(path: string): boolean { export function trackedRealpath(path: string): string | null { path = resolve(path); + if (virtualSources.has(path)) return path; try { const target = systemRealpath(path); record({ op: "realpath", path, target }); @@ -223,9 +247,13 @@ export function trackedAccessibleEntries( } files.sort(); directories.sort(); - const answer = { files, directories }; - record({ op: "entries", path, files, directories }); - return answer; + record({ op: "entries", path, files: [...files], directories }); + for (const virtual of virtualSources.keys()) { + if (dirname(virtual) === path && !files.includes(basename(virtual))) + files.push(basename(virtual)); + } + files.sort(); + return { files, directories }; } catch { // Enumeration can fail even while the path remains a directory. Preserve // the failed operation itself so a permission/ACL repair invalidates the diff --git a/packages/compiler/src/frontend/lowering/assertion-members.ts b/packages/compiler/src/frontend/lowering/assertion-members.ts new file mode 100644 index 000000000..d61c379d6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/assertion-members.ts @@ -0,0 +1,168 @@ +/* Members a type assertion attaches to record values. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { type IrType, typeEquals } from "../../ir/ir.js"; +import { hostsAll } from "./expressions/hosted-narrowing.js"; + +/** `route as Route & { order: number; internal: boolean }` followed by + * `view.internal = true` adds a property to the very object `route` + * names: Node has no layouts, so every later read of `route` (its JSON, + * its keys, a spread) sees the member. Native records are closed structs, + * and the asserted intersection maps to a different struct, so neither a + * conversion (a copy that severs the write from `route`) nor an erasure + * (a struct without the slot) is faithful. + * + * This pass finds such assertions before any body lowers and gives each + * record arm of the operand's type the asserted members it does not + * declare, as optional fields. Values the program builds hold them absent, + * which is invisible to every key surface (`in`, Object.keys, JSON, + * inspect, spread). The asserted type then refines the stored arms + * instead of naming new ones (hosted-narrowing.ts), so the assertion keeps + * the value and its writes land in the stored object. + * + * Only the plain form qualifies: one intersection whose constituents are + * the operand's own type and type literals of required property + * signatures, over plain data types (object types, aliases and interfaces; + * not classes or the standard library's types). Arms that are tuples or + * carry an index signature, members whose type does not map, and members + * two assertions type differently are left alone: those assertions keep + * their exact-shape diagnostics. Assertions whose operand already declares + * every member are skipped before any type maps, so programs without the + * form intern exactly the types they did before. */ +export function extendAssertedRecordMembers( + lowerer: Lowerer, + files: readonly ts.SourceFile[], +): void { + const additions = new Map>(); + const conflicts = new Set(); + const consider = (node: ts.AsExpression | ts.TypeAssertion): void => { + let typeNode: ts.TypeNode = node.type; + while (ts.isParenthesizedTypeNode(typeNode)) typeNode = typeNode.type; + if (!ts.isIntersectionTypeNode(typeNode)) return; + const parts = typeNode.types.nodes; + const literals = parts.filter((p) => ts.isTypeLiteralNode(p)); + const bases = parts.filter((p) => !ts.isTypeLiteralNode(p)); + if (literals.length === 0 || bases.length !== 1) return; + const memberNodes: { name: string; type: ts.TypeNode }[] = []; + for (const literal of literals) { + if (!ts.isTypeLiteralNode(literal)) return; + for (const member of literal.members.nodes) { + // A required member: the form that writes through the view. An + // optional one only reads, and keeps its exact-shape fence. + if (!ts.isPropertySignatureDeclaration(member) || !member.type || member.questionToken) + return; + const name = ts.isIdentifier(member.name) + ? member.name.text + : ts.isStringLiteral(member.name) + ? member.name.text + : null; + if (name === null || name.startsWith("%")) return; + memberNodes.push({ name, type: member.type }); + } + } + // Checker-only gate first: an assertion every constituent of whose + // operand type already declares the members (`e as Expr & { kind: + // "mul" }`, the refinement idiom) attaches nothing, and mapping its + // types here would intern them out of the usual order. + const operandTs = lowerer.typeOf(node.expression); + const constituents = operandTs.isUnionType() ? ts.constituentTypes(operandTs) : [operandTs]; + const declares = (t: ts.Type, name: string): boolean => + lowerer.checker.getPropertyOfType(t, name) !== undefined; + if (constituents.every((t) => memberNodes.every((m) => declares(t, m.name)))) return; + // Plain data types only: object literal types, aliases and interfaces + // (not classes, and not the standard library's types). + const dataType = (t: ts.Type): boolean => { + const symbol = t.getAliasSymbol() ?? t.getSymbol(); + return ( + !!symbol && + (symbol.flags & ts.SymbolFlags.Class) === 0 && + symbol.declarations.length > 0 && + symbol.declarations.every( + (d) => + !lowerer.isStdlibFile(d.getSourceFile()) && + (ts.isTypeLiteralNode(d) || + ts.isInterfaceDeclaration(d) || + ts.isTypeAliasDeclaration(d)), + ) + ); + }; + if (!constituents.every(dataType)) return; + const operand = lowerer.mapTypeOf(operandTs); + const base = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(bases[0]!)); + if (!operand || !base || !typeEquals(operand, base)) return; + const arms = + operand.kind === "union" ? (lowerer.unions.get(operand.unionId)?.arms ?? []) : [operand]; + if (arms.length === 0) return; + const shapeIds: string[] = []; + for (const arm of arms) { + if (arm.kind !== "record") return; + const shape = lowerer.shapes.get(arm.shapeId); + if (!shape || shape.tuple || shape.indexValue) return; + shapeIds.push(arm.shapeId); + } + const members: { name: string; type: IrType }[] = []; + for (const { name, type } of memberNodes) { + if (shapeIds.every((id) => lowerer.shapes.get(id)!.fields.some((f) => f.name === name))) + continue; + const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(type)); + if (!mapped || mapped.kind === "void") return; + const optional = lowerer.withUndefinedArmOf(mapped); + if (!optional) return; + members.push({ name, type: optional }); + } + for (const shapeId of shapeIds) { + const shape = lowerer.shapes.get(shapeId)!; + for (const member of members) { + if (shape.fields.some((f) => f.name === member.name)) continue; + let added = additions.get(shapeId); + if (!added) additions.set(shapeId, (added = new Map())); + const prior = added.get(member.name); + if (prior && !typeEquals(prior, member.type)) conflicts.add(`${shapeId}:${member.name}`); + else added.set(member.name, member.type); + } + } + }; + const visit = (node: ts.Node): void => { + if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) consider(node); + ts.forEachChild(node, visit); + }; + for (const sf of files) { + if (sf.isDeclarationFile) continue; + // Cheap text gate: the form needs an intersection with a type literal. + if (!/&\s*\{/.test(sf.text)) continue; + visit(sf); + } + for (const [shapeId, added] of additions) { + const extra = [...added] + .filter(([name]) => !conflicts.has(`${shapeId}:${name}`)) + .map(([name, type]) => ({ name, type })); + lowerer.shapes.extendShape(shapeId, extra); + } + if (additions.size > 0) keepStoredTypeOfAssertedGlobals(lowerer, files); +} + +/** A module-level `const view = value as T & { member: X }` binding keeps + * the stored type of `value` once the members are attached, exactly as a + * local binding does (lowerVarDecl): the assertion keeps the object, and + * writes through the binding must reach it. Globals took their checker + * (or annotated) type during collection, before the members existed; an + * annotation the stored type hosts gives way to it the same way. */ +function keepStoredTypeOfAssertedGlobals(lowerer: Lowerer, files: readonly ts.SourceFile[]): void { + for (const sf of files) { + if (sf.isDeclarationFile) continue; + for (const statement of sf.statements.nodes) { + if (!ts.isVariableStatement(statement)) continue; + if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue; + for (const decl of statement.declarationList.declarations.nodes) { + if (!ts.isIdentifier(decl.name) || !decl.initializer) continue; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isAsExpression(init) && !ts.isTypeAssertion(init)) continue; + const global = lowerer.globalOf(decl.name); + const stored = lowerer.mapTypeOf(lowerer.typeOf(init.expression)); + if (!global || !stored || (stored.kind !== "union" && stored.kind !== "record")) continue; + if (hostsAll(lowerer, stored, global.type)) global.type = stored; + } + } + } +} diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 7e1852600..6be798de8 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -12,6 +12,7 @@ import { lowerEventsEmitterHelper, lowerEventsOnce } from "./lower-event-emitter import type { Lowerer } from "./lowerer.js"; import { lowerTimersMemberCall } from "./lower-timers.js"; import { lowerStreamModuleCall } from "./lower-stream.js"; +import { lowerCallbackPromisify } from "./builtins/promisify-callback.js"; import { lowerNodeModuleCall } from "./builtins/module-resolution.js"; import { builtinModuleFnOf, builtinFenceHintOf } from "./surfaces.js"; @@ -87,6 +88,10 @@ export function lowerBuiltinCall( if (testServed) return testServed; const utilServed = lowerer.lowerUtilModuleCall(expr, bi, loc); if (utilServed) return utilServed; + if (bi.module === "util" && bi.member === "promisify") { + const promisified = lowerCallbackPromisify(lowerer, expr, loc); + if (promisified) return promisified; + } const streamServed = lowerStreamModuleCall(lowerer, expr, bi, loc); if (streamServed) return streamServed; const fsTs = lowerer.lowerFsToUnixTimestampCall(expr, bi, loc); diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index 1669978af..afb221d04 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -1,11 +1,12 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { isJsSourceFile } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { builtinModuleFnOf } from "../surfaces.js"; import { registerHttpClientFnBinding } from "../lower-server.js"; import { isPromisifiedZlibMember } from "./compression.js"; import { isPromisifiedCryptoMember } from "./crypto.js"; import { isPromisifiedDnsMember } from "../lower-dgram.js"; +import { callbackPromisifyShape } from "./promisify-callback.js"; /** Compile-time util.promisify projections. The immutable result binding * owns no runtime slot: calls rewrite to the target's promise lowering, @@ -164,11 +165,14 @@ export function registerPromisifiedBuiltinDecl( : target?.module === "dns" && isPromisifiedDnsMember(target.member) ? { kind: "promisified-dns" as const, member: target.member } : null; + // A typed callback-last function value: the call lowers to a native + // promisified function, and the binding holds it like any closure. + if (!projection && callbackPromisifyShape(lowerer, e, locOf(e)) !== null) return false; if (!projection) { lowerer.noLowering( "util.promisify of this target", argNode ?? e, - "child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart are the supported targets", + "child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService, the fs callback functions with an fs/promises counterpart, and typed callback-last functions whose callback takes (error?: Error | null, value?) are the supported targets", ); } const symbol = lowerer.checker.getSymbolAtLocation(nameNode); diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 0313cdd10..a8b4dbcd6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -1,7 +1,16 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; -import { BOOL, CAUGHT, DYN, F64, type IrExpr, type IrType, STRING } from "../../../ir/ir.js"; +import { + BOOL, + CAUGHT, + DYN, + DYN_CLASS_PROPERTIES, + F64, + type IrExpr, + type IrType, + STRING, +} from "../../../ir/ir.js"; const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([ "actual", @@ -126,7 +135,13 @@ export function lowerErrorCodeProperty( declared, ); } - const systemField = ["errno", "syscall", "path", "dest"].includes(expr.name.text); + // A program class under the receiver's class can carry its own `code` + // (a declared field or a property added through an untyped reference), + // which Node reads instead of the errno code: read through the error's + // dynamic view, which answers both. + const systemField = + ["errno", "syscall", "path", "dest"].includes(expr.name.text) || + (expr.name.text === "code" && programClassMayOwnCode(lowerer, recvT.className)); if ( !systemField && expr.name.text !== "code" && @@ -162,7 +177,9 @@ export function lowerErrorCodeProperty( type: DYN, loc, }, - lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING), + expr.name.text === "code" + ? lowerer.envValueType() + : lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING), ); } return { @@ -184,6 +201,19 @@ export function lowerErrorCodeProperty( }; } +/** Whether a class declared in the program, `className` or one of its + * subclasses, has its own `code` field or a dynamic property bag. The + * runtime error classes keep `code` in the errno slot. */ +function programClassMayOwnCode(lowerer: Lowerer, className: string): boolean { + for (const [name, info] of lowerer.classes) { + if (name.startsWith("%")) continue; + if (name !== className && !lowerer.isSubclassOf(name, className)) continue; + if (info.def.fields.some((f) => f.name === "code" || f.name === DYN_CLASS_PROPERTIES)) + return true; + } + return false; +} + /** Whether values of `type` are errors: class instances whose class is * %Error or extends it. A subclass of a class that does not extend Error * cannot be an error either, so the answer depends on the static class diff --git a/packages/compiler/src/frontend/lowering/builtins/ipc.ts b/packages/compiler/src/frontend/lowering/builtins/ipc.ts index cf9d96117..90448854d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ipc.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ipc.ts @@ -1,7 +1,17 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; -import { BOOL, type IrExpr, type IrType, STRING, type SrcLoc } from "../../../ir/ir.js"; +import { + BOOL, + type IrExpr, + type IrStmt, + type IrType, + STRING, + type SrcLoc, + UNDEFINED_T, +} from "../../../ir/ir.js"; +import { nodeThrowExpr } from "../../../ir/build.js"; +import { processHasIpc } from "./process.js"; /** Method calls on ChildProcess receivers: `child.on("exit"|"error", cb)` * registers a listener with the event loop's child registry. The event @@ -86,8 +96,54 @@ export function ipcSendCallback(lowerer: Lowerer, node: ts.Expression): IrExpr { return cb; } +/** process.send in a program without fork targets. This binary never opens + * a channel then, so only a Node.js parent's NODE_CHANNEL_FD can define + * process.send, and that channel has no lowering: a call fences when it runs. + * Without any channel the calls behave as Node's do on an undefined member: + * `process.send?.(...)` skips its arguments and answers undefined, and + * `process.send(...)` evaluates them and throws a TypeError. The message is + * never sent, so its type is not checked. */ +function lowerChannelLessSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + const loc = locOf(call); + const fence = (type: IrType): IrExpr => + nodeThrowExpr( + 0, + "SC2020", + "process.send over an IPC channel opened by a Node.js parent (NODE_CHANNEL_FD) is not supported yet", + type, + loc, + ); + const hasIpc = processHasIpc(lowerer, loc); + if (call.questionDotToken) { + const type = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? BOOL; + const absent: IrExpr = + type.kind === "bool" + ? { kind: "boolLit", value: false, type: BOOL, loc } + : lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ); + return { kind: "ternary", cond: hasIpc, then: fence(type), else_: absent, type, loc }; + } + const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(ts.isSpreadElement(arg) ? arg.expression : arg), + loc: locOf(arg), + })); + const result: IrExpr = { + kind: "ternary", + cond: hasIpc, + then: fence(BOOL), + else_: nodeThrowExpr(1, "", "process.send is not a function", BOOL, loc), + type: BOOL, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: BOOL, loc }; +} + export function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { const loc = locOf(call); + if (lowerer.forkTargets.length === 0) return lowerChannelLessSend(lowerer, call); if ( call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2 || diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 555eaf84f..dc2f5ee89 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -17,6 +17,7 @@ import { type IrExpr, type IrLibFn, type IrStmt, + type IrType, STRING, type SrcLoc, UNDEFINED_T, @@ -30,6 +31,144 @@ import { canonicalBuiltinModule } from "../../builtin-modules.js"; import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js"; import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js"; +/** Whether the process has an IPC channel, Node's test for defining + * process.send and process.disconnect: a channel this fork child opened + * (programs with fork targets only), or a non-empty NODE_CHANNEL_FD from a + * Node.js parent, the variable Node itself tests at startup. Built from the + * existing environment read, so the presence test links no runtime code. */ +export function processHasIpc(lowerer: Lowerer, loc: SrcLoc): IrExpr { + const envT = lowerer.envValueType(); + const unionId = (envT as IrType & { kind: "union" }).unionId; + const stringTag = lowerer.armTag(unionId, STRING); + const value = lowerer.declareHiddenLocal("%channelFd", envT); + const fromNode: IrExpr = { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: value.id, + init: { + kind: "libCall", + fn: "process.envGet", + args: [strLit("NODE_CHANNEL_FD", loc)], + type: envT, + loc, + }, + loc, + }, + ], + result: { + kind: "logical", + op: "&&", + left: { + kind: "unionIsTag", + unionId, + tag: stringTag, + negated: false, + value: varRef(value.id, envT, loc), + type: BOOL, + loc, + }, + right: { + kind: "toBool", + operand: { + kind: "unionNarrow", + unionId, + tag: stringTag, + value: varRef(value.id, envT, loc), + type: STRING, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + if (lowerer.forkTargets.length === 0) return fromNode; + return { + kind: "logical", + op: "||", + left: { kind: "libCall", fn: "process.ipcOpen", args: [], type: BOOL, loc }, + right: fromNode, + type: BOOL, + loc, + }; +} + +/** True when the value of `node` is consumed only by ToBoolean: a + * condition, the operand of `!`, `Boolean(node)`, or an operand of `&&` / + * `||` whose own value is consumed that way (or discarded, for the left + * operand). */ +export function isTruthinessOnlyRead(lowerer: Lowerer, node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) + return parent.expression === child; + if (ts.isForStatement(parent)) return parent.condition === child; + if (ts.isConditionalExpression(parent)) return parent.condition === child; + if (ts.isPrefixUnaryExpression(parent)) return parent.operator === ts.SyntaxKind.ExclamationToken; + if (ts.isCallExpression(parent)) { + return ( + !parent.questionDotToken && + parent.arguments.nodes.length === 1 && + parent.arguments.nodes[0] === child && + lowerer.isStdlibGlobal(parent.expression, "Boolean") + ); + } + if (ts.isBinaryExpression(parent)) { + const op = parent.operatorToken.kind; + if (op !== ts.SyntaxKind.AmpersandAmpersandToken && op !== ts.SyntaxKind.BarBarToken) + return false; + if (isTruthinessOnlyRead(lowerer, parent)) return true; + let outer: ts.Node | undefined = parent.parent; + while (outer !== undefined && ts.isParenthesizedExpression(outer)) outer = outer.parent; + return parent.left === child && outer !== undefined && ts.isExpressionStatement(outer); + } + return false; +} + +/** Whether a program source reads process.exitCode. Such a program + * records its exit-code assignments (process.setExitCodeTracked), so a + * read can tell an unset code from 0; other programs keep the untracked + * write. Computed once per lowering pass. */ +export function programReadsExitCode(lowerer: Lowerer): boolean { + if (lowerer.readsExitCode !== null) return lowerer.readsExitCode; + let found = false; + const walk = (node: ts.Node): void => { + if (found) return; + if ( + ts.isPropertyAccessExpression(node) && + node.name.text === "exitCode" && + !( + ts.isBinaryExpression(node.parent) && + node.parent.left === node && + node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) && + lowerer.stdlibGlobalMember(node, "process") === "exitCode" + ) { + found = true; + return; + } + ts.forEachChild(node, walk); + }; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile) continue; + walk(sf); + if (found) break; + } + lowerer.readsExitCode = found; + return found; +} + /** The pinned Node release the runtime reports as process.versions.node * (packages/runtime/src/scr_lib.c SCR_NODE_COMPAT_VERSION, kept equal to * internal/compatibility/node-v24.json by test/node-pin.test.ts). */ @@ -374,9 +513,45 @@ export function lowerProcessProperty( const member = lowerer.stdlibGlobalMember(expr, "process"); if (member === null) return null; const loc = locOf(expr); + // `if (process.send)`: Node defines process.send and process.disconnect + // only when the process has an IPC channel. A read whose value only + // feeds a truthiness test answers that presence; other value uses keep + // the member fence. + if ( + (member === "send" || member === "disconnect") && + !expr.questionDotToken && + isTruthinessOnlyRead(lowerer, expr) + ) { + return processHasIpc(lowerer, loc); + } if (member === "versions") { return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc }; } + // process.exitCode: undefined until something assigns it, then the + // assigned code. A read the checker narrowed to number follows an + // assignment, so the code is set. + if (member === "exitCode" && !expr.questionDotToken) { + const current: IrExpr = { + kind: "libCall", + fn: "process.currentExitCode", + args: [], + type: F64, + loc, + }; + if (lowerer.mapTypeOf(lowerer.typeOf(expr))?.kind === "f64") return current; + const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) }; + return { + kind: "ternary", + cond: { kind: "libCall", fn: "process.hasExitCode", args: [], type: BOOL, loc }, + then: lowerer.coerceToExpected(current, type), + else_: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ), + type, + loc, + }; + } // process.features as a value (`"sea" in process.features`): an object // holding none of the flags, consistent with the member reads above. if (member === "features") { @@ -1575,10 +1750,36 @@ export function lowerProcessMethodCall( } if (member === "exit") { const arg = call.arguments.nodes[0]; - const code: IrExpr = + let code: IrExpr = arg !== undefined ? lowerer.lowerExprExpecting(arg, F64) : { kind: "libCall", fn: "process.currentExitCode", args: [], type: F64, loc }; + // process.exit(code) assigns process.exitCode before the 'exit' + // listeners run. Only a program that reads the property can observe + // that, so only such a program records the assignment. + if (arg !== undefined && programReadsExitCode(lowerer)) { + const binding = lowerer.declareHiddenLocal("%exitCode", F64); + code = { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: binding.id, init: code, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "process.setExitCodeTracked", + args: [varRef(binding.id, F64, loc)], + type: VOID, + loc, + }, + loc, + }, + ], + result: varRef(binding.id, F64, loc), + type: F64, + loc, + }; + } return { kind: "libCall", fn: "process.exit", args: [code], type: VOID, loc }; } return null; // process.argv(...) etc. are tsc errors before lowering @@ -1594,6 +1795,16 @@ export function lowerProcessOptionalMethodCall( lowerer: Lowerer, expr: ts.CallExpression, ): IrExpr | null { + // `process.send!(message)`: the asserted direct call of the optional + // member (the @types/node spelling under strict null checks). + if ( + !expr.questionDotToken && + ts.isNonNullExpression(expr.expression) && + ts.isPropertyAccessExpression(expr.expression.expression) && + !expr.expression.expression.questionDotToken && + lowerer.stdlibGlobalMember(expr.expression.expression, "process") === "send" + ) + return lowerProcessIpcSend(lowerer, expr); if (!expr.questionDotToken) return null; if (!ts.isPropertyAccessExpression(expr.expression)) return null; const member = lowerer.stdlibGlobalMember(expr.expression, "process"); diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 2656da18b..48106d082 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -65,6 +65,9 @@ export function lowerPromiseStaticCall( const compatible = (inner: IrType): boolean => { if (typeEquals(inner, resultT.inner)) return true; if (!resultArms) return false; + // A Promise entry fulfills with undefined, which the result + // holds when the checker folded `void` into its undefined arm. + if (inner.kind === "void") return resultArms.some((a) => a.kind === "undefinedT"); if (inner.kind === "union") { const arms = lowerer.unions.get(inner.unionId)?.arms ?? []; return arms.every((a) => resultArms.some((b) => typeEquals(a, b))); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts new file mode 100644 index 000000000..cdb8a1fbb --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -0,0 +1,377 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { varRef } from "../../../ir/build.js"; +import { + BOOL, + type IrExpr, + type IrLocal, + type IrParam, + type IrStmt, + type IrType, + type SrcLoc, + VOID, + funcOf, + isUnitType, + typeEquals, + typeKey, +} from "../../../ir/ir.js"; + +const ERROR_T: IrType = { kind: "object", className: "%Error" }; + +/** The parts of `util.promisify(fn)` for a typed callback-last function: + * `fn(...args, (err, value) => void)` becomes `(...args) => Promise`. */ +export interface CallbackShape { + fnT: IrType & { kind: "func" }; + resultT: IrType & { kind: "func" }; + args: readonly IrType[]; + callbackT: IrType & { kind: "func" }; + /** The parameter's declared type: callbackT, or `callbackT | undefined` + * for an optional callback. */ + callbackParamT: IrType; + inner: IrType; + /** The error parameter's union and the tag of its Error arm, or null when + * the callback declares no error parameter. */ + error: { type: IrType; errorTag: number; unitTags: number[] } | null; +} + +function callbackShape( + lowerer: Lowerer, + fnT: IrType | null, + resultT: IrType | null, + loc: SrcLoc, +): CallbackShape | null { + if (fnT?.kind !== "func" || resultT?.kind !== "func") return null; + if (fnT.rest || fnT.restAbi || fnT.argumentsAll || resultT.rest || resultT.restAbi) return null; + const callbackParamT = fnT.params[fnT.params.length - 1]; + // An optional callback (`close(callback?: (err?: Error) => void)`) takes + // its function arm; the call passes the callback into the union. + const callbackT = + callbackParamT?.kind === "union" + ? (() => { + const arms = lowerer.unions.get(callbackParamT.unionId)?.arms ?? []; + const fns = arms.filter((arm) => arm.kind === "func"); + return fns.length === 1 && + arms.every((arm) => arm.kind === "func" || arm.kind === "undefinedT") + ? fns[0] + : undefined; + })() + : callbackParamT; + if (callbackT?.kind !== "func" || callbackT.ret.kind !== "void") return null; + if (callbackT.rest || callbackT.restAbi || callbackT.params.length > 2) return null; + const args = fnT.params.slice(0, -1); + if ( + resultT.ret.kind !== "promise" || + resultT.params.length !== args.length || + !resultT.params.every((type, i) => typeEquals(type, args[i]!)) + ) + return null; + const inner = resultT.ret.inner; + let error: CallbackShape["error"] = null; + const errorT = callbackT.params[0]; + if (errorT !== undefined) { + if (errorT.kind !== "union") return null; + const arms = lowerer.unions.get(errorT.unionId)?.arms ?? []; + const errorTags = arms.flatMap((arm, tag) => + arm.kind === "object" && + (arm.className === "%Error" || lowerer.isSubclassOf(arm.className, "%Error")) + ? [tag] + : [], + ); + const unitTags = arms.flatMap((arm, tag) => (isUnitType(arm) ? [tag] : [])); + if (errorTags.length !== 1 || errorTags.length + unitTags.length !== arms.length) return null; + error = { type: errorT, errorTag: errorTags[0]!, unitTags }; + } + if (inner.kind !== "void") { + const valueT = callbackT.params[1]; + if (valueT === undefined) return null; + const probe: IrExpr = { kind: "varRef", localId: "%probe", type: valueT, loc }; + if (!typeEquals(lowerer.coerceToExpected(probe, inner).type, inner)) return null; + } + return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error }; +} + +/** The callback shape of a `promisify(fn)` call, or null. */ +export function callbackPromisifyShape( + lowerer: Lowerer, + call: ts.CallExpression, + loc: SrcLoc, +): CallbackShape | null { + if (call.questionDotToken || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; + if (ts.isSpreadElement(argNode)) return null; + return callbackShape( + lowerer, + lowerer.mapTypeOf(lowerer.typeOf(argNode)), + lowerer.mapTypeOf(lowerer.typeOf(call)), + loc, + ); +} + +function captureParam(localId: string, type: IrType): IrParam { + return { localId, name: localId, type }; +} + +function boxedLocal(localId: string, type: IrType): IrLocal { + return { id: localId, name: localId, type, mutable: false, boxed: true }; +} + +function plainLocal(localId: string, type: IrType): IrLocal { + return { id: localId, name: localId, type, mutable: false }; +} + +/** `util.promisify(fn)` over a typed callback-last function value (a bound + * method, a function declaration, a closure): a native function that calls + * `fn` with the arguments and a callback, and settles its promise from the + * callback like Node's wrapper: a truthy error rejects, otherwise the first + * value fulfills. A synchronous throw rejects (the executor's rule). Null + * when the argument or result does not have that typed shape. */ +export function lowerCallbackPromisify( + lowerer: Lowerer, + call: ts.CallExpression, + loc: SrcLoc, +): IrExpr | null { + const shape = callbackPromisifyShape(lowerer, call, loc); + if (shape === null) return null; + const argNode = call.arguments.nodes[0]!; + let fn = lowerer.lowerExpr(argNode); + if (!typeEquals(fn.type, shape.fnT)) fn = lowerer.coerceToExpected(fn, shape.fnT); + if (!typeEquals(fn.type, shape.fnT)) return null; + return { + kind: "call", + callee: promisifyMaker(lowerer, shape, loc), + args: [fn], + type: shape.resultT, + loc, + }; +} + +/** Interns the helper family for one (function, result) type pair and + * answers the maker: `(fn) => closure(...args) => Promise`. */ +function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): string { + const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const base = `%promisify.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, base); + const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; + const promiseT = resultT.ret; + const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID); + const rejectT = funcOf([ERROR_T], VOID); + const executorT = funcOf([resolveT, rejectT], VOID); + const argIds = args.map((_, i) => `a${i}.0`); + + // The callback: (err, value) => err ? reject(err) : resolve(value). + const cbParams: IrParam[] = []; + callbackT.params.forEach((type, i) => + cbParams.push(captureParam(i === 0 ? "err.0" : "value.0", type)), + ); + const cbBody: IrStmt[] = []; + if (error !== null) { + const err = varRef("err.0", error.type, loc); + let truthy: IrExpr | null = null; + for (const tag of error.unitTags) { + const part: IrExpr = { + kind: "unionIsTag", + unionId: (error.type as IrType & { kind: "union" }).unionId, + tag, + negated: true, + value: err, + type: BOOL, + loc, + }; + truthy = + truthy === null + ? part + : { kind: "logical", op: "&&", left: truthy, right: part, type: BOOL, loc }; + } + const errorArm = lowerer.unions.get((error.type as IrType & { kind: "union" }).unionId)!.arms[ + error.errorTag + ]!; + let reason: IrExpr = { + kind: "unionNarrow", + unionId: (error.type as IrType & { kind: "union" }).unionId, + tag: error.errorTag, + value: err, + type: errorArm, + loc, + }; + if (!typeEquals(errorArm, ERROR_T)) + reason = { kind: "upcast", value: reason, type: ERROR_T, loc }; + const reject: IrStmt[] = [ + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef("rej.0", rejectT, loc), + args: [reason], + type: VOID, + loc, + }, + loc, + }, + { kind: "return", value: null, loc }, + ]; + cbBody.push( + truthy === null ? reject[0]! : { kind: "if", cond: truthy, then: reject, else_: null, loc }, + ); + if (truthy === null) cbBody.push(reject[1]!); + } + const value = + inner.kind === "void" + ? [] + : [lowerer.coerceToExpected(varRef("value.0", callbackT.params[1]!, loc), inner)]; + cbBody.push({ + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef("res.0", resolveT, loc), + args: value, + type: VOID, + loc, + }, + loc, + }); + lowerer.liftedFns.push({ + name: `${base}.cb`, + params: cbParams, + returnType: VOID, + captures: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)], + locals: [ + boxedLocal("res.0", resolveT), + boxedLocal("rej.0", rejectT), + ...cbParams.map((p) => plainLocal(p.localId, p.type)), + ], + body: cbBody, + loc, + }); + lowerer.liftedFns.push({ + name: `${base}.mkcb`, + params: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)], + returnType: callbackT, + locals: [boxedLocal("res.0", resolveT), boxedLocal("rej.0", rejectT)], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: `${base}.cb`, + captures: ["res.0", "rej.0"], + type: callbackT, + loc, + }, + loc, + }, + ], + loc, + }); + + // The executor: (resolve, reject) => fn(...args, callback). + const captured: IrParam[] = [captureParam("f.0", fnT)]; + args.forEach((type, i) => captured.push(captureParam(argIds[i]!, type))); + lowerer.liftedFns.push({ + name: `${base}.exec`, + params: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)], + returnType: VOID, + captures: captured, + locals: [ + ...captured.map((p) => boxedLocal(p.localId, p.type)), + plainLocal("res.0", resolveT), + plainLocal("rej.0", rejectT), + ], + body: [ + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef("f.0", fnT, loc), + args: [ + ...args.map((type, i) => varRef(argIds[i]!, type, loc)), + lowerer.coerceToExpected( + { + kind: "call", + callee: `${base}.mkcb`, + args: [varRef("res.0", resolveT, loc), varRef("rej.0", rejectT, loc)], + type: callbackT, + loc, + }, + callbackParamT, + ), + ], + // The function's own return type; the value is discarded. + type: fnT.ret, + loc, + }, + loc, + }, + ], + loc, + }); + lowerer.liftedFns.push({ + name: `${base}.mkexec`, + params: captured, + returnType: executorT, + locals: captured.map((p) => boxedLocal(p.localId, p.type)), + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: `${base}.exec`, + captures: captured.map((p) => p.localId), + type: executorT, + loc, + }, + loc, + }, + ], + loc, + }); + + // The promisified function: (...args) => new Promise(executor). + const implParams: IrParam[] = []; + args.forEach((type, i) => implParams.push(captureParam(argIds[i]!, type))); + lowerer.liftedFns.push({ + name: `${base}.impl`, + params: implParams, + returnType: promiseT, + captures: [captureParam("f.0", fnT)], + locals: [boxedLocal("f.0", fnT), ...implParams.map((p) => plainLocal(p.localId, p.type))], + body: [ + { + kind: "return", + value: { + kind: "newPromise", + executor: { + kind: "call", + callee: `${base}.mkexec`, + args: [ + varRef("f.0", fnT, loc), + ...implParams.map((p) => varRef(p.localId, p.type, loc)), + ], + type: executorT, + loc, + }, + type: promiseT, + loc, + }, + loc, + }, + ], + loc, + }); + lowerer.liftedFns.push({ + name: base, + params: [captureParam("f.0", fnT)], + returnType: resultT, + locals: [boxedLocal("f.0", fnT)], + body: [ + { + kind: "return", + value: { kind: "closure", fnName: `${base}.impl`, captures: ["f.0"], type: resultT, loc }, + loc, + }, + ], + loc, + }); + return base; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 6baeb65d1..bcf77a39b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -18,6 +18,7 @@ import { READLINE_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "../surfaces.j import { streamSidesOf } from "../lower-stream.js"; import type { IrLibFn } from "../../../ir/builtin-signatures.js"; import { voidizedCallback } from "../lower-server.js"; +import { builtinImportOf } from "./module-bindings.js"; /** The ambient module declaring `d`, or null. */ function ambientModuleOf(d: ts.Node): string | null { @@ -270,6 +271,80 @@ function lowerReadlineUiCall( return { kind: "seqExpr", stmts, result: recv, type: F64, loc }; } +/** The module ("readline" or "readline/promises") whose Interface class + * `node` names, or null. */ +function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | null { + const builtin = ts.isIdentifier(node) + ? builtinImportOf(lowerer, node) + : ts.isPropertyAccessExpression(node) && !node.questionDotToken + ? lowerer.builtinMemberOf(node) + : null; + if (builtin?.member !== "Interface") return null; + return builtin.module === "readline" || builtin.module === "readline/promises" + ? builtin.module + : null; +} + +/** `rl instanceof readline.Interface` and `rl instanceof + * readlinePromises.Interface` over a readline Interface: an interface is + * an instance of the class of the module that created it (Node's + * readline/promises Interface does not extend node:readline's). Null for + * other instanceof tests. */ +export function lowerReadlineInstanceof( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const module = interfaceClassModule(lowerer, expr.right); + if (module === null || !isReadlineTyped(lowerer, expr.left)) return null; + const loc = locOf(expr); + const recv = lowerer.lowerExprExpecting(expr.left, F64); + const isPromises: IrExpr = { + kind: "libCall", + fn: "rl.isPromises", + args: [recv], + type: BOOL, + loc, + }; + if (module === "readline/promises") return isPromises; + return { kind: "unary", op: "!", operand: isPromises, type: BOOL, loc }; +} + +/** `rl[Symbol.dispose]()` on a readline Interface: Node's dispose method + * calls close(), which does nothing on a closed interface. Null for other + * element-access calls. */ +export function lowerReadlineDisposeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const access = call.expression; + if (!ts.isElementAccessExpression(access) || access.questionDotToken || call.questionDotToken) { + return null; + } + let key = access.argumentExpression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + if ( + !ts.isPropertyAccessExpression(key) || + lowerer.stdlibGlobalMember(key, "Symbol") !== "dispose" + ) { + return null; + } + if (!isReadlineTyped(lowerer, access.expression)) return null; + if (call.arguments.nodes.length > 0) { + lowerer.noLowering("readline.Interface[Symbol.dispose] with arguments", call); + } + const recv = lowerer.lowerExprExpecting(access.expression, F64); + return { kind: "libCall", fn: "rl.close", args: [recv], type: VOID, loc: locOf(call) }; +} + +/** A question query or setPrompt argument: the string, or for a value + * that need not be one (JavaScript), the checked-dynamic value, which the + * runtime writes the way Node's kWriteToOutput does (dyn: true). */ +function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; dyn: boolean } { + if (lowerer.typeOf(node).flags & ts.TypeFlags.StringLike) { + return { value: lowerer.lowerExprExpecting(node, STRING), dyn: false }; + } + const value = lowerer.lowerExpr(node); + if (value.type.kind === "string") return { value, dyn: false }; + return { value: lowerer.coerceInto(node, value, DYN), dyn: true }; +} + /** Method calls on readline Interface receivers (both modules): * - question(query, cb) — the callback form; question(query, { signal }?) * — the promise form (readline/promises); @@ -303,18 +378,20 @@ export function lowerReadlineMethodCall( args.length <= 2 ) { const recv = receiver(); - const query = lowerer.lowerExprExpecting(args[0]!, STRING); + const query = lowerPromptArg(lowerer, args[0]!); const signal = questionSignal(lowerer, args[1]); const type = { kind: "promise", inner: STRING } as const; if (signal === null) { - return { kind: "libCall", fn: "rl.questionAsync", args: [recv, query], type, loc }; + const fn = query.dyn ? "rl.questionAsyncDyn" : "rl.questionAsync"; + return { kind: "libCall", fn, args: [recv, query.value], type, loc }; } const sig = lowerer.lowerExprExpecting(signal, DYN); - return { kind: "libCall", fn: "rl.questionAsyncSignal", args: [recv, query, sig], type, loc }; + const fn = query.dyn ? "rl.questionAsyncSignalDyn" : "rl.questionAsyncSignal"; + return { kind: "libCall", fn, args: [recv, query.value, sig], type, loc }; } if (name === "question" && args.length === 2) { const recv = receiver(); - const query = lowerer.lowerExprExpecting(args[0]!, STRING); + const query = lowerPromptArg(lowerer, args[0]!); const cb = lowerer.lowerExpr(args[1]!); if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( @@ -331,7 +408,8 @@ export function lowerReadlineMethodCall( `question callbacks whose parameter is not 'string' (got '${lowerer.fmt(param)}')`, ); } - return { kind: "libCall", fn: "rl.question", args: [recv, query, cb], type: VOID, loc }; + const fn = query.dyn ? "rl.questionDyn" : "rl.question"; + return { kind: "libCall", fn, args: [recv, query.value, cb], type: VOID, loc }; } const simple: Record = { close: "rl.close", @@ -366,8 +444,9 @@ export function lowerReadlineMethodCall( } if (name === "setPrompt" && args.length === 1) { const recv = receiver(); - const prompt = lowerer.lowerExprExpecting(args[0]!, STRING); - return { kind: "libCall", fn: "rl.setPrompt", args: [recv, prompt], type: VOID, loc }; + const prompt = lowerPromptArg(lowerer, args[0]!); + const fn = prompt.dyn ? "rl.setPromptDyn" : "rl.setPrompt"; + return { kind: "libCall", fn, args: [recv, prompt.value], type: VOID, loc }; } if (name === "getPrompt" && args.length === 0) { return { kind: "libCall", fn: "rl.getPrompt", args: [receiver()], type: STRING, loc }; @@ -871,6 +950,25 @@ export function lowerReadlineCursorCall( return { kind: "seqExpr", stmts, result, type: BOOL, loc }; } +/** A checked-dynamic input or output (a JavaScript function's result, an + * object's member, an element that may be undefined) as the stream it + * holds: the runtime refuses a value that is not a stream with that + * side, and answers null for an output of null or undefined. Other + * values pass through. */ +function streamFromDyn(lowerer: Lowerer, node: ts.Expression, output: boolean): IrExpr { + let v = lowerer.lowerExpr(node); + if (v.type.kind === "union") v = lowerer.coerceInto(node, v, DYN); + if (v.type.kind !== "dyn") return v; + const loc = locOf(node); + return { + kind: "libCall", + fn: output ? "rl.outputFromDyn" : "rl.inputFromDyn", + args: [v, boolLit(output, loc)], + type: { kind: "object", className: output ? "%Writable" : "%Readable" }, + loc, + }; +} + /** The interface's input: process.stdin (null) or a readable stream. */ function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null { if ( @@ -879,7 +977,7 @@ function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null { ) { return null; } - const v = lowerer.lowerExpr(node); + const v = streamFromDyn(lowerer, node, false); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; const sides = streamSidesOf(lowerer, info); if (sides !== "r" && sides !== "rw") { @@ -904,7 +1002,7 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | return "none"; } if (ts.isIdentifier(node) && node.text === "undefined") return "none"; - const v = lowerer.lowerExpr(node); + const v = streamFromDyn(lowerer, node, true); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; const sides = streamSidesOf(lowerer, info); if (sides !== "w" && sides !== "rw") { @@ -923,8 +1021,8 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | * stream; the output process.stdout, a writable stream, or absent * (prompts are then not written). rl.create's arguments: the mode literal * (1 stream input, 2 stream output, 4 stdout output, 8 signal, 16 - * prompt), then the input and output streams when present, crlfDelay, - * the prompt and the signal when present. An interface that can be a + * prompt, 32 node:readline/promises), then the input and output streams + * when present, crlfDelay, the prompt and the signal when present. An interface that can be a * terminal one (a `terminal` option that is not false, or none with * process.stdout as the output, which is a terminal when it is a TTY) * goes through rl.terminal with the completer. */ @@ -1107,7 +1205,8 @@ export function lowerReadlineCreateCall( (typeof output === "object" ? 2 : 0) | (output === "stdout" ? 4 : 0) | (signal !== null ? 8 : 0) | - (prompt !== null ? 16 : 0); + (prompt !== null ? 16 : 0) | + (promises ? 32 : 0); const args: IrExpr[] = [numLit(mode, loc)]; if (input !== null) args.push(input); if (typeof output === "object") args.push(output); diff --git a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts new file mode 100644 index 000000000..4b61f98c1 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts @@ -0,0 +1,181 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { type ClassInfo } from "../lower-classes.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { varRef } from "../../../ir/build.js"; +import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../../ir/ir.js"; + +/** `x.constructor` on an instance typed as EventEmitter or a stream class + * in a TypeScript file: the receiver's runtime ClassInfo, or null. The + * runtime records each instance's Node class name (EventEmitter, Readable, + * ReadStream, Gzip, Socket, a program subclass's name), which the + * lowerings below read with emitter.className. */ +export function runtimeConstructorReceiver( + lowerer: Lowerer, + expr: ts.Expression, +): { receiver: ts.Expression; info: ClassInfo } | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isPropertyAccessExpression(e) || e.questionDotToken) return null; + if (e.name.text !== "constructor" || isJsSourceFile(e.getSourceFile())) return null; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(e.expression)); + if (mapped?.kind !== "object") return null; + const info = lowerer.classes.get(mapped.className); + if (!info?.def.runtime || (!info.builtinEmitter && info.builtinStream === undefined)) return null; + return { receiver: e.expression, info }; +} + +/** `x.constructor.name`: the instance's class name. */ +export function lowerRuntimeConstructorName( + lowerer: Lowerer, + receiver: ts.Expression, + loc: SrcLoc, +): IrExpr { + return { + kind: "libCall", + fn: "emitter.className", + args: [lowerer.lowerExpr(receiver)], + type: STRING, + loc, + }; +} + +/** `x.constructor === C` for EventEmitter or a stream class C: true when + * the instance's own class is C, which needs both the vtable (C's own + * interval, not a subclass's) and the class name (fs, zlib, crypto, child + * process and worker streams share the base classes' vtables). */ +export function lowerRuntimeConstructorIs( + lowerer: Lowerer, + receiver: ts.Expression, + target: ClassInfo, + negated: boolean, + loc: SrcLoc, +): IrExpr { + const value = lowerer.lowerExpr(receiver); + const local = lowerer.declareHiddenLocal("%constructorOf", value.type); + const ref = varRef(local.id, value.type, loc); + const test = exactClassTest(lowerer, ref, target, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result: negated ? { kind: "unary", op: "!", operand: test, type: BOOL, loc } : test, + type: BOOL, + loc, + }; +} + +/** `x.constructor` as a value: the class object of the instance's own class + * among EventEmitter and the stream classes, as the receiver's static + * class's class value. Instances of the other runtime classes (fs, zlib, + * crypto, child process and worker streams) and of program subclasses + * report SC1090 at run time. */ +export function lowerRuntimeConstructor( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + info: ClassInfo, +): IrExpr { + const loc = locOf(access); + const resultT: IrType = { kind: "classval", className: info.def.name }; + const value = lowerer.lowerExpr(access.expression); + const local = lowerer.declareHiddenLocal("%constructorOf", value.type); + const ref = varRef(local.id, value.type, loc); + let result: IrExpr = refusalCall(lowerer, resultT, loc); + for (const candidate of valueClassesUnder(info)) { + lowerer.defineClassValueConstruct(candidate); + lowerer.defineClassValueConstruct(info); + const classRef: IrExpr = { + kind: "classRef", + className: candidate.def.name, + type: { kind: "classval", className: candidate.def.name }, + loc, + }; + result = { + kind: "ternary", + cond: exactClassTest(lowerer, ref, candidate, loc), + then: candidate === info ? classRef : { kind: "upcast", value: classRef, type: resultT, loc }, + else_: result, + type: resultT, + loc, + }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result, + type: resultT, + loc, + }; +} + +/** `info` and its runtime descendants whose values construct. */ +function valueClassesUnder(info: ClassInfo): ClassInfo[] { + const out: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + if (!c.def.runtime) return; + if (c.valueCtorParams !== undefined) out.push(c); + for (const s of c.subclasses) visit(s); + }; + visit(info); + return out; +} + +/** The instance is of class `target` itself: inside its interval, outside + * each direct subclass's (runtime or program), and named as it. */ +function exactClassTest(lowerer: Lowerer, ref: IrExpr, target: ClassInfo, loc: SrcLoc): IrExpr { + let test: IrExpr = { + kind: "instanceOf", + value: ref, + className: target.def.name, + type: BOOL, + loc, + }; + for (const sub of target.subclasses) { + test = { + kind: "logical", + op: "&&", + left: test, + right: { + kind: "unary", + op: "!", + operand: { kind: "instanceOf", value: ref, className: sub.def.name, type: BOOL, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + const name: IrExpr = { + kind: "strEq", + negated: false, + left: { kind: "libCall", fn: "emitter.className", args: [ref], type: STRING, loc }, + right: { kind: "strLit", value: target.def.name.slice(1), type: STRING, loc }, + type: BOOL, + loc, + }; + return { kind: "logical", op: "&&", left: test, right: name, type: BOOL, loc }; +} + +/** The run-time refusal for an instance whose class has no class value. */ +function refusalCall(lowerer: Lowerer, type: IrType, loc: SrcLoc): IrExpr { + const name = `%constructor.refusal:${type.kind === "classval" ? type.className : ""}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: type, + body: [ + { + kind: "runtimeFence", + code: "SC1090", + message: + "reading 'constructor' of an fs, zlib, crypto, child process or worker stream, or of an instance of a program subclass of EventEmitter or a stream class, is not supported yet", + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [], type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 6db1735db..7570fff88 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -27,6 +27,7 @@ import { isStreamUndefCallExpr } from "./lower-server.js"; import { nsPathPrefix } from "./lower-namespaces.js"; import { declSymbolOf } from "./lower-modules.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; +import { restPackingAdapterFits } from "./coercions/function-adapters.js"; import { implicitMonoFile, implicitAnyParamSymbolsOf } from "./generic-functions.js"; /** How a parameter participates in CALL-SITE COMPLETION (the frontend @@ -46,12 +47,15 @@ export type ParamMode = "required" | "omittable" | "rest" | "dynRest" | "argumen * otherwise. `bodyType` is present exactly for DEFAULTED program params: * the plain T the body sees after the prologue applies the default (see * declareParams). `callDefault` is the compile-time default value for a - * table-backed builtin's omittable slot. */ + * table-backed builtin's omittable slot. `entryNarrow` marks a dyn ABI slot + * inherited from a JavaScript base method whose TypeScript override declares + * a typed parameter: the prologue narrows the checked value to that type. */ export interface ParamShape { type: IrType; mode: ParamMode; bodyType?: IrType; callDefault?: IrExpr; + entryNarrow?: IrType; } export interface FnSig { @@ -522,6 +526,21 @@ export function requireExactArityValue( // optional/defaulted params spell their `T | undefined` slots (mapType's // completed-signature contract), so omitted trailing args complete with // the undefined arm like any direct call. + // A nested function declaration whose only non-required parameter is a + // typed rest: its value carries the typed-rest ABI its own type spells, + // and every use converts it at its slot (same signature, or the rest + // packing adapter for fixed parameters). + if ( + contextual === null && + ts.isFunctionDeclaration(blame) && + blame.name !== undefined && + funcType.kind === "func" && + funcType.restAbi === "typed" && + shapes.every((s) => s.mode === "required" || s.mode === "rest") + ) { + const own = lowerer.mapTypeOf(lowerer.typeOf(blame.name)); + if (own && typeEquals(own, funcType)) return; + } const target = contextual ? lowerer.checker.getContextualType(contextual) : undefined; const mapped = target ? lowerer.mapTypeOf(target) @@ -582,6 +601,16 @@ export function requireExactArityValue( ) { return; } + // A typed-rest function in a slot with fixed parameters (`log(...m: + // string[])` into `(m: string) => void`): the slot coercion's packing + // adapter collects the slot's arguments into the rest array. + if ( + mappedFn?.kind === "func" && + funcType.kind === "func" && + restPackingAdapterFits(lowerer, funcType, mappedFn) + ) { + return; + } // An uncontextualized npm-static implementation function can still // have a declaration-backed completed ABI: the package's .d.ts // projection supplies the body-facing parameter/return types while @@ -740,7 +769,11 @@ function appendArgumentsParameter( const classMethod = ts.isMethodDeclaration(node) && (ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent)); - if (!classMethod && !isNodeEsmFile(node.getSourceFile(), lowerer.program)) { + if ( + !classMethod && + !isNodeEsmFile(node.getSourceFile(), lowerer.program) && + !unmappedArguments(node) + ) { lowerer.unsupported( "SC1090", node, @@ -751,6 +784,35 @@ function appendArgumentsParameter( shapes.push({ type: DYN, mode }); } +/** Whether `arguments` inside `node` is Node's unmapped object: strict code + * (a 'use strict' prologue on the file or an enclosing function, or a class + * body) or a non-simple parameter list (defaults, rest, destructuring). Its + * elements then never alias the parameters, which is the ES module case. */ +function unmappedArguments(node: ts.SignatureDeclaration): boolean { + if ( + node.parameters.nodes.some( + (p) => + p.initializer !== undefined || p.dotDotDotToken !== undefined || !ts.isIdentifier(p.name), + ) + ) + return true; + const useStrict = (statements: readonly ts.Statement[]): boolean => { + for (const statement of statements) { + if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) + return false; + if (statement.expression.text === "use strict") return true; + } + return false; + }; + for (let n: ts.Node | undefined = node; n; n = n.parent) { + if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return true; + if (ts.isFunctionLike(n) && n.body && ts.isBlock(n.body) && useStrict(n.body.statements.nodes)) + return true; + if (ts.isSourceFile(n)) return useStrict(n.statements.nodes); + } + return false; +} + /** Signature checks + param shapes + IR func type for any lambda-like * node. The func type's params are the ABI types: optional/default params * become `T | undefined`, and typed rest becomes one packed-array slot. diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index 5ea4001fc..f06dde5a2 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -22,13 +22,18 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { cjsExportAssignmentOf, + cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isModuleExportsAccess, isRequireStatement, locOf, + npmStaticDepSf7, + resolveImport, } from "../program.js"; +import { cjsWholeExportValueOf, inlineProgramRequireOf } from "./cjs-inline-require.js"; import { isCjsModuleGlobal } from "./lower-node-module.js"; +import { bindingNeverReassigned } from "./binding-analysis.js"; import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js"; /** True for the CommonJS wrapper's `exports` binding (not shadowed by any @@ -428,3 +433,128 @@ export function cjsExportCallIgnoresThis( return false; }); } + +/** The value behind a binding of a CommonJS member export (an `import { + * name }` or a top-level `const { name } = require(...)`) written once as + * `module.exports. = ` or `exports. = `, when the + * module exports a value whole and the member is a property added to it + * (`exports = module.exports = Client; exports.StatsD = Client`). The + * checker gives such a binding a synthesized property symbol, so the + * module's statements answer. The value is a binding of the exporting + * module or a primitive literal, which the use site reads again. Follows + * `module.exports = require("./x")` re-exports. Null for any other binding. */ +export function cjsMemberAliasValueOf(lowerer: Lowerer, ref: ts.Identifier): ts.Expression | null { + const sym = lowerer.checker.getSymbolAtLocation(ref); + if (!sym) return null; + const decls = sym.declarations; + // `import { StatsD } from "pkg"` + const specifier = sym.flags & ts.SymbolFlags.Alias ? decls.find(ts.isImportSpecifier) : undefined; + if (specifier !== undefined) { + const importDecl = specifier.parent?.parent?.parent; + if ( + importDecl === undefined || + !ts.isImportDeclaration(importDecl) || + !ts.isStringLiteral(importDecl.moduleSpecifier) + ) + return null; + const spec = importDecl.moduleSpecifier.text; + const from = importDecl.getSourceFile(); + const dep = + resolveImport(lowerer.program, from, spec, "import") ?? + npmStaticDepSf7(lowerer.program, from, spec, "import"); + if (dep === null) return null; + return memberAliasIn(lowerer, dep, (specifier.propertyName ?? specifier.name).text, 0); + } + // `const { shout } = require("./lib")` at a module's top level + const element = decls.find(ts.isBindingElement); + if (element === undefined || element.dotDotDotToken || !ts.isIdentifier(element.name)) + return null; + const pattern = element.parent; + const declaration = pattern?.parent; + if ( + pattern === undefined || + !ts.isObjectBindingPattern(pattern) || + declaration === undefined || + !ts.isVariableDeclaration(declaration) || + declaration.initializer === undefined || + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0 + ) + return null; + const key = element.propertyName ?? element.name; + if (!ts.isIdentifier(key)) return null; + const req = inlineProgramRequireOf(lowerer, declaration.initializer); + if (req === null) return null; + return memberAliasIn(lowerer, req.dep, key.text, 0); +} + +function memberAliasIn( + lowerer: Lowerer, + sf: ts.SourceFile, + name: string, + depth: number, +): ts.Expression | null { + const member = memberWritesIn(lowerer, sf, name, depth); + // One write: a member written again would be a runtime slot. + return member !== null && member.writes === 1 ? member.value : null; +} + +/** The writes of member `name` in `sf` and in the modules it re-exports + * through `module.exports = require("./x")`, which share its exports + * object. `value` is the last write's value when the use site can read + * it again. Null when the module does not export a value whole. */ +function memberWritesIn( + lowerer: Lowerer, + sf: ts.SourceFile, + name: string, + depth: number, +): { value: ts.Expression | null; writes: number } | null { + if (depth > 32 || sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) return null; + const whole = cjsWholeExportValueOf(sf); + if (whole === null) return null; + let value: ts.Expression | null = null; + let writes = 0; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + if (inner !== null) { + // The re-exported module runs first; this module's own writes follow. + const dep = memberWritesIn(lowerer, inner.dep, name, depth + 1); + if (dep === null) return null; + ({ value, writes } = dep); + } + for (const stmt of sf.statements.nodes) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind !== "member" || !ts.isIdentifier(cjs.name) || cjs.name.text !== name) continue; + writes++; + if (cjsExportDiscardReason(stmt) !== null) return null; + let written: ts.Expression = cjs.value; + while (ts.isParenthesizedExpression(written)) written = written.expression; + value = rereadableMemberValue(lowerer, stmt, written) ? written : null; + } + return { value, writes }; +} + +/** True when reading `value` again at the use site observes what the + * member write `stmt` stored: a primitive literal, or a module binding + * declared before the write and never reassigned. */ +function rereadableMemberValue( + lowerer: Lowerer, + stmt: ts.Statement, + value: ts.Expression, +): boolean { + if ( + ts.isStringLiteral(value) || + ts.isNumericLiteral(value) || + ts.isNoSubstitutionTemplateLiteral(value) || + value.kind === ts.SyntaxKind.TrueKeyword || + value.kind === ts.SyntaxKind.FalseKeyword + ) + return true; + if (!ts.isIdentifier(value)) return false; + const sym = lowerer.resolveValueSymbol(value); + if (sym === null || sym.declarations.length !== 1) return false; + const decl = sym.declarations[0]!; + if (decl.getSourceFile() !== stmt.getSourceFile()) return false; + // A function declaration is initialized when the module starts; any + // other binding must be initialized before the write stores it. + if (!ts.isFunctionDeclaration(decl) && decl.end > stmt.pos) return false; + return bindingNeverReassigned(lowerer, sym, decl); +} diff --git a/packages/compiler/src/frontend/lowering/class-attributes.ts b/packages/compiler/src/frontend/lowering/class-attributes.ts new file mode 100644 index 000000000..4e7bbe801 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-attributes.ts @@ -0,0 +1,440 @@ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + CLASS_ATTRIBUTES, + CLASS_FROZEN, + CLASS_NOT_EXTENSIBLE, + CLASS_SEALED, + DYN, + F64, + FIELD_NOT_WRITABLE, + STRING, + VOID, + canConvertToDyn, + canDynCheckTo, + classFieldAttributesName, + isClassOwnEnumerableFieldName, + isDynTypedRefType, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { numLit, strLit, varRef } from "../../ir/build.js"; +import { transformStmtList } from "../../ir/traverse.js"; +import { locOf } from "../program.js"; +import { type Lowerer, PoisonError } from "./lowerer.js"; +import type { ClassInfo } from "./lower-classes.js"; +import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; +import { runtimeRooted } from "./class-descriptors.js"; + +/* Object.freeze, Object.seal, Object.preventExtensions and + * Object.defineProperty on class instances. Only the hierarchies these calls + * name pay for them: such a hierarchy gets a CLASS_ATTRIBUTES integrity word + * per instance, an attribute word per redefined field, and a check before + * each write to a declared field (guardClassAttributeWrites). The instance's + * checked view carries the attributes, so reflection answers from it. */ + +type ClassObjectType = Extract; + +function attributeClass(lowerer: Lowerer, target: IrExpr): ClassInfo | null { + if (!isDynTypedRefType(target.type)) return null; + const info = lowerer.classes.get(target.type.className); + if (!info || runtimeRooted(info) || info.decl == null) return null; + return info; +} + +/** Adds a zero-initialized f64 to every class of the hierarchy, at the same + * offset, like the property bag. */ +function addHiddenField(info: ClassInfo, name: string): void { + let root = info; + while (root.base && !root.base.def.runtime) root = root.base; + if (root.fields.has(name)) return; + const index = root.def.fields.length; + const add = (current: ClassInfo): void => { + if (!current.fields.has(name)) { + current.fields.set(name, F64); + current.def.fields.splice(index, 0, { name, type: F64 }); + } + current.subclasses.forEach(add); + }; + add(root); +} + +function hiddenFieldGet(receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrExpr { + return { kind: "fieldGet", obj: receiver, className, field, type: F64, loc }; +} + +/** Object.freeze, Object.seal and Object.preventExtensions of a class + * instance: sets the instance's integrity bits, then applies the operation to + * the properties added to the instance. Answers the instance. */ +export function lowerClassIntegrity( + lowerer: Lowerer, + call: ts.CallExpression, + member: "freeze" | "seal" | "preventExtensions", + target: IrExpr, +): IrExpr | null { + const info = attributeClass(lowerer, target); + if (!info) return null; + addHiddenField(info, CLASS_ATTRIBUTES); + const loc = locOf(call); + const type = target.type as ClassObjectType; + const bits = + member === "freeze" + ? CLASS_NOT_EXTENSIBLE | CLASS_SEALED | CLASS_FROZEN + : member === "seal" + ? CLASS_NOT_EXTENSIBLE | CLASS_SEALED + : CLASS_NOT_EXTENSIBLE; + const slot = lowerer.declareHiddenLocal("%integrityReceiver", type); + const receiver = (): IrExpr => varRef(slot.id, type, loc); + const bag: IrExpr = { + kind: "call", + callee: classPropertiesHelper(lowerer, loc).name, + args: [lowerer.coerceToExpected(receiver(), DYN)], + type: DYN, + loc, + }; + const operation = { + freeze: "dyn.freeze", + seal: "dyn.seal", + preventExtensions: "dyn.preventExtensions", + } as const; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: target, loc }, + { + kind: "fieldSet", + obj: receiver(), + className: type.className, + field: CLASS_ATTRIBUTES, + value: { + kind: "bin", + op: "|", + left: hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc), + right: numLit(bits, loc), + type: F64, + loc, + }, + attributesChecked: true, + loc, + }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn: operation[member], args: [bag], type: DYN, loc }, + loc, + }, + ], + result: receiver(), + type, + loc, + }; +} + +/** An object literal with a get or set property. */ +function literalAccessorDescriptor(node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + return ( + ts.isObjectLiteralExpression(node) && + node.properties.nodes.some( + (property) => + property.name !== undefined && + (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && + (property.name.text === "get" || property.name.text === "set"), + ) + ); +} + +/** Object.defineProperty with a string-literal key naming a declared field + * of a class instance: a data descriptor changes the field's value and + * attributes, validated as Node validates ordinary objects. Answers the + * instance, or null when the call is not of that form. An object-literal + * accessor descriptor is refused at compile time; other accessor + * descriptors throw SC2020 when the call runs. */ +export function lowerClassFieldDefine( + lowerer: Lowerer, + call: ts.CallExpression, + target: IrExpr, +): IrExpr | null { + const info = attributeClass(lowerer, target); + const key = call.arguments.nodes[1]!; + if (!info || !ts.isStringLiteral(key)) return null; + const field = key.text; + const fieldType = info.fields.get(field); + if ( + fieldType === undefined || + !isClassOwnEnumerableFieldName(field) || + [...(info.symbolFields?.values() ?? [])].includes(field) + ) + return null; + if (literalAccessorDescriptor(call.arguments.nodes[2]!)) { + lowerer.pushDiag({ + code: "SC2020", + message: `Object.defineProperty with an accessor descriptor for the declared field '${field}' is not supported yet`, + loc: locOf(call), + }); + throw new PoisonError(); + } + const getRecord = (id: string) => lowerer.shapes.get(id); + const getUnion = (id: string) => lowerer.unions.get(id); + if ( + !canConvertToDyn(fieldType, getRecord, getUnion) || + !canDynCheckTo(fieldType, getRecord, getUnion) + ) + return null; + const attributesField = classFieldAttributesName(field); + addHiddenField(info, CLASS_ATTRIBUTES); + addHiddenField(info, attributesField); + const loc = locOf(call); + const type = target.type as ClassObjectType; + const receiverSlot = lowerer.declareHiddenLocal("%defineReceiver", type); + const descriptorSlot = lowerer.declareHiddenLocal("%defineDescriptor", DYN); + const holderSlot = lowerer.declareHiddenLocal("%defineHolder", DYN); + const receiver = (): IrExpr => varRef(receiverSlot.id, type, loc); + const descriptor = (): IrExpr => varRef(descriptorSlot.id, DYN, loc); + const holder = (): IrExpr => varRef(holderSlot.id, DYN, loc); + const name = (): IrExpr => strLit(field, loc); + const current: IrExpr = { + kind: "fieldGet", + obj: receiver(), + className: type.className, + field, + type: fieldType, + loc, + }; + const defined: IrExpr = { + kind: "libCall", + fn: "class.definedValue", + args: [holder(), name()], + type: DYN, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: receiverSlot.id, init: target, loc }, + { + kind: "varDecl", + localId: descriptorSlot.id, + init: lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN), + loc, + }, + { + kind: "varDecl", + localId: holderSlot.id, + init: { + kind: "libCall", + fn: "class.defineField", + args: [ + lowerer.coerceToExpected(current, DYN), + hiddenFieldGet(receiver(), type.className, attributesField, loc), + hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc), + name(), + descriptor(), + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "class.descriptorHasValue", + args: [descriptor()], + type: BOOL, + loc, + }, + then: [ + { + kind: "fieldSet", + obj: receiver(), + className: type.className, + field, + value: lowerer.coerceToExpected(defined, fieldType), + attributesChecked: true, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "fieldSet", + obj: receiver(), + className: type.className, + field: attributesField, + value: { + kind: "libCall", + fn: "class.definedBits", + args: [holder(), name()], + type: F64, + loc, + }, + attributesChecked: true, + loc, + }, + ], + result: receiver(), + type, + loc, + }; +} + +function classDisplayName(info: ClassInfo): string { + return info.def.jsName ?? info.decl?.name?.text ?? info.def.name.replace(/^%/, ""); +} + +/** The instance's own class name for an error message: a helper testing the + * most derived classes of the hierarchy first. */ +function instanceClassName( + lowerer: Lowerer, + info: ClassInfo, + receiver: IrExpr, + helpers: Map, + loc: SrcLoc, +): IrExpr { + if (info.subclasses.length === 0) return strLit(classDisplayName(info), loc); + let helper = helpers.get(info.def.name); + if (!helper) { + const type: IrType = { kind: "object", className: info.def.name }; + const value = varRef("value", type, loc); + const descendants = (current: ClassInfo): ClassInfo[] => + current.subclasses.flatMap((child) => [...descendants(child), child]); + const body: IrStmt[] = descendants(info).map((child): IrStmt => ({ + kind: "if", + cond: { kind: "instanceOf", value, className: child.def.name, type: BOOL, loc }, + then: [{ kind: "return", value: strLit(classDisplayName(child), loc), loc }], + else_: null, + loc, + })); + body.push({ kind: "return", value: strLit(classDisplayName(info), loc), loc }); + helper = { + name: `%class.instanceName.${helpers.size}`, + params: [{ localId: "value", name: "value", type }], + returnType: STRING, + locals: [{ id: "value", name: "value", type, mutable: false }], + body, + loc, + }; + helpers.set(info.def.name, helper); + } + return { kind: "call", callee: helper.name, args: [receiver], type: STRING, loc }; +} + +/** Checks the integrity word, and the field's attribute word when it has one, + * before every write to a declared field of a hierarchy that has them. A + * read-only field throws Node's TypeError in strict mode code; a sloppy-mode + * write leaves it unchanged. Other classes keep their plain stores. */ +export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunction[]): void { + const guarded = new Set( + [...lowerer.classes.values()] + .filter((info) => info.def.fields.some((field) => field.name === CLASS_ATTRIBUTES)) + .map((info) => info.def.name), + ); + if (guarded.size === 0) return; + const helpers = new Map(); + for (const fn of functions) { + const local = (type: IrType, loc: SrcLoc): Extract => { + const id = `%attributeWrite.${fn.locals.length}`; + fn.locals.push({ id, name: id, type, mutable: false }); + return { kind: "varRef", localId: id, type, loc }; + }; + fn.body = transformStmtList(fn.body, { + stmt: (statement) => { + if ( + statement.kind !== "fieldSet" || + statement.attributesChecked || + !guarded.has(statement.className) || + !isClassOwnEnumerableFieldName(statement.field) + ) + return statement; + const info = lowerer.classes.get(statement.className)!; + const loc = statement.loc; + const receiver = local(statement.obj.type, loc); + const value = local(statement.value.type, loc); + const masked = (field: string, bit: number): IrExpr => ({ + kind: "bin", + op: "&", + left: hiddenFieldGet(receiver, statement.className, field, loc), + right: numLit(bit, loc), + type: F64, + loc, + }); + const attributesField = classFieldAttributesName(statement.field); + let readOnly = masked( + CLASS_ATTRIBUTES, + statement.define ? CLASS_NOT_EXTENSIBLE : CLASS_FROZEN, + ); + if (!statement.define && info.fields.has(attributesField)) { + readOnly = { + kind: "bin", + op: "|", + left: readOnly, + right: masked(attributesField, FIELD_NOT_WRITABLE), + type: F64, + loc, + }; + } + const reject: IrStmt[] = statement.define + ? [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "class.throwNotExtensible", + args: [strLit(statement.field, loc)], + type: VOID, + loc, + }, + loc, + }, + ] + : statement.sloppy + ? [] + : [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "class.throwReadOnly", + args: [ + strLit(statement.field, loc), + instanceClassName(lowerer, info, receiver, helpers, loc), + ], + type: VOID, + loc, + }, + loc, + }, + ]; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: receiver.localId, init: statement.obj, loc }, + { kind: "varDecl", localId: value.localId, init: statement.value, loc }, + { + kind: "if", + cond: { + kind: "bin", + op: "!==", + left: readOnly, + right: numLit(0, loc), + type: BOOL, + loc, + }, + then: reject, + else_: [{ ...statement, obj: receiver, value, attributesChecked: true }], + loc, + }, + ], + loc, + }; + }, + expr: (expression) => expression, + }); + } + for (const helper of helpers.values()) functions.push(helper); +} diff --git a/packages/compiler/src/frontend/lowering/class-descriptors.ts b/packages/compiler/src/frontend/lowering/class-descriptors.ts index 473ed2496..cb980f0d0 100644 --- a/packages/compiler/src/frontend/lowering/class-descriptors.ts +++ b/packages/compiler/src/frontend/lowering/class-descriptors.ts @@ -23,7 +23,7 @@ function bagOnlyProperty(owner: ClassInfo, name: string): boolean { return !owner.fields.has(name) && owner.subclasses.every((child) => bagOnlyProperty(child, name)); } -function runtimeRooted(info: ClassInfo): boolean { +export function runtimeRooted(info: ClassInfo): boolean { for (let current: ClassInfo | null = info; current; current = current.base) { if ( current.def.runtime || diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 6b515c1ff..2587a4b65 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -44,6 +44,7 @@ import { observedPrototypeMethod, reflectClassPrototype, } from "./class-prototypes.js"; +import { perEvaluationPrivateNames } from "./private-brands.js"; import { errorPropertyRead, errorPropertyWrite, @@ -2694,6 +2695,7 @@ export class ClassDynamicDispatch { !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && !info.fields.has(dispatch.name) && + !(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`)) ) @@ -2885,10 +2887,12 @@ export class ClassDynamicDispatch { for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]); for (const [symbol, name] of symbolEntries) { const declaration = symbol.valueDeclaration; + // Module-scope consts and CommonJS key-table properties + // (classSymbolKeyOfSymbol) both hold their key in a global. if ( declaration && - ts.isVariableDeclaration(declaration) && - ts.isIdentifier(declaration.name) + ((ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name)) || + ts.isPropertyAssignment(declaration)) ) { const global = lowerer.globalsBySymbol.get(symbol); if (global) @@ -4311,19 +4315,43 @@ export class ClassDynamicDispatch { loc, }; if (accessor && info.decl) { - const method = classMethodValue(lowerer, info.decl, info, member, loc); - if (method) { - const call: IrExpr = { - kind: "libCall", - fn: "dyn.reflectApply", - args: [ - lowerer.coerceToExpected(method, DYN), - actualReceiver, - { kind: "dynArrLit", elems: write ? [varRef("p.1", DYN, loc)] : [], type: DYN, loc }, - ], - type: DYN, - loc, - }; + // An accessor whose private names belong to one evaluation runs as + // the instance's own evaluation's prototype accessor. + const declarer = accessor.declarer; + const prototype = perEvaluationPrivateNames(declarer) + ? classPrototypeData(lowerer, declarer, loc, upcastTo(lowerer, receiver, declarer.def.name)) + : null; + const method = prototype ? null : classMethodValue(lowerer, info.decl, info, member, loc); + if (prototype || method) { + const call: IrExpr = prototype + ? { + kind: "libCall", + fn: write ? "dyn.reflectSet" : "dyn.reflectGet", + args: [ + prototype, + lowerer.coerceToExpected(key, DYN), + ...(write ? [varRef("p.1", DYN, loc)] : []), + actualReceiver, + ], + type: write ? BOOL : DYN, + loc, + } + : { + kind: "libCall", + fn: "dyn.reflectApply", + args: [ + lowerer.coerceToExpected(method!, DYN), + actualReceiver, + { + kind: "dynArrLit", + elems: write ? [varRef("p.1", DYN, loc)] : [], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; return [ { kind: "if", @@ -4498,6 +4526,110 @@ export class ClassDynamicDispatch { }, ]; } + // An Error's own `stack`: a string is the native error's stack, which + // typed reads share; any other value is kept with the instance's own + // properties, which a read checks first. A class whose chain declares + // a `stack` accessor answers through it (the accessor code below). + if ( + name === "stack" && + !info.fields.has(name) && + lowerer.isSubclassOf(info.def.name, "%Error") && + !findMethodOn(lowerer, info, "get:stack") && + !findMethodOn(lowerer, info, "set:stack") + ) { + const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + const error = lowerer.upcastTo(receiver, "%Error"); + if (write) { + const helper = classPropertiesHelper(lowerer, loc); + this.generated.add(helper); + const store: IrStmt = { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ + { + kind: "call", + callee: helper.name, + args: [lowerer.coerceToExpected(receiver, DYN)], + type: DYN, + loc, + }, + key, + varRef("p.1", DYN, loc), + ], + type: VOID, + loc, + }, + loc, + }; + return [ + { + kind: "if", + cond: { + kind: "dynTest", + test: "string", + value: varRef("p.1", DYN, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "error.setStack", + args: [error, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING)], + type: VOID, + loc, + }, + loc, + }, + { + kind: "if", + cond: this.readBagHas( + lowerer, + "dyn.hasKey", + this.readPropertyBag(lowerer, receiver, loc), + key, + loc, + ), + then: [store], + else_: null, + loc, + }, + ], + else_: [store], + loc, + }, + { kind: "return", value: null, loc }, + ]; + } + const bag = this.readPropertyBag(lowerer, receiver, loc); + return [ + { + kind: "return", + value: { + kind: "ternary", + cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc), + then: { + kind: "libCall", + fn: "dyn.bagGet", + args: [bag, key, lowerer.coerceToExpected(receiver, DYN)], + type: DYN, + loc, + }, + else_: lowerer.coerceToExpected( + { kind: "libCall", fn: "error.stack", args: [error], type: STRING, loc }, + DYN, + ), + type: DYN, + loc, + }, + loc, + }, + ]; + } if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error")) return write ? [ diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 10572377e..795307c93 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -27,6 +27,7 @@ import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js"; import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js"; import { errorToStringMethod } from "./error-methods.js"; import { classCallbackValue, isClassCallback } from "./class-callbacks.js"; +import { perEvaluationPrivateNames } from "./private-brands.js"; export interface ClassMethodValueSelection { expression: ts.PropertyAccessExpression; @@ -51,18 +52,20 @@ export function lowerClassMethodValue( } const loc = locOf(expr); const receiver = lowerer.lowerExpr(expr.expression); + const declared = findMethodOn(lowerer, info, method); // A JS checker may name a class while the value is held in checked // storage. Read its actual property so own replacements and getters - // participate, and never pass a dynamic value to a native selector. - if (receiver.type.kind === "dyn") + // participate, and never pass a dynamic value to a native selector. A + // method whose private names belong to one evaluation is that + // evaluation's prototype method as well. + if (receiver.type.kind === "dyn" || (!!declared && perEvaluationPrivateNames(declared.declarer))) return { kind: "dynKeyGet", - value: receiver, + value: lowerer.coerceToExpected(receiver, DYN), key: { kind: "strLit", value: method, type: STRING, loc }, type: DYN, loc, }; - const declared = findMethodOn(lowerer, info, method); // Abstract declarations have no prototype function. Concrete descendants // provide the extracted value through the same reachability-driven selector. let value = declared?.sig.abstract @@ -537,10 +540,13 @@ export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunctio !(expr.kind === "closure" && expr.captures.includes(thisParam.localId)), }); if (usesThis) continue; - // A method that never uses this remains callable when detached. + // A method that never uses this remains callable when detached. Without + // class captures it needs no class environment, so the detached copy + // drops the (empty) capture list, which requires a receiver. const name = `%method.receiverless:${target.name}`; + const { classCaptures: _classCaptures, ...detached } = target; functions.push({ - ...target, + ...detached, name, params: target.params.slice(1), locals: target.locals.filter((local) => local.id !== thisParam.localId), diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 2787c9021..d337dc802 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -22,7 +22,7 @@ import { type ClassInfo, } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; -import { newFnCtx } from "./lowerer.js"; +import { PoisonError, newFnCtx } from "./lowerer.js"; import { classMethodValue } from "./class-method-values.js"; /** Data added to a top-level class prototype has shared identity and remains @@ -376,14 +376,37 @@ export function reflectClassPrototype( const method = ts.isMethodDeclaration(member) ? name : `${ts.isGetAccessor(member) ? "get" : "set"}:${name}`; - const value = classMethodValue(lowerer, info.decl, info, method, loc); + // A member whose value or key has no lowering refuses the reflection + // at run time; its own diagnostic must not escape this walk (the + // walk runs from emitReachable, after statement fences are placed). + let before = lowerer.diags.length; + let value: IrExpr | null; + try { + value = classMethodValue(lowerer, info.decl, info, method, loc); + } catch (error) { + if (!(error instanceof PoisonError)) throw error; + lowerer.diags.splice(before); + return refuse( + "prototype reflection with a member that has no lowering is not supported yet", + ); + } if (!value || !lowerer.dynConvertible(value.type)) return refuse( "prototype reflection with a member that cannot cross a checked value boundary is not supported yet", ); - const key: IrExpr = ts.isComputedPropertyName(member.name) - ? lowerer.lowerExpr(member.name.expression) - : { kind: "strLit", value: member.name.text, type: STRING, loc }; + let key: IrExpr; + before = lowerer.diags.length; + try { + key = ts.isComputedPropertyName(member.name) + ? lowerer.lowerExpr(member.name.expression) + : { kind: "strLit", value: member.name.text, type: STRING, loc }; + } catch (error) { + if (!(error instanceof PoisonError)) throw error; + lowerer.diags.splice(before); + return refuse( + "prototype reflection with a computed member key that has no lowering is not supported yet", + ); + } initialization.then.push( define( key, @@ -556,6 +579,15 @@ function prototypeProtectedKeys( const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : []; const entry = entries.find(([, field]) => field === name); const declaration = entry ? entry[0].valueDeclaration : undefined; + // A CommonJS key-table property holds its key in the table's global. + const tableKey = + declaration && ts.isPropertyAssignment(declaration) + ? lowerer.globalsBySymbol.get(entry![0]) + : undefined; + if (tableKey) { + keys.push({ kind: "varRef", localId: tableKey.id, type: tableKey.type, loc }); + continue; + } if ( !declaration || !ts.isVariableDeclaration(declaration) || diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index b99c24a15..445014b88 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -111,12 +111,15 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool } /** Whether all parameters and the result adapt without a trap-only path. - * Rest signatures are excluded; extra destination parameters are ignored. */ + * Rest signatures are excluded, except a typed-rest source in a slot with + * fixed parameters (the rest packing adapter); extra destination + * parameters are ignored. */ export function cleanFuncAdaptable( lowerer: Lowerer, src: IrType & { kind: "func" }, dst: IrType & { kind: "func" }, ): boolean { + if (restPackingAdapterFits(lowerer, src, dst)) return true; if (src.rest === true || dst.rest === true) return false; if (src.params.length > dst.params.length) return false; for (let i = 0; i < src.params.length; i++) { @@ -127,15 +130,145 @@ export function cleanFuncAdaptable( return src.ret.kind === "void" && dst.ret.kind === "dyn"; } +/** Whether a typed-rest function (`(...m: T[]) => R`) can serve a slot + * with fixed parameters (`(m: T) => void`): the slot's parameters convert + * to the function's fixed parameters, then to the rest element type. */ +export function restPackingAdapterFits( + lowerer: Lowerer, + fromT: IrType & { kind: "func" }, + toT: IrType & { kind: "func" }, +): boolean { + if (fromT.rest !== true || fromT.restAbi !== "typed" || toT.rest === true) return false; + const fixed = fromT.params.length - 1; + const restT = fromT.params[fixed]; + if (restT?.kind !== "array" || toT.params.length < fixed) return false; + for (let i = 0; i < toT.params.length; i++) { + if (!lowerer.coercibleValue(toT.params[i]!, i < fixed ? fromT.params[i]! : restT.elem)) + return false; + } + return toT.ret.kind === "void" || lowerer.coercibleValue(fromT.ret, toT.ret); +} + +/** The adapter for restPackingAdapterFits: forwards the fixed arguments and + * packs the remaining ones into the rest array, as a JavaScript call of the + * function with those arguments does. */ +function restPackingAdapter( + lowerer: Lowerer, + fromT: IrType & { kind: "func" }, + toT: IrType & { kind: "func" }, + loc: SrcLoc, +): string { + const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + lowerer.coercions.retags.set(key, name); + lowerer.freshClosureAdapters.add(name); + const fixed = fromT.params.length - 1; + const restT = fromT.params[fixed] as IrType & { kind: "array" }; + const params: IrParam[] = toT.params.map((t, i) => ({ + localId: `a.${i}`, + name: `a${i}`, + type: t, + })); + const arg = (i: number, want: IrType): IrExpr => + lowerer.coerceToExpected( + { kind: "varRef", localId: `a.${i}`, type: toT.params[i]!, loc }, + want, + ); + const args: IrExpr[] = []; + for (let i = 0; i < fixed; i++) args.push(arg(i, fromT.params[i]!)); + const packed: IrExpr[] = []; + for (let i = fixed; i < toT.params.length; i++) packed.push(arg(i, restT.elem)); + args.push({ kind: "arrayLit", elems: packed, type: restT, loc }); + const call: IrExpr = { + kind: "callValue", + callee: { kind: "varRef", localId: "f.0", type: fromT, loc }, + receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, + args, + type: fromT.ret, + loc, + }; + const body: IrStmt[] = + toT.ret.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: lowerer.coerceToExpected(call, toT.ret), loc }]; + lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc)); + return name; +} + +/** Adapt a fixed-parameter function to a checked-dynamic rest signature + * without fixed parameters (`() => {}` into mock.fn()'s `(...args: any[]) + * => undefined`): the adapter receives the hidden argument array and + * passes its element i as parameter i, undefined when the caller passed + * fewer arguments, as JS does. Null unless every parameter is + * checked-dynamic, which holds a missing argument as undefined, and the + * result is discarded or converts. */ +function funcIntoDynRestAdapter( + lowerer: Lowerer, + fromT: IrType & { kind: "func" }, + toT: IrType & { kind: "func" }, + loc: SrcLoc, +): string | null { + if (fromT.rest === true || fromT.params.some((p) => p.kind !== "dyn")) return null; + if (toT.ret.kind !== "void" && !lowerer.coercibleValue(fromT.ret, toT.ret)) return null; + const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + 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 }]; + const call: IrExpr = { + kind: "callValue", + callee: { kind: "varRef", localId: "f.0", type: fromT, loc }, + receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, + args: fromT.params.map((_, i): IrExpr => ({ + kind: "dynKeyGet", + value: { kind: "varRef", localId: "rest.0", type: DYN, loc }, + key: { kind: "strLit", value: String(i), type: STRING, loc }, + type: DYN, + loc, + })), + type: fromT.ret, + loc, + }; + let body: IrStmt[]; + if (toT.ret.kind === "void") { + body = [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ]; + } else { + const result = lowerer.coerceToExpected(call, toT.ret); + if (!typeEquals(result.type, toT.ret)) + throw new InternalCompilerError("lowerer bug: probed fn-adapter return stopped coercing"); + body = [{ kind: "return", value: result, loc }]; + } + lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc)); + return name; +} + /** Adapt a function value to a destination signature, forwarding and * converting its arguments and result. Unsupported parameter/result pairs - * produce checked traps when called; rest signatures remain refused. */ + * produce checked traps when called. Rest signatures are refused, except a + * typed-rest source in a slot with fixed parameters, which the rest + * packing adapter serves, and a fixed-parameter function into a + * checked-dynamic rest signature without fixed parameters + * (funcIntoDynRestAdapter). */ export function funcCoerceAdapter( lowerer: Lowerer, fromT: IrType & { kind: "func" }, toT: IrType & { kind: "func" }, loc: SrcLoc, ): string | null { + if (restPackingAdapterFits(lowerer, fromT, toT)) + return restPackingAdapter(lowerer, fromT, toT, loc); + if (toT.rest === true && toT.restAbi === undefined && toT.params.length === 0) + return funcIntoDynRestAdapter(lowerer, fromT, toT, loc); if (fromT.rest === true || toT.rest === true) return null; if (fromT.params.length > toT.params.length) return null; // Piece dispositions beyond coercibleValue, all CHECKER-APPROVED diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 2a8de78b9..cecbde717 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -66,6 +66,10 @@ import { lowerAbstractEquality } from "../abstract-equality.js"; import { lowerInExpression } from "./relational.js"; import { lowerDynTypeofTest, lowerErrorCodeTypeofTest } from "./typeof.js"; import { builtinClassRefOf, sameBuiltinClass } from "../builtins/class-values.js"; +import { + lowerRuntimeConstructorIs, + runtimeConstructorReceiver, +} from "../builtins/runtime-constructor.js"; import { tryLowerIndexedComparison } from "../indexed-comparison.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "../strict-equality.js"; import { withUnitArm } from "../../type-mapper.js"; @@ -1322,6 +1326,24 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr lowerUnionTypeofTest(lowerer, expr, loc); if (test) return test; } + // `x.constructor === Readable` for an instance of EventEmitter or a + // stream class: the instance's own class, compared without a class value. + if ( + op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken + ) { + for (const [side, other] of [ + [expr.left, expr.right], + [expr.right, expr.left], + ] as const) { + const ctor = runtimeConstructorReceiver(lowerer, side); + const target = ctor ? builtinClassRefOf(lowerer, other) : null; + if (ctor && (target?.kind === "emitter" || target?.kind === "stream")) { + const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken; + return lowerRuntimeConstructorIs(lowerer, ctor.receiver, target.info, negated, loc); + } + } + } // `C === EventEmitter` between two builtin class references: the // classes are compile-time values, so the answer folds. if ( diff --git a/packages/compiler/src/frontend/lowering/expressions/hosted-narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/hosted-narrowing.ts new file mode 100644 index 000000000..87a7ba38e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/expressions/hosted-narrowing.ts @@ -0,0 +1,157 @@ +/* Narrowings that refine record arms without naming a stored arm. */ +import type { Lowerer } from "../lowerer.js"; +import { type IrExpr, type IrType, type SrcLoc, typeEquals } from "../../../ir/ir.js"; + +/** A checker narrowing can refine the arms of a union of records with an + * intersection: a predicate `r is Route & { path: string }`, or the + * `"path" in r` narrowing, which intersects arms that do not declare the + * name with `Record<"path", unknown>`. Each narrowed arm then maps to a + * new record shape that is not an arm of the stored union, although the + * value never changed layout. A stored record arm HOSTS a narrowed record + * when both name the same members, each narrowed member refined to a + * subset of the stored slot type (or to `unknown`); a member an assertion + * attached to the stored shape (assertion-members.ts) may be missing from + * the narrowed one. A required member the stored arm does not declare + * cannot exist on its values: native records are closed, so `in` already + * answers false there and the narrowed arm is uninhabited. + * + * Only unions of two or more record arms without a literal discriminant + * qualify: discriminated unions narrow by their discriminant, and a + * record with unit arms keeps its own predicate-strengthening reads. + * + * Returns the stored arms that can hold a value of the narrowed type, or + * null when the union does not qualify, when some narrowed arm is neither + * a stored arm nor a record, or when no stored arm is excluded (there is + * nothing to narrow). */ +export function hostedArmTags( + lowerer: Lowerer, + fromId: string, + narrowed: IrType, +): Set | null { + const from = lowerer.unions.get(fromId); + if (!from || !recordUnion(lowerer, fromId)) return null; + const narrowedArms = + narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed]; + if (!narrowedArms || narrowedArms.length === 0) return null; + const allowed = new Set(); + let refined = false; + for (const arm of narrowedArms) { + const direct = lowerer.armTag(fromId, arm); + if (direct >= 0) { + allowed.add(direct); + continue; + } + if (arm.kind !== "record") return null; + refined = true; + from.arms.forEach((stored, tag) => { + if (stored.kind === "record" && recordHosts(lowerer, stored.shapeId, arm.shapeId)) { + allowed.add(tag); + } + }); + } + if (!refined || allowed.size === 0 || allowed.size === from.arms.length) return null; + return allowed; +} + +/** A union of at least two record arms without a literal discriminant. */ +function recordUnion(lowerer: Lowerer, unionId: string): boolean { + const def = lowerer.unions.get(unionId); + if (!def || def.discriminant) return false; + return def.arms.filter((arm) => arm.kind === "record").length >= 2; +} + +/** Whether every value of `narrowed` (a checker refinement of a value + * stored as `storage`) lives in the stored layout: each narrowed arm is a + * stored arm or a record a stored record arm hosts (see hostedArmTags), + * and at least one needs the hosting. A union storage must qualify as in + * hostedArmTags; a single record only when an assertion attached members + * to it. A binding initialized by such an assertion keeps the stored type, + * so its member writes reach the asserted object. */ +export function hostsAll(lowerer: Lowerer, storage: IrType, narrowed: IrType): boolean { + if (typeEquals(storage, narrowed)) return false; + let storedArms: readonly IrType[]; + if (storage.kind === "union") { + if (!recordUnion(lowerer, storage.unionId)) return false; + storedArms = lowerer.unions.get(storage.unionId)!.arms; + } else if (storage.kind === "record" && lowerer.shapes.attachedNames(storage.shapeId).length) { + storedArms = [storage]; + } else { + return false; + } + const narrowedArms = + narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed]; + if (!narrowedArms || narrowedArms.length === 0) return false; + let refined = false; + for (const arm of narrowedArms) { + if (storedArms.some((stored) => typeEquals(stored, arm))) continue; + if (arm.kind !== "record") return false; + const hosted = storedArms.some( + (stored) => stored.kind === "record" && recordHosts(lowerer, stored.shapeId, arm.shapeId), + ); + if (!hosted) return false; + refined = true; + } + return refined; +} + +function recordHosts(lowerer: Lowerer, storedId: string, narrowedId: string): boolean { + const stored = lowerer.shapes.get(storedId); + const narrowed = lowerer.shapes.get(narrowedId); + if (!stored || !narrowed || stored.tuple || narrowed.tuple) return false; + if (stored.indexValue || narrowed.indexValue) return false; + const attached = lowerer.shapes.attachedNames(storedId); + for (const sf of stored.fields) { + if (!narrowed.fields.some((nf) => nf.name === sf.name) && !attached.includes(sf.name)) + return false; + } + for (const nf of narrowed.fields) { + const sf = stored.fields.find((f) => f.name === nf.name); + if (!sf || !slotRefines(lowerer, nf.type, sf.type)) return false; + } + return true; +} + +/** Whether a stored slot of type `stored` can be read as `narrowed`: the + * same type, `unknown`, or a subset of the stored union's arms (a read + * site narrows from the stored slot exactly as it does for a predicate + * strengthening one record). */ +function slotRefines(lowerer: Lowerer, narrowed: IrType, stored: IrType): boolean { + if (typeEquals(narrowed, stored) || narrowed.kind === "dyn") return true; + if (stored.kind !== "union") return false; + if (narrowed.kind !== "union") return lowerer.armTag(stored.unionId, narrowed) >= 0; + const arms = lowerer.unions.get(narrowed.unionId)?.arms; + return !!arms && arms.every((arm) => lowerer.armTag(stored.unionId, arm) >= 0); +} + +/** The stored view of a hosted narrowing (see hostedArmTags): one stored + * record arm extracted with the checked helper, or the sub-union of the + * hosting arms re-tagged with the excluded arms as traps. Both keep the + * value's stored layout, so member reads and writes reach the same + * object. Null when the narrowing is not a hosted one. */ +export function hostedStorageView( + lowerer: Lowerer, + value: IrExpr, + narrowed: IrType, + loc: SrcLoc, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const fromId = value.type.unionId; + const allowed = hostedArmTags(lowerer, fromId, narrowed); + if (!allowed) return null; + const from = lowerer.unions.get(fromId)!; + if (allowed.size === 1) { + const arm = from.arms[[...allowed][0]!]!; + const helper = lowerer.narrowedArmHelper(fromId, arm, loc); + return helper ? lowerer.checkedNarrowCall(helper, value, arm, loc) : null; + } + const arms = from.arms.filter((_, tag) => allowed.has(tag)); + const subId = lowerer.unions.transform(from, arms); + const trappable = new Set(); + from.arms.forEach((_, tag) => { + if (!allowed.has(tag)) trappable.add(tag); + }); + const helper = lowerer.unionRetagHelper(fromId, subId, loc, trappable); + if (!helper) return null; + const type: IrType = { kind: "union", unionId: subId }; + return { kind: "call", callee: helper, args: [value], type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 5a8169300..d7a58800e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,4 +1,5 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ +import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; import * as ts from "../../ts7/adapter.js"; import * as posix from "node:path/posix"; import { type Lowerer, PoisonError } from "../lowerer.js"; @@ -351,19 +352,25 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // MODULE compile-time constants, the containing file's location // (Node's exact values when the same tree runs in place; WASI uses // the equivalent guest-visible path; locals shadowing the names - // resolved above). In a file with ESM syntax Node never defines them - // (ReferenceError) — fence rather than invent a value there. + // resolved above), the same strings as import.meta.dirname/filename. + // In a JavaScript ES module Node never defines them, so the read throws + // Node's catchable ReferenceError. A .ts/.tsx file keeps them even when + // Node's strip-types rule would load it as ESM: TypeScript projects are + // commonly built as CommonJS bundles (tsc, esbuild, tsx) where these + // globals exist, and @types/node declares them for every file. if ( (expr.text === "__dirname" || expr.text === "__filename") && lowerer.isStdlibGlobal(expr, expr.text) ) { const sf = expr.getSourceFile(); - if (isNodeEsmFile(sf, lowerer.program)) { - lowerer.unsupported( - "SC1090", - expr, - `'${expr.text}' in an ES module (Node defines the CJS module globals only in CommonJS modules)`, - ); + if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) { + return { + kind: "libCall", + fn: "global.undefRead", + args: [{ kind: "strLit", value: expr.text, type: STRING, loc }], + type: STRING, + loc, + }; } // The WASI runner mounts the host cwd at guest `/` and the host temp // directory at `/tmp`. Bake the matching GUEST spelling so resource @@ -859,6 +866,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: lowerer.checker.getSymbolAtLocation(expr), ); } + // An import of a CommonJS member export whose value is a binding of the + // exporting module (`module.exports = Client; module.exports.StatsD = + // Client`): the member holds that binding's value. + const memberAlias = cjsMemberAliasValueOf(lowerer, expr); + if (memberAlias !== null) return lowerer.lowerExpr(memberAlias); // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 207000717..b54da8180 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -29,6 +29,7 @@ import { isOptionalProcessStreamProperty } from "../builtins/process.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; import { checkedArmExtraction } from "../coercions/unions.js"; +import { hostedStorageView, hostsAll } from "./hosted-narrowing.js"; import { nodeThrowExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import { @@ -659,6 +660,12 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx }; } } + // A narrowing that refines record arms with an intersection keeps the + // stored layout: view the value as the stored arms that can hold it. + if (narrowed?.kind === "union") { + const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc); + if (hosted) return hosted; + } // A checker type narrowed to a UNIT arm (the `=== undefined` branch) // also stays union-typed: a unit arm has no payload to extract, and // nothing useful reads such a value anyway. (Standalone undefined maps @@ -668,6 +675,14 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx return expr; } const tag = lowerer.armTag(expr.type.unionId, narrowed); + if ( + tag < 0 && + narrowed.kind === "record" && + !lowerer.unions.get(expr.type.unionId)?.discriminant + ) { + const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc); + if (hosted) return hosted; + } if (tag < 0) { // The checker may refine fields inside a discriminated record too. // That creates a different mapped shape, but does not change the @@ -926,6 +941,10 @@ export function runtimeOptionalReceiverRead( loc, ); if (!helper) { + // Intersection-refined record arms read through their stored + // layout (the predicate `r is Route & { path: string }`). + const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc); + if (hosted) return hosted; lowerer.unsupported( "SC1090", expr, @@ -946,6 +965,10 @@ export function runtimeOptionalReceiverRead( // path below; mixed storage also needs to validate its concrete class. if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view; } + if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) { + const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc); + if (hosted) return hosted; + } const undefTag = lowerer.armTag(local.type.unionId, UNDEFINED_T); const def = lowerer.unions.get(local.type.unionId); if (!def || undefTag < 0) @@ -1151,6 +1174,18 @@ export function withRuntimeOptionalNarrowed( * memory); * - dyn → anything else (closures, class instances, void) is rejected: * those types cannot be found inside a JSON dyn. */ +/** A class instance, or a union of class instances and unit arms (a + * runtime-optional binding such as `const node = stack.pop()!`). */ +function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "object") return true; + if (type.kind !== "union") return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return ( + arms.some((arm) => arm.kind === "object") && + arms.every((arm) => arm.kind === "object" || isUnitType(arm)) + ); +} + /** `e as T` and the old-style assertion `e` — one node shape (both * carry `.type` and `.expression`), one lowering. */ export function lowerAsExpression( @@ -1228,8 +1263,10 @@ export function lowerAsExpression( if (target?.kind === "dyn" && (targetTs0.flags & ts.TypeFlags.Object) !== 0) { // A class instance asserted to a class-backed protocol view (a type // class instances flow into): the same identity-keeping box an - // implicit flow into the view uses. - if (inner.type.kind === "object" && lowerer.classImplementedProtocol(targetTs0)) + // implicit flow into the view uses. A runtime-optional binding of a + // class (`const node = stack.pop()!`) boxes the same way, and its + // undefined arm stays undefined, as in Node. + if (classInstanceOrAbsent(lowerer, inner.type) && lowerer.classImplementedProtocol(targetTs0)) return lowerer.coerceInto(expr, inner, DYN); const sourceShape = inner.type.kind === "record" ? lowerer.shapes.get(inner.type.shapeId) : undefined; @@ -1286,6 +1323,12 @@ export function lowerAsExpression( ) { return lowerer.coerceInto(expr, inner, target); } + // `job as Job & { startedAt: number }` on a record: the asserted + // record refines the stored one, so the assertion keeps the object + // (a width copy would sever writes through the asserted view). + if (inner.type.kind === "record" && target && hostsAll(lowerer, inner.type, target)) { + return inner; + } // Static assertions normally erase, but record layouts are // monomorphic: a consumer selected from the asserted shape must see // that shape physically. Reuse the ordinary slot coercion so plain @@ -1312,6 +1355,12 @@ export function lowerAsExpression( return lowerer.checkedNarrowCall(helper, inner, target, locOf(expr)); } } + // An intersection refining the stored record arms keeps the value + // (hosted-narrowing.ts): the stored arms that can hold it. + if (target && (target.kind === "union" || target.kind === "record")) { + const hosted = hostedStorageView(lowerer, inner, target, locOf(expr)); + if (hosted) return hosted; + } } // `node.parent as Sub | undefined`: a union target whose class arm is // narrower than the stored class gets the same checked class view as diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 5ef3991da..4366b8d6b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -214,6 +214,37 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp !nullish && !and && element.kind === "f64" ? arrayNumberRead(receiver, key, elementLoc) : arrayValueRead(lowerer, receiver, key, element, elementLoc); + } else if ( + receiver.type.kind === "record" && + key.type.kind === "string" && + pureIndexShapeOf(lowerer, receiver.type.shapeId) !== null + ) { + // `timings[phase] ??= v` over a pure index-signature record + // (`Record`): the keyed read answers the value or + // undefined for a missing key, and the write inserts or replaces + // the key, exactly the two halves `o[k]` and `o[k] = v` lower to. + const shapeId = receiver.type.shapeId; + const indexValue = pureIndexShapeOf(lowerer, shapeId)!; + key = save(key, "%shortCircuitKey"); + const readType = + indexValue.kind === "dyn" ? indexValue : lowerer.withUndefinedArmOf(indexValue); + if (!readType) { + lowerer.unsupported( + "SC1090", + target, + "short-circuit assignment through this indexed receiver", + ); + } + storage = indexValue; + read = { kind: "recordKeyGet", obj: receiver, shapeId, key, type: readType, loc }; + write = (value) => ({ + kind: "recordKeySet", + obj: receiver, + shapeId, + key, + value: lowerer.coerceToExpected(value, indexValue), + loc, + }); } else { lowerer.unsupported( "SC1090", @@ -265,3 +296,11 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp } return finish({ kind: "nullish", left: read, right, type: read.type, loc }); } + +/** The value type of a pure index-signature record shape (no declared + * fields, not a tuple), or null. */ +function pureIndexShapeOf(lowerer: Lowerer, shapeId: string): IrType | null { + const shape = lowerer.shapes.get(shapeId); + if (!shape || shape.tuple || shape.fields.length > 0 || !shape.indexValue) return null; + return shape.indexValue; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index b7414f986..98fc9397d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,5 +1,6 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ import * as ts from "../../ts7/adapter.js"; +import { localPrivateBrandClass } from "../private-brands.js"; import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js"; import { ARRAY_METHODS, @@ -76,6 +77,11 @@ import { lifetimeRegionMemberOf } from "../../experimental-import.js"; import { lowerInternTableProperty } from "../intern-tables.js"; import { lowerThreadSlotsProperty } from "../thread-slots.js"; import { builtinClassRefOf, runtimeClassValueOfType } from "../builtins/class-values.js"; +import { + lowerRuntimeConstructor, + lowerRuntimeConstructorName, + runtimeConstructorReceiver, +} from "../builtins/runtime-constructor.js"; import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js"; import { lowerClassPrototypeData } from "../class-prototypes.js"; import { lowerEnumAccess } from "../lower-enums.js"; @@ -290,6 +296,16 @@ export function lowerPropertyAccessExpr( const ref = builtinClassRefOf(lowerer, expr.expression); if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc }; } + // `x.constructor.name` and `x.constructor` on an instance of + // EventEmitter or a stream class (builtins/runtime-constructor.ts). + if (expr.name.text === "name" && !expr.questionDotToken) { + const ctor = runtimeConstructorReceiver(lowerer, expr.expression); + if (ctor) return lowerRuntimeConstructorName(lowerer, ctor.receiver, loc); + } + { + const ctor = runtimeConstructorReceiver(lowerer, expr); + if (ctor) return lowerRuntimeConstructor(lowerer, expr, ctor.info); + } // `C.name` through a runtime-provided class VALUE (a `typeof Readable` // parameter): the class object's stored name. if ( @@ -3205,6 +3221,22 @@ export function fieldTarget( fieldType: F64, }; } + // `this.#x` in a function-local class's prototype method: the field of + // the brand-checked receiver (checkedPrivateReceiver). + if ( + ts.isPrivateIdentifier(access.name) && + access.expression.kind === ts.SyntaxKind.ThisKeyword && + lowerer.resolveThis()?.type.kind === "dyn" + ) { + const owner = localPrivateBrandClass(lowerer, access.name); + if (owner) { + const ownerType: IrType & { kind: "object" } = { + kind: "object", + className: owner.info.def.name, + }; + return classFieldTarget(lowerer, access.expression, ownerType, access.name.text); + } + } const stored = ts.isIdentifier(access.expression) ? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type) : undefined; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 55baa8014..4aa62534e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,5 +1,11 @@ /* instanceof and in, including private-name and runtime-key in. */ import * as ts from "../../ts7/adapter.js"; +import { + checkedPrivateIn, + localPrivateBrandClass, + primitiveOperand, + privateInMessage, +} from "../private-brands.js"; import type { Lowerer } from "../lowerer.js"; import { BOOL, @@ -17,20 +23,22 @@ import { UNDEFINED_T, isUnitType, typeEquals, + typeKey, } from "../../../ir/ir.js"; import { refuseWebAssemblyUse } from "../builtins/webassembly.js"; import { lowerSocketInstanceOf } from "../lower-server.js"; import { builtinClassRefOf } from "../builtins/class-values.js"; import { lowerZlibInstanceOf } from "../builtins/compression.js"; -import { varRef } from "../../../ir/build.js"; +import { numLit, strLit, varRef } from "../../../ir/build.js"; import { lowerDisposableStackInstanceOf } from "../builtins/disposable-stack.js"; import { lowerBlobInstanceof } from "../builtins/blob.js"; +import { lowerReadlineInstanceof } from "../builtins/readline.js"; import { lowerKeyObjectInstanceof } from "../builtins/key-object.js"; import { lowerCryptoKeyInstanceof } from "../builtins/webcrypto.js"; import { lowerNetAddressInstanceof } from "../builtins/net-address.js"; import { fenceEarlyNsMemberRef, nsMemberIdentOf } from "../lower-namespaces.js"; import { maybePromiseInstanceOf } from "../maybe-promise.js"; -import { isSafeToDiscard } from "./evaluation-safety.js"; +import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { type ClassInfo, storedClassValueType } from "../lower-classes.js"; import { classInstanceOf } from "../class-dynamic-dispatch.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -172,6 +180,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; + const readlineInterface = lowerReadlineInstanceof(lowerer, expr); + if (readlineInterface) return readlineInterface; } if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); @@ -1038,6 +1048,76 @@ function lowerPrivateIn( ); } } + // A checked value in JavaScript (`#x in this` inside a method, whose + // receiver is the actual call-site value): the brand is present exactly + // when the value is a class instance at or below the declarer. A typed + // reference carries the class it was boxed under: the declarer or a + // descendant answers true; an ancestor answers per instance. + if (recv.type.kind === "dyn" && recv.kind === "varRef" && isJsSourceFile(expr.getSourceFile())) { + // A function-local class has one brand per evaluation. + if (info.localClass) { + const owner = localPrivateBrandClass(lowerer, priv); + if (!owner) { + lowerer.unsupported( + "SC1090", + expr, + `'${pname} in …' inside a function-local class that is not a named declaration`, + ); + } + return checkedPrivateIn(lowerer, owner.info, owner.decl, pname, recv, loc); + } + const typedRefIs = (className: string): IrExpr => ({ + kind: "libCall", + fn: "dyn.typedRefIs", + args: [ + recv, + { kind: "strLit", value: typeKey({ kind: "object", className }), type: STRING, loc }, + ], + type: BOOL, + loc, + }); + const or = (left: IrExpr | null, right: IrExpr): IrExpr => + left === null ? right : { kind: "logical", op: "||", left, right, type: BOOL, loc }; + let test: IrExpr | null = null; + const below = (c: ClassInfo): void => { + test = or(test, typedRefIs(c.def.name)); + for (const sub of c.subclasses) below(sub); + }; + below(info); + for (let base = info.base; base; base = base.base) { + const baseType: IrType = { kind: "object", className: base.def.name }; + test = or(test, { + kind: "logical", + op: "&&", + left: typedRefIs(base.def.name), + right: { + kind: "instanceOf", + value: { kind: "dynCheck", value: recv, type: baseType, loc }, + className: declName, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }); + } + // Only objects carry private names: a primitive operand throws Node's + // TypeError. + return { + kind: "ternary", + cond: primitiveOperand(recv, loc), + then: { + kind: "libCall", + fn: "error.nodeThrow", + args: [numLit(1, loc), strLit("", loc), privateInMessage(pname, recv, loc)], + type: BOOL, + loc, + }, + else_: test!, + type: BOOL, + loc, + }; + } if (recv.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -1349,13 +1429,14 @@ export function lowerInExpression( // arm answers membership STATICALLY (a declared non-optional field is // always present, an undeclared name never is), so the whole test // collapses to runtime TAG tests — tsc's own narrowing then types the - // branches (the discriminating-`in` idiom). Optional (undefined-armed) - // fields and tuple/index-signature arms answer per VALUE, not per arm - // — those unions keep the fence below. + // branches (the discriminating-`in` idiom). An arm declaring the key + // as an optional (undefined-armed) slot answers per VALUE instead: + // its tag test joins that payload's presence test (see + // perValueUnionIn). Tuple/index-signature arms keep the fence below. if (recv.type.kind === "union") { const unionId = recv.type.unionId; const arms = lowerer.unions.get(unionId)?.arms ?? []; - const answers: { tag: number; has: boolean }[] = []; + const answers: { tag: number; has: boolean | "slot"; arm: IrType }[] = []; let staticAnswers = arms.length > 0; for (const arm of arms) { const tag = lowerer.armTag(unionId, arm); @@ -1372,14 +1453,17 @@ export function lowerInExpression( f.type.kind === "union" && lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 ) { - staticAnswers = false; // an optional slot: presence is per-value - break; + answers.push({ tag, has: "slot", arm }); // presence is per-value + continue; } // Accessor properties are own properties to `in` (Node answers // true without invoking the getter) — either slot present makes // the name a member. const acc = shape.fields.some((x) => x.name === `%get:${key}` || x.name === `%set:${key}`); - answers.push({ tag, has: f !== undefined || acc || inherited }); + answers.push({ tag, has: f !== undefined || acc || inherited, arm }); + } + if (staticAnswers && answers.some((a) => a.has === "slot")) { + return perValueUnionIn(lowerer, recv, unionId, key, answers, loc); } if (staticAnswers) { const pureRecv = @@ -1531,6 +1615,75 @@ export function lowerInExpression( ); } +/** `"k" in u` over a union of fixed record shapes where some arm declares + * `k` as an optional slot. Arms without the name answer false and arms + * with an always-present member answer true, both by tag; an optional + * slot answers its payload's own-property presence (an explicit + * undefined is present, an omitted or deleted property is not), read + * through the checked arm extraction the tag test has just proven. The + * receiver evaluates once: a non-repeatable one is stabilized in a + * hidden local first. */ +function perValueUnionIn( + lowerer: Lowerer, + recv: IrExpr, + unionId: string, + key: string, + answers: readonly { tag: number; has: boolean | "slot"; arm: IrType }[], + loc: SrcLoc, +): IrExpr { + const recvType = recv.type; + const stable = isSafeToRepeat(recv) ? null : lowerer.declareHiddenLocal("%inRecv", recvType); + const ref = (): IrExpr => (stable ? varRef(stable.id, recvType, loc) : recv); + const isTag = (tag: number): IrExpr => ({ + kind: "unionIsTag", + unionId, + tag, + negated: false, + value: ref(), + type: BOOL, + loc, + }); + let out: IrExpr | null = null; + for (const a of answers) { + if (a.has === false) continue; + let test = isTag(a.tag); + if (a.has === "slot" && a.arm.kind === "record") { + test = { + kind: "logical", + op: "&&", + left: test, + right: { + kind: "recordHas", + obj: { + kind: "unionNarrow", + unionId, + tag: a.tag, + value: ref(), + type: a.arm, + loc, + }, + shapeId: a.arm.shapeId, + field: key, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + out = out ? { kind: "logical", op: "||", left: out, right: test, type: BOOL, loc } : test; + } + const result: IrExpr = out ?? { kind: "boolLit", value: false, type: BOOL, loc }; + if (!stable) return result; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: stable.id, init: recv, loc }], + result, + type: BOOL, + loc, + }; +} + /** The runtime-key `in` (see lowerInExpression): `k in r` where k is a * runtime string and r an index-signature record — an interned * `%rec.haskey.(k, r)` walks the declared names (a string-equality diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index 30d24e22a..c56ef4097 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -13,6 +13,7 @@ import { } from "../../../ir/ir.js"; import { lowerWebAssemblyTypeof } from "../builtins/webassembly.js"; import { isNavigatorGlobal } from "../builtins/navigator.js"; +import { processHasIpc } from "../builtins/process.js"; import { lowerPerfHooksTypeof } from "../builtins/performance.js"; import { isNodeModuleValue } from "../lower-node-module.js"; import { builtinClassRefOf } from "../builtins/class-values.js"; @@ -27,7 +28,7 @@ import { typeofAnswer, } from "../lower-exprs.js"; import { tryLowerExpression } from "./try-lower-expression.js"; -import { isJsSourceFile } from "../../program.js"; +import { isJsSourceFile, isNodeEsmFile } from "../../program.js"; import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { varRef } from "../../../ir/build.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; @@ -38,8 +39,41 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (webAssembly) return webAssembly; if (lowerer.isStdlibGlobal(expr.expression, "process")) return { kind: "strLit", value: "object", type: STRING, loc }; + { + let operand = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + const ipcMember = + ts.isPropertyAccessExpression(operand) && !operand.questionDotToken + ? lowerer.stdlibGlobalMember(operand, "process") + : null; + if (ipcMember === "send" || ipcMember === "disconnect") { + return { + kind: "ternary", + cond: processHasIpc(lowerer, loc), + then: { kind: "strLit", value: "function", type: STRING, loc }, + else_: { kind: "strLit", value: "undefined", type: STRING, loc }, + type: STRING, + loc, + }; + } + } if (lowerer.isStdlibGlobal(expr.expression, "fetch")) return { kind: "strLit", value: "function", type: STRING, loc }; + // `__dirname` / `__filename` in a JavaScript ES module, where Node never + // defines them: typeof answers "undefined" instead of throwing. + { + let operand = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if ( + ts.isIdentifier(operand) && + (operand.text === "__dirname" || operand.text === "__filename") && + lowerer.isStdlibGlobal(operand, operand.text) + ) { + const sf = operand.getSourceFile(); + if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) + return { kind: "strLit", value: "undefined", type: STRING, loc }; + } + } if (isNavigatorGlobal(lowerer, expr.expression)) return { kind: "strLit", value: "object", type: STRING, loc }; const performance = lowerPerfHooksTypeof(lowerer, expr.expression); diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 61943eff4..2270f6043 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -1,5 +1,7 @@ /* Increment, decrement and prefix unary operators, and compound field updates. */ import * as ts from "../../ts7/adapter.js"; +import { isStrictCode } from "../strict-mode.js"; +import { isPrivateWriteReceiver } from "../private-brands.js"; import type { Lowerer } from "../lowerer.js"; import { type FieldTarget, @@ -524,6 +526,19 @@ export function fieldSetStmt( loc: SrcLoc, blame: ts.Node, ): IrStmt { + // A write through a brand-checked receiver (checkedPrivateReceiver): the + // value evaluates first, then the brand check throws, as in Node. + if (isPrivateWriteReceiver(target.obj) && value.kind !== "varRef") { + const stored = lowerer.declareHiddenLocal("%privateWriteValue", value.type); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: stored.id, init: value, loc }, + fieldSetStmt(lowerer, target, varRef(stored.id, stored.type, loc), loc, blame), + ], + loc, + }; + } // A member write through an unassigned deferred-init field: the value // still evaluates first, then the write throws Node's TypeError. const deferredSlot = @@ -737,6 +752,7 @@ export function fieldSetStmt( className: target.className, field: target.field, value, + ...(isStrictCode(blame) ? {} : { sloppy: true as const }), loc, } : { diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index ba8001792..437a29563 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -224,12 +224,30 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const loops: number[] = []; let loopIds = 0; const loopKey = (): string => loops.join("/"); + /** Bindings initialized by another binding's reference (`const view = + * value`, the snapshot a union-receiver write takes), so writes through + * either count as writes through the one object. */ + const aliases = new Map(); + const bindingOf = (obj: IrExpr): string | null => { + let o = obj; + while (o.kind === "unionNarrow" || o.kind === "downcast") o = o.value; + if (o.kind !== "varRef") return null; + let id = o.localId; + const seen = new Set(); + for (;;) { + const alias = aliases.get(id); + if (!alias || alias.fn !== fnName || inits.get(id)?.count !== 1 || seen.has(id)) return id; + seen.add(id); + id = alias.target; + } + }; const noteInit = (id: string, init: IrExpr): void => { const previous = inits.get(id); if (previous !== undefined) { previous.count++; return; } + if (init.kind === "varRef") aliases.set(id, { target: init.localId, fn: fnName }); const fresh = init.kind === "recordLit" || init.kind === "recordClone" || @@ -315,7 +333,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio field: st.field, kind: st.value.kind === "fieldAbsent" ? "delete" : "write", fn: fnName, - binding: st.obj.kind === "varRef" ? st.obj.localId : null, + binding: bindingOf(st.obj), loop: loopKey(), pos, }); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 5a4838782..ae864c2cb 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -35,6 +35,7 @@ import { import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js"; import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerClassSymbolMethodCall } from "./symbol-methods.js"; +import { lowerReadlineDisposeCall } from "./builtins/readline.js"; import { errorToStringCall } from "./error-methods.js"; import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js"; import { @@ -226,6 +227,7 @@ import { } from "./optional-arguments.js"; import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; +import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerUrlFactory } from "./lower-url.js"; import { nullishValueUnitOf } from "./binding-analysis.js"; @@ -1683,6 +1685,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (timersDispose) return timersDispose; const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr); if (symbolMethod) return symbolMethod; + const readlineDispose = lowerReadlineDisposeCall(lowerer, expr); + if (readlineDispose) return readlineDispose; lowerer.fenceUnsupportedFetchConstructorMember(expr.expression); lowerer.fenceStaticResponseMember(expr.expression, "call"); lowerer.fenceStaticHeadersMember(expr.expression, "call"); @@ -6769,6 +6773,99 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str return helper; } +/** Whether `Object.assign(instance, record)` can copy every field of the + * record shape into a field the class declares with the same type: a plain + * record (no tuple, index signature or accessor slots) and a compiled + * class whose chain has no builtin or runtime-provided storage and no + * accessor for the written names. */ +function classAssignPlan(lowerer: Lowerer, className: string, srcShapeId: string): boolean { + const info = lowerer.classes.get(className); + const shape = lowerer.shapes.get(srcShapeId); + if (!info || !shape || shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape)) + return false; + for (let c: ClassInfo | null = info; c; c = c.base) { + if (c.builtinError || c.builtinEmitter || c.builtinStream !== undefined || c.def.runtime) + return false; + } + return shape.fields.every((f) => { + const declared = info.fields.get(f.name); + return ( + declared !== undefined && + (typeEquals(declared, f.type) || lowerer.widthLiftPlan(f.type, declared) !== null) && + !lowerer.findMethodOn(info, f.name) && + !lowerer.findMethodOn(info, `get:${f.name}`) && + !lowerer.findMethodOn(info, `set:${f.name}`) + ); + }); +} + +/** Interned `%obj.assignClass.(o, s)` — Object.assign's per-field + * write of a record into a class instance (classAssignPlan's gate): + * optional source fields write only when present, and the instance + * returns. */ +function classAssignHelper( + lowerer: Lowerer, + className: string, + srcShapeId: string, + loc: SrcLoc, +): string | null { + if (!classAssignPlan(lowerer, className, srcShapeId)) return null; + const key = `obj.assignClass:${className}:${srcShapeId}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, helper); + const sShape = lowerer.shapes.get(srcShapeId)!; + const oT: IrType = { kind: "object", className }; + const sT: IrType = { kind: "record", shapeId: srcShapeId }; + const oRef: IrExpr = { kind: "varRef", localId: "o.0", type: oT, loc }; + const sRef: IrExpr = { kind: "varRef", localId: "s.0", type: sT, loc }; + const body: IrStmt[] = []; + const info = lowerer.classes.get(className)!; + for (const f of sShape.fields) { + const declared = info.fields.get(f.name)!; + const read: IrExpr = { + kind: "recordGet", + obj: sRef, + shapeId: srcShapeId, + field: f.name, + type: f.type, + loc, + }; + const lift = typeEquals(declared, f.type) ? null : lowerer.widthLiftPlan(f.type, declared); + const set: IrStmt = { + kind: "fieldSet", + obj: oRef, + className, + field: f.name, + value: lift === null ? read : lowerer.applyWidthLift(lift, read, declared, loc), + loc, + }; + const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc); + body.push( + present.kind === "recordHas" + ? { kind: "if", cond: present, then: [set], else_: null, loc } + : set, + ); + } + body.push({ kind: "return", value: oRef, loc }); + lowerer.liftedFns.push({ + name: helper, + params: [ + { localId: "o.0", name: "o", type: oT }, + { localId: "s.0", name: "s", type: sT }, + ], + returnType: oT, + locals: [ + { id: "o.0", name: "o", type: oT, mutable: true }, + { id: "s.0", name: "s", type: sT, mutable: true }, + ], + body, + loc, + }); + return helper; +} + /** Interned `%obj.assign.(t, s)` — Object.assign's per-field copy * over signature-free records (every source field lands on a same-named, * same-typed target field — the caller's gate): optional source fields @@ -7410,6 +7507,28 @@ function lowerObjectStaticCall( return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc }; } } + // A CLASS-INSTANCE target and a plain record source whose fields are + // all fields the class declares with the same types + // (`Object.assign(this, { retries: 3 })`): the per-field write into + // the instance, returning it — the instance mutates in place. + if (tProbe?.type.kind === "object" && sProbe?.type.kind === "record") { + const helper = classAssignPlan(lowerer, tProbe.type.className, sProbe.type.shapeId); + if (helper) { + const loc = locOf(call); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const source = lowerer.lowerExpr(call.arguments.nodes[1]!); + if (target.type.kind === "object" && source.type.kind === "record") { + const name = classAssignHelper( + lowerer, + target.type.className, + source.type.shapeId, + loc, + ); + if (name) + return { kind: "call", callee: name, args: [target, source], type: target.type, loc }; + } + } + } if (tProbe?.type.kind === "record" && sProbe?.type.kind === "record") { const tShape = lowerer.shapes.get(tProbe.type.shapeId); const sShape = lowerer.shapes.get(sProbe.type.shapeId); @@ -7500,6 +7619,25 @@ function lowerObjectStaticCall( const srcNode = spread ? argNode.expression : argNode; const rawSource = lowerer.lowerExpr(srcNode); fenceSymbolFieldCopy(lowerer, srcNode, rawSource.type, spread); + // A class instance copied into a fresh object + // (`Object.assign({}, instance)`): the result is a plain + // object holding the instance's own fields, never an + // instance, so a result the checker types as the class has + // no representation (the checked cast would throw). + if ( + rawSource.type.kind === "object" && + freshLiteralTarget && + !ts.isExpressionStatement(call.parent) + ) { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (resultT !== null && resultT.kind === "object") { + lowerer.unsupported( + "SC1090", + srcNode, + `Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`, + ); + } + } const src = lowerer.coerceToExpected(rawSource, DYN); if (src.type.kind !== "dyn") { ok = false; @@ -7580,6 +7718,8 @@ function lowerObjectStaticCall( if (target && declaredTarget && isDynTypedRefType(declaredTarget)) target = lowerer.coerceToExpected(target, declaredTarget); if (target && member === "defineProperty") { + const field = lowerClassFieldDefine(lowerer, call, target); + if (field) return field; const native = lowerClassDataDescriptor(lowerer, call, member, target); if (native) return native; } @@ -7722,6 +7862,19 @@ function lowerObjectStaticCall( loc, }; } + if (member === "preventExtensions" || member === "seal") { + const integrity = lowerClassIntegrity(lowerer, call, member, value); + if (integrity) return integrity; + } else if (isDynTypedRefType(value.type)) { + // The instance's checked view carries its integrity state. + return { + kind: "libCall", + fn: member === "isSealed" ? "dyn.isSealed" : "dyn.isExtensible", + args: [lowerer.coerceToExpected(value, DYN)], + type: BOOL, + loc: locOf(call), + }; + } if (value.type.kind === "dyn") { const operation = { preventExtensions: "dyn.preventExtensions", @@ -7792,6 +7945,8 @@ function lowerObjectStaticCall( ) ) return value; + const integrity = lowerClassIntegrity(lowerer, call, "freeze", value); + if (integrity) return integrity; lowerer.noLowering( "Object.freeze of a possibly-aliased value", call, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 8a5170a7a..a31a62e61 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -88,7 +88,6 @@ import { isCjsJsFile, isJsSourceFile, isModuleExportsAccess, - isNodeTypesPath, locOf, } from "../program.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js"; @@ -1046,9 +1045,11 @@ export function builtinStreamInfoOf( if (rec.lib === symbol.name) irName = name; } if (!irName) return null; + // Both the bundled declarations and @types/node declare the classes + // inside `declare module "stream"` (@types/node nests them in the module's + // `internal` namespace). const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d)) return false; - if (isNodeTypesPath(d.getSourceFile().fileName)) return false; let node: ts.Node | undefined = d.parent; while (node) { if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { @@ -3368,6 +3369,56 @@ export function collectClassShapeInner( .map((p): ParamShape => ({ type: p.type, mode: "omittable" })), ); } + // A TypeScript override of a JavaScript method (`class T extends + // Agent` over an npm-static package) keeps the base's checked-value + // ABI: each untyped base slot stays dyn, and the override's typed + // parameter narrows from it at entry with a checked conversion. An + // untyped base return takes the override's value boxed. Calls on + // a statically typed receiver see the same ABI. + if ( + overridden && + !isJsSourceFile(member.getSourceFile()) && + overridden.declarer.decl !== null && + isJsSourceFile(overridden.declarer.decl.getSourceFile()) && + member.asteriskToken === undefined && + !overridden.sig.async && + !overridden.sig.gen && + shapes.length <= overridden.sig.params.length && + shapes.every((p, i) => { + const inherited = overridden.sig.params[i]!; + return ( + (p.mode === "required" || p.mode === "omittable") && + p.bodyType === undefined && + (inherited.mode === "required" || inherited.mode === "omittable") && + (typeEquals(p.type, inherited.type) || + (inherited.type.kind === "dyn" && lowerer.dynConvertible(p.type))) + ); + }) && + overridden.sig.params + .slice(shapes.length) + .every( + (p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable"), + ) + ) { + for (let i = 0; i < shapes.length; i++) { + const own = shapes[i]!; + if (overridden.sig.params[i]!.type.kind === "dyn" && own.type.kind !== "dyn") + shapes[i] = { type: DYN, mode: own.mode, entryNarrow: own.type }; + } + shapes.push( + ...overridden.sig.params + .slice(shapes.length) + .map((p): ParamShape => ({ type: p.type, mode: "omittable" })), + ); + if ( + overridden.sig.ret.kind === "dyn" && + ft.ret.kind !== "dyn" && + ft.ret.kind !== "void" && + lowerer.dynConvertible(ft.ret) + ) + ft.ret = DYN; + ft.params = shapes.map((shape) => shape.type); + } if ( overridden && member.asteriskToken === undefined && @@ -5412,6 +5463,75 @@ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string { return ""; } +/** Each evaluation of a function-local class creates fresh private names, + * while the compiled class shares one layout across evaluations. A + * JavaScript class's prototype methods are closures per evaluation, so the + * names agree when every private member is an instance field and every use + * is on `this` (privateNamesOnlyOnThis): native methods only run on their + * own evaluation's instances, and the closures, which receive any `this`, + * check the receiver's evaluation against the class's own binding + * (private-brands.ts), so the class must be a named declaration (undici's + * webidl FastIterableIterator). The checker rejects writes to that binding. */ +function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean { + if (!isJsSourceFile(expr.getSourceFile()) || !ts.isClassDeclaration(expr) || !expr.name) + return false; + if ( + expr.members.nodes.some( + (member) => + member.name !== undefined && + ts.isPrivateIdentifier(member.name) && + !ts.isPropertyDeclaration(member), + ) + ) + return false; + return privateNamesOnlyOnThis(expr); +} + +/** Every private name is used only on `this` inside the class's own instance + * members (`this.#x`, `#x in this`), outside nested classes and non-arrow + * functions, and the class has no static members that could hand instances + * across evaluations. */ +function privateNamesOnlyOnThis(expr: ts.ClassLikeDeclaration): boolean { + let ok = true; + for (const member of expr.members.nodes) { + if (ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) return false; + ts.walkPreorder(member, (node) => { + if (!ok) return "stop"; + // A nested class or non-arrow function rebinds `this`: any private + // name inside it disqualifies. + if ( + node !== member && + (ts.isClassDeclaration(node) || + ts.isClassExpression(node) || + ts.isFunctionExpression(node) || + ts.isFunctionDeclaration(node)) + ) { + ts.walkPreorder(node, (inner) => { + if (ts.isPrivateIdentifier(inner)) ok = false; + return ok ? undefined : "stop"; + }); + return "skip"; + } + if (ts.isPrivateIdentifier(node)) { + const parent = node.parent; + const onThis = + parent === member || + (ts.isPropertyAccessExpression(parent) && + parent.name === node && + parent.expression.kind === ts.SyntaxKind.ThisKeyword) || + (ts.isBinaryExpression(parent) && + parent.left === node && + parent.operatorToken.kind === ts.SyntaxKind.InKeyword && + parent.right.kind === ts.SyntaxKind.ThisKeyword); + if (!onThis) ok = false; + } + return undefined; + }); + if (!ok) return false; + } + return true; +} + /** Collect a class expression once per enclosing specialization. Function-local * evaluations allocate fresh constructor objects; top-level expressions use * immortal templates. Static initialization remains limited to whole top-level @@ -5449,7 +5569,8 @@ export function lowerClassExpressionInfo( } if ( local && - expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) + expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) && + !localPrivateFieldsAgree(expr) ) { lowerer.unsupported("SC1090", expr, "function-local classes with private brands"); } @@ -7942,6 +8063,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL className: info.def.name, field: f.name, value: dynUndefinedExpr(loc), + define: true, loc, }); } @@ -7957,6 +8079,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL className: info.def.name, field: f.name, value, + define: true, loc: locOf(f.initializer), }); } catch (e) { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index fb8db2657..40f87aff2 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js"; * ToBoolean/ToString coercion helpers, and field/element reads and writes * (FieldTarget). */ import * as ts from "../ts7/adapter.js"; +import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands.js"; import { constituentTypes } from "../ts7/checker.js"; import type { Lowerer } from "./lowerer.js"; import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js"; @@ -458,6 +459,20 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const local = lowerer.resolveThis(); if (local) { const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc }; + // `this.#x` in a function-local class's prototype method, whose `this` + // is the call-site value: the field lives on the native instance of + // this evaluation (checkedPrivateReceiver). + const access = expr.parent; + if ( + local.type.kind === "dyn" && + ts.isPropertyAccessExpression(access) && + access.expression === expr && + ts.isPrivateIdentifier(access.name) + ) { + const owner = localPrivateBrandClass(lowerer, access.name); + if (owner) + return checkedPrivateReceiver(lowerer, owner.info, owner.decl, access, value, loc); + } const initialized = lowerer.ctx.superInitialized; if (initialized) return { diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 245d1b676..3be155402 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -21,6 +21,7 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + CLASS_FIELD_ATTRIBUTES_PREFIX, classDynViewSupported, DYN_CLASS_PROPERTIES, isClassOwnEnumerableFieldName, @@ -500,12 +501,16 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined) ); } -/** Whether a class holds properties added outside its declared layout, which - * only its checked-dynamic view can list. Decided after lowering, when the +/** Whether a class holds properties added outside its declared layout, or + * fields Object.defineProperty can make non-enumerable, which only its + * checked-dynamic view lists as Node does. Decided after lowering, when the * class has its final fields. */ function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolean { return ( - info.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + info.def.fields.some( + (field) => + field.name === DYN_CLASS_PROPERTIES || field.name.startsWith(CLASS_FIELD_ATTRIBUTES_PREFIX), + ) && classDynViewSupported( info.def.fields, (id) => lowerer.shapes.get(id), @@ -2011,6 +2016,47 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc); } +/** %d and %i over a checked-dynamic argument, as Node's formatWithOptions + * reads it: a bigint prints as `${n}n`, a symbol as NaN, and any other + * value as `number` (Number(arg) for %d, parseInt(String(arg)) for %i) + * formatted. The argument evaluates once. */ +function formatDynInteger( + lowerer: Lowerer, + value: IrExpr, + number: (held: IrExpr) => IrExpr, + loc: SrcLoc, +): IrExpr { + const held = lowerer.declareHiddenLocal("%formatArg", DYN); + const ref = (): IrExpr => varRef(held.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: held.id, init: value, loc }], + result: { + kind: "ternary", + cond: { kind: "dynTest", test: "bigint", value: ref(), type: BOOL, loc }, + then: { + kind: "strConcat", + left: { kind: "libCall", fn: "dyn.toStringCoerce", args: [ref()], type: STRING, loc }, + right: strLit("n", loc), + type: STRING, + loc, + }, + else_: { + kind: "ternary", + cond: { kind: "dynTest", test: "symbol", value: ref(), type: BOOL, loc }, + then: strLit("NaN", loc), + else_: { kind: "libCall", fn: "insp.f64", args: [number(ref())], type: STRING, loc }, + type: STRING, + loc, + }, + type: STRING, + loc, + }, + type: STRING, + loc, + }; +} + /** %d and %i over the argument types Node formats without a number * conversion: a bigint prints as `${n}n` (formatBigInt) and a symbol as NaN; * null is Number(null) (0) under %d and parseInt("null") (NaN) under %i, and @@ -2141,19 +2187,20 @@ export function lowerFormatCall( }; } // A checked-dynamic argument (`console.log('... actual %d.', - // context.actual)` — test/common's exit report): VALIDATE it as a - // number (dynCheck) and format. A non-number dyn value throws the - // catchable TypeError where Node would print its ToNumber (NaN - // for objects) — it throws instead of returning a wrong result - // (SEMANTICS.md). + // context.actual)` — test/common's exit report): Number(arg). if (value.type.kind === "dyn") { - return { - kind: "libCall", - fn: "insp.f64", - args: [{ kind: "dynCheck", value, type: F64, loc }], - type: STRING, + return formatDynInteger( + lowerer, + value, + (held) => ({ + kind: "libCall", + fn: "dyn.numberConstructor", + args: [held], + type: F64, + loc, + }), loc, - }; + ); } lowerer.noLowering( `util.format %d of '${lowerer.fmt(value.type)}' values`, @@ -2185,6 +2232,24 @@ export function lowerFormatCall( }; return { kind: "libCall", fn: "insp.f64", args: [parsed], type: STRING, loc }; } + // parseInt(String(arg)) over a checked-dynamic argument. + if (value.type.kind === "dyn") { + return formatDynInteger( + lowerer, + value, + (held) => ({ + kind: "libCall", + fn: "num.parseInt", + args: [ + { kind: "libCall", fn: "dyn.toStringCoerce", args: [held], type: STRING, loc }, + numLit(0, loc), + ], + type: F64, + loc, + }), + loc, + ); + } lowerer.noLowering(`util.format %i of '${lowerer.fmt(value.type)}' values`, node); break; } diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 4f446bb89..be68abaca 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -154,7 +154,12 @@ function mockWrapperFactory( const { fn, call, calls, once, onceEntry, result, args } = mockContextFields(lowerer, ctxType)!; const impl = `${name}.impl`; const ctx = () => varRef("ctx.0", ctxType, loc); + // A checked-dynamic rest function (mock.fn()'s default type) takes its + // arguments as one trailing dyn array, recorded as the call's arguments + // and spread into the implementation. + const dynRest = fn.rest === true && fn.restAbi === undefined; const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + if (dynRest) params.push({ localId: "rest.0", name: "args", type: DYN }); const paramRefs = () => params.map((p) => varRef(p.localId, p.type, loc)); const argumentsValue = (): IrExpr => fn.rest @@ -219,13 +224,23 @@ function mockWrapperFactory( loc, }; }; - const invoke: IrExpr = { - kind: "callValue", - callee: varRef("f.0", fn, loc), - args: paramRefs(), - type: fn.ret, - loc, - }; + const invoke: IrExpr = dynRest + ? { + kind: "dynCall", + callee: { kind: "dynFrom", value: varRef("f.0", fn, loc), type: DYN, loc }, + calleeName: "implementation", + args: paramRefs(), + spreads: [{ arg: 0, what: "args" }], + type: DYN, + loc, + } + : { + kind: "callValue", + callee: varRef("f.0", fn, loc), + args: paramRefs(), + type: fn.ret, + loc, + }; const tryBody: IrStmt[] = voidReturn ? [ { kind: "exprStmt", expr: invoke, loc }, @@ -629,7 +644,37 @@ function requireUndefinedResultArm( } } -/** mock.fn(original, implementation?) and t.mock.fn(...). */ +/** `%mock.noop.N`: mock.fn()'s original, Node's `function () {}` — it + * returns undefined. Null when the function type's return type cannot hold + * undefined. */ +function mockNoopOriginal( + lowerer: Lowerer, + ctxType: IrType, + fn: FuncType, + loc: SrcLoc, +): IrExpr | null { + const voidReturn = fn.ret.kind === "void" || fn.ret.kind === "undefinedT"; + const undefinedValue = voidReturn ? null : lowerer.wrappedUndefined(fn.ret, loc); + if (!voidReturn && !undefinedValue) return null; + const name = liftOnce(lowerer, ctxType, "noop", (name) => { + const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + if (fn.rest === true && fn.restAbi === undefined) + params.push({ localId: "rest.0", name: "args", type: DYN }); + lowerer.liftedFns.push({ + name, + params, + returnType: fn.ret, + locals: params.map((p) => local(p.localId, p.name, p.type)), + body: [{ kind: "return", value: undefinedValue, loc }], + loc, + }); + }); + return { kind: "closure", fnName: name, captures: [], type: fn, loc }; +} + +/** mock.fn(original?, implementation?) and t.mock.fn(...). Without an + * original, the mock's original and implementation are a function that + * returns undefined, as in Node. */ export function lowerMockFn( lowerer: Lowerer, call: ts.CallExpression, @@ -658,11 +703,19 @@ export function lowerMockFn( const { fn } = fields; requireUndefinedResultArm(lowerer, fields, call, what); const args = call.arguments.nodes; - if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) { + if (args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, call, - "the supported forms are mock.fn(original) and mock.fn(original, implementation); mock.fn() and mock options have no lowering", + "the supported forms are mock.fn(), mock.fn(original) and mock.fn(original, implementation); mock options have no lowering", + ); + } + const noop = args.length === 0 ? mockNoopOriginal(lowerer, ctxType!, fn, loc) : null; + if (args.length === 0 && !noop) { + lowerer.noLowering( + `${what}() of a function returning '${lowerer.fmt(fn.ret)}'`, + call, + "mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined", ); } const stmts: IrStmt[] = []; @@ -670,7 +723,7 @@ export function lowerMockFn( stmts.push({ kind: "varDecl", localId: originalLocal.id, - init: lowerer.lowerExprExpecting(args[0]!, fn), + init: noop ?? lowerer.lowerExprExpecting(args[0]!, fn), loc, }); const implementation = diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 0e1438fc9..7cdb9ef5d 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2476,6 +2476,20 @@ function lowerNetSocketMethodCall( ? value : lowerer.coerceInto(call, value, declared); } + if (name === "isSessionReused" && args.length === 0) { + // TLSSocket.isSessionReused(): the client never offers a cached + // session (no session resumption, a documented TLS divergence), so + // no handshake resumes one. A plain socket asserted to be a + // TLSSocket answers false too, where Node has no such method. + const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: receiver, loc }], + result: boolLit(false, loc), + type: BOOL, + loc, + }; + } if (name === "resetAndDestroy") { // An RST close; a connecting socket resets once it connects. Answers // the socket, Node's chaining. diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 9866fba8a..c39c9ad8d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,4 +1,5 @@ import { declaredMemberOf } from "./expressions/property-access.js"; +import { hostsAll } from "./expressions/hosted-narrowing.js"; import { matchAllRowsReadByElements, storedMatchAllRowsReadByElements, @@ -16,7 +17,7 @@ import { InternalCompilerError } from "../../errors.js"; * flow (if/while/for/for-of/do, switch, try/catch, abrupt completions), * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; -import { processDataMemberValue } from "./builtins/process.js"; +import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; @@ -87,11 +88,11 @@ import { isCjsJsFile, isEsModuleStamp, isJsSourceFile, - isNodeEsmFile, locOf, requireEarlyReference, requireSpecOf, } from "../program.js"; +import { isStrictCode } from "./strict-mode.js"; import { COMPOUND_ASSIGN_OPS, type CompoundOp, @@ -1477,6 +1478,18 @@ function lowerLabeled(lowerer: Lowerer, stmt: ts.LabeledStatement): IrStmt | IrS return { kind: "block", body, labels, loc: locOf(stmt) }; } +// Value kinds a `throw` converts to a checked-dynamic value, so the catch +// binding sees the thrown object rather than a type-erased reference. +const THROWN_AS_DYN: ReadonlySet = new Set([ + "array", + "union", + "map", + "set", + "bytes", + "func", + "regex", +]); + export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null { if (ts.isClassDeclaration(stmt)) { if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations"); @@ -1655,14 +1668,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt value = lowerer.coerceToExpected(value, DYN); } else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) { value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) }; - } else if ( - (value.type.kind === "array" || value.type.kind === "union") && - lowerer.dynConvertible(value.type) - ) { - // Arrays, and unions over records and arrays, keep their identity - // and contents the same way: the catch binding, `Array.isArray`, + } else if (THROWN_AS_DYN.has(value.type.kind) && lowerer.dynConvertible(value.type)) { + // Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes and + // unions over them keep their identity and contents the same way: + // the catch binding, `typeof`, `instanceof`, `Array.isArray`, // inspection and typed views read the thrown object. - value = lowerer.coerceToExpected(value, DYN); + value = lowerer.coerceInto(stmt.expression, value, DYN); } if (value.type.kind === "date") { lowerer.unsupported( @@ -5472,6 +5483,23 @@ export function lowerVarDecl( settledType = init.type; } } + // `const view = route as Route & { order: number }`: the asserted type + // refines the record arms the value is stored in (the assertion keeps + // that value, see hosted-narrowing.ts). The binding keeps the stored + // type, so writes through it reach the asserted object. An annotation + // the stored type hosts (`const view: Route & { order: number } = ...`) + // keeps it too: coercing into the annotated layout would copy the + // value and read members the writes have not created yet. + if ( + !isLet && + decl.initializer && + isAssertionInitializer(decl.initializer) && + (settledType.kind === "union" || settledType.kind === "record") && + (init.type.kind === "union" || init.type.kind === "record") && + hostsAll(lowerer, init.type, settledType) + ) { + settledType = init.type; + } // An exact class behind a generic interface keeps its native // representation so generic calls can specialize against its body. if ( @@ -6149,35 +6177,6 @@ export function lowerTry(lowerer: Lowerer, stmt: ts.TryStatement): IrStmt { }; } -function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean { - for (const statement of statements) { - if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) - return false; - if (statement.expression.text === "use strict") return true; - } - return false; -} - -function isStrictDelete(node: ts.DeleteExpression): boolean { - for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) { - if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true; - if ( - ts.isFunctionLike(parent) && - parent.body && - ts.isBlock(parent.body) && - hasUseStrictDirective(parent.body.statements.nodes) - ) - return true; - if (ts.isSourceFile(parent)) { - return ( - hasUseStrictDirective(parent.statements.nodes) || - (!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent))) - ); - } - } - return false; -} - /** Statement-position `delete`: process.env keys → process.envUnset * (unsetenv), pure `Record` keys → recordKeyDelete (the * overflow Map delete), declared OPTIONAL fields → the field's ABSENT @@ -6293,7 +6292,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt obj = lowerer.coerceToExpected(obj, DYN); } if (obj.type.kind === "dyn") { - const strict = { kind: "boolLit", value: isStrictDelete(expr), type: BOOL, loc } as const; + const strict = { kind: "boolLit", value: isStrictCode(expr), type: BOOL, loc } as const; if (ts.isElementAccessExpression(target)) { const key = lowerer.lowerExprExpecting(target.argumentExpression, DYN); return { @@ -6456,7 +6455,10 @@ function lowerCjsExportStatement( // storage — collectGlobals skipped it; importers re-resolve through // cjsMemberExportClassSymbol): the statement lowers to nothing. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) { - return { kind: "block", body: [], loc }; + // An inline require still evaluates its module here. + const req = inlineProgramRequireOf(lowerer, cjs.value); + const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null; + return init ?? { kind: "block", body: [], loc }; } // The tsc VOID-INIT PREAMBLE (`exports.leaf = void 0;` with the real // `exports.leaf = leaf;` further down — every tsc-emitted dist file @@ -6917,12 +6919,13 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if ( ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && - isCjsExportsIdentifier(lowerer, expr.left) + isCjsExportsIdentifier(lowerer, expr.left) && + cjsExportAssignmentOf(stmtNode)?.kind !== "table" ) { lowerer.unsupported( "SC1090", expr.left, - "rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports. = … and module.exports = value", + "rebinding 'exports' without module.exports (exports = value): members attached through 'exports' afterwards are properties of that value, which module.exports does not hold — write exports = module.exports = value", ); } const cjs = cjsExportAssignmentOf(stmtNode); @@ -7275,9 +7278,12 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ) { const loc = locOf(expr); const value = lowerer.lowerExprExpecting(expr.right, F64); + const fn = programReadsExitCode(lowerer) + ? "process.setExitCodeTracked" + : "process.setExitCode"; return { kind: "exprStmt", - expr: { kind: "libCall", fn: "process.setExitCode", args: [value], type: VOID, loc }, + expr: { kind: "libCall", fn, args: [value], type: VOID, loc }, loc, }; } @@ -11224,3 +11230,9 @@ export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclarat requireEarlyReference(lowerer.program, decl) === null ); } + +function isAssertionInitializer(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ts.isAsExpression(e) || ts.isTypeAssertion(e); +} diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 5769e1e0a..dd0ddcc0e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -999,6 +999,15 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C }; } +/** True when a value of Node's argument type `passed` widens implicitly + * into the declared parameter type `declared` (one of its union arms, or a + * sub-union of it). */ +function widensInto(lowerer: Lowerer, passed: IrType, declared: IrType, loc: SrcLoc): boolean { + if (declared.kind !== "union") return false; + const probe: IrExpr = { kind: "varRef", localId: "%probe", type: passed, loc }; + return typeEquals(lowerer.coerceToExpected(probe, declared).type, declared); +} + /* ── user subclasses (`extends Readable`, the underscore-override form) ── */ /** The wrapper closure binding one overridden underscore method as the @@ -1075,10 +1084,17 @@ function streamMethodWrapper( ); } } else if (!typeEquals(got.type, want)) { + // A declared supertype of what Node passes (`chunk: Buffer | string`) + // is sound: the wrapper takes Node's value and widens it into the + // declared parameter. + if (widensInto(lowerer, want, got.type, loc)) { + paramTypes.push(want); + continue; + } lowerer.noLowering( `a '${methodName}' method whose parameter ${i} is '${lowerer.fmt(got.type)}' where Node passes '${lowerer.fmt(want)}'`, blame, - "annotate the parameter with the Node signature's type (chunks are Buffers here)", + "annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here)", ); } paramTypes.push(got.type); @@ -1094,7 +1110,11 @@ function streamMethodWrapper( for (let i = 0; i < paramTypes.length; i++) { const local = lowerer.declareHiddenLocal(`a${i}`, paramTypes[i]!); params.push({ localId: local.id, name: local.name, type: paramTypes[i]! }); - argRefs.push({ kind: "varRef", localId: local.id, type: paramTypes[i]!, loc }); + const arg: IrExpr = { kind: "varRef", localId: local.id, type: paramTypes[i]!, loc }; + const declaredType = declared[i]!.type; + argRefs.push( + typeEquals(arg.type, declaredType) ? arg : lowerer.coerceToExpected(arg, declaredType), + ); } const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisType, loc }; let call: IrExpr; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6428b7c45..bc556dd3b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,4 @@ +import { extendAssertedRecordMembers } from "./assertion-members.js"; import { jsDynamicDeclarations } from "./js-dynamic-objects.js"; import { lifetimeRegionType, @@ -116,6 +117,7 @@ import { InternalCompilerError } from "../../errors.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js"; import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; +import { guardClassAttributeWrites } from "./class-attributes.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; @@ -129,7 +131,8 @@ import { } from "../tier-analysis.js"; import { DeferredModuleInitializers } from "./deferred-module-initializers.js"; import { finalizeClassMethodValues } from "./class-method-values.js"; -import type { ClassSymbolKey } from "./symbol-fields.js"; +import { finalizePrivateBrands } from "./private-brands.js"; +import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js"; import type { HttpClientFnBinding } from "./lower-server.js"; export type { WidthLift } from "./width-lift.js"; export { newFnCtx, type FnCtx } from "./function-context.js"; @@ -299,6 +302,7 @@ import { import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; import { cjsNamespaceModule, + cjsWholeExportValueOf, inlineNamespaceRequireOf, inlineProgramRequireOf, } from "./cjs-inline-require.js"; @@ -1961,8 +1965,13 @@ export class Lowerer { * their symbol/node keys; no module-level cache should retain a pass. */ readonly classSymbolKeys = new Map(); readonly registeredClassSymbols = new Map(); + /** Symbol-key identities by description, in resolution order (the slot + * ordinals of key-table keys — symbolKeyOf). */ + readonly symbolFieldIdentities = new Map(); readonly httpClientFnBindings = new Map(); streamPropMutatedSyms: Set | null = null; + /** Whether some source reads process.exitCode (processReadsExitCode). */ + readsExitCode: boolean | null = null; requestInitPropMutatedSyms: Set | null = null; emitterEventTable: Map | null = null; emitterComputedPatterns: ComputedEventPattern[] = []; @@ -2115,6 +2124,51 @@ export class Lowerer { return fallback ?? null; } + /** A TypeScript object literal handed to a JavaScript checked-value + * parameter whose inferred type is open (`constructor({ factory, ...options + * } = {})` — the rest element admits any property): the literal names a + * property the contextual JS type does not declare, so it has no record + * layout there and is built as the plain object the JS side receives. */ + private excessJsContextualProperty( + literal: ts.ObjectLiteralExpression, + contextual: readonly ts.Type[], + ): boolean { + const objects = contextual.filter( + (part) => !(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)), + ); + if (objects.length === 0) return false; + // Only a destructuring pattern with a rest element is open; other + // inferred JS types keep their record lowering. + const fromJs = objects.every((part) => { + const sym = part.getSymbol(); + return ( + sym !== undefined && + sym.declarations.some((d) => { + if (!isJsSourceFile(d.getSourceFile())) return false; + // The pattern itself, or the `= {}` default of a pattern parameter. + const pattern = + ts.isObjectLiteralExpression(d) && + d.parent !== undefined && + ts.isParameter(d.parent) && + d.parent.initializer === d + ? d.parent.name + : d; + return ( + ts.isObjectBindingPattern(pattern) && + pattern.elements.nodes.some((element) => element.dotDotDotToken !== undefined) + ); + }) + ); + }); + if (!fromJs) return false; + return literal.properties.nodes.some((prop) => { + if (!prop.name || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) + return false; + const name = prop.name.text; + return objects.every((part) => !this.checker.getPropertyOfType(part, name)); + }); + } + /** The index value of a keyed read of a record's index signature that * can answer undefined for a missing key while its checker type does not * admit undefined, or null. A literal key naming a declared field is a @@ -3719,13 +3773,18 @@ export class Lowerer { ? d.parent : null; if (!assign || assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null; - if (!ts.isIdentifier(assign.right)) return null; const stmt = assign.parent; if (!stmt || !ts.isExpressionStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null; if (!isJsSourceFile(stmt.parent)) return null; const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind !== "member" || cjs.expr !== assign) return null; if (cjsExportDiscardReason(stmt) !== null) return null; + // `module.exports.C = require('./c')` over a module whose whole export + // is a class identifier: the same alias plumbing; the statement keeps + // the require's module evaluation at its position. + const required = this.memberExportRequiredClass(assign.right); + if (required !== undefined) return required; + if (!ts.isIdentifier(assign.right)) return null; let value = this.checker.getSymbolAtLocation(assign.right); if (!value) return null; if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); @@ -3733,6 +3792,22 @@ export class Lowerer { return isClass ? value : null; } + /** For an inline program `require("./c")`: the class symbol its module's + * kept `module.exports = ` names, or null when the + * whole export is something else. Undefined when `expr` is not such a + * require. */ + memberExportRequiredClass(expr: ts.Expression): ts.Symbol | null | undefined { + const req = inlineProgramRequireOf(this, expr); + if (req === null) return undefined; + const whole = cjsWholeExportValueOf(req.dep); + if (whole === null || !ts.isIdentifier(whole.rhs)) return null; + let value = this.checker.getSymbolAtLocation(whole.rhs); + if (!value) return null; + if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); + const isClass = value.declarations.some((decl) => ts.isClassDeclaration(decl)); + return isClass ? value : null; + } + /** The CommonJS JS module a DEFAULT-import binding's declaration loads * (`import d from "./lib.cjs"`), or null: Node's ESM-CJS interop binds * the default to module.exports — exactly a require binding — so those @@ -6710,6 +6785,10 @@ export class Lowerer { } this.collectProgram(parts); this.speculativeTypeQueries(() => { + extendAssertedRecordMembers( + this, + parts.map((fp) => fp.sf), + ); this.analyzeRuntimeOptionalArrayReads(parts); this.analyzeDateJsonResults(parts); this.analyzeGuardedRecordReadGlobals(parts); @@ -6856,7 +6935,9 @@ export class Lowerer { finishModule(functions: IrFunction[]): LowerResult { if (this.diags.length === 0) fuseArrayPipelines(this, functions); finalizeClassMethodValues(this, functions); + finalizePrivateBrands(this, functions); trackClassFieldCreation(this, functions); + guardClassAttributeWrites(this, functions); // Globals typed by a class that never REGISTERED (a JS class whose // collection fenced — Symbol-keyed fields, an unsupported base): the @@ -7116,6 +7197,10 @@ export class Lowerer { }; this.collectProgram(parts); this.speculativeTypeQueries(() => { + extendAssertedRecordMembers( + this, + parts.map((fp) => fp.sf), + ); this.analyzeRuntimeOptionalArrayReads(parts); this.analyzeDateJsonResults(parts); this.analyzeGuardedRecordReadGlobals(parts); @@ -8037,6 +8122,17 @@ export class Lowerer { return this.checker.getUnknownType(); } } + // A symbol-keyed field read through a CommonJS key-table key reads as + // the field's layout type (tableKeyFieldType). + if (ts.isElementAccessExpression(node) && isJsSourceFile(node.getSourceFile())) { + const field = tableKeyFieldType(this, node); + if (field) { + if (field.kind === "f64") return this.checker.getNumberType(); + if (field.kind === "string") return this.checker.getStringType(); + if (field.kind === "bool") return this.checker.getBooleanType(); + return this.checker.getUnknownType(); + } + } // A checked JS method receives its actual call-site receiver. Its // declaration's inferred class type must not turn property reads back // into native field loads when the method is borrowed by another object. @@ -9675,7 +9771,8 @@ export class Lowerer { builtinOption("SpawnOptions") || builtinOption("UrlObject") || builtinOption("URLFormatOptions") || - !this.mapTypeOf(this.typeOf(x)) + !this.mapTypeOf(this.typeOf(x)) || + this.excessJsContextualProperty(x, parts) ) { return lowerDynObjectLiteral(this, x); } @@ -10962,6 +11059,21 @@ export class Lowerer { return; } const name = (decl.name as ts.Identifier).text; + if (shape.entryNarrow) { + // A typed override parameter over an inherited checked-value slot: + // the body local narrows from the dyn argument at entry. + const loc = locOf(decl); + const slot = this.declareHiddenLocal(name, shape.type); + params.push({ localId: slot.id, name, type: shape.type }); + const narrowed = this.coerceInto( + decl.name, + { kind: "varRef", localId: slot.id, type: shape.type, loc }, + shape.entryNarrow, + ); + const body = this.declareLocal(decl.name, name, shape.entryNarrow, true); + prologue.push({ kind: "varDecl", localId: body.id, init: narrowed, loc }); + return; + } if (shape.mode === "omittable" && shape.bodyType && decl.initializer) { const abi = shape.type; if (abi.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts index 205cc63e9..592136bec 100644 --- a/packages/compiler/src/frontend/lowering/module-member-this.ts +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -25,24 +25,228 @@ function exportedFunction(decl: ts.Node): ts.Node | null { return null; } -/** Whether a function's own body reads `this` (arrows share it; other - * functions and classes have their own). */ -function readsOwnThis(fn: ts.Node): boolean { - let found = false; +/** Whether `node` is `f.apply(this, ...)` or `f.call(this, ...)`, which + * passes the receiver on to `f`. */ +function forwardsThis(node: ts.Node): node is ts.CallExpression & { + expression: ts.PropertyAccessExpression; +} { + if (!ts.isCallExpression(node)) return false; + const callee = node.expression; + if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return false; + if (callee.name.text !== "apply" && callee.name.text !== "call") return false; + return node.arguments.nodes[0]?.kind === ts.SyntaxKind.ThisKeyword; +} + +/* The arguments a call gave a function's parameters, while the analysis + * follows the call into the function: each with the bindings of its own + * scope. */ +type Bindings = ReadonlyMap; +const noBindings: Bindings = new Map(); + +// How many calls and functions deep the analysis follows a forwarded +// `this`: enough for a wrapper over a function factory, as Effect's +// `dual(3, body).apply(this, arguments)`. +const maxDepth = 6; + +/** Whether `fn`'s own body uses `this` only to pass it on with `.apply` or + * `.call` to functions that ignore it (and so also when it doesn't use + * `this` at all). */ +function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth: number): boolean { + let ignores = true; const visit = (node: ts.Node): void => { - if (found) return; + if (!ignores) return; if (node.kind === ts.SyntaxKind.ThisKeyword) { - found = true; + ignores = false; return; } if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node)) return; + if (forwardsThis(node)) { + if (!ignoresThis(lowerer, node.expression.expression, bindings, depth + 1)) { + ignores = false; + return; + } + visit(node.expression.expression); + for (const arg of node.arguments.nodes.slice(1)) visit(arg); + return; + } node.forEachChild(visit); }; fn.forEachChild(visit); + return ignores; +} + +/** The function `name` provably holds: a function declaration, a const + * initialized with a function, or an import of one. */ +function functionOfName( + lowerer: Lowerer, + name: ts.Identifier, + bindings: Bindings, +): { fn: ts.Node | null; bound?: { expr: ts.Expression; bindings: Bindings } } { + let symbol = lowerer.checker.getSymbolAtLocation(name); + if (symbol === undefined) return { fn: null }; + const bound = bindings.get(symbol); + if (bound) return { fn: null, bound }; + if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + const decls = symbol.declarations; + if (decls.length !== 1) return { fn: null }; + const decl = decls[0]!; + if (ts.isFunctionDeclaration(decl)) return { fn: decl }; + if ( + ts.isVariableDeclaration(decl) && + decl.initializer && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 + ) { + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { fn: init }; + } + return { fn: null }; +} + +/** + * Whether `fn` (with its nested functions) may change what its parameter + * `param` holds: an assignment, update, destructuring or for-in/for-of + * target that writes it, a declaration of the same name, a write to an + * element of `arguments` (which aliases parameters outside strict mode), or + * a call of `eval`. + */ +function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean { + const symbol = lowerer.checker.getSymbolAtLocation(param); + const refersTo = (node: ts.Node): boolean => + ts.isIdentifier(node) && + (node.text === param.text || lowerer.checker.getSymbolAtLocation(node) === symbol); + // Whether writing `target` writes the parameter: the name itself, or the + // name inside a destructuring pattern. + const writes = (target: ts.Node): boolean => { + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (refersTo(target)) return true; + if ( + ts.isElementAccessExpression(target) && + ts.isIdentifier(target.expression) && + target.expression.text === "arguments" + ) + return true; + if (ts.isArrayLiteralExpression(target)) return target.elements.nodes.some(writes); + if (ts.isObjectLiteralExpression(target)) + return target.properties.nodes.some((p) => + ts.isShorthandPropertyAssignment(p) + ? refersTo(p.name) + : ts.isPropertyAssignment(p) + ? writes(p.initializer) + : ts.isSpreadAssignment(p) && writes(p.expression), + ); + if (ts.isSpreadElement(target)) return writes(target.expression); + // A default in a pattern: `[body = other] = values`. + if (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.EqualsToken) + return writes(target.left); + return false; + }; + const declaresName = (node: ts.Node): boolean => { + const name = (node as { name?: ts.Node }).name; + return name !== undefined && ts.isIdentifier(name) && name.text === param.text; + }; + let found = false; + const visit = (node: ts.Node): void => { + if (found) return; + if ( + (ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && + writes(node.left)) || + ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) && + writes(node.operand)) || + ((ts.isForOfStatement(node) || ts.isForInStatement(node)) && + !ts.isVariableDeclarationList(node.initializer) && + writes(node.initializer)) || + ((ts.isVariableDeclaration(node) || + ts.isBindingElement(node) || + ts.isParameter(node) || + ts.isFunctionDeclaration(node) || + ts.isClassDeclaration(node)) && + declaresName(node)) || + (ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === "eval") + ) { + found = true; + return; + } + node.forEachChild(visit); + }; + // The function's own parameters declare the name; their defaults can write it. + const params = (fn as { parameters?: ts.NodeArray }).parameters; + for (const p of params?.nodes ?? []) if (p.initializer) visit(p.initializer); + const body = (fn as { body?: ts.Node }).body; + if (body !== undefined) visit(body); return found; } +/** The expressions `fn` returns from its own body. */ +function returnedExpressions(fn: ts.Node): ts.Expression[] | null { + const body = (fn as { body?: ts.Node }).body; + if (body === undefined) return null; + if (!ts.isBlock(body)) return [body as ts.Expression]; + const result: ts.Expression[] = []; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; + if (ts.isReturnStatement(node) && node.expression) result.push(node.expression); + node.forEachChild(visit); + }; + body.forEachChild(visit); + return result; +} + +/** Whether the function `expr` evaluates to provably ignores the `this` + * it is called with. */ +function ignoresThis( + lowerer: Lowerer, + expr: ts.Expression, + bindings: Bindings, + depth: number, +): boolean { + if (depth > maxDepth) return false; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (ts.isArrowFunction(expr)) return true; + if (ts.isFunctionExpression(expr)) return ignoresOwnThis(lowerer, expr, bindings, depth); + if (ts.isIdentifier(expr)) { + const { fn, bound } = functionOfName(lowerer, expr, bindings); + if (bound) return ignoresThis(lowerer, bound.expr, bound.bindings, depth + 1); + if (fn === null) return false; + return ts.isArrowFunction(fn) || ignoresOwnThis(lowerer, fn, noBindings, depth + 1); + } + // A call of a known function: what it returns, with its parameters bound + // to this call's arguments. + if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) { + const { fn } = functionOfName(lowerer, expr.expression, bindings); + if (fn === null) return false; + const params = (fn as { parameters?: ts.NodeArray }).parameters; + const inner = new Map(bindings); + for (const [i, param] of (params?.nodes ?? []).entries()) { + if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return false; + const arg = expr.arguments.nodes[i]; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (arg === undefined || ts.isSpreadElement(arg) || symbol === undefined) return false; + // The parameter holds the argument for as long as the function runs. + if (mayRebind(lowerer, fn, param.name)) return false; + inner.set(symbol, { expr: arg, bindings }); + } + const returned = returnedExpressions(fn); + return returned !== null && returned.every((r) => ignoresThis(lowerer, r, inner, depth + 1)); + } + return false; +} + +/** Whether calling `fn` with another `this` than Node's can change what it + * does: it reads `this`, or passes it on to a function that may. */ +function readsOwnThis(lowerer: Lowerer, fn: ts.Node): boolean { + return !ignoresOwnThis(lowerer, fn, noBindings, 0); +} + export function refuseModuleMemberThis( lowerer: Lowerer, call: ts.CallExpression, @@ -54,7 +258,7 @@ export function refuseModuleMemberThis( if (symbol === undefined) return; for (const decl of symbol.declarations) { const fn = exportedFunction(decl); - if (fn === null || !readsOwnThis(fn)) continue; + if (fn === null || !readsOwnThis(lowerer, fn)) continue; lowerer.unsupported( "SC1090", call, diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts new file mode 100644 index 000000000..e7b3edbd8 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -0,0 +1,328 @@ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + DYN, + RUNTIME_ERROR_CLASSES, + STRING, + typeKey, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { helperLoc, strLit, varRef } from "../../ir/build.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; +import type { ClassInfo } from "./lower-classes.js"; + +/** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and + * `write` return the receiver as the declaring class for a field access. */ +type BrandUse = "in" | "read" | "write"; + +const PREFIX = "%private."; + +/** A function-local JavaScript class with private names. Its prototype + * methods and accessors exist once per evaluation and check their receiver's + * evaluation, so a method or accessor taken from an instance is the function + * of that instance's own evaluation. */ +export function perEvaluationPrivateNames(info: ClassInfo): boolean { + const declaration = info.localClass ? info.decl : null; + return ( + !!declaration && + isJsSourceFile(declaration.getSourceFile()) && + declaration.members.nodes.some( + (member) => member.name !== undefined && ts.isPrivateIdentifier(member.name), + ) + ); +} + +/** The function-local class declaring a private name, when its private names + * need a per-evaluation brand check: the nearest enclosing class that + * declares the name, as JavaScript scoping resolves it. */ +export function localPrivateBrandClass( + lowerer: Lowerer, + priv: ts.PrivateIdentifier, +): { info: ClassInfo; decl: ts.ClassDeclaration & { name: ts.Identifier } } | null { + for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) { + if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue; + const declares = node.members.nodes.some((member) => { + const name = (member as { name?: ts.PropertyName }).name; + return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text; + }); + if (!declares) continue; + if (!ts.isClassDeclaration(node) || !node.name) return null; + const symbol = lowerer.checker.getSymbolAtLocation(node.name); + const info = + lowerer.currentClass?.decl === node + ? lowerer.currentClass + : symbol + ? lowerer.classBySymbol.get(symbol) + : undefined; + if (!info?.localClass) return null; + return { info, decl: node as ts.ClassDeclaration & { name: ts.Identifier } }; + } + return null; +} + +/** Each evaluation of a function-local class creates fresh private names, + * while one compiled class serves every evaluation. Its JavaScript prototype + * methods are closures per evaluation whose `this` is a checked value, so a + * private name on `this` is present exactly when the receiver is an instance + * of the class (or a subclass) whose `%classEnvironment` field holds the + * class value of the closure's own evaluation. The class's own binding names + * that value (lowerClassExpressionInfo accepts only named declarations, whose + * binding the checker keeps from being assigned). */ +export function checkedPrivateReceiver( + lowerer: Lowerer, + info: ClassInfo, + decl: ts.ClassDeclaration & { name: ts.Identifier }, + access: ts.PropertyAccessExpression, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read"; + return brandCall(lowerer, info, decl, use, access.name.text, receiver, loc); +} + +/** Whether a checked value is a primitive: `#x in v` throws for those + * before any brand lookup. */ +export function primitiveOperand(value: IrExpr, loc: SrcLoc): IrExpr { + return (["nullish", "number", "string", "boolean", "bigint", "symbol"] as const) + .map((test): IrExpr => ({ kind: "dynTest", test, value, type: BOOL, loc })) + .reduce((left, right) => ({ kind: "logical", op: "||", left, right, type: BOOL, loc })); +} + +/** Node's TypeError message for `#x in v` on a primitive, which spells the + * value as String(v) does. */ +export function privateInMessage(name: string, value: IrExpr, loc: SrcLoc): IrExpr { + return { + kind: "strConcat", + left: strLit(`Cannot use 'in' operator to search for '${name}' in `, loc), + right: { kind: "libCall", fn: "dyn.stringConstructor", args: [value], type: STRING, loc }, + type: STRING, + loc, + }; +} + +/** A receiver checkedPrivateReceiver built for a field write. */ +export function isPrivateWriteReceiver(expr: IrExpr): boolean { + return expr.kind === "call" && expr.callee.startsWith(`${PREFIX}write:`); +} + +/** `#x in v` for a checked value inside a function-local class: the brand + * test above, after the TypeError a primitive operand throws. */ +export function checkedPrivateIn( + lowerer: Lowerer, + info: ClassInfo, + decl: ts.ClassDeclaration & { name: ts.Identifier }, + name: string, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + return brandCall(lowerer, info, decl, "in", name, receiver, loc); +} + +function brandCall( + lowerer: Lowerer, + info: ClassInfo, + decl: ts.ClassDeclaration & { name: ts.Identifier }, + use: BrandUse, + name: string, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const className = info.def.name; + const classType: IrType = { kind: "classval", className }; + const resultType: IrType = use === "in" ? BOOL : { kind: "object", className }; + const helper = `${PREFIX}${use}:${className}:${name}`; + if (!lowerer.valueHelpers.has(helper)) { + const at = helperLoc(loc); + lowerer.liftedFns.push({ + name: helper, + params: [ + { localId: "value.0", name: "value", type: DYN }, + { localId: "class.0", name: "class", type: classType }, + ], + returnType: resultType, + locals: [ + { id: "value.0", name: "value", type: DYN, mutable: false }, + { id: "class.0", name: "class", type: classType, mutable: false }, + ], + // finalizePrivateBrands fills the body once every class is collected, + // including subclasses declared after this use. + body: [], + loc: at, + }); + lowerer.valueHelpers.set(helper, helper); + } + const evaluation = lowerer.classValueRef(info, decl.name); + return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc }; +} + +/** A plain assignment, a destructuring target, or a for-in/of target writes + * the field. Compound assignments and updates read it first, and Node + * reports the failed read. */ +function isPrivateWriteTarget(access: ts.PropertyAccessExpression): boolean { + let node: ts.Node = access; + for (;;) { + const parent = node.parent; + if (ts.isParenthesizedExpression(parent)) { + node = parent; + continue; + } + if (ts.isBinaryExpression(parent)) + return parent.left === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; + if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) + return parent.initializer === node; + // Inside a destructuring pattern: keep walking to the assignment. + if ( + ts.isArrayLiteralExpression(parent) || + ts.isSpreadElement(parent) || + (ts.isPropertyAssignment(parent) && parent.initializer === node) || + ts.isObjectLiteralExpression(parent) || + ts.isSpreadAssignment(parent) + ) { + node = parent; + continue; + } + return false; + } +} + +/** Fill the brand helpers: one branch per class a checked reference to an + * instance can be boxed under (the declarer, its subclasses, and its + * ancestors when the instance is at or below the declarer). */ +export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]): void { + for (const fn of functions) { + if (!fn.name.startsWith(PREFIX) || fn.body.length > 0) continue; + // `%private.::<#name>`; class names may contain colons. + const spec = fn.name.slice(PREFIX.length); + const use = spec.slice(0, spec.indexOf(":")) as BrandUse; + const className = spec.slice(use.length + 1, spec.lastIndexOf(":")); + const name = spec.slice(spec.lastIndexOf(":") + 1); + const loc = fn.loc; + const value = varRef("value.0", DYN, loc); + const evaluation = varRef("class.0", { kind: "classval", className }, loc); + const ownerType: IrType = { kind: "object", className }; + const body: IrStmt[] = []; + let matches = 0; + for (const candidate of lowerer.classes.values()) { + const candidateName = candidate.def.name; + if (RUNTIME_ERROR_CLASSES.has(candidateName)) continue; + const below = candidateName === className || lowerer.isSubclassOf(candidateName, className); + const above = lowerer.isSubclassOf(className, candidateName); + if (!below && !above) continue; + const candidateType: IrType = { kind: "object", className: candidateName }; + const checked: IrExpr = { kind: "dynCheck", value, type: candidateType, loc }; + const local = `receiver.${matches++}`; + fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false }); + const receiver = varRef(local, ownerType, loc); + const sameEvaluation: IrExpr = { + kind: "bin", + op: "===", + left: { + kind: "fieldGet", + obj: receiver, + className, + field: `%classEnvironment:${className}`, + type: evaluation.type, + loc, + }, + right: evaluation, + type: BOOL, + loc, + }; + const match = (typed: IrExpr): IrStmt[] => [ + { kind: "varDecl", localId: local, init: typed, loc }, + { + kind: "if", + cond: sameEvaluation, + then: [ + { + kind: "return", + value: use === "in" ? { kind: "boolLit", value: true, type: BOOL, loc } : receiver, + loc, + }, + ], + else_: null, + loc, + }, + ]; + const then: IrStmt[] = below + ? match( + candidateName === className + ? checked + : { kind: "upcast", value: checked, type: ownerType, loc }, + ) + : [ + { + kind: "if", + cond: { kind: "instanceOf", value: checked, className, type: BOOL, loc }, + then: match({ kind: "downcast", value: checked, type: ownerType, loc }), + else_: null, + loc, + }, + ]; + body.push({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, strLit(typeKey(candidateType), loc)], + type: BOOL, + loc, + }, + then, + else_: null, + loc, + }); + } + const typeError = (message: IrExpr): IrStmt => ({ + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [message], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }); + if (use === "in") { + body.push( + { + kind: "if", + cond: primitiveOperand(value, loc), + then: [typeError(privateInMessage(name, value, loc))], + else_: null, + loc, + }, + { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, + ); + } else { + const verb = use === "read" ? "reading" : "setting"; + const action = use === "read" ? "Cannot read properties" : "Cannot set properties"; + for (const unit of ["undefined", "null"] as const) { + body.push({ + kind: "if", + cond: { kind: "dynTest", test: unit, value, type: BOOL, loc }, + then: [typeError(strLit(`${action} of ${unit} (${verb} '${name}')`, loc))], + else_: null, + loc, + }); + } + body.push( + typeError( + strLit( + use === "read" + ? `Cannot read private member ${name} from an object whose class did not declare it` + : `Cannot write private member ${name} to an object whose class did not declare it`, + loc, + ), + ), + ); + } + fn.body = body; + } +} diff --git a/packages/compiler/src/frontend/lowering/strict-mode.ts b/packages/compiler/src/frontend/lowering/strict-mode.ts new file mode 100644 index 000000000..86653c1d3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/strict-mode.ts @@ -0,0 +1,34 @@ +import * as ts from "../ts7/adapter.js"; +import { isCjsJsFile, isNodeEsmFile } from "../program.js"; + +function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean { + for (const statement of statements) { + if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) + return false; + if (statement.expression.text === "use strict") return true; + } + return false; +} + +/** Whether `node` runs in strict mode code: inside a class body, a function + * with a "use strict" directive, a module, or a script that starts with + * "use strict". CommonJS JavaScript is sloppy otherwise. */ +export function isStrictCode(node: ts.Node): boolean { + for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) { + if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true; + if ( + ts.isFunctionLike(parent) && + parent.body && + ts.isBlock(parent.body) && + hasUseStrictDirective(parent.body.statements.nodes) + ) + return true; + if (ts.isSourceFile(parent)) { + return ( + hasUseStrictDirective(parent.statements.nodes) || + (!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent))) + ); + } + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 7ae1371c2..6fbbe3be4 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2656,8 +2656,8 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "node-builtin.process.exitCode", kind: "node-builtin", name: "process.exitCode", - fns: ["process.setExitCode"], - note: "numeric writes in statement position set the implicit exit status; process.exit() reads it", + fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"], + note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it", }, // ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the // host's trust anchors, read and replaced. Dedicated paths, and @@ -2969,7 +2969,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< }, util: { promisify: - "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart; call the bound function directly", + "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart, plus typed callback-last function values whose callback takes (error?: Error | null, value?); call the bound function directly", }, "stream/consumers": { blob: diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 5d7a20c06..0791fc6cd 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -1,5 +1,10 @@ import * as ts from "../ts7/adapter.js"; -import { bindingEarlyUse7 } from "../program.js"; +import { + bindingEarlyUse7, + cjsExportDiscardReason, + isCjsExportTableLiteral, + isModuleExportsAccess, +} from "../program.js"; import type { Lowerer } from "./lowerer.js"; import type { IrType } from "../../ir/ir.js"; @@ -39,7 +44,7 @@ function constantSymbolDescription( } export function symbolFieldDisplayName(field: string): string { - return field.slice(FIELD_PREFIX.length); + return field.slice(FIELD_PREFIX.length).replace(/^\d+:/, ""); } /** CopyDataProperties includes symbols, unlike the string-keyed view used @@ -96,6 +101,11 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS cache.set(sym, null); const decls = sym.declarations; const decl = sym.valueDeclaration; + if (decls.length === 1 && decl && ts.isPropertyAssignment(decl)) { + const key = cjsTableSymbolKeyOf(lowerer, sym, decl); + if (key) cache.set(sym, key); + return key; + } if (decls.length !== 1 || !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; @@ -124,21 +134,9 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS cache.set(sym, alias); return alias; } - if (!init || !ts.isCallExpression(init) || init.questionDotToken) return null; - const callee = init.expression; - const registered = - ts.isPropertyAccessExpression(callee) && !callee.questionDotToken && callee.name.text === "for"; - const root = registered ? callee.expression : callee; - if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null; - const arg = - init.arguments.nodes.length === 0 - ? null - : init.arguments.nodes.length === 1 - ? init.arguments.nodes[0]! - : undefined; - if (arg === undefined || (registered && arg === null)) return null; - const description = arg === null ? "" : constantSymbolDescription(lowerer, arg); - if (description === null) return null; + const call = init ? literalSymbolCallOf(lowerer, init) : null; + if (!call) return null; + const { registered, description } = call; // Ordinary Symbol() is a fresh identity per evaluation, so only the // module-scope declaration above can name one fixed layout slot. const sf = decl.getSourceFile(); @@ -184,6 +182,18 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS ) !== null ) return null; + const key = symbolKeyOf(lowerer, sym, registered, description); + cache.set(sym, key); + return key; +} + +function symbolKeyOf( + lowerer: Lowerer, + sym: ts.Symbol, + registered: boolean, + description: string, + table = false, +): ClassSymbolKey { // A registry name denotes one symbol even when several source bindings // initialize it. Intern those names into a representative checker symbol; // the layout table then has one identity-keyed domain for both forms. @@ -193,16 +203,175 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS if (existing) identity = existing; else lowerer.registeredClassSymbols.set(description, sym); } - const key: ClassSymbolKey = { + // Distinct identities can share a description (undici's core and mock + // key tables both hold a `Symbol('close')`, and MockClient inherits one + // and declares the other). A key-table key whose description an earlier + // identity already holds takes an ordinal that keeps its slot apart; the + // printable name drops it. Const keys keep their plain names. + let identities = lowerer.symbolFieldIdentities.get(description); + if (!identities) lowerer.symbolFieldIdentities.set(description, (identities = [])); + let ordinal = identities.indexOf(identity); + if (ordinal < 0) ordinal = identities.push(identity) - 1; + return { sym, identity, // Symbol slots are not string properties. The reserved prefix keeps // them out of the native class's string-keyed dynamic view, including // Object.keys/JSON serialization and string-property writeback. - fieldName: `${FIELD_PREFIX}Symbol(${description})`, + fieldName: `${FIELD_PREFIX}${table && ordinal > 0 ? `${ordinal}:` : ""}Symbol(${description})`, }; - cache.set(sym, key); - return key; +} + +/** `Symbol()` or `Symbol.for()` on the standard global. */ +function literalSymbolCallOf( + lowerer: Lowerer, + init: ts.Expression, +): { registered: boolean; description: string } | null { + if (!ts.isCallExpression(init) || init.questionDotToken) return null; + const callee = init.expression; + const registered = + ts.isPropertyAccessExpression(callee) && !callee.questionDotToken && callee.name.text === "for"; + const root = registered ? callee.expression : callee; + if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null; + const arg = + init.arguments.nodes.length === 0 + ? null + : init.arguments.nodes.length === 1 + ? init.arguments.nodes[0]! + : undefined; + if (arg === undefined || (registered && arg === null)) return null; + const description = arg === null ? "" : constantSymbolDescription(lowerer, arg); + return description === null ? null : { registered, description }; +} + +/** A key table property: `kX: Symbol('x')` in a JS module's effective + * top-level `module.exports = { ... }` literal (undici's core/symbols.js). + * The module evaluates once, so the property names one symbol identity for + * every requirer, as a module-scope const does. Requirers can observe the + * table only after its assignment completes, provided nothing before it + * can run code (a require there could close a cycle and publish the + * initial empty exports object instead). Any write to the property, or + * replacement through `exports`/`module.exports`, keeps it unstable. */ +function cjsTableSymbolKeyOf( + lowerer: Lowerer, + sym: ts.Symbol, + decl: ts.PropertyAssignment, +): ClassSymbolKey | null { + const table = decl.parent; + if (!ts.isObjectLiteralExpression(table) || !ts.isIdentifier(decl.name)) return null; + const assign = table.parent; + if ( + !ts.isBinaryExpression(assign) || + assign.right !== table || + !isCjsExportTableLiteral(table) || + !ts.isExpressionStatement(assign.parent) + ) + return null; + const stmt = assign.parent; + const sf = stmt.getSourceFile(); + if (cjsExportDiscardReason(stmt) !== null) return null; + const call = literalSymbolCallOf(lowerer, decl.initializer); + if (!call) return null; + const inert = (expr: ts.Expression | undefined): boolean => { + if (!expr) return true; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + return ( + ts.isIdentifier(expr) || + ts.isArrowFunction(expr) || + ts.isFunctionExpression(expr) || + ts.isStringLiteralLike(expr) || + ts.isNumericLiteral(expr) || + expr.kind === ts.SyntaxKind.TrueKeyword || + expr.kind === ts.SyntaxKind.FalseKeyword || + expr.kind === ts.SyntaxKind.NullKeyword || + literalSymbolCallOf(lowerer, expr) !== null + ); + }; + const statements = sf.statements.nodes; + const prefixInert = statements + .slice(0, statements.indexOf(stmt)) + .every( + (statement) => + (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) || + ts.isFunctionDeclaration(statement) || + (ts.isVariableStatement(statement) && + statement.declarationList.declarations.nodes.every( + (d) => ts.isIdentifier(d.name) && inert(d.initializer), + )), + ); + if (!prefixInert) return null; + for (const property of table.properties.nodes) { + if (property === decl) break; + if (ts.isShorthandPropertyAssignment(property)) continue; + if ( + !ts.isPropertyAssignment(property) || + ts.isComputedPropertyName(property.name) || + !inert(property.initializer) + ) + return null; + } + // Requirers can reach the property through a checker-synthesized symbol + // (the export= type's member); the declaration's own symbol is the one + // identity every requirer and the table's export global share. + const canonical = lowerer.checker.getSymbolAtLocation(decl.name) ?? sym; + if (tablePropertyWritten(lowerer, new Set([sym, canonical]), decl.name.text, sf)) return null; + return { ...symbolKeyOf(lowerer, canonical, call.registered, call.description, true), sym }; +} + +function tablePropertyWritten( + lowerer: Lowerer, + symbols: ReadonlySet, + name: string, + home: ts.SourceFile, +): boolean { + let written = false; + const targets = (node: ts.Node): boolean => { + if (ts.isPropertyAccessExpression(node)) { + if (node.name.text !== name) return false; + const recv = node.expression; + if ( + node.getSourceFile() === home && + ((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv)) + ) + return true; + const target = lowerer.checker.getSymbolAtLocation(node.name); + return target !== undefined && symbols.has(target); + } + if (ts.isElementAccessExpression(node)) { + const key = node.argumentExpression; + if (!ts.isStringLiteralLike(key) || key.text !== name) return false; + const target = lowerer.checker.getSymbolAtLocation(node); + return target !== undefined && symbols.has(target); + } + return false; + }; + for (const sf of lowerer.program.getSourceFiles()) { + if (written) break; + if (sf.isDeclarationFile || !sf.text.includes(name)) continue; + ts.walkPreorder(sf, (node) => { + if (written) return "stop"; + let target: ts.Node | undefined; + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + target = node.left; + else if (ts.isDeleteExpression(node)) target = node.expression; + else if ( + (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) + ) + target = node.operand; + if (target && targets(target)) { + written = true; + return "stop"; + } + return undefined; + }); + } + return written; } function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): boolean { @@ -245,3 +414,29 @@ function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): bo }); return written; } + +/** The layout type of a symbol-keyed field read through a CommonJS + * key-table key (`this[kCount]` with `kCount` from a `module.exports = { + * kCount: Symbol('count') }` table), or null. The checker types such keys + * as plain `symbol`, so it answers the read with the union of every + * symbol-keyed property of the class; the field's own type is the read's + * type. Keys declared as consts keep their unique-symbol checker types. */ +export function tableKeyFieldType( + lowerer: Lowerer, + node: ts.ElementAccessExpression, +): IrType | null { + const key = node.argumentExpression; + if (!ts.isIdentifier(key) && !ts.isPropertyAccessExpression(key)) return null; + const keyType = lowerer.checker.getTypeAtLocation(key); + if (!(keyType.flags & ts.TypeFlags.ESSymbol) || keyType.flags & ts.TypeFlags.UniqueESSymbol) + return null; + const resolved = classSymbolKeyOf(lowerer, key); + if (!resolved) return null; + const decl = resolved.identity.valueDeclaration; + if (!decl || !ts.isPropertyAssignment(decl)) return null; + const receiver = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(node.expression)); + if (receiver?.kind !== "object") return null; + const info = lowerer.classes.get(receiver.className); + const field = info?.symbolFields?.get(resolved.identity); + return field === undefined ? null : (info!.fields.get(field) ?? null); +} diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 50b5ecf09..8227c6bd0 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -22,6 +22,7 @@ import { threadSlotsMemberOf, } from "../experimental-import.js"; import * as ts from "../ts7/adapter.js"; +import { withUndefinedArm } from "../type-mapper.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; import { locOf } from "../program.js"; @@ -238,10 +239,16 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): const args = [name, receiver, key]; switch (member) { case "get": { + // The slots hold values in the layout of the type the constructor registered (`family.value`, + // which `set` stores with). The receiver's annotation can name a structurally wider value + // type, so the value read is converted to the call's type like any value flowing into it. const resultTs = lowerer.typeOf(expr); const result = lowerer.mapTypeOf(resultTs); if (result === null) lowerer.badType(expr, resultTs); - return { kind: "intrinsic", name: "slots.get", args, type: result!, loc }; + const stored = withUndefinedArm(family.value, lowerer.unions); + if (stored === null) lowerer.badType(expr, resultTs); + const read: IrExpr = { kind: "intrinsic", name: "slots.get", args, type: stored!, loc }; + return typeKey(stored!) === typeKey(result!) ? read : lowerer.coerceInto(expr, read, result!); } case "has": case "delete": diff --git a/packages/compiler/src/frontend/mock-shapes.ts b/packages/compiler/src/frontend/mock-shapes.ts index 1baf2cd04..50781c272 100644 --- a/packages/compiler/src/frontend/mock-shapes.ts +++ b/packages/compiler/src/frontend/mock-shapes.ts @@ -12,10 +12,11 @@ * object; empty for mock.fn * * A MockFunctionCall is { arguments, error, result, this }: the - * arguments as a tuple of F's parameters, the result as F's return type or - * undefined (a checked-dynamic value for functions returning void or - * undefined, since a record field cannot have a unit type), and error and - * this as checked-dynamic values. */ + * arguments as a tuple of F's parameters (a checked-dynamic array for + * mock.fn()'s `(...args: any[]) => undefined`), the result as F's return + * type or undefined (a checked-dynamic value for functions returning void + * or undefined, since a record field cannot have a unit type), and error + * and this as checked-dynamic values. */ import { DYN, F64, NULL_T, UNDEFINED_T, VOID, typeKey, type IrType } from "../ir/ir.js"; import type { TypeMapperCtx } from "./type-mapper.js"; @@ -34,11 +35,17 @@ function orUndefined(t: IrType, reg: MockShapeRegistries): IrType { } /** The arguments type of a call record: a tuple of the parameters, or the - * rest array itself for a function whose only parameter is a rest array. - * Null for other rest shapes. A function without parameters records the + * rest array itself for a function whose only parameter is a rest array, + * or a checked-dynamic array for a function returning nothing whose only + * parameter is a checked-dynamic rest (mock.fn()'s default, `(...args: + * any[]) => undefined`). Null for other rest shapes. A function without parameters records the * empty tuple, which maps to an array of the unit-only union, as * type-mapper.ts maps `[]`. */ function mockArgumentsType(fn: IrType & { kind: "func" }, reg: MockShapeRegistries): IrType | null { + if (fn.rest && fn.restAbi === undefined) { + const returnsNothing = fn.ret.kind === "void" || fn.ret.kind === "undefinedT"; + return fn.params.length === 0 && returnsNothing ? DYN : null; + } if (fn.rest) { if (fn.params.length !== 1 || fn.params[0]!.kind !== "array") return null; return fn.params[0]!; diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts new file mode 100644 index 000000000..6bc830d74 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts @@ -0,0 +1,300 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; +import { rewriteBundlerCjsExports } from "./npm-static-rewrite.js"; +import type { BundledChunkSplit } from "./npm-static-bundled-chunks.js"; + +const keyedHelper = `var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +`; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function split(source: string, file = "/pkg/dist/index.js") { + return nodeFrontendServices().splitCjsChunks(source, file); +} + +function run(file: string) { + const result = spawnSync(process.execPath, [file], { encoding: "utf8" }); + return { status: result.status, stdout: result.stdout, stderr: result.stderr }; +} + +/** Runs the original bundle and its split modules side by side. */ +function expectSameBehavior(source: string): BundledChunkSplit { + const dir = mkdtempSync(join(tmpdir(), "scriptc-chunks-")); + dirs.push(dir); + const original = join(dir, "original.js"); + writeFileSync(original, source); + const bundle = join(dir, "index.js"); + const result = split(source, bundle); + expect(result).not.toBeNull(); + expect(result).not.toHaveProperty("degrade"); + const answer = result as BundledChunkSplit; + writeFileSync(bundle, answer.text); + for (const chunk of answer.chunks) writeFileSync(chunk.path, chunk.source); + // Respelled calls and blanked declarations keep the line structure. + expect(answer.text.split("\n")).toHaveLength(source.split("\n").length); + const oracle = run(original); + expect(oracle.status).toBe(0); + expect(run(bundle)).toEqual(oracle); + return answer; +} + +test("chunks run once, at their first require, with shared module instances", () => { + const answer = expectSameBehavior(`"use strict"; +${keyedHelper} +var require_b = __commonJS({ + "b.js"(exports2, module2) { + console.log("b init"); + module2.exports = { n: 1 }; + } +}); +var require_a = __commonJS({ + "a.js"(exports2) { + console.log("a init"); + var b = require_b(); + exports2.value = b.n + 1; + exports2.keys = Object.keys({ exports2 }); + } +}); +console.log("start"); +console.log(require_a().value, require_a().value, require_b().n, require_a().keys); +`); + expect(answer.chunks.map((c) => basename(c.path))).toEqual([ + "index.js.require_b.cjs", + "index.js.require_a.cjs", + ]); + expect(answer.text).not.toContain("require_a()"); +}); + +test("the arrow helper form and minified spellings split the same way", () => { + expectSameBehavior( + `var __commonJS=(cb,mod)=>()=>(mod||cb((mod={exports:{}}).exports,mod),mod.exports);` + + `var require_x=__commonJS((exports,module)=>{module.exports={v:"x"}});` + + `console.log(require_x().v,require_x()===require_x());`, + ); +}); + +test("esbuild 0.11's Object.keys spelling is recognized", () => { + expectSameBehavior(`var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[Object.keys(cb)[0]])((mod = {exports: {}}).exports, mod), mod.exports; +}; +var require_y = __commonJS({ + "y.js"(exports) { + exports.y = 3; + } +}); +console.log(require_y().y); +`); +}); + +test("pure helpers a chunk reads travel with it", () => { + const answer = expectSameBehavior(`${keyedHelper}var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +var require_named = __commonJS({ + "named.js"(exports2, module2) { + module2.exports = __name(function () {}, "renamed"); + } +}); +console.log(require_named().name); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("var __name ="); + expect(chunk).toContain("var __defProp = Object.defineProperty;"); +}); + +test("a helper travels with a chunk whose nested scopes declare its name", () => { + const answer = expectSameBehavior(`${keyedHelper}function label(n) { + return "label " + n; +} +function wrap(n) { + function inner() { + var label = "inner"; + return label; + } + return label(n) + " " + inner(); +} +var require_e = __commonJS({ + "e.js"(exports2, module2) { + function nested() { + var label = "nested"; + return label; + } + var seen = []; + for (let label of ["loop"]) seen.push(label); + try { + throw "caught"; + } catch (label) { + seen.push(label); + } + { + const label = "block"; + seen.push(label); + } + module2.exports = { outer: label(1), indirect: (0, label)(3), wrapped: wrap(2), nested: nested(), seen }; + } +}); +var e = require_e(); +console.log(e.outer, e.indirect, e.wrapped, e.nested, e.seen.join(",")); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("function label(n)"); + expect(chunk).toContain("function wrap(n)"); +}); + +test("a named function expression that only calls itself travels with a chunk", () => { + const answer = expectSameBehavior(`${keyedHelper}var fact = function self(n) { + return n <= 1 ? 1 : n * self(n - 1); +}; +var kind = function self(self) { + return typeof self; +}; +var outer = function self() { + var self = 2; + return self; +}; +var require_g = __commonJS({ + "g.js"(exports2, module2) { + module2.exports = [fact(5), kind(1), outer()]; + } +}); +console.log(require_g().join(",")); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("var fact = function self(n)"); +}); + +test("a helper a parameter default reads travels with a chunk whose body declares its name", () => { + const answer = expectSameBehavior(`${keyedHelper}var ten = function () { + return 10; +}; +var add = function (n, base = ten()) { + var ten = 1; + return n + base + ten; +}; +var require_g = __commonJS({ + "g.js"(exports2, module2) { + module2.exports = add(1); + } +}); +console.log(require_g()); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("var ten = function ()"); +}); + +test.each([ + ["exports it", `function tally() {}`, `module2.exports = { tally };`], + ["reads its properties", `function tally() {}`, `exports2.calls = tally.calls;`], + ["constructs it", `function tally() {}`, `exports2.made = new tally();`], + ["stores an arrow helper", `var tally = () => 1;`, `exports2.fn = tally;`], + [ + "calls a function that reads its own name as a value", + `var tally = function self() { self.n = (self.n || 0) + 1; return self.n; };`, + `exports2.count = tally();`, + ], + [ + "calls a function whose parameter default reads its own name", + `var tally = function self(box = self) { box.n = (box.n || 0) + 1; return box.n; };`, + `exports2.count = tally();`, + ], + [ + "calls a function whose parameter default reads its own name past a body var of that name", + `var tally = function self(box = self) { var self = 0; box.n = (box.n || 0) + 1; return box.n + self; };`, + `exports2.count = tally();`, + ], +])("a chunk that %s keeps the package refused", (_, declaration, statement) => { + const result = split(`${keyedHelper}${declaration} +var require_f = __commonJS({ + "f.js"(exports2, module2) { + ${statement} + } +}); +require_f(); +`); + expect(result).toEqual({ + degrade: expect.stringContaining( + "uses the bundle's top-level function 'tally' as a value", + ) as string, + }); +}); + +test.each([ + ["reads its factory's receiver", `console.log(this);`], + ["reads its factory's arguments object", `console.log(arguments.length);`], + ["returns from its top level", `if (exports2) return;`], + ["uses its module object beyond module.exports", `console.log(module2.id);`], + ["reads __filename", `console.log(__filename);`], + ["rebinds its module parameters", `function f(module2) { return module2; }`], + ["reads the bundle's top-level binding 'state'", `console.log(state);`], +])("a chunk that %s keeps the package refused", (reason, statement) => { + const result = split(`${keyedHelper}var state = { n: 1 }; +var require_c = __commonJS({ + "c.js"(exports2, module2) { + ${statement} + } +}); +require_c(); +`); + expect(result).toEqual({ degrade: expect.stringContaining(reason) as string }); +}); + +test("a factory used other than as a direct call keeps the package refused", () => { + const result = + split(`${keyedHelper}var require_d = __commonJS({ "d.js"(exports2) { exports2.d = 1; } }); +var load = require_d; +load(); +`); + expect(result).toEqual({ + degrade: expect.stringContaining("used other than as a direct call") as string, + }); +}); + +test("an unrecognized helper body keeps the package refused", () => { + const result = + split(`var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod); +var require_e = __commonJS((exports) => { exports.e = 1; }); +require_e(); +`); + expect(result).toEqual({ degrade: expect.stringContaining("__commonJS helper") as string }); +}); + +test("files without chunks and ES-module bundles are left alone", () => { + expect(split("exports.x = 1;\n")).toBeNull(); + expect( + split(`${keyedHelper}var require_f = __commonJS({ "f.js"(e) { e.f = 1; } }); +export default require_f(); +`), + ).toBeNull(); +}); + +test("an identifier-minified bundle names its lost helper names", () => { + // esbuild --minify output: the helper shapes survive, their names do not. + const result = split( + `var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` + + `var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`, + ); + expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string }); +}); + +test("an ES-module bundle of CommonJS chunks names the chunk interop", () => { + const result = rewriteBundlerCjsExports( + `${keyedHelper}var __toESM = (mod) => mod; +var require_g = __commonJS({ "g.js"(exports) { exports.g = 1; } }); +var import_g = __toESM(require_g()); +export const g = import_g.g; +`, + "/pkg/dist/index.js", + ); + expect(result).toEqual({ + degrade: expect.stringContaining("bundled CommonJS modules") as string, + }); +}); diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts new file mode 100644 index 000000000..33ff865ec --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -0,0 +1,858 @@ +/* Bundled CommonJS chunks: esbuild wraps every CommonJS module it bundles + * in a lazily evaluated factory, + * + * var require_x = __commonJS({ "src/x.js"(exports, module) { BODY } }); + * + * and each `require_x()` runs BODY once (later calls answer the cached + * module.exports, a call during BODY answers the partial one). That is + * exactly a CommonJS module that the bundle requires at the call's + * position, so each recognized chunk becomes its own module: a virtual + * sibling file `..cjs` holding BODY, its module + * parameters respelled as the CommonJS globals, and every `require_x()` + * call becomes `require("./..cjs")`. The program then + * compiles chunks with the ordinary CommonJS machinery: evaluation at the + * require's position, the run-once module cache, export tables, and the + * __toESM interop erasure. + * + * Only shapes whose meaning is verified move: the helper must be one of + * the recognized __commonJS spellings, every reference to a factory + * binding must be a direct call, and BODY must not observe the + * difference between a factory and a module (its `this`, `arguments`, + * `__filename`, the module object beyond `module.exports`, a top-level + * return, or a binding of the bundle other than the pure helpers copied + * beside it). Anything else answers a degrade reason. */ +import * as ts from "./ts7/syntax.js"; +import { helperTokens } from "./helper-tokens.js"; + +export interface BundledChunkSplit { + /** The bundle with chunk declarations blanked and calls respelled. */ + text: string; + /** Virtual sibling modules, in declaration order. */ + chunks: { path: string; source: string }[]; +} + +interface Edit { + start: number; + end: number; + text: string; +} + +const CHUNK_SHAPE_DEGRADE = + "its shipped JS bundles CommonJS modules through the __commonJS helper in a shape the " + + "recognizer cannot verify"; + +/** The helper a counter-suffixed bundler name spells (`__commonJS2`). */ +function helperBase(name: string): string { + return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name; +} + +function unparen(e: ts.Expression): ts.Expression { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} + +/** True for an identifier occurrence that reads or writes a binding (as + * opposed to a property name, a label, or a declaration's own name). + * Shorthand property values count: `{ x }` reads x. */ +function isReference(id: ts.Identifier): boolean { + const p = id.parent; + if (p === undefined) return false; + if (ts.isPropertyAccessExpression(p)) return p.expression === id; + if (ts.isPropertyAssignment(p)) return p.initializer === id; + if ( + ts.isMethodDeclaration(p) || + ts.isPropertyDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) + ) + return p.name !== id; + if (ts.isBindingElement(p)) return p.initializer === id; + if (ts.isVariableDeclaration(p) || ts.isParameter(p)) return p.initializer === id; + if ( + ts.isFunctionDeclaration(p) || + ts.isFunctionExpression(p) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) + ) + return p.name !== id; + if (ts.isLabeledStatement(p)) return p.label !== id; + if (ts.isBreakStatement(p) || ts.isContinueStatement(p)) return false; + if (ts.isMetaProperty(p)) return false; + return true; +} + +/** Names a subtree declares anywhere inside it: variables (destructuring + * included), parameters, function and class names, catch bindings. */ +function declaredNames(root: ts.Node): Set { + const names = new Set(); + // Binding patterns nest BindingElements, which the walk visits too. + ts.walkPreorder(root, (n) => { + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + if (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) { + const name = n.name; + if (name !== undefined && ts.isIdentifier(name)) names.add(name.text); + } else if ( + (ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n)) && + n.name !== undefined + ) + names.add(n.name.text); + return undefined; + }); + return names; +} + +/** Adds the identifiers a binding name (an identifier or a destructuring + * pattern) declares. */ +function addBindingNames(name: ts.BindingName, out: Set): void { + if (ts.isIdentifier(name)) { + out.add(name.text); + return; + } + for (const element of name.elements.nodes) { + if (!ts.isOmittedExpression(element) && element.name !== undefined) + addBindingNames(element.name, out); + } +} + +function isFunctionScope(n: ts.Node): n is ts.SignatureDeclaration { + return ( + ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isArrowFunction(n) || + ts.isMethodDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isConstructorDeclaration(n) + ); +} + +/** The `var` names a function body declares, without entering nested + * functions. Function declarations in nested blocks count as block-scoped + * here; treating one as free only adds an unused helper copy. */ +function varScopedNames(body: ts.Node): Set { + const out = new Set(); + ts.walkPreorder(body, (n) => { + if (n !== body && (isFunctionScope(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n))) + return "skip"; + if (ts.isVariableDeclarationList(n) && (n.flags & ts.NodeFlags.BlockScoped) === 0) { + for (const d of n.declarations.nodes) addBindingNames(d.name, out); + } + return undefined; + }); + return out; +} + +/** The names a function binds for references in its parameter list + * (default values, computed keys of destructuring patterns): its + * parameters and a function expression's own name. Parameter initializers + * run in their own scope, so the body's `var`s are not visible there. */ +function parameterScopeNames(scope: ts.SignatureDeclaration): Set { + const out = new Set(); + for (const param of scope.parameters.nodes) addBindingNames(param.name, out); + if (ts.isFunctionExpression(scope) && scope.name !== undefined) out.add(scope.name.text); + return out; +} + +const NO_NAMES: ReadonlySet = new Set(); + +/** True when `node` lies inside `ancestor` (or is it). */ +function within(node: ts.Node, ancestor: ts.Node): boolean { + for (let n: ts.Node | undefined = node; n !== undefined; n = n.parent) + if (n === ancestor) return true; + return false; +} + +/** The names one scope node binds for the references inside it. */ +function scopeNames(scope: ts.Node): Set { + const out = new Set(); + if (isFunctionScope(scope)) { + for (const param of scope.parameters.nodes) addBindingNames(param.name, out); + if (ts.isFunctionExpression(scope) && scope.name !== undefined) out.add(scope.name.text); + if (scope.body !== undefined) for (const name of varScopedNames(scope.body)) out.add(name); + } else if (ts.isBlock(scope) || ts.isSourceFile(scope) || ts.isCaseBlock(scope)) { + const statements = ts.isCaseBlock(scope) + ? scope.clauses.nodes.flatMap((clause) => clause.statements.nodes) + : scope.statements.nodes; + for (const stmt of statements) { + if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name) + out.add(stmt.name.text); + else if ( + ts.isVariableStatement(stmt) && + (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0 + ) { + for (const d of stmt.declarationList.declarations.nodes) addBindingNames(d.name, out); + } + } + } else if (ts.isCatchClause(scope) && scope.variableDeclaration !== undefined) { + addBindingNames(scope.variableDeclaration.name, out); + } else if ( + (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) && + scope.initializer !== undefined && + ts.isVariableDeclarationList(scope.initializer) && + (scope.initializer.flags & ts.NodeFlags.BlockScoped) !== 0 + ) { + for (const d of scope.initializer.declarations.nodes) addBindingNames(d.name, out); + } else if ( + (ts.isClassDeclaration(scope) || ts.isClassExpression(scope)) && + scope.name !== undefined + ) { + out.add(scope.name.text); + } + return out; +} + +/** Resolves references against one bundle's scopes, keeping each scope's + * names for the bundle's split. */ +class BundleScopes { + private readonly names = new Map>(); + private readonly parameterNames = new Map>(); + + private of(scope: ts.Node): Set { + let names = this.names.get(scope); + if (names === undefined) { + names = scopeNames(scope); + this.names.set(scope, names); + } + return names; + } + + /** The names `scope` binds for a reference reached through its child + * `from`: a function's parameter list sees its parameters and own name + * but not its body's `var`s, and a computed method name sees none of + * them. */ + private visible(scope: ts.Node, from: ts.Node): ReadonlySet { + if (!isFunctionScope(scope) || from === scope.body) return this.of(scope); + if (from === scope.name) return NO_NAMES; + let names = this.parameterNames.get(scope); + if (names === undefined) { + names = parameterScopeNames(scope); + this.parameterNames.set(scope, names); + } + return names; + } + + /** True when a scope between the reference and `root` (inclusive) + * binds its name. */ + boundWithin(id: ts.Identifier, root: ts.Node): boolean { + for (let from: ts.Node = id, n = id.parent; n !== undefined; from = n, n = n.parent) { + if (this.visible(n, from).has(id.text)) return true; + if (n === root) return false; + } + return false; + } + + /** True when a scope inside `root` (exclusive) binds the reference's + * name. */ + boundInside(id: ts.Identifier, root: ts.Node): boolean { + for ( + let from: ts.Node = id, n = id.parent; + n !== undefined && n !== root; + from = n, n = n.parent + ) { + if (this.visible(n, from).has(id.text)) return true; + } + return false; + } + + /** True when the reference resolves to a top-level binding of `sf`. */ + topLevel(id: ts.Identifier, sf: ts.SourceFile): boolean { + for ( + let from: ts.Node = id, n = id.parent; + n !== undefined && n !== sf; + from = n, n = n.parent + ) { + if (this.visible(n, from).has(id.text)) return false; + } + return true; + } +} + +/** Every binding reference inside a subtree. */ +function references(root: ts.Node): ts.Identifier[] { + const out: ts.Identifier[] = []; + ts.walkPreorder(root, (n) => { + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + if (ts.isIdentifier(n) && isReference(n)) out.push(n); + return undefined; + }); + return out; +} + +/** True when the reference is written (assignment target, update operand, + * loop target, delete operand). */ +function isWritten(id: ts.Identifier): boolean { + let child: ts.Node = id; + for (let p = child.parent; p !== undefined; child = p, p = p.parent) { + if (ts.isBinaryExpression(p)) { + return ( + p.left === child && + p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + p.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ); + } + if (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) { + return ( + p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken + ); + } + if (ts.isForInStatement(p) || ts.isForOfStatement(p)) return p.initializer === child; + if (ts.isDeleteExpression(p)) return true; + if ( + !( + ts.isParenthesizedExpression(p) || + ts.isArrayLiteralExpression(p) || + ts.isObjectLiteralExpression(p) || + ts.isShorthandPropertyAssignment(p) || + ts.isPropertyAssignment(p) || + ts.isSpreadElement(p) + ) + ) + return false; + } + return false; +} + +/** The recognized __commonJS helper forms, as token templates over the + * helper's own parameter names (CB, MOD) and the own-key reader (KEYS: + * esbuild's `__getOwnPropNames` alias, or `Object.keys` in esbuild 0.11): + * keyed: `(CB, MOD) => function [name]() { return ; }` — the + * factory is the only method of an object literal; + * arrow: `(CB, MOD) => () => ()` — the factory itself. */ +const KEYED_RETURN = + "MOD || (0, CB[KEYS(CB)[0]])((MOD = { exports: {} }).exports, MOD), MOD.exports"; +const ARROW_BODY = "(MOD || CB((MOD = { exports: {} }).exports, MOD), MOD.exports)"; + +function template(text: string, cb: string, mod: string, keys: string): string | null { + return helperTokens( + text + .replace(/\bCB\b/g, cb) + .replace(/\bMOD\b/g, mod) + .replace(/\bKEYS\b/g, keys), + ); +} + +type HelperForm = "keyed" | "arrow"; + +/** The helper's form, or null when its initializer is not recognized. + * `ownNames` answers whether an identifier names the verified + * Object.getOwnPropertyNames alias. */ +function helperForm( + init: ts.Expression, + sf: ts.SourceFile, + ownNames: (name: string) => boolean, +): HelperForm | null { + const fn = unparen(init); + if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 2) return null; + const params = fn.parameters.nodes.map((p) => + ts.isIdentifier(p.name) && p.initializer === undefined && p.dotDotDotToken === undefined + ? p.name.text + : null, + ); + const [cb, mod] = params; + if (cb === null || mod === null || cb === undefined || mod === undefined || cb === mod) + return null; + if (ts.isBlock(fn.body)) return null; + const body = unparen(fn.body); + if (ts.isFunctionExpression(body)) { + if (body.parameters.nodes.length !== 0 || body.asteriskToken || body.modifiers?.nodes.length) + return null; + const stmts = body.body.statements.nodes; + const ret = stmts[0]; + if (stmts.length !== 1 || ret === undefined || !ts.isReturnStatement(ret) || !ret.expression) + return null; + const actual = helperTokens(ret.expression.getText(sf)); + if (actual === null) return null; + // The own-key reader: esbuild's alias or the 0.11 spelling. + const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(ret.expression.getText(sf)); + const keys = keysRef?.[1]; + if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; + return actual === template(KEYED_RETURN, cb, mod, keys) ? "keyed" : null; + } + if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { + const actual = helperTokens(body.body.getText(sf)); + return actual !== null && actual === template(ARROW_BODY, cb, mod, "") ? "arrow" : null; + } + return null; +} + +/** A top-level declaration the virtual chunk can copy verbatim: a pure + * helper whose evaluation runs nothing (a function, or a member read of + * Object such as `Object.create` / `Object.prototype.hasOwnProperty`). */ +function copyableInitializer(init: ts.Expression | undefined): boolean { + if (init === undefined) return false; + const e = unparen(init); + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) return true; + let walk: ts.Expression = e; + let depth = 0; + while (ts.isPropertyAccessExpression(walk) && ts.isIdentifier(walk.name)) { + walk = walk.expression; + depth++; + } + return depth > 0 && ts.isIdentifier(walk) && walk.text === "Object"; +} + +/** A top-level declaration whose value is a function. */ +function isFunctionValued(node: ts.VariableDeclaration | ts.FunctionDeclaration): boolean { + if (ts.isFunctionDeclaration(node)) return true; + const init = node.initializer && unparen(node.initializer); + return init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)); +} + +/** True when a named function expression initializer (`var f = function + * self() {}`) uses its own name only as a callee: `self` is the copied + * function, so it is as observable as the top-level binding. */ +function selfCalledOnly( + node: ts.VariableDeclaration | ts.FunctionDeclaration, + scopes: BundleScopes, +): boolean { + if (!ts.isVariableDeclaration(node) || node.initializer === undefined) return true; + const fn = unparen(node.initializer); + if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return true; + const self = fn.name.text; + // A parameter of the same name hides the function's own name everywhere + // in it; a `var` of the same name hides it only in the body, since the + // parameter initializers do not see the body's `var`s. + if (fn.parameters.nodes.some((p) => bindsName(p.name, self))) return true; + const bodyVar = varScopedNames(fn.body).has(self); + const body = fn.body; + return references(fn).every( + (r) => + r.text !== self || scopes.boundInside(r, fn) || (bodyVar && within(r, body)) || isCallee(r), + ); +} + +/** True when a binding name (an identifier or a destructuring pattern) + * declares `name`. */ +function bindsName(binding: ts.BindingName, name: string): boolean { + const names = new Set(); + addBindingNames(binding, names); + return names.has(name); +} + +/** True when a reference is the function a call invokes: `f(...)`, or the + * `(0, f)(...)` form that drops the receiver. */ +function isCallee(id: ts.Identifier): boolean { + let callee: ts.Node = id; + const p = id.parent; + if ( + p !== undefined && + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.CommaToken && + p.right === id && + p.parent !== undefined && + ts.isParenthesizedExpression(p.parent) + ) + callee = p.parent; + while (callee.parent !== undefined && ts.isParenthesizedExpression(callee.parent)) + callee = callee.parent; + const call = callee.parent; + return call !== undefined && ts.isCallExpression(call) && call.expression === callee; +} + +/** One recognized chunk declaration. */ +interface Chunk { + name: string; + stmt: ts.VariableStatement; + params: string[]; + body: ts.Block; + path: string; + spec: string; +} + +interface TopLevel { + stmt: ts.Statement; + /** The declarator (or function declaration) itself. */ + node: ts.VariableDeclaration | ts.FunctionDeclaration; +} + +/** True when the file opens with a "use strict" directive prologue. */ +function strictPrologue(stmts: readonly ts.Statement[]): boolean { + for (const s of stmts) { + if (!ts.isExpressionStatement(s) || !ts.isStringLiteral(s.expression)) return false; + if (s.expression.text === "use strict") return true; + } + return false; +} + +/** A cheap text probe: the helper's name, or its minified body's + * `(m={exports:{}}).exports,m` fragment. */ +export function mayBundleChunks(source: string): boolean { + return ( + source.includes("__commonJS") || + /\{\s*exports\s*:\s*\{\s*\}\s*\}\s*\)\s*\.exports\s*,/.test(source) + ); +} + +/** Splits a CommonJS bundle's __commonJS chunks into virtual modules (see + * the header). Null: no chunks (or not a CommonJS bundle) — serve the file + * unchanged. */ +export function splitBundledChunks( + sf: ts.SourceFile, + filePath: string, +): BundledChunkSplit | { degrade: string } | null { + const source = sf.text; + if (!mayBundleChunks(source)) return null; + // ES-module bundles have no require: their chunks keep the existing + // treatment (the Bun factory recognizer, or the ESM interop refusal). + if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null; + + const top = new Map(); + const duplicate = new Set(); + for (const stmt of sf.statements.nodes) { + const add = (name: string, node: TopLevel["node"]): void => { + if (top.has(name)) duplicate.add(name); + else top.set(name, { stmt, node }); + }; + if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) add(stmt.name.text, stmt); + else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (ts.isIdentifier(d.name)) add(d.name.text, d); + } + } + } + const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS"); + + // Every name a recognizer relies on is written nowhere and declared + // once at the top level; `Object` itself is never rebound. + const allRefs = references(sf); + const written = new Set(); + for (const r of allRefs) if (isWritten(r)) written.add(r.text); + const declaredAnywhere = declaredNames(sf); + if (declaredAnywhere.has("Object") || written.has("Object")) + return { degrade: CHUNK_SHAPE_DEGRADE }; + const stable = (name: string): boolean => !duplicate.has(name) && !written.has(name); + // Names whose every reference in the bundle is a callee: `f(...)` or the + // `(0, f)(...)` form. + const scopes = new BundleScopes(); + const valueUsed = new Set(); + for (const r of allRefs) if (!isCallee(r) && scopes.topLevel(r, sf)) valueUsed.add(r.text); + const calledOnly = (name: string): boolean => !valueUsed.has(name); + const ownNames = (name: string): boolean => { + const t = top.get(name); + if (t === undefined || !stable(name) || !ts.isVariableDeclaration(t.node)) return false; + const init = t.node.initializer; + return ( + init !== undefined && + helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames") + ); + }; + if (helperNames.length === 0) { + // The helper's shape under another name: an identifier-minified bundle, + // whose other interop helpers lost their names as well. + for (const [name, t] of top) { + const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; + if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null) + return { + degrade: + "its shipped JS is an identifier-minified bundle whose bundler-interop helpers " + + "(__commonJS and the rest) have lost the names the recognizers verify", + }; + } + return null; + } + const forms = new Map(); + for (const name of helperNames) { + const t = top.get(name)!; + const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; + const form = init !== undefined && stable(name) ? helperForm(init, sf, ownNames) : null; + if (form === null) return { degrade: CHUNK_SHAPE_DEGRADE }; + forms.set(name, form); + } + + // The chunk declarations: `var NAME = HELPER(factory);`, one declarator. + const chunks = new Map(); + const base = filePath.replace(/^.*[\\/]/, ""); + for (const [name, t] of top) { + if (!ts.isVariableDeclaration(t.node) || t.node.initializer === undefined) continue; + const call = unparen(t.node.initializer); + if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) continue; + const form = forms.get(call.expression.text); + if (form === undefined) continue; + const stmt = t.stmt; + if ( + !ts.isVariableStatement(stmt) || + stmt.declarationList.declarations.nodes.length !== 1 || + call.arguments.nodes.length !== 1 || + !stable(name) + ) + return { degrade: CHUNK_SHAPE_DEGRADE }; + let factory: ts.Node = call.arguments.nodes[0]!; + if (form === "keyed") { + if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1) + return { degrade: CHUNK_SHAPE_DEGRADE }; + factory = factory.properties.nodes[0]!; + if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name)) + return { degrade: CHUNK_SHAPE_DEGRADE }; + } else { + factory = unparen(factory as ts.Expression); + if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)) + return { degrade: CHUNK_SHAPE_DEGRADE }; + } + const fn = factory as ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression; + if ( + fn.body === undefined || + !ts.isBlock(fn.body) || + fn.asteriskToken !== undefined || + (fn.modifiers?.nodes.length ?? 0) > 0 || + fn.parameters.nodes.length > 2 + ) + return { degrade: CHUNK_SHAPE_DEGRADE }; + const params: string[] = []; + for (const p of fn.parameters.nodes) { + if (!ts.isIdentifier(p.name) || p.initializer !== undefined || p.dotDotDotToken) + return { degrade: CHUNK_SHAPE_DEGRADE }; + params.push(p.name.text); + } + const file = `${base}.${name}.cjs`; + chunks.set(name, { + name, + stmt, + params, + body: fn.body, + path: filePath.slice(0, filePath.length - base.length) + file, + spec: `./${file}`, + }); + } + if (chunks.size === 0) return null; + + // Every factory reference is a direct zero-argument call. Calls inside a + // chunk body belong to that chunk's text, the rest to the bundle's. + const callEdits: Edit[] = []; + for (const r of allRefs) { + const chunk = chunks.get(r.text); + if (chunk === undefined) continue; + const call = r.parent; + if ( + call === undefined || + !ts.isCallExpression(call) || + call.expression !== r || + call.arguments.nodes.length !== 0 || + call.questionDotToken !== undefined + ) + return { + degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`, + }; + callEdits.push({ + start: call.getStart(sf), + end: call.getEnd(), + text: `require(${JSON.stringify(chunk.spec)})`, + }); + } + for (const name of chunks.keys()) { + if (declaredAnywhere.has(name) && countDeclarations(sf, name) > 1) + return { degrade: CHUNK_SHAPE_DEGRADE }; + } + + const bundleStrict = strictPrologue(sf.statements.nodes); + const out: BundledChunkSplit["chunks"] = []; + const bundleEdits: Edit[] = [...callEdits]; + for (const chunk of chunks.values()) { + const result = chunkSource( + sf, + chunk, + chunks, + top, + stable, + calledOnly, + scopes, + callEdits, + bundleStrict, + ); + if (typeof result !== "string") return result; + out.push({ path: chunk.path, source: result }); + bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + } + return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out }; +} + +function countDeclarations(sf: ts.SourceFile, name: string): number { + let n = 0; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isVariableDeclaration(node) || + ts.isParameter(node) || + ts.isBindingElement(node) || + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node)) && + node.name !== undefined && + ts.isIdentifier(node.name) && + node.name.text === name + ) + n++; + return undefined; + }); + return n; +} + +/** Applies non-overlapping edits inside [from, to): replaced spans keep + * their newlines (blanked text stays line-aligned); edits nested in a + * blanked span vanish with it. */ +function applyEdits(source: string, from: number, to: number, edits: Edit[]): string { + const inside = edits + .filter((e) => e.start >= from && e.end <= to) + .sort((a, b) => a.start - b.start || b.end - a.end); + let text = ""; + let at = from; + for (const e of inside) { + if (e.start < at) continue; // nested in an earlier edit + text += source.slice(at, e.start); + const replaced = source.slice(e.start, e.end); + // A respelled span keeps its line breaks after the replacement. + text += + e.text === "" ? replaced.replace(/[^\r\n]/g, " ") : e.text + replaced.replace(/[^\n]/g, ""); + at = e.end; + } + return text + source.slice(at, to); +} + +/** The virtual module text for one chunk, or a degrade reason. */ +function chunkSource( + sf: ts.SourceFile, + chunk: Chunk, + chunks: ReadonlyMap, + top: ReadonlyMap, + stable: (name: string) => boolean, + calledOnly: (name: string) => boolean, + scopes: BundleScopes, + callEdits: readonly Edit[], + bundleStrict: boolean, +): string | { degrade: string } { + const why = (reason: string): { degrade: string } => ({ + degrade: `its bundled CommonJS module '${chunk.name}' ${reason}`, + }); + const [exportsParam, moduleParam] = chunk.params; + const body = chunk.body; + const local = declaredNames(body); + for (const p of chunk.params) { + if ( + local.has(p) || + (p !== "exports" && local.has("exports")) || + (p !== "module" && local.has("module")) + ) + return why( + "rebinds its module parameters inside its body (an identifier-minified bundle the recognizer cannot respell)", + ); + } + + /* The factory's own frame must stay unobservable: its receiver, its + * arguments object, and a top-level return have no module equivalent. */ + let frameUse: string | null = null; + /** The innermost frame boundary between a node and the chunk body. */ + const frameOf = (n: ts.Node): "factory" | "arrow" | "function" => { + let arrow = false; + for (let p = n.parent; p !== undefined && p !== body; p = p.parent) { + if ( + ts.isFunctionDeclaration(p) || + ts.isFunctionExpression(p) || + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) || + ts.isConstructorDeclaration(p) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) + ) + return "function"; + if (ts.isArrowFunction(p)) arrow = true; + } + return arrow ? "arrow" : "factory"; + }; + ts.walkPreorder(body, (n) => { + if (frameUse !== null) return "stop"; + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + let use: string | null = null; + if (n.kind === ts.SyntaxKind.ThisKeyword) use = "reads its factory's receiver (`this`)"; + else if (n.kind === ts.SyntaxKind.SuperKeyword) use = "reads `super` at its top level"; + else if (ts.isIdentifier(n) && n.text === "arguments" && isReference(n)) + use = "reads its factory's arguments object"; + else if (ts.isMetaProperty(n)) use = "reads a meta property at its top level"; + else if (ts.isReturnStatement(n)) { + if (frameOf(n) === "factory") frameUse = "returns from its top level"; + return undefined; + } + if (use !== null && frameOf(n) !== "function") frameUse = use; + return undefined; + }); + if (frameUse !== null) return why(frameUse); + + const edits: Edit[] = callEdits.filter((e) => e.start >= body.pos && e.end <= body.end); + const copied = new Set(); + const copyQueue: string[] = []; + for (const r of references(body)) { + const name = r.text; + if (chunks.has(name)) continue; // a call edit already respells it + if (name === exportsParam || name === moduleParam) { + if (name === moduleParam) { + const p = r.parent; + if (!ts.isPropertyAccessExpression(p) || p.expression !== r || p.name.text !== "exports") + return why("uses its module object beyond module.exports"); + } + const global = name === exportsParam ? "exports" : "module"; + if (name !== global) { + const p = r.parent; + edits.push( + ts.isShorthandPropertyAssignment(p) + ? { start: r.getStart(sf), end: r.getEnd(), text: `${name}: ${global}` } + : { start: r.getStart(sf), end: r.getEnd(), text: global }, + ); + } + continue; + } + // The factory function itself binds the parameters and `var`s. + if (scopes.boundWithin(r, body.parent ?? body)) continue; + if (name === "exports" || name === "module") + return why(`reads the bundle's own '${name}' binding`); + if (name === "__filename") return why("reads __filename, which names the bundle file"); + if (top.has(name)) { + if (!copied.has(name)) { + copied.add(name); + copyQueue.push(name); + } + } + } + + // The pure helpers BODY reads, closed over their own top-level reads. + const header: string[] = []; + for (let i = 0; i < copyQueue.length; i++) { + const name = copyQueue[i]!; + const t = top.get(name)!; + const node = t.node; + const pure = + stable(name) && + (ts.isFunctionDeclaration(node) || + (ts.isVariableDeclaration(node) && copyableInitializer(node.initializer))); + if (!pure || chunks.has(name)) + return why(`reads the bundle's top-level binding '${name}', which it cannot carry along`); + // Each chunk gets its own copy, so a function's identity and its + // properties must stay unobservable: the bundle only ever calls it. + if (isFunctionValued(node) && (!calledOnly(name) || !selfCalledOnly(node, scopes))) + return why( + `uses the bundle's top-level function '${name}' as a value, which its chunks cannot share`, + ); + for (const r of references(node)) { + if (scopes.boundWithin(r, node) || !top.has(r.text) || copied.has(r.text)) continue; + copied.add(r.text); + copyQueue.push(r.text); + } + } + // Declaration order of the bundle: a helper reads only earlier ones. + const ordered = [...copied].sort( + (a, b) => top.get(a)!.node.getStart(sf) - top.get(b)!.node.getStart(sf), + ); + for (const name of ordered) { + const node = top.get(name)!.node; + header.push(ts.isFunctionDeclaration(node) ? node.getText(sf) : `var ${node.getText(sf)};`); + } + + const bodyStrict = strictPrologue(body.statements.nodes); + const text = applyEdits(sf.text, body.getStart(sf) + 1, body.getEnd() - 1, edits); + const prologue = bundleStrict || bodyStrict ? '"use strict";\n' : ""; + return prologue + (header.length > 0 ? header.join("\n") + "\n" : "") + text + "\n"; +} diff --git a/packages/compiler/src/frontend/npm-static-rewrite-host.ts b/packages/compiler/src/frontend/npm-static-rewrite-host.ts index ee36f58d1..524239901 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-host.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-host.ts @@ -2,7 +2,12 @@ import { dirname, join, resolve as resolvePath } from "node:path"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; import type { Ts7SourceParser } from "./ts7/source-parser.js"; -import { trackedDirectoryExists, trackedFileExists, trackedReadFile } from "./input-tracker.js"; +import { + isVirtualSource, + trackedDirectoryExists, + trackedFileExists, + trackedReadFile, +} from "./input-tracker.js"; import { resolveExports } from "./resolve.js"; import { packageNameOfSpecifier } from "./workspace-registry.js"; import { isBundlerCjsCandidate, rewriteBundlerCjsSyntax } from "./npm-static-rewrite-syntax.js"; @@ -134,7 +139,7 @@ function requireTargetEsModuleStamped( } catch { /* unlexable — keep probing */ } - if (src.includes("__toCommonJS(")) return true; + if (/__toCommonJS\d*\(/.test(src)) return true; const fwd = /module\.exports\s*=\s*require\(\s*["']([^"']+)["']\s*\)/.exec(src); if (fwd !== null) return requireTargetEsModuleStamped(parser, file, fwd[1]!, depth + 1); return false; @@ -163,9 +168,12 @@ export function rewriteBundlerCjsWithParser( parser: Ts7SourceParser, source: string, filePath: string, + liveExportTable?: (filePath: string) => void, ): string | { degrade: string } | null { if (!isBundlerCjsCandidate(source)) return null; return rewriteBundlerCjsSyntax(parser.parse(filePath, source, "js"), filePath, { + liveExportTable, + requireOnly: isVirtualSource, requireTargetEsModuleStamped: (from, spec) => requireTargetEsModuleStamped(parser, from, spec), starTargetNames: (from, spec) => { const target = resolveRelativeCjs(from, spec); diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index 7938471f3..1730e1153 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -12,6 +12,70 @@ import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js"; export interface CjsRewriteHost { requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean; starTargetNames(fromFile: string, specifier: string): Set; + /** Told when the rewritten file's tail export table stands for a live + * getter table the original installed before it could run another + * module's code (liveExportTableFirst): a module of a require cycle that + * captured the export object early observes exactly that table. */ + liveExportTable?: ((filePath: string) => void) | undefined; + /** True for a module only ever loaded through require() (a bundle's + * CommonJS chunk split into its own module): no ES-module importer + * exists, so Node's lexer-visible name set bounds nothing and every + * name the export plumbing defines is part of its surface. */ + requireOnly?: ((filePath: string) => boolean) | undefined; +} + +/** True when esbuild's export object (`module.exports = __toCommonJS(T)` + * over a getter table `__export(T, {...})`) is installed before anything + * that can run another module's code, and nothing else writes the export + * object. Every statement before the installation is then a declaration, + * a directive, or the getter-table call (whose getters run only when + * read), and variable initializers evaluate no calls outside function + * bodies. The getters read live bindings, so the object a cycle partner + * captures early answers every later read exactly like the final table. */ +function liveExportTableFirst( + sf: ts.SourceFile, + installAt: number, + target: string, + exportTables: readonly { target: string }[], +): boolean { + if (!exportTables.some((t) => t.target === target)) return false; + let callsOutsideFunctions = false; + const noCalls = (root: ts.Node): boolean => { + callsOutsideFunctions = false; + ts.walkPreorder(root, (n) => { + if (callsOutsideFunctions) return "stop"; + if (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) + return "skip"; + if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n)) + callsOutsideFunctions = true; + return undefined; + }); + return !callsOutsideFunctions; + }; + let sawTable = false; + for (const stmt of sf.statements.nodes) { + if (stmt.getStart(sf) >= installAt) return sawTable; + if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) continue; + if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; + if (ts.isVariableStatement(stmt)) { + if ( + stmt.declarationList.declarations.nodes.every( + (d) => ts.isIdentifier(d.name) && (d.initializer === undefined || noCalls(d.initializer)), + ) + ) + continue; + return false; + } + if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) { + const table = exportCallOf(stmt.expression); + if (table !== null && table.target === target) { + sawTable = true; + continue; + } + } + return false; + } + return false; } export function isBundlerCjsCandidate(source: string): boolean { @@ -24,16 +88,31 @@ export function isBundlerCjsCandidate(source: string): boolean { ); } +/** The helper a bundler-emitted name spells. esbuild suffixes a helper + * that a bundled module declares again with a counter (a bundled esbuild + * dist inside another bundle carries `__export2`, `__toCommonJS2`, …); the + * recognizers below verify each call's shape, so the counter carries no + * meaning beyond the name collision it avoided. */ +export function bundlerHelperBase(name: string): string { + return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name; +} + /** The last NAME of a callee: bare identifier or any member chain's final - * name (`tslib_1.__exportStar` → "__exportStar"). */ + * name (`tslib_1.__exportStar` → "__exportStar"), helper counters dropped. */ function calleeNameOf(call: ts.CallExpression): string | null { const callee = call.expression; - if (ts.isIdentifier(callee)) return callee.text; + if (ts.isIdentifier(callee)) return bundlerHelperBase(callee.text); if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) - return callee.name.text; + return bundlerHelperBase(callee.name.text); return null; } +/** True for an identifier spelling the named helper (counter-suffixed + * spellings included). */ +function isHelperIdent(node: ts.Node, base: string): node is ts.Identifier { + return ts.isIdentifier(node) && bundlerHelperBase(node.text) === base; +} + /** An entry the canonical table can carry. */ interface EntrySpec { /** The export name (may need string-key spelling). */ @@ -194,13 +273,8 @@ interface ToEsmPlan { * __copyProps(…, mod) call copying onto a conditional that tests * isNodeMode/__esModule and defines "default" with value mod. Comments and * formatting are free; the SHAPE is the contract. */ -function recognizedToEsmDecl(stmt: ts.Statement): boolean { - if (!ts.isVariableStatement(stmt)) return false; - const decls = stmt.declarationList.declarations.nodes; - if (decls.length !== 1) return false; - const d = decls[0]!; - if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined) - return false; +function recognizedToEsmDecl(d: ts.VariableDeclaration): boolean { + if (!isHelperIdent(d.name, "__toESM") || d.initializer === undefined) return false; let init: ts.Expression = d.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (!ts.isArrowFunction(init)) return false; @@ -218,12 +292,7 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { expr = expr.right; } while (ts.isParenthesizedExpression(expr)) expr = expr.expression; - if ( - !ts.isCallExpression(expr) || - !ts.isIdentifier(expr.expression) || - expr.expression.text !== "__copyProps" - ) - return false; + if (!ts.isCallExpression(expr) || !isHelperIdent(expr.expression, "__copyProps")) return false; if (expr.arguments.nodes.length < 2) return false; const from = expr.arguments.nodes[1]!; if (!ts.isIdentifier(from) || from.text !== mod) return false; @@ -234,12 +303,7 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false; let stamp: ts.Expression = to.whenTrue; while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression; - if ( - !ts.isCallExpression(stamp) || - !ts.isIdentifier(stamp.expression) || - stamp.expression.text !== "__defProp" - ) - return false; + if (!ts.isCallExpression(stamp) || !isHelperIdent(stamp.expression, "__defProp")) return false; if (stamp.arguments.nodes.length !== 3) return false; const nameArg = stamp.arguments.nodes[1]!; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false; @@ -278,19 +342,23 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH degrade: reason, }); - const declStmts = sf.statements.nodes.filter( - (s) => - ts.isVariableStatement(s) && - s.declarationList.declarations.nodes.some( - (d) => ts.isIdentifier(d.name) && d.name.text === "__toESM", - ), - ); + // Top-level declarators of the helper, any statement shape (minified + // dists share one `var` among several helpers), by spelling. + const decls = new Map(); + for (const s of sf.statements.nodes) { + if (!ts.isVariableStatement(s)) continue; + for (const d of s.declarationList.declarations.nodes) { + if (!isHelperIdent(d.name, "__toESM")) continue; + const list = decls.get(d.name.text); + if (list === undefined) decls.set(d.name.text, [d]); + else list.push(d); + } + } const refs: ts.Identifier[] = []; const calls: ts.CallExpression[] = []; const collect = (n: ts.Node): void => { - if (ts.isIdentifier(n) && n.text === "__toESM") refs.push(n); - if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "__toESM") - calls.push(n); + if (isHelperIdent(n, "__toESM")) refs.push(n); + if (ts.isCallExpression(n) && isHelperIdent(n.expression, "__toESM")) calls.push(n); }; ts.walkPreorder(sf, collect); if (calls.length === 0 && refs.length === 0) return plan; @@ -298,15 +366,16 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH // a helper that escapes as a VALUE is outside the recognized shape. const callees = new Set(calls.map((c) => c.expression)); const declNames = new Set(); - for (const s of declStmts) { - for (const d of (s as ts.VariableStatement).declarationList.declarations.nodes) { - if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name); - } - } + for (const list of decls.values()) for (const d of list) declNames.add(d.name); if (refs.some((r) => !callees.has(r) && !declNames.has(r))) return fail(TO_ESM_ESCAPE_DEGRADE); if (calls.length === 0) return plan; // declared but never called — dead helper - if (declStmts.length !== 1 || !recognizedToEsmDecl(declStmts[0]!)) - return fail(TO_ESM_SHAPE_DEGRADE); + // Every spelling a call uses must be declared exactly once, in the + // recognized shape. + for (const call of calls) { + const list = decls.get((call.expression as ts.Identifier).text); + if (list === undefined || list.length !== 1 || !recognizedToEsmDecl(list[0]!)) + return fail(TO_ESM_SHAPE_DEGRADE); + } /** Module-scope interop bindings: name → whether `.default` IS the * module (pad the access) rather than a member read of its `default`. */ @@ -467,16 +536,20 @@ export function rewriteBundlerCjsSyntax( // ESM bundles can contain closed CommonJS function factories. Recognize // those before refusing interop that needs a dynamic module namespace. - if (ts.isExternalModule(sf)) { + // TypeScript forces every `.cjs` file to be an external module + // (isFileForcedToBeModuleByFormat), yet Node always loads one as + // CommonJS: the extension, not that flag, decides the format there. + if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) { const bundled = rewriteBundledFunctionImports(source, sf); if (bundled !== null) return bundled; - return source.includes("__toESM(") - ? { - degrade: - "its ES-module dist routes an external dependency through the __toESM " + - "bundler-interop helper, which cannot compile natively in ESM output", - } - : null; + if (!source.includes("__toESM(")) return null; + return { + degrade: source.includes("__commonJS") + ? "its ES-module bundle reaches bundled CommonJS modules through the __commonJS and " + + "__toESM bundler-interop helpers, which cannot compile natively in ESM output" + : "its ES-module dist routes an external dependency through the __toESM " + + "bundler-interop helper, which cannot compile natively in ESM output", + }; } // The __toESM interop pass (see the section header): wrapper call sites @@ -644,6 +717,11 @@ export function rewriteBundlerCjsSyntax( } const lexed = cjsLexedExportsOfFile(sf); + if (host.requireOnly?.(filePath) === true) { + for (const t of exportTables) for (const [name] of t.entries) lexed.exports.add(name); + for (const d of definePropEntries) lexed.exports.add(d.name); + for (const m of memberStmts) for (const name of m.names) lexed.exports.add(name); + } let emitTable = sawBundleShape && (lexed.exports.size > 0 || lexed.reexports.length > 0); const starSpecs: string[] = []; @@ -814,21 +892,35 @@ export function rewriteBundlerCjsSyntax( "__reExport", "__toESM", "__toCommonJS", + "__commonJS", "__exportStar", "__createBinding", "__setModuleDefault", "__importStar", "__importDefault", ]); + /* Helper declarators by name. A minified dist shares one `var` among + * several helpers (`var __getProtoOf=…,__hasOwnProp=…;`): each + * declarator is its own candidate, and the statement pads whole only + * once every declarator in it is dead. */ const helperDecls = new Map(); + const helperStmts: ts.VariableStatement[] = []; for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; const decls = stmt.declarationList.declarations.nodes; - if (decls.length !== 1) continue; - const d = decls[0]!; - if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) { - helperDecls.set(d.name.text, { start: stmt.getStart(sf), end: stmt.getEnd() }); + let anyHelper = false; + for (const d of decls) { + if (!ts.isIdentifier(d.name) || !HELPER_NAMES.has(bundlerHelperBase(d.name.text))) continue; + if (helperDecls.has(d.name.text)) continue; // redeclared: keep the first, never sweep twice + anyHelper = true; + helperDecls.set( + d.name.text, + decls.length === 1 + ? { start: stmt.getStart(sf), end: stmt.getEnd() } + : { start: d.getStart(sf), end: d.getEnd() }, + ); } + if (anyHelper && decls.length > 1) helperStmts.push(stmt); } // every identifier occurrence of a helper name outside its own // declarator name node, position-recorded for the span test @@ -862,8 +954,39 @@ export function rewriteBundlerCjsSyntax( } } } + // A swept declarator of a shared statement pads with its separating + // comma, and the statement pads whole once every declarator is dead: + // padding the bare declarator span would leave a dangling comma. + const declaratorSpans = new Set<{ start: number; end: number }>(); + const declaratorPads: { start: number; end: number }[] = []; + for (const stmt of helperStmts) { + const decls = stmt.declarationList.declarations.nodes; + const deadAt = decls.map((d) => { + if (!ts.isIdentifier(d.name) || !removed.has(d.name.text)) return false; + const span = helperDecls.get(d.name.text)!; + if (span.start !== d.getStart(sf)) return false; + declaratorSpans.add(span); + return true; + }); + if (deadAt.every((x) => x)) { + declaratorPads.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + continue; + } + // Dead declarators before the last live one take their trailing + // comma; the dead tail after it pads from that declarator's end. + const lastLive = deadAt.lastIndexOf(false); + for (let i = 0; i < decls.length; i++) { + if (!deadAt[i]) continue; + const d = decls[i]!; + declaratorPads.push( + i < lastLive + ? { start: d.getStart(sf), end: decls[i + 1]!.getStart(sf) } + : { start: decls[lastLive]!.getEnd(), end: d.getEnd() }, + ); + } + } neutralize.length = 0; - neutralize.push(...dead); + neutralize.push(...dead.filter((span) => !declaratorSpans.has(span)), ...declaratorPads); } /* ── emission ─────────────────────────────────────────────────────── */ @@ -875,6 +998,15 @@ export function rewriteBundlerCjsSyntax( if (!emitTable) return text; // interop pads only — nothing appends + if ( + esbuildMainTarget !== null && + tablePos !== null && + memberStmts.length === 0 && + definePropEntries.length === 0 && + liveExportTableFirst(sf, tablePos, esbuildMainTarget, exportTables) + ) + host.liveExportTable?.(filePath); + const lines: string[] = [""]; const hasAnyFallback = [...entries.values()].some((s) => s.kind === "any"); if (hasAnyFallback) { diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 3c8fa22e9..7dffd05f7 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -13,7 +13,13 @@ import { registerWorkspacePackage, workspacePackageOfPath, } from "./workspace-registry.js"; -import { trackedExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; +import { + clearVirtualSources, + setVirtualSource, + trackedExists, + trackedReadFile, + trackedRealpath, +} from "./input-tracker.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -28,12 +34,24 @@ const offenders = new Map(); * the host probes the same file many times, and the rewrite parses. */ const rewriteCache = new Map(); +/** Rewritten files whose tail export table stands for a live getter table + * installed before the file could run another module's code (the rewrite + * certifies it; require-cycle admission consults it). */ +const liveExportFiles = new Set(); + +export function npmStaticLiveExportTable(path: string): boolean { + return liveExportFiles.has(path.split("\\").join("/")); +} + export function setNpmStaticPackages>(packages: T): void { activePackages = new Set(packages); declarationOverloads = new Map(); declarationProperties = new Map(); offenders.clear(); + fileDegrades.clear(); rewriteCache.clear(); + liveExportFiles.clear(); + clearVirtualSources(); untypedPkgCache.clear(); realpathProbed.clear(); } @@ -81,6 +99,21 @@ export function npmStaticOffenders(): ReadonlyMap { return offenders; } +/** Files whose bundler interop the rewrite could not finish this attempt, + * with their package and reason. A file refuses its package only when + * the compiled program can load it (npmStaticLoadableFiles): a package is + * judged by what its imported subpaths reach, not by every file it ships. */ +const fileDegrades = new Map(); + +export function npmStaticFileDegrades(): ReadonlyMap { + return fileDegrades; +} + +function reportFileDegrade(path: string, pkg: string, reason: string): void { + const key = path.split("\\").join("/"); + if (!fileDegrades.has(key)) fileDegrades.set(key, { pkg, reason }); +} + /** The DefinitelyTyped name mangling ("@scope/pkg" → "scope__pkg") — the * @types twin that must hide alongside the package's own declarations. */ function mangledTypesName(name: string): string { @@ -362,7 +395,18 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow if (hit !== undefined) return hit ?? undefined; let rewritten: string | null = null; try { - const original = trackedReadFile(path); + let original = trackedReadFile(path); + // A bundle's CommonJS chunks move to their own modules first, so + // every later pass (and this shadow, for each chunk) sees plain + // CommonJS module text. + const split = original === null ? null : services.splitCjsChunks(original, path); + if (split !== null && "degrade" in split) { + reportFileDegrade(path, target.pkg, split.degrade); + original = null; + } else if (split !== null) { + for (const chunk of split.chunks) setVirtualSource(chunk.path, chunk.source); + original = split.text; + } if (original !== null) { const namepaths = services.jsDocNamepaths(path, original); const source = namepaths ?? original; @@ -385,9 +429,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow classFields?.text ?? source, path, + (live) => liveExportFiles.add(live.split("\\").join("/")), ); if (answer !== null && typeof answer === "object") { - reportNpmStaticOffender(target.pkg, answer.degrade); + reportFileDegrade(path, target.pkg, answer.degrade); } else { rewritten = answer ?? @@ -395,7 +440,8 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow propertyProjected?.text ?? findWidened?.text ?? classFields?.text ?? - namepaths; + namepaths ?? + (split !== null ? original : null); } } } catch { @@ -434,6 +480,14 @@ function looksUnminified(source: string): boolean { return source.length / lines.length <= 200; } +/** A whitespace/syntax-minified bundler dist whose interop helpers keep + * their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers + * verify those shapes and refuse the package precisely when they cannot, + * so the minification heuristic need not refuse it up front. */ +function namedBundlerHelpers(source: string): boolean { + return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source); +} + function hasTransformMarkers(source: string): boolean { return TRANSFORM_MARKERS.some((m) => source.includes(m)); } @@ -456,7 +510,8 @@ export function npmStaticIneligibleReason( if (jsEntry === null) return "no runtime JS entry resolves"; const source = trackedReadFile(jsEntry); if (source === null) return `its runtime entry ${jsEntry} cannot be read`; - if (!looksUnminified(source)) return "its shipped JS looks minified"; + if (!looksUnminified(source) && !namedBundlerHelpers(source)) + return "its shipped JS looks minified"; if (hasTransformMarkers(source)) { return "its shipped JS carries build-transform markers (bundled/transpiled dist)"; } diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 024c69d98..676b2bb3e 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -10,11 +10,13 @@ import { checkPreflight, isNodeTypesPath, locOf, + npmStaticLoadableFiles, requiresOf, resolveNpmImport, type LoadResult, } from "./program.js"; import { + npmStaticFileDegrades, npmStaticIneligibleReason, npmStaticOffenders, npmStaticPackageOfPath, @@ -458,6 +460,15 @@ function loadFrontend( const typecheckRefused = new Set(); while (effective.size > 0) { const reasons = new Map(npmStaticOffenders()); + // Bundler interop the rewrite could not finish refuses its package + // only where the compiled program can load the file. + if (npmStaticFileDegrades().size > 0) { + const loadable = npmStaticLoadableFiles(load.program, load.moduleOrder); + for (const [file, { pkg, reason }] of npmStaticFileDegrades()) { + if (!reasons.has(pkg) && (loadable === null || loadable.has(file))) + reasons.set(pkg, reason); + } + } for (const d of preflight) { const pkg = npmStaticPackageOfPath(d.loc.file); if (pkg !== null && !reasons.has(pkg)) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index d7c2ba66a..192a36ef9 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -68,6 +68,7 @@ import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, + npmStaticLiveExportTable, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticDeclarationOverloads, @@ -1438,6 +1439,8 @@ export function requireBindingEarlyUse7( k: number, decl: ts.VariableDeclaration, precedingDecls: readonly ts.VariableDeclaration[], + /** A reference of the binding whose early evaluation is known exact. */ + exactEarlyRead?: (reference: ts.Identifier) => boolean, ): ts.Identifier | null { const checker = program.getTypeChecker(); const bindings = new Set(); @@ -1669,6 +1672,7 @@ export function requireBindingEarlyUse7( const sym = symbolOf(n); if (sym === undefined) return undefined; if (bindings.has(sym)) { + if (exactEarlyRead?.(n) === true) return undefined; hit = n; return "stop"; } @@ -3119,6 +3123,16 @@ function cjsCycleRefusal7( // anything else can change what a module of the cycle observes. let stray: ts.Node | null = null; const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement)); + // A bundler export table the rewrite certified (npmStaticLiveExportTable) + // stands for the live getter object the original installed before its + // first require: the tail table settles as if it ran there. + const tail = stmts[stmts.length - 1]; + if ( + tail !== undefined && + npmStaticLiveExportTable(m.fileName) && + cjsExportAssignmentOf(tail)?.kind === "table" + ) + settled.add(tail); ts.walkPreorder(m, (n) => { if (stray !== null) return "stop"; if (ts.isStatement(n) && settled.has(n)) return "skip"; @@ -3171,7 +3185,9 @@ function cjsCycleRefusal7( if (req.decl === null) continue; const dep = resolveImport7(program, m, req.spec, "require"); if (dep === null || !members.has(dep)) continue; - const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, []); + const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, [], (id) => + functionExportRead7(id, dep), + ); if (reached !== null) { return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`; } @@ -3181,6 +3197,161 @@ function cjsCycleRefusal7( return null; } +/** True when `use` initializes a top-level const whose every other + * reference is a value of an export statement (`module.exports = { … }` + * or `exports.n = …`). */ +function publishedAlias7(use: ts.Node, decl: ts.Node | undefined): boolean { + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + decl.initializer !== use || + !ts.isIdentifier(decl.name) + ) + return false; + const list = decl.parent; + const stmt = list?.parent; + const sf = stmt?.parent; + if ( + list === undefined || + (list.flags & ts.NodeFlags.Const) === 0 || + stmt === undefined || + !ts.isVariableStatement(stmt) || + sf === undefined || + !ts.isSourceFile(sf) + ) + return false; + const alias = decl.name; + const published = new Set(); + for (const st of sf.statements.nodes) { + const cjs = cjsExportAssignmentOf(st); + if (cjs === null) continue; + if (cjs.kind === "member") published.add(cjs.value); + else if (cjs.obj !== null) { + for (const p of cjs.obj.properties.nodes) { + if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) + published.add(p.initializer); + else if (ts.isShorthandPropertyAssignment(p)) published.add(p.name); + } + } + } + let kept = false; + ts.walkPreorder(sf, (n) => { + if (kept) return "stop"; + if (!ts.isIdentifier(n) || n === alias || n.text !== alias.text) return undefined; + const p = n.parent; + if ( + p !== undefined && + ((ts.isPropertyAccessExpression(p) && p.name === n) || + (ts.isPropertyAssignment(p) && p.name === n)) + ) + return undefined; + if (!published.has(n)) kept = true; + return undefined; + }); + return !kept; +} + +/** True for a plain read `binding.name` (not a call, not a write, not a + * value that is stored or passed on) of the cycle member `dep` whose + * export `name` is a top-level function declaration nothing reassigns. + * Every member settles its exports before entering the cycle, so Node + * reads that same function whenever the read runs, and the compiled read + * resolves to the declaration itself. */ +function functionExportRead7(id: ts.Identifier, dep: ts.SourceFile): boolean { + const access = id.parent; + if (!ts.isPropertyAccessExpression(access) || access.expression !== id) return false; + // The value the read yields: through parentheses and the right operand + // of a comma, so `(0, binding.name)(...)` is the call it spells. + let use: ts.Node = access; + for (;;) { + const p = use.parent; + if (ts.isParenthesizedExpression(p)) use = p; + else if ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.CommaToken && + p.right === use + ) + use = p; + else break; + } + const parent = use.parent; + // Only a use that neither runs the function nor keeps it is a read: a + // stored alias (`const f = binding.name`, `{ f: binding.name }`) or an + // argument can be called while the cycle initializes. A top-level const + // that only publishes it (`module.exports = { name: f }`, the static + // rewrite's re-export spelling) runs nothing: a module that reads the + // export back checks that read itself. + const observes = + publishedAlias7(use, parent) || + ts.isExpressionStatement(parent) || + ts.isTypeOfExpression(parent) || + ts.isVoidExpression(parent) || + (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) || + (ts.isBinaryExpression(parent) && + (parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken)) || + ((ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) && + parent.expression === use) || + (ts.isConditionalExpression(parent) && parent.condition === use); + if (!observes) return false; + const name = access.name.text; + // The exported value's identifier: a table entry or a member export. + const values: ts.Expression[] = []; + for (const stmt of dep.statements.nodes) { + if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; + const e = stmt.expression; + if (e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue; + if (isModuleExportsAccess(e.left)) { + if (!ts.isObjectLiteralExpression(e.right)) return false; + for (const p of e.right.properties.nodes) { + if (ts.isSpreadAssignment(p)) return false; + if (ts.isShorthandPropertyAssignment(p) && p.name.text === name) + values.push(p.name as ts.Identifier); + else if ( + ts.isPropertyAssignment(p) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && + p.name.text === name + ) + values.push(p.initializer); + } + } else if ( + ts.isPropertyAccessExpression(e.left) && + e.left.name.text === name && + ((ts.isIdentifier(e.left.expression) && e.left.expression.text === "exports") || + isModuleExportsAccess(e.left.expression)) + ) { + values.push(e.right); + } + } + const value = values.length === 1 ? values[0]! : null; + if (value === null || !ts.isIdentifier(value)) return false; + const fnName = value.text; + if ( + !dep.statements.nodes.some( + (st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined, + ) + ) + return false; + let rebound = false; + ts.walkPreorder(dep, (n) => { + if (rebound) return "stop"; + if ( + (ts.isBinaryExpression(n) && + ts.isIdentifier(n.left) && + n.left.text === fnName && + n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) || + (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === fnName) || + (ts.isFunctionDeclaration(n) && n.name?.text === fnName && !ts.isSourceFile(n.parent)) + ) + rebound = true; + return undefined; + }); + return !rebound; +} + /* ── exact cyclic initialization ───────────────────────────────────────── * Cycles that fail the declaration-only rule still evaluate exactly like * Node when their shape is representable: the guarded %init calls already @@ -5930,6 +6101,58 @@ export function orderedImportsOf( return out; } +/** The files a compiled program can load: the module order closed over + * every literal require() and import() call it contains (deferred and + * nested ones included) and, for opted-in package JavaScript, over every + * import and export declaration as well. A package file outside this set + * is reachable only through types, so whatever it spells cannot affect + * the compiled program. Null when a reachable file loads a computed + * specifier: then any file may load. */ +export function npmStaticLoadableFiles( + program: ts.Program, + moduleOrder: readonly ts.SourceFile[], +): Set | null { + const seen = new Set(); + const work: ts.SourceFile[] = []; + const add = (sf: ts.SourceFile | null | undefined): void => { + if (sf === null || sf === undefined || seen.has(sf) || sf.isDeclarationFile) return; + seen.add(sf); + work.push(sf); + }; + for (const sf of moduleOrder) add(sf); + let computed = false; + while (work.length > 0 && !computed) { + const sf = work.pop()!; + const packageJs = + npmStaticPackageOfPath(sf.fileName) !== null && isJsSourceFileName(sf.fileName); + const follow = (spec: string, kind: "import" | "require"): void => { + if (canonicalBuiltinModule(spec) !== null) return; + add(resolveImport7(program, sf, spec, kind) ?? npmStaticDepSf7(program, sf, spec, kind)); + }; + ts.walkPreorder(sf, (n) => { + if (computed) return "stop"; + if (ts.isCallExpression(n)) { + const isImport = n.expression.kind === ts.SyntaxKind.ImportKeyword; + const isRequire = ts.isIdentifier(n.expression) && n.expression.text === "require"; + if (!isImport && !isRequire) return undefined; + const arg = n.arguments.nodes[0]; + if (arg !== undefined && ts.isStringLiteralLike(arg)) + follow(arg.text, isImport ? "import" : "require"); + else if (packageJs || isImport) computed = true; + } else if ( + packageJs && + (ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) && + n.moduleSpecifier !== undefined && + ts.isStringLiteral(n.moduleSpecifier) + ) { + follow(n.moduleSpecifier.text, "import"); + } + return undefined; + }); + } + return computed ? null : new Set([...seen].map((sf) => sf.fileName.split("\\").join("/"))); +} + /** The program source file a bare specifier reaches when it names an * opted-in --npm-static package (the shipped-JS entry preflight admitted * as a module edge), else null. No offender reporting here — preflight @@ -5967,10 +6190,15 @@ export function isModuleExportsAccess(expr: ts.Expression): expr is ts.PropertyA * - member: `exports.f = ` / `module.exports.f = `. * Null for everything else. Callers gate on top-level position in a JS * module file — these forms anywhere else keep their generic fences. */ -export function cjsExportAssignmentOf( - stmt: ts.Statement, -): - | { kind: "table"; obj: ts.ObjectLiteralExpression | null; expr: ts.BinaryExpression } +export function cjsExportAssignmentOf(stmt: ts.Statement): + | { + kind: "table"; + obj: ts.ObjectLiteralExpression | null; + expr: ts.BinaryExpression; + /** `exports = module.exports = value`: the `exports` binding names + * the new value too, so later `exports.f =` members attach to it. */ + rebindsExports?: true; + } | { kind: "member"; name: ts.MemberName; value: ts.Expression; expr: ts.BinaryExpression } | null { if (!ts.isExpressionStatement(stmt)) return null; @@ -5979,6 +6207,20 @@ export function cjsExportAssignmentOf( if (isModuleExportsAccess(e.left)) { return { kind: "table", obj: ts.isObjectLiteralExpression(e.right) ? e.right : null, expr: e }; } + if (ts.isIdentifier(e.left) && e.left.text === "exports") { + let inner: ts.Expression = e.right; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + if ( + ts.isBinaryExpression(inner) && + inner.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isModuleExportsAccess(inner.left) && + // `exports = module.exports = {}` is the fresh-table reset, which the + // export walk handles on its own. + !ts.isObjectLiteralExpression(inner.right) + ) { + return { kind: "table", obj: null, expr: inner, rebindsExports: true }; + } + } if (ts.isPropertyAccessExpression(e.left) && !e.left.questionDotToken) { const recv = e.left.expression; const isExports = ts.isIdentifier(recv) && recv.text === "exports"; @@ -6110,6 +6352,8 @@ export function cjsExportDiscardReason(stmt: ts.Statement): string | null { return "export members attached before a 'module.exports =' assignment (Node replaces the export object — importers never see this member; attach it to module.exports AFTER the table)"; } const left = mine.expr.left as ts.PropertyAccessExpression; + const last = cjsExportAssignmentOf(stmts[lastTable]!); + if (last?.kind === "table" && last.rebindsExports === true) return null; return ts.isIdentifier(left.expression) && left.expression.text === "exports" ? "'exports.f =' after a 'module.exports =' assignment ('exports' still references the replaced object — Node discards this member; write module.exports.f instead)" : null; diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index ca4dd2b36..c6bb24ae5 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -2,6 +2,11 @@ import { Ts7SourceParser, type Ts7SourceKind } from "./ts7/source-parser.js"; import { Ts7Host, type Ts7ApiFactory, type Ts7HostOptions } from "./ts7/program-host.js"; import { rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js"; import { isBundlerCjsCandidate } from "./npm-static-rewrite-syntax.js"; +import { + mayBundleChunks, + splitBundledChunks, + type BundledChunkSplit, +} from "./npm-static-bundled-chunks.js"; import * as syntax from "./npm-static-declaration-syntax.js"; import type { SourceFile } from "./ts7/ast-types.js"; @@ -55,10 +60,21 @@ export class FrontendServices { this.ensureOpen(); return new Ts7Host(this.createApi, { ...options, cwd: options?.cwd ?? this.cwd }); } - rewriteCjs(source: string, path: string): string | { degrade: string } | null { + rewriteCjs( + source: string, + path: string, + liveExportTable?: (path: string) => void, + ): string | { degrade: string } | null { this.ensureOpen(); if (!isBundlerCjsCandidate(source)) return null; - return rewriteBundlerCjsWithParser(this.sourceParser(), source, path); + return rewriteBundlerCjsWithParser(this.sourceParser(), source, path, liveExportTable); + } + /** A CommonJS bundle's __commonJS chunks as their own modules (null: no + * chunks; a degrade reason: chunks the recognizer cannot move). */ + splitCjsChunks(source: string, path: string): BundledChunkSplit | { degrade: string } | null { + this.ensureOpen(); + if (!mayBundleChunks(source)) return null; + return splitBundledChunks(this.parse(path, source, "js"), path); } nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f2440a521..9282aae19 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -6,6 +6,7 @@ import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../ import { BYTES_ELEMENT_NAME, arrayOf, + isDynTypedRefType, BOOL, bytesOf, canConvertToDyn, @@ -125,6 +126,14 @@ export function jsOpenObjectType( : type; } +/** The modules whose RequestOptions interface maps to the checked dynamic. */ +const HTTP_REQUEST_OPTION_MODULES: ReadonlySet = new Set([ + "http", + "node:http", + "https", + "node:https", +]); + /** The WebCrypto interfaces whose values are checked-dynamic: the crypto * global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries * (scr_webcrypto.c). */ @@ -325,7 +334,7 @@ export class ShapeRegistry { ) { return; } - shape.declaredOrder = declaredOrder; + shape.declaredOrder = this.withExtendedNames(shape.id, declaredOrder); this.declaredOrderPriority.set(shape.id, this.currentDeclaredOrderPriority); } @@ -341,7 +350,7 @@ export class ShapeRegistry { } const shape = this.byId.get(shapeId); if (!shape) continue; - shape.declaredOrder = best.order; + shape.declaredOrder = this.withExtendedNames(shapeId, best.order); this.declaredOrderPriority.set(shapeId, best.priority); } for (const finalize of this.derivedDeclaredOrderFinalizers) finalize(); @@ -374,7 +383,7 @@ export class ShapeRegistry { // still obeys first-writer-wins. if (shape?.declaredOrder !== originalOrder) return; const next = order(); - if (next !== undefined) shape.declaredOrder = next; + if (next !== undefined) shape.declaredOrder = this.withExtendedNames(shape.id, next); }; } @@ -482,6 +491,53 @@ export class ShapeRegistry { get(shapeId: string): IrRecordShape | undefined { return this.byId.get(shapeId); } + + /** Names extendShape added to a shape, which every key-order table the + * shape adopts lists after its declared names. */ + private readonly extendedNames = new Map(); + + /** The members extendShape attached to a shape (empty for most). */ + attachedNames(shapeId: string): readonly string[] { + return this.extendedNames.get(shapeId) ?? []; + } + + private withExtendedNames(shapeId: string, order: string[]): string[] { + const extra = this.extendedNames.get(shapeId); + if (!extra) return order; + const missing = extra.filter((name) => !order.includes(name)); + return missing.length === 0 ? order : [...order, ...missing]; + } + + /** Adds optional fields to an interned shape in place: members a type + * assertion attaches to the shape's values (`r as R & { tag: string }` + * followed by `view.tag = ...`; see assertion-members.ts). Every value + * of the shape holds them absent until a write through such a view, so + * the shape keeps its id, both structural keys resolve to it, and its + * key-order table lists the new names after the declared ones. Runs + * before any body lowers; existing layouts change, so the revision + * advances like a closing recursive placeholder. */ + extendShape(shapeId: string, extra: readonly { name: string; type: IrType }[]): void { + const shape = this.byId.get(shapeId); + if (!shape || shape.tuple || extra.length === 0) return; + const fresh = extra.filter((f) => !shape.fields.some((x) => x.name === f.name)); + if (fresh.length === 0) return; + shape.fields = [...shape.fields, ...fresh].sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0, + ); + this.extendedNames.set(shapeId, [ + ...(this.extendedNames.get(shapeId) ?? []), + ...fresh.map((f) => f.name), + ]); + if (shape.declaredOrder) + shape.declaredOrder = this.withExtendedNames(shapeId, shape.declaredOrder); + const candidates = this.declaredOrderCandidates.get(shapeId); + if (candidates) { + for (const c of candidates) c.order = this.withExtendedNames(shapeId, c.order); + } + this.revision++; + const key = this.keyOf(shape.fields, false, shape.indexValue); + if (!this.byKey.has(key)) this.byKey.set(key, shapeId); + } } /** The frontend's union interner — mirrors ShapeRegistry. A union's @@ -1543,6 +1599,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { contextResolutions++; return DYN; } + // Readonly over a program class (Object.freeze's result type) is the + // same instance. + const readonlyClass = readonlyClassType(type, ctx); + if (readonlyClass !== null) return readonlyClass; if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) { const bound = resolveTypeParam(type); if (bound) { @@ -2038,12 +2098,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // {}` — classNamer spells them "%anon" (unique per file), the same // name their collection registered. !classDecl.getSourceFile().isDeclarationFile && - checker.getConstructSignatures(widened).length === 0 + checker.getConstructSignatures(widened).length === 0 && + // A TypeScript class declared OUTSIDE the lowered program (reached + // through `import type` alone) never registers, and the program can + // hold no instance of it: its instance type maps below as the + // structural type it describes, like an interface. + (ctx.isProgramFile(classDecl.getSourceFile()) || isJsSourceFile(classDecl.getSourceFile())) ) { - // Declared OUTSIDE the lowered program (reached through the type world - // alone — jsdoc `typeof import()` over a never-required module): the - // class will never register, so the instance type is unmappable, not - // an object type naming a struct that cannot exist. + // A JavaScript class outside the lowered program (jsdoc `typeof + // import()` over a never-required module) stays unmappable: JSDoc + // types are not checked, so its slots fall back to the dynamic value. if (!ctx.isProgramFile(classDecl.getSourceFile())) return null; // The class inside a MIXIN function: per-call-site instantiations // share this one node — the CURRENT instantiation (collecting or @@ -2447,6 +2511,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // spawn consumes a presence-sensitive option bag, including optional // stdio and env values. Keep it checked-dynamic for native normalization. if (isStdlibInterface("SpawnOptions")) return DYN; + // http.RequestOptions / https.RequestOptions as a declared type: an + // option record whose members include functions, agents and signals. + // Values ride checked-dynamic storage, like SpawnOptions. + if ( + psym?.name === "RequestOptions" && + psym.declarations.some((d) => { + if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(d.getSourceFile())) return false; + for (let p: ts.Node | undefined = d.parent; p !== undefined; p = p.parent) { + if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) + return HTTP_REQUEST_OPTION_MODULES.has(p.name.text); + } + return false; + }) + ) + return DYN; // exactly like npm-declared types. Static fetch has one deliberately // narrower native representation: Response is an opaque checked- @@ -4320,10 +4399,13 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // params). Declaration files keep their existing paths: library // callback slots such as String#replace's `(substring, ...args: any[])` // have intrinsic lowerings that inspect the unmapped type. + // node:test's default mock function type, `(...args: any[]) => + // undefined` (the type parameter default of mock.fn), takes the same + // ABI: mock.fn() without an original records whatever its callers pass. if ( rest && decl !== undefined && - !decl.getSourceFile().isDeclarationFile && + (!decl.getSourceFile().isDeclarationFile || isNodeTestDefaultFunctionParam(decl, ctx)) && (pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker))) ) { dynRest = true; @@ -5055,6 +5137,19 @@ function mentionsMappedParams( return false; } +function readonlyClassType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const alias = type.getAliasSymbol(); + if (alias?.name !== "Readonly") return null; + const args = type.getAliasTypeArguments(); + if (args.length !== 1 || !args[0]) return null; + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); + if (!isLibAlias) return null; + const mapped = mapType(args[0], ctx); + return mapped !== null && isDynTypedRefType(mapped) ? mapped : null; +} + function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam, shapes, unions } = ctx; if (!resolveTypeParam) return null; @@ -6516,6 +6611,23 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { } } +/** A parameter of a function type that is the default of a type parameter + * declared in node:test's standard-library declarations (MockTracker.fn's + * `F extends Function = (...args: any[]) => undefined`). */ +function isNodeTestDefaultFunctionParam(decl: ts.Declaration, ctx: TypeMapperCtx): boolean { + const fnType = decl.parent; + return ( + ts.isParameter(decl) && + fnType !== undefined && + ts.isFunctionTypeNode(fnType) && + fnType.parent !== undefined && + ts.isTypeParameterDeclaration(fnType.parent) && + fnType.parent.defaultType === fnType && + ctx.isStdlibFile(decl.getSourceFile()) && + isDeclaredInAmbientModule(decl, "test") + ); +} + /** `any[]` (or `readonly any[]`): an array type whose element is `any`. */ export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean { if (!checker.isArrayType(t)) return false; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 4d4193f73..6757851d9 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -346,6 +346,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "process.kill", "process.killNum", "process.setExitCode", + "process.setExitCodeTracked", // Node's argument grammar throws ERR_INVALID_ARG_TYPE at the call. "process.emitWarning", // cpuUsage(prev)'s field validation: negative/non-finite prev fields @@ -448,6 +449,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "https.requestFn", "https.requestFnCb", "rl.question", + "rl.questionDyn", "rl.checkOptions", "rl.checkCompleter", "readable.fromArr", @@ -470,6 +472,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "rl.resume", "rl.prompt", "rl.promptWith", + "rl.setPromptDyn", + "rl.getPrompt", + "rl.cursorPos", + "rl.inputFromDyn", + "rl.outputFromDyn", "rl.write", "rl.writeKey", "rl.cursor", @@ -591,6 +598,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.mapSeedEntry", "dyn.freeze", "dyn.isFrozen", + "class.defineField", + "class.throwReadOnly", + "class.throwNotExtensible", "dyn.toString", "dyn.valueOf", "string.fromCodePoint", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 697a185a5..2cbb35185 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -206,6 +206,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.in": { argTypes: [DYN, STRING], result: BOOL }, "dyn.freeze": { argTypes: [DYN], result: DYN }, "dyn.isFrozen": { argTypes: [DYN], result: BOOL }, + /** Object.defineProperty on a declared class field (scr_json.c): the + * field's current value (borrowed), its FIELD_* attribute word, the + * instance's CLASS_* integrity word, the field name and the descriptor. + * Validates as Node does (TypeError "Cannot redefine property: f") and + * answers a one-property holder of the defined value and attributes (+1), + * read back with class.definedBits and class.definedValue. */ + "class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN }, + "class.definedBits": { argTypes: [DYN, STRING], result: F64 }, + "class.definedValue": { argTypes: [DYN, STRING], result: DYN }, + /** Whether a property descriptor has a `value` member. */ + "class.descriptorHasValue": { argTypes: [DYN], result: BOOL }, + /** Throws Node's TypeError for a write to a read-only class field: the + * field name and the instance's class name. */ + "class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID }, + /** Throws Node's TypeError for a class field initializer on a + * non-extensible instance: the field name. */ + "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, @@ -2391,6 +2408,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** Rest listeners use the checked function bridge to pack event arguments. */ "child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, "process.connected": { argTypes: [], result: BOOL }, + /** Whether this fork child opened its channel; part of process.send's + * presence in programs with fork targets. Never throws. */ + "process.ipcOpen": { argTypes: [], result: BOOL }, "process.send": { argTypes: [STRING], result: BOOL }, "process.sendCb": { argTypes: [STRING, null], result: BOOL }, "process.disconnect": { argTypes: [], result: VOID }, @@ -3025,8 +3045,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "process.exit": { argTypes: [F64], result: VOID }, /** Numeric process.exitCode write (integer validation, implicit exit status). */ "process.setExitCode": { argTypes: [F64], result: VOID }, + /** The process.exitCode write of a program that also reads the + * property: the same validation, and the code counts as assigned. */ + "process.setExitCodeTracked": { argTypes: [F64], result: VOID }, /** The code for process.exit() with no argument, or zero when unset. */ "process.currentExitCode": { argTypes: [], result: F64 }, + /** Whether process.exitCode reads a code rather than undefined. Never throws. */ + "process.hasExitCode": { argTypes: [], result: BOOL }, "process.cwd": { argTypes: [], result: STRING }, /** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both * always answer (the checker's `getuid?` optionality covers Windows — @@ -3231,6 +3256,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** SameValue over checked values, including NaN, signed zero and reference identity. */ "dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL }, "error.stack": { argTypes: [null], result: STRING }, + "error.setStack": { argTypes: [null, STRING], result: VOID }, "error.stackLimitGet": { argTypes: [], result: F64 }, "error.stackLimitSet": { argTypes: [F64], result: VOID }, // Receiver (any error-hierarchy object) and the program-dependent @@ -3499,6 +3525,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * throws err when unhandled. count/countFn/names/listeners/getMax/ * setMax/setDefaultMax/getDefaultMax are the introspection surface. */ "emitter.new": { argTypes: [], result: VOID }, + /** `x.constructor.name` of an emitter or stream (a borrowed object of + * EventEmitter's hierarchy): the +1 class name the instance records. */ + "emitter.className": { argTypes: [null], result: STRING }, "emitter.ctor": { argTypes: [null], result: VOID }, "emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, "emitter.off": { argTypes: [null, STRING, null], result: VOID }, @@ -4016,6 +4045,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [F64, STRING, DYN], result: { kind: "promise", inner: STRING }, }, + /** The question forms with a checked-dynamic query (a JavaScript value + * that need not be a string). */ + "rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID }, + "rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignalDyn": { + argTypes: [F64, DYN, DYN], + result: { kind: "promise", inner: STRING }, + }, "rl.close": { argTypes: [F64], result: VOID }, "rl.pause": { argTypes: [F64], result: VOID }, "rl.resume": { argTypes: [F64], result: VOID }, @@ -4023,6 +4060,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** prompt(preserveCursor): [handle, preserveCursor] */ "rl.promptWith": { argTypes: [F64, BOOL], result: VOID }, "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, + "rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID }, "rl.getPrompt": { argTypes: [F64], result: STRING }, "rl.write": { argTypes: [F64, STRING], result: VOID }, /** write(data, key): [handle, hasData, data, hasKey, hasName, name, @@ -4055,6 +4093,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID }, "rl.line": { argTypes: [F64], result: STRING }, "rl.isTerminal": { argTypes: [F64], result: BOOL }, + "rl.isPromises": { argTypes: [F64], result: BOOL }, + /** createInterface's input or output as a checked-dynamic value: + * [value, output] — the stream; refused when it is not one. */ + "rl.inputFromDyn": { argTypes: [DYN, BOOL], result: { kind: "object", className: "%Readable" } }, + "rl.outputFromDyn": { + argTypes: [DYN, BOOL], + result: { kind: "object", className: "%Writable" }, + }, "rl.cursor": { argTypes: [F64], result: F64 }, /** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq * [kind (0-3), the first and second numbers, whether the second is diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index b84c0594e..d6ff0fe72 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1932,8 +1932,23 @@ export type IrStmt = | { kind: "return"; value: IrExpr | null; awaited?: true; loc: SrcLoc } /** Field write `obj.f = v` — statement-only, like `assign`/`arraySet`. * Evaluation order: obj, then value. The old value is released; ownership - * of a refcounted new value MOVES into the object. */ - | { kind: "fieldSet"; obj: IrExpr; className: string; field: string; value: IrExpr; loc: SrcLoc } + * of a refcounted new value MOVES into the object. `sloppy` marks a write + * in sloppy-mode code, which skips a read-only field instead of throwing. + * `define` marks a class field initializer, which defines the property + * (a non-extensible instance rejects it). `attributesChecked` marks a + * write that already honors the instance's property attributes + * (CLASS_ATTRIBUTES). */ + | { + kind: "fieldSet"; + obj: IrExpr; + className: string; + field: string; + value: IrExpr; + sloppy?: true; + define?: true; + attributesChecked?: true; + loc: SrcLoc; + } /** Record field write `r.f = v` — mirrors `fieldSet` exactly (evaluation * order obj then value; old value released; refcounted new value moved * in), with a shape id in place of a class name. */ @@ -3891,6 +3906,9 @@ function isJsonSafeAt( const shape = getRecord(t.shapeId); if (!shape) return false; if (recordTextCodecClass(shape) !== null) return false; + // Accessor slots are not function-valued properties: Node's + // JSON.stringify invokes the getter (see accessorSlotProp). + if (shapeHasAccessorSlots(shape)) return false; // The recursive knot: answer true and let the rest of the graph // decide (any unsafe constituent ends the traversal immediately). if (visiting.has(t.shapeId)) return true; @@ -4075,6 +4093,27 @@ export function isClassOwnEnumerableFieldName(name: string): boolean { /** Compiler-owned storage for properties added through untyped references. */ export const DYN_CLASS_PROPERTIES = "%dynProperties"; +/** Compiler-owned integrity state of a class instance whose hierarchy is + * passed to Object.freeze, Object.seal, Object.preventExtensions or + * Object.defineProperty: an f64 holding CLASS_NOT_EXTENSIBLE, CLASS_SEALED + * and CLASS_FROZEN bits. Other hierarchies have no such field. */ +export const CLASS_ATTRIBUTES = "%attributes"; +export const CLASS_NOT_EXTENSIBLE = 1; +export const CLASS_SEALED = 2; +export const CLASS_FROZEN = 4; + +/** Compiler-owned attributes of one declared field that Object.defineProperty + * changes: an f64 holding FIELD_NOT_WRITABLE, FIELD_NOT_ENUMERABLE and + * FIELD_NOT_CONFIGURABLE bits (zero is an ordinary data property). */ +export const CLASS_FIELD_ATTRIBUTES_PREFIX = "%attr:"; +export const FIELD_NOT_WRITABLE = 1; +export const FIELD_NOT_ENUMERABLE = 2; +export const FIELD_NOT_CONFIGURABLE = 4; + +export function classFieldAttributesName(field: string): string { + return CLASS_FIELD_ATTRIBUTES_PREFIX + field; +} + /** A class capsule can always preserve its exact native identity. Its * optional property view additionally needs converters in both directions; * fields such as Maps may remain opaque without preventing the round trip. */ @@ -4228,6 +4267,9 @@ function canBoxDynCompositeAt( const shape = getRecord(t.shapeId); if (!shape) return false; if (recordTextCodecClass(shape) !== null) return false; + // Accessor slots hold the getter and setter closures, not property + // values (see accessorSlotProp). + if (shapeHasAccessorSlots(shape)) return false; // Recursive shapes answer coinductively, like isJsonSafeType. if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9bab3e79f..4d59a6241 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,59 +9,6 @@ import { } from "./ir.js"; import { everyModuleNode } from "./traverse.js"; -export interface RuntimeFeatures { - regex: boolean; - copying: boolean; - legacyTextDecoder: boolean; - fileHandle: boolean; - fetch: boolean; - eventTarget: boolean; - processEvents: boolean; - /** process.emitWarning is assigned (process.setEmitWarning). Declared - * in the literal's order: JSON.stringify of the features record must - * match between the Node-run and self-hosted compilers. */ - warningReplace: boolean; - /** stdin setEncoding / string data listeners / stdio 'resize' events. */ - stdioExtended: boolean; - emitter: boolean; - stream: boolean; - zlib: boolean; - dc: boolean; - assert: boolean; - dynInvoke: boolean; - dynAsync: boolean; - /** The program has dynamic-tier jsval operations (js.* builtins, the - * jsval pass): links the scr_js_* units (two-tier design I3). */ - dynTier: boolean; - /** The program enters AsyncLocalStorage stores (store.run, enterWith, - * a diagnostics_channel store binding) or reads the test runner's - * context (getTestContext): its timer, immediate, tick and - * queueMicrotask scheduling goes through the runtime's *_als entry - * points, which carry the active stores into each callback. */ - asyncContext: boolean; - inspect: boolean; - childProcess: boolean; - net: boolean; - symbol: boolean; - bigint: boolean; - searchParams: boolean; - qs: boolean; - parseArgs: boolean; - fsWatch: boolean; - nodeTest: boolean; - /** The program calls mock.timers members: its timer calls go through - * scr_test.c's mock-aware entry points. */ - mockTimers: boolean; - dgram: boolean; - http: boolean; - http2: boolean; - tls: boolean; - tlsCa: boolean; - cipher: boolean; - /** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */ - webcrypto: boolean; -} - const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ // DEP0040 rides the dyn-async unit's warning machinery. "punycode.warn", @@ -204,6 +151,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "fileHandle.createWriteStream", "fs.createReadStream", "fs.createWriteStream", + "rl.inputFromDyn", + "rl.outputFromDyn", "sp.finished", "sp.pipeline", "sp.pipelineStages", @@ -262,6 +211,7 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([ "process.connected", "process.disconnect", "process.forkTarget", + "process.ipcOpen", "process.onDisconnect", "process.onMessage", "process.send", @@ -311,9 +261,6 @@ const COPYING_BYTES_METHODS: ReadonlySet = new Set([ "with", ]); -const startsWithAny = (fn: string, prefixes: readonly string[]): boolean => - prefixes.some((prefix) => fn.startsWith(prefix)); - /** Inspect types and expressions together in one traversal. This is a fresh * snapshot: callers may mutate an IR module between compilation passes. */ /** The checked-dynamic undefined value (dynUndefinedExpr). */ @@ -393,60 +340,366 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean } } +/** The libCall fns served by the OPTIONAL events unit (scr_events.c): + * process signal/exit listeners and the piped-stdin surface. */ +const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ + "process.stdio", + "process.onSignal", + "process.offSignal", + "process.onExit", + "process.offExit", + "stdin.onData", + "stdin.onEnd", + "stdin.onError", + "stdin.nextChunk", + // node:readline rides the stdin unit (scr_readline.c links beside + // scr_events.c under the same gate). + "rl.create", + "rl.createSignal", + "rl.question", + "rl.questionAsync", + "rl.questionAsyncSignal", + "rl.questionDyn", + "rl.questionAsyncDyn", + "rl.questionAsyncSignalDyn", + "rl.close", + "rl.pause", + "rl.resume", + "rl.prompt", + "rl.promptWith", + "rl.setPrompt", + "rl.setPromptDyn", + "rl.getPrompt", + "rl.write", + "rl.writeKey", + "rl.on", + "rl.iterOpen", + "rl.iterNext", + "rl.iterReturn", + "rl.checkOptions", + "rl.checkCompleter", + "rl.setOptions", + "rl.setHistory", + "rl.history", + "rl.numProp", + "rl.terminal", + "rl.line", + "rl.isTerminal", + "rl.isPromises", + "rl.cursor", + "rl.seq", + "rl.moves", + "rl.cursorPos", + "rl.keypressStdin", + "rl.keypressStream", + "stdin.onKeypress", + "rl.uiOpen", + "rl.uiOpenStream", + "rl.uiCursorTo", + "rl.uiMoveCursor", + "rl.uiClearLine", + "rl.uiClearScreenDown", + "rl.uiRollback", + "rl.uiCommit", +]); + +/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the + * allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so + * divergence-57 binaries keep their exact link line. */ +const HTTP2_LEGACY_FNS: ReadonlySet = new Set([ + "http2.streamNoop", + "http2.streamUndefCall", +]); + /** Names that reach the extended process-stream surface (the * SCR_STDIO_EXTENDED events unit): stdin.setEncoding and 'resize'. */ const STDIO_EXTENDED_NAMES: ReadonlySet = new Set(["setEncoding", "resize"]); -function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures { - const features: RuntimeFeatures = { - regex: false, - copying: false, - legacyTextDecoder: false, - fileHandle: false, - fetch: false, - // Subclasses of the runtime EventTarget/Event bases tear down and box - // through the EventTarget unit even when no event.* libCall survives. - eventTarget: (mod.classes ?? []).some( - (c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS, - ), - processEvents: false, - warningReplace: false, - stdioExtended: false, - emitter: - mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), - stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), - zlib: false, - dc: false, - assert: false, - dynInvoke: mod.workers === true, - dynAsync: +/** The libCall nodes the rules below test. */ +type LibCallNode = IrExpr & { kind: "libCall" }; + +/** What makes a program need one optional runtime unit. The feature is set + * when any condition holds: `initial` from the module's declarations, a + * libCall named in `fns` or starting with one of `prefixes`, a libCall + * `libCall` accepts, another expression node `expr` accepts, or a type + * slot of one of the `types` kinds. A libCall or type that needs several + * units appears in each unit's rule. */ +interface RuntimeFeatureRule { + initial?: (mod: IrModule) => boolean; + fns?: readonly IrLibFn[]; + prefixes?: readonly string[]; + libCall?: (node: LibCallNode) => boolean; + expr?: (node: IrExpr, tables: () => TypeTables) => boolean; + types?: readonly IrType["kind"][]; +} + +/** Gives every rule in the table one type. */ +const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r; + +/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */ +const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."]; + +/** One rule per runtime feature, sorted by name. A new feature is an + * entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan + * and its early exit derive from this table. */ +const RUNTIME_FEATURE_RULES = { + assert: rule({ prefixes: ["assert."] }), + /** The program enters AsyncLocalStorage stores (store.run, enterWith, + * a diagnostics_channel store binding) or reads the test runner's + * context (getTestContext): its timer, immediate, tick and + * queueMicrotask scheduling goes through the runtime's *_als entry + * points, which carry the active stores into each callback. */ + asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }), + bigint: rule({ fns: ["process.hrtimeValue"], prefixes: ["bigint."], types: ["bigint"] }), + childProcess: rule({ + fns: [...CHILD_PROCESS_LIB_FNS], + prefixes: CHILD_PROCESS_LIB_PREFIXES, + types: ["child", "childStream", "childWriter", "spawnRes"], + }), + /** 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"] }), + copying: rule({ + expr: (node, _tables) => + (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || + (node.kind === "bytesIntrinsic" && COPYING_BYTES_METHODS.has(node.method)), + }), + dc: rule({ prefixes: ["dc."] }), + /** dns.lookup lives in the dgram unit. */ + dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }), + dynAsync: rule({ + initial: (mod) => mod.workers === true || mod.functions.some( (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn", ), - dynTier: false, + // The legacy url functions and crypto.createCipheriv emit their + // deprecation warnings through the dyn-async unit. + fns: [ + ...DYN_ASYNC_LIB_FNS, + ...READABLE_DYN_ASYNC_LIB_FNS, + ...LEGACY_URL_LIB_FNS, + "crypto.cipherNew", + ], + prefixes: READABLE_DYN_ASYNC_PREFIXES, + expr: (node, tables) => + (node.kind === "awaitExpr" && node.type.kind === "dyn") || + // Rejection reasons cross as dyn values (allSettled's records, any's + // AggregateError list). + (node.kind === "intrinsic" && + (node.name === "promise.allSettled" || node.name === "promise.any")) || + (node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())), + }), + dynInvoke: rule({ + initial: (mod) => mod.workers === true, + fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS], + expr: (node, _tables) => node.kind === "dynInvoke", + }), + /** The program has dynamic-tier jsval operations (js.* builtins, the + * jsval pass): links the scr_js_* units (two-tier design I3). */ + dynTier: rule({ prefixes: ["js."] }), + /** The stream unit emits through the emitter unit, also when the + * program constructs no stream (a pipeline of functions). */ + emitter: rule({ + initial: (mod) => + mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), + fns: [...STREAM_LIB_FNS], + prefixes: ["emitter.", ...STREAM_LIB_PREFIXES], + }), + /** Subclasses of the runtime EventTarget/Event bases tear down and box + * through the EventTarget unit even when no event.* libCall survives. */ + eventTarget: rule({ + initial: (mod) => + (mod.classes ?? []).some( + (c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS, + ), + fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS], + prefixes: ["event."], + }), + fetch: rule({ prefixes: ["fetch."] }), + fileHandle: rule({ types: ["fileHandle"] }), + fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }), + http: rule({ + prefixes: ["http.", "tls.", "https.", "http2."], + types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"], + }), + /** The real h2 surface (scr_http2.c); the legacy http2.* libCalls of the + * allowHTTP1 slice are served by scr_http.c and scr_tls.c. */ + http2: rule({ + libCall: (node) => node.fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(node.fn), + types: ["http2Session", "http2Stream"], + }), + /** scr_test.c renders thrown strings with the inspect unit's string + * formatting; scr_util_callable.c wraps the inspect unit; readline's + * terminal unit prints completion errors through it when there is a + * completer. */ + inspect: rule({ + fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS], + prefixes: ["insp.", "console.", "test."], + libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!), + }), + legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }), + /** The program calls mock.timers members: its timer calls go through + * scr_test.c's mock-aware entry points. */ + mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }), + net: rule({ + prefixes: NET_PREFIXES, + types: [ + "netServer", + "netSocket", + "http2Session", + "http2Stream", + "httpReq", + "httpRes", + "httpClientReq", + "secureCtx", + ], + }), + nodeTest: rule({ prefixes: ["test."], types: ["testCtx"] }), + /** scr_util_callable.c reads options through the parseArgs unit's + * descriptors. */ + parseArgs: rule({ fns: [...PARSE_ARGS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS] }), + processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }), + /** scr_url_legacy.c parses and stringifies queries. */ + qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }), + /** scr_url_legacy.c lowercases and normalizes hostnames with the regex + * unit's libunicode, NODE_DEBUG matching in scr_util_callable.c and + * readline's terminal unit use the regex unit, and locale case mapping + * runs its default conversion. */ + regex: rule({ + fns: [ + ...REGEX_LIB_FNS, + ...LEGACY_URL_LIB_FNS, + ...UTIL_CALLABLE_LIB_FNS, + "rl.terminal", + "str.localeCase", + ], + expr: (node, _tables) => + node.kind === "regexLit" || + node.kind === "regexIntrinsic" || + (node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)), + types: ["regex"], + }), + searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }), + /** stdin setEncoding / string data listeners / stdio 'resize' events. A + * typed string 'data' listener takes setEncoding-decoded chunks; the + * process stream object's setEncoding and 'resize' members are reached + * by name (dynamic member calls and event-name strings). */ + stdioExtended: rule({ + libCall: (node) => + node.fn === "stdin.onData" && + node.args[0]?.type.kind === "func" && + node.args[0].type.params[0]?.kind === "string", + expr: (node, _tables) => + (node.kind === "dynInvoke" && STDIO_EXTENDED_NAMES.has(node.method)) || + (node.kind === "strLit" && STDIO_EXTENDED_NAMES.has(node.value)), + }), + stream: rule({ + initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), + fns: [...STREAM_LIB_FNS], + prefixes: STREAM_LIB_PREFIXES, + }), + /** A Dirent row converted to dyn carries Node's Symbol(type) key + * (scr_dyn_dirent_finish, defined in scr_symbol.c). */ + symbol: rule({ + initial: (mod) => (mod.records ?? []).some(isDirentShape), + fns: [...SYMBOL_LIB_FNS], + prefixes: ["sym.", "disposable."], + types: ["symbol"], + }), + tls: rule({ prefixes: ["tls.", "https.", "http2."], types: ["secureCtx"] }), + tlsCa: rule({ prefixes: ["tlsca."] }), + /** process.emitWarning is assigned (process.setEmitWarning). */ + warningReplace: rule({ fns: ["process.setEmitWarning"] }), + /** crypto.subtle importKey/sign/verify/digest and CryptoKey: + * scr_webcrypto.c. */ + webcrypto: rule({ prefixes: ["webcrypto."] }), + zlib: rule({ prefixes: ["zlib."] }), +}; + +type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES; + +export type RuntimeFeatures = Record; + +const RUNTIME_FEATURE_NAMES: RuntimeFeatureName[] = []; +for (const name in RUNTIME_FEATURE_RULES) RUNTIME_FEATURE_NAMES.push(name as RuntimeFeatureName); + +/** Every feature off, in the table's order. TypeScript rejects this + * literal when it misses a feature or names one without a rule. */ +function noRuntimeFeatures(): RuntimeFeatures { + return { + assert: false, asyncContext: false, - inspect: false, - childProcess: false, - net: false, - // A Dirent row converted to dyn carries Node's Symbol(type) key - // (scr_dyn_dirent_finish, defined in scr_symbol.c). - symbol: (mod.records ?? []).some(isDirentShape), bigint: false, - searchParams: false, - qs: false, - parseArgs: false, - fsWatch: false, - nodeTest: false, - mockTimers: false, + childProcess: false, + cipher: false, + copying: false, + dc: false, dgram: false, + dynAsync: false, + dynInvoke: false, + dynTier: false, + emitter: false, + eventTarget: false, + fetch: false, + fileHandle: false, + fsWatch: false, http: false, http2: false, + inspect: false, + legacyTextDecoder: false, + mockTimers: false, + net: false, + nodeTest: false, + parseArgs: false, + processEvents: false, + qs: false, + regex: false, + searchParams: false, + stdioExtended: false, + stream: false, + symbol: false, tls: false, tlsCa: false, - cipher: false, + warningReplace: false, webcrypto: false, + zlib: false, }; +} + +const ruleOf = (name: RuntimeFeatureName): RuntimeFeatureRule => RUNTIME_FEATURE_RULES[name]; + +/** The rules indexed for the scan: features by libCall name and by type + * kind, and the prefix and predicate conditions in rule order. */ +const FEATURES_BY_FN = new Map(); +const FEATURES_BY_TYPE = new Map(); +const FEATURE_PREFIXES: { prefix: string; name: RuntimeFeatureName }[] = []; +const FEATURE_LIBCALL_TESTS: RuntimeFeatureName[] = []; +const FEATURE_EXPR_TESTS: RuntimeFeatureName[] = []; +for (const name of RUNTIME_FEATURE_NAMES) { + const rule = ruleOf(name); + for (const fn of rule.fns ?? []) { + const names = FEATURES_BY_FN.get(fn); + if (names) names.push(name); + else FEATURES_BY_FN.set(fn, [name]); + } + for (const kind of rule.types ?? []) { + const names = FEATURES_BY_TYPE.get(kind); + if (names) names.push(name); + else FEATURES_BY_TYPE.set(kind, [name]); + } + for (const prefix of rule.prefixes ?? []) FEATURE_PREFIXES.push({ prefix, name }); + if (rule.libCall) FEATURE_LIBCALL_TESTS.push(name); + if (rule.expr) FEATURE_EXPR_TESTS.push(name); +} + +function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures { + const features = noRuntimeFeatures(); + for (const name of RUNTIME_FEATURE_NAMES) { + const initial = ruleOf(name).initial; + if (initial && initial(mod)) features[name] = true; + } const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; const typeTables = (): TypeTables => @@ -458,207 +711,19 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run const expr = (node: IrExpr): boolean => { if (node.kind === "libCall") { const fn = node.fn; - if (fn.startsWith("js.")) features.dynTier = true; - if (REGEX_LIB_FNS.has(fn)) features.regex = true; - if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true; - if (fn.startsWith("fetch.")) features.fetch = true; - if (fn === "process.setEmitWarning") features.warningReplace = true; - // A typed string 'data' listener takes setEncoding-decoded chunks. - if ( - fn === "stdin.onData" && - node.args[0]?.type.kind === "func" && - node.args[0].type.params[0]?.kind === "string" - ) - features.stdioExtended = true; - if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; - if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true; - if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; - // With a completer, readline's terminal unit prints completion errors - // through the inspect unit. - if (fn === "rl.terminal") { - features.regex = true; - if (!isDynUndefined(node.args[3]!)) features.inspect = true; - } - if (fn.startsWith("emitter.")) features.emitter = true; - if (STREAM_LIB_FNS.has(fn) || startsWithAny(fn, STREAM_LIB_PREFIXES)) { - // the stream unit emits through the emitter unit, also when the - // program constructs no stream (a pipeline of functions) - features.stream = true; - features.emitter = true; - } - if (fn.startsWith("zlib.")) features.zlib = true; - if (READABLE_DYN_ASYNC_LIB_FNS.has(fn) || startsWithAny(fn, READABLE_DYN_ASYNC_PREFIXES)) - features.dynAsync = true; - if (fn.startsWith("dc.")) features.dc = true; - if (fn.startsWith("assert.")) features.assert = true; - if (DYN_INVOKE_LIB_FNS.has(fn)) features.dynInvoke = true; - if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; - if (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true; - if ( - fn.startsWith("insp.") || - fn.startsWith("console.") || - fn === "global.native" || - fn === "util.styleText" - ) - features.inspect = true; - if (CHILD_PROCESS_LIB_FNS.has(fn) || startsWithAny(fn, CHILD_PROCESS_LIB_PREFIXES)) - features.childProcess = true; - if (fn.startsWith("net.")) features.net = true; - if (fn.startsWith("http.")) { - features.net = true; - features.http = true; - } - if (fn.startsWith("tls.") || fn.startsWith("https.") || fn.startsWith("http2.")) { - features.net = true; - features.http = true; - features.tls = true; - } - if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true; - if (fn.startsWith("sym.") || fn.startsWith("disposable.") || SYMBOL_LIB_FNS.has(fn)) - features.symbol = true; - if (fn === "process.hrtimeValue") features.bigint = true; - if (fn.startsWith("bigint.")) features.bigint = true; - if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true; - if (QS_LIB_FNS.has(fn)) features.qs = true; - // scr_url_legacy.c lowercases and normalizes hostnames (the regex - // unit's libunicode), parses and stringifies queries, and emits its - // deprecation warnings through the dyn-async unit. - if (LEGACY_URL_LIB_FNS.has(fn)) { - features.regex = true; - features.qs = true; - features.dynAsync = true; - } - if (PARSE_ARGS_LIB_FNS.has(fn)) features.parseArgs = true; - if (fn === "util.styleText") features.processEvents = true; - // scr_util_callable.c: dyn wrappers over the descriptor, inspect, - // regex (NODE_DEBUG matching) and terminal-color units. - if (UTIL_CALLABLE_LIB_FNS.has(fn)) { - features.parseArgs = true; - features.inspect = true; - features.dynInvoke = true; - features.regex = true; - } - if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true; - // scr_test.c renders thrown strings in the failing-tests section with - // the inspect unit's string formatting. - if (fn.startsWith("test.")) { - features.nodeTest = true; - features.inspect = true; - if (fn.startsWith("test.mockTimers") || fn.startsWith("test.ctxMockTimers")) { - features.mockTimers = true; - } - } - if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; - if (fn.startsWith("tlsca.")) features.tlsCa = true; - // Locale case mapping runs the default conversion of the regex unit. - if (fn === "str.localeCase") features.regex = true; - // scr_cipher.c over the mbedTLS archive; DEP0182 reports through the - // process-warning unit, and unknown output encodings render through - // the inspect unit. - if (fn === "crypto.cipherNew") { - features.cipher = true; - features.dynAsync = true; - features.inspect = true; - } - // crypto.subtle and CryptoKey: scr_webcrypto.c. - if (fn.startsWith("webcrypto.")) features.webcrypto = true; + for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true; + for (const { prefix, name } of FEATURE_PREFIXES) + if (!features[name] && fn.startsWith(prefix)) features[name] = true; + for (const name of FEATURE_LIBCALL_TESTS) + if (!features[name] && ruleOf(name).libCall!(node)) features[name] = true; } else { - switch (node.kind) { - case "regexLit": - case "regexIntrinsic": - features.regex = true; - break; - case "strIntrinsic": - if (CASE_MAPPING_STR_METHODS.has(node.method)) features.regex = true; - break; - case "arrIntrinsic": - if (COPYING_ARR_METHODS.has(node.method)) features.copying = true; - break; - case "bytesIntrinsic": - if (COPYING_BYTES_METHODS.has(node.method)) features.copying = true; - break; - case "dynInvoke": - features.dynInvoke = true; - if (STDIO_EXTENDED_NAMES.has(node.method)) features.stdioExtended = true; - break; - // The process stream object's setEncoding and 'resize' members are - // reached by name (dynamic member calls and event-name strings). - case "strLit": - if (STDIO_EXTENDED_NAMES.has(node.value)) features.stdioExtended = true; - break; - case "awaitExpr": - if (node.type.kind === "dyn") features.dynAsync = true; - break; - case "intrinsic": - // Rejection reasons cross as dyn values (allSettled's records, - // any's AggregateError list). - if (node.name === "promise.allSettled" || node.name === "promise.any") - features.dynAsync = true; - break; - case "dynFrom": - if (!features.dynAsync && boxesPromise(node.value.type, typeTables(), new Set())) - features.dynAsync = true; - break; - } + for (const name of FEATURE_EXPR_TESTS) + if (!features[name] && ruleOf(name).expr!(node, typeTables)) features[name] = true; } return keepGoing(); }; const type = (node: IrType): boolean => { - switch (node.kind) { - case "regex": - features.regex = true; - break; - case "fileHandle": - features.fileHandle = true; - break; - case "procStream": - features.processEvents = true; - break; - case "child": - case "childStream": - case "childWriter": - case "spawnRes": - features.childProcess = true; - break; - case "netServer": - case "netSocket": - features.net = true; - break; - case "http2Session": - case "http2Stream": - features.net = true; - features.http2 = true; - break; - case "httpReq": - case "httpRes": - case "httpClientReq": - features.net = true; - features.http = true; - break; - case "secureCtx": - features.net = true; - features.http = true; - features.tls = true; - break; - case "symbol": - features.symbol = true; - break; - case "bigint": - features.bigint = true; - break; - case "searchParams": - features.searchParams = true; - break; - case "fsWatcher": - features.fsWatch = true; - break; - case "testCtx": - features.nodeTest = true; - break; - case "dgramSocket": - features.dgram = true; - break; - } + for (const name of FEATURES_BY_TYPE.get(node.kind) ?? []) features[name] = true; return keepGoing(); }; if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type }); @@ -702,64 +767,6 @@ export function moduleUsesFetch(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "fetch").fetch; } -/** The libCall fns served by the OPTIONAL events unit (scr_events.c): - * process signal/exit listeners and the piped-stdin surface. */ -const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ - "process.stdio", - "process.onSignal", - "process.offSignal", - "process.onExit", - "process.offExit", - "stdin.onData", - "stdin.onEnd", - "stdin.onError", - "stdin.nextChunk", - // node:readline rides the stdin unit (scr_readline.c links beside - // scr_events.c under the same gate). - "rl.create", - "rl.createSignal", - "rl.question", - "rl.questionAsync", - "rl.questionAsyncSignal", - "rl.close", - "rl.pause", - "rl.resume", - "rl.prompt", - "rl.promptWith", - "rl.setPrompt", - "rl.getPrompt", - "rl.write", - "rl.writeKey", - "rl.on", - "rl.iterOpen", - "rl.iterNext", - "rl.iterReturn", - "rl.checkOptions", - "rl.checkCompleter", - "rl.setOptions", - "rl.setHistory", - "rl.history", - "rl.numProp", - "rl.terminal", - "rl.line", - "rl.isTerminal", - "rl.cursor", - "rl.seq", - "rl.moves", - "rl.cursorPos", - "rl.keypressStdin", - "rl.keypressStream", - "stdin.onKeypress", - "rl.uiOpen", - "rl.uiOpenStream", - "rl.uiCursorTo", - "rl.uiMoveCursor", - "rl.uiClearLine", - "rl.uiClearScreenDown", - "rl.uiRollback", - "rl.uiCommit", -]); - /** True when the module uses the process-events surface — the link switch * that pulls scr_events.c into the binary and has the emitted main call * scr_events_install (native-toolchain.ts + emitter; the scr_regex/scr_fetch/scr_zlib @@ -949,14 +956,6 @@ export function moduleUsesHttpServer(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "http").http; } -/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the - * allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so - * divergence-57 binaries keep their exact link line. */ -const HTTP2_LEGACY_FNS: ReadonlySet = new Set([ - "http2.streamNoop", - "http2.streamUndefCall", -]); - /** True when the module uses the REAL h2 surface (scr_http2.c): any core * http2.* libCall, or an h2 handle type left behind by a fenced statement * (its emitted release call needs the unit — the moduleUsesNet story). */ diff --git a/packages/compiler/src/ir/unshare.ts b/packages/compiler/src/ir/unshare.ts index 36dd6176f..a83d52a5f 100644 --- a/packages/compiler/src/ir/unshare.ts +++ b/packages/compiler/src/ir/unshare.ts @@ -42,14 +42,15 @@ export function unshareNodes(mod: IrModule): IrModule { } if (affected.size === 0) return mod; // Rebuilding a function copies every node that has children; a repeated - // childless node reads back as a fresh object at each of its sites there. + // childless node gets its own shallow copy at each of its sites there. + // Both kinds of copy keep sharing the type and location objects. const expr = (node: IrExpr): IrExpr => { const copy = mapExprChildren(node, expr, stmt); - return copy !== node || !repeated.has(node) ? copy : reparseExpr(node); + return copy !== node || !repeated.has(node) ? copy : copyChildlessExpr(node); }; const stmt = (node: IrStmt): IrStmt => { const copy = mapStmtChildren(node, expr, stmt); - return copy !== node || !repeated.has(node) ? copy : reparseStmt(node); + return copy !== node || !repeated.has(node) ? copy : copyChildlessStmt(node); }; return { ...mod, @@ -58,3 +59,66 @@ export function unshareNodes(mod: IrModule): IrModule { ), }; } + +/** A shallow copy of a node that mapExprChildren returns unchanged. Each + * case spreads one node kind, so the copy is a plain record copy of that + * kind. Any other kind is copied through the encoding. */ +function copyChildlessExpr(node: IrExpr): IrExpr { + switch (node.kind) { + case "varRef": + return { ...node }; + case "strLit": + return { ...node }; + case "numLit": + return { ...node }; + case "boolLit": + return { ...node }; + case "unitLit": + return { ...node }; + case "closure": + return { ...node }; + case "moduleNsRef": + return { ...node }; + case "incDec": + return { ...node }; + case "chainRecv": + return { ...node }; + case "regexLit": + return { ...node }; + case "templateStrings": + return { ...node }; + case "setNew": + return { ...node }; + case "mapNew": + return { ...node }; + case "dynObjLit": + return { ...node }; + case "selfRef": + return { ...node }; + case "promiseWithResolvers": + return { ...node }; + case "classRef": + return { ...node }; + case "fieldAbsent": + return { ...node }; + default: + return reparseExpr(node); + } +} + +/** A shallow copy of a statement that mapStmtChildren returns unchanged, + * one case per kind as in copyChildlessExpr. */ +function copyChildlessStmt(node: IrStmt): IrStmt { + switch (node.kind) { + case "break": + return { ...node }; + case "continue": + return { ...node }; + case "runtimeFence": + return { ...node }; + case "rethrow": + return { ...node }; + default: + return reparseStmt(node); + } +} diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 4a961dfca..50ba236b8 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4109,6 +4109,7 @@ function validateFunction( if (checkLibEmitterCall(e, sig.result)) return; if ( e.fn === "error.stack" || + e.fn === "error.setStack" || e.fn === "error.cause" || e.fn === "error.hasCause" || e.fn === "error.setCause" || diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 01d12dedc..8f6064868 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1988,7 +1988,7 @@ "kind": "node-builtin", "name": "process.exitCode", "status": "static", - "note": "numeric writes in statement position set the implicit exit status; process.exit() reads it" + "note": "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it" }, { "id": "node-builtin.process.getBuiltinModule", @@ -2532,7 +2532,7 @@ "name": "util.promisify", "status": "unsupported", "code": "SC2020", - "note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart; call the bound function directly" + "note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart, plus typed callback-last function values whose callback takes (error?: Error | null, value?); call the bound function directly" }, { "id": "node-builtin.util.stripVTControlCharacters", diff --git a/packages/compiler/test/bytes-field-borrow-emission.test.ts b/packages/compiler/test/bytes-field-borrow-emission.test.ts new file mode 100644 index 000000000..168cd78ac --- /dev/null +++ b/packages/compiler/test/bytes-field-borrow-emission.test.ts @@ -0,0 +1,94 @@ +import { expect, test } from "vitest"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import { + F64, + VOID, + bytesOf, + type IrExpr, + type IrFunction, + type IrModule, + type IrStmt, +} from "../src/ir/ir.js"; +import { validateModule } from "../src/ir/validate.js"; + +const loc = { file: "bytes-field-borrow.ts", start: 0, end: 0 }; +const units = bytesOf("u16"); +const scanner = { kind: "object" as const, className: "Scanner" }; +const self = (): IrExpr => ({ kind: "varRef", localId: "this", type: scanner, loc }); +const chars = (): IrExpr => ({ + kind: "fieldGet", + obj: self(), + className: "Scanner", + field: "chars", + type: units, + loc, +}); +const local = (): IrExpr => ({ kind: "varRef", localId: "c", type: units, loc }); +const read = (index: number): IrExpr => ({ + kind: "bytesIntrinsic", + method: "get", + receiver: local(), + args: [{ kind: "numLit", value: index, type: F64, loc }], + type: F64, + loc, +}); + +/** `const c = this.chars; ...between; return c[0] + c[1]`. */ +function method(name: string, between: IrStmt[]): IrFunction { + return { + name, + params: [{ localId: "this", name: "this", type: scanner }], + returnType: F64, + locals: [ + { id: "this", name: "this", type: scanner, mutable: false }, + { id: "c", name: "c", type: units, mutable: false }, + ], + body: [ + { kind: "varDecl", localId: "c", init: chars(), loc }, + ...between, + { + kind: "return", + value: { kind: "bin", op: "+", left: read(0), right: read(1), type: F64 }, + loc, + }, + ], + loc, + } as IrFunction; +} + +test("a typed-array local read from a field the rest of its block never writes borrows it", () => { + const replace: IrStmt = { + kind: "fieldSet", + obj: self(), + className: "Scanner", + field: "chars", + value: { + kind: "bytesNew", + source: { kind: "numLit", value: 4, type: F64, loc }, + type: units, + loc, + }, + loc, + }; + const mod: IrModule = { + irVersion: 16, + sourceFile: loc.file, + entry: "main", + functions: [ + { name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, + method("%Scanner.peek", []), + method("%Scanner.swap", [replace]), + ], + classes: [{ name: "Scanner", fields: [{ name: "chars", type: units }], methods: [], loc }], + }; + expect(validateModule(mod)).toEqual([]); + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const body = (name: string): string => + new RegExp(`^define [^\\n]*@sc_bf__x25_Scanner_${name}\\b[^]*?^}`, "m").exec(llvm)?.[0] ?? ""; + expect(body("peek")).not.toBe(""); + // The field keeps the array alive while the borrowed local reads it. + expect(body("peek")).not.toContain("bytes_retain"); + expect(body("peek")).not.toContain("bytes_release"); + // Replacing the field could free the array the local holds: it keeps a reference. + expect(body("swap")).toContain("call ptr @scr_bytes_retain_v("); +}); diff --git a/packages/compiler/test/class-attributes.test.ts b/packages/compiler/test/class-attributes.test.ts new file mode 100644 index 000000000..1a6fea654 --- /dev/null +++ b/packages/compiler/test/class-attributes.test.ts @@ -0,0 +1,84 @@ +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 } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lowerModule(source: string, file = "main.ts"): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-class-attributes-")); + try { + const entry = join(dir, file); + const outPath = join(dir, "main.ir.json"); + 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")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function fieldsOf(mod: IrModule, className: string): string[] { + const cls = (mod.classes ?? []).find( + (c) => c.name === className || c.name.endsWith(`.${className}`), + ); + if (!cls) throw new Error(`no class ${className}`); + return cls.fields.map((f) => f.name); +} + +function functionsCalling(mod: IrModule, fn: string): string[] { + return mod.functions + .filter((f) => JSON.stringify(f.body).includes(`"fn":"${fn}"`)) + .map((f) => f.name); +} + +test("only hierarchies passed to an integrity call get attribute state and write checks", async () => { + const mod = await lowerModule(` +export {}; +class Point { x = 1; } +class Other { x = 1; } +function movePoint(p: Point): void { p.x = 2; } +function moveOther(o: Other): void { o.x = 2; } +const p = new Point(); +Object.freeze(p); +movePoint(p); +moveOther(new Other()); +`); + expect(fieldsOf(mod, "Point")).toContain("%attributes"); + expect(fieldsOf(mod, "Other")).not.toContain("%attributes"); + const checked = functionsCalling(mod, "class.throwReadOnly"); + expect(checked).toContain("movePoint"); + expect(checked).not.toContain("moveOther"); +}); + +test("a redefined field gets its own attribute word", async () => { + const mod = await lowerModule(` +export {}; +class Config { mode = "dev"; level = 1; } +const c = new Config(); +Object.defineProperty(c, "mode", { writable: false }); +`); + const fields = fieldsOf(mod, "Config"); + expect(fields).toContain("%attributes"); + expect(fields).toContain("%attr:mode"); + expect(fields).not.toContain("%attr:level"); +}); + +test("sloppy-mode writes skip a read-only field without throwing", async () => { + const mod = await lowerModule( + ` +class Box { constructor() { this.x = 1; } } +const b = new Box(); +Object.freeze(b); +b.x = 2; +`, + "main.cjs", + ); + expect(fieldsOf(mod, "Box")).toContain("%attributes"); + // Class bodies are strict mode code; the top-level write is not. + const throwing = functionsCalling(mod, "class.throwReadOnly"); + expect(throwing.length).toBeGreaterThan(0); + expect(throwing.every((name) => name.includes("constructor"))).toBe(true); +}); diff --git a/packages/compiler/test/local-class-private-boundaries.test.ts b/packages/compiler/test/local-class-private-boundaries.test.ts new file mode 100644 index 000000000..fdbe2e8f8 --- /dev/null +++ b/packages/compiler/test/local-class-private-boundaries.test.ts @@ -0,0 +1,63 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { analyze } from "../src/index.js"; + +function refusals(file: string, source: string): string[] { + const dir = mkdtempSync(join(tmpdir(), "scriptc-local-class-private-")); + try { + const entry = join(dir, file); + writeFileSync(entry, source); + const { coverage } = analyze(entry); + return [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])].map((d) => d.message); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +// Function-local JavaScript classes compile private fields used only on +// `this` when the class is a named declaration: each evaluation's prototype +// methods check the receiver's evaluation against that binding. Private +// methods and accessors, class expressions, private names on other objects, +// and TypeScript classes keep the refusal. +test.each([ + [ + "a private method", + "main.js", + "function make() { class Box { #read() { return 1; } read() { return this.#read(); } } return new Box(); } console.log(make().read());", + ], + [ + "a private accessor", + "main.js", + "function make() { class Box { get #value() { return 1; } read() { return this.#value; } } return new Box(); } console.log(make().read());", + ], + [ + "a class expression", + "main.js", + "function make() { const Box = class { #value = 1; read() { return this.#value; } }; return new Box(); } console.log(make().read());", + ], + [ + "a private name on another object", + "main.js", + "function make() { class Box { #value = 1; has(other) { return #value in other; } } return new Box(); } console.log(make().has({}));", + ], + [ + "a TypeScript class", + "main.ts", + "function make() { class Box { #value = 1; read(): number { return this.#value; } } return new Box(); } console.log(make().read());", + ], +] as const)("function-local private names keep the refusal with %s", (_name, file, source) => { + expect(refusals(file, source)).toContainEqual( + expect.stringContaining("function-local classes with private brands"), + ); +}); + +test("function-local private fields used on this compile", () => { + expect( + refusals( + "main.js", + "function make() { class Box { #value = 1; read() { return this.#value; } has() { return #value in this; } } return new Box(); } const box = make(); console.log(box.read(), box.has());", + ), + ).toEqual([]); +}); diff --git a/packages/compiler/test/native-class-descriptors.test.ts b/packages/compiler/test/native-class-descriptors.test.ts index d4aff3949..bd0a06dac 100644 --- a/packages/compiler/test/native-class-descriptors.test.ts +++ b/packages/compiler/test/native-class-descriptors.test.ts @@ -12,7 +12,7 @@ function fixture(source: string) { } test.each([ - "Object.defineProperty(value, 'x', {value: 2, writable: false});", + "Object.defineProperty(value, 'x', {get() { return 2; }});", "Object.defineProperties(value, {x: {value: 2}});", ])("keeps unsafe native descriptor changes fenced: %s", (operation) => { const { dir, entry } = fixture( @@ -30,6 +30,42 @@ test.each([ } }); +test("data descriptor changes on native fields match Node", async () => { + const { dir, entry } = fixture(` +class Value { constructor() { this.x = 1; } } +const value = new Value(); +Object.defineProperty(value, 'x', { value: 2, writable: false }); +console.log(value.x, JSON.stringify(Object.getOwnPropertyDescriptor(value, 'x'))); +value.x = 3; +console.log(value.x, Object.keys(value)); +`); + try { + const result = await compile(entry, { + outDir: dir, + outPath: join(dir, "program"), + sanitize: process.env["SCRIPTC_SAN"] === "1", + }); + if (!result.ok) throw new Error(JSON.stringify(result.diagnostics)); + const run = spawnSync(result.binaryPath, { encoding: "utf8", timeout: 30_000 }); + const oracle = spawnSync(process.execPath, [entry], { encoding: "utf8", timeout: 30_000 }); + expect(run.error).toBeUndefined(); + expect(oracle.status).toBe(0); + expect({ + status: run.status, + signal: run.signal, + stdout: run.stdout, + stderr: run.stderr, + }).toEqual({ + status: oracle.status, + signal: oracle.signal, + stdout: oracle.stdout, + stderr: oracle.stderr, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test("JS overrides with different return types match Node", async () => { const { dir, entry } = fixture(` class Base { copy(value = 1) { return value; } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5467-typed-array-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5467-typed-array-loop-versions.ts.json new file mode 100644 index 000000000..a4be59696 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5467-typed-array-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5467-typed-array-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5468-typed-array-loop-aliasing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5468-typed-array-loop-aliasing.ts.json new file mode 100644 index 000000000..517cbab4c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5468-typed-array-loop-aliasing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5468-typed-array-loop-aliasing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts.json new file mode 100644 index 000000000..79c7d0c63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json new file mode 100644 index 000000000..eef427a5e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5607-string-char-compare.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json new file mode 100644 index 000000000..855057006 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5608-string-char-switch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json new file mode 100644 index 000000000..7f0bf061d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5609-string-switch-optional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json new file mode 100644 index 000000000..001c7ea32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5730-thread-slots-widened-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json new file mode 100644 index 000000000..fab2fd504 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5750-string-from-char-codes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json new file mode 100644 index 000000000..0eebe2326 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5751-string-length-cache.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json new file mode 100644 index 000000000..84c394e4a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5775-md5-lengths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5816-fixed-into-unknown-rest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5816-fixed-into-unknown-rest.ts.json new file mode 100644 index 000000000..8354f5c48 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5816-fixed-into-unknown-rest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5816-fixed-into-unknown-rest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5876-readline-dispose-instanceof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5876-readline-dispose-instanceof.ts.json new file mode 100644 index 000000000..959c704b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5876-readline-dispose-instanceof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5876-readline-dispose-instanceof.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5877-readline-question-query.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5877-readline-question-query.cjs.json new file mode 100644 index 000000000..fe2bcfeee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5877-readline-question-query.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5877-readline-question-query.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5878-readline-dynamic-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5878-readline-dynamic-streams.mjs.json new file mode 100644 index 000000000..ddd665241 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5878-readline-dynamic-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5878-readline-dynamic-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5891-typed-array-field-borrows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5891-typed-array-field-borrows.ts.json new file mode 100644 index 000000000..efd0044d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5891-typed-array-field-borrows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5891-typed-array-field-borrows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5916-dyn-error-stack-property.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5916-dyn-error-stack-property.ts.json new file mode 100644 index 000000000..fa5b5460f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5916-dyn-error-stack-property.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5916-dyn-error-stack-property.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5983-runtime-constructor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5983-runtime-constructor.ts.json new file mode 100644 index 000000000..004b1e350 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5983-runtime-constructor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5983-runtime-constructor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6004-class-instance-integrity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6004-class-instance-integrity.ts.json new file mode 100644 index 000000000..4d21236c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6004-class-instance-integrity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6004-class-instance-integrity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6005-class-instance-integrity-sloppy.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6005-class-instance-integrity-sloppy.cjs.json new file mode 100644 index 000000000..1101d2180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6005-class-instance-integrity-sloppy.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6005-class-instance-integrity-sloppy.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6058-class-view-asserted-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6058-class-view-asserted-bindings.ts.json new file mode 100644 index 000000000..b2a73c8a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6058-class-view-asserted-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6058-class-view-asserted-bindings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6061-type-only-class-fields/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6061-type-only-class-fields/main.ts.json new file mode 100644 index 000000000..68bdcec61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6061-type-only-class-fields/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6061-type-only-class-fields/holder.ts", + "/tests/corpus/6061-type-only-class-fields/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json new file mode 100644 index 000000000..9e48b2910 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6063-union-class-field-checks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6342-throw-builtin-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6342-throw-builtin-identity.ts.json new file mode 100644 index 000000000..09b036461 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6342-throw-builtin-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6342-throw-builtin-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6345-inspect-dyn-holes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6345-inspect-dyn-holes.ts.json new file mode 100644 index 000000000..d9027a759 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6345-inspect-dyn-holes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6345-inspect-dyn-holes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json new file mode 100644 index 000000000..3e144e748 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6346-inspect-dyn-dataview.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json new file mode 100644 index 000000000..91c408e7a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6347-dataview-methods-js.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json new file mode 100644 index 000000000..b5c392112 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6348-errno-code-own-field.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6348-errno-code-own-field.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6356-format-integer-dyn.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6356-format-integer-dyn.ts.json new file mode 100644 index 000000000..6e1082a83 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6356-format-integer-dyn.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6356-format-integer-dyn.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-dyn-error-stack-property-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-dyn-error-stack-property-js.js.json new file mode 100644 index 000000000..7bea48883 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-dyn-error-stack-property-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6400-dyn-error-stack-property-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-dyn-error-own-property-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-dyn-error-own-property-names.ts.json new file mode 100644 index 000000000..cf96c4f96 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-dyn-error-own-property-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6401-dyn-error-own-property-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-dyn-error-own-property-names-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-dyn-error-own-property-names-js.js.json new file mode 100644 index 000000000..49cf88884 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-dyn-error-own-property-names-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6402-dyn-error-own-property-names-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6407-dyn-bytes-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6407-dyn-bytes-in.ts.json new file mode 100644 index 000000000..e4b1b5d88 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6407-dyn-bytes-in.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6407-dyn-bytes-in.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6408-dyn-bytes-in-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6408-dyn-bytes-in-js.js.json new file mode 100644 index 000000000..30bd05d7e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6408-dyn-bytes-in-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6408-dyn-bytes-in-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json new file mode 100644 index 000000000..8b84ce6d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6416-tls-session-reused.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json new file mode 100644 index 000000000..5832b91f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6417-object-assign-class-target.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json new file mode 100644 index 000000000..7a030cd19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6418-nullish-assign-index-record.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6420-process-send-presence.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6420-process-send-presence.ts.json new file mode 100644 index 000000000..69db221e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6420-process-send-presence.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6420-process-send-presence.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6421-process-send-presence-fork/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6421-process-send-presence-fork/main.ts.json new file mode 100644 index 000000000..abde055bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6421-process-send-presence-fork/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6421-process-send-presence-fork/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6422-process-exitcode-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6422-process-exitcode-read.ts.json new file mode 100644 index 000000000..15abd3cdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6422-process-exitcode-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6422-process-exitcode-read.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6423-process-exitcode-default.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6423-process-exitcode-default.ts.json new file mode 100644 index 000000000..31bf2584e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6423-process-exitcode-default.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6423-process-exitcode-default.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6424-process-exit-keeps-exitcode.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6424-process-exit-keeps-exitcode.ts.json new file mode 100644 index 000000000..ce051e3fa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6424-process-exit-keeps-exitcode.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6424-process-exit-keeps-exitcode.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6425-stream-hook-wider-chunk.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6425-stream-hook-wider-chunk.ts.json new file mode 100644 index 000000000..4c6db1400 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6425-stream-hook-wider-chunk.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6425-stream-hook-wider-chunk.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6426-promise-race-void-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6426-promise-race-void-entry.ts.json new file mode 100644 index 000000000..e4d66ecf4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6426-promise-race-void-entry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6426-promise-race-void-entry.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6427-http-request-options-type.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6427-http-request-options-type.ts.json new file mode 100644 index 000000000..3e403ece6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6427-http-request-options-type.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6427-http-request-options-type.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6428-promisify-callback-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6428-promisify-callback-functions.ts.json new file mode 100644 index 000000000..76c3e65d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6428-promisify-callback-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6428-promisify-callback-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6429-esm-js-dirname.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6429-esm-js-dirname.mjs.json new file mode 100644 index 000000000..0168f85ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6429-esm-js-dirname.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6429-esm-js-dirname.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6430-npm-constructor-function-class/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6430-npm-constructor-function-class/main.ts.json new file mode 100644 index 000000000..76bd8e813 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6430-npm-constructor-function-class/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6430-npm-constructor-function-class/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6431-cjs-exports-rebinding/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6431-cjs-exports-rebinding/main.cjs.json new file mode 100644 index 000000000..8fc4a2acc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6431-cjs-exports-rebinding/main.cjs.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6431-cjs-exports-rebinding/lib.cjs", + "/tests/corpus/6431-cjs-exports-rebinding/extended.cjs", + "/tests/corpus/6431-cjs-exports-rebinding/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6445-rest-function-fixed-slot.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6445-rest-function-fixed-slot.ts.json new file mode 100644 index 000000000..b39c2fa38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6445-rest-function-fixed-slot.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6445-rest-function-fixed-slot.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6446-rest-function-nested-declaration.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6446-rest-function-nested-declaration.ts.json new file mode 100644 index 000000000..41c034395 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6446-rest-function-nested-declaration.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6446-rest-function-nested-declaration.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6460-in-union-optional-slots.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6460-in-union-optional-slots.ts.json new file mode 100644 index 000000000..8491507eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6460-in-union-optional-slots.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6460-in-union-optional-slots.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6461-predicate-intersection-narrowing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6461-predicate-intersection-narrowing.ts.json new file mode 100644 index 000000000..3ce078006 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6461-predicate-intersection-narrowing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6461-predicate-intersection-narrowing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6462-assertion-attached-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6462-assertion-attached-members.ts.json new file mode 100644 index 000000000..36e333373 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6462-assertion-attached-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6462-assertion-attached-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json new file mode 100644 index 000000000..38b99f9fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6480-catch-typeof-untyped-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-class-hierarchy-npm/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-class-hierarchy-npm/main.ts.json new file mode 100644 index 000000000..f44d1965c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-class-hierarchy-npm/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/cjs-class-hierarchy-npm/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-open-options-npm/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-open-options-npm/main.ts.json new file mode 100644 index 000000000..3c3043a09 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-open-options-npm/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/cjs-open-options-npm/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-strict-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-strict-arguments.cjs.json new file mode 100644 index 000000000..4e52c7e57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-strict-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/cjs-strict-arguments.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-symbol-key-collisions/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-symbol-key-collisions/main.js.json new file mode 100644 index 000000000..205da46f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-symbol-key-collisions/main.js.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/cjs-symbol-key-collisions/core-symbols.js", + "/tests/corpus/cjs-symbol-key-collisions/client.js", + "/tests/corpus/cjs-symbol-key-collisions/mock-symbols.js", + "/tests/corpus/cjs-symbol-key-collisions/mock-client.js", + "/tests/corpus/cjs-symbol-key-collisions/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-symbol-key-table/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-symbol-key-table/main.js.json new file mode 100644 index 000000000..3975cce3a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-symbol-key-table/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/cjs-symbol-key-table/symbols.js", + "/tests/corpus/cjs-symbol-key-table/queue.js", + "/tests/corpus/cjs-symbol-key-table/upper-queue.js", + "/tests/corpus/cjs-symbol-key-table/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-unmapped-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-unmapped-arguments.cjs.json new file mode 100644 index 000000000..bff6dd797 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/cjs-unmapped-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/cjs-unmapped-arguments.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-detached-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-detached-methods.js.json new file mode 100644 index 000000000..9eb8fdf73 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-detached-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-local-detached-methods.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-detached-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-detached-methods.ts.json new file mode 100644 index 000000000..6ef52390d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-detached-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-local-detached-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-private-evaluations.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-private-evaluations.js.json new file mode 100644 index 000000000..0216d2c2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-private-evaluations.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-local-private-evaluations.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-private-fields.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-private-fields.js.json new file mode 100644 index 000000000..d63dfd6a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-local-private-fields.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-local-private-fields.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/private-in-checked-primitives.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/private-in-checked-primitives.js.json new file mode 100644 index 000000000..1e5b83999 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/private-in-checked-primitives.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/private-in-checked-primitives.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/boot-path-node-surface-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/boot-path-node-surface-fences.ts.json new file mode 100644 index 000000000..de58c3423 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/boot-path-node-surface-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/boot-path-node-surface-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-member-alias-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-member-alias-writes/main.ts.json new file mode 100644 index 000000000..bd3ba799d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-member-alias-writes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/cjs-member-alias-writes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-field-accessor-descriptor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-field-accessor-descriptor.ts.json new file mode 100644 index 000000000..3d6639169 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-field-accessor-descriptor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/class-field-accessor-descriptor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json new file mode 100644 index 000000000..085f01796 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/object-assign-class-copy.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-literal-accessor-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-literal-accessor-values.ts.json new file mode 100644 index 000000000..08ae4c161 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-literal-accessor-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/object-literal-accessor-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/promisify-callback-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/promisify-callback-functions.ts.json new file mode 100644 index 000000000..a4a753c3d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/promisify-callback-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/promisify-callback-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-subclass-hooks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-subclass-hooks.ts.json new file mode 100644 index 000000000..295d7deda --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-subclass-hooks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/stream-subclass-hooks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json new file mode 100644 index 000000000..953193649 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-session-timings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts.json new file mode 100644 index 000000000..cc5bc69c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts.json new file mode 100644 index 000000000..382eef7b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts.json new file mode 100644 index 000000000..f609f128e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts.json new file mode 100644 index 000000000..be3b2af16 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts.json new file mode 100644 index 000000000..68cde3de1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts.json new file mode 100644 index 000000000..90e304cbe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts.json new file mode 100644 index 000000000..6443d170b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts.json new file mode 100644 index 000000000..f51a29951 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts.json new file mode 100644 index 000000000..c3e0442f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 23c803879..3fdfd7ff5 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -1,5 +1,10 @@ import { withWorkerRuntimeVariants } from "../runtime-pack-common/worker-matrix.mjs"; import { withLibraryRuntimeFlavors } from "../runtime-pack-common/library-matrix.mjs"; +import { + BASE_RUNTIME_SOURCES, + OPTIONAL_RUNTIME_SOURCES, + variantsFor, +} from "../runtime-pack-common/runtime-pack-matrix.mjs"; /** * The executable runtime-pack matrix. This is the single source of truth for @@ -9,45 +14,6 @@ import { withLibraryRuntimeFlavors } from "../runtime-pack-common/library-matrix */ const any = (...features) => ({ any: features }); -const all = (...features) => ({ all: features }); - -const BASE_RUNTIME_SOURCES = [ - "scr_context.c", - "scr_shared.c", - "scr_number.c", - "scr_bigint.c", - "scr_string.c", - "scr_grapheme.c", - "scr_array.c", - "scr_bytes.c", - "scr_bytes_io.c", - "scr_fs_cp.c", - "scr_map.c", - "scr_closure.c", - "scr_ffi.c", - "scr_object.c", - "scr_union.c", - "scr_exception.c", - "scr_error.c", - "scr_console.c", - "scr_lib.c", - "scr_hash.c", - "scr_path.c", - "scr_os.c", - "scr_url.c", - "scr_url_params.c", - "scr_json.c", - "scr_node_builtin.c", - "scr_async.c", - "scr_crypto_async.c", - "scr_child.c", - "scr_cycle.c", - "scr_publish.c", - "scr_region.c", - "scr_itable.c", - "scr_slots.c", - "scr_alloc.c", -]; // ld64 dead-strips Mach-O symbol subsections without an ELF-style compile // flag. Keep this checked-in metadata alongside the matrix so pack build and @@ -57,107 +23,6 @@ export const EXECUTABLE_SECTION_ELIMINATION = { link_flags: ["-Wl,-dead_strip"], }; -const optional = [ - ["scr_copying.c", "copying"], - ["scr_file_handle.c", "fileHandle"], - ["scr_regex.c", "regex"], - ["scr_glob.c", "regex"], - ["scr_idna.c", "regex"], - ["scr_assert.c", any("assert", "regex", "symbol")], - ["scr_inspect.c", "inspect"], - ["scr_console_native.c", "inspect"], - ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")], - ["scr_dc.c", "dc"], - // The dynamic tier's jsval values (two-tier design I3): linked only into - // programs whose IR has dynamic-tier operations. - ["scr_js_value.c", "dynTier"], - ["scr_event_target.c", any("eventTarget", "nativeFetch")], - ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], - // The performance timeline emits its buffer warning through scr_async_dyn.c. - ["scr_perf.c", "dynAsync"], - // Blob and File box their reads as dyn promises through scr_async_dyn.c. - ["scr_blob.c", "dynAsync"], - ["scr_zlib.c", "zlib"], - ["scr_events.c", "events"], - ["scr_readline.c", "events"], - ["scr_readline_tty.c", all("events", "regex")], - ["scr_events_emitter.c", "emitter"], - ["scr_dyn_handle.c", any("emitter", "netEffective")], - ["scr_symbol.c", "symbol"], - ["scr_bigint_assert.c", all("assert", "bigint")], - ["scr_qs.c", "qs"], - // The legacy url unit lowercases and normalizes through the regex unit - // and warns through the dyn-async one. - ["scr_url_legacy.c", all("regex", "qs", "dynAsync")], - ["scr_util.c", "parseArgs"], - // Utility objects must only reference runtime families selected with them. - ["scr_util_compare.c", all("parseArgs", "symbol")], - ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], - ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], - ["scr_stream.c", "stream"], - // The zlib stream classes reference both the zlib engine and the stream unit. - ["scr_zlib_stream.c", all("zlib", "stream")], - // The Readable operators await through the dyn-async unit. - ["scr_stream_ops.c", all("stream", "dynAsync")], - // fs.ReadStream / fs.WriteStream reference only the stream unit and base units. - ["scr_fs_stream.c", "stream"], - // FileHandle streams reference both the stream unit and FileHandle. - ["scr_fs_stream_fh.c", all("stream", "fileHandle")], - ["scr_loop_kqueue.c", any("netEffective", "dgram")], - ["scr_loop_epoll.c", any("netEffective", "dgram")], - ["scr_loop_wsapoll.c", any("netEffective", "dgram")], - ["scr_net.c", "netEffective"], - ["scr_http.c", "httpEffective"], - ["scr_http2.c", "http2"], - ["scr_dgram.c", "dgram"], - ["scr_watch.c", "watch"], - ["scr_ffi_queue.c", "foreignFfi"], - ["scr_test.c", "nodeTest"], - ["scr_tls_ca.c", "tlsCaEffective"], - ["scr_tls.c", "tlsEffective"], - ["scr_cipher.c", "cipher"], - ["scr_webcrypto.c", "webcrypto"], - ["scr_fetch.c", "nativeFetch"], -]; - -function variantsFor(source) { - const variants = [{ id: "default", when: {}, defines: [] }]; - if (source === "scr_bytes.c") { - variants.push({ - id: "text-decoder-legacy", - when: { textDecoderLegacy: true }, - defines: ["SCR_TEXT_DECODER_LEGACY"], - }); - } - // Programs that assign process.emitWarning link the replacement-aware - // warning paths; every other program keeps the default object. - if (source === "scr_async_dyn.c") { - variants.push({ - id: "warning-replace", - when: { warningReplace: true }, - defines: ["SCR_WARNING_REPLACE"], - }); - } - // stdin setEncoding, string data listeners and stdio 'resize' events. - if (source === "scr_events.c") { - variants.push({ - id: "stdio-extended", - when: { stdioExtended: true }, - defines: ["SCR_STDIO_EXTENDED"], - }); - } - if (source === "scr_cycle.c") { - // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with - // nursery collections; programs built without it link the default. - variants.push({ - id: "young-nursery", - when: { youngNursery: true }, - defines: ["SCR_YIN"], - }); - } - return variants; -} - export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors( withWorkerRuntimeVariants({ schema: "scriptc.runtime-pack-matrix.v1", @@ -172,7 +37,7 @@ export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors( executable_section_elimination: EXECUTABLE_SECTION_ELIMINATION, runtime_units: [ ...BASE_RUNTIME_SOURCES.map((source) => ({ source, predicate: true })), - ...optional.map(([source, predicate]) => ({ source, predicate })), + ...OPTIONAL_RUNTIME_SOURCES.map(([source, predicate]) => ({ source, predicate })), ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })), archives: [ { id: "quickjs", predicate: false }, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index d7b4118e8..1e959eccc 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -9,7 +9,11 @@ import { withLibraryRuntimeFlavors } from "./library-matrix.mjs"; const any = (...features) => ({ any: features }); const all = (...features) => ({ all: features }); -const BASE_RUNTIME_SOURCES = [ +/* Every runtime pack matrix uses these lists, so a new runtime source is + * one line here. Their order is the link order of the units an executable + * selects: keep a new unit next to the units it references, and treat any + * reordering as a change to every executable's layout. */ +export const BASE_RUNTIME_SOURCES = [ "scr_context.c", "scr_shared.c", "scr_number.c", @@ -47,7 +51,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_alloc.c", ]; -const OPTIONAL = [ +export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_copying.c", "copying"], ["scr_file_handle.c", "fileHandle"], ["scr_regex.c", "regex"], @@ -110,7 +114,7 @@ const OPTIONAL = [ ["scr_fetch.c", "nativeFetch"], ]; -function variantsFor(source) { +export function variantsFor(source) { const variants = [{ id: "default", when: {}, defines: [] }]; if (source === "scr_bytes.c") { variants.push({ @@ -175,7 +179,7 @@ export function createRuntimePackMatrix({ (source) => ({ source, predicate: true }), ), ...extraRuntimeSources.map((source) => ({ source, predicate: true })), - ...OPTIONAL.filter(([source]) => !omitOptionalSources.includes(source)).map( + ...OPTIONAL_RUNTIME_SOURCES.filter(([source]) => !omitOptionalSources.includes(source)).map( ([source, predicate]) => ({ source, predicate }), ), ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })), diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 246d27f0a..935736612 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -935,12 +935,19 @@ static void scr_sa_trim(void) { * interrupts every running thread of the process to flush its TLB, and * glibc's trim locks the arenas other threads allocate from, so discards * wait for the last worker of a group, which hands them to one helper; - * - the helper waits SCR_SA_DISCARD_DELAY_MS (100 by default) and stops if + * - the helper waits a delay (scr_sa_discard_delay_ms: 1 s to start, + * doubled each time workers start again soon after a discard) and stops if * a worker started meanwhile: a program that starts its next workers or * exits within that time does not return pages it would fault back in or * never touch again, and the last worker of the next group hands the * discards to a new helper; a thread that finds every arena claimed * discards one waiting arena and claims it; + * - once the event loop of the thread that is not a worker runs, the + * helper then discards one arena at a time, and only while that loop + * waits with nothing ready (scr_sa_loop_waiting): a program that does + * its next work on that thread alone (a compiler rebuilding one file in + * watch mode) would otherwise have it slowed by the discards. A loop that + * does not wait gets them SCR_SA_DISCARD_DELAY_MAX_MS after the delay; * - the helper then returns the pages * it carved (MADV_DONTNEED; the start of the first page, which it may * share with older blocks, is cleared), frees the recorded system blocks, @@ -1214,12 +1221,20 @@ void scr_sa_thread_shares(void) { /* Worker threads that started and have not parked. */ static _Atomic(unsigned) scr_sa_workers_live; +#ifdef SCR_SA_TRIM +static void scr_sa_group_starting(void); +#endif + /* Library archives run on their embedder's threads and keep their memory. */ static void scr_sa_worker_starting(void) { #ifndef SCR_LIB scr_sa_worker = true; +#ifdef SCR_SA_TRIM + if (atomic_fetch_add_explicit(&scr_sa_workers_live, 1, memory_order_relaxed) == 0) scr_sa_group_starting(); +#else atomic_fetch_add_explicit(&scr_sa_workers_live, 1, memory_order_relaxed); #endif +#endif } #define SCR_SA_WORKER_STARTING() scr_sa_worker_starting() #endif @@ -1309,28 +1324,91 @@ static bool scr_sa_drain_some(bool one) { static bool scr_sa_drain(void) { return scr_sa_drain_some(false); } -/* SCR_SA_DISCARD_DELAY_MS: how long the trim helper waits after the last - * worker parks before it returns their memory (default 100). */ -static long scr_sa_discard_delay_ms(void) { - const char *delay = getenv("SCR_SA_DISCARD_DELAY_MS"); - if (!delay) return 100; - long ms = strtol(delay, NULL, 10); +/* How long the trim helper waits after the last worker parks before it + * returns their memory: SCR_SA_DISCARD_DELAY_MS (1000 by default) to start. + * Each time workers start again within ten delays of a discard, the delay + * doubles, up to SCR_SA_DISCARD_DELAY_MAX_MS (10000 by default): a program + * that keeps starting workers (a compiler rebuilding in watch mode, a + * server) would fault the returned pages straight back in. Memory that + * stays idle for the grown delay still goes back. */ +static _Atomic(long) scr_sa_delay_grown_ms; /* 0 until a discard proved too early */ +static _Atomic(long) scr_sa_discarded_ms; /* when the helper last discarded; 0: not since */ + +static long scr_sa_env_ms(const char *name, long fallback) { + const char *value = getenv(name); + if (!value) return fallback; + long ms = strtol(value, NULL, 10); return ms < 0 ? 0 : ms; } +static long scr_sa_discard_delay_ms(void) { + long start = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MS", 1000); + long max = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MAX_MS", 10000); + long grown = atomic_load_explicit(&scr_sa_delay_grown_ms, memory_order_relaxed); + long delay = grown > start ? grown : start; + return max >= start && delay > max ? max : delay; +} + +static long scr_sa_now_ms(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (long)now.tv_sec * 1000 + now.tv_nsec / 1000000 + 1; +} + +/* The first worker of a group starts: a discard shortly before means the + * delay was too short for this program. */ +static void scr_sa_group_starting(void) { + long at = atomic_exchange_explicit(&scr_sa_discarded_ms, 0, memory_order_relaxed); + if (!at) return; + long delay = scr_sa_discard_delay_ms(); + if (scr_sa_now_ms() - at >= 10 * (delay > 10 ? delay : 10)) return; + long max = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MAX_MS", 10000); + long grown = delay > 0 ? 2 * delay : 1; + if (grown > max) grown = max; + if (grown > atomic_load_explicit(&scr_sa_delay_grown_ms, memory_order_relaxed)) + atomic_store_explicit(&scr_sa_delay_grown_ms, grown, memory_order_relaxed); +} + +/* The event loop of the thread that is not a worker: an odd count while it + * waits with nothing ready, an even one while it runs, 0 until it first + * waits. Only that thread writes it. */ +static _Atomic(unsigned long) scr_sa_loop_turns; + +void scr_sa_loop_waiting(bool waiting) { + if (scr_sa_worker) return; + unsigned long turns = atomic_load_explicit(&scr_sa_loop_turns, memory_order_relaxed); + if ((bool)(turns & 1) != waiting) atomic_store_explicit(&scr_sa_loop_turns, turns + 1, memory_order_relaxed); +} + /* Waits out the delay first, and stops if a worker starts meanwhile: the * discards then wait for the last worker of that group. A program that exits * within the delay does not spend its last milliseconds returning pages, and * one that starts its next workers within it does not return pages while - * they start. */ + * they start. Once the event loop runs, each discard then waits until the + * loop has been in one wait for a step, or until the maximum delay has + * passed. */ static void *scr_sa_drain_main(void *arg) { (void)arg; - for (long left = scr_sa_discard_delay_ms(); left > 0; left -= 5) { - struct timespec pause = {0, (left < 5 ? left : 5) * 1000000L}; + long delay = scr_sa_discard_delay_ms(), step = delay > 200 ? 20 : 5; + for (long left = delay; left > 0; left -= step) { + struct timespec pause = {0, (left < step ? left : step) * 1000000L}; nanosleep(&pause, NULL); if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) != 0) return NULL; } - if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) == 0) scr_sa_drain(); + long max = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MAX_MS", 10000); + unsigned long seen = 0; + for (long waited = 0; atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) == 0;) { + unsigned long turns = atomic_load_explicit(&scr_sa_loop_turns, memory_order_relaxed); + if (turns == 0 || ((turns & 1) && turns == seen) || waited >= max) { + if (!scr_sa_drain_some(true)) break; + atomic_store_explicit(&scr_sa_discarded_ms, scr_sa_now_ms(), memory_order_relaxed); + continue; + } + seen = turns; + struct timespec pause = {0, step * 1000000L}; + nanosleep(&pause, NULL); + waited += step; + } return NULL; } @@ -1361,6 +1439,7 @@ void scr_sa_wait_trims(void) { #else void scr_sa_wait_trims(void) {} void scr_sa_thread_shares(void) {} +void scr_sa_loop_waiting(bool waiting) { (void)waiting; } #endif static void scr_sa_park_arena(void); @@ -1564,6 +1643,7 @@ void *scr_mem_realloc(void *p, size_t n) { return realloc(p, n); } void scr_sa_thread_park(void) {} void scr_sa_wait_trims(void) {} void scr_sa_thread_shares(void) {} +void scr_sa_loop_waiting(bool waiting) { (void)waiting; } void scr_sa_free_foreign(void *p) { free(p); } #endif #endif diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index aa6b0b60b..6c83afdf9 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -700,11 +700,91 @@ static void check_discard_delay(void) { assert(pthread_join(holder, NULL) == 0); assert(pthread_join(plain, NULL) == 0); scr_sa_wait_trims(); - unsetenv("SCR_SA_DISCARD_DELAY_MS"); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); assert(resident_pages(first, discard_hi) < total / 2); discard_in_old_arena = true; } +/* A discard that workers follow within ten delays was too early: the next + * delay is twice as long, and memory idle for that long still goes back. */ +static void *plain_discard_worker(void *arg) { + (void)arg; + scr_sa_thread_starting(); + allocate_discardable(); + scr_sa_thread_park(); + return NULL; +} + +static void check_discard_delay_grows(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "10000", 1); + discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); + assert(discard_blocks); + /* A plain thread holds the arena whose slice the earlier checks filled. */ + atomic_store(&discard_step, 0); + pthread_t plain; + assert(pthread_create(&plain, NULL, holding_thread, NULL) == 0); + wait_step(1); + run(plain_discard_worker); /* returned after the delay */ + pthread_t t; + assert(pthread_create(&t, NULL, plain_discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + struct timespec pause = {0, 140000000L}; + nanosleep(&pause, NULL); + assert(resident_pages(first, discard_hi) > total / 2); /* the delay is 200 ms or more now */ + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); + atomic_store(&discard_step, 3); + assert(pthread_join(plain, NULL) == 0); + free(discard_blocks); +} + +/* Once the event loop runs, discards wait for it to wait with nothing + * ready; a loop that does not wait gets them SCR_SA_DISCARD_DELAY_MAX_MS + * after the delay. The test's thread plays the loop's. */ +static void check_discard_waits_for_loop(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "10000", 1); + discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); + assert(discard_blocks); + atomic_store(&discard_step, 0); + pthread_t plain; + assert(pthread_create(&plain, NULL, holding_thread, NULL) == 0); + wait_step(1); + scr_sa_loop_waiting(true); + scr_sa_loop_waiting(false); /* the loop runs */ + pthread_t t; + assert(pthread_create(&t, NULL, plain_discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + struct timespec pause = {0, 900000000L}; + nanosleep(&pause, NULL); + assert(resident_pages(first, discard_hi) > total / 2); /* past the delay */ + scr_sa_loop_waiting(true); + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + /* A loop that keeps running. */ + scr_sa_loop_waiting(false); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "300", 1); + assert(pthread_create(&t, NULL, plain_discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + total = (discard_hi - first) / page; + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); + atomic_store(&discard_step, 3); + assert(pthread_join(plain, NULL) == 0); + free(discard_blocks); +} + static void check_discard(void) { discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); assert(discard_blocks); @@ -819,10 +899,15 @@ int main(void) { check_heap_huge_pages(); #endif #if defined(SCR_WORKERS) && SCR_SMALL_ALLOC + /* A short discard delay, which stays at its start until check_discard_delay_grows. */ + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); check_threads(); #ifdef __linux__ check_trim(); check_discard(); + check_discard_delay_grows(); + check_discard_waits_for_loop(); #endif printf("allocator checks passed: size-class workers\n"); #else diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 88ad0f7bd..8185422af 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -56,24 +56,6 @@ #include #endif -#ifdef __wasi__ -/* WASI Preview 1 has no process-spawn capability. The child unit is not - * linked for this target, but the generic loop keeps its quiescence hooks; - * their empty implementation makes the absence explicit at link time - * without weakening async/timer support. */ -bool scr_children_pending(void) { return false; } -bool scr_children_ready(void) { return false; } -bool scr_children_reffed_pending(void) { return false; } -bool scr_children_failed_pending(void) { return false; } -void scr_children_poll(void) {} -int scr_children_wake_fd(void) { return -1; } -bool scr_children_wait(double max_wait_ms) { - (void)max_wait_ms; - return false; -} -void scr_children_teardown(void) {} -#endif - #if defined(__has_feature) #if __has_feature(address_sanitizer) #define SCR_ASAN_FIBERS 1 @@ -4072,6 +4054,13 @@ void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pol scr_watch_pollfd_fn = pollfd; } +/* The child-process hook (scr_child.c, installed with the first spawned + * child or IPC channel). Programs that never create one keep it NULL and + * do not link the child unit; WASI never links it. */ +static SCR_TL const ScrLoopChildren *scr_loop_children = NULL; + +void scr_loop_set_children(const ScrLoopChildren *hooks) { scr_loop_children = hooks; } + /* The foreign-FFI queue hook (scr_ffi.c when a format-5 descriptor exists). * Its pending count includes live registrations (reffed by default) and * queued deliveries; dispatch runs exactly one callback per loop turn. */ @@ -4326,12 +4315,13 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * death landing before this sweep fired its 'exit' where Node * deterministically never does. The teardown below releases the * undelivered listeners, so the RC audit stays clean. */ - if (scr_children_pending()) { + if (scr_loop_children != NULL && scr_loop_children->pending()) { bool held = - scr_reffed_timers > 0 || scr_reffed_immediates > 0 || scr_children_reffed_pending() || + scr_reffed_timers > 0 || scr_reffed_immediates > 0 || + scr_loop_children->reffed_pending() || /* spawn failures settle regardless: their 'error' is a * next-tick event, delivered even on an unref'd child */ - scr_children_failed_pending() || + scr_loop_children->failed_pending() || (scr_events_pending_fn != NULL && scr_events_pending_fn()) || (scr_net_pending_fn != NULL && scr_net_pending_fn()) || (scr_dgram_pending_fn != NULL && scr_dgram_pending_fn()) || @@ -4340,7 +4330,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { scr_fs_renames_pending() || scr_crypto_bytes_pending() || scr_loop_deferred_pending() || scr_workers_pending(); if (held) { - scr_children_poll(); + scr_loop_children->poll(); if (scr_exc_pending()) return false; /* uncaught throw in a listener */ if (scr_ready_len > 0 || scr_nt_head != NULL) continue; } @@ -4351,7 +4341,8 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * nextTicks enqueued by a station listener are the same story. */ if (scr_stream_pending_fn != NULL && scr_stream_pending_fn()) continue; if (scr_nt_head != NULL) continue; - bool kids = scr_children_pending(); + bool kids = scr_loop_children != NULL && scr_loop_children->pending(); + bool kids_reffed = scr_loop_children != NULL && scr_loop_children->reffed_pending(); bool events = scr_events_pending_fn != NULL && scr_events_pending_fn(); bool net = scr_net_pending_fn != NULL && scr_net_pending_fn(); /* `net` keeps the process alive; `net_io` (unref'd handles too) decides @@ -4369,7 +4360,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * Children follow the same rule: an unref'd child is still REAPED * while the loop runs (kids drives the sweeps and sleeps above) but * only reffed ones keep the process alive. */ - if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !events && !net && !dgram && !watch && !ffi && !renames && !crypto && !workers) break; + if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !kids_reffed && !events && !net && !dgram && !watch && !ffi && !renames && !crypto && !workers) break; /* Sleep to the earliest deadline, then run every due timer (each may * enqueue microtasks, which the next iteration drains first). Who * sleeps depends on what is pending: @@ -4413,12 +4404,15 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * station has run. Completed sends and disconnects can also become ready * during that turn. No fd will wake us for this userspace work: return to * dispatch without sleeping, still allowing due timers and immediates. */ - if (scr_children_ready()) due = now; + if (scr_loop_children != NULL && scr_loop_children->ready()) due = now; #ifdef SCR_WORKERS if (scr_workers_ready_fn && scr_workers_ready_fn()) due = now; #endif #if !defined(_WIN32) && !defined(__wasi__) if (due > now) scr_fiber_stacks_clear(); +#endif +#ifdef SCR_WORKERS + if (due > now) scr_sa_loop_waiting(true); #endif bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn(); #ifdef SCR_WORKERS @@ -4522,7 +4516,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { } } if (kids) { - int cfd = scr_children_wake_fd(); + int cfd = scr_loop_children->wake_fd(); if (cfd >= 0) { fds[nfds].fd = cfd; fds[nfds].events = POLLIN; @@ -4553,10 +4547,10 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * unconsumed event keeps the kqueue fd readable and would turn the * next poll into a busy spin while other children still run. The * WNOHANG sweep at the next turn's top stays the reaper. */ - if (kids) (void)scr_children_wait(0); + if (kids) (void)scr_loop_children->wait(0); now = scr_now_ms(); #endif /* !_WIN32 && !__wasi__ */ - } else if (kids && scr_children_wait(due > now ? due - now : 0)) { + } else if (kids && scr_loop_children->wait(due > now ? due - now : 0)) { now = scr_now_ms(); /* woke early on an exit, or hit the deadline */ } else { if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS; @@ -4573,6 +4567,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { now = due; } } +#ifdef SCR_WORKERS + scr_sa_loop_waiting(false); +#endif while (scr_ntimers > 0 && scr_timers[0].deadline_ms <= now) { ScrTimer t = scr_timer_pop(); /* Timer callbacks are plain sync closures, run on the main stack. @@ -4710,7 +4707,7 @@ bool scr_loop_run(ScrPromise *top_level) { /* Unref'd children the loop never reaped: release the * registry's references (their listeners never fire — the process is * exiting, Node's behavior; the OS reparents the children). */ - scr_children_teardown(); + if (scr_loop_children != NULL) scr_loop_children->teardown(); scr_fibers_abandoned = scr_fibers_live; scr_note_abandoned_fibers(scr_fibers_abandoned); return rejection_failed; diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 87ead5755..ec685656d 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -33,6 +33,7 @@ static void scr_ipc_deliver(void); static void scr_ipc_settle(ScrIpc *ipc); static void scr_child_fire_spawn(ScrChild *c); static void scr_child_drop_spawn_listeners(ScrChild *c); +static void scr_child_loop_install(void); static ScrArr *scr_fork_argv(double target, uintptr_t read_handle, uintptr_t write_handle, ScrArr *args) { @@ -2372,6 +2373,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, scr_children_reffed_n++; c->next = scr_children; scr_children = scr_child_retain(c); + scr_child_loop_install(); return c; } @@ -5081,6 +5083,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, scr_children_reffed_n++; c->next = scr_children; scr_children = scr_child_retain(c); + scr_child_loop_install(); return c; } @@ -6087,6 +6090,7 @@ static ScrIpc *scr_ipc_new(ScrChildStream *reader, ScrChildWriter *writer) { ipc->registered = true; ipc->next = scr_ipcs; scr_ipcs = scr_ipc_retain(ipc); /* service-registry reference */ + scr_child_loop_install(); ipc->writer_error_cb = scr_ipc_closure(ipc, (void *)&scr_ipc_writer_error); scr_child_writer_on_error(writer, ipc->writer_error_cb, &scr_ipc_writer_error, false); @@ -6406,6 +6410,7 @@ void scr_child_ipc_on_disconnect(ScrChild *c, ScrClosure *cb, bool once) { bool scr_process_ipc_connected(void) { return scr_process_ipc != NULL && scr_process_ipc->connected; } +bool scr_process_ipc_open(void) { return scr_process_ipc != NULL; } bool scr_process_ipc_send(ScrStr *json) { return scr_ipc_send(scr_process_ipc, json, NULL, NULL); } @@ -6458,7 +6463,7 @@ static ScrDyn *scr_child_bridge_readable(ScrChildStream *reader, ScrDyn **cached return NULL; } if (!*cached) { - ScrStream *stream = scr_child_stream_ops->readable(); + ScrStream *stream = scr_child_stream_ops->readable("Socket"); scr_child_stream_on_data(reader, scr_child_bridge_callback(stream, (void *)scr_child_bridge_data), scr_child_bridge_data, false); scr_child_stream_on_end(reader, scr_child_bridge_callback(stream, (void *)scr_child_bridge_end), false); @@ -6578,7 +6583,7 @@ static ScrDyn *scr_child_bridge_writable(ScrChildWriter *writer, ScrDyn **cached return NULL; } if (!*cached) { - ScrStream *stream = scr_child_stream_ops->writable( + ScrStream *stream = scr_child_stream_ops->writable("Socket", scr_child_writer_bridge_callback(writer, (void *)scr_child_bridge_write), scr_child_bridge_write, scr_child_writer_bridge_callback(writer, (void *)scr_child_bridge_final), scr_child_bridge_final); *cached = scr_child_writable_box(stream); @@ -6755,6 +6760,26 @@ void scr_child_dyn_install(void) { scr_dyn_handle_install(SCR_DYNH_CHILD, &scr_child_dyn_ops); } +static const ScrLoopChildren scr_child_loop_hooks = { + &scr_children_pending, + &scr_children_ready, + &scr_children_reffed_pending, + &scr_children_failed_pending, + &scr_children_poll, + &scr_children_wait, + &scr_children_wake_fd, + &scr_children_teardown, +}; + +/* Points the loop at the child unit (once per thread, like the + * registries) when the first child or IPC channel is created. */ +static void scr_child_loop_install(void) { + static SCR_TL bool installed = false; + if (installed) return; + installed = true; + scr_loop_set_children(&scr_child_loop_hooks); +} + void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener) { scr_child_dyn_install(); ScrDyn *self = scr_dyn_new_handle(child, SCR_DYNH_CHILD); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index 1187985c8..f59e02e74 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -182,6 +182,9 @@ void scr_child_ipc_on_disconnect(ScrChild *c, ScrClosure *cb /*moves*/, bool once); double scr_process_fork_target(double target_count); bool scr_process_ipc_connected(void); +/* Whether this fork child opened its channel (process.send's presence in + * a program with fork targets; disconnecting keeps it). */ +bool scr_process_ipc_open(void); bool scr_process_ipc_send(ScrStr *json); bool scr_process_ipc_send_cb(ScrStr *json, ScrClosure *cb /*moves*/, ScrIpcSendFn fn); @@ -253,9 +256,13 @@ void scr_child_dyn_install(void); void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener); /* borrowed */ /* Optional native-stream bridge. The table avoids a stream dependency * in programs that only use the child-process surface. */ +/* readable/writable take the stream's class name (the constructor name + * Node reports: Socket for child stdio, ReadableWorkerStdio and + * WritableWorkerStdio for worker stdio). */ typedef struct ScrChildStreamOps { - ScrStream *(*readable)(void); - ScrStream *(*writable)(ScrClosure *, ScrStreamChunkInv, ScrClosure *, ScrStreamPlainInv); + ScrStream *(*readable)(const char *cls); + ScrStream *(*writable)(const char *cls, ScrClosure *, ScrStreamChunkInv, ScrClosure *, + ScrStreamPlainInv); void *(*retain)(void *); void (*release)(void *); ScrTraceFn trace; @@ -292,5 +299,19 @@ bool scr_children_ready(void); bool scr_children_failed_pending(void); void scr_children_poll(void); bool scr_children_wait(double max_wait_ms); +/* The loop reaches the functions above through this table. The child + * unit installs it with the first spawned child or IPC channel, so + * programs that never create either do not link the unit. */ +typedef struct ScrLoopChildren { + bool (*pending)(void); + bool (*ready)(void); + bool (*reffed_pending)(void); + bool (*failed_pending)(void); + void (*poll)(void); + bool (*wait)(double max_wait_ms); + int (*wake_fd)(void); + void (*teardown)(void); +} ScrLoopChildren; +void scr_loop_set_children(const ScrLoopChildren *hooks); #endif /* SCR_CHILD_H */ diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 4b6b6a6a6..d71f46b13 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -71,6 +71,7 @@ void scr_crypto_hash_stream_set(ScrCryptoHash *hash, void *stream, void (*releas void scr_crypto_hash_stream_detach(void *hash); void scr_crypto_hash_stream_close(ScrCryptoHash *hash); bool scr_crypto_hash_stream_usable(const ScrCryptoHash *hash); +bool scr_crypto_hash_is_hmac(const ScrCryptoHash *hash); void scr_crypto_hash_stream_hold(ScrCryptoHash *hash); void scr_crypto_hash_stream_unhold(ScrCryptoHash *hash); ScrStream *scr_crypto_hash_stream_piped(ScrCryptoHash *hash); diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index d700e6ba8..fd5d64c81 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -121,7 +121,9 @@ * mature the same two rules one level down: a growth fraction * measured from the last mature-or-full pass and scheduled * by everything mature passes free, and a backlog rule over - * the mature buffer. + * the mature buffer. Until a mature pass has had + * candidates, the backlog threshold widens with the growth + * fraction (scr_cyc_backlog_threshold). * scheduled age a mature or old candidate has waited through a nursery's * worth of event-loop checkpoints. Candidate COUNT cannot * bound the garbage behind one root, and an idle heap does @@ -217,6 +219,7 @@ typedef struct { unsigned char growth_shift[SCR_CYC_NGENS]; unsigned char growth_streak[SCR_CYC_NGENS]; unsigned backlog_shift[SCR_CYC_NGENS]; + bool backlog_judged[SCR_CYC_NGENS]; size_t fallen; size_t live_seen; size_t full_after; @@ -256,6 +259,7 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD}; #define scr_cyc_growth_shift (scr_cyc_thread.growth_shift) #define scr_cyc_growth_streak (scr_cyc_thread.growth_streak) #define scr_cyc_backlog_shift (scr_cyc_thread.backlog_shift) +#define scr_cyc_backlog_judged (scr_cyc_thread.backlog_judged) #define scr_cyc_trigger (scr_cyc_thread.trigger) #define scr_cyc_alloc_trigger (scr_cyc_thread.alloc_trigger) #define scr_gray_referenced (scr_cyc_thread.gray_referenced) @@ -535,11 +539,13 @@ static unsigned scr_cyc_growth_max_shift(void) { } /* Doublings of each generation's backlog threshold earned by consecutive - * passes that found almost nothing to free (see scr_cyc_backlog_threshold). + * passes that found almost nothing to free (see scr_cyc_backlog_threshold), + * and whether a pass at that level has had candidates to judge them by. * Only the mature and old entries are used. */ #define SCR_CYC_BACKLOG_MAX_SHIFT 8 #ifndef SCR_WORKERS static SCR_TL unsigned scr_cyc_backlog_shift[SCR_CYC_NGENS]; +static SCR_TL bool scr_cyc_backlog_judged[SCR_CYC_NGENS]; #endif /* Mature objects in the last full pass's dead set (scr_cyc_adapt_growth). */ @@ -684,11 +690,21 @@ static SCR_CYC_SCHEDULING bool scr_cyc_grown(unsigned gen) { * this trigger rests, leaving heap growth, the scheduled age and exit to * collect. That only delays garbage made from existing data: replacing it * requires allocation, which grows the heap and trips a growth trigger. The - * first productive pass at that level restores the original fraction. */ + * first productive pass at that level restores the original fraction. + * + * Only passes that had candidates adapt the threshold. Until one has, the + * level uses its growth fraction's doublings instead (scr_cyc_adapt_growth), + * which every growth-triggered pass at the level adapts, candidates or not. + * A program can keep a level's buffer empty while its heap grows, so that + * growth passes there find nothing and widen, and then fill the buffer with + * live objects (a structure it builds and reads as it builds it); at the + * base threshold, the first backlog pass would walk a quarter of the live + * heap to find nothing. */ static size_t scr_cyc_backlog_threshold(unsigned gen) { size_t t = scr_cyc_live / SCR_CYC_GROWTH_DIV; if (t < SCR_CYC_NURSERY_CANDIDATES) t = SCR_CYC_NURSERY_CANDIDATES; - unsigned shift = scr_cyc_backlog_shift[gen]; + unsigned shift = + scr_cyc_backlog_judged[gen] ? scr_cyc_backlog_shift[gen] : scr_cyc_growth_shift[gen]; return t > (SIZE_MAX >> shift) ? SIZE_MAX : t << shift; } @@ -788,6 +804,7 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow size_t old_reclaimed = scr_cyc_old_freed - old_freed_before + scr_cyc_mature_dead; if (gen == SCR_CYC_NURSERY) return; if (scr_cands.n != 0) { + scr_cyc_backlog_judged[gen] = true; if (freed * SCR_CYC_GROWTH_DIV >= scr_cands.n) scr_cyc_backlog_shift[gen] = 0; else if (scr_cyc_backlog_shift[gen] < SCR_CYC_BACKLOG_MAX_SHIFT) @@ -2088,6 +2105,13 @@ size_t scr_cyc_pass_at(unsigned gen) { return scr_cyc_pass(gen); } /* Tests only: route appended candidates of bypassing types, as the next * scheduling decision or pass would. */ void scr_cyc_route_bypassed_at(void) { scr_cyc_route_bypassed(); } +/* Tests only: the backlog threshold at `gen`, and forgetting what passes + * there have taught it, as on a thread that has not collected yet. */ +size_t scr_cyc_backlog_threshold_at(unsigned gen) { return scr_cyc_backlog_threshold(gen); } +void scr_cyc_forget_backlog_at(unsigned gen) { + scr_cyc_backlog_shift[gen] = 0; + scr_cyc_backlog_judged[gen] = false; +} #endif size_t scr_cyc_collect_set(const ScrCycSet *set, unsigned gen) { diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a3d506a10..6fdbbb408 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -937,7 +937,17 @@ double scr_dyn_arr_len(const ScrDyn *d); ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i); ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key); void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value); +void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value); ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor); +void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits); +void scr_dyn_class_view_attributes(ScrDyn *view, double bits); +ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name, + ScrDyn *descriptor); +double scr_class_defined_bits(ScrDyn *holder, ScrStr *name); +ScrDyn *scr_class_defined_value(ScrDyn *holder, ScrStr *name); +bool scr_class_descriptor_has_value(ScrDyn *descriptor); +void scr_class_throw_read_only(ScrStr *field, ScrStr *owner); +void scr_class_throw_not_extensible(ScrStr *field); ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key); ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args); ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 8a1bfa728..90e8d0363 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1558,6 +1558,17 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } + /* The typed array methods below are not DataView's; it inherits + * Object.prototype's. */ + if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { + ScrStr *tag = scr_str_new("[object DataView]", 17); + ScrDyn *out = scr_dyn_new_str(tag); + scr_str_release(tag); + return out; + } + if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + dyn_throw_not_fn(what); + return NULL; } size_t blen = bytes->len; if (dyn_name_is(method, "at")) { diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 6f616c35a..4f514681b 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -354,6 +354,12 @@ ScrStr *scr_error_stack(ScrError *e) { return scr_str_retain(e->stack); } +void scr_error_set_stack(ScrError *e, ScrStr *stack) { + ScrStr *replacement = scr_str_retain(stack); + scr_str_release(e->stack); + e->stack = replacement; +} + bool scr_error_is(const void *obj) { const ScrVt *vt = ((const ScrError *)obj)->vt; return scr_error_vts[SCR_ERR_ERROR].pre <= vt->pre && diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index ce60dac23..453ebf3e1 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -252,6 +252,9 @@ void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *)); void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)); ScrStr *scr_error_default_name(const ScrError *e); ScrStr *scr_error_stack(ScrError *e); +/* Replaces the error's stack string, as an assignment to `stack` does. + * Borrowed operands. */ +void scr_error_set_stack(ScrError *e, ScrStr *stack); ScrError *scr_error_retain(ScrError *e); void scr_error_release(ScrError *e); /* dispatches through e->vt */ diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index ceecb15e3..e0e305bde 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -324,6 +324,13 @@ ScrEmitter *scr_emitter_new(void) { return em; } +/* x.constructor.name of an emitter, a stream, or an instance of an emitted + * subclass: the class name every allocation stamps (EventEmitter, + * Readable, ReadStream, Gzip, Socket, the program class's name). +1. */ +ScrStr *scr_emitter_class_name(const ScrEmitter *em) { + return scr_str_new(em->cls, strlen(em->cls)); +} + /* super() into the emitter prefix of an emitted subclass: the allocation * already zeroed reg and stamped the concrete class's display name, so * construction has nothing left to do — the call exists so the super() diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index 99da57a50..a92ae1864 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -38,6 +38,7 @@ extern SCR_TL ScrVt scr_emitter_vt; /* main() stamps pre/post */ extern SCR_TL double scr_emitter_default_max; ScrEmitter *scr_emitter_new(void); /* +1, collector-headered */ +ScrStr *scr_emitter_class_name(const ScrEmitter *em); /* +1 */ /* Internal owner hook; observes listener presence without running script. */ void scr_emitter_observe(ScrEmitter *em, void (*observe)(void *, const ScrStr *, bool), void *context); diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 97bf4ebaa..70a5ab075 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -562,6 +562,7 @@ static ScrStream *scr_fss_new(const ScrDyn *path, const ScrDyn *opts_in, bool re scr_dyn_release(eo); } if (!s) goto fail; + s->cls = readable ? "ReadStream" : "WriteStream"; bool unknown_encoding = false; if (!readable) { /* setDefaultEncoding(options.encoding) after the Writable constructor: diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 3194528d3..3903cabeb 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -823,6 +823,41 @@ static ScrStr *scr_insp_array_buffer(const ScrBytes *h, const char *cls, double return out; } +/* DataView: formatValue's `DataView { [byteLength], [byteOffset], + * [buffer] }`, the buffer rendered one level deeper. `[DataView]` beyond + * depth. */ +static ScrStr *scr_insp_data_view(const ScrBytes *b, double recurse, double depth) { + if (recurse > depth) return scr_str_new("[DataView]", 10); + const ScrBytes *owner = b->backing ? b->backing : b; + char line[64]; + scr_insp_begin(recurse + 1); + int n = snprintf(line, sizeof line, "[byteLength]: %.0f", scr_bytes_byte_len(b)); + ScrStr *entry = scr_str_new(line, (size_t)n); + scr_insp_entry(entry, false); + scr_str_release(entry); + n = snprintf(line, sizeof line, "[byteOffset]: %.0f", scr_bytes_byte_offset(b)); + entry = scr_str_new(line, (size_t)n); + scr_insp_entry(entry, false); + scr_str_release(entry); + ScrStr *buffer = scr_insp_array_buffer(owner, owner->shared ? "SharedArrayBuffer" : "ArrayBuffer", + recurse + 1, depth); + InspBuf eb = {0}; + ib_cstr(&eb, "[buffer]: "); + ib_bytes(&eb, buffer->data, buffer->len); + scr_str_release(buffer); + entry = ib_take(&eb); + scr_insp_entry(entry, false); + scr_str_release(entry); + ScrStr *base = scr_str_new("", 0); + ScrStr *b0 = scr_str_new("DataView {", 10); + ScrStr *b1 = scr_str_new("}", 1); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, false, false); + scr_str_release(base); + scr_str_release(b0); + scr_str_release(b1); + return out; +} + static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth); /* A runtime error through its node: the cached one, or one built from its @@ -1362,20 +1397,35 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { case SCR_DYN_STR: return scr_insp_str(d->v.str); case SCR_DYN_ARR: { - if (d->v.arr.len == 0) return scr_str_new("[]", 2); + size_t len = d->v.arr.len; + if (len == 0) return scr_str_new("[]", 2); if (recurse > depth) return scr_str_new("[Array]", 7); scr_insp_begin(recurse + 1); - size_t shown = d->v.arr.len < 100 ? d->v.arr.len : 100; - for (size_t i = 0; i < shown; i++) { - ScrDyn *item = d->v.arr.items[i]; + /* Node's maxArrayLength (100) counts output entries; a run of holes + * is one `` entry (formatSpecialArray). */ + const unsigned char *presence = d->v.arr.presence; + size_t i = 0; + for (size_t shown = 0; i < len && shown < 100; shown++) { + if (presence && !presence[i]) { + size_t next = i + 1; + while (next < len && !presence[next]) next++; + char hole[48]; + int hn = snprintf(hole, sizeof hole, "<%zu empty item%s>", next - i, next - i == 1 ? "" : "s"); + ScrStr *s = scr_str_new(hole, (size_t)hn); + scr_insp_entry(s, false); + scr_str_release(s); + i = next; + continue; + } + ScrDyn *item = d->v.arr.items[i++]; ScrStr *s = scr_insp_dyn(item, recurse + 1, depth); scr_insp_entry(s, item->kind == SCR_DYN_NUM); scr_str_release(s); } - bool more = d->v.arr.len > 100; + bool more = i < len; if (more) { - ScrStr *m = scr_insp_more_items((double)(d->v.arr.len - 100)); - scr_insp_entry(m, d->v.arr.items[100]->kind == SCR_DYN_NUM); + ScrStr *m = scr_insp_more_items((double)(len - i)); + scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM); scr_str_release(m); } ScrStr *base = scr_str_new("", 0); @@ -1527,6 +1577,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_BYTES: { ScrBytes *b = d->v.bytes; + if (b->is_data_view) return scr_insp_data_view(b, recurse, depth); if (d->buffer) { /* Buffer-flavored (a stream chunk): Node's form. */ return scr_insp_buffer(b); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 83b56bea4..c98816c4b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1585,8 +1585,12 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn } if (obj->non_extensible) { scr_dyn_release(value); - static const char message[] = "Cannot add property, object is not extensible"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot add property "); + scr_jb_write(&buffer, key, key_len); + scr_jb_puts(&buffer, ", object is not extensible"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return; } if (obj->v.obj.len == obj->v.obj.cap) { @@ -1922,9 +1926,40 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return scr_dyn_retain(object); } +/* A class instance's integrity state lives in its native layout, which only + * statically typed calls can reach (the compiler adds the state to the + * hierarchies they name). Through an untyped value the operation throws. */ +static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *operation) { + if (object->kind != SCR_DYN_TYPED_REF) return false; + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, operation); + scr_jb_puts(&buffer, " of a class instance held in an untyped value is not supported yet"); + ScrStr *message = scr_jb_finish(&buffer); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return true; +} + +/* Integrity queries on a class instance answer from its checked view, which + * carries the instance's attributes (scr_dyn_class_view_attributes). */ +static bool scr_dyn_typed_ref_view_answer(ScrDyn *object, bool (*query)(const ScrDyn *), + bool *answer) { + if (object->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + *answer = scr_exc_pending() ? false : query(view); + scr_dyn_release(view); + return true; +} + +static bool scr_dyn_is_extensible_view(const ScrDyn *view) { + return (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR) && !view->non_extensible; +} + ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } + if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; if (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) { object->non_extensible = true; if (object->kind == SCR_DYN_ARR && object->v.arr.properties) @@ -1936,12 +1971,15 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { bool scr_dyn_is_extensible(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return false; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; } - return (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) && !object->non_extensible; + bool answer; + if (scr_dyn_typed_ref_view_answer(object, scr_dyn_is_extensible_view, &answer)) return answer; + return scr_dyn_is_extensible_view(object); } ScrDyn *scr_dyn_seal(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } + if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); if (object->kind == SCR_DYN_OBJ) { object->non_extensible = true; @@ -1958,6 +1996,8 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) { bool scr_dyn_is_sealed(const ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return false; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; } + bool answer; + if (scr_dyn_typed_ref_view_answer((ScrDyn *)object, scr_dyn_is_sealed, &answer)) return answer; if (object->symbol_properties && !scr_dyn_is_sealed(object->symbol_properties)) return false; if (object->kind == SCR_DYN_ARR) return object->non_extensible && (object->v.arr.len == 0 || object->v.arr.sealed) && @@ -2122,6 +2162,70 @@ static bool scr_dyn_array_proto_name(const ScrStr *key) { return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); } +/* Own string keys of %TypedArray%.prototype, plus BYTES_PER_ELEMENT from + * each typed array's own prototype. */ +static bool scr_dyn_typed_array_proto_name(const ScrStr *key) { + static const char *const names[] = { + "constructor", "buffer", "byteLength", "byteOffset", "length", "entries", "keys", "values", + "at", "copyWithin", "every", "fill", "filter", "find", "findIndex", "findLast", + "findLastIndex", "forEach", "includes", "indexOf", "join", "lastIndexOf", "map", "reverse", + "reduce", "reduceRight", "set", "slice", "some", "sort", "subarray", "toReversed", "toSorted", + "with", "toLocaleString", "toString", "BYTES_PER_ELEMENT" + }; + return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); +} + +/* Own string keys of Buffer.prototype that %TypedArray%.prototype lacks. */ +static bool scr_dyn_buffer_proto_name(const ScrStr *key) { + static const char *const names[] = { + "readBigUInt64LE", "readBigUInt64BE", "readBigUint64LE", "readBigUint64BE", "readBigInt64LE", + "readBigInt64BE", "writeBigUInt64LE", "writeBigUInt64BE", "writeBigUint64LE", + "writeBigUint64BE", "writeBigInt64LE", "writeBigInt64BE", "readUIntLE", "readUInt32LE", + "readUInt16LE", "readUInt8", "readUIntBE", "readUInt32BE", "readUInt16BE", "readUintLE", + "readUint32LE", "readUint16LE", "readUint8", "readUintBE", "readUint32BE", "readUint16BE", + "readIntLE", "readInt32LE", "readInt16LE", "readInt8", "readIntBE", "readInt32BE", + "readInt16BE", "writeUIntLE", "writeUInt32LE", "writeUInt16LE", "writeUInt8", "writeUIntBE", + "writeUInt32BE", "writeUInt16BE", "writeUintLE", "writeUint32LE", "writeUint16LE", + "writeUint8", "writeUintBE", "writeUint32BE", "writeUint16BE", "writeIntLE", "writeInt32LE", + "writeInt16LE", "writeInt8", "writeIntBE", "writeInt32BE", "writeInt16BE", "readFloatLE", + "readFloatBE", "readDoubleLE", "readDoubleBE", "writeFloatLE", "writeFloatBE", "writeDoubleLE", + "writeDoubleBE", "asciiSlice", "base64Slice", "base64urlSlice", "latin1Slice", "hexSlice", + "ucs2Slice", "utf8Slice", "asciiWrite", "base64Write", "base64urlWrite", "latin1Write", + "hexWrite", "ucs2Write", "utf8Write", "parent", "offset", "copy", "equals", "inspect", + "compare", "write", "toJSON", "swap16", "swap32", "swap64" + }; + return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); +} + +/* Own string keys of DataView.prototype. */ +static bool scr_dyn_data_view_proto_name(const ScrStr *key) { + static const char *const names[] = { + "constructor", "buffer", "byteLength", "byteOffset", "getInt8", "setInt8", "getUint8", + "setUint8", "getInt16", "setInt16", "getUint16", "setUint16", "getInt32", "setInt32", + "getUint32", "setUint32", "getFloat32", "setFloat32", "getFloat64", "setFloat64", + "getBigInt64", "setBigInt64", "getBigUint64", "setBigUint64", "getFloat16", "setFloat16" + }; + return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); +} + +/* `in` on a typed array, Buffer or DataView. On a typed array, a canonical + * numeric key (one that ToString(ToNumber(key)) reproduces, or "-0") + * answers by bounds alone: an integral index, not -0, below the length. + * Other keys name members of %TypedArray%.prototype, Buffer.prototype for a + * Buffer, or Object.prototype. A DataView has no indexed elements: its keys + * name members of DataView.prototype or Object.prototype. */ +static bool scr_dyn_bytes_in(const ScrDyn *v, const ScrStr *key) { + if (v->v.bytes->is_data_view) return scr_dyn_data_view_proto_name(key) || scr_dyn_object_proto_name(key); + if (key->len == 2 && memcmp(key->data, "-0", 2) == 0) return false; + double index = scr_string_to_number(key); + ScrStr *canonical = scr_f64_to_scrstr(index); + bool numeric = canonical->len == key->len && memcmp(canonical->data, key->data, key->len) == 0; + scr_str_release(canonical); + if (numeric) return index >= 0 && index == floor(index) && index < (double)v->v.bytes->len; + return scr_dyn_typed_array_proto_name(key) || (v->buffer && scr_dyn_buffer_proto_name(key)) || + scr_dyn_object_proto_name(key); +} + static bool scr_dyn_object_proto_key(const ScrDyn *target, const ScrStr *key) { return !target->null_proto && scr_dyn_object_proto_name(key); } @@ -3456,10 +3560,19 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS /* Shared view metadata and indexed reads for both compiler backends. */ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { ScrBytes *bytes = value->v.bytes; - if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return scr_dyn_new_num(scr_bytes_len(bytes)); if (key->len == 10 && memcmp(key->data, "byteLength", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_len(bytes)); if (key->len == 10 && memcmp(key->data, "byteOffset", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_offset(bytes)); if (key->len == 6 && memcmp(key->data, "buffer", 6) == 0) return scr_array_buffer_from_bytes(bytes); + if (bytes->is_data_view) { + /* A DataView has no length or indexed elements. */ + if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { + static const char message[] = "DataView's constructor through unknown is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return NULL; + } + return scr_dyn_retain(scr_dyn_undefined()); + } + if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return scr_dyn_new_num(scr_bytes_len(bytes)); if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { if (!value->buffer) return scr_bytes_constructor_for(bytes->elem); /* Buffer retains its separate builtin identity. */ @@ -4243,6 +4356,25 @@ void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value scr_dyn_obj_put(obj, key, key_len, value); } +/* A class instance's view takes its native field values whatever the + * attributes copied into it (a frozen instance's view is built the same way + * as any other). Consumes value. */ +void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { + ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) + : scr_dyn_find_linear_entry(obj, key, key_len); + if (existing && !existing->accessor) { + scr_dyn_region_store(obj, value); + ScrDyn *old = existing->value; + existing->value = value; + scr_dyn_release(old); + return; + } + bool non_extensible = obj->non_extensible; + obj->non_extensible = false; + scr_dyn_obj_put(obj, key, key_len, value); + obj->non_extensible = non_extensible; +} + static ScrStr *scr_dyn_property_key(const ScrDyn *key) { return scr_dyn_string_coerce_js(key); } @@ -6104,9 +6236,12 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) (!accessor && ((!prior->writable && writable) || (!prior->writable && value && !scr_dyn_property_same_value(prior->value, value)))))) { scr_dyn_descriptor_fields_drop(fields); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot redefine property: "); + scr_jb_write(&buffer, name->data, name->len); scr_str_release(name); - static const char msg[] = "Cannot redefine property"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return NULL; } if (prior) { @@ -6286,6 +6421,28 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_release(d); return; } + /* A checked-dynamic primitive throws as the primitive itself, as a + * statically typed `throw 5` does: catch bindings test and extract the + * same snapshot kinds (`typeof e === "number"`), and a primitive has no + * identity to keep. */ + if (d->kind == SCR_DYN_NUM) { + double value = d->v.num; + scr_dyn_release(d); + scr_throw_f64(value); + return; + } + if (d->kind == SCR_DYN_BOOL) { + bool value = d->v.b; + scr_dyn_release(d); + scr_throw_bool(value); + return; + } + if (d->kind == SCR_DYN_STR) { + ScrStr *value = scr_str_retain(d->v.str); + scr_dyn_release(d); + scr_throw_str(value); + return; + } scr_throw_ref_classified(d, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, scr_dyn_is_object(d)); } @@ -6364,7 +6521,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "[object "); - scr_jb_puts(&b, scr_bytes_elem_name(d->v.bytes->elem)); + scr_jb_puts(&b, d->v.bytes->is_data_view ? "DataView" : scr_bytes_elem_name(d->v.bytes->elem)); scr_jb_putc(&b, ']'); return scr_jb_finish(&b); } @@ -6719,10 +6876,111 @@ static void scr_errdyn_teardown(void) { free(cache); } +/* An error's own `stack`: a non-enumerable accessor shared by every error, + * as V8 defines it. The getter formats the stack on first access, so its + * heading is the name and message at that point, and later reads answer + * the same string. Both functions find the error through the receiver or + * its prototype chain; on any other receiver the getter answers undefined + * and the setter changes nothing but the receiver's own accessor. */ +static SCR_TL ScrDyn *scr_error_stack_accessors[2]; +static SCR_TL bool scr_error_stack_accessors_registered; + +static void scr_error_stack_accessors_cleanup(void) { + for (size_t i = 0; i < 2; i++) { + scr_dyn_release(scr_error_stack_accessors[i]); + scr_error_stack_accessors[i] = NULL; + } + scr_error_stack_accessors_registered = false; +} + +/* The native error behind an accessor receiver. +1 or NULL. */ +static ScrError *scr_error_stack_holder(const ScrDyn *receiver) { + for (const ScrDyn *current = receiver; current; current = scr_dyn_proto_next(current)) { + ScrError *error = scr_errdyn_err_of(current); + if (error) return error; + if (current->kind != SCR_DYN_OBJ) return NULL; + } + return NULL; +} + +static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrError *error = scr_error_stack_holder(self); + scr_dyn_release(self); + if (!error) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *stack = scr_error_stack(error); + scr_error_release(error); + /* A name or message getter that throws leaves its exception pending. */ + if (!stack) return NULL; + ScrDyn *out = scr_dyn_new_str(stack); + scr_str_release(stack); + return out; +} + +static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); + +/* Whether an own property is an error's `stack` accessor. */ +static bool scr_dyn_entry_is_error_stack(const ScrDynEntry *entry) { + return entry->accessor && entry->setter && entry->setter->kind == SCR_DYN_FUNC && + entry->setter->v.fn.thunk == scr_error_stack_set_call; +} + +static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = scr_dyn_this_get(); + ScrError *error = scr_error_stack_holder(self); + ScrDyn *value = argc > 0 ? args[0] : scr_dyn_undefined(); + if (error && value->kind == SCR_DYN_STR) { + scr_error_set_stack(error, value->v.str); + } else if (self->kind == SCR_DYN_OBJ) { + /* The native slot holds strings only, and a typed reference's + * snapshot names no error: the receiver's own accessor becomes a data + * property holding the value, which a snapshot's commit stores. */ + ScrStr *key = scr_str_new("stack", 5); + ScrDynEntry *entry = scr_dyn_entry(self, key); + scr_str_release(key); + if (entry && scr_dyn_entry_is_error_stack(entry)) { + ScrDyn *old_value = entry->value, *old_getter = entry->getter, *old_setter = entry->setter; + entry->value = scr_dyn_retain(value); + entry->getter = entry->setter = NULL; + entry->accessor = false; + entry->writable = true; + scr_dyn_release(old_value); + scr_dyn_release(old_getter); + scr_dyn_release(old_setter); + } + } + scr_error_release(error); + scr_dyn_release(self); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static void scr_error_dyn_add_stack(ScrDyn *d) { + if (!scr_error_stack_accessors_registered) { + scr_error_stack_accessors_registered = true; + scr_atexit(scr_error_stack_accessors_cleanup); + } + if (!scr_error_stack_accessors[0]) { + scr_error_stack_accessors[0] = + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_get_call, 0, "native:error.stack", NULL); + scr_error_stack_accessors[1] = + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_set_call, 1, "native:error.stack", NULL); + } + scr_dyn_obj_set(d, "stack", 5, scr_dyn_retain(scr_dyn_undefined())); + ScrDynEntry *entry = &d->v.obj.entries[d->v.obj.len - 1]; + entry->accessor = true; + entry->getter = scr_dyn_retain(scr_error_stack_accessors[0]); + entry->setter = scr_dyn_retain(scr_error_stack_accessors[1]); + entry->enumerable = false; +} + ScrDyn *scr_error_dyn_fields(const ScrError *e) { ScrDyn *d = scr_dyn_new_obj(); scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */ d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + /* Node lists `stack` first among an error's own properties. */ + scr_error_dyn_add_stack(d); if (e->name_present) { scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; @@ -6744,11 +7002,6 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; } - if (e->stack) { - /* Node's own `stack` is non-enumerable. */ - scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; - } if (e->system_message) { /* SystemError: code, the read-only `info` context ({ message, path, * syscall, errno, code }), then errno, syscall, path and dest. */ @@ -7059,6 +7312,8 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return d->v.symbol.render(d->v.symbol.value); case SCR_DYN_BYTES: if (d->buffer) return scr_bytes_to_str(d->v.bytes, enc); + /* DataView has no toString of its own: Object.prototype.toString. */ + if (d->v.bytes->is_data_view) return scr_str_new("[object DataView]", 17); { ScrStr *separator = scr_str_new(",", 1); ScrStr *out = scr_bytes_join(d->v.bytes, separator); @@ -7911,6 +8166,7 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { return out; } if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key); + if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key); if (scr_dyn_has_key(v, key)) return true; if (scr_exc_pending()) return false; if (v->kind == SCR_DYN_OBJ) return scr_dyn_builtin_inherited(v, key); @@ -8211,7 +8467,7 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) { case SCR_DYN_ARR: return "array"; case SCR_DYN_OBJ: return "object"; case SCR_DYN_UNDEF: return "undefined"; - case SCR_DYN_BYTES: return scr_bytes_elem_name(d->v.bytes->elem); + case SCR_DYN_BYTES: return d->v.bytes->is_data_view ? "DataView" : scr_bytes_elem_name(d->v.bytes->elem); case SCR_DYN_FUNC: return "function"; case SCR_DYN_HANDLE: return scr_dyn_handle_cls(d); /* "got IncomingMessage" */ case SCR_DYN_PROMISE: return "Promise"; /* "got Promise" */ @@ -10860,6 +11116,17 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion ScrDyn *out = scr_dyn_new_obj(); for (size_t i = 0; i < v->v.obj.len; i++) { const ScrDynEntry *e = &v->v.obj.entries[i]; + if (scr_dyn_entry_is_error_stack(e)) { + /* A stack that was already formatted copies as a non-enumerable + * data property; an unformatted one has nothing to copy. */ + ScrError *error = scr_errdyn_err_of(v); + if (error && error->stack) { + scr_dyn_obj_set(out, "stack", 5, scr_dyn_new_str(error->stack)); + out->v.obj.entries[out->v.obj.len - 1].enumerable = false; + } + scr_error_release(error); + continue; + } ScrDyn *c = scr_sc_clone(e->value, &self, region); if (c == NULL) { scr_dyn_release(out); @@ -11383,7 +11650,20 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { scr_dyn_release(table); return out; } - if (value->kind == SCR_DYN_OBJ) return scr_dyn_obj_own_keys(value); + if (value->kind == SCR_DYN_OBJ) { + ScrDyn *keys = scr_dyn_obj_own_keys(value); + if (!scr_dyn_obj_get(value, "%error", 6)) return keys; + /* The checked-dynamic tree's error marker is not a property. */ + ScrDyn *names = scr_dyn_new_arr(); + scr_dyn_arr_reserve(names, keys->v.arr.len); + for (size_t i = 0; i < keys->v.arr.len; i++) { + const ScrStr *key = keys->v.arr.items[i]->v.str; + if (key->len == 6 && memcmp(key->data, "%error", 6) == 0) continue; + scr_dyn_arr_push(names, scr_dyn_retain(keys->v.arr.items[i])); + } + scr_dyn_release(keys); + return names; + } if (value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES) { ScrDyn *keys = scr_dyn_new_arr(); size_t length = value->kind == SCR_DYN_ARR ? value->v.arr.len : @@ -13955,6 +14235,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt ScrDyn *scr_dyn_freeze(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { value->non_extensible = true; @@ -13982,6 +14263,8 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { bool scr_dyn_is_frozen(const ScrDyn *value) { if (scr_dyn_class_reflection_fence(value)) return false; + bool answer; + if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer; if (value->symbol_properties && !scr_dyn_is_frozen(value->symbol_properties)) return false; if (value->kind == SCR_DYN_ARR) return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) && @@ -14036,6 +14319,130 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { } } +/* ── class instance attributes ─────────────────────────────────────────── + * A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions + * or Object.defineProperty keeps an integrity state per instance and an + * attribute word per redefined field (CLASS_* and FIELD_* bits in the + * compiler's ir.ts). The checked view of an instance carries them, so + * reflection over the view answers as Node does. */ +enum { + SCR_CLASS_NOT_EXTENSIBLE = 1, + SCR_CLASS_SEALED = 2, + SCR_CLASS_FROZEN = 4, + SCR_FIELD_NOT_WRITABLE = 1, + SCR_FIELD_NOT_ENUMERABLE = 2, + SCR_FIELD_NOT_CONFIGURABLE = 4, +}; + +void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { + unsigned b = (unsigned)bits; + if (b == 0 || view->kind != SCR_DYN_OBJ) return; + ScrDynEntry *entry = scr_dyn_entry(view, name); + if (!entry) return; + if (b & SCR_FIELD_NOT_WRITABLE) entry->writable = false; + if (b & SCR_FIELD_NOT_ENUMERABLE) entry->enumerable = false; + if (b & SCR_FIELD_NOT_CONFIGURABLE) entry->configurable = false; +} + +void scr_dyn_class_view_attributes(ScrDyn *view, double bits) { + unsigned b = (unsigned)bits; + if (b == 0 || view->kind != SCR_DYN_OBJ) return; + view->non_extensible = true; + for (size_t i = 0; i < view->v.obj.len; i++) { + ScrDynEntry *entry = &view->v.obj.entries[i]; + if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false; + if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false; + } +} + +/* Object.defineProperty on a declared class field. The descriptor applies to + * a one-property object holding the field's current value and attributes, + * so validation and errors match ordinary objects. Answers that object (+1), + * or NULL with a pending exception. */ +ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name, + ScrDyn *descriptor) { + unsigned f = (unsigned)field_bits, o = (unsigned)object_bits; + ScrDyn *holder = scr_dyn_new_obj(); + scr_dyn_obj_set(holder, name->data, name->len, scr_dyn_retain(current)); + ScrDynEntry *entry = scr_dyn_entry(holder, name); + entry->writable = !(f & SCR_FIELD_NOT_WRITABLE) && !(o & SCR_CLASS_FROZEN); + entry->enumerable = !(f & SCR_FIELD_NOT_ENUMERABLE); + entry->configurable = + !(f & SCR_FIELD_NOT_CONFIGURABLE) && !(o & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)); + ScrDyn *key = scr_dyn_new_str(name); + ScrDyn *defined = scr_dyn_define_property(holder, key, descriptor); + scr_dyn_release(key); + if (!defined) { + scr_dyn_release(holder); + return NULL; + } + scr_dyn_release(defined); + entry = scr_dyn_entry(holder, name); + if (entry->accessor) { + scr_dyn_release(holder); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Object.defineProperty with an accessor descriptor for the declared field '"); + scr_jb_write(&buffer, name->data, name->len); + scr_jb_puts(&buffer, "' is not supported yet"); + ScrStr *message = scr_jb_finish(&buffer); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return NULL; + } + return holder; +} + +/* The field attribute word of a scr_class_define_field result. */ +double scr_class_defined_bits(ScrDyn *holder, ScrStr *name) { + const ScrDynEntry *entry = scr_dyn_entry(holder, name); + unsigned bits = 0; + if (!entry->writable) bits |= SCR_FIELD_NOT_WRITABLE; + if (!entry->enumerable) bits |= SCR_FIELD_NOT_ENUMERABLE; + if (!entry->configurable) bits |= SCR_FIELD_NOT_CONFIGURABLE; + return (double)bits; +} + +/* The value a scr_class_define_field result holds (+1). */ +ScrDyn *scr_class_defined_value(ScrDyn *holder, ScrStr *name) { + return scr_dyn_retain(scr_dyn_entry(holder, name)->value); +} + +/* Whether a property descriptor names a value (Node's ToPropertyDescriptor + * reads `value` with [[HasProperty]]). */ +bool scr_class_descriptor_has_value(ScrDyn *descriptor) { + if (descriptor->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(descriptor); + bool has = !scr_exc_pending() && scr_class_descriptor_has_value(view); + scr_dyn_release(view); + return has; + } + return descriptor->kind == SCR_DYN_OBJ && scr_dyn_obj_get(descriptor, "value", 5) != NULL; +} + +/* Node's TypeError for a write to a read-only field of a class instance. */ +void scr_class_throw_read_only(ScrStr *field, ScrStr *owner) { + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot assign to read only property '"); + scr_jb_write(&buffer, field->data, field->len); + scr_jb_puts(&buffer, "' of object '#<"); + scr_jb_write(&buffer, owner->data, owner->len); + scr_jb_puts(&buffer, ">'"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + +/* Node's TypeError for a class field initializer on a non-extensible + * instance (a base constructor froze or sealed `this`). */ +void scr_class_throw_not_extensible(ScrStr *field) { + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot define property "); + scr_jb_write(&buffer, field->data, field->len); + scr_jb_puts(&buffer, ", object is not extensible"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + /* ── compiled generators through untyped values ─────────────────────── */ static SCR_TL ScrDyn *scr_generator_next_getter; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 930d44818..2e6f35f67 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -2037,6 +2037,19 @@ double scr_process_exit_code_or_zero(void) { return (double)scr_exit_code_hint_get(); } +static SCR_TL bool scr_exit_code_assigned = false; + +void scr_process_set_exit_code_tracked(double code) { + int converted; + if (!scr_process_exit_code_of(code, &converted)) return; + scr_exit_code_note(converted); + scr_exit_code_assigned = true; +} + +bool scr_process_has_exit_code(void) { + return scr_exit_code_assigned || scr_exit_code_hint_get() != 0; +} + #if defined(__linux__) #include #include @@ -7263,45 +7276,112 @@ static ScrStr *scr_digest_encode(const unsigned char *d, size_t n, int enc) { /* ── MD5 (RFC 1321) ───────────────────────────────────────────────── */ -static const uint32_t scr_md5_k[64] = { - 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, - 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, - 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, - 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, - 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, - 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, - 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, - 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, - 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, - 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, - 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}; -static const unsigned char scr_md5_r[64] = { - 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, - 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, - 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, - 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; +/* The 64 steps are unrolled with their message word, constant and + * rotation inlined. a + m + K does not depend on the previous step, so + * each step's critical path is the round function of b, an add, the + * rotate and the add of b. G is written as (b & d) + (c & ~d) (the two + * terms share no bits) so that its c & ~d half joins that early sum. The + * empty asm statement keeps the compiler from moving the constant's add + * after the round function, which puts it back on the critical path. */ +#if (defined(__GNUC__) || defined(__clang__)) && !defined(__wasm__) +#define SCR_MD5_PIN(v) __asm__("" : "+r"(v)) +#else +#define SCR_MD5_PIN(v) ((void)0) +#endif +#define SCR_MD5_F(b, c, d) ((d) ^ ((b) & ((c) ^ (d)))) +#define SCR_MD5_H(b, c, d) ((b) ^ (c) ^ (d)) +#define SCR_MD5_I(b, c, d) ((c) ^ ((b) | ~(d))) +#define SCR_MD5_STEP(f, a, b, c, d, xk, s) \ + do { \ + (a) += (xk); \ + SCR_MD5_PIN(a); \ + (a) += f((b), (c), (d)); \ + (a) = ((a) << (s) | (a) >> (32 - (s))) + (b); \ + } while (0) +#define SCR_MD5_GSTEP(a, b, c, d, xk, s) \ + do { \ + (a) += ((c) & ~(d)) + (xk); \ + SCR_MD5_PIN(a); \ + (a) += (b) & (d); \ + (a) = ((a) << (s) | (a) >> (32 - (s))) + (b); \ + } while (0) -SCR_DIGEST_ALIGNED static void scr_md5_block(uint32_t h[4], const unsigned char *p) { - uint32_t m[16]; - for (int i = 0; i < 16; i++) { - m[i] = (uint32_t)p[i * 4] | ((uint32_t)p[i * 4 + 1] << 8) | - ((uint32_t)p[i * 4 + 2] << 16) | ((uint32_t)p[i * 4 + 3] << 24); - } +SCR_DIGEST_ALIGNED static void scr_md5_blocks(uint32_t h[4], const unsigned char *p, size_t blocks) { uint32_t a = h[0], b = h[1], c = h[2], d = h[3]; - for (int i = 0; i < 64; i++) { - uint32_t f, g; - if (i < 16) { f = (b & c) | (~b & d); g = (uint32_t)i; } - else if (i < 32) { f = (d & b) | (~d & c); g = (5u * i + 1) & 15; } - else if (i < 48) { f = b ^ c ^ d; g = (3u * i + 5) & 15; } - else { f = c ^ (b | ~d); g = (7u * i) & 15; } - uint32_t t = d; - d = c; - c = b; - uint32_t x = a + f + scr_md5_k[i] + m[g]; - b = b + ((x << scr_md5_r[i]) | (x >> (32 - scr_md5_r[i]))); - a = t; + for (; blocks != 0; blocks--, p += 64) { + uint32_t m[16]; + for (int i = 0; i < 16; i++) { + m[i] = (uint32_t)p[i * 4] | ((uint32_t)p[i * 4 + 1] << 8) | + ((uint32_t)p[i * 4 + 2] << 16) | ((uint32_t)p[i * 4 + 3] << 24); + } + uint32_t a0 = a, b0 = b, c0 = c, d0 = d; + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[0] + 0xd76aa478u, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[1] + 0xe8c7b756u, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[2] + 0x242070dbu, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[3] + 0xc1bdceeeu, 22); + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[4] + 0xf57c0fafu, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[5] + 0x4787c62au, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[6] + 0xa8304613u, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[7] + 0xfd469501u, 22); + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[8] + 0x698098d8u, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[9] + 0x8b44f7afu, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[10] + 0xffff5bb1u, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[11] + 0x895cd7beu, 22); + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[12] + 0x6b901122u, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[13] + 0xfd987193u, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[14] + 0xa679438eu, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[15] + 0x49b40821u, 22); + SCR_MD5_GSTEP(a, b, c, d, m[1] + 0xf61e2562u, 5); + SCR_MD5_GSTEP(d, a, b, c, m[6] + 0xc040b340u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[11] + 0x265e5a51u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[0] + 0xe9b6c7aau, 20); + SCR_MD5_GSTEP(a, b, c, d, m[5] + 0xd62f105du, 5); + SCR_MD5_GSTEP(d, a, b, c, m[10] + 0x02441453u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[15] + 0xd8a1e681u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[4] + 0xe7d3fbc8u, 20); + SCR_MD5_GSTEP(a, b, c, d, m[9] + 0x21e1cde6u, 5); + SCR_MD5_GSTEP(d, a, b, c, m[14] + 0xc33707d6u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[3] + 0xf4d50d87u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[8] + 0x455a14edu, 20); + SCR_MD5_GSTEP(a, b, c, d, m[13] + 0xa9e3e905u, 5); + SCR_MD5_GSTEP(d, a, b, c, m[2] + 0xfcefa3f8u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[7] + 0x676f02d9u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[12] + 0x8d2a4c8au, 20); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[5] + 0xfffa3942u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[8] + 0x8771f681u, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[11] + 0x6d9d6122u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[14] + 0xfde5380cu, 23); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[1] + 0xa4beea44u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[4] + 0x4bdecfa9u, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[7] + 0xf6bb4b60u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[10] + 0xbebfbc70u, 23); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[13] + 0x289b7ec6u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[0] + 0xeaa127fau, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[3] + 0xd4ef3085u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[6] + 0x04881d05u, 23); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[9] + 0xd9d4d039u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[12] + 0xe6db99e5u, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[15] + 0x1fa27cf8u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[2] + 0xc4ac5665u, 23); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[0] + 0xf4292244u, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[7] + 0x432aff97u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[14] + 0xab9423a7u, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[5] + 0xfc93a039u, 21); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[12] + 0x655b59c3u, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[3] + 0x8f0ccc92u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[10] + 0xffeff47du, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[1] + 0x85845dd1u, 21); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[8] + 0x6fa87e4fu, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[15] + 0xfe2ce6e0u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[6] + 0xa3014314u, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[13] + 0x4e0811a1u, 21); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[4] + 0xf7537e82u, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[11] + 0xbd3af235u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[2] + 0x2ad7d2bbu, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[9] + 0xeb86d391u, 21); + a += a0; b += b0; c += c0; d += d0; } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[0] = a; h[1] = b; h[2] = c; h[3] = d; } typedef struct ScrMd5Ctx { @@ -7329,14 +7409,15 @@ static void scr_md5_update(ScrMd5Ctx *ctx, const unsigned char *data, size_t len data += take; len -= take; if (ctx->tail_len == 64) { - scr_md5_block(ctx->h, ctx->tail); + scr_md5_blocks(ctx->h, ctx->tail, 1); ctx->tail_len = 0; } } - while (len >= 64) { - scr_md5_block(ctx->h, data); - data += 64; - len -= 64; + if (len >= 64) { + size_t blocks = len / 64; + scr_md5_blocks(ctx->h, data, blocks); + data += blocks * 64; + len -= blocks * 64; } if (len != 0) { memcpy(ctx->tail, data, len); @@ -7354,8 +7435,7 @@ static size_t scr_md5_final(const ScrMd5Ctx *source, unsigned char out[32]) { memset(tail + rem + 1, 0, pad - rem - 1 - 8); uint64_t bits = ctx.bytes * 8; for (int b = 0; b < 8; b++) tail[pad - 8 + b] = (unsigned char)(bits >> (8 * b)); - scr_md5_block(ctx.h, tail); - if (pad == 128) scr_md5_block(ctx.h, tail + 64); + scr_md5_blocks(ctx.h, tail, pad / 64); for (int j = 0; j < 4; j++) { for (int b = 0; b < 4; b++) out[j * 4 + b] = (unsigned char)(ctx.h[j] >> (8 * b)); } @@ -8473,6 +8553,8 @@ void scr_crypto_hash_stream_close(ScrCryptoHash *hash) { if (stream) stream_release(stream); } +bool scr_crypto_hash_is_hmac(const ScrCryptoHash *hash) { return hash->hmac; } + bool scr_crypto_hash_stream_usable(const ScrCryptoHash *hash) { if (!hash->stream_closed) return true; static const char msg[] = "Stream use of a Hash or Hmac after its stream closed has no lowering"; @@ -10333,7 +10415,9 @@ ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem) { * keep other surrogates lone, WTF-8 encode. */ static ScrStr *scr_str_from_units(size_t n, uint32_t (*unit)(void *, size_t), void *src) { if (n == 1) return scr_str_from_char_code_one((double)unit(src, 0)); - char *out = malloc(n * 3 + 1); /* worst case: 3 bytes per UTF-16 unit */ + /* Up to 64 units encode on the stack (3 bytes per UTF-16 unit at most). */ + char small[64 * 3 + 1]; + char *out = n <= 64 ? small : malloc(n * 3 + 1); if (!out) { scr_trap("scriptc: out of memory\n"); } @@ -10350,10 +10434,19 @@ static ScrStr *scr_str_from_units(size_t n, uint32_t (*unit)(void *, size_t), vo o += scr_wtf8_put(out + o, cp); } ScrStr *s = scr_str_new(out, o); - free(out); + if (out != small) free(out); return s; } +static uint32_t scr_fcc_units_unit(void *src, size_t i) { + return scr_to_uint32(((const double *)src)[i]) & 0xFFFFu; +} + +/* String.fromCharCode(c0, ..., cn-1) with the codes in a caller's array. */ +ScrStr *scr_str_from_char_codes(const double *codes, size_t n) { + return scr_str_from_units(n, scr_fcc_units_unit, (void *)codes); +} + static uint32_t scr_fcc_arr_unit(void *src, size_t i) { return scr_to_uint32(scr_arr_get_f64((ScrArr *)src, (double)i)) & 0xFFFFu; } diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 4c973e3e5..690f84875 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -149,6 +149,11 @@ void scr_process_exit(double code); extern const unsigned char scr_detached_exit; void scr_process_set_exit_code(double code); double scr_process_exit_code_or_zero(void); +/* The writer and the presence test of programs that read process.exitCode: + * the code reads undefined until an assignment (or a nonzero exit status + * the runtime chose, such as an uncaught exception's 1). */ +void scr_process_set_exit_code_tracked(double code); +bool scr_process_has_exit_code(void); /* process._exiting: true once the exit sequence began (process.exit or * the exit-listener runner set the flag). Never throws. */ extern SCR_TL bool scr_process_in_exit; diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c index f983bd289..39e36f4e5 100644 --- a/packages/runtime/src/scr_mailbox.c +++ b/packages/runtime/src/scr_mailbox.c @@ -349,7 +349,9 @@ void scr_channel_set_owner(ScrChannel *channel, int side, ScrMailbox *owner, uns } /* postMessage from `side`: the message (consumed) queues on the other end - * unless either end has closed. */ + * unless either end has closed. Only a message that finds the end's queue + * empty wakes its owner: the owner's loop counts an end with queued + * messages as ready, so it does not wait while the queue is non-empty. */ bool scr_channel_post(ScrChannel *channel, int side, ScrMessage *message, unsigned message_kind, unsigned wake_kind) { ScrMailEvent *event = calloc(1, sizeof(*event)); @@ -363,10 +365,11 @@ bool scr_channel_post(ScrChannel *channel, int side, ScrMessage *message, unsign scr_mail_event_free(event); return false; } + bool first = peer->count == 0; *peer->tail = event; peer->tail = &event->next; peer->count++; - ScrMailbox *wake = peer->owner ? scr_mailbox_retain(peer->owner) : NULL; + ScrMailbox *wake = first && peer->owner ? scr_mailbox_retain(peer->owner) : NULL; scr_channel_unlock(channel); scr_channel_wake(wake, wake_kind); return true; diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 7c969a2e3..374b5469a 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -119,7 +119,7 @@ static void *scr_message_alloc(size_t count, size_t size) { static size_t scr_message_grow(size_t capacity) { if (capacity > SIZE_MAX / 2) scr_trap("scriptc: message too large\n"); - return capacity ? capacity * 2 : 16; + return capacity ? capacity * 2 : 4; } static unsigned char *scr_message_copy(const void *data, size_t length) { @@ -672,6 +672,15 @@ done: free(encoder.identities); free(encoder.stack); if (scr_exc_pending()) { scr_message_free(message); return NULL; } + /* A queued message keeps its nodes until it is received: drop the + * growth slack. */ + if (message->count && message->count < message->capacity) { + ScrMessageNode *nodes = realloc(message->nodes, message->count * sizeof(*nodes)); + if (nodes) { + message->nodes = nodes; + message->capacity = message->count; + } + } return message; } @@ -801,6 +810,12 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { } if (!values[i]->v.arr.properties) values[i]->v.arr.properties = scr_dyn_new_obj(); scr_dyn_obj_set(values[i]->v.arr.properties, property->key, property->length, value); + } else if (node->kind == SCR_MESSAGE_ERROR && property->length == 5 && !memcmp(property->key, "stack", 5)) { + /* The new error's own `stack` accessor stores the value. */ + ScrStr *key = scr_str_new(property->key, property->length); + scr_dyn_key_set(values[i], key, value); + scr_str_release(key); + scr_dyn_release(value); } else scr_dyn_obj_set(values[i], property->key, property->length, value); } if (node->kind == SCR_MESSAGE_ERROR) { diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index f43195116..1b85fb0fe 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -52,6 +52,8 @@ ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */ * adjacent surrogate pairs combine, other surrogates stay lone. Borrowed * args; the string result is +1; neither throws. */ ScrStr *scr_str_from_char_code(ScrArr *codes); +/* String.fromCharCode over n codes in a caller-owned array (no ScrArr). */ +ScrStr *scr_str_from_char_codes(const double *codes, size_t n); /* Single numeric argument; avoids argument packing. Defined in scr_string.c * to share its character cache. Same ToUint16 and lone-surrogate behavior. */ ScrStr *scr_str_from_char_code_one(double code); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index eb28fb2e7..8611741da 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -352,6 +352,10 @@ void scr_sa_thread_starting(void); * pointer, or a process-wide cache keeps an immortal object. Its memory * then stays when it exits (scr_alloc.c). */ void scr_sa_thread_shares(void); +/* The event loop of the calling thread is about to wait with nothing ready + * (true), or is done waiting (false); worker threads are ignored. The + * memory of parked workers goes back while the loop waits (scr_alloc.c). */ +void scr_sa_loop_waiting(bool waiting); #define SCR_SA_FREE_FOREIGN(p) scr_sa_free_foreign(p) #else #define SCR_SA_FREE_FOREIGN(p) free(p) diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index e031a4e34..1bc48bbe7 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -185,11 +185,28 @@ void scr_rl_do_resume(ScrRl *rl) { } } +bool scr_rl_prompt_writable(ScrRl *rl) { + if (!rl->prompt_value) return true; + scr_dyn_arg_type_fail("stringToWrite", "of type string", rl->prompt_value); + return false; +} + static void scr_rl_prompt_now(ScrRl *rl) { if (rl->paused) scr_rl_do_resume(rl); if (scr_exc_pending()) return; if (rl->tty) scr_rl_tty_ops->prompt(rl, false); - else scr_rl_write_out(rl, rl->prompt); + else if (scr_rl_prompt_writable(rl)) scr_rl_write_out(rl, rl->prompt); +} + +/* setPrompt(this[kOldPrompt]) when a question is answered or cancelled */ +static void scr_rl_restore_prompt(ScrRl *rl) { + if (!rl->old_prompt) return; + scr_str_release(rl->prompt); + rl->prompt = rl->old_prompt; + rl->old_prompt = NULL; + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + rl->prompt_value = rl->old_prompt_value; + rl->old_prompt_value = NULL; } /* ── close ─────────────────────────────────────────────────────────── */ @@ -251,11 +268,7 @@ void scr_rl_on_line(ScrRl *rl, ScrStr *line) { ScrClosure *cb = rl->q_cb; ScrRlLineFn fn = rl->q_fn; rl->q_cb = NULL; - if (rl->old_prompt) { - scr_str_release(rl->prompt); - rl->prompt = rl->old_prompt; - rl->old_prompt = NULL; - } + scr_rl_restore_prompt(rl); fn(cb, line); scr_closure_release(cb); return; @@ -619,6 +632,8 @@ static void scr_rl_cleanup_atexit(void) { if (rl->out) rl->io->release(rl->out); scr_str_release(rl->prompt); if (rl->old_prompt) scr_str_release(rl->old_prompt); + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + if (rl->old_prompt_value) scr_dyn_release(rl->old_prompt_value); free(rl->line_buf); free(rl->parked); free(rl); @@ -748,7 +763,58 @@ void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, ScrRlLi return; } rl->old_prompt = rl->prompt; + rl->old_prompt_value = rl->prompt_value; rl->prompt = scr_str_retain(query); + rl->prompt_value = NULL; + rl->q_cb = cb; + rl->q_fn = fn; + scr_rl_prompt_now(rl); +} + +/* A prompt that is not a string: String(value) for a primitive, which is + * what the terminal's line refresh and getCursorPos concatenate. An + * object's valueOf/toString would run on every refresh, and a symbol + * throws there, so a terminal interface refuses them (false, the refusal + * pending); another interface only writes the prompt, which throws for + * any value that is not a string, so it keeps ''. +1. */ +static bool scr_rl_prompt_text(ScrRl *rl, ScrDyn *value, ScrStr **out) { + if (scr_rl_primitive_prompt(value)) { + *out = scr_dyn_string_coerce(value); + return true; + } + if (!rl->tty) { + *out = scr_str_new("", 0); + return true; + } + static const char m[] = + "a terminal readline prompt that is an object or a symbol has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "SC2020"); + return false; +} + +/* question(query, cb) with a checked-dynamic query: a string goes through + * scr_rl_question. Another value becomes the prompt, the callback is + * kept, and the prompt write throws ERR_INVALID_ARG_TYPE on an interface + * that is not a terminal (the question stays pending, as in Node). */ +void scr_rl_question_dyn(double id, ScrDyn *query, ScrClosure *cb /*moves*/, ScrRlLineFn fn) { + if (query->kind == SCR_DYN_STR) { + scr_rl_question(id, query->v.str, cb, fn); + return; + } + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed || rl->q_cb) { + scr_rl_question(id, NULL, cb, fn); /* the closed throw, or the re-prompt */ + return; + } + ScrStr *text; + if (!scr_rl_prompt_text(rl, query, &text)) { + scr_closure_release(cb); + return; + } + rl->old_prompt = rl->prompt; + rl->old_prompt_value = rl->prompt_value; + rl->prompt = text; + rl->prompt_value = scr_dyn_retain(query); rl->q_cb = cb; rl->q_fn = fn; scr_rl_prompt_now(rl); @@ -857,11 +923,7 @@ static ScrDyn *scr_rl_ask_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size if (rl && rl->q_cb) { scr_closure_release(rl->q_cb); rl->q_cb = NULL; - if (rl->old_prompt) { - scr_str_release(rl->prompt); - rl->prompt = rl->old_prompt; - rl->old_prompt = NULL; - } + scr_rl_restore_prompt(rl); if (rl->tty) { scr_rl_tty_ops->clear_line(rl); } else { @@ -877,8 +939,11 @@ static ScrDyn *scr_rl_ask_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size /* readline/promises question(query, { signal }): rejects with * ERR_USE_AFTER_CLOSE on a closed interface and with an AbortError when - * the signal is (or becomes) aborted. */ -ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add) { + * the signal is (or becomes) aborted. A checked-dynamic query (query_dyn, + * query NULL) that is not a string rejects with what question() throws, + * as Node's promise executor does. */ +static ScrPromise *scr_rl_ask(double id, ScrStr *query, ScrDyn *query_dyn, ScrDyn *signal, + ScrAbortAddFn add) { RlAsk *a = calloc(1, sizeof *a); if (!a) scr_rl_oom(); a->rc = 1; @@ -928,10 +993,29 @@ ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, Sc scr_box_set_ref(cb->caps[0], a); /* the creation reference moves here */ cb->caps[1] = scr_box_new_obj(&scr_rl_dyn_retain_v, &scr_rl_dyn_release_v, NULL); scr_box_set_ref(cb->caps[1], dispose); /* moves (NULL without a signal) */ - scr_rl_question(id, query, cb, &scr_rl_ask_answer); + if (!query_dyn) { + scr_rl_question(id, query, cb, &scr_rl_ask_answer); + return result; + } + scr_rl_ask_retain_v(a); + scr_rl_question_dyn(id, query_dyn, cb, &scr_rl_ask_answer); + if (scr_exc_pending() && !a->settled) { + a->settled = true; + scr_promise_reject_pending(a->p); + } + scr_rl_ask_release_v(a); return result; } +ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add) { + return scr_rl_ask(id, query, NULL, signal, add); +} + +ScrPromise *scr_rl_question_promise_dyn(double id, ScrDyn *query, ScrDyn *signal, + ScrAbortAddFn add) { + return scr_rl_ask(id, NULL, query, signal, add); +} + void scr_rl_close(double id) { ScrRl *rl = scr_rl_find(id); if (rl) scr_rl_settle_close(rl); /* twice is a no-op, like Node */ @@ -985,13 +1069,48 @@ void scr_rl_set_prompt(double id, ScrStr *prompt) { if (!rl) return; scr_str_release(rl->prompt); rl->prompt = scr_str_retain(prompt); + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + rl->prompt_value = NULL; +} + +/* setPrompt(value) with a checked-dynamic value; borrows. */ +void scr_rl_set_prompt_dyn(double id, ScrDyn *prompt) { + if (prompt->kind == SCR_DYN_STR) { + scr_rl_set_prompt(id, prompt->v.str); + return; + } + ScrRl *rl = scr_rl_find(id); + if (!rl) return; + ScrStr *text; + if (!scr_rl_prompt_text(rl, prompt, &text)) return; + scr_str_release(rl->prompt); + rl->prompt = text; + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + rl->prompt_value = scr_dyn_retain(prompt); } ScrStr *scr_rl_get_prompt(double id) { ScrRl *rl = scr_rl_find(id); + if (rl && rl->prompt_value) { + static const char m[] = "rl.getPrompt() of a prompt that is not a string has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "SC2020"); + return scr_str_new("", 0); + } return rl ? scr_str_retain(rl->prompt) : scr_str_new("", 0); } +/* Called after a node:readline/promises interface opens: its Interface + * class is not node:readline's, which instanceof tells apart. */ +void scr_rl_mark_promises(double id) { + ScrRl *rl = scr_rl_find(id); + if (rl) rl->promises = true; +} + +bool scr_rl_is_promises(double id) { + ScrRl *rl = scr_rl_find(id); + return rl && rl->promises; +} + bool scr_rl_is_terminal(double id) { ScrRl *rl = scr_rl_find(id); return rl && rl->tty; diff --git a/packages/runtime/src/scr_readline.h b/packages/runtime/src/scr_readline.h index 9385f6dd7..4557dcb4d 100644 --- a/packages/runtime/src/scr_readline.h +++ b/packages/runtime/src/scr_readline.h @@ -47,13 +47,22 @@ void scr_rl_input_error(double id, ScrError *err); void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrStr *)); ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add); +/* The same with a checked-dynamic query (borrowed). A query that is not a + * string makes the prompt write throw ERR_INVALID_ARG_TYPE (the promise + * form rejects with it) unless the interface is a terminal; the question + * stays pending, as in Node. */ +void scr_rl_question_dyn(double id, ScrDyn *query, ScrClosure *cb /*moves*/, + void (*fn)(ScrClosure *, ScrStr *)); +ScrPromise *scr_rl_question_promise_dyn(double id, ScrDyn *query, ScrDyn *signal, + ScrAbortAddFn add); void scr_rl_close(double id); void scr_rl_pause(double id); /* may throw ERR_USE_AFTER_CLOSE */ void scr_rl_resume(double id); /* may throw ERR_USE_AFTER_CLOSE */ void scr_rl_prompt(double id); /* may throw ERR_USE_AFTER_CLOSE */ void scr_rl_prompt_with(double id, bool preserve); /* prompt(preserveCursor); may throw */ void scr_rl_set_prompt(double id, ScrStr *prompt); -ScrStr *scr_rl_get_prompt(double id); /* +1 */ +void scr_rl_set_prompt_dyn(double id, ScrDyn *prompt); /* borrows; may throw */ +ScrStr *scr_rl_get_prompt(double id); /* +1; may throw (a prompt that is not a string) */ void scr_rl_write(double id, ScrStr *data); /* may throw ERR_USE_AFTER_CLOSE */ void scr_rl_on(double id, double ev, ScrClosure *cb /*moves*/, void *fn, bool once); /* createInterface's history and editing options, in Node's order: the @@ -83,6 +92,8 @@ double scr_rl_num_prop(double id, double which); * when there is a completer (the inspect unit links only then). */ void scr_rl_terminal(double id, double mode, bool on, ScrDyn *completer, bool promises, ScrStr *(*inspect)(ScrDyn *, double, double)); +void scr_rl_mark_promises(double id); +bool scr_rl_is_promises(double id); bool scr_rl_is_terminal(double id); /* readline.cursorTo / moveCursor / clearLine / clearScreenDown: the data * written (+1; kind 0-3), after the argument checks (may throw), and @@ -135,6 +146,9 @@ void scr_rl_error_thunk_dyn(ScrClosure *cb, ScrError *err); * the input stream or NULL for process.stdin; out the output stream or * NULL (out_stdout picks process.stdout). Both borrowed, like prompt * (NULL: the default "> "). */ +/* A checked-dynamic input or output as its stream (+1; NULL for an output + * of null or undefined); refuses other values (may throw). */ +ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output); double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double crlf_delay, ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add); void scr_stdin_data_thunk0(ScrClosure *cb, ScrBytes *chunk); diff --git a/packages/runtime/src/scr_readline_internal.h b/packages/runtime/src/scr_readline_internal.h index 0fe2e2cb2..49ef42f39 100644 --- a/packages/runtime/src/scr_readline_internal.h +++ b/packages/runtime/src/scr_readline_internal.h @@ -56,11 +56,17 @@ typedef struct ScrRl { bool stdin_in; /* reads process.stdin through the shared consumer */ bool dead; /* stdin only: created after stdin ended */ bool paused; + bool promises; /* created by node:readline/promises */ const ScrRlIo *io; /* the stream side, for a stream input or output */ void *in; /* the input stream (+1), or NULL */ void *out; /* the output stream (+1), or NULL */ bool out_stdout; ScrStr *prompt, *old_prompt; + /* A prompt that is not a string (setPrompt or question with another + * value), +1, or NULL; prompt then holds String(value) for a terminal + * interface. Writing it the way Node's kWriteToOutput does throws + * ERR_INVALID_ARG_TYPE. old_prompt_value is old_prompt's. */ + ScrDyn *prompt_value, *old_prompt_value; ScrClosure *q_cb; /* the pending question's callback (owned) */ ScrRlLineFn q_fn; /* the last promise question (kQuestionReject), which the terminal's @@ -114,6 +120,14 @@ ScrArr *scr_rl_history_of(ScrRl *rl); /* borrowed; created on first use */ void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err); void scr_rl_emit_history(ScrRl *rl); void scr_rl_write_out(ScrRl *rl, ScrStr *text); +/* kWriteToOutput(prompt)'s validateString: false with ERR_INVALID_ARG_TYPE + * pending when the prompt is not a string. */ +bool scr_rl_prompt_writable(ScrRl *rl); +/* A prompt value whose String() the interface keeps in prompt. */ +static inline bool scr_rl_primitive_prompt(const ScrDyn *v) { + return v->kind == SCR_DYN_NULL || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_BOOL || + v->kind == SCR_DYN_NUM || v->kind == SCR_DYN_BIGINT; +} /* kOnLine: a pending question takes the line, else 'line'. Moves line. */ void scr_rl_on_line(ScrRl *rl, ScrStr *line); void scr_rl_do_pause(ScrRl *rl); diff --git a/packages/runtime/src/scr_readline_tty.c b/packages/runtime/src/scr_readline_tty.c index 38271411f..1c5fcd5f6 100644 --- a/packages/runtime/src/scr_readline_tty.c +++ b/packages/runtime/src/scr_readline_tty.c @@ -2255,7 +2255,7 @@ static void tty_prompt(ScrRl *rl, bool preserve) { bool dumb = term && term->len == 4 && memcmp(term->data, "dumb", 4) == 0; if (term) scr_str_release(term); if (dumb) { - scr_rl_write_out(rl, rl->prompt); + if (scr_rl_prompt_writable(rl)) scr_rl_write_out(rl, rl->prompt); return; } if (!preserve) t->cursor = 0; @@ -2333,6 +2333,12 @@ double scr_rl_cursor_pos(double id, double which) { if (rl->tty) { pos = tty_cursor_pos(rl); } else { + if (rl->prompt_value && !scr_rl_primitive_prompt(rl->prompt_value)) { + static const char m[] = + "rl.getCursorPos() with a prompt that is an object or a symbol has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "SC2020"); + return 0; + } ScrRlTty probe = {0}; probe.columns = INFINITY; rl->tty = &probe; diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 244e2ec5d..5f0bb493d 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -4728,6 +4728,31 @@ static void scr_stream_rl_hooks_free(void) { } } +/* createInterface's input or output given as a checked-dynamic value (a + * JavaScript function's result, an object's member): the stream (+1). + * An output of null or undefined is NULL (no output). A value that is + * not a stream with a readable (input) or writable (output) side is + * refused. */ +ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) { + if (output && (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) return NULL; + if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && + !memcmp(value->v.typed_ref.type_key, "object:", 7)) { + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = stream->vt; + bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || + scr_vt_within(vt, &scr_duplex_vt); + bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) + : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); + if (ok) return stream; + value->v.typed_ref.release(stream); + } + static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering"; + static const char out_m[] = "createInterface with an output that is not a writable stream has no lowering"; + if (output) scr_throw_error_msg_code(SCR_ERR_ERROR, out_m, sizeof out_m - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, in_m, sizeof in_m - 1, "SC2020"); + return NULL; +} + double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double crlf_delay, ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add) { double id = scr_rl_open(&scr_stream_rl_io, in ? scr_stream_retain(in) : NULL, @@ -6350,15 +6375,19 @@ void scr_stream_install(void) { static void scr_stream_child_read(ScrClosure *cb, ScrStream *s, double size) { (void)cb; (void)s; (void)size; } -static ScrStream *scr_stream_child_readable(void) { - return scr_stream_new_readable(-1, true, true, +static ScrStream *scr_stream_child_readable(const char *cls) { + ScrStream *s = scr_stream_new_readable(-1, true, true, scr_closure_new((void *)scr_stream_child_read, 0), scr_stream_child_read, NULL, NULL); + s->cls = cls; + return s; } -static ScrStream *scr_stream_child_writable(ScrClosure *write, ScrStreamChunkInv write_inv, - ScrClosure *final_cb, ScrStreamPlainInv final_inv) { - return scr_stream_new_writable(-1, true, true, write, write_inv, +static ScrStream *scr_stream_child_writable(const char *cls, ScrClosure *write, + ScrStreamChunkInv write_inv, ScrClosure *final_cb, ScrStreamPlainInv final_inv) { + ScrStream *s = scr_stream_new_writable(-1, true, true, write, write_inv, final_cb, final_inv, NULL, NULL); + s->cls = cls; + return s; } const ScrChildStreamOps scr_stream_child_ops = { scr_stream_child_readable, scr_stream_child_writable, @@ -6578,6 +6607,7 @@ ScrStream *scr_crypto_hash_stream(ScrCryptoHash *hash) { scr_hash_stream_step(box), scr_hash_stream_transform, scr_hash_stream_step(box), scr_hash_stream_flush, scr_hash_stream_step(box), scr_hash_stream_destroy); + s->cls = scr_crypto_hash_is_hmac(hash) ? "Hmac" : "Hash"; /* The steps keep the box alive as long as the stream. */ scr_crypto_hash_stream_set(hash, scr_stream_retain(s), scr_stream_release_v, box); scr_box_release(box); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 8358658b5..77c020ee0 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -115,6 +115,15 @@ static SCR_TL bool scr_sidx_cleanup_registered; * moves one active entry; only clearing or initializing changes the count. */ static SCR_TL size_t scr_sidx_active; +/* UTF-16 length table (scr_str_utf16_len below). */ +#define SCR_U16LEN_N 1024 +typedef struct { + const ScrStr *s; + size_t units; +} ScrU16Len; +/* Allocated by the first length it records; freed with the index. */ +static SCR_TL ScrU16Len *scr_u16len_tab; + static void scr_sidx_clear(ScrSidx *e, unsigned *live) { if (e->s) { scr_sidx_active--; @@ -137,6 +146,8 @@ static void scr_sidx_reset_all(void) { } scr_sidx_sparse_clock = 0; scr_sidx_cursor_clock = 0; + free(scr_u16len_tab); + scr_u16len_tab = NULL; } static void scr_sidx_register_cleanup(void) { @@ -190,7 +201,27 @@ static __attribute__((noinline)) void scr_sidx_purge_active(const ScrStr *s) { } } +/* UTF-16 lengths of non-ASCII strings longer than 64 bytes, direct-mapped + * by address. The four index entries above evict each other when a + * program reads the lengths of more long texts than that in turn (every + * source file of a program), and each miss walks the whole string. A slot + * is dropped wherever the index drops its facts: when the string is freed + * or reallocated, and when an append changes it in place. */ + +/* The slot for s, or NULL before any length is recorded. */ +static inline ScrU16Len *scr_u16len_slot(const ScrStr *s) { + if (!scr_u16len_tab) return NULL; + uint64_t h = ((uint64_t)(uintptr_t)s >> 4) * UINT64_C(0x9E3779B97F4A7C15); + return &scr_u16len_tab[h >> 54]; +} + +static inline void scr_u16len_forget(const ScrStr *s) { + ScrU16Len *slot = scr_u16len_slot(s); + if (slot && slot->s == s) slot->s = NULL; +} + static void scr_sidx_purge(const ScrStr *s) { + scr_u16len_forget(s); if (scr_sidx_active) scr_sidx_purge_active(s); } @@ -288,6 +319,7 @@ static ScrSidx *scr_sidx_for(const ScrStr *s) { * receiver may move from the cursor tier to the sparse tier on its next * lookup, so invalidate either possible entry. */ static void scr_sidx_concat_append(const ScrStr *s, size_t oldlen) { + scr_u16len_forget(s); if (scr_sidx_active == 0) return; for (int i = 0; i < SCR_SIDX_N; i++) { ScrSidx *entries[] = {&scr_sidx_sparse_tab[i], &scr_sidx_cursor_tab[i]}; @@ -1400,6 +1432,8 @@ static void scr_sidx_materialize_identity_prefix(const ScrStr *s, ScrSidx *e) { static size_t scr_sidx_len(const ScrStr *s, ScrSidx *e) { if (s->ascii) return s->len; if (e->u16len != SCR_U16_UNKNOWN) return e->u16len; + ScrU16Len *slot = scr_u16len_slot(s); + if (slot && slot->s == s) return e->u16len = slot->units; while (e->indexed_cb < s->len) scr_sidx_extend_one(s, e); scr_sidx_finish(s, e); return s->ascii ? s->len : e->u16len; @@ -1899,7 +1933,20 @@ double scr_str_utf16_len(ScrStr *s) { ScrSidx *last = scr_sidx_last; /* a scan's receiver, length known */ if (last && last->s == s && last->u16len != SCR_U16_UNKNOWN) return (double)(int64_t)last->u16len; /* lengths fit int64_t */ - return (double)scr_sidx_len(s, scr_sidx(s)); + ScrU16Len *slot = scr_u16len_slot(s); + if (slot && slot->s == s) return (double)(int64_t)slot->units; + size_t units = scr_sidx_len(s, scr_sidx(s)); + /* The walk may prove the string ASCII, which needs no slot. */ + if (s->ascii) return (double)(int64_t)units; + if (!scr_u16len_tab) { + /* Without memory the lengths are counted on each read, as before. */ + scr_u16len_tab = calloc(SCR_U16LEN_N, sizeof *scr_u16len_tab); + if (!scr_u16len_tab) return (double)(int64_t)units; + scr_sidx_register_cleanup(); + slot = scr_u16len_slot(s); + } + *slot = (ScrU16Len){s, units}; + return (double)(int64_t)units; } double scr_str_char_code_at(ScrStr *s, double i) { diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 53041f3d5..b4efa3e0d 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -122,7 +122,7 @@ static ScrClosure *scr_worker_stdin_callback(ScrWorkerThread *thread, void *fire } static ScrStream *scr_worker_stdin_new(ScrWorkerThread *thread) { - return scr_worker_stdio_ops->writable( + return scr_worker_stdio_ops->writable("WritableWorkerStdio", scr_worker_stdin_callback(thread, (void *)scr_worker_stdin_write), scr_worker_stdin_write, scr_worker_stdin_callback(thread, (void *)scr_worker_stdin_final), scr_worker_stdin_final); } @@ -1350,7 +1350,8 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt for (int i = 0; i < 2; i++) { view->stdio_captured[i] = (capture >> i) & 1; /* without the stream unit nothing can read the stream: the output is dropped */ - if (view->stdio_captured[i] && scr_worker_stdio_ops) view->stdio[i] = scr_worker_stdio_ops->readable(); + if (view->stdio_captured[i] && scr_worker_stdio_ops) + view->stdio[i] = scr_worker_stdio_ops->readable("ReadableWorkerStdio"); } view->stdin_captured = capture & 4; if (view->stdin_captured && scr_worker_stdio_ops) view->stdin_stream = scr_worker_stdin_new(thread); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index ce1e9ba85..d4ea39a93 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -1054,6 +1054,35 @@ static void check_rebuffer_past_mature(void) { * cycle-headered count has grown by SCR_CYC_NURSERY_ALLOCATIONS (4096 in * scr_cycle.c) since the last one, so the dead nodes left uncollected stay * within that much plus the ring in hand. */ +size_t scr_cyc_backlog_threshold_at(unsigned gen); +void scr_cyc_forget_backlog_at(unsigned gen); + +/* A heap that grows while its mature buffer stays empty: every release is + * of one old object, so the mature passes that growth brings have no + * candidates and only widen the mature growth fraction. Until a mature pass + * has had candidates, the backlog threshold widens with it, so a program + * that then reads the structure it built does not walk a quarter of it at + * once to find nothing. */ +static void check_unjudged_backlog_follows_growth(void) { + enum { LIVE = 65536 }; + Node **leaves = calloc(LIVE, sizeof(*leaves)); + if (!leaves) scr_trap("cycle test: out of memory\n"); + Node *anchor = make_leaf(); + age_to_old(anchor); + scr_cyc_forget_backlog_at(SCR_CYC_MATURE); + for (size_t i = 0; i < LIVE; i++) { + leaves[i] = make_leaf(); + read_live(anchor); + } + check(scr_cyc_backlog_threshold_at(SCR_CYC_MATURE) > scr_cyc_live / 4, + "an unjudged mature backlog stayed at a quarter of the live heap"); + drop_leaves(leaves, LIVE); + anchor->rc--; + scr_cyc_on_dead(anchor); + node_free(anchor); + free(leaves); +} + static void check_allocation_growth_runs_passes(void) { enum { RINGS = 80, NODES_PER_RING = 64, ALLOCATION_WINDOW = 4096 }; scr_collect_cycles(); @@ -1364,6 +1393,7 @@ int main(void) { check_publication_takes_candidates(); check_limited_pass(); check_large_old_buffer_with_nursery_candidates(); + check_unjudged_backlog_follows_growth(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 063af2ab9..55eb65e72 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -1,4 +1,5 @@ import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; // Case-sharded harnesses run in every job without Vitest file sharding. export const effectFiles = ["tests/harness/effect.test.ts", "tests/harness/effect4.test.ts"]; @@ -97,7 +98,28 @@ export const darwinFiles = [ "tests/harness/terminal-geometry.test.ts", ]; -export function ciTestPlan({ shard, flavor, workers = 4, suite = "full" }) { +// LeakSanitizer over the sanitized corpus on Linux (macOS ASan has none, and +// the workflow keeps it off everywhere else): a leak fails its program. The +// suppressions file lists intentional leaks, with the reason for each. +export const leakSuppressions = fileURLToPath( + new URL("../tests/harness/lsan.supp", import.meta.url), +); + +function corpusLeakEnv(flavor, platform) { + if (flavor !== "san" || platform !== "linux") return {}; + return { + ASAN_OPTIONS: "detect_leaks=1", + LSAN_OPTIONS: `suppressions=${leakSuppressions}:print_suppressions=0`, + }; +} + +export function ciTestPlan({ + shard, + flavor, + workers = 4, + suite = "full", + platform = process.platform, +}) { const match = /^([1-9]\d*)\/([1-9]\d*)$/.exec(shard); if ( !match || @@ -194,6 +216,7 @@ export function ciTestPlan({ shard, flavor, workers = 4, suite = "full" }) { args: ["test", ...corpusFiles], env: { ...env, + ...corpusLeakEnv(flavor, platform), SCRIPTC_TEST_WORKERS: String(workers - sideWorkers), SCRIPTC_LLVM_TEST_MODE: "dev", }, diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 871754258..426be3143 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -391,9 +391,9 @@ export function dynamicTierSymbols(names) { /* Bench workloads whose release binaries link dynamic-tier symbols on * b14377864: JSON.parse trees, worker messages, regex exec metadata * (two-tier §3.3 L5), and a shared scr_dyn_call/scr_dyn_define_property core - * not yet traced to its typed root. bench/thread-slots, added later, links - * them through worker messages: node:worker_threads values are dyn objects. - * Every other bench workload is fully typed and must link none. The gate and + * not yet traced to its typed root. bench/thread-slots and + * bench/channel-messages, added later, link them through worker messages: + * node:worker_threads values are dyn objects. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -406,6 +406,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/atomics-contended", "bench/intern-table", "bench/thread-slots", + "bench/channel-messages", "bench/typed-array-scan-workers", "bench/log-summary", "bench/inventory-report", diff --git a/tests/corpus/5467-typed-array-loop-versions.ts b/tests/corpus/5467-typed-array-loop-versions.ts new file mode 100644 index 000000000..35a1da145 --- /dev/null +++ b/tests/corpus/5467-typed-array-loop-versions.ts @@ -0,0 +1,298 @@ +// Typed-array loops whose accesses are proved in bounds by one test before +// the loop run without per-element checks; every other entry runs the +// checked loop. Both must keep JavaScript's exact out-of-bounds behavior: +// reads of missing elements are undefined (NaN in arithmetic), writes are +// ignored, DataView accesses throw at the failing iteration, and +// fractional, negative and NaN indices never touch storage. + +const table: Int32Array = new Int32Array(256); +for (let n = 0; n < 256; n++) table[n] = (n * 2654435761) | 0; +const shortTable: Int32Array = new Int32Array(100); +for (let n = 0; n < 100; n++) shortTable[n] = n + 1; + +function masked(t: Int32Array, data: Uint8Array): number { + let h = 0; + for (let i = 0; i < data.length; i++) h = (h ^ t[(h ^ data[i]) & 0xff]) | 0; + return h; +} + +function maskedWrite(t: Int32Array, count: number): void { + for (let i = 0; i < count; i++) t[(i * 7) & 0xff] = i; +} + +const data = new Uint8Array(64); +for (let i = 0; i < data.length; i++) data[i] = (i * 37 + 11) & 0xff; +console.log("masked full", masked(table, data)); +console.log("masked short", masked(shortTable, data)); +const writable = new Int32Array(200); +maskedWrite(writable, 300); +console.log("masked write", writable[0], writable[7], writable[199], writable.length); + +// Counter plus an invariant base, in bounds and running off either end. +function shifted(src: Float64Array, out: Float64Array, base: number, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + out[base + i] = src[i] * 2; + sum += src[base + i - 1]; + } + return sum; +} + +const src = new Float64Array(8); +for (let i = 0; i < 8; i++) src[i] = i + 0.25; +for (const [base, n] of [ + [1, 4], + [0, 8], + [5, 8], + [-3, 6], + [2, 0], + [3, 2.5], +]) { + const out = new Float64Array(8); + const sum = shifted(src, out, base, n); + console.log("shifted", base, n, sum, Array.from(out).join(",")); +} + +// Descending loops, including ones that walk below zero. +function descending(a: Uint16Array, low: number): number { + let sum = 0; + for (let i = a.length - 1; i >= low; i--) { + a[i] = a[i] + 1000; + sum += a[i]; + } + return sum; +} +const u16 = new Uint16Array([65000, 1, 2, 3]); +console.log("descending", descending(u16, 0), Array.from(u16).join(",")); +console.log("descending below", descending(u16, -2), Array.from(u16).join(",")); + +// Fractional starts and offsets never name an element. +function fractional(a: Float32Array): string { + const seen: string[] = []; + for (let i = 0.5; i < 3; i++) seen.push(String(a[i])); + for (let i = 0; i < 3; i++) { + a[i + 0.5] = 99; + seen.push(String(a[i] + a[i - 0.5])); + } + return seen.join(" "); +} +console.log("fractional", fractional(new Float32Array([1.5, 2.5, 3.5]))); + +function nanIndex(a: Int8Array, offset: number): number { + let sum = 0; + for (let i = 0; i < a.length; i++) { + a[i + offset] = 120; + sum += a[i + offset] | 0; + } + return sum; +} +const i8 = new Int8Array([1, 2, 3]); +console.log("nan index", nanIndex(i8, NaN), nanIndex(i8, -0), Array.from(i8).join(",")); + +// Unproven limits keep the integer loop's guard: huge, infinite and NaN. +function limits(a: Uint8Array, limit: number): number { + let sum = 0; + for (let i = 0; i < limit; i++) { + if (i >= a.length + 2) break; + sum += a[i] | 0; + a[i] = i; + } + return sum; +} +const lim = new Uint8Array([5, 6, 7]); +console.log("limits", limits(lim, 2), limits(lim, 1e300), limits(lim, Infinity), limits(lim, NaN)); + +// A DataView access past the end throws after the earlier iterations ran. +const viewProgress: number[] = [0]; +function viewLoop(view: DataView, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + sum += view.getUint32(i * 4, true) + view.getUint8(i * 4 + 3); + viewProgress[0] = sum; + } + return sum; +} +function viewSum(view: DataView, n: number): number { + viewProgress[0] = -1; + try { + return viewLoop(view, n); + } catch (e) { + console.log("view throw", (e as Error).name, viewProgress[0]); + } + return viewProgress[0]; +} +const dvBytes = new Uint8Array(16); +for (let i = 0; i < 16; i++) dvBytes[i] = i * 3; +const view = new DataView(dvBytes.buffer); +console.log("view ok", viewSum(view, 4)); +console.log("view partial", viewSum(view, 6)); + +// Strided loops: the last counter value of a non-unit step is exact, so a +// record that ends at the last element still takes the unchecked loop and +// a partial trailing record does not. +function quads(words: Uint16Array): number { + let sum = 0; + for (let i = 0; i < words.length; i += 4) sum += words[i] + words[i + 3] * 2; + return sum; +} +console.log("quads", quads(new Uint16Array([1, 2, 3, 4, 5, 6, 7, 8]))); +console.log("quads partial", quads(new Uint16Array([1, 2, 3, 4, 5, 6]))); +function backTriples(words: Uint16Array): number { + let sum = 0; + for (let i = words.length - 1; i >= 0; i -= 3) sum = sum * 3 + words[i] + (words[i - 2] | 0); + return sum; +} +console.log("back triples", backTriples(new Uint16Array([1, 2, 3, 4, 5, 6]))); +console.log("back triples partial", backTriples(new Uint16Array([1, 2, 3, 4, 5, 6, 7]))); +function scaled(words: Uint16Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) sum += words[i * 2] - (words[(i + 1) * 2 - 1] | 0) + (words[9 - i] | 0); + return sum; +} +const scaledWords = new Uint16Array(10); +for (let i = 0; i < 10; i++) scaledWords[i] = i * i; +console.log("scaled", scaled(scaledWords, 5), scaled(scaledWords, 6), scaled(scaledWords, 11)); +// A negative zero index names element 0 and stays -0 as a number. +function negativeZero(words: Uint16Array, n: number): string { + const seen: string[] = []; + for (let i = 0; i < n; i++) { + const k = i * -1; + const m = (i - 1) * 2; + seen.push(`${words[k]}:${Object.is(k, -0)}:${words[m] | 0}:${Object.is(m, -0)}`); + } + return seen.join(" "); +} +console.log("negative zero", negativeZero(scaledWords, 3)); + +// Clamped and wrapping element types on the unchecked path. +function convert(c: Uint8ClampedArray, s: Int16Array): string { + for (let i = 0; i < 4; i++) { + c[i] = i * 100 - 50.5; + s[i] = i * 20000; + } + return Array.from(c).join(",") + " " + Array.from(s).join(","); +} +console.log("convert", convert(new Uint8ClampedArray(4), new Int16Array(4))); + +// Module receivers stay correct across calls in the loop body, including a +// call that writes the same arrays. +const left: Float64Array = new Float64Array(16); +const right: Float64Array = new Float64Array(16); +function poke(k: number): number { + right[k] = right[k] + 1; + return k; +} +for (let i = 0; i < 16; i++) left[i] = i; +let crossed = 0; +for (let i = 0; i < 16; i++) { + right[i] = left[i] * 3; + crossed += right[poke(i)]; +} +console.log("module receivers", crossed, right[0], right[15]); + +// A body that reassigns its receiver keeps reading the current binding. +function rebinding(first: Uint8Array, second: Uint8Array): number { + let current = first; + let sum = 0; + for (let i = 0; i < 4; i++) { + sum += current[i] | 0; + if (i === 1) current = second; + } + return sum; +} +console.log("rebinding", rebinding(new Uint8Array([1, 2, 3, 4]), new Uint8Array([10, 20]))); + +// A throwing body leaves the writes of earlier iterations in place. +function failing(a: Uint32Array, at: number): void { + for (let i = 0; i < a.length; i++) { + if (i === at) throw new Error("stop at " + i); + a[i] = 4294967295 - i; + } +} +const u32 = new Uint32Array(5); +try { + failing(u32, 3); +} catch (e) { + console.log((e as Error).message, Array.from(u32).join(",")); +} + +// An outer loop proved in bounds around an inner loop that is not. +function nested(grid: Int32Array, w: number, h: number): number { + let sum = 0; + for (let y = 0; y < h; y++) { + sum += grid[y & 7]; + for (let x = 0; x < w; x++) { + grid[y * w + x] = grid[y * w + x] + 1; + sum += grid[y * w + x] | 0; + } + } + return sum; +} +const grid = new Int32Array(16); +console.log("nested", nested(grid, 4, 4), nested(grid, 5, 4), nested(grid, 4, 5), grid.join(",")); + +// A matrix product over module-level constant arrays. +const N = 6; +const ma: Float64Array = new Float64Array(N * N); +const mb: Float64Array = new Float64Array(N * N); +const mc: Float64Array = new Float64Array(N * N); +for (let i = 0; i < N * N; i++) { + ma[i] = (i % 7) - 3; + mb[i] = (i % 5) * 0.5; +} +for (let i = 0; i < N; i++) { + for (let k = 0; k < N; k++) { + const aik = ma[i * N + k]; + const kOff = k * N; + const iOff = i * N; + for (let j = 0; j < N; j++) mc[iOff + j] += aik * mb[kOff + j]; + } +} +let trace = 0; +for (let i = 0; i < N; i++) trace += mc[i * N + i]; +console.log("matmul", trace, mc[1], mc[N * N - 1]); + +// Float16Array elements are two bytes; versioned loops bound them in +// elements, and stores round to nearest-even in the unchecked copy too. +function halves(src: Float16Array, out: Float16Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + out[i] = src[i + 1] * 1.001 + 0.1; + sum += src[i]; + } + return sum; +} +const halfSrc = new Float16Array(10); +for (let i = 0; i < halfSrc.length; i++) halfSrc[i] = i * 0.3 - 1; +const halfOut = new Float16Array(10); +console.log("f16 in bounds", halves(halfSrc, halfOut, 9), Array.from(halfOut).join(",")); +console.log("f16 past end", halves(halfSrc, halfOut, 12), Array.from(halfOut).join(",")); +const halfShort = new Float16Array(4); +console.log("f16 short out", halves(halfSrc, halfShort, 9), Array.from(halfShort).join(",")); +console.log("f16 rounding", halves(new Float16Array([65519, 2049, 1e-8, 0]), new Float16Array(3), 3)); + +function halfTable(t: Float16Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) sum += t[i & 7]; + return sum; +} +console.log("f16 masked", halfTable(new Float16Array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5]), 20)); +console.log("f16 masked short", halfTable(new Float16Array([0.5, 1.5, 2.5, 3.5, 4.5]), 20)); + +// DataView half-precision accesses are two bytes wide: the last odd offset +// throws. +function halfView(v: DataView, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + v.setFloat16(i * 2, i + 0.25, true); + sum += v.getFloat16(i * 2 + 1, false); + } + return sum; +} +const halfBytes = new DataView(new ArrayBuffer(11)); +console.log("f16 view", halfView(halfBytes, 4)); +try { + halfView(halfBytes, 6); +} catch (e) { + console.log((e as Error).message, new Uint8Array(halfBytes.buffer).join(",")); +} diff --git a/tests/corpus/5468-typed-array-loop-aliasing.ts b/tests/corpus/5468-typed-array-loop-aliasing.ts new file mode 100644 index 000000000..019913d1c --- /dev/null +++ b/tests/corpus/5468-typed-array-loop-aliasing.ts @@ -0,0 +1,107 @@ +// Element stores may alias any other view of the same buffer, whatever its +// element type, offset or length, but never a typed array's own length and +// data pointer. Loops that write through one view and read through another +// must observe every store in program order, also when the loop runs +// without per-element bounds checks. + +const buffer = new ArrayBuffer(64); +const f64 = new Float64Array(buffer); +const u8 = new Uint8Array(buffer); +const i32 = new Int32Array(buffer, 8, 8); + +// Writes through Float64Array observed through a Uint8Array in the same +// iteration. +let topBytes = 0; +for (let i = 0; i < f64.length; i++) { + f64[i] = -(i + 1) * 1.5; + topBytes += u8[i * 8 + 7]; +} +console.log("f64 then u8", topBytes, u8[7], u8[63]); + +// Byte stores observed through an overlapping Int32Array window. +let words = 0; +for (let i = 0; i < 32; i++) { + u8[8 + i] = i + 1; + words = (words + i32[i >> 2]) | 0; +} +console.log("u8 then i32", words, i32[0], i32[7]); + +// A shifted subarray reads what the previous iteration wrote: a prefix +// recurrence that any reordering or vector widening would break. +const chain = new Uint8Array(40); +chain[0] = 1; +const ahead = chain.subarray(1); +for (let i = 0; i < ahead.length; i++) ahead[i] = chain[i] + 2; +console.log("recurrence", chain[0], chain[1], chain[20], chain[39]); + +// Overlapping copies in both directions. +const values = new Int16Array(12); +for (let i = 0; i < values.length; i++) values[i] = i * 10; +const lower = values.subarray(0, 11); +const upper = values.subarray(1); +for (let i = 0; i < lower.length; i++) lower[i] = upper[i]; +console.log("copy down", Array.from(values).join(",")); +for (let i = 0; i < values.length; i++) values[i] = i * 10; +for (let i = 0; i < lower.length; i++) upper[i] = lower[i]; +console.log("copy up", Array.from(values).join(",")); + +// Same-array accumulation with a lagging read. +function lagged(a: Float64Array, lag: number): number { + for (let i = lag; i < a.length; i++) a[i] = a[i] + a[i - lag]; + return a[a.length - 1]; +} +const fib = new Float64Array(30); +fib[0] = 1; +fib[1] = 1; +console.log("lagged", lagged(fib, 1), lagged(fib, 2)); + +// Two parameters that are the same array, and a destination that aliases +// part of its own source. +function axpy(alpha: number, x: Float64Array, y: Float64Array, n: number): void { + for (let i = 0; i < n; i++) y[i] = y[i] + alpha * x[i]; +} +const same = new Float64Array([1, 2, 3, 4, 5, 6, 7, 8]); +axpy(2, same, same, same.length); +console.log("axpy same", Array.from(same).join(",")); +const whole = new Float64Array([1, 2, 3, 4, 5, 6, 7, 8]); +axpy(1, whole, whole.subarray(2), 6); +console.log("axpy shifted", Array.from(whole).join(",")); + +// Element stores never change a length or a module binding, even when a +// float store writes the bit pattern of a small length. +const meta: Float64Array = new Float64Array(4); +const lengths: number[] = []; +for (let i = 0; i < 4; i++) { + meta[i] = 5e-324 * (i + 1); + lengths.push(meta.length); +} +console.log("lengths", lengths.join(","), meta[3] > 0); + +// Global numbers next to typed-array stores keep their values. +let scale = 3; +const scaled: Float32Array = new Float32Array(6); +for (let i = 0; i < scaled.length; i++) { + scaled[i] = scale * i; + if (i === 2) scale = 0.5; +} +console.log("scaled", Array.from(scaled).join(",")); + +// Half-precision stores observed through integer views of the same buffer +// in the same iteration, and byte stores observed through Float16Array. +const halfBuffer = new ArrayBuffer(32); +const f16 = new Float16Array(halfBuffer); +const u16 = new Uint16Array(halfBuffer); +const halfBytes = new Uint8Array(halfBuffer); +let bits = 0; +for (let i = 0; i < f16.length; i++) { + f16[i] = (i - 8) * 0.75; + bits = (bits + u16[i]) | 0; +} +console.log("f16 then u16", bits, u16[0], u16[15]); +let halfSum = 0; +for (let i = 0; i < f16.length; i++) { + halfBytes[i * 2 + 1] = 0x3c + (i & 3); + halfBytes[i * 2] = i; + halfSum += f16[i]; +} +console.log("u8 then f16", halfSum, f16[0], f16[15]); diff --git a/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts b/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts new file mode 100644 index 000000000..f899b9d07 --- /dev/null +++ b/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts @@ -0,0 +1,46 @@ +// A typed-array loop whose initializer rebinds the array it indexes: the +// bounds the loop relies on belong to the array the initializer installs, +// so reads past the shorter replacement are undefined (NaN in arithmetic) +// and writes past it are ignored, as in JavaScript. + +function sumAfterRebinding(data: Uint8Array, replacement: Uint8Array, n: number): number { + let total = 0; + for (let i = ((data = replacement), 0); i < n; i++) total += data[i]; + return total; +} + +function fillAfterRebinding(data: Int32Array, replacement: Int32Array, n: number): Int32Array { + for (let i = ((data = replacement), 0); i < n; i++) data[i] = i + 1; + return data; +} + +// The initializer assigns the receiver without declaring the counter. +function countAfterAssignment(data: Float64Array, replacement: Float64Array, n: number): number { + let i: number; + let total = 0; + for (data = replacement, i = 0; i < n; i++) total += data[i]; + return total; +} + +const long = new Uint8Array(64).fill(1); +const short = new Uint8Array(4).fill(2); +console.log("sum", sumAfterRebinding(long, short, 64)); +console.log("sum in bounds", sumAfterRebinding(long, short, 4)); + +const target = new Int32Array(64); +const small = new Int32Array(3); +const filled = fillAfterRebinding(target, small, 64); +console.log("fill", filled.length, Array.from(filled).join(","), target[0], target[63]); + +const wide = new Float64Array(32).fill(0.5); +const narrow = new Float64Array(2).fill(4); +console.log("count", countAfterAssignment(wide, narrow, 32)); +console.log("count in bounds", countAfterAssignment(wide, narrow, 2)); + +// A loop whose initializer leaves the receiver alone still runs normally. +function plainSum(data: Uint8Array, n: number): number { + let total = 0; + for (let i = 0; i < n; i++) total += data[i]; + return total; +} +console.log("plain", plainSum(long, 64), plainSum(short, 6)); diff --git a/tests/corpus/5607-string-char-compare.ts b/tests/corpus/5607-string-char-compare.ts new file mode 100644 index 000000000..f80581c10 --- /dev/null +++ b/tests/corpus/5607-string-char-compare.ts @@ -0,0 +1,37 @@ +// Comparing one character of a string with a one-unit literal: index and +// charAt forms, === and !==, the literal on either side, indexes out of +// range, negative, fractional and NaN, multi-byte and astral text, lone +// surrogates, and literals of other lengths. +function bits(s: string, i: number): string { + return [ + s[i] === "_", s.charAt(i) === "_", "_" === s.charAt(i), s.charAt(i) !== "_", s[i] !== "_", + s[i] === "a", s.charAt(i) === "a", "a" === s.charAt(i), s.charAt(i) !== "a", s[i] !== "a", + s[i] === "é", s.charAt(i) === "é", "é" === s.charAt(i), s.charAt(i) !== "é", s[i] !== "é", + s[i] === "\uD83D", s.charAt(i) === "\uD83D", "\uD83D" === s.charAt(i), s.charAt(i) !== "\uD83D", s[i] !== "\uD83D", + s[i] === "\uDE00", s.charAt(i) === "\uDE00", "\uDE00" === s.charAt(i), s.charAt(i) !== "\uDE00", s[i] !== "\uDE00", + s[i] === "\uD800", s.charAt(i) === "\uD800", "\uD800" === s.charAt(i), s.charAt(i) !== "\uD800", s[i] !== "\uD800", + s[i] === "語", s.charAt(i) === "語", "語" === s.charAt(i), s.charAt(i) !== "語", s[i] !== "語", + s[i] === "", s.charAt(i) === "", "" === s.charAt(i), s.charAt(i) !== "", s[i] !== "", + s[i] === "ab", s.charAt(i) === "ab", "ab" === s.charAt(i), s.charAt(i) !== "ab", s[i] !== "ab", + ] + .map((b) => (b ? "1" : "0")) + .join(""); +} +const samples: string[] = ["a_b", "", "é_ü", "x😀y", "\uD800z", "日本_語", "ab"]; +const indexes = [0, 1, 2, 3, 4, -1, 1.5, NaN, 100]; +for (const s of samples) console.log(JSON.stringify(s), indexes.map((i) => bits(s, i)).join(" ")); + +function countUnderscores(text: string): number { + let n = 0; + for (let i = 0; i < text.length; i++) if (text[i] === "_") n++; + return n; +} +function firstSpace(text: string): number { + for (let i = 0; i < text.length; i++) if (text.charAt(i) === " ") return i; + return -1; +} +console.log(countUnderscores("a_b_c__d"), countUnderscores(""), firstSpace("type Foo = Bar"), firstSpace("none")); +let k = 0; +const s = "ab"; +const at = (): number => k++; +console.log(s.charAt(at()) === "a", s.charAt(at()) === "a", k); diff --git a/tests/corpus/5608-string-char-switch.ts b/tests/corpus/5608-string-char-switch.ts new file mode 100644 index 000000000..8ea54f1f5 --- /dev/null +++ b/tests/corpus/5608-string-char-switch.ts @@ -0,0 +1,95 @@ +// switch on one character of a string with one-unit case literals: the +// index and charAt forms, indexes out of range, negative, fractional and +// NaN, astral text and lone surrogates, duplicate cases, fallthrough, a +// switch without default, few and many cases, and an index expression +// with a side effect. +function classify(ch: string): string { + return ch; +} +function kind(s: string, i: number): string { + switch (s[i]) { + case "(": + case ")": + return "paren"; + case "{": + return "brace"; + case "é": + return "e-acute"; + case "\uD83D": + return "high"; + case "\uDE00": + return "low"; + case "\uD800": + return "lone"; + case "(": + return "duplicate"; + case " ": + return "space"; + default: + return "other"; + } +} +function few(s: string, i: number): number { + let n = 0; + switch (s.charAt(i)) { + case "a": + n += 1; + case "b": + n += 2; + break; + case "c": + n += 4; + } + return n; +} +const samples = ["(x) {é}", "😀", "\uD800", "", "abc", " "]; +const indexes = [0, 1, 2, 3, -1, 0.5, NaN, 100]; +for (const s of samples) { + console.log( + JSON.stringify(s), + indexes.map((i) => kind(s, i)).join(","), + indexes.map((i) => few(s, i)).join(","), + ); +} +let calls = 0; +const text = "abc"; +const next = (): number => calls++; +for (let r = 0; r < 4; r++) { + switch (text.charAt(next())) { + case "a": + console.log("a", calls); + break; + case "c": + console.log("c", calls); + break; + default: + console.log("default", calls); + } +} +function tokens(text: string): string { + const out: string[] = []; + for (let i = 0; i < text.length; i++) { + switch (text[i]) { + case "(": + case ")": + out.push("p"); + break; + case "+": + case "-": + case "*": + case "/": + out.push("o"); + break; + case "\uD83D": + out.push("h"); + break; + case " ": + break; + default: + out.push(text[i]); + } + } + return out.join(""); +} +console.log(tokens("(a + b) * c / d - é"), tokens("x😀y"), tokens("")); +console.log(classify("ok")); diff --git a/tests/corpus/5609-string-switch-optional.ts b/tests/corpus/5609-string-switch-optional.ts new file mode 100644 index 000000000..20e36b739 --- /dev/null +++ b/tests/corpus/5609-string-switch-optional.ts @@ -0,0 +1,57 @@ +// switch on a string | undefined value with string literal cases: array +// reads, optional fields and Map lookups that may be undefined, matching +// and missing values, the empty string, non-ASCII and long literals, +// duplicate cases, fallthrough, default in the middle, and few cases. +interface Token { + text?: string; +} +function kind(t: string | undefined): string { + switch (t) { + case "function": + case "class": + return "decl"; + case "": + return "empty"; + case "naïve": + return "unicode"; + case "a-much-longer-keyword-than-eight-bytes": + return "long"; + case "function": + return "duplicate"; + default: + return t === undefined ? "missing" : "other"; + case "return": + return "flow"; + } +} +function fallthrough(t: string | undefined): number { + let n = 0; + switch (t) { + case "a": + n += 1; + case "b": + n += 2; + break; + default: + n += 8; + case "c": + n += 4; + } + return n; +} +function few(t: string | undefined): string { + switch (t) { + case "x": + return "x"; + } + return "none"; +} +const words = ["function", "class", "", "naïve", "a-much-longer-keyword-than-eight-bytes", "return", "other", "Function"]; +for (let i = 0; i <= words.length; i++) { + const w: string | undefined = words[i]; + console.log(i, kind(w), fallthrough(w), few(w)); +} +const tokens: Token[] = [{ text: "a" }, {}, { text: "c" }, { text: "x" }]; +console.log(tokens.map((t) => `${kind(t.text)}:${fallthrough(t.text)}:${few(t.text)}`).join(" ")); +const table = new Map([["k", "return"], ["e", ""]]); +console.log(["k", "e", "z"].map((key) => kind(table.get(key))).join(" ")); diff --git a/tests/corpus/5730-thread-slots-widened-values.ts b/tests/corpus/5730-thread-slots-widened-values.ts new file mode 100644 index 000000000..88a1803e7 --- /dev/null +++ b/tests/corpus/5730-thread-slots-widened-values.ts @@ -0,0 +1,45 @@ +import { ThreadSlots } from "@scriptc/experimental"; + +// A ThreadSlots const annotated with a structurally wider value type than +// its constructor's: the values are stored with the constructor's layout and +// read back through the annotation's. Node.js prints the same. + +class Key { + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +interface Counted { + count: number; + label: string; +} + +interface Labeled { + label: string; +} + +interface Tagged { + label: string; + tag?: string; +} + +const counted: ThreadSlots = new ThreadSlots("widened.counted"); +const tagged: ThreadSlots = new ThreadSlots("widened.tagged"); + +const first = new Key(1); +const second = new Key(2); +const value: Counted = { count: 7, label: "seven" }; +counted.set(first, value); +const read = counted.get(first)!; +console.log(read.label, counted.get(second), counted.has(first)); + +tagged.set(second, { count: 2, label: "two" } as Counted); +const t = tagged.get(second); +console.log(t?.label, t?.tag, tagged.get(first)); + +function labelOf(slots: Labeled | undefined): string { + return slots === undefined ? "none" : slots.label; +} +console.log(labelOf(counted.get(first)), labelOf(counted.get(second))); diff --git a/tests/corpus/5750-string-from-char-codes.ts b/tests/corpus/5750-string-from-char-codes.ts new file mode 100644 index 000000000..6229e6d81 --- /dev/null +++ b/tests/corpus/5750-string-from-char-codes.ts @@ -0,0 +1,27 @@ +// String.fromCharCode with several arguments: ASCII and non-ASCII units, +// surrogate pairs that combine, lone and reversed surrogates, ToUint16 of +// large, negative, fractional and NaN codes, zero to seventeen arguments, +// and the spread form. +const show = (s: string): string => + `${JSON.stringify(s)}:${s.length}:${Array.from(s, (c) => c.codePointAt(0)!.toString(16)).join(".")}`; +console.log(show(String.fromCharCode())); +console.log(show(String.fromCharCode(104, 105))); +console.log(show(String.fromCharCode(0xe9, 0x65e5, 0x41))); +console.log(show(String.fromCharCode(0xd83d, 0xde00))); +console.log(show(String.fromCharCode(0xde00, 0xd83d))); +console.log(show(String.fromCharCode(0xd800, 0x41, 0xdc00))); +console.log(show(String.fromCharCode(0x10041, -1, 65.9, NaN, 2 ** 32 + 66))); +console.log( + show(String.fromCharCode(97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112)), +); +console.log( + show(String.fromCharCode(97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113)), +); +const codes = [0x48, 0xd83d, 0xde00, 0x21]; +console.log(show(String.fromCharCode(...codes))); +let out = ""; +for (let i = 0; i < 26; i++) out += String.fromCharCode(97 + i, 65 + i); +console.log(out, out.length); +let calls = 0; +const next = (): number => 65 + calls++; +console.log(String.fromCharCode(next(), next(), next()), calls); diff --git a/tests/corpus/5751-string-length-cache.ts b/tests/corpus/5751-string-length-cache.ts new file mode 100644 index 000000000..e0e5f8c67 --- /dev/null +++ b/tests/corpus/5751-string-length-cache.ts @@ -0,0 +1,36 @@ +// .length of long non-ASCII strings: many texts read in turn, appends in +// place (including a lone high surrogate completed by a low one), strings +// freed and rebuilt at reused addresses, split pieces, and slices and +// searches on texts whose lengths were read before. +let unit = ""; +while (unit.length < 300) unit += "naïve 日本語 "; +const texts: string[] = []; +for (let i = 0; i < 40; i++) texts.push(unit.repeat(1 + (i % 7)) + "é".repeat(i)); +let sum = 0; +for (let r = 0; r < 3; r++) for (const t of texts) sum += t.length; +console.log(sum, texts[39].length, texts[0].length); + +let grow = unit; +const lengths: number[] = []; +for (let i = 0; i < 20; i++) { + grow += i % 2 ? "ü" : "x"; + lengths.push(grow.length); +} +console.log(lengths.join(",")); +let pair = unit + "\uD83D"; +const before = pair.length; +pair += "\uDE00"; +console.log(before, pair.length, pair.codePointAt(pair.length - 2)!.toString(16)); + +const seen: number[] = []; +for (let i = 0; i < 50; i++) { + const temp = unit + "ß".repeat(i % 5) + i; + seen.push(temp.length); +} +console.log(seen.slice(0, 10).join(","), seen.reduce((a, b) => a + b, 0)); + +const parts = (unit + "|").repeat(30).split("|"); +console.log(parts.length, parts[0].length, parts.map((p) => p.length).reduce((a, b) => a + b, 0)); + +const big = texts[39]; +console.log(big.slice(big.length - 5), big.substring(10, 20).length, big.indexOf("日本"), big.lastIndexOf("é"), big.length); diff --git a/tests/corpus/5775-md5-lengths.ts b/tests/corpus/5775-md5-lengths.ts new file mode 100644 index 000000000..957491568 --- /dev/null +++ b/tests/corpus/5775-md5-lengths.ts @@ -0,0 +1,34 @@ +// MD5 and md5-sha1 over every length from 0 to 520 bytes, fed whole and +// in 1-, 7-, 64- and 100-byte updates, then over 1 MiB, and HMAC-MD5 +// over a few lengths. Each length's digests are folded into one running +// SHA-512 so the output stays short. +import { createHash, createHmac } from "node:crypto"; + +const data = Buffer.alloc(1 << 20); +let x = 0x9e3779b9; +for (let i = 0; i < data.length; i++) { + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + data[i] = x & 0xff; +} + +function digest(algorithm: string, input: Buffer, chunk: number): Buffer { + const hash = createHash(algorithm); + if (chunk === 0) hash.update(input); + else for (let at = 0; at < input.length; at += chunk) hash.update(input.subarray(at, at + chunk)); + return hash.digest(); +} + +for (const algorithm of ["md5", "md5-sha1"]) { + for (const chunk of [0, 1, 7, 64, 100]) { + const fold = createHash("sha512"); + for (let length = 0; length <= 520; length++) fold.update(digest(algorithm, data.subarray(0, length), chunk)); + console.log(algorithm, "chunk", chunk, fold.digest("hex").slice(0, 32)); + } + console.log(algorithm, "1 MiB", digest(algorithm, data, 0).toString("hex").slice(0, 32), digest(algorithm, data, 4096).toString("hex").slice(0, 32)); +} +console.log("abc", createHash("md5").update("abc").digest("hex")); +for (const length of [0, 1, 55, 56, 63, 64, 65, 119, 120, 1000]) { + console.log("hmac", length, createHmac("md5", "key").update(data.subarray(0, length)).digest("hex").slice(0, 32)); +} diff --git a/tests/corpus/5816-fixed-into-unknown-rest.ts b/tests/corpus/5816-fixed-into-unknown-rest.ts new file mode 100644 index 000000000..f6d287021 --- /dev/null +++ b/tests/corpus/5816-fixed-into-unknown-rest.ts @@ -0,0 +1,39 @@ +// A function with only `unknown` parameters, or none, flows into a +// `(...args: unknown[]) => R` slot: each call passes the callers' +// arguments in order, undefined for a parameter the caller left out, and +// drops the extra ones, as JS calls do. Sync and async functions, stored +// in variables and arrays and passed as arguments. +type Handler = (...args: unknown[]) => void; +type AsyncHandler = (...args: unknown[]) => Promise; + +const none: Handler = () => console.log("none"); +const one: Handler = (a: unknown) => console.log("one", a); +const two: Handler = (a: unknown, b: unknown) => console.log("two", a, b); + +none(); +none(1, 2, 3); +one(); +one("x"); +one("x", "y"); +two(1); +two(1, [2], { three: 3 }); + +const handlers: Handler[] = [none, one, two]; +for (const h of handlers) h("all", true); + +function dispatch(h: Handler, ...args: unknown[]): void { + h(...args); +} +dispatch(two, "spread", null); +dispatch(one); + +const first: AsyncHandler = async (value: unknown) => value; +const pair: AsyncHandler = async (a: unknown, b: unknown) => [a, b]; + +async function main(): Promise { + console.log(await first(10, 20)); + console.log(await first()); + console.log(await pair("p")); + console.log(await pair("p", "q", "r")); +} +main().then(() => console.log("done")); diff --git a/tests/corpus/5876-readline-dispose-instanceof.ts b/tests/corpus/5876-readline-dispose-instanceof.ts new file mode 100644 index 000000000..63f058e9b --- /dev/null +++ b/tests/corpus/5876-readline-dispose-instanceof.ts @@ -0,0 +1,28 @@ +// rl[Symbol.dispose]() closes the interface as close() does, and a second +// call does nothing. instanceof tells node:readline's Interface from +// node:readline/promises' one. +import * as readline from "node:readline"; +import * as readlinePromises from "node:readline/promises"; +import { PassThrough } from "node:stream"; + +const rl = readline.createInterface({ input: new PassThrough() }); +rl.on("close", () => console.log("close 1")); +rl[Symbol.dispose](); +console.log("disposed"); +rl[Symbol.dispose](); +rl.close(); +console.log("after second dispose"); + +const prl = readlinePromises.createInterface({ input: new PassThrough() }); +prl.on("close", () => console.log("close 2")); +console.log(rl instanceof readline.Interface, rl instanceof readlinePromises.Interface); +console.log(prl instanceof readline.Interface, prl instanceof readlinePromises.Interface); +prl[Symbol.dispose](); + +function make(): readline.Interface { + return readline.createInterface({ input: new PassThrough() }); +} +const made = make(); +made.on("close", () => console.log("close 3")); +made[Symbol.dispose](); +console.log("done"); diff --git a/tests/corpus/5877-readline-question-query.cjs b/tests/corpus/5877-readline-question-query.cjs new file mode 100644 index 000000000..734311b16 --- /dev/null +++ b/tests/corpus/5877-readline-question-query.cjs @@ -0,0 +1,51 @@ +// question() with a query that is not a string: a non-terminal interface +// throws (callback form) or rejects (readline/promises) with Node's +// ERR_INVALID_ARG_TYPE from the output write, and the question stays +// pending; a terminal interface writes the query as a string. +const readline = require("readline"); +const readlinePromises = require("readline/promises"); +const { PassThrough } = require("stream"); + +function capture(name) { + const output = new PassThrough(); + output.on("data", (d) => console.log(name, JSON.stringify(String(d)))); + return output; +} + +async function main() { + const input = new PassThrough(); + const rl = readline.createInterface({ input, output: capture("out") }); + rl.on("line", (line) => console.log("line", line)); + try { + rl.question(42, (answer) => console.log("answer", answer)); + } catch (e) { + console.log(e.name, e.code, e.message); + } + try { + rl.prompt(); + } catch (e) { + console.log("prompt", e.code, e.message); + } + input.write("first\nsecond\n"); + await new Promise((resolve) => setImmediate(resolve)); + console.log("prompt after", JSON.stringify(rl.getPrompt())); + rl.close(); + + for (const query of [undefined, null, { a: 1 }, [1, 2], true]) { + const prl = readlinePromises.createInterface({ input: new PassThrough() }); + try { + await prl.question(query); + } catch (e) { + console.log("rejected", e.code, e.message); + } + prl.close(); + } + + const tin = new PassThrough(); + const trl = readline.createInterface({ input: tin, output: capture("tty"), terminal: true }); + trl.question(7, (answer) => console.log("tty answer", answer)); + tin.write("x\r"); + await new Promise((resolve) => setImmediate(resolve)); + trl.close(); +} +main(); diff --git a/tests/corpus/5878-readline-dynamic-streams.mjs b/tests/corpus/5878-readline-dynamic-streams.mjs new file mode 100644 index 000000000..b1b9f4ee5 --- /dev/null +++ b/tests/corpus/5878-readline-dynamic-streams.mjs @@ -0,0 +1,41 @@ +// createInterface over an input or output that is a function's result, +// an object's member or an array element. +import * as readline from "node:readline"; +import * as readlinePromises from "node:readline/promises"; +import { PassThrough, Readable, Writable } from "node:stream"; + +function source(lines) { + return Readable.from([lines.join("\n") + "\n"]); +} +const streams = { input: source(["a", "b"]), output: new PassThrough() }; +streams.output.on("data", (d) => console.log("out", JSON.stringify(String(d)))); + +const rl1 = readline.createInterface({ input: source(["one", "two"]) }); +for await (const line of rl1) console.log("rl1", line); + +const rl2 = readline.createInterface({ input: streams.input, output: streams.output, prompt: "? " }); +rl2.prompt(); +for await (const line of rl2) console.log("rl2", line); + +const inputs = [source(["x"]), source(["y"])]; +for (const [i, input] of inputs.entries()) { + const rl = readline.createInterface(input); + rl.on("line", (line) => console.log("rl3", i, line)); + await new Promise((resolve) => { + rl.on("close", () => resolve(undefined)); + }); +} + +const sink = new Writable({ + write(chunk, _encoding, callback) { + console.log("sink", JSON.stringify(String(chunk))); + callback(); + }, +}); +const make = () => ({ input: new PassThrough(), output: sink }); +const opts = make(); +const prl = readlinePromises.createInterface(opts.input, opts.output); +const answer = prl.question("name? "); +opts.input.write("ada\n"); +console.log("answer", await answer); +prl.close(); diff --git a/tests/corpus/5891-typed-array-field-borrows.ts b/tests/corpus/5891-typed-array-field-borrows.ts new file mode 100644 index 000000000..cdd315191 --- /dev/null +++ b/tests/corpus/5891-typed-array-field-borrows.ts @@ -0,0 +1,68 @@ +// Typed-array locals read from fields and parameters: reads while nothing +// replaces the field, the field replaced directly or by a callee while the +// local is in use, rebound parameters, views and detached-length reads. + +class Scanner { + chars: Uint16Array; + pos = 0; + constructor(text: string) { + this.chars = Scanner.units(text); + } + static units(text: string): Uint16Array { + const out = new Uint16Array(text.length + 2); + for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i); + return out; + } + sum(): number { + const chars = this.chars; + let total = 0; + for (let i = 0; i < chars.length; i++) total += chars[i]!; + return total; + } + replaceThenRead(): number { + const chars = this.chars; + this.chars = Scanner.units("zz"); + return chars[0]! * 1000 + this.chars[0]!; + } + reset(text: string): void { + this.chars = Scanner.units(text); + } + readAcrossReset(): string { + const chars = this.chars; + const before = chars[0]!; + this.reset("q"); + return before + ":" + chars[1] + ":" + this.chars[0] + ":" + chars.length; + } +} + +function firstOf(units: Uint16Array, fallback: Uint16Array): number { + if (units.length === 0) units = fallback; + let n = 0; + while (n < units.length && units[n] !== 0) n++; + return n; +} + +function walk(a: Uint8Array): number { + let total = 0; + while (a.length > 1) { + total += a[0]!; + a = a.subarray(1); + } + return total + a.length; +} + +const s = new Scanner("hello"); +console.log(s.sum(), s.replaceThenRead(), s.sum()); +const t = new Scanner("abc"); +console.log(t.readAcrossReset(), t.sum()); +console.log(firstOf(new Uint16Array(0), Scanner.units("xyz")), firstOf(Scanner.units("ab"), new Uint16Array(1))); +console.log(walk(new Uint8Array([1, 2, 3, 4, 5])), walk(new Uint8Array(0))); + +const holders: Scanner[] = []; +for (let i = 0; i < 50; i++) holders.push(new Scanner("item" + i)); +let acc = 0; +for (const h of holders) { + acc += h.sum(); + acc += h.replaceThenRead(); +} +console.log(acc); diff --git a/tests/corpus/5916-dyn-error-stack-property.ts b/tests/corpus/5916-dyn-error-stack-property.ts new file mode 100644 index 000000000..2e1b42ef3 --- /dev/null +++ b/tests/corpus/5916-dyn-error-stack-property.ts @@ -0,0 +1,147 @@ +// Errors held as `any` or `unknown` have an own `stack` like Node's: a +// non-enumerable accessor that every error shares. The first read formats +// "Name: message" from the error's name and message at that point, and +// later reads answer the same string. Assigning a value replaces it, +// `delete` removes it, Object.keys, spread, Object.assign and +// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects +// that inherit from an error read the error's stack. Typed and dynamic +// reads answer the same string, and a stack read through `any` captures +// frames. Only the first line is printed: frame lines differ between +// runtimes. + +class ParseError extends Error { + constructor(message: string) { + super(message); + this.name = "ParseError"; + } +} + +class PlainSubclass extends Error {} + +function dyn(value: unknown): any { + return value; +} + +function firstLine(value: unknown): string { + return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`; +} + +function describe(label: string, error: any): void { + console.log( + label, + "|", + typeof error.stack, + firstLine(error.stack), + "|", + Object.keys(error).join(","), + JSON.stringify(error), + "|", + "stack" in error, + Object.hasOwn(error, "stack"), + error.propertyIsEnumerable("stack"), + ); +} + +try { + throw new RangeError("thrown"); +} catch (error) { + describe("caught", error); +} + +const unknownError: unknown = new TypeError("bad thing"); +describe("unknown", unknownError); +describe("plain", dyn(new Error("plain"))); +describe("no message", dyn(new SyntaxError())); +describe("subclass", dyn(new ParseError("line 3"))); +describe("plain subclass", dyn(new PlainSubclass("inherited name"))); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +describe("own property", coded); + +const descriptor = Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack")!; +const other = Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack")!; +console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable); +console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set); +console.log("foreign receiver", descriptor.get!.call({}), firstLine(descriptor.get!.call(dyn(new TypeError("b"))))); +const subclassDescriptor = Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack")!; +console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get); + +const renamed = dyn(new Error("before")); +renamed.message = "after"; +renamed.name = "Renamed"; +console.log("first read", firstLine(renamed.stack)); +renamed.message = "later"; +console.log("later read", firstLine(renamed.stack)); +const subclassRenamed = dyn(new PlainSubclass("before")); +subclassRenamed.message = "after"; +console.log("subclass first read", firstLine(subclassRenamed.stack)); + +const replaced = dyn(new Error("replaced")); +replaced.stack = "custom stack\n at nowhere"; +describe("assigned", replaced); +console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")!).join(",")); +const subclassReplaced = dyn(new ParseError("replaced")); +subclassReplaced.stack = "subclass stack"; +console.log("subclass assigned", subclassReplaced.stack); +const typedReplaced = new ParseError("typed write"); +typedReplaced.stack = "typed subclass stack"; +console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack); +const subclassNumbered = dyn(new PlainSubclass("numbered")); +subclassNumbered.stack = 7; +console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack); +subclassNumbered.stack = "string again"; +console.log("subclass string again", subclassNumbered.stack); +const numbered = dyn(new Error("numbered")); +numbered.stack = 5; +console.log("number", numbered.stack, typeof numbered.stack); +const removed = dyn(new Error("removed")); +delete removed.stack; +console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack")); + +const spreadSource = dyn(new Error("spread")); +console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length); +console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack)); + +const typed = new Error("typed"); +const view = dyn(typed); +console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack)); +const typedSubclass = new ParseError("typed subclass"); +console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack); +const assignedThroughView = new Error("typed"); +dyn(assignedThroughView).stack = "through the view"; +console.log("assigned through view", assignedThroughView.stack); + +function inner(): any { + return dyn(new Error("deep")); +} +function outer(): any { + return inner(); +} +const deep: string = outer().stack; +console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer")); +function makeSubclass(): any { + return dyn(new PlainSubclass("deep")); +} +console.log("subclass frames", (makeSubclass().stack as string).includes("at makeSubclass")); + +const formatted = dyn(new Error("formatted")); +console.log("formatted before clone", firstLine(formatted.stack)); +console.log("clone keeps a formatted stack", firstLine(dyn(structuredClone(formatted)).stack)); + +class GrumpyError extends Error { + override get name(): string { + throw new Error("no name today"); + } +} +try { + console.log("throwing name getter", firstLine(dyn(new GrumpyError("grumpy")).stack)); +} catch (error) { + console.log("throwing name getter threw", (error as Error).message); +} + +function readKey(target: any, key: string): unknown { + return target[key]; +} +const computed = readKey(inner(), ["sta", "ck"].join("")) as string; +console.log("computed key", firstLine(computed), computed.includes("at inner")); diff --git a/tests/corpus/5983-runtime-constructor.ts b/tests/corpus/5983-runtime-constructor.ts new file mode 100644 index 000000000..3fb9672d0 --- /dev/null +++ b/tests/corpus/5983-runtime-constructor.ts @@ -0,0 +1,48 @@ +// x.constructor on instances of EventEmitter and the stream classes: +// .name is the class Node reports (fs, zlib and program subclasses +// included), === compares with the instance's own class, and the value +// is the class object for EventEmitter and the stream classes. +import { EventEmitter } from "node:events"; +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; +import * as fs from "node:fs"; +import * as zlib from "node:zlib"; + +class Bus extends EventEmitter {} + +const emitter = new EventEmitter(); +const bus: EventEmitter = new Bus(); +console.log("emitter", emitter.constructor === EventEmitter, emitter.constructor.name); +console.log("subclass", bus.constructor === EventEmitter, bus.constructor.name); + +const streams: Readable[] = [new Readable(), new Duplex(), new Transform(), new PassThrough()]; +for (const s of streams) { + console.log( + "stream", + s.constructor.name, + s.constructor === Readable, + s.constructor === Duplex, + s.constructor === Transform, + s.constructor === PassThrough, + s.constructor !== Readable, + ); +} + +const from = Readable.from(["a"]); +console.log("from", from.constructor.name, from.constructor === Readable); + +const readStream: Readable = fs.createReadStream(import.meta.filename); +const writeStream: Writable = fs.createWriteStream("/dev/null"); +console.log("fs", readStream.constructor.name, readStream.constructor === Readable); +console.log("fs", writeStream.constructor.name, writeStream.constructor === Writable); +readStream.destroy(); +writeStream.destroy(); + +const gzip: Transform = zlib.createGzip(); +const inflate: Transform = zlib.createInflateRaw(); +console.log("zlib", gzip.constructor.name, inflate.constructor.name, gzip.constructor === Transform); + +const kinds: EventEmitter[] = [new EventEmitter(), new Writable(), new PassThrough()]; +for (const k of kinds) { + const Kind = k.constructor as typeof EventEmitter; + console.log("value", Kind.name, Kind === EventEmitter, k instanceof Kind, new Kind() instanceof EventEmitter); +} diff --git a/tests/corpus/6004-class-instance-integrity.ts b/tests/corpus/6004-class-instance-integrity.ts new file mode 100644 index 000000000..7df516802 --- /dev/null +++ b/tests/corpus/6004-class-instance-integrity.ts @@ -0,0 +1,169 @@ +export {}; +import { inspect } from "node:util"; + +const message = (run: () => void): string => { + try { + run(); + return "no error"; + } catch (e) { + return `${e instanceof TypeError ? "TypeError" : "Error"}: ${(e as Error).message}`; + } +}; + +class Point { + x = 1; + y = 2; + label = "p"; + move(dx: number): void { + this.x += dx; + } +} + +const p = new Point(); +console.log(Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p)); +const same = Object.freeze(p); +console.log(same === p, Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p)); +console.log(message(() => { + p.x = 5; +})); +console.log(message(() => p.move(1))); +console.log(p.x, Object.keys(p)); +const q = new Point(); +q.x = 9; +console.log(q.x, Object.isFrozen(q)); + +class Point3 extends Point { + z = 3; +} +const p3 = new Point3(); +Object.freeze(p3); +console.log(message(() => { + p3.z = 1; +})); +const asBase: Point = new Point3(); +Object.freeze(asBase); +console.log(message(() => { + asBase.y = 0; +})); +console.log(asBase.y); + +class Counter { + n = 0; +} +const c = Object.seal(new Counter()); +c.n = 5; +console.log(c.n, Object.isSealed(c), Object.isFrozen(c), Object.isExtensible(c)); +const d = new Counter(); +Object.preventExtensions(d); +d.n = 2; +console.log(d.n, Object.isExtensible(d), Object.isSealed(d), Object.isFrozen(d)); +const loose: any = d; +console.log(message(() => { + loose.extra = 1; +})); + +class Config { + mode = "dev"; + level = 1; +} +const cfg = new Config(); +Object.defineProperty(cfg, "mode", { value: "prod", writable: false }); +console.log(cfg.mode, Object.keys(cfg), Object.values(cfg)); +console.log(message(() => { + cfg.mode = "x"; +})); +Object.defineProperty(cfg, "level", { enumerable: false }); +console.log(Object.keys(cfg), Object.entries(cfg), cfg.level); +Object.defineProperty(cfg, "level", { configurable: false }); +console.log(message(() => Object.defineProperty(cfg, "level", { enumerable: true }))); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "mode"))); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "level"))); +cfg.level = 7; +console.log(cfg.level); + +const fz = Object.freeze(new Config()); +console.log(message(() => Object.defineProperty(fz, "level", { value: 2 }))); +console.log(message(() => Object.defineProperty(fz, "level", { value: 1 }))); +console.log(fz.level, JSON.stringify(Object.getOwnPropertyDescriptor(fz, "level"))); + +class Frozen { + a = 1; + constructor() { + Object.freeze(this); + } +} +const frozenInCtor = new Frozen(); +console.log(frozenInCtor.a, Object.isFrozen(frozenInCtor)); +console.log(message(() => { + frozenInCtor.a = 2; +})); + +class Extended extends Frozen { + b = 2; +} +console.log(message(() => new Extended())); + +class Empty {} +const empty = new Empty(); +console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty)); +Object.preventExtensions(empty); +console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty)); + +class Totals { + count = 1; + items: string[] = []; +} +const t = new Totals(); +Object.freeze(t); +console.log(message(() => { + t.count += 1; +})); +console.log(message(() => { + t.count++; +})); +console.log(message(() => { + ({ value: t.count } = { value: 5 }); +})); +t.items.push("still mutable"); +console.log(t.count, t.items, Object.isFrozen(t.items)); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(t, "count"))); + +class Holder { + value: T; + constructor(value: T) { + this.value = value; + } +} +const h = Object.freeze(new Holder("text")); +console.log(h.value, Object.isFrozen(h)); +const h2 = new Holder(4); +Object.seal(h2); +h2.value = 5; +console.log(h2.value, Object.isSealed(h2)); + +class Settings { + mode = "dev"; + level = 1; +} +const settings = new Settings(); +console.log(settings); +Object.defineProperty(settings, "level", { enumerable: false }); +console.log(settings, inspect(settings)); +console.log([settings], { settings }); + +class Pair { + x = 1; + y = 2; +} +const pair = new Pair(); +Object.freeze(pair); +console.log(pair, Object.isFrozen(pair)); +const sealed = new Pair(); +(sealed as any).extra = "added"; +Object.seal(sealed); +console.log(sealed, Object.isSealed(sealed)); + +// JSON.stringify reads the same view: non-enumerable fields are skipped. +console.log(JSON.stringify(cfg), JSON.stringify(settings)); +console.log(JSON.stringify({ settings, pair, sealed })); +console.log(JSON.stringify([fz, t])); diff --git a/tests/corpus/6005-class-instance-integrity-sloppy.cjs b/tests/corpus/6005-class-instance-integrity-sloppy.cjs new file mode 100644 index 000000000..92c959d34 --- /dev/null +++ b/tests/corpus/6005-class-instance-integrity-sloppy.cjs @@ -0,0 +1,40 @@ +class Box { + constructor() { + this.x = 1; + } + bump() { + this.x = 2; + } +} + +const b = new Box(); +Object.freeze(b); +b.x = 5; +console.log(b.x, Object.isFrozen(b)); + +function strictWrite() { + "use strict"; + try { + b.x = 6; + } catch (e) { + console.log(e instanceof TypeError, e.message); + } +} +strictWrite(); + +try { + b.bump(); +} catch (e) { + console.log("class body:", e.message); +} +console.log(b.x); + +const s = new Box(); +Object.seal(s); +s.x = 3; +console.log(s.x, Object.isSealed(s), Object.isFrozen(s)); + +const plain = new Box(); +Object.defineProperty(plain, "x", { writable: false }); +plain.x = 9; +console.log(plain.x, JSON.stringify(Object.getOwnPropertyDescriptor(plain, "x"))); diff --git a/tests/corpus/6058-class-view-asserted-bindings.ts b/tests/corpus/6058-class-view-asserted-bindings.ts new file mode 100644 index 000000000..5014e3b4b --- /dev/null +++ b/tests/corpus/6058-class-view-asserted-bindings.ts @@ -0,0 +1,78 @@ +// A class instance held in a binding that asserts a possibly undefined +// value (`const node = stack.pop()!`) keeps that undefined at run time. +// Asserting the binding to a view of the class (`node as unknown as View`) +// gives the same live instance a direct cast does: getters run on each +// read, the view is the instance for identity, and an undefined value +// stays undefined until a property read throws Node's TypeError. +interface View { + readonly kind: number; + readonly label: string; +} + +abstract class Base { + reads = 0; + abstract get kind(): number; + get label(): string { + this.reads++; + return `node${this.kind}`; + } +} + +class Leaf extends Base { + private readonly value: number; + constructor(value: number) { + super(); + this.value = value; + } + get kind(): number { + return this.value; + } +} + +class Branch extends Base { + get kind(): number { + return 10; + } +} + +function walk(roots: Base[], cb: (node: View) => void): void { + while (roots.length > 0) { + const node = roots.pop()!; + cb(node as unknown as View); + } +} + +const asView = (node: Base): View => node as unknown as View; + +function walkWith(roots: Base[], cb: (node: View) => void): void { + while (roots.length > 0) { + const node = roots.pop()!; + cb(asView(node)); + } +} + +const seen: View[] = []; +let total = 0; +const leaf = new Leaf(2); +walk([new Leaf(1), leaf, new Branch()], (view) => { + total += view.kind; + seen.push(view); +}); +console.log(total, seen.map((view) => view.label).join(",")); +console.log(seen.includes(leaf as unknown as View), leaf.reads); + +const labels: string[] = []; +walkWith([new Branch(), new Leaf(3)], (view) => labels.push(`${view.kind}:${view.label}`)); +console.log(labels.join(" ")); + +function first(roots: Base[]): View { + const node = roots.pop()!; + return node as unknown as View; +} +const missing = first([]); +console.log(missing === undefined); +try { + console.log(missing.kind); +} catch (error) { + console.log(error instanceof TypeError, (error as Error).message); +} diff --git a/tests/corpus/6061-type-only-class-fields/emitted.ts b/tests/corpus/6061-type-only-class-fields/emitted.ts new file mode 100644 index 000000000..aa2ebb2df --- /dev/null +++ b/tests/corpus/6061-type-only-class-fields/emitted.ts @@ -0,0 +1,23 @@ +export class Emitted { + readonly count: number; + readonly files: string[]; + constructor(count: number, files: string[]) { + this.count = count; + this.files = files; + } + summary(): string { + return `${this.count} files`; + } +} + +export class Sealed { + #secret = 1; + read(): number { + return this.#secret; + } +} + +export class Totals { + emitted = 0; + skipped = 0; +} diff --git a/tests/corpus/6061-type-only-class-fields/holder.ts b/tests/corpus/6061-type-only-class-fields/holder.ts new file mode 100644 index 000000000..d3cc1d1ab --- /dev/null +++ b/tests/corpus/6061-type-only-class-fields/holder.ts @@ -0,0 +1,13 @@ +import type { Emitted, Sealed, Totals } from "./emitted.ts"; + +export class Holder { + items: Emitted | undefined = undefined; + sealed: Sealed | undefined = undefined; + history: Emitted[] = []; + totals: Totals = { emitted: 0, skipped: 0 }; + record(result: Emitted): void { + this.items = result; + this.history.push(result); + this.totals.emitted += result.count; + } +} diff --git a/tests/corpus/6061-type-only-class-fields/main.ts b/tests/corpus/6061-type-only-class-fields/main.ts new file mode 100644 index 000000000..264bd4c6b --- /dev/null +++ b/tests/corpus/6061-type-only-class-fields/main.ts @@ -0,0 +1,17 @@ +// Classes that the program reaches only through `import type`: no module +// loads emitted.ts, so no instance of its classes exists, and their types +// describe the object shapes the program does hold, as an interface would. +// Fields of those types hold undefined, object literals, and arrays of them. +import { Holder } from "./holder.ts"; +import type { Emitted } from "./emitted.ts"; + +const holder = new Holder(); +console.log(holder.items === undefined, holder.sealed === undefined); +const result: Emitted = { count: 2, files: ["a.js", "b.js"], summary: () => "two files" }; +holder.record(result); +holder.record({ count: 0, files: [], summary: () => "none" }); +console.log(holder.items?.count, holder.items?.summary(), holder.history.length); +console.log(holder.history[0] === result, holder.history[0]!.files.join(",")); +console.log(JSON.stringify(holder.history.map((entry) => entry.count))); +holder.totals.skipped++; +console.log(holder.totals, JSON.stringify(holder.totals)); diff --git a/tests/corpus/6063-union-class-field-checks.ts b/tests/corpus/6063-union-class-field-checks.ts new file mode 100644 index 000000000..426182b28 --- /dev/null +++ b/tests/corpus/6063-union-class-field-checks.ts @@ -0,0 +1,65 @@ +// A checked conversion of a dynamic value to a union of object types picks +// the member whose fields match, including a field of type `C | undefined` +// that holds an instance of the class C: workerData read as such a union, +// and an unknown value asserted to it, give the instance or undefined. +import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads"; +import { publish } from "@scriptc/threads"; + +class Job { + readonly name: string; + constructor(name: string) { + this.name = name; + } +} + +interface WithJob { + readonly kind: "job"; + readonly job: Job | undefined; +} +interface Plain { + readonly kind: "plain"; + readonly note: string; +} +interface Counted { + readonly kind: "counted"; + readonly count: number; + readonly last: Job | undefined; +} +type Data = WithJob | Plain | Counted; + +function describe(data: Data): string { + switch (data.kind) { + case "job": + return data.job === undefined ? "job none" : `job ${data.job.name}`; + case "plain": + return `plain ${data.note}`; + case "counted": + return `counted ${data.count} ${data.last?.name ?? "none"}`; + } +} + +function run(data: Data): Promise { + return new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: data }); + worker.on("message", (message) => console.log("worker:", message)); + worker.on("error", (error) => console.log("worker failed:", String(error))); + worker.on("exit", () => resolve()); + }); +} + +if (isMainThread) { + const values: unknown[] = [ + { kind: "job", job: new Job("local") }, + { kind: "job", job: undefined }, + { kind: "plain", note: "text" }, + { kind: "counted", count: 2, last: new Job("tail") }, + ]; + for (const value of values) console.log("local:", describe(value as Data)); + const shared = publish(new Job("shared")); + await run({ kind: "job", job: shared }); + await run({ kind: "job", job: undefined }); + await run({ kind: "plain", note: "worker" }); + await run({ kind: "counted", count: 3, last: shared }); +} else { + parentPort!.postMessage(describe(workerData as Data)); +} diff --git a/tests/corpus/6342-throw-builtin-identity.ts b/tests/corpus/6342-throw-builtin-identity.ts new file mode 100644 index 000000000..457b94138 --- /dev/null +++ b/tests/corpus/6342-throw-builtin-identity.ts @@ -0,0 +1,56 @@ +// Thrown Maps, Sets, typed arrays, Buffers and functions reach the catch +// binding as the same objects: inspection, String(), typeof, instanceof +// and typed views see the thrown value. +function report(label: string, f: () => void): void { + try { + f(); + } catch (e) { + console.log(label, e, typeof e, e instanceof Map, e instanceof Set, ArrayBuffer.isView(e)); + } +} + +const shared = new Map([["a", 1]]); +try { + throw shared; +} catch (e) { + console.log(e === shared, String(e)); + (e as Map).set("b", 2); +} +console.log(shared); + +report("set", () => { + throw new Set([1, 2]); +}); +report("u8", () => { + throw new Uint8Array([1, 2, 3]); +}); +report("f64", () => { + throw new Float64Array([1.5, -0]); +}); +report("buffer", () => { + throw Buffer.from("hi"); +}); +report("function", () => { + throw function named(): number { + return 1; + }; +}); +const twice = (x: number): number => x * 2; +try { + throw twice; +} catch (e) { + console.log(e, typeof e, (e as (x: number) => number)(21)); +} + +function pick(k: number): Map | number[] { + return k ? new Map([["k", k]]) : [k]; +} +for (const k of [0, 3]) report(`union ${k}`, () => { + throw pick(k); +}); + +try { + throw new Set(["x"]); +} catch (e) { + console.log([...(e as Set)].join(","), (e as Set).has("x")); +} diff --git a/tests/corpus/6345-inspect-dyn-holes.ts b/tests/corpus/6345-inspect-dyn-holes.ts new file mode 100644 index 000000000..60ecbfa73 --- /dev/null +++ b/tests/corpus/6345-inspect-dyn-holes.ts @@ -0,0 +1,29 @@ +// Holey arrays reached through `unknown` print their holes as Node does: +// runs of holes as `` entries, which count toward the +// 100-entry limit, inside nested values too. +function show(label: string, value: unknown): void { + console.log(label, value); +} +const a: number[] = []; +a[2] = 1; +show("lead", a); +const t: string[] = ["x"]; +t[4] = "y"; +t.length = 7; +show("runs", t); +const one: number[] = [1]; +one[2] = 3; +show("single", one); +const wide: number[] = []; +for (let i = 0; i < 150; i += 2) wide[i] = i; +show("wide", wide); +const dense: number[] = []; +for (let i = 0; i < 120; i++) dense[i] = i; +dense[130] = 1; +show("dense", dense); +show("nested", { rows: [a, one] }); +const made: unknown = new Array(3); +show("ctor", made); +const holes: number[] = []; +holes.length = 3; +show("length", holes); diff --git a/tests/corpus/6346-inspect-dyn-dataview.ts b/tests/corpus/6346-inspect-dyn-dataview.ts new file mode 100644 index 000000000..ff4acfafd --- /dev/null +++ b/tests/corpus/6346-inspect-dyn-dataview.ts @@ -0,0 +1,41 @@ +// A DataView reached through `unknown` keeps its own behavior: Node's +// `DataView { [byteLength], [byteOffset], [buffer] }` inspection, the +// "[object DataView]" string and tag, no length or indexed elements, and +// no typed array methods. +import { inspect } from "node:util"; + +function show(label: string, value: unknown): void { + console.log(label, value); +} + +const buf = new ArrayBuffer(8); +const whole = new DataView(buf); +whole.setUint16(0, 0x1234); +show("whole", whole); +show("offset", new DataView(buf, 2, 4)); +show("empty", new DataView(new ArrayBuffer(0))); +show("big", new DataView(new ArrayBuffer(120))); +show("nested", { view: new DataView(buf, 6) }); +show("shared", new DataView(new SharedArrayBuffer(2))); + +const u: unknown = whole; +console.log(inspect({ a: { u } }, { depth: 1 }), inspect([u], { depth: 0 })); +console.log(String(u), Object.prototype.toString.call(u), `${u}`); + +const view: unknown = new DataView(buf, 1); +const o = view as { [k: string]: unknown }; +console.log(o.length, o[0], o.byteLength, o.byteOffset); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const loose = view as any; +console.log(loose.toString(), loose.valueOf() === view); +for (const method of ["at", "join", "fill", "slice"]) { + try { + loose[method](0); + } catch (e) { + console.log(method, (e as Error).name); + } +} +console.log(view instanceof DataView, ArrayBuffer.isView(view)); +// A typed array through `unknown` is unaffected. +const bytes: unknown = new Uint8Array([1, 2]); +console.log(bytes, String(bytes), `${bytes}`, (bytes as Uint8Array).length); diff --git a/tests/corpus/6347-dataview-methods-js.cjs b/tests/corpus/6347-dataview-methods-js.cjs new file mode 100644 index 000000000..680aaa011 --- /dev/null +++ b/tests/corpus/6347-dataview-methods-js.cjs @@ -0,0 +1,25 @@ +'use strict'; +// A DataView held in an untyped binding answers its own methods and +// Object.prototype's, and none of the typed array methods. +const view = [new DataView(new ArrayBuffer(4))][0]; +function attempt(label, f) { + try { + console.log(label, f()); + } catch (e) { + console.log(label, `${e.name}: ${e.message}`); + } +} +function use(v) { + v.setUint16(0, 0x1234); + attempt('get', () => [v.getUint16(0), v.getUint8(1)].join(' ')); + attempt('toString', () => v.toString()); + attempt('toLocaleString', () => v.toLocaleString()); + attempt('valueOf', () => v.valueOf() === v); + attempt('join', () => v.join('-')); + attempt('at', () => v.at(0)); + attempt('fill', () => v.fill(1)); + attempt('slice', () => v.slice(0, 1)); + attempt('indexOf', () => v.indexOf(0)); + attempt('string', () => `${String(v)} ${v}`); +} +use(view); diff --git a/tests/corpus/6348-errno-code-own-field.ts b/tests/corpus/6348-errno-code-own-field.ts new file mode 100644 index 000000000..02a3d722a --- /dev/null +++ b/tests/corpus/6348-errno-code-own-field.ts @@ -0,0 +1,46 @@ +// `(err as NodeJS.ErrnoException).code` reads an Error subclass's own +// `code` field, as Node reads the own property, and still reads the errno +// code of runtime errors and undefined for plain errors. +import { readFileSync } from "node:fs"; + +class AppError extends Error { + code: string; + constructor(message: string, code = "E_APP") { + super(message); + this.name = "AppError"; + this.code = code; + } +} +class DbError extends AppError { + constructor() { + super("db", "E_DB"); + } +} +class Later extends Error { + code?: string; +} + +function codeOf(err: unknown): string { + if (err instanceof Error) return (err as NodeJS.ErrnoException).code ?? "(none)"; + return "(not an error)"; +} + +console.log(codeOf(new AppError("direct"))); +console.log(codeOf(new DbError())); +const wrapped = new TypeError("wrapped", { cause: new AppError("cause", "E_CAUSE") }); +console.log(codeOf(wrapped.cause), codeOf(wrapped)); +try { + throw new DbError(); +} catch (err) { + console.log(codeOf(err)); +} +const later = new Later("later"); +console.log(codeOf(later)); +later.code = "E_LATER"; +console.log(codeOf(later)); +try { + readFileSync("/nonexistent/scriptc-corpus-6348"); +} catch (err) { + console.log(codeOf(err)); +} +console.log(codeOf(new Error("plain")), codeOf(new RangeError("range")), codeOf("text")); diff --git a/tests/corpus/6356-format-integer-dyn.ts b/tests/corpus/6356-format-integer-dyn.ts new file mode 100644 index 000000000..a4bd53ab0 --- /dev/null +++ b/tests/corpus/6356-format-integer-dyn.ts @@ -0,0 +1,9 @@ +// util.format's %d and %i over checked-dynamic arguments, as Node reads +// them: a bigint prints as `${n}n`, a symbol as NaN, and any other value +// as Number(arg) (%d) or parseInt(String(arg)) (%i). +import { format } from "node:util"; +const values: unknown[] = [{}, [5], [1, 2], "12px", " 0x1f", "", null, undefined, true, 3.9, -0, 10n, Symbol("s"), new Date(0), { valueOf: () => 7 }, NaN, "1e3"]; +for (const v of values) console.log(format("%d|%i", v, v)); +console.log(format("%d and %i", 2n ** 70n, -5n)); +const report: unknown = { actual: 3 }; +console.log("... actual %d.", (report as { actual: unknown }).actual); diff --git a/tests/corpus/6400-dyn-error-stack-property-js.js b/tests/corpus/6400-dyn-error-stack-property-js.js new file mode 100644 index 000000000..f0f666103 --- /dev/null +++ b/tests/corpus/6400-dyn-error-stack-property-js.js @@ -0,0 +1,148 @@ +// In JavaScript sources, errors have an own `stack` like Node's: a +// non-enumerable accessor that every error shares. The first read formats +// "Name: message" from the error's name and message at that point, and +// later reads answer the same string. Assigning a value replaces it, +// `delete` removes it, Object.keys, spread, Object.assign and +// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects +// that inherit from an error read the error's stack. Typed and dynamic +// reads answer the same string, and a stack read through `any` captures +// frames. Only the first line is printed: frame lines differ between +// runtimes. + +class ParseError extends Error { + constructor(message) { + super(message); + this.name = "ParseError"; + } +} + +class PlainSubclass extends Error {} + +function dyn(value) { + return value; +} + +function firstLine(value) { + return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`; +} + +function describe(label, error) { + console.log( + label, + "|", + typeof error.stack, + firstLine(error.stack), + "|", + Object.keys(error).join(","), + JSON.stringify(error), + "|", + "stack" in error, + Object.hasOwn(error, "stack"), + error.propertyIsEnumerable("stack"), + ); +} + +try { + throw new RangeError("thrown"); +} catch (error) { + describe("caught", error); +} + +const unknownError = new TypeError("bad thing"); +describe("unknown", unknownError); +describe("plain", dyn(new Error("plain"))); +describe("no message", dyn(new SyntaxError())); +describe("subclass", dyn(new ParseError("line 3"))); +describe("plain subclass", dyn(new PlainSubclass("inherited name"))); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +describe("own property", coded); + +const descriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack")); +const other = dyn(Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack")); +console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable); +console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set); +console.log("foreign receiver", descriptor.get.call({}), firstLine(descriptor.get.call(dyn(new TypeError("b"))))); +const subclassDescriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack")); +console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get); + +const renamed = dyn(new Error("before")); +renamed.message = "after"; +renamed.name = "Renamed"; +console.log("first read", firstLine(renamed.stack)); +renamed.message = "later"; +console.log("later read", firstLine(renamed.stack)); +const subclassRenamed = dyn(new PlainSubclass("before")); +subclassRenamed.message = "after"; +console.log("subclass first read", firstLine(subclassRenamed.stack)); + +const replaced = dyn(new Error("replaced")); +replaced.stack = "custom stack\n at nowhere"; +describe("assigned", replaced); +console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")).join(",")); +const subclassReplaced = dyn(new ParseError("replaced")); +subclassReplaced.stack = "subclass stack"; +console.log("subclass assigned", subclassReplaced.stack); +const typedReplaced = new ParseError("typed write"); +typedReplaced.stack = "typed subclass stack"; +console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack); +const subclassNumbered = dyn(new PlainSubclass("numbered")); +subclassNumbered.stack = 7; +console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack); +subclassNumbered.stack = "string again"; +console.log("subclass string again", subclassNumbered.stack); +const numbered = dyn(new Error("numbered")); +numbered.stack = 5; +console.log("number", numbered.stack, typeof numbered.stack); +const removed = dyn(new Error("removed")); +delete removed.stack; +console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack")); + +const spreadSource = dyn(new Error("spread")); +console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length); +console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack)); + +const typed = new Error("typed"); +const view = dyn(typed); +console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack)); +const typedSubclass = new ParseError("typed subclass"); +console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack); +const assignedThroughView = new Error("typed"); +dyn(assignedThroughView).stack = "through the view"; +console.log("assigned through view", assignedThroughView.stack); + +function inner() { + return dyn(new Error("deep")); +} +function outer() { + return inner(); +} +const deep = outer().stack; +console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer")); +function makeSubclass() { + return dyn(new PlainSubclass("deep")); +} +console.log("subclass frames", makeSubclass().stack.includes("at makeSubclass")); + +const formatted = dyn(new Error("formatted")); +console.log("formatted before clone", firstLine(formatted.stack)); +console.log("clone keeps a formatted stack", firstLine(dyn(structuredClone(formatted)).stack)); + +class GrumpyError extends Error { + /** @returns {string} */ + get name() { + throw new Error("no name today"); + } +} +try { + console.log("throwing name getter", firstLine(dyn(new GrumpyError("grumpy")).stack)); +} catch (error) { + console.log("throwing name getter threw", error.message); +} + +function readKey(target, key) { + return target[key]; +} +const computed = readKey(inner(), ["sta", "ck"].join("")); +console.log("computed key", firstLine(computed), computed.includes("at inner")); diff --git a/tests/corpus/6401-dyn-error-own-property-names.ts b/tests/corpus/6401-dyn-error-own-property-names.ts new file mode 100644 index 000000000..1020cf014 --- /dev/null +++ b/tests/corpus/6401-dyn-error-own-property-names.ts @@ -0,0 +1,44 @@ +// The own property names of errors held as `any` or `unknown` match +// Node's: Object.getOwnPropertyNames, Reflect.ownKeys and +// Object.getOwnPropertyDescriptors list `stack`, `message` and the +// properties the error has, in order, and nothing else. + +class PlainSubclass extends Error {} + +function dyn(value: unknown): any { + return value; +} + +function names(label: string, error: any): void { + console.log( + label, + "|", + Object.getOwnPropertyNames(error).join(","), + "|", + Reflect.ownKeys(error).map(String).join(","), + "|", + Object.keys(Object.getOwnPropertyDescriptors(error)).join(","), + ); +} + +names("error", dyn(new Error("plain"))); +names("type error", dyn(new TypeError("bad thing"))); +names("no message", dyn(new RangeError())); +names("cause", dyn(new Error("outer", { cause: new Error("inner") }))); +names("subclass", dyn(new PlainSubclass("sub"))); + +const unknownError: unknown = new SyntaxError("unexpected"); +names("unknown", unknownError); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +coded.detail = 1; +names("own properties", coded); + +try { + throw new URIError("thrown"); +} catch (error) { + names("caught", error); +} + +console.log("marker", Object.getOwnPropertyNames(dyn(new Error("x"))).some((name) => name.startsWith("%"))); diff --git a/tests/corpus/6402-dyn-error-own-property-names-js.js b/tests/corpus/6402-dyn-error-own-property-names-js.js new file mode 100644 index 000000000..6f6ca7fb7 --- /dev/null +++ b/tests/corpus/6402-dyn-error-own-property-names-js.js @@ -0,0 +1,44 @@ +// In JavaScript sources, the own property names of errors match +// Node's: Object.getOwnPropertyNames, Reflect.ownKeys and +// Object.getOwnPropertyDescriptors list `stack`, `message` and the +// properties the error has, in order, and nothing else. + +class PlainSubclass extends Error {} + +function dyn(value) { + return value; +} + +function names(label, error) { + console.log( + label, + "|", + Object.getOwnPropertyNames(error).join(","), + "|", + Reflect.ownKeys(error).map(String).join(","), + "|", + Object.keys(Object.getOwnPropertyDescriptors(error)).join(","), + ); +} + +names("error", dyn(new Error("plain"))); +names("type error", dyn(new TypeError("bad thing"))); +names("no message", dyn(new RangeError())); +names("cause", dyn(new Error("outer", { cause: new Error("inner") }))); +names("subclass", dyn(new PlainSubclass("sub"))); + +const unknownError = new SyntaxError("unexpected"); +names("unknown", unknownError); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +coded.detail = 1; +names("own properties", coded); + +try { + throw new URIError("thrown"); +} catch (error) { + names("caught", error); +} + +console.log("marker", Object.getOwnPropertyNames(dyn(new Error("x"))).some((name) => name.startsWith("%"))); diff --git a/tests/corpus/6407-dyn-bytes-in.ts b/tests/corpus/6407-dyn-bytes-in.ts new file mode 100644 index 000000000..e08eff8bc --- /dev/null +++ b/tests/corpus/6407-dyn-bytes-in.ts @@ -0,0 +1,28 @@ +// `in` on typed arrays, Buffers and DataViews held as `any` matches Node's. +// On a typed array a canonical numeric key answers by bounds (integral, +// not -0, below the length), and "1.5", "-0", "NaN", "Infinity", "01" and +// "1e0" answer false. Other keys name members of %TypedArray%.prototype, +// Buffer.prototype for a Buffer, each constructor's BYTES_PER_ELEMENT, or +// Object.prototype. A DataView has no indexed elements and answers its own +// prototype's members. + +const bytes: any = new Uint8Array([1, 2, 255]); +const floats: any = new Float64Array(2); +const buf: any = Buffer.from("ab"); +for (const [label, v] of [["u8", bytes], ["f64", floats], ["buffer", buf]] as [string, any][]) { + const keys: any[] = [0, 1, 2, 3, -1, "0", "1.5", "-0", "length", "byteLength", "buffer", "map", "set", "toString", "nope", "write"]; + console.log(label, keys.map((k) => `${String(k)}:${k in v}`).join(" ")); +} +const k: any = JSON.parse("1"); +console.log("computed", k in bytes, "length" in bytes, "map" in bytes); +const view: any = new DataView(new ArrayBuffer(4)); +console.log("dataview", [0, "getInt8", "setUint16", "byteLength", "buffer", "length", "map", "toString"].map((k) => `${String(k)}:${k in view}`).join(" ")); +const clamped: any = new Uint8ClampedArray(3); + +console.log("clamped", 2 in clamped, 3 in clamped, "BYTES_PER_ELEMENT" in clamped); +const sub: any = new Uint8Array([1, 2, 3, 4]).subarray(1, 3); +console.log("subarray", 0 in sub, 1 in sub, 2 in sub); +console.log("hasOwn", Object.hasOwn(bytes, 0), Object.hasOwn(bytes, 3), Object.hasOwn(bytes, "length"), bytes.hasOwnProperty(1)); +console.log("symbol", Symbol.iterator in bytes); +const empty: any = new Uint8Array(0); +console.log("empty", 0 in empty, "Infinity" in bytes, "NaN" in bytes, "1e0" in bytes, "01" in bytes); diff --git a/tests/corpus/6408-dyn-bytes-in-js.js b/tests/corpus/6408-dyn-bytes-in-js.js new file mode 100644 index 000000000..9677c094b --- /dev/null +++ b/tests/corpus/6408-dyn-bytes-in-js.js @@ -0,0 +1,35 @@ +// In JavaScript sources, `in` on typed arrays, Buffers and DataViews +// matches Node's. On a typed array a canonical numeric key answers by +// bounds (integral, not -0, below the length), and "1.5", "-0", "NaN", +// "Infinity", "01" and "1e0" answer false. Other keys name members of +// %TypedArray%.prototype, Buffer.prototype for a Buffer, each +// constructor's BYTES_PER_ELEMENT, or Object.prototype. A DataView has no +// indexed elements and answers its own prototype's members. + +function dyn(value) { + return value; +} + +const bytes = dyn(new Uint8Array([1, 2, 255])); +const floats = dyn(new Float64Array(2)); +const buf = dyn(Buffer.from("ab")); +function row(label, v) { + const keys = [0, 1, 2, 3, -1, "0", "1.5", "-0", "length", "byteLength", "buffer", "map", "set", "toString", "nope", "write"]; + console.log(label, keys.map((k) => `${String(k)}:${k in v}`).join(" ")); +} +row("u8", bytes); +row("f64", floats); +row("buffer", buf); +const k = dyn(JSON.parse("1")); +console.log("computed", k in bytes, "length" in bytes, "map" in bytes); +const view = dyn(new DataView(new ArrayBuffer(4))); +console.log("dataview", [0, "getInt8", "setUint16", "byteLength", "buffer", "length", "map", "toString"].map((k) => `${String(k)}:${k in view}`).join(" ")); +const clamped = dyn(new Uint8ClampedArray(3)); + +console.log("clamped", 2 in clamped, 3 in clamped, "BYTES_PER_ELEMENT" in clamped); +const sub = dyn(new Uint8Array([1, 2, 3, 4]).subarray(1, 3)); +console.log("subarray", 0 in sub, 1 in sub, 2 in sub); +console.log("hasOwn", Object.hasOwn(bytes, 0), Object.hasOwn(bytes, 3), Object.hasOwn(bytes, "length"), bytes.hasOwnProperty(1)); +console.log("symbol", Symbol.iterator in bytes); +const empty = dyn(new Uint8Array(0)); +console.log("empty", 0 in empty, "Infinity" in bytes, "NaN" in bytes, "1e0" in bytes, "01" in bytes); diff --git a/tests/corpus/6416-tls-session-reused.ts b/tests/corpus/6416-tls-session-reused.ts new file mode 100644 index 000000000..a30f4f1b0 --- /dev/null +++ b/tests/corpus/6416-tls-session-reused.ts @@ -0,0 +1,33 @@ +// TLSSocket.isSessionReused() on both ends of a fresh TLS connection, +// before and after the handshake: a first handshake never resumes a +// session, so every answer is false. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); + +let serverLine: string | null = null; +let clientLine: string | null = null; +function both(): void { + if (serverLine === null || clientLine === null) return; + console.log(serverLine); + console.log(clientLine); +} + +const server = tls.createServer({ key, cert }, (socket) => { + serverLine = `server reused ${socket.isSessionReused()}`; + both(); + socket.end("hi"); +}); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as { port: number }).port; + const client = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca }, () => { + clientLine = `client reused ${client.isSessionReused()}`; + both(); + }); + console.log("before handshake", client.isSessionReused()); + client.on("data", (d: Buffer) => console.log("data", d.toString())); + client.on("close", () => server.close()); +}); diff --git a/tests/corpus/6417-object-assign-class-target.ts b/tests/corpus/6417-object-assign-class-target.ts new file mode 100644 index 000000000..4c2746693 --- /dev/null +++ b/tests/corpus/6417-object-assign-class-target.ts @@ -0,0 +1,36 @@ +// Object.assign with a class instance as the target and a plain record as +// the source: the record's fields land on the instance's own fields (also +// readonly ones, which only the checker guards), optional source fields +// only when present, and the call answers the instance itself. +class Settings { + retries = 1; + readonly name: string = "default"; + verbose = false; + label?: string; + describe(): string { + return `${this.name}:${this.retries}:${this.verbose}:${this.label ?? "-"}`; + } +} + +const s = new Settings(); +const same = Object.assign(s, { retries: 3, verbose: true }); +console.log(same === s, s.describe()); + +function patch(target: Settings, update: { label?: string }): Settings { + return Object.assign(target, update); +} +console.log(patch(s, { label: "x" }).describe()); +console.log(patch(s, {}).describe()); +console.log(patch(s, { label: undefined }).describe(), Object.assign(s, { retries: 0 }).describe()); + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} +const p = new Point(1, 2); +Object.assign(p, { y: 5 }); +console.log(p.x, p.y, JSON.stringify(p), p instanceof Point); diff --git a/tests/corpus/6418-nullish-assign-index-record.ts b/tests/corpus/6418-nullish-assign-index-record.ts new file mode 100644 index 000000000..97918cd98 --- /dev/null +++ b/tests/corpus/6418-nullish-assign-index-record.ts @@ -0,0 +1,29 @@ +// Short-circuit assignment through a string-keyed index-signature record +// (`timings[phase] ??= value`): a missing key takes the right side, a +// present one keeps its value, and `||=` and `&&=` test truthiness. +class Telemetry { + private readonly timings: Record = {}; + private readonly labels: { [key: string]: string } = {}; + mark(phase: string, at: number): number { + return (this.timings[phase] ??= at); + } + label(key: string, value: string): string { + this.labels[key] ||= value; + this.labels[key] &&= this.labels[key].toUpperCase(); + return this.labels[key]; + } + dump(): string { + return JSON.stringify(this.timings) + " " + JSON.stringify(this.labels); + } +} + +const t = new Telemetry(); +console.log(t.mark("dns", 5), t.mark("tcp", 7), t.mark("dns", 9)); +console.log(t.label("a", "first"), t.label("a", "second"), t.label("", "empty")); +console.log(t.dump()); + +const counts: Record = { zero: 0 }; +const keys = ["zero", "one", "zero"]; +for (const k of keys) counts[k] ??= 10; +for (const k of keys) counts[k] ||= 20; +console.log(JSON.stringify(counts)); diff --git a/tests/corpus/6420-process-send-presence.ts b/tests/corpus/6420-process-send-presence.ts new file mode 100644 index 000000000..59773ab44 --- /dev/null +++ b/tests/corpus/6420-process-send-presence.ts @@ -0,0 +1,24 @@ +// process.send exists only with an IPC channel: presence checks in every +// truthiness position, typeof, and guarded calls. +const viaIpc = Boolean(process.send); +console.log("Boolean", viaIpc); +console.log("not", !process.send, !!process.disconnect); +console.log("typeof", typeof process.send, typeof process.disconnect); +console.log("ternary", process.send ? "ipc" : "stdout"); +if (process.send) { + process.send({ kind: "log", line: "never" }); +} else { + console.log("if: no channel"); +} +let polls = 0; +while (process.send && polls < 3) polls++; +console.log("while", polls); +process.send && process.send({ unused: true }); +const sink = !process.send || !process.disconnect ? "console" : "ipc"; +console.log("or", sink); +console.log("optional", process.send?.({ value: 1 })); +try { + process.send!({ value: 2 }); +} catch (e) { + console.log("direct", (e as Error).name, (e as Error).message); +} diff --git a/tests/corpus/6421-process-send-presence-fork/main.ts b/tests/corpus/6421-process-send-presence-fork/main.ts new file mode 100644 index 000000000..11a634672 --- /dev/null +++ b/tests/corpus/6421-process-send-presence-fork/main.ts @@ -0,0 +1,13 @@ +// process.send and process.disconnect exist in a forked child (an IPC +// channel) and not in the parent; the child's presence checks pick the +// channel and its guarded send reaches the parent. +import { fork } from "node:child_process"; + +console.log("parent", Boolean(process.send), typeof process.send, typeof process.disconnect); +const child = fork(new URL("./worker.ts", import.meta.url), [], { + stdio: ["ignore", "inherit", "inherit", "ipc"], +}); +child.on("message", (message: { sink: string; send: string; disconnect: string }) => { + console.log("child says", message.sink, message.send, message.disconnect); +}); +child.on("exit", (code) => console.log("child exit", code)); diff --git a/tests/corpus/6421-process-send-presence-fork/worker.ts b/tests/corpus/6421-process-send-presence-fork/worker.ts new file mode 100644 index 000000000..81e8288a7 --- /dev/null +++ b/tests/corpus/6421-process-send-presence-fork/worker.ts @@ -0,0 +1,6 @@ +const sink = process.send ? "ipc" : "stdout"; +if (process.send) { + process.send({ sink, send: typeof process.send, disconnect: typeof process.disconnect }); +} +if (!process.send) console.log("unreachable"); +process.disconnect(); diff --git a/tests/corpus/6422-process-exitcode-read.ts b/tests/corpus/6422-process-exitcode-read.ts new file mode 100644 index 000000000..c320b4a0d --- /dev/null +++ b/tests/corpus/6422-process-exitcode-read.ts @@ -0,0 +1,22 @@ +// @exit: 7 +// process.exitCode: unset reads undefined, assignments (including 0) read back, +// and process.exit(n) assigns the code before 'exit' listeners run. +console.log("initial", process.exitCode); +process.on("exit", (code) => { + console.log("exit listener", code, process.exitCode); +}); +process.exitCode = 0; +console.log("after 0", process.exitCode); +process.exitCode = 4; +const seen = process.exitCode; +console.log("after 4", seen, typeof process.exitCode); +process.exitCode = process.exitCode ?? 9; +console.log("kept", process.exitCode); +try { + process.exitCode = 1.5; +} catch (e) { + console.log("rejected", (e as Error).message, process.exitCode); +} +setTimeout(() => { + process.exit(7); +}, 1); diff --git a/tests/corpus/6423-process-exitcode-default.ts b/tests/corpus/6423-process-exitcode-default.ts new file mode 100644 index 000000000..5dbc72294 --- /dev/null +++ b/tests/corpus/6423-process-exitcode-default.ts @@ -0,0 +1,10 @@ +// @exit: 2 +// The exit status follows process.exitCode when the program ends normally. +function settle(failed: boolean): void { + if (failed) process.exitCode = process.exitCode ?? 2; +} +console.log("start", process.exitCode === undefined); +settle(true); +settle(true); +console.log("code", process.exitCode); +process.on("exit", () => console.log("exiting with", process.exitCode)); diff --git a/tests/corpus/6424-process-exit-keeps-exitcode.ts b/tests/corpus/6424-process-exit-keeps-exitcode.ts new file mode 100644 index 000000000..e5a0b86d2 --- /dev/null +++ b/tests/corpus/6424-process-exit-keeps-exitcode.ts @@ -0,0 +1,5 @@ +// @exit: 5 +// process.exit() without an argument keeps process.exitCode. +process.exitCode = 5; +process.on("exit", (code) => console.log("exit", code, process.exitCode)); +process.exit(); diff --git a/tests/corpus/6425-stream-hook-wider-chunk.ts b/tests/corpus/6425-stream-hook-wider-chunk.ts new file mode 100644 index 000000000..be46b18be --- /dev/null +++ b/tests/corpus/6425-stream-hook-wider-chunk.ts @@ -0,0 +1,46 @@ +// Stream hooks may declare a supertype of what Node passes: chunks typed +// Buffer | string still arrive as Buffers. +import { Transform, Writable } from "node:stream"; + +class LineSink extends Writable { + private rest = ""; + readonly lines: string[] = []; + _write(chunk: Buffer | string, encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("chunk", typeof chunk, Buffer.isBuffer(chunk), encoding); + const parts = (this.rest + chunk.toString()).split("\n"); + this.rest = parts.pop() ?? ""; + this.lines.push(...parts); + cb(); + } + _final(cb: (err?: Error | null) => void): void { + if (this.rest !== "") this.lines.push(this.rest); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: string | Buffer, _enc: string, cb: (err?: Error | null, data?: string) => void): void { + cb(null, chunk.toString().toUpperCase()); + } +} + +class Failing extends Writable { + _write(chunk: Uint8Array | string, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + cb(new Error(`refused ${chunk.toString().length} bytes`)); + } +} + +const sink = new LineSink(); +sink.write("a\nb"); +sink.write(Buffer.from("c\nd")); +sink.end("e"); +sink.on("finish", () => console.log("lines", JSON.stringify(sink.lines))); + +const up = new Upper(); +up.on("data", (d: Buffer) => console.log("upper", d.toString())); +up.write("hello "); +up.end("world"); + +const bad = new Failing(); +bad.on("error", (e) => console.log("error", e.message)); +bad.write("xyz"); diff --git a/tests/corpus/6426-promise-race-void-entry.ts b/tests/corpus/6426-promise-race-void-entry.ts new file mode 100644 index 000000000..b33f3b664 --- /dev/null +++ b/tests/corpus/6426-promise-race-void-entry.ts @@ -0,0 +1,31 @@ +// Promise.race over Promise and Promise: the void +// entry fulfills the race with undefined. +interface Msg { id: string } +function delay(ms: number, value: T): Promise { + return new Promise((resolve) => setTimeout(() => resolve(value), ms)); +} +function poll(ms: number, id: string | null): Promise { + return delay(ms, id === null ? undefined : { id }); +} +function exited(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +async function main(): Promise { + const a = await Promise.race([poll(5, "m1"), exited(50)]); + console.log("message first:", a ? a.id : "none"); + const b = await Promise.race([poll(50, "m2"), exited(5)]); + console.log("exit first:", b === undefined, b ? b.id : "none"); + const c = await Promise.race([exited(5), poll(1, null)]); + console.log("empty poll first:", c); + const settled = Promise.resolve(); + const d = await Promise.race([settled, poll(0, "m3")]); + console.log("settled void entry wins:", d); + const n = await Promise.race([delay(20, 42), exited(1)]); + console.log("number or void:", n); + try { + await Promise.race([poll(20, "late"), Promise.reject(new Error("boom"))]); + } catch (e) { + console.log("rejected:", (e as Error).message); + } +} +void main(); diff --git a/tests/corpus/6427-http-request-options-type.ts b/tests/corpus/6427-http-request-options-type.ts new file mode 100644 index 000000000..17cb2d855 --- /dev/null +++ b/tests/corpus/6427-http-request-options-type.ts @@ -0,0 +1,10 @@ +// http.RequestOptions as a declared type: a checked-dynamic option record. +import type http from "node:http"; +const socket = process.env.NO_SUCH_SOCKET_VAR; +const base: http.RequestOptions = socket ? { socketPath: socket } : { host: "127.0.0.1", port: 8080 }; +const full: http.RequestOptions = { ...base, path: "/x", method: "POST", headers: { "x-a": "1" } }; +console.log(JSON.stringify(base), JSON.stringify(full)); +function describe(o: http.RequestOptions): string { + return `${String(o.host)}:${String(o.port)}${String(o.path ?? "/")}`; +} +console.log(describe(full), describe({ host: "h" })); diff --git a/tests/corpus/6428-promisify-callback-functions.ts b/tests/corpus/6428-promisify-callback-functions.ts new file mode 100644 index 000000000..b899dd292 --- /dev/null +++ b/tests/corpus/6428-promisify-callback-functions.ts @@ -0,0 +1,80 @@ +// util.promisify over typed callback-only functions (bound methods, one with +// an optional callback, a function declaration, an inline arrow, a function +// that returns a value): the error argument rejects, the value fulfills, the +// return value is ignored, a synchronous throw rejects, and settlement is +// async. +import { promisify } from "node:util"; + +class Connection { + private closed = false; + private readonly name: string; + constructor(name: string) { + this.name = name; + } + close(callback: (error?: Error) => void): void { + setTimeout(() => { + if (this.closed) { + callback(new Error(`${this.name} already closed`)); + return; + } + this.closed = true; + callback(); + }, 1); + } + flush(callback?: (error?: Error) => void): void { + setImmediate(() => callback?.(this.closed ? new Error("flushed after close") : undefined)); + } + stats(callback: (error: Error | null, open: number) => void): void { + setImmediate(() => (this.closed ? callback(new RangeError("closed"), 0) : callback(null, 3))); + } +} + +function boom(callback: (error: Error | null) => void): void { + if (callback !== undefined) throw new TypeError("synchronous failure"); +} + +let pending = 0; +function size(callback: (error: Error | null, size: number) => void): number { + setImmediate(() => callback(null, 4)); + return ++pending; +} + +async function main(): Promise { + const c = new Connection("db"); + const stats = promisify(c.stats.bind(c)); + console.log("open", await stats()); + await promisify(c.flush.bind(c))(); + console.log("flushed"); + const closeAsync = promisify(c.close.bind(c)); + await closeAsync(); + console.log("closed once"); + try { + await closeAsync(); + } catch (e) { + console.log("second close:", (e as Error).message); + } + try { + await stats(); + } catch (e) { + console.log("stats:", (e as Error).name, (e as Error).message); + } + await promisify(c.flush.bind(c))().catch((e) => console.log("flush:", (e as Error).message)); + try { + await promisify(boom)(); + } catch (e) { + console.log("sync throw:", (e as Error).name, (e as Error).message); + } + const order: string[] = []; + const tick = promisify((done: (error?: Error) => void) => { + order.push("called"); + done(); + }); + const p = tick().then(() => order.push("settled")); + order.push("returned"); + await p; + console.log(order.join(", ")); + await promisify(new Connection("inline").close.bind(new Connection("inline")))(); + console.log("inline done"); + console.log("size", await promisify(size)(), pending); +} +void main(); diff --git a/tests/corpus/6429-esm-js-dirname.mjs b/tests/corpus/6429-esm-js-dirname.mjs new file mode 100644 index 000000000..2ecc4088a --- /dev/null +++ b/tests/corpus/6429-esm-js-dirname.mjs @@ -0,0 +1,15 @@ +// A JavaScript ES module has no __dirname or __filename (Node throws a +// ReferenceError where they are read); import.meta carries the location. +// TypeScript files keep the CommonJS globals as the source path. +try { + console.log(__dirname); +} catch (error) { + console.log("no __dirname:", error instanceof Error); +} +console.log(import.meta.filename.endsWith("6429-esm-js-dirname.mjs")); +console.log(typeof __dirname, typeof __filename); +try { + console.log(__filename.length); +} catch (error) { + console.log(error.name, error.message); +} diff --git a/tests/corpus/6430-npm-constructor-function-class/main.ts b/tests/corpus/6430-npm-constructor-function-class/main.ts new file mode 100644 index 000000000..6098c1e19 --- /dev/null +++ b/tests/corpus/6430-npm-constructor-function-class/main.ts @@ -0,0 +1,19 @@ +// Constructing a CommonJS constructor-function "class" imported by a named +// member alias (`exports = module.exports = Client; exports.StatsD = +// Client`) through a re-exporting package index: prototype methods, a child +// client linked with util.inherits, and a callback-style close. +import { StatsD } from "metrics-client"; + +const client = new StatsD({ prefix: "app." }); +client.onSent((line: string) => console.log("event", line)); +console.log(client.increment("boot")); +console.log(client.gauge("heap", 42)); +console.log(typeof client.increment, typeof client.onSent); +const child = client.childClient({ prefix: "db." }); +console.log(child.increment("query", 3)); +console.log(child.gauge("pool", 1), child.sent.length, client.sent.length); +const bare = new StatsD(); +console.log(bare.increment("x"), bare.sent.length); +client.close((error: Error | null, count: number) => { + console.log("closed", error, count); +}); diff --git a/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/index.d.ts b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/index.d.ts new file mode 100644 index 000000000..c53bdd8ae --- /dev/null +++ b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/index.d.ts @@ -0,0 +1,14 @@ +export interface ClientOptions { + prefix?: string; + suffix?: string; +} +export declare class StatsD { + constructor(options?: ClientOptions); + prefix: string; + sent: string[]; + onSent(listener: (line: string) => void): this; + increment(stat: string, value?: number): string; + gauge(stat: string, value: number): string; + close(callback: (error: Error | null, count: number) => void): void; + childClient(options?: ClientOptions): StatsD; +} diff --git a/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/index.js b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/index.js new file mode 100644 index 000000000..eb7384718 --- /dev/null +++ b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/index.js @@ -0,0 +1 @@ +module.exports = require("./lib/client"); diff --git a/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/lib/client.js b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/lib/client.js new file mode 100644 index 000000000..7dd5968c9 --- /dev/null +++ b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/lib/client.js @@ -0,0 +1,50 @@ +"use strict"; +// A constructor-function "class" in the style of statsd clients: a function +// expression, prototype methods, a child client linked with util.inherits, +// and the whole export plus a named alias. +const util = require("util"); + +const Client = function (options) { + const opts = options || {}; + this.prefix = opts.prefix || ""; + this.suffix = opts.suffix || ""; + this.sent = []; + this.listeners = []; +}; + +Client.prototype.send = function (stat, value, type) { + const line = this.prefix + stat + this.suffix + ":" + value + "|" + type; + this.sent.push(line); + for (const listener of this.listeners) listener(line); + return line; +}; +Client.prototype.onSent = function (listener) { + this.listeners.push(listener); + return this; +}; +Client.prototype.increment = function (stat, value) { + return this.send(stat, value === undefined ? 1 : value, "c"); +}; +Client.prototype.gauge = function (stat, value) { + return this.send(stat, value, "g"); +}; +Client.prototype.close = function (callback) { + const count = this.sent.length; + setImmediate(() => callback(null, count)); +}; +Client.prototype.childClient = function (options) { + return new ChildClient(this, options); +}; + +const ChildClient = function (parent, options) { + Client.call(this, { + prefix: parent.prefix + ((options && options.prefix) || ""), + suffix: parent.suffix, + }); + this.parent = parent; +}; +util.inherits(ChildClient, Client); + +exports = module.exports = Client; +exports.StatsD = Client; +exports.ChildClient = ChildClient; diff --git a/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/package.json b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/package.json new file mode 100644 index 000000000..af4ede9ee --- /dev/null +++ b/tests/corpus/6430-npm-constructor-function-class/node_modules/metrics-client/package.json @@ -0,0 +1 @@ +{ "name": "metrics-client", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6431-cjs-exports-rebinding/extended.cjs b/tests/corpus/6431-cjs-exports-rebinding/extended.cjs new file mode 100644 index 000000000..d07438ee9 --- /dev/null +++ b/tests/corpus/6431-cjs-exports-rebinding/extended.cjs @@ -0,0 +1,3 @@ +"use strict"; +module.exports = require("./lib.cjs"); +module.exports.extra = "added"; diff --git a/tests/corpus/6431-cjs-exports-rebinding/lib.cjs b/tests/corpus/6431-cjs-exports-rebinding/lib.cjs new file mode 100644 index 000000000..52b6b8c9b --- /dev/null +++ b/tests/corpus/6431-cjs-exports-rebinding/lib.cjs @@ -0,0 +1,10 @@ +"use strict"; +function format(value) { + return "[" + value + "]"; +} +function shout(value) { + return String(value).toUpperCase(); +} +exports = module.exports = format; +exports.shout = shout; +exports.version = "1.2.3"; diff --git a/tests/corpus/6431-cjs-exports-rebinding/main.cjs b/tests/corpus/6431-cjs-exports-rebinding/main.cjs new file mode 100644 index 000000000..cfeec742b --- /dev/null +++ b/tests/corpus/6431-cjs-exports-rebinding/main.cjs @@ -0,0 +1,9 @@ +// `exports = module.exports = value` rebinds the wrapper's exports binding +// too, so members attached through `exports` afterwards are exported. +const format = require("./lib.cjs"); +const { shout, version } = require("./lib.cjs"); +console.log(format("a"), shout("b"), version); +console.log(typeof format, typeof shout); +// A module that re-exports another module's exports and adds a member. +const { extra, shout: loud } = require("./extended.cjs"); +console.log(extra, loud("c")); diff --git a/tests/corpus/6445-rest-function-fixed-slot.ts b/tests/corpus/6445-rest-function-fixed-slot.ts new file mode 100644 index 000000000..e78e83836 --- /dev/null +++ b/tests/corpus/6445-rest-function-fixed-slot.ts @@ -0,0 +1,49 @@ +// A rest-parameter function in a slot with fixed parameters: the slot's +// arguments arrive in the rest array, as in a JavaScript call. +function log(...messages: string[]): void { + console.log(`log(${messages.length}):`, messages.join(" | ")); +} +function tagged(tag: string, ...values: number[]): number { + console.log(tag, values.join(",")); + return values.length; +} + +class Line { + private readonly sink: (m: string) => void; + constructor(sink: (m: string) => void) { + this.sink = sink; + } + emit(m: string): void { + this.sink(m); + } +} + +new Line(log).emit("one"); +const pair: (a: string, b: string) => void = log; +pair("x", "y"); +const none: () => void = log; +none(); +const count: (tag: string, a: number, b: number) => number = tagged; +console.log("count", count("t", 1, 2)); +["a", "b"].forEach((m) => new Line(log).emit(m)); + +// Nullable and optional slots: the value takes the slot's function arm. +class OptionalLine { + private readonly sink: ((m: string) => void) | null; + constructor(sink: ((m: string) => void) | null) { + this.sink = sink; + } + emit(m: string): void { + if (this.sink) this.sink(m); + else console.log("no sink:", m); + } +} +new OptionalLine(log).emit("nullable"); +new OptionalLine(null).emit("dropped"); +function relay(m: string, sink?: (m: string) => void): void { + sink?.(m); +} +relay("optional", log); +relay("absent"); +const maybe: ((a: string, b: string) => void) | undefined = log; +maybe?.("p", "q"); diff --git a/tests/corpus/6446-rest-function-nested-declaration.ts b/tests/corpus/6446-rest-function-nested-declaration.ts new file mode 100644 index 000000000..1b2d7aa17 --- /dev/null +++ b/tests/corpus/6446-rest-function-nested-declaration.ts @@ -0,0 +1,26 @@ +// Nested rest-parameter function declarations passed to a fixed slot. +class Line { + private readonly sink: (m: string) => void; + constructor(sink: (m: string) => void) { + this.sink = sink; + } + emit(m: string): void { + this.sink(m); + } +} +interface Logger { + line: () => Line; + log: (...messages: string[]) => void; +} +function makeLogger(prefix: string): Logger { + return { + line: () => new Line(log), + log, + }; + function log(...messages: string[]) { + console.log(prefix, messages.join(" ")); + } +} +const logger = makeLogger(">"); +logger.log("a", "b"); +logger.line().emit("c"); diff --git a/tests/corpus/6460-in-union-optional-slots.ts b/tests/corpus/6460-in-union-optional-slots.ts new file mode 100644 index 000000000..f381217b9 --- /dev/null +++ b/tests/corpus/6460-in-union-optional-slots.ts @@ -0,0 +1,53 @@ +// `"k" in u` over a union of records where some arms declare `k` as an +// optional slot: those arms answer by the value's own property presence +// (an explicit undefined is present, an omitted or deleted key is not), +// the other arms by their tag. +type Cond = { type: string; value?: string }; +type HandleRoute = { handle: string; src?: string; dest?: string; status?: number }; +type SourceRoute = { + src: string; + dest?: string; + headers?: { [key: string]: string }; + middlewarePath?: string; + continue?: boolean; + status?: number; + has?: Cond[]; +}; +type Route = HandleRoute | SourceRoute; + +const routes: Route[] = [ + { handle: "filesystem" }, + { src: "/a", middlewarePath: "/mw" }, + { src: "/b", dest: "/c", status: undefined }, + { handle: "miss", src: "/x", dest: undefined }, + { src: "/d", has: [{ type: "header" }] }, +]; + +for (const r of routes) { + console.log("handle" in r, "middlewarePath" in r, "dest" in r, "status" in r, "has" in r, "toString" in r); +} + +// A deleted optional key leaves the value; a later write brings it back. +const second = routes[1]!; +if ("middlewarePath" in second) { + delete second.middlewarePath; +} +console.log("middlewarePath" in second, JSON.stringify(second)); +if (!("handle" in second)) { + second.middlewarePath = "/again"; +} +console.log("middlewarePath" in second, JSON.stringify(second)); + +// The receiver evaluates once, even when it is a call. +let calls = 0; +function pick(i: number): Route { + calls++; + return routes[i]!; +} +console.log("dest" in pick(2), "dest" in pick(0), "src" in pick(3), calls); + +// `in` as a narrowing test inside a predicate and a filter callback. +function hasMiddleware(route: Route): boolean { + return "middlewarePath" in route && typeof route.middlewarePath === "string"; +} +console.log(routes.filter(hasMiddleware).length, routes.filter((r) => "status" in r).length); diff --git a/tests/corpus/6461-predicate-intersection-narrowing.ts b/tests/corpus/6461-predicate-intersection-narrowing.ts new file mode 100644 index 000000000..7e8ff9d8a --- /dev/null +++ b/tests/corpus/6461-predicate-intersection-narrowing.ts @@ -0,0 +1,70 @@ +// A predicate `r is Route & { path: string }` refines the record arms of +// a union without naming a stored arm: arms that do not declare `path` +// cannot hold such a value, the others are read through their own +// layout. The narrowed value can be read in place, in templates, in +// object literals and from closures. +type HandleRoute = { handle: string; src?: string; dest?: string }; +type SourceRoute = { src: string; dest?: string; path?: string; status?: number }; +type Route = HandleRoute | SourceRoute; + +function isPathRoute(route: Route): route is Route & { path: string } { + return "path" in route && typeof route.path === "string"; +} + +type Output = { path: string; index: number }; + +function collect(result: { names: Set; routes: Route[] }): Map { + const out = new Map(); + for (const route of result.routes) { + if (!isPathRoute(route)) continue; + let name = route.path; + if (!result.names.has(name)) name = name.replace(/^\//, ""); + if (!result.names.has(name)) { + throw new Error(`Mapping ${route.path} not found`); + } + out.set(name, { path: route.path, index: out.get(name)?.index ?? out.size }); + } + return out; +} + +const routes: Route[] = [ + { handle: "filesystem" }, + { src: "/a", path: "/one" }, + { src: "/b" }, + { src: "/c", path: "two" }, + { src: "/d", path: "/one", status: 308 }, +]; +console.log([...collect({ names: new Set(["one", "two"]), routes })]); +try { + collect({ names: new Set(), routes: [{ src: "/z", path: "/missing" }] }); +} catch (e) { + console.log((e as Error).message); +} + +// Closures capture the narrowed binding. +const readers: (() => string)[] = []; +for (const route of routes) { + if (!isPathRoute(route)) continue; + readers.push(() => `${route.src} -> ${route.path}`); +} +console.log(readers.map((read) => read())); + +// Three arms, two of which can hold the refinement: the narrowed value is +// either of them, and their shared members still read. +type A = { kind: string; a: number; tag?: string }; +type B = { kind: string; b: boolean; tag?: string }; +type C = { kind: string; c: string }; +type Item = A | B | C; +function isTagged(item: Item): item is Item & { tag: string } { + return "tag" in item && item.tag !== undefined; +} +const items: Item[] = [ + { kind: "a", a: 1, tag: "x" }, + { kind: "b", b: true }, + { kind: "c", c: "plain" }, + { kind: "b", b: false, tag: "y" }, +]; +for (const item of items) { + if (isTagged(item)) console.log(item.kind, item.tag.toUpperCase()); + else console.log(item.kind, "untagged"); +} diff --git a/tests/corpus/6462-assertion-attached-members.ts b/tests/corpus/6462-assertion-attached-members.ts new file mode 100644 index 000000000..8a2be31b7 --- /dev/null +++ b/tests/corpus/6462-assertion-attached-members.ts @@ -0,0 +1,58 @@ +// `route as Route & { order: number; internal: boolean }` followed by +// writes through the asserted view adds the members to the object the +// original binding names: they appear wherever the route is observed +// later, in the order the writes created them. +type HandleRoute = { handle: string; src?: string; dest?: string; status?: number }; +type SourceRoute = { src: string; dest?: string; path?: string; status?: number }; +type Route = HandleRoute | SourceRoute; + +function markInternal(input: Route[], out: Route[], order: number): void { + for (const route of input) { + const marked = route as Route & { internal: boolean; order: number }; + marked.internal = true; + marked.order = order; + out.unshift(route); + } +} + +const out: Route[] = [{ handle: "filesystem" }, { src: "/plain", dest: "/x" }]; +markInternal([{ src: "/a" }, { handle: "miss" }, { src: "/b", status: 404 }], out, 3); +console.log(JSON.stringify(out)); +console.log(out); +console.log(out.map((r) => ("internal" in (r as Route & { internal?: boolean }) ? "marked" : "plain"))); +console.log(out.map((r) => ({ ...r }))); + +// Values the assertion never reached keep no trace of the members. +const fresh: Route = { src: "/new" }; +console.log(JSON.stringify(fresh), Object.keys(fresh)); + +// A single record shape works the same way, and the asserted view reads +// back what it wrote. +type Job = { id: string; retries?: number }; +const job: Job = { id: "j1" }; +const tracked = job as Job & { startedAt: number }; +tracked.startedAt = 42; +console.log(tracked.startedAt + 1, JSON.stringify(job), Object.keys(job)); + +// An annotated binding of the asserted type keeps the object as well: +// the members appear on it once written, not before. +function stamp(job: Job): Job & { finishedAt: number } { + const done: Job & { finishedAt: number } = job as Job & { finishedAt: number }; + done.finishedAt = 7; + return done; +} +const second: Job = { id: "j2", retries: 1 }; +console.log(stamp(second).finishedAt, JSON.stringify(second), Object.keys(second)); + +const third: Job = { id: "j3" }; +const queued: Job & { queuedAt: number } = third as Job & { queuedAt: number }; +queued.queuedAt = 9; +console.log(queued.queuedAt, JSON.stringify(third), Object.keys(third)); + +function markAnnotated(route: Route): void { + const marked: Route & { internal: boolean } = route as Route & { internal: boolean }; + marked.internal = false; +} +const annotatedRoute: Route = { handle: "hit" }; +markAnnotated(annotatedRoute); +console.log(JSON.stringify(annotatedRoute)); diff --git a/tests/corpus/6480-catch-typeof-untyped-throws.ts b/tests/corpus/6480-catch-typeof-untyped-throws.ts new file mode 100644 index 000000000..98122f851 --- /dev/null +++ b/tests/corpus/6480-catch-typeof-untyped-throws.ts @@ -0,0 +1,40 @@ +// Values thrown from `unknown` storage read back through typeof tests on +// the catch binding, like the same values thrown with static types. +const thrown: unknown[] = [5, 0, -1.5, "text", "", true, false, null, undefined, { kind: "box" }]; +function describe(value: unknown): string { + try { + throw value; + } catch (e) { + const same = e === value; + if (typeof e === "number") return `${same} number ${e + 1}`; + if (typeof e === "string") return `${same} string ${e.length}`; + if (typeof e === "boolean") return `${same} boolean ${!e}`; + if (typeof e === "object") return `${same} object ${e === null ? "null" : "value"}`; + return `${same} ${typeof e}`; + } +} +for (const value of thrown) console.log(describe(value)); +function rethrow(value: unknown): void { + try { + throw value; + } catch (e) { + if (typeof e !== "string") throw e; + console.log("kept string", e); + } +} +for (const value of ["s", 7, false]) { + try { + rethrow(value); + } catch (e) { + console.log("rethrown", typeof e, String(e)); + } +} +let count = 0; +for (const value of thrown) { + try { + throw value; + } catch (e) { + if (typeof e === "number" && e > 0) count += e; + } +} +console.log(count); diff --git a/tests/corpus/cjs-class-hierarchy-npm/main.ts b/tests/corpus/cjs-class-hierarchy-npm/main.ts new file mode 100644 index 000000000..b709aa556 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/main.ts @@ -0,0 +1,58 @@ +// Class hierarchies across the CommonJS modules of an npm package (the +// undici dispatcher shape): an EventEmitter-rooted base in one module, +// symbol-keyed fields whose keys come from a shared export table, a +// computed accessor, subclasses in further modules re-exported through +// require bindings and inline requires, and TypeScript subclasses that +// override a JavaScript method with typed parameters and call super. +import { Agent, Dispatcher, DispatcherBase, Pool, describe, type Handler } from "dispatchkit"; + +class TracingAgent extends Agent { + readonly trace: string[] = []; + + constructor() { + super({ label: "tracing", prefix: "+" }); + } + + dispatch(path: string, handler: Handler): boolean { + this.trace.push(path); + return super.dispatch(path.toUpperCase(), handler); + } +} + +class LimitedPool extends Pool { + rejected = 0; + + dispatch(path: string, handler: Handler): boolean { + const ok = super.dispatch(path, handler); + if (!ok) this.rejected++; + return ok; + } +} + +const print: Handler = (line) => console.log("handled", line); + +function run(d: Dispatcher, paths: string[]): void { + for (const path of paths) console.log(path, "->", d.dispatch(path, print)); +} + +const plain = new Agent(); +run(plain, ["/a", "/b"]); +console.log(describe(plain), plain.kind, plain.prefix); + +const tracing = new TracingAgent(); +tracing.on("closed", (label: unknown, count: unknown) => console.log("closed event", label, count)); +run(tracing, ["/x", "/y/z"]); +console.log(tracing.trace.join(","), tracing.dispatched, tracing.closed); +tracing.close(); +tracing.close(); +console.log(describe(tracing), tracing.kind); +try { + tracing.dispatch("/late", print); +} catch (err) { + console.log("error", (err as Error).message); +} + +const pool = new LimitedPool(3); +run(pool, ["/1", "/long", "/2"]); +console.log(pool.rejected, pool.size, pool.dispatched, pool instanceof DispatcherBase); +console.log(describe(pool), pool.kind); diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/index.d.ts b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/index.d.ts new file mode 100644 index 000000000..bde4d4117 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/index.d.ts @@ -0,0 +1,27 @@ +import { EventEmitter } from "node:events"; + +export type Handler = (line: string) => void; + +export declare class Dispatcher extends EventEmitter { + dispatch(path: string, handler: Handler): boolean; + close(): void; +} + +export declare class DispatcherBase extends Dispatcher { + constructor(opts?: { label?: string }); + get closed(): boolean; + get dispatched(): number; + get kind(): string; +} + +export declare class Agent extends DispatcherBase { + constructor(opts?: { label?: string; prefix?: string }); + prefix: string; +} + +export declare class Pool extends Agent { + constructor(size: number); + size: number; +} + +export declare function describe(d: Dispatcher): string; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/index.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/index.js new file mode 100644 index 000000000..1f6cd7f85 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/index.js @@ -0,0 +1,12 @@ +'use strict'; + +// The undici index shape: classes re-exported through require bindings and +// inline requires of modules whose whole export is the class. +const Dispatcher = require('./lib/dispatcher'); +const { describe } = require('./lib/describe'); + +module.exports.Dispatcher = Dispatcher; +module.exports.DispatcherBase = require('./lib/dispatcher-base'); +module.exports.Agent = require('./lib/agent'); +module.exports.Pool = require('./lib/pool'); +module.exports.describe = describe; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/agent.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/agent.js new file mode 100644 index 000000000..95b2e02f2 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/agent.js @@ -0,0 +1,18 @@ +'use strict'; + +const DispatcherBase = require('./dispatcher-base'); +const { kCount, kDispatch, kLabel } = require('./symbols'); + +class Agent extends DispatcherBase { + constructor({ label = 'agent', prefix = '>' } = {}) { + super({ label }); + this.prefix = prefix; + } + + [kDispatch](path, handler) { + handler(this.prefix + ' ' + this[kLabel] + ' ' + path + ' #' + this[kCount]); + return true; + } +} + +module.exports = Agent; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/describe.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/describe.js new file mode 100644 index 000000000..7c0c3aef7 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/describe.js @@ -0,0 +1,9 @@ +'use strict'; + +const { kCount, kLabel } = require('./symbols'); + +function describe(d) { + return d[kLabel] + ': ' + d[kCount] + ' dispatched, ' + (d.closed ? 'closed' : 'open'); +} + +module.exports = { describe }; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/dispatcher-base.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/dispatcher-base.js new file mode 100644 index 000000000..091455592 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/dispatcher-base.js @@ -0,0 +1,48 @@ +'use strict'; + +const Dispatcher = require('./dispatcher'); +const { kClosed, kDispatch, kCount, kLabel, kOnClose } = require('./symbols'); + +const KIND = 'kind'; + +class DispatcherBase extends Dispatcher { + // Symbol-keyed fields whose keys come from another module. + [kClosed] = false; + + [kCount] = 0; + + [kOnClose] = null; + + constructor(opts) { + super(); + this[kLabel] = (opts && opts.label) || 'base'; + } + + get closed() { + return this[kClosed]; + } + + get dispatched() { + return this[kCount]; + } + + // A computed accessor name. + get [KIND]() { + return 'dispatcher:' + this[kLabel]; + } + + close() { + if (this[kClosed]) return; + this[kClosed] = true; + this.emit('closed', this[kLabel], this[kCount]); + } + + dispatch(path, handler) { + if (this[kClosed]) throw new Error(this[kLabel] + ' is closed'); + this[kCount]++; + // Subclasses declare the symbol-keyed method this calls. + return this[kDispatch](path, handler); + } +} + +module.exports = DispatcherBase; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/dispatcher.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/dispatcher.js new file mode 100644 index 000000000..510208c49 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/dispatcher.js @@ -0,0 +1,15 @@ +'use strict'; + +const EventEmitter = require('node:events'); + +class Dispatcher extends EventEmitter { + dispatch() { + throw new Error('not implemented'); + } + + close() { + throw new Error('not implemented'); + } +} + +module.exports = Dispatcher; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/pool.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/pool.js new file mode 100644 index 000000000..837239fbd --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/pool.js @@ -0,0 +1,20 @@ +'use strict'; + +const Agent = require('./agent'); +const { kCount, kDispatch, kLabel } = require('./symbols'); + +class Pool extends Agent { + constructor(size) { + super({ label: 'pool', prefix: '*' }); + this.size = size; + } + + // Overrides the symbol-keyed method Agent declares. + [kDispatch](path, handler) { + if (path.length > this.size) return false; + handler(this.prefix + ' ' + this[kLabel] + ' ' + path + ' #' + this[kCount]); + return true; + } +} + +module.exports = Pool; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/symbols.js b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/symbols.js new file mode 100644 index 000000000..a795c4178 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/lib/symbols.js @@ -0,0 +1,11 @@ +'use strict'; + +// Keys shared across modules through one export table (undici's +// core/symbols.js). Two keys deliberately share no description. +module.exports = { + kClosed: Symbol('closed'), + kDispatch: Symbol('dispatch'), + kCount: Symbol('count'), + kLabel: Symbol('label'), + kOnClose: Symbol('on close'), +}; diff --git a/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/package.json b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/package.json new file mode 100644 index 000000000..e187b1539 --- /dev/null +++ b/tests/corpus/cjs-class-hierarchy-npm/node_modules/dispatchkit/package.json @@ -0,0 +1,6 @@ +{ + "name": "dispatchkit", + "version": "1.0.0", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/corpus/cjs-open-options-npm/main.ts b/tests/corpus/cjs-open-options-npm/main.ts new file mode 100644 index 000000000..9fd133155 --- /dev/null +++ b/tests/corpus/cjs-open-options-npm/main.ts @@ -0,0 +1,12 @@ +// TypeScript object literals passed to a JavaScript constructor whose +// options parameter is a destructuring pattern with a rest element: the +// literal's extra properties reach the rest object. +import { Agent } from "optkit"; + +console.log(new Agent().describe("a")); +console.log(new Agent({ keepAliveTimeout: 10 }).describe("b")); +console.log(new Agent({ maxOrigins: 2, keepAliveTimeout: 5, pipelining: 1 }).describe("c")); +console.log(new Agent({ factory: (origin) => "client(" + origin + ")" }).describe("d")); + +class TracingAgent extends Agent {} +console.log(new TracingAgent({ pipelining: 3 }).describe("e")); diff --git a/tests/corpus/cjs-open-options-npm/node_modules/optkit/index.d.ts b/tests/corpus/cjs-open-options-npm/node_modules/optkit/index.d.ts new file mode 100644 index 000000000..643b92d84 --- /dev/null +++ b/tests/corpus/cjs-open-options-npm/node_modules/optkit/index.d.ts @@ -0,0 +1,11 @@ +export interface Options { + factory?: (origin: string) => string; + maxOrigins?: number; + keepAliveTimeout?: number; + pipelining?: number; +} + +export declare class Agent { + constructor(opts?: Options); + describe(origin: string): string; +} diff --git a/tests/corpus/cjs-open-options-npm/node_modules/optkit/index.js b/tests/corpus/cjs-open-options-npm/node_modules/optkit/index.js new file mode 100644 index 000000000..0034fc442 --- /dev/null +++ b/tests/corpus/cjs-open-options-npm/node_modules/optkit/index.js @@ -0,0 +1,21 @@ +'use strict'; + +function defaultFactory(origin) { + return 'pool(' + origin + ')'; +} + +// undici's Agent constructor shape: named options with defaults, and a rest +// element collecting every other option for the per-origin dispatchers. +class Agent { + constructor({ factory = defaultFactory, maxOrigins = Infinity, ...options } = {}) { + this.factory = factory; + this.maxOrigins = maxOrigins; + this.options = options; + } + + describe(origin) { + return this.factory(origin) + ' max=' + this.maxOrigins + ' ' + JSON.stringify(this.options); + } +} + +module.exports = { Agent }; diff --git a/tests/corpus/cjs-open-options-npm/node_modules/optkit/package.json b/tests/corpus/cjs-open-options-npm/node_modules/optkit/package.json new file mode 100644 index 000000000..9552d73ee --- /dev/null +++ b/tests/corpus/cjs-open-options-npm/node_modules/optkit/package.json @@ -0,0 +1,6 @@ +{ + "name": "optkit", + "version": "1.0.0", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/corpus/cjs-strict-arguments.cjs b/tests/corpus/cjs-strict-arguments.cjs new file mode 100644 index 000000000..78af27f0d --- /dev/null +++ b/tests/corpus/cjs-strict-arguments.cjs @@ -0,0 +1,43 @@ +// `arguments` as a value in CommonJS functions with parameters where Node +// gives the unmapped object: a 'use strict' file prologue (undici's +// webidl.argumentLengthCheck(arguments, 1, name) idiom) and non-simple +// parameter lists. The object's elements never alias the parameters. +'use strict'; + +function argumentLengthCheck(args, min, ctx) { + if (args.length < min) { + throw new TypeError(`${ctx}: ${min} argument${min === 1 ? '' : 's'} required, but only ${args.length} present.`); + } +} + +function getCookies(headers) { + argumentLengthCheck(arguments, 1, 'getCookies'); + return 'cookies of ' + headers; +} + +function setCookie(headers, cookie) { + argumentLengthCheck(arguments, 2, 'setCookie'); + headers = headers + '!'; + return headers + ' ' + cookie + ' ' + arguments[0]; +} + +function count(first, second) { + const args = arguments; + let total = 0; + for (let i = 0; i < args.length; i++) total += args[i]; + return first + ':' + second + ':' + total; +} + +console.log(getCookies('h1')); +console.log(setCookie('h2', 'c=1')); +try { + getCookies(); +} catch (err) { + console.log(err.name, err.message); +} +try { + setCookie('only'); +} catch (err) { + console.log(err.message); +} +console.log(count(1, 2), count(1, 2, 3, 4), count(5)); diff --git a/tests/corpus/cjs-symbol-key-collisions/client.js b/tests/corpus/cjs-symbol-key-collisions/client.js new file mode 100644 index 000000000..aaa9dbb5d --- /dev/null +++ b/tests/corpus/cjs-symbol-key-collisions/client.js @@ -0,0 +1,16 @@ +'use strict'; + +const { kClose, kUrl } = require('./core-symbols'); + +class Client { + constructor(url) { + this[kUrl] = url; + this[kClose] = 'core close slot'; + } + + close() { + return 'closing ' + this[kUrl] + ' (' + this[kClose] + ')'; + } +} + +module.exports = Client; diff --git a/tests/corpus/cjs-symbol-key-collisions/core-symbols.js b/tests/corpus/cjs-symbol-key-collisions/core-symbols.js new file mode 100644 index 000000000..e42309aa3 --- /dev/null +++ b/tests/corpus/cjs-symbol-key-collisions/core-symbols.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + kClose: Symbol('close'), + kUrl: Symbol('url'), +}; diff --git a/tests/corpus/cjs-symbol-key-collisions/main.js b/tests/corpus/cjs-symbol-key-collisions/main.js new file mode 100644 index 000000000..9f66616b4 --- /dev/null +++ b/tests/corpus/cjs-symbol-key-collisions/main.js @@ -0,0 +1,11 @@ +// Distinct symbols from two CommonJS key tables that share one description: +// each keeps its own slot, and inspect prints both under Node's spelling. +'use strict'; + +const { inspect } = require('node:util'); +const MockClient = require('./mock-client'); + +const m = new MockClient('http://a'); +console.log(m.shutdown()); +console.log(m.shutdown()); +console.log(inspect(m)); diff --git a/tests/corpus/cjs-symbol-key-collisions/mock-client.js b/tests/corpus/cjs-symbol-key-collisions/mock-client.js new file mode 100644 index 000000000..17113c4bb --- /dev/null +++ b/tests/corpus/cjs-symbol-key-collisions/mock-client.js @@ -0,0 +1,24 @@ +'use strict'; + +const Client = require('./client'); +const { kClose, kCalls } = require('./mock-symbols'); + +// Inherits the core `Symbol('close')` slot and declares a method under the +// mock table's distinct `Symbol('close')` (undici's MockClient shape). +class MockClient extends Client { + constructor(url) { + super(url); + this[kCalls] = ''; + } + + [kClose]() { + this[kCalls] = this[kCalls] + '*'; + return 'mock close ' + this[kCalls]; + } + + shutdown() { + return this[kClose]() + ' then ' + this.close(); + } +} + +module.exports = MockClient; diff --git a/tests/corpus/cjs-symbol-key-collisions/mock-symbols.js b/tests/corpus/cjs-symbol-key-collisions/mock-symbols.js new file mode 100644 index 000000000..9726a8444 --- /dev/null +++ b/tests/corpus/cjs-symbol-key-collisions/mock-symbols.js @@ -0,0 +1,7 @@ +'use strict'; + +// A second table reuses a description the first already holds. +module.exports = { + kClose: Symbol('close'), + kCalls: Symbol('calls'), +}; diff --git a/tests/corpus/cjs-symbol-key-collisions/package.json b/tests/corpus/cjs-symbol-key-collisions/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/cjs-symbol-key-collisions/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/cjs-symbol-key-table/main.js b/tests/corpus/cjs-symbol-key-table/main.js new file mode 100644 index 000000000..2c639bf6e --- /dev/null +++ b/tests/corpus/cjs-symbol-key-table/main.js @@ -0,0 +1,19 @@ +// Symbol-keyed class fields and methods whose keys come from a CommonJS +// key table in another module: field declarations, constructor and method +// assignments, a base method calling a subclass's symbol-keyed method, +// reads through the table from a third module, and inspect output. +'use strict'; + +const { inspect } = require('node:util'); +const UpperQueue = require('./upper-queue'); +const { kState, kSize, kName } = require('./symbols'); + +const q = new UpperQueue('jobs'); +console.log(q.push('a'), q.push('b'), q.push(3)); +console.log(q.state, q[kSize], q[kName]); +console.log(q.drain()); +console.log(q.state, q[kSize], q[kState]); +q[kName] = 'renamed'; +console.log(q.state); +console.log(inspect(q)); +console.log(Object.keys(q)); diff --git a/tests/corpus/cjs-symbol-key-table/package.json b/tests/corpus/cjs-symbol-key-table/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/cjs-symbol-key-table/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/cjs-symbol-key-table/queue.js b/tests/corpus/cjs-symbol-key-table/queue.js new file mode 100644 index 000000000..c5b7fdaf5 --- /dev/null +++ b/tests/corpus/cjs-symbol-key-table/queue.js @@ -0,0 +1,37 @@ +'use strict'; + +const { kState, kSize, kName, kRun } = require('./symbols'); + +class Queue { + // Field declarations keyed by table symbols. + [kState] = 'idle'; + + [kSize] = 0; + + constructor(name) { + this[kName] = name; + this.items = []; + } + + push(item) { + this.items.push(item); + this[kSize] = this.items.length; + return this[kSize]; + } + + drain() { + this[kState] = 'draining'; + // The subclass declares the symbol-keyed method this calls. + const out = this[kRun](this.items); + this.items = []; + this[kSize] = 0; + this[kState] = 'idle'; + return out; + } + + get state() { + return this[kName] + ' is ' + this[kState]; + } +} + +module.exports = Queue; diff --git a/tests/corpus/cjs-symbol-key-table/symbols.js b/tests/corpus/cjs-symbol-key-table/symbols.js new file mode 100644 index 000000000..1eca50273 --- /dev/null +++ b/tests/corpus/cjs-symbol-key-table/symbols.js @@ -0,0 +1,11 @@ +// The key table: one module exports every symbol its siblings use as +// class keys (undici's core/symbols.js). Each property names one symbol +// identity for all requirers. +'use strict'; + +module.exports = { + kState: Symbol('state'), + kSize: Symbol('size'), + kName: Symbol('name'), + kRun: Symbol('run'), +}; diff --git a/tests/corpus/cjs-symbol-key-table/upper-queue.js b/tests/corpus/cjs-symbol-key-table/upper-queue.js new file mode 100644 index 000000000..cebdb5b58 --- /dev/null +++ b/tests/corpus/cjs-symbol-key-table/upper-queue.js @@ -0,0 +1,12 @@ +'use strict'; + +const Queue = require('./queue'); +const symbols = require('./symbols'); + +class UpperQueue extends Queue { + [symbols.kRun](items) { + return items.map((item) => String(item).toUpperCase()).join(' '); + } +} + +module.exports = UpperQueue; diff --git a/tests/corpus/cjs-unmapped-arguments.cjs b/tests/corpus/cjs-unmapped-arguments.cjs new file mode 100644 index 000000000..ab4a8f3cb --- /dev/null +++ b/tests/corpus/cjs-unmapped-arguments.cjs @@ -0,0 +1,28 @@ +// Sloppy-mode CommonJS functions whose parameter lists are not simple +// (defaults, destructuring) get the unmapped `arguments` object: +// writing a parameter never shows through it. +function withDefault(a, b = 2) { + a = 'changed'; + return [a, b, arguments.length, arguments[0]].join(' '); +} + +function withPattern({ x }, y) { + y = 'changed'; + return [x, y, arguments.length, arguments[1]].join(' '); +} + +function passAlong(first, { y } = {}) { + first = 'changed'; + return collect(arguments) + ' / ' + y + ' ' + first; +} + +function collect(args) { + const out = []; + for (let i = 0; i < args.length; i++) out.push(String(args[i])); + return out.join(','); +} + +console.log(withDefault('orig')); +console.log(withDefault('orig', 5, 6)); +console.log(withPattern({ x: 1 }, 'orig')); +console.log(passAlong('a', { y: 'b' }, 'c')); diff --git a/tests/corpus/function-local-detached-methods.js b/tests/corpus/function-local-detached-methods.js new file mode 100644 index 000000000..9bde77a19 --- /dev/null +++ b/tests/corpus/function-local-detached-methods.js @@ -0,0 +1,22 @@ +// A method of a function-local class that never reads `this` keeps working +// when it is taken off an instance and called detached or with another +// receiver. +'use strict'; + +function make(label) { + class Box { + one() { + return 1; + } + + named() { + return `box ${label}`; + } + } + return new Box(); +} + +const box = make('a'); +const one = box.one; +console.log(one(), box.one.call(null), box.one.call({}), box.named()); +console.log([box, make('b')].map((b) => b.one()).join(',')); diff --git a/tests/corpus/function-local-detached-methods.ts b/tests/corpus/function-local-detached-methods.ts new file mode 100644 index 000000000..ba4d3ba3c --- /dev/null +++ b/tests/corpus/function-local-detached-methods.ts @@ -0,0 +1,19 @@ +// A method of a function-local class that never reads `this` keeps working +// when it is taken off an instance and called detached or with another +// receiver. +function make(label: string) { + class Box { + one(): number { + return 1; + } + + named(): string { + return `box ${label}`; + } + } + return new Box(); +} + +const box = make("a"); +const one = box.one; +console.log(one(), box.one.call(null), box.one.call({}), box.named()); diff --git a/tests/corpus/function-local-private-evaluations.js b/tests/corpus/function-local-private-evaluations.js new file mode 100644 index 000000000..6e18a4766 --- /dev/null +++ b/tests/corpus/function-local-private-evaluations.js @@ -0,0 +1,172 @@ +// Each evaluation of a function-local class creates its own private names. +// A method taken from one evaluation and called with an instance of another +// (through call, a prototype read, or a checked receiver) finds no brand: +// field reads and writes throw Node's TypeErrors and `#x in this` answers +// false. Calls within one evaluation share the instance's fields. +'use strict'; + +function make(label) { + class Counter { + #count = 0; + #label = label; + #format = (n) => `${label}#${n}`; + + bump() { + this.#count = this.#count + 1; + return `${this.#label}:${this.#count}`; + } + + add(n) { + this.#count += n; + return this.#count; + } + + increment() { + this.#count++; + return this.#count; + } + + reset() { + [this.#count] = [0]; + return this.#count; + } + + assign() { + return (this.#count = 7); + } + + order(note) { + this.#count = note(); + return this.#count; + } + + has() { + return #count in this; + } + + later() { + return () => this.#label; + } + + describe() { + return this.#format(this.#count); + } + + get count() { + return this.#count; + } + + set count(value) { + this.#count = value; + } + } + + class Tagged extends Counter { + tag() { + return `tagged ${this.bump()}`; + } + } + + return { counter: () => new Counter(), tagged: () => new Tagged() }; +} + +function attempt(label, fn) { + try { + console.log(label, fn()); + } catch (e) { + console.log(label, e instanceof TypeError, e.message); + } +} + +const one = make('one'); +const two = make('two'); +const a = one.counter(); +const b = two.counter(); +const a2 = one.counter(); + +console.log(a.bump(), a.bump(), b.bump(), a2.bump()); +console.log(a.add(5), a.increment(), a.has(), a.later()()); + +// Borrowed within one evaluation: the fields are the instance's own. +const bump = a.bump; +console.log(bump.call(a2), a2.bump(), bump.call(a)); +console.log(Object.getPrototypeOf(a).bump.call(a2)); + +// Borrowed across evaluations. +attempt('call read', () => a.bump.call(b)); +attempt('compound', () => a.add.call(b, 1)); +attempt('update', () => a.increment.call(b)); +attempt('destructure', () => a.reset.call(b)); +attempt('assign value', () => a.assign.call(b)); +attempt('value first', () => + a.order.call(b, () => { + console.log('value evaluated'); + return 1; + }), +); +attempt('proto read', () => Object.getPrototypeOf(a).bump.call(b)); +attempt('has other', () => a.has.call(b)); +attempt('has own', () => a.has.call(a2)); +attempt('arrow', () => a.later.call(b)()); +attempt('field call', () => a.describe.call(b)); +attempt('field call own', () => a.describe.call(a2)); +attempt('apply', () => a.bump.apply(b)); +attempt('bind', () => a.bump.bind(b)()); +attempt('Reflect.apply', () => Reflect.apply(a.bump, b, [])); +const proto = Object.getPrototypeOf(a); +const bumpDescriptor = /** @type {any} */ (Object.getOwnPropertyDescriptor(proto, 'bump')); +const countDescriptor = /** @type {any} */ (Object.getOwnPropertyDescriptor(proto, 'count')); +attempt('descriptor value', () => bumpDescriptor.value.call(b)); +attempt('getter', () => countDescriptor.get.call(b)); +attempt('setter', () => countDescriptor.set.call(b, 5)); +attempt('Reflect.get', () => Reflect.get(a, 'count', b)); +attempt('Reflect.set', () => Reflect.set(a, 'count', 3, b)); +attempt('own getter', () => [countDescriptor.get.call(a2), Reflect.get(a, 'count', a2)].join(' ')); +console.log(b.bump(), b.count, (b.count = 10), b.bump()); + +// Receivers that are not instances. +for (const recv of [{}, [], undefined, null, 1, 'x']) { + attempt(`read ${String(recv)}`, () => a.bump.call(recv)); + attempt(`write ${String(recv)}`, () => a.reset.call(recv)); + attempt(`has ${String(recv)}`, () => a.has.call(recv)); +} + +// A subclass declared after the class in the same function carries the +// brand of its own evaluation. +const t1 = one.tagged(); +const t2 = two.tagged(); +console.log(t1.tag(), t2.tag(), t1.has(), a.has.call(t1), a.has.call(t2)); +attempt('subclass across', () => a.bump.call(t2)); +attempt('subclass own', () => b.bump.call(t2)); + +// undici's iterator guard: the error comes from the method itself. +function createIterator(name) { + class FastIterableIterator { + #target; + #index = 0; + + constructor(target) { + this.#target = target; + } + + next() { + if (typeof this !== 'object' || this === null || !(#target in this)) { + throw new TypeError(`'next' called on an object that does not implement interface ${name} Iterator.`); + } + const index = this.#index; + if (index >= this.#target.length) return { value: undefined, done: true }; + this.#index = index + 1; + return { value: this.#target[index], done: false }; + } + } + return (target) => new FastIterableIterator(target); +} + +const headers = createIterator('Headers'); +const params = createIterator('Params'); +const it = headers(['a', 'b']); +const next = it.next; +console.log(JSON.stringify(next.call(it)), JSON.stringify(it.next()), JSON.stringify(next.call(it))); +for (const recv of [headers(['c']), params(['d']), {}, null]) { + attempt('next', () => JSON.stringify(next.call(recv))); +} diff --git a/tests/corpus/function-local-private-fields.js b/tests/corpus/function-local-private-fields.js new file mode 100644 index 000000000..c8c999659 --- /dev/null +++ b/tests/corpus/function-local-private-fields.js @@ -0,0 +1,48 @@ +// A class declared inside a function with private fields used only on +// `this` (undici's webidl createIterator): each evaluation's instances meet +// only their own methods, so one compiled layout serves every evaluation. +'use strict'; + +function createIterator(name, entries) { + class FastIterableIterator { + #target; + #kind; + #index; + + constructor(target, kind) { + this.#target = target; + this.#kind = kind; + this.#index = 0; + } + + next() { + if (typeof this !== 'object' || this === null || !(#target in this)) { + throw new TypeError(`'next' called on an object that does not implement interface ${name} Iterator.`); + } + const values = entries(this.#target); + if (this.#index >= values.length) return { value: undefined, done: true }; + const pair = values[this.#index]; + this.#index = this.#index + 1; + if (this.#kind === 'key') return { value: pair[0], done: false }; + if (this.#kind === 'value') return { value: pair[1], done: false }; + return { value: pair[0] + '=' + pair[1], done: false }; + } + } + return function (target, kind) { + return new FastIterableIterator(target, kind); + }; +} + +const headers = createIterator('Headers', (t) => t.pairs); +const params = createIterator('Params', (t) => t.list); + +function drain(it) { + const out = []; + for (let step = it.next(); !step.done; step = it.next()) out.push(step.value); + return out.join(','); +} + +const target = { pairs: [['a', '1'], ['b', '2']] }; +console.log(drain(headers(target, 'key'))); +console.log(drain(headers(target, 'value'))); +console.log(drain(params({ list: [['x', 'y']] }, 'key+value'))); diff --git a/tests/corpus/private-in-checked-primitives.js b/tests/corpus/private-in-checked-primitives.js new file mode 100644 index 000000000..f73902b74 --- /dev/null +++ b/tests/corpus/private-in-checked-primitives.js @@ -0,0 +1,28 @@ +// `#x in v` where v is a checked value: objects answer by brand, and a +// primitive operand throws Node's TypeError, which names the value as +// String(v) spells it. +'use strict'; + +class Base {} + +class Box extends Base { + #value = 1; + + static is(v) { + return #value in v; + } +} + +class Sub extends Box {} + +console.log(Box.is(new Box()), Box.is(new Sub()), Box.is(new Base())); +console.log(Box.is({}), Box.is([]), Box.is(function () {}), Box.is(Box)); + +const primitives = [1, -0, NaN, 1.5, 'str', '', true, false, undefined, null, 10n, -5n, Symbol('s'), Symbol()]; +for (const v of primitives) { + try { + console.log('no throw', Box.is(v)); + } catch (e) { + console.log(e instanceof TypeError, e.message); + } +} diff --git a/tests/diagnostics/boot-path-node-surface-fences.ts b/tests/diagnostics/boot-path-node-surface-fences.ts new file mode 100644 index 000000000..146a32aa1 --- /dev/null +++ b/tests/diagnostics/boot-path-node-surface-fences.ts @@ -0,0 +1,23 @@ +// The boot-path Node surface keeps its boundaries: process.send answers +// presence checks but stays fenced as a stored value, a stream hook must +// declare Node's argument type or a supertype of it, and util.promisify +// over a function value needs an Error-typed callback error. +import { promisify } from "node:util"; +import { Writable } from "node:stream"; + +const send = process.send; +console.log(typeof send); + +class Counter extends Writable { + _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(chunk); + cb(); + } +} +new Counter().end("x"); + +function legacy(cb: (code: number, value: string) => void): void { + cb(0, "v"); +} +const legacyAsync = promisify(legacy); +legacyAsync().then((v) => console.log(v)); diff --git a/tests/diagnostics/cjs-member-alias-writes/main.ts b/tests/diagnostics/cjs-member-alias-writes/main.ts new file mode 100644 index 000000000..d3723c41a --- /dev/null +++ b/tests/diagnostics/cjs-member-alias-writes/main.ts @@ -0,0 +1,6 @@ +// A member export is read through its binding only when that binding still +// holds the exported value: a member that holds a reassigned binding, or one +// that both a module and its re-exporter write, is refused. +import { counter, version } from "metrics"; +console.log(counter); +console.log(version); diff --git a/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/index.d.ts b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/index.d.ts new file mode 100644 index 000000000..c9c639246 --- /dev/null +++ b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/index.d.ts @@ -0,0 +1,6 @@ +declare function metrics(): string; +declare namespace metrics { + const counter: number; + const version: string; +} +export = metrics; diff --git a/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/index.js b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/index.js new file mode 100644 index 000000000..7449f0e8a --- /dev/null +++ b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/index.js @@ -0,0 +1,3 @@ +"use strict"; +module.exports = require("./lib.js"); +module.exports.version = "2.0.0"; diff --git a/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/lib.js b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/lib.js new file mode 100644 index 000000000..913fe8bd1 --- /dev/null +++ b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/lib.js @@ -0,0 +1,8 @@ +"use strict"; +let counter = 1; +exports = module.exports = function metrics() { + return "metrics"; +}; +exports.counter = counter; +exports.version = "1.0.0"; +counter = 5; diff --git a/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/package.json b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/package.json new file mode 100644 index 000000000..f97fe551f --- /dev/null +++ b/tests/diagnostics/cjs-member-alias-writes/node_modules/metrics/package.json @@ -0,0 +1,6 @@ +{ + "name": "metrics", + "version": "1.0.0", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/diagnostics/class-field-accessor-descriptor.ts b/tests/diagnostics/class-field-accessor-descriptor.ts new file mode 100644 index 000000000..66078f6c2 --- /dev/null +++ b/tests/diagnostics/class-field-accessor-descriptor.ts @@ -0,0 +1,14 @@ +// Object.defineProperty can change a declared class field's value and +// attributes, but not turn it into an accessor: a literal get or set +// descriptor is refused at compile time, naming the field. +class Value { + x = 1; +} + +const value = new Value(); +Object.defineProperty(value, "x", { + get() { + return 2; + }, +}); +console.log(value.x); diff --git a/tests/diagnostics/local-class-boundaries.ts b/tests/diagnostics/local-class-boundaries.ts index a82dfb869..2c43e9900 100644 --- a/tests/diagnostics/local-class-boundaries.ts +++ b/tests/diagnostics/local-class-boundaries.ts @@ -1,4 +1,4 @@ -function privateBrand() { return class { #value = 1; read(): number { return this.#value; } }; } +function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } function selfName() { return class Local { copy() { return new Local(); } }; } class Base {} function derived() { return class extends Base {}; } diff --git a/tests/diagnostics/local-class-type-query-brand.ts b/tests/diagnostics/local-class-type-query-brand.ts index 566304627..aeb905cad 100644 --- a/tests/diagnostics/local-class-type-query-brand.ts +++ b/tests/diagnostics/local-class-type-query-brand.ts @@ -1,11 +1,15 @@ // The same speculative element-type query, reaching a function-local class -// with a private brand through the function's returned array. +// with a private brand through the function's returned array. The brand is +// tested on another object, so the evaluations' private names must differ. function make() { class Box { #v = 1; read(): number { return this.#v; } + same(other: object): boolean { + return #v in other; + } } return [new Box()]; } diff --git a/tests/diagnostics/maybe-promise-fences.ts b/tests/diagnostics/maybe-promise-fences.ts index efe541c9e..1dcccaf08 100644 --- a/tests/diagnostics/maybe-promise-fences.ts +++ b/tests/diagnostics/maybe-promise-fences.ts @@ -30,8 +30,3 @@ console.log(typeof widened); // `instanceof Promise` on a value that is never a promise. const record = { v: 1 }; console.log(record instanceof Promise); - -// A variadic `unknown[]` slot takes closures with the same variadic ABI. -type HandlerFn = (...args: unknown[]) => Promise; -const fixed: HandlerFn = async (first: unknown) => first; -fixed(1, 2).then((v) => console.log(v)); diff --git a/tests/diagnostics/mock-fences.ts b/tests/diagnostics/mock-fences.ts index 4425dfa5f..ef92f366f 100644 --- a/tests/diagnostics/mock-fences.ts +++ b/tests/diagnostics/mock-fences.ts @@ -1,8 +1,9 @@ -// node:test mocks lower for mock.fn(original[, implementation]), +// node:test mocks lower for mock.fn([original[, implementation]]), // mock.method(object, methodName[, implementation]) on records, their // t.mock twins, and the trackers' restoreAll and reset. The rest of the // mock surface is refused, each with a named diagnostic: mock.fn() without -// an original; mock.method on a class instance or a builtin object, with +// an original for a function type whose return type does not include +// undefined; mock.method on a class instance or a builtin object, with // an options object, or with an implementation of another type when the // result is used; and functions whose union return type has no undefined // arm (the calls log records undefined for calls that throw). @@ -11,8 +12,11 @@ import { mock, test } from "node:test"; const add = mock.fn((a: number, b: number) => a + b); // lowers add(1, 2); -const empty = mock.fn(); // no original function -empty(); +const empty = mock.fn(); // lowers +empty(1, "two"); + +const counted = mock.fn<() => number>(); // no original, returns number +counted(); const obj = { value: () => 1 }; mock.method(obj, "value"); // lowers diff --git a/tests/diagnostics/object-assign-class-copy.ts b/tests/diagnostics/object-assign-class-copy.ts new file mode 100644 index 000000000..4899306a9 --- /dev/null +++ b/tests/diagnostics/object-assign-class-copy.ts @@ -0,0 +1,11 @@ +// Object.assign copying a class instance into a fresh object: the result +// is a plain object holding the instance's own fields, never an instance, +// so a result the checker types as the class is refused rather than +// failing its checked cast at run time. +class Config { + b = 1; + a = 2; +} +const config = new Config(); +const copy = Object.assign({}, config); +console.log(JSON.stringify(copy), Object.keys(copy).join(",")); diff --git a/tests/diagnostics/object-literal-accessor-values.ts b/tests/diagnostics/object-literal-accessor-values.ts new file mode 100644 index 000000000..107ab1f69 --- /dev/null +++ b/tests/diagnostics/object-literal-accessor-values.ts @@ -0,0 +1,31 @@ +// Object literals with get/set accessors keep their closures in reserved +// slots, so their typed values have no JSON or checked-dynamic form: Node's +// JSON.stringify and dynamic reads invoke the getter, which neither a typed +// serializer nor a converted copy could do. + +function hold(value: unknown): unknown { + return value; +} + +const counter = { + count: 1, + get doubled(): number { + return 2; + }, +}; + +// JSON.stringify of the typed value would omit the getter's property. +console.log(JSON.stringify(counter)); + +// Converting the typed value to `unknown` or `any` would lose the getter. +hold(counter); +const viaAny: any = counter; +console.log(viaAny); + +// A literal written into an `any` slot directly keeps its accessor. +const direct: any = { + get value(): number { + return 3; + }, +}; +console.log(direct.value); diff --git a/tests/diagnostics/thread-slots-fences.ts b/tests/diagnostics/thread-slots-fences.ts index 7ec71db34..ed985c803 100644 --- a/tests/diagnostics/thread-slots-fences.ts +++ b/tests/diagnostics/thread-slots-fences.ts @@ -27,3 +27,8 @@ function local(slots: ThreadSlots, key: Key): State | undefined { const ctor = ThreadSlots; const getter = first.get; console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); + +// A read through an annotated value type the stored values can't be converted +// to: they are stored as records, which a read as the class can't convert. +const records: ThreadSlots = new ThreadSlots("fences.records"); +console.log(records.get(new Key())); diff --git a/tests/fixtures/esm-path-globals/lib/helper.ts b/tests/fixtures/esm-path-globals/lib/helper.ts new file mode 100644 index 000000000..61960c097 --- /dev/null +++ b/tests/fixtures/esm-path-globals/lib/helper.ts @@ -0,0 +1,2 @@ +export const helperDir: string = __dirname; +export const helperFile: string = __filename; diff --git a/tests/fixtures/esm-path-globals/main.ts b/tests/fixtures/esm-path-globals/main.ts new file mode 100644 index 000000000..d3d3c3b8a --- /dev/null +++ b/tests/fixtures/esm-path-globals/main.ts @@ -0,0 +1,12 @@ +// __dirname and __filename in TypeScript files with import syntax: each +// module reads its own source location, the same strings as +// import.meta.dirname and import.meta.filename. +import { basename, join } from "node:path"; +import { helperDir, helperFile } from "./lib/helper.ts"; + +console.log(typeof __dirname, typeof __filename); +console.log(basename(__dirname), basename(__filename)); +console.log(__dirname === import.meta.dirname, __filename === import.meta.filename); +console.log(join(__dirname, "lib") === helperDir, basename(helperFile)); +const home = typeof __dirname === "string" && __dirname.endsWith("/lib") ? join(__dirname, "..") : "/tmp"; +console.log(home); diff --git a/tests/fixtures/node-test/cases/mock-fn-default.ts b/tests/fixtures/node-test/cases/mock-fn-default.ts new file mode 100644 index 000000000..4666dfa5e --- /dev/null +++ b/tests/fixtures/node-test/cases/mock-fn-default.ts @@ -0,0 +1,72 @@ +// mock.fn() and t.mock.fn() without an original: the mock returns +// undefined and records the arguments of each call, whatever its callers +// pass. mockImplementation and mockImplementationOnce replace its +// behavior, and restore() brings back the function that returns undefined. +// mock.fn() records F's parameters, for a function type whose return +// type is void or includes undefined. +import assert from "node:assert"; +import { mock, test } from "node:test"; + +test("records calls with any arguments", () => { + const fn = mock.fn(); + fn(1, "two"); + fn(); + fn(true, null, [3]); + assert.strictEqual(fn.mock.callCount(), 3); + const [first, second, third] = fn.mock.calls; + assert.deepStrictEqual(first!.arguments, [1, "two"]); + assert.strictEqual(second!.arguments.length, 0); + assert.deepStrictEqual(third!.arguments, [true, null, [3]]); + assert.ok(first!.result === undefined); + assert.ok(first!.error === undefined); + assert.ok(first!.this === undefined); +}); + +test("changes implementations", () => { + const seen: string[] = []; + const fn = mock.fn(); + fn.mock.mockImplementation((...args: unknown[]) => { + seen.push(`impl ${args.length}`); + return undefined; + }); + fn.mock.mockImplementationOnce(() => { + seen.push("once"); + return undefined; + }); + fn("a"); + fn("a", "b"); + fn.mock.restore(); + fn("c"); + assert.deepStrictEqual(seen, ["once", "impl 2"]); + assert.strictEqual(fn.mock.callCount(), 3); + fn.mock.resetCalls(); + assert.strictEqual(fn.mock.callCount(), 0); +}); + +test("records a throwing implementation", () => { + const fn = mock.fn(); + fn.mock.mockImplementation(() => { + throw new Error("boom"); + }); + assert.throws(() => fn(7), { message: "boom" }); + const call = fn.mock.calls[0]!; + assert.deepStrictEqual(call.arguments, [7]); + assert.ok(call.error instanceof Error); +}); + +test("t.mock.fn() and typed mock.fn()", (t) => { + const fn = t.mock.fn(); + fn("x"); + assert.strictEqual(fn.mock.callCount(), 1); + const typed = t.mock.fn<(a: number, b: string) => void>(); + typed(1, "x"); + typed(2, "y"); + assert.strictEqual(typed.mock.callCount(), 2); + assert.strictEqual(typed.mock.calls[1]!.arguments[0], 2); + assert.strictEqual(typed.mock.calls[1]!.arguments[1], "y"); + const maybe = t.mock.fn<() => number | undefined>(); + assert.ok(maybe() === undefined); + maybe.mock.mockImplementation(() => 4); + assert.ok(maybe() === 4); + assert.ok(maybe.mock.calls[1]!.result === 4); +}); diff --git a/tests/fixtures/node-types/promisify-callback-functions.ts b/tests/fixtures/node-types/promisify-callback-functions.ts new file mode 100644 index 000000000..78be5e3bd --- /dev/null +++ b/tests/fixtures/node-types/promisify-callback-functions.ts @@ -0,0 +1,67 @@ +// util.promisify over typed callback-last functions with leading arguments +// under @types/node: bound methods, a function declaration, rejection from +// the callback error, a synchronous throw, and asynchronous settlement. +import { promisify } from "node:util"; + +class Client { + private closed = false; + private readonly name: string; + constructor(name: string) { + this.name = name; + } + close(callback: (error?: Error) => void): void { + setTimeout(() => { + if (this.closed) { + callback(new Error(`${this.name} already closed`)); + return; + } + this.closed = true; + callback(); + }, 1); + } + fetch(key: string, callback: (error: Error | null, value?: string) => void): void { + setImmediate(() => { + if (key === "") callback(new RangeError("empty key")); + else callback(null, `${this.name}:${key}`); + }); + } +} + +function sum(a: number, b: number, done: (err: Error | null, total: number) => void): void { + if (a < 0) throw new TypeError("negative"); + process.nextTick(() => done(null, a + b)); +} + +async function main(): Promise { + const c = new Client("db"); + const closeAsync = promisify(c.close.bind(c)); + await closeAsync(); + console.log("closed once"); + try { + await closeAsync(); + } catch (e) { + console.log("second close:", (e as Error).message); + } + const fetchAsync = promisify(c.fetch.bind(c)); + console.log(await fetchAsync("k")); + try { + await fetchAsync(""); + } catch (e) { + console.log("fetch:", (e as Error).name, (e as Error).message); + } + const sumAsync = promisify(sum); + console.log("sum", await sumAsync(2, 3)); + try { + await sumAsync(-1, 3); + } catch (e) { + console.log("sync throw:", (e as Error).name, (e as Error).message); + } + await promisify(new Client("inline").close.bind(new Client("inline")))(); + console.log("inline done"); + const order: string[] = []; + const p = sumAsync(1, 1).then((v) => order.push(`then ${v}`)); + order.push("after call"); + await p; + console.log(order.join(", ")); +} +void main(); diff --git a/tests/fixtures/node-types/stream-subclass-hooks.ts b/tests/fixtures/node-types/stream-subclass-hooks.ts new file mode 100644 index 000000000..0e7cfbad5 --- /dev/null +++ b/tests/fixtures/node-types/stream-subclass-hooks.ts @@ -0,0 +1,46 @@ +// Stream subclasses under @types/node: a Writable whose hooks declare a +// supertype of Node's chunk type, a Transform, and an erroring Writable. +import { Transform, Writable } from "node:stream"; + +class LineSink extends Writable { + private rest = ""; + readonly lines: string[] = []; + _write(chunk: Buffer | string, encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("chunk", typeof chunk, Buffer.isBuffer(chunk), encoding); + const parts = (this.rest + chunk.toString()).split("\n"); + this.rest = parts.pop() ?? ""; + this.lines.push(...parts); + cb(); + } + _final(cb: (err?: Error | null) => void): void { + if (this.rest !== "") this.lines.push(this.rest); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: string | Buffer, _enc: string, cb: (err?: Error | null, data?: string) => void): void { + cb(null, chunk.toString().toUpperCase()); + } +} + +class Failing extends Writable { + _write(chunk: Uint8Array | string, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + cb(new Error(`refused ${chunk.toString().length} bytes`)); + } +} + +const sink = new LineSink(); +sink.write("a\nb"); +sink.write(Buffer.from("c\nd")); +sink.end("e"); +sink.on("finish", () => console.log("lines", JSON.stringify(sink.lines))); + +const up = new Upper(); +up.on("data", (d: Buffer) => console.log("upper", d.toString())); +up.write("hello "); +up.end("world"); + +const bad = new Failing(); +bad.on("error", (e) => console.log("error", e.message)); +bad.write("xyz"); diff --git a/tests/fixtures/node-types/tls-session-timings.ts b/tests/fixtures/node-types/tls-session-timings.ts new file mode 100644 index 000000000..7ebb53ce9 --- /dev/null +++ b/tests/fixtures/node-types/tls-session-timings.ts @@ -0,0 +1,31 @@ +// Under @types/node: a connector-style TLS client reads +// isSessionReused() through a TLSSocket assertion and keeps per-phase +// timings in a string-keyed record with `??=`. +import * as net from "node:net"; +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); + +const timings: Record = {}; +function mark(phase: string, value: number): void { + timings[phase] ??= value; +} + +const server = tls.createServer({ key, cert }, (socket) => socket.end("ok")); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const socket: net.Socket = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca }, () => { + mark("secure", 1); + mark("secure", 2); + console.log("reused", (socket as tls.TLSSocket).isSessionReused()); + }); + socket.on("data", (d: Buffer) => console.log("data", d.toString())); + socket.on("close", () => { + mark("close", 3); + console.log(JSON.stringify(timings)); + server.close(); + }); +}); diff --git a/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts b/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts new file mode 100644 index 000000000..5cbbc3dbc --- /dev/null +++ b/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts @@ -0,0 +1,29 @@ +// esbuild's bundled CommonJS chunks: each package below is real esbuild +// output (0.25 unminified, 0.25 whitespace+syntax minified, 0.17) of one +// source that bundles CommonJS modules through the __commonJS helper. +// Every chunk compiles as its own CommonJS module: it runs once, at its +// first require (the top-level interop imports run while the bundle +// loads; `lazy` requires its chunk only when called), chunk-to-chunk +// requires share one module instance, an esbuild-built dist bundled as a +// chunk keeps its suffixed helpers, and the external dependency passes +// through the __toESM interop. gtcjsdist is a "type": "module" package +// whose CommonJS entry is a .cjs bundle using the node-mode interop. +import { describe, external, lazy, nested } from "gtchunk"; +import { describe as dws, external as ews, lazy as lws, nested as nws } from "gtchunkws"; +import { describe as d17, external as e17, lazy as l17, nested as n17 } from "gtchunk17"; +import { viaNodeMode } from "gtcjsdist"; + +console.log("main start"); +console.log("plain", describe()); +console.log("plain", external()); +console.log("plain", nested()); +console.log("plain", lazy(), lazy()); +console.log("minified", dws()); +console.log("minified", ews()); +console.log("minified", nws()); +console.log("minified", lws(), lws()); +console.log("older", d17()); +console.log("older", e17()); +console.log("older", n17()); +console.log("older", l17(), l17()); +console.log(viaNodeMode()); diff --git a/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts b/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts new file mode 100644 index 000000000..042102b64 --- /dev/null +++ b/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts @@ -0,0 +1,11 @@ +// A require cycle between esbuild's per-file CommonJS outputs (index ↔ +// merge, index ↔ append). Each file installs its live getter table +// before its first require, so a partner that loads while index.js is +// still initializing sees index's partially initialized exports, and both +// partners call back into index from their functions. index.js re-exports +// mergeAll from merge.js through its getter table. +import { run, mergeAll } from "gtcycle"; + +console.log(run([" a ", "", "@x"], ["b", "@keep"])); +console.log(run(["c"], [])); +console.log(mergeAll(["@keep", " d"], ["@drop"]).join("|")); diff --git a/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts b/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts new file mode 100644 index 000000000..1ed90d432 --- /dev/null +++ b/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts @@ -0,0 +1,11 @@ +// The same esbuild CommonJS cycle entered through a partner: merge.js +// loads first, requires index.js, which requires merge.js back while +// merge is still initializing (its getter table is already installed) and +// re-exports mergeAll from it: the re-export reads merge's hoisted +// function while merge is still initializing. +import { mergeAll } from "gtcycle/dist/merge.js"; +import { run, mergeAll as reexported } from "gtcycle"; + +console.log(mergeAll([" x "], ["@keep"]).join(",")); +console.log(reexported === mergeAll); +console.log(run(["y"], ["@z"])); diff --git a/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts b/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts new file mode 100644 index 000000000..3efeb9eb1 --- /dev/null +++ b/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts @@ -0,0 +1,6 @@ +// A require cycle whose partner reads a const export of index.js while +// index is still initializing: Node throws a ReferenceError (the binding +// is in its temporal dead zone), and scriptc refuses the cycle. +import { show } from "gtcyclebad"; + +console.log(show()); diff --git a/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts b/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts new file mode 100644 index 000000000..b34359cc7 --- /dev/null +++ b/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts @@ -0,0 +1,10 @@ +// The program reaches gtsubpath only through its leaf data module (and a +// type from its main entry). The main entry reaches a file whose bundler +// interop the recognizer cannot verify, but no compiled code can load it, +// so the package compiles: a package is judged by what its imported +// subpaths reach. +import type { Manager } from "gtsubpath"; +import { managers } from "gtsubpath/dist/data.js"; + +const names: Manager[] = managers.filter((m) => m.lockfile.endsWith(".yaml")); +console.log(managers.length, names.map((m) => m.name).join(",")); diff --git a/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts b/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts new file mode 100644 index 000000000..d52b3731d --- /dev/null +++ b/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts @@ -0,0 +1,7 @@ +// A bundled CommonJS chunk that calls the bundle's top-level `label` while +// a nested function of the chunk declares its own `label`: the chunk +// carries the top-level function along, and each reference keeps its own +// binding. +import { inner, outer } from "gtchunkshadow"; + +console.log(outer(), inner()); diff --git a/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts b/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts new file mode 100644 index 000000000..21ca81b40 --- /dev/null +++ b/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts @@ -0,0 +1,7 @@ +// Two bundled CommonJS chunks export the bundle's top-level `tally` and +// count calls on a property of it. Node prints "1 2 true": both chunks +// share one function. A per-chunk copy would split its identity and its +// properties, so the package stays refused. +import { counts, same } from "gtchunkshared"; + +console.log(counts(), same()); diff --git a/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts b/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts new file mode 100644 index 000000000..3cca145e0 --- /dev/null +++ b/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts @@ -0,0 +1,7 @@ +// A require cycle whose partner calls an export of index.js with esbuild's +// `(0, import_index.peek)()` spelling while index is still initializing: +// peek reads a const in its temporal dead zone, so Node throws a +// ReferenceError, and scriptc refuses the cycle. +import { show } from "gtcyclecall"; + +console.log(show()); diff --git a/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts b/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts new file mode 100644 index 000000000..ae2be341a --- /dev/null +++ b/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts @@ -0,0 +1,8 @@ +// Require cycles whose partner stores an export of index.js (in a const, +// or in an object literal) and calls it while index is still +// initializing: the export reads a const in its temporal dead zone, so +// Node throws a ReferenceError, and scriptc refuses both cycles. +import { show } from "gtcyclealias"; +import { show as showBox } from "gtcyclebox"; + +console.log(show(), showBox()); diff --git a/tests/fixtures/npm/node_modules/gtchunk/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunk/dist/index.d.ts new file mode 100644 index 000000000..bd3d4e582 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(): string; +export declare function external(): string; +export declare function nested(): string; +export declare function lazy(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunk/dist/index.js b/tests/fixtures/npm/node_modules/gtchunk/dist/index.js new file mode 100644 index 000000000..7820e3ae4 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk/dist/index.js @@ -0,0 +1,132 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// chunk/vendor/settings/counter.js +var require_counter = __commonJS({ + "chunk/vendor/settings/counter.js"(exports2, module2) { + var n = 0; + console.log("counter init"); + module2.exports = { + next() { + n += 1; + return n; + } + }; + } +}); + +// chunk/vendor/settings/index.js +var require_settings = __commonJS({ + "chunk/vendor/settings/index.js"(exports2) { + "use strict"; + console.log("settings init"); + var counter = require_counter(); + exports2.LABEL = "bundled"; + exports2.VERSION = 2; + exports2.bump = function() { + return counter.next(); + }; + } +}); + +// chunk/vendor/prebuilt/index.js +var require_prebuilt = __commonJS({ + "chunk/vendor/prebuilt/index.js"(exports2, module2) { + var __defProp2 = Object.defineProperty; + var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; + var __getOwnPropNames2 = Object.getOwnPropertyNames; + var __hasOwnProp2 = Object.prototype.hasOwnProperty; + var __export2 = (target, all) => { + for (var name in all) + __defProp2(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps2 = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames2(from)) + if (!__hasOwnProp2.call(to, key) && key !== except) + __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); + var prebuilt_exports = {}; + __export2(prebuilt_exports, { + PREBUILT_TAG: () => PREBUILT_TAG2, + greet: () => greet2 + }); + module2.exports = __toCommonJS2(prebuilt_exports); + function greet2(name) { + return "hello " + name; + } + var PREBUILT_TAG2 = "prebuilt"; + } +}); + +// chunk/vendor/lazy.js +var require_lazy = __commonJS({ + "chunk/vendor/lazy.js"(exports2, module2) { + console.log("lazy init"); + module2.exports = { value: "lazy-value" }; + } +}); + +// chunk/src/index.ts +var index_exports = {}; +__export(index_exports, { + describe: () => describe, + external: () => external, + lazy: () => lazy, + nested: () => nested +}); +module.exports = __toCommonJS(index_exports); +var import_settings = __toESM(require_settings()); +var core = __toESM(require("gtcore")); +var import_gtcore = __toESM(require("gtcore")); +var import_prebuilt = __toESM(require_prebuilt()); +function describe() { + return import_settings.LABEL + "@" + import_settings.default.VERSION + " bump " + (0, import_settings.bump)() + " " + (0, import_settings.bump)(); +} +function external() { + return core.tag + ":" + core.label(1) + ":" + import_gtcore.default.label(2); +} +function nested() { + return (0, import_prebuilt.greet)("chunk") + " " + import_prebuilt.PREBUILT_TAG; +} +function lazy() { + return require_lazy().value; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + describe, + external, + lazy, + nested +}); diff --git a/tests/fixtures/npm/node_modules/gtchunk/package.json b/tests/fixtures/npm/node_modules/gtchunk/package.json new file mode 100644 index 000000000..2812a5188 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtchunk", + "version": "1.0.0", + "description": "esbuild 0.25.10 output (--bundle --platform=node --format=cjs --external:gtcore): bundled CommonJS modules behind the __commonJS helper, including an esbuild-built dist with suffixed helpers", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtchunk17/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.d.ts new file mode 100644 index 000000000..bd3d4e582 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(): string; +export declare function external(): string; +export declare function nested(): string; +export declare function lazy(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunk17/dist/index.js b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.js new file mode 100644 index 000000000..afda6af56 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.js @@ -0,0 +1,132 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// chunk/vendor/settings/counter.js +var require_counter = __commonJS({ + "chunk/vendor/settings/counter.js"(exports, module2) { + var n = 0; + console.log("counter init"); + module2.exports = { + next() { + n += 1; + return n; + } + }; + } +}); + +// chunk/vendor/settings/index.js +var require_settings = __commonJS({ + "chunk/vendor/settings/index.js"(exports) { + "use strict"; + console.log("settings init"); + var counter = require_counter(); + exports.LABEL = "bundled"; + exports.VERSION = 2; + exports.bump = function() { + return counter.next(); + }; + } +}); + +// chunk/vendor/prebuilt/index.js +var require_prebuilt = __commonJS({ + "chunk/vendor/prebuilt/index.js"(exports, module2) { + var __defProp2 = Object.defineProperty; + var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; + var __getOwnPropNames2 = Object.getOwnPropertyNames; + var __hasOwnProp2 = Object.prototype.hasOwnProperty; + var __export2 = (target, all) => { + for (var name in all) + __defProp2(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps2 = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames2(from)) + if (!__hasOwnProp2.call(to, key) && key !== except) + __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); + var prebuilt_exports = {}; + __export2(prebuilt_exports, { + PREBUILT_TAG: () => PREBUILT_TAG2, + greet: () => greet2 + }); + module2.exports = __toCommonJS2(prebuilt_exports); + function greet2(name) { + return "hello " + name; + } + var PREBUILT_TAG2 = "prebuilt"; + } +}); + +// chunk/vendor/lazy.js +var require_lazy = __commonJS({ + "chunk/vendor/lazy.js"(exports, module2) { + console.log("lazy init"); + module2.exports = { value: "lazy-value" }; + } +}); + +// chunk/src/index.ts +var src_exports = {}; +__export(src_exports, { + describe: () => describe, + external: () => external, + lazy: () => lazy, + nested: () => nested +}); +module.exports = __toCommonJS(src_exports); +var import_settings = __toESM(require_settings()); +var core = __toESM(require("gtcore")); +var import_gtcore = __toESM(require("gtcore")); +var import_prebuilt = __toESM(require_prebuilt()); +function describe() { + return import_settings.LABEL + "@" + import_settings.default.VERSION + " bump " + (0, import_settings.bump)() + " " + (0, import_settings.bump)(); +} +function external() { + return core.tag + ":" + core.label(1) + ":" + import_gtcore.default.label(2); +} +function nested() { + return (0, import_prebuilt.greet)("chunk") + " " + import_prebuilt.PREBUILT_TAG; +} +function lazy() { + return require_lazy().value; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + describe, + external, + lazy, + nested +}); diff --git a/tests/fixtures/npm/node_modules/gtchunk17/package.json b/tests/fixtures/npm/node_modules/gtchunk17/package.json new file mode 100644 index 000000000..73761f181 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk17/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtchunk17", + "version": "1.0.0", + "description": "the gtchunk source bundled by esbuild 0.17.19: chunk factories take (exports, module2)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.d.ts new file mode 100644 index 000000000..238cf1744 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function outer(): string; +export declare function inner(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.js b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.js new file mode 100644 index 000000000..0f89c7ec5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.js @@ -0,0 +1,52 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +function label(n) { + return "label " + n; +} + +// chunk/vendor/shadow.js +var require_shadow = __commonJS({ + "chunk/vendor/shadow.js"(exports2, module2) { + function nested() { + var label = "nested"; + return label; + } + module2.exports = { outer: label(1), inner: nested() }; + } +}); + +// chunk/src/index.ts +var index_exports = {}; +__export(index_exports, { + inner: () => inner, + outer: () => outer +}); +module.exports = __toCommonJS(index_exports); +function outer() { + return require_shadow().outer; +} +function inner() { + return require_shadow().inner; +} +0 && (module.exports = { + inner, + outer +}); diff --git a/tests/fixtures/npm/node_modules/gtchunkshadow/package.json b/tests/fixtures/npm/node_modules/gtchunkshadow/package.json new file mode 100644 index 000000000..6830fbd2c --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshadow/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtchunkshadow", + "version": "1.0.0", + "description": "esbuild's CommonJS bundle shape: a chunk calls a top-level function whose name a nested scope of the chunk also declares", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.d.ts new file mode 100644 index 000000000..3a47a0495 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function same(): boolean; +export declare function counts(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.js b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.js new file mode 100644 index 000000000..0d3aacec8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.js @@ -0,0 +1,56 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +function tally() { + tally.calls = (tally.calls || 0) + 1; + return tally.calls; +} + +// chunk/vendor/a.js +var require_a = __commonJS({ + "chunk/vendor/a.js"(exports2, module2) { + module2.exports = { tally, first: tally() }; + } +}); + +// chunk/vendor/b.js +var require_b = __commonJS({ + "chunk/vendor/b.js"(exports2, module2) { + module2.exports = { tally, second: tally() }; + } +}); + +// chunk/src/index.ts +var index_exports = {}; +__export(index_exports, { + counts: () => counts, + same: () => same +}); +module.exports = __toCommonJS(index_exports); +function same() { + return require_a().tally === require_b().tally; +} +function counts() { + return require_a().first + " " + require_b().second; +} +0 && (module.exports = { + counts, + same +}); diff --git a/tests/fixtures/npm/node_modules/gtchunkshared/package.json b/tests/fixtures/npm/node_modules/gtchunkshared/package.json new file mode 100644 index 000000000..bfc90c5d2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshared/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtchunkshared", + "version": "1.0.0", + "description": "esbuild's CommonJS bundle shape: two chunks export one top-level function and count calls on a property of it", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtchunkws/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.d.ts new file mode 100644 index 000000000..bd3d4e582 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(): string; +export declare function external(): string; +export declare function nested(): string; +export declare function lazy(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunkws/dist/index.js b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.js new file mode 100644 index 000000000..f022baf09 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.js @@ -0,0 +1 @@ +var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf,__hasOwnProp=Object.prototype.hasOwnProperty;var __commonJS=(cb,mod)=>function(){return mod||(0,cb[__getOwnPropNames(cb)[0]])((mod={exports:{}}).exports,mod),mod.exports};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var require_counter=__commonJS({"chunk/vendor/settings/counter.js"(exports2,module2){var n=0;console.log("counter init");module2.exports={next(){return n+=1,n}}}});var require_settings=__commonJS({"chunk/vendor/settings/index.js"(exports2){"use strict";console.log("settings init");var counter=require_counter();exports2.LABEL="bundled";exports2.VERSION=2;exports2.bump=function(){return counter.next()}}});var require_prebuilt=__commonJS({"chunk/vendor/prebuilt/index.js"(exports2,module2){var __defProp2=Object.defineProperty,__getOwnPropDesc2=Object.getOwnPropertyDescriptor,__getOwnPropNames2=Object.getOwnPropertyNames,__hasOwnProp2=Object.prototype.hasOwnProperty,__export2=(target,all)=>{for(var name in all)__defProp2(target,name,{get:all[name],enumerable:!0})},__copyProps2=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames2(from))!__hasOwnProp2.call(to,key)&&key!==except&&__defProp2(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc2(from,key))||desc.enumerable});return to},__toCommonJS2=mod=>__copyProps2(__defProp2({},"__esModule",{value:!0}),mod),prebuilt_exports={};__export2(prebuilt_exports,{PREBUILT_TAG:()=>PREBUILT_TAG2,greet:()=>greet2});module2.exports=__toCommonJS2(prebuilt_exports);function greet2(name){return"hello "+name}var PREBUILT_TAG2="prebuilt"}});var require_lazy=__commonJS({"chunk/vendor/lazy.js"(exports2,module2){console.log("lazy init");module2.exports={value:"lazy-value"}}});var index_exports={};__export(index_exports,{describe:()=>describe,external:()=>external,lazy:()=>lazy,nested:()=>nested});module.exports=__toCommonJS(index_exports);var import_settings=__toESM(require_settings()),core=__toESM(require("gtcore")),import_gtcore=__toESM(require("gtcore")),import_prebuilt=__toESM(require_prebuilt());function describe(){return import_settings.LABEL+"@"+import_settings.default.VERSION+" bump "+(0,import_settings.bump)()+" "+(0,import_settings.bump)()}function external(){return core.tag+":"+core.label(1)+":"+import_gtcore.default.label(2)}function nested(){return(0,import_prebuilt.greet)("chunk")+" "+import_prebuilt.PREBUILT_TAG}function lazy(){return require_lazy().value}0&&(module.exports={describe,external,lazy,nested}); diff --git a/tests/fixtures/npm/node_modules/gtchunkws/package.json b/tests/fixtures/npm/node_modules/gtchunkws/package.json new file mode 100644 index 000000000..5502f80a0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkws/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtchunkws", + "version": "1.0.0", + "description": "the gtchunk source bundled by esbuild 0.25.10 with --minify-whitespace --minify-syntax: helpers share var statements", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.cjs b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.cjs new file mode 100644 index 000000000..8836b5691 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.cjs @@ -0,0 +1,43 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// cjsdist/src/index.ts +var index_exports = {}; +__export(index_exports, { + viaNodeMode: () => viaNodeMode +}); +module.exports = __toCommonJS(index_exports); +var core = __toESM(require("gtcore"), 1); +var import_gtcore = __toESM(require("gtcore"), 1); +function viaNodeMode() { + return import_gtcore.default.tag + "/" + core.label(7) + "/" + import_gtcore.default.label(8); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + viaNodeMode +}); diff --git a/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.d.cts b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.d.cts new file mode 100644 index 000000000..6f77b9943 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.d.cts @@ -0,0 +1 @@ +export declare function viaNodeMode(): string; diff --git a/tests/fixtures/npm/node_modules/gtcjsdist/package.json b/tests/fixtures/npm/node_modules/gtcjsdist/package.json new file mode 100644 index 000000000..8b416fca1 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcjsdist/package.json @@ -0,0 +1,11 @@ +{ + "name": "gtcjsdist", + "version": "1.0.0", + "description": "a type-module package whose CommonJS entry is an esbuild 0.25.10 .cjs bundle using the node-mode __toESM interop", + "type": "module", + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/append.d.ts b/tests/fixtures/npm/node_modules/gtcycle/dist/append.d.ts new file mode 100644 index 000000000..e33c31f90 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/append.d.ts @@ -0,0 +1 @@ +export declare function appendAll(xs: string[], tail: string): string[]; diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/append.js b/tests/fixtures/npm/node_modules/gtcycle/dist/append.js new file mode 100644 index 000000000..cdfeb6c6c --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/append.js @@ -0,0 +1,30 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var append_exports = {}; +__export(append_exports, { + appendAll: () => appendAll +}); +module.exports = __toCommonJS(append_exports); +var import_index = require("./index"); +function appendAll(xs, tail) { + return xs.some(import_index.isHandler) ? xs : xs.concat([tail]); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + appendAll +}); diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcycle/dist/index.d.ts new file mode 100644 index 000000000..4b86308a9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/index.d.ts @@ -0,0 +1,4 @@ +export { mergeAll } from "./merge"; +export declare function isHandler(x: string): boolean; +export declare function normalize(xs: string[]): string[]; +export declare function run(a: string[], b: string[]): string; diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/index.js b/tests/fixtures/npm/node_modules/gtcycle/dist/index.js new file mode 100644 index 000000000..92f6d2a39 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/index.js @@ -0,0 +1,44 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + isHandler: () => isHandler, + mergeAll: () => import_merge2.mergeAll, + normalize: () => normalize, + run: () => run +}); +module.exports = __toCommonJS(index_exports); +var import_append = require("./append"); +var import_merge = require("./merge"); +var import_merge2 = require("./merge"); +function isHandler(x) { + return x.startsWith("@"); +} +function normalize(xs) { + return xs.map((x) => x.trim()).filter((x) => x.length > 0); +} +function run(a, b) { + return (0, import_append.appendAll)((0, import_merge.mergeAll)(a, b), "@end").join(","); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + isHandler, + mergeAll, + normalize, + run +}); diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/merge.d.ts b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.d.ts new file mode 100644 index 000000000..bbe6b6a4d --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.d.ts @@ -0,0 +1 @@ +export declare function mergeAll(a: string[], b: string[]): string[]; diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/merge.js b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.js new file mode 100644 index 000000000..114e7d8cf --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.js @@ -0,0 +1,30 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var merge_exports = {}; +__export(merge_exports, { + mergeAll: () => mergeAll +}); +module.exports = __toCommonJS(merge_exports); +var import_index = require("./index"); +function mergeAll(a, b) { + return (0, import_index.normalize)(a.concat(b)).filter((x) => !(0, import_index.isHandler)(x) || x === "@keep"); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + mergeAll +}); diff --git a/tests/fixtures/npm/node_modules/gtcycle/package.json b/tests/fixtures/npm/node_modules/gtcycle/package.json new file mode 100644 index 000000000..5b41b8a52 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcycle", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output (no bundling) of three modules that import each other", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.d.ts new file mode 100644 index 000000000..5f4373e2f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.d.ts @@ -0,0 +1,3 @@ +export declare const LIMIT = 3; +export declare function show(): string; +export declare function peek(): number; diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.js new file mode 100644 index 000000000..77aca3c9b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.js @@ -0,0 +1,37 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + peek: () => peek, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function peek() { + return LIMIT; +} +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclealias/dist/reader.js new file mode 100644 index 000000000..943e61a0a --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/dist/reader.js @@ -0,0 +1,34 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const read = import_index.peek; +const captured = read(); +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/package.json b/tests/fixtures/npm/node_modules/gtcyclealias/package.json new file mode 100644 index 000000000..9ee7441e0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclealias", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output shape of a cycle whose partner stores an exported function in a const and calls it while that function's const binding is uninitialized", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.d.ts new file mode 100644 index 000000000..0fa366319 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare const LIMIT = 3; +export declare function show(): string; diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.js new file mode 100644 index 000000000..5bd708a37 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.js @@ -0,0 +1,33 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclebad/dist/reader.js new file mode 100644 index 000000000..a58bdaa51 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/dist/reader.js @@ -0,0 +1,33 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const captured = import_index.LIMIT; +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/package.json b/tests/fixtures/npm/node_modules/gtcyclebad/package.json new file mode 100644 index 000000000..793c97fc9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclebad", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output of a cycle whose partner reads a const binding before its declaration runs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.d.ts new file mode 100644 index 000000000..5f4373e2f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.d.ts @@ -0,0 +1,3 @@ +export declare const LIMIT = 3; +export declare function show(): string; +export declare function peek(): number; diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.js new file mode 100644 index 000000000..77aca3c9b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.js @@ -0,0 +1,37 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + peek: () => peek, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function peek() { + return LIMIT; +} +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclebox/dist/reader.js new file mode 100644 index 000000000..b8f8a927e --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/dist/reader.js @@ -0,0 +1,34 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const box = { read: import_index.peek }; +const captured = box.read(); +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/package.json b/tests/fixtures/npm/node_modules/gtcyclebox/package.json new file mode 100644 index 000000000..67f943195 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclebox", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output shape of a cycle whose partner stores an exported function in an object literal and calls it while that function's const binding is uninitialized", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.d.ts new file mode 100644 index 000000000..5f4373e2f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.d.ts @@ -0,0 +1,3 @@ +export declare const LIMIT = 3; +export declare function show(): string; +export declare function peek(): number; diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.js new file mode 100644 index 000000000..77aca3c9b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.js @@ -0,0 +1,37 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + peek: () => peek, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function peek() { + return LIMIT; +} +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclecall/dist/reader.js new file mode 100644 index 000000000..c98d1ae46 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/dist/reader.js @@ -0,0 +1,33 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const captured = (0, import_index.peek)(); +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/package.json b/tests/fixtures/npm/node_modules/gtcyclecall/package.json new file mode 100644 index 000000000..cc1069a6f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclecall", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output shape of a cycle whose partner calls an exported function through (0, binding.fn)() while that function's const binding is uninitialized", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/data.d.ts b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.d.ts new file mode 100644 index 000000000..356c8bbc9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.d.ts @@ -0,0 +1,2 @@ +import type { Manager } from "./index.js"; +export declare const managers: Manager[]; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/data.js b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.js new file mode 100644 index 000000000..918a296a8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.js @@ -0,0 +1,6 @@ +"use strict"; +exports.managers = [ + { name: "npm", lockfile: "package-lock.json" }, + { name: "pnpm", lockfile: "pnpm-lock.yaml" }, + { name: "yarn", lockfile: "yarn.lock" }, +]; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/detect.js b/tests/fixtures/npm/node_modules/gtsubpath/dist/detect.js new file mode 100644 index 000000000..d129041fe --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/detect.js @@ -0,0 +1,11 @@ +// A hand-rolled __toESM the recognizer cannot verify (see gtdrift). +"use strict"; +var __toESM = (mod) => { + var ns = { default: mod }; + for (var key in mod) ns[key] = mod[key]; + return ns; +}; +var import_gtcore = __toESM(require("gtcore")); +exports.detect = function detect(files) { + return import_gtcore.default.label(files.length); +}; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.d.ts new file mode 100644 index 000000000..24b05d86d --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.d.ts @@ -0,0 +1,6 @@ +export interface Manager { + name: string; + lockfile: string; +} +export declare const managers: Manager[]; +export declare function detect(files: string[]): string; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/index.js b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.js new file mode 100644 index 000000000..a330d28c0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.js @@ -0,0 +1,5 @@ +"use strict"; +const detect = require("./detect.js"); +const data = require("./data.js"); +exports.detect = detect.detect; +exports.managers = data.managers; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/package.json b/tests/fixtures/npm/node_modules/gtsubpath/package.json new file mode 100644 index 000000000..f785cb154 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtsubpath", + "version": "1.0.0", + "description": "a package whose main entry reaches a file with bundler interop the recognizer cannot verify, beside a leaf data module the program imports by subpath", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/harness/__snapshots__/boot-path-node-surface-fences.ts.txt b/tests/harness/__snapshots__/boot-path-node-surface-fences.ts.txt new file mode 100644 index 000000000..c287a1641 --- /dev/null +++ b/tests/harness/__snapshots__/boot-path-node-surface-fences.ts.txt @@ -0,0 +1,35 @@ +boot-path-node-surface-fences.ts:8:14 - error SC2020: 'process.send' is part of the standard library types but has no scriptc lowering yet + + 7 | + 8 | const send = process.send; + | ^~~~~~~~~~~~ + 9 | console.log(typeof send); + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +boot-path-node-surface-fences.ts:11:1 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet + + 10 | + 11 | class Counter extends Writable { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 | _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + + hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here) + +boot-path-node-surface-fences.ts:22:31 - error SC2020: 'util.promisify of this target' is part of the standard library types but has no scriptc lowering yet + + 21 | } + 22 | const legacyAsync = promisify(legacy); + | ^~~~~~ + 23 | legacyAsync().then((v) => console.log(v)); + + hint: child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService, the fs callback functions with an fs/promises counterpart, and typed callback-last functions whose callback takes (error?: Error | null, value?) are the supported targets + +boot-path-node-surface-fences.ts:23:1 - error SC2004: uses of 'legacyAsync' inherit the blocker on its declaration + + 22 | const legacyAsync = promisify(legacy); + 23 | legacyAsync().then((v) => console.log(v)); + | ^~~~~~~~~~~ + 24 | + + hint: the declaration of 'legacyAsync' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/cjs-member-alias-writes/main.ts.txt b/tests/harness/__snapshots__/cjs-member-alias-writes/main.ts.txt new file mode 100644 index 000000000..1682e9bfb --- /dev/null +++ b/tests/harness/__snapshots__/cjs-member-alias-writes/main.ts.txt @@ -0,0 +1,13 @@ +cjs-member-alias-writes/main.ts:5:13 - error SC1090: the reference to 'counter' (a binding form with no lowering) is not supported yet + + 4 | import { counter, version } from "metrics"; + 5 | console.log(counter); + | ^~~~~~~ + 6 | console.log(version); + +cjs-member-alias-writes/main.ts:6:13 - error SC1090: the reference to 'version' (a binding form with no lowering) is not supported yet + + 5 | console.log(counter); + 6 | console.log(version); + | ^~~~~~~ + 7 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/class-field-accessor-descriptor.ts.txt b/tests/harness/__snapshots__/class-field-accessor-descriptor.ts.txt new file mode 100644 index 000000000..986e0d023 --- /dev/null +++ b/tests/harness/__snapshots__/class-field-accessor-descriptor.ts.txt @@ -0,0 +1,6 @@ +class-field-accessor-descriptor.ts:9:1 - error SC2020: Object.defineProperty with an accessor descriptor for the declared field 'x' is not supported yet + + 8 | const value = new Value(); + 9 | Object.defineProperty(value, "x", { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | get() { \ No newline at end of file diff --git a/tests/harness/__snapshots__/local-class-boundaries.ts.txt b/tests/harness/__snapshots__/local-class-boundaries.ts.txt index 04dbc336c..6706a1278 100644 --- a/tests/harness/__snapshots__/local-class-boundaries.ts.txt +++ b/tests/harness/__snapshots__/local-class-boundaries.ts.txt @@ -1,12 +1,12 @@ local-class-boundaries.ts:1:34 - error SC1090: function-local classes with private brands are not supported yet - 1 | function privateBrand() { return class { #value = 1; read(): number { return this.#value; } }; } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | function selfName() { return class Local { copy() { return new Local(); } }; } local-class-boundaries.ts:2:64 - error SC1090: named self references inside function-local classes are not supported yet - 1 | function privateBrand() { return class { #value = 1; read(): number { return this.#value; } }; } + 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } 2 | function selfName() { return class Local { copy() { return new Local(); } }; } | ^~~~~ 3 | class Base {} diff --git a/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt b/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt index 6db3418a8..ea877dec3 100644 --- a/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt +++ b/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt @@ -1,6 +1,6 @@ -local-class-type-query-brand.ts:4:3 - error SC1090: function-local classes with private brands are not supported yet +local-class-type-query-brand.ts:5:3 - error SC1090: function-local classes with private brands are not supported yet - 3 | function make() { - 4 | class Box { + 4 | function make() { + 5 | class Box { | ^~~~~~~~~~~ - 5 | #v = 1; \ No newline at end of file + 6 | #v = 1; \ No newline at end of file diff --git a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt index 58fb23517..159e88f76 100644 --- a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt +++ b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt @@ -55,11 +55,4 @@ maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: numb 31 | const record = { v: 1 }; 32 | console.log(record instanceof Promise); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 33 | - -maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise' values where '() => Promise' is expected is not supported yet - - 35 | type HandlerFn = (...args: unknown[]) => Promise; - 36 | const fixed: HandlerFn = async (first: unknown) => first; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 37 | fixed(1, 2).then((v) => console.log(v)); \ No newline at end of file + 33 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/mock-fences.ts.txt b/tests/harness/__snapshots__/mock-fences.ts.txt index 6b4defcbb..d4b8115e8 100644 --- a/tests/harness/__snapshots__/mock-fences.ts.txt +++ b/tests/harness/__snapshots__/mock-fences.ts.txt @@ -1,62 +1,62 @@ -mock-fences.ts:14:15 - error SC2020: 'mock.fn of this function type' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:18:17 - error SC2020: 'mock.fn() of a function returning 'number'' is part of the standard library types but has no scriptc lowering yet - 13 | - 14 | const empty = mock.fn(); // no original function - | ^~~~~~~~~ - 15 | empty(); + 17 | + 18 | const counted = mock.fn<() => number>(); // no original, returns number + | ^~~~~~~~~~~~~~~~~~~~~~~ + 19 | counted(); - hint: mock functions lower for functions with fixed parameters, or a single rest array parameter, and an implementation of the same type + hint: mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined -mock-fences.ts:27:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:31:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet - 26 | } - 27 | mock.method(new Counter(), "count"); // class instance + 30 | } + 31 | mock.method(new Counter(), "count"); // class instance | ^~~~~~~~~~~~~ - 28 | mock.method(console, "log"); // builtin object + 32 | mock.method(console, "log"); // builtin object hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal -mock-fences.ts:28:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:32:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet - 27 | mock.method(new Counter(), "count"); // class instance - 28 | mock.method(console, "log"); // builtin object + 31 | mock.method(new Counter(), "count"); // class instance + 32 | mock.method(console, "log"); // builtin object | ^~~~~~~ - 29 | mock.method(obj, "value", { times: 1 }); // options + 33 | mock.method(obj, "value", { times: 1 }); // options hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal -mock-fences.ts:29:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:33:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet - 28 | mock.method(console, "log"); // builtin object - 29 | mock.method(obj, "value", { times: 1 }); // options + 32 | mock.method(console, "log"); // builtin object + 33 | mock.method(obj, "value", { times: 1 }); // options | ^~~~~~~~~~~~ - 30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type + 34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type hint: mock options (times, getter and setter) have no lowering -mock-fences.ts:30:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:34:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet - 29 | mock.method(obj, "value", { times: 1 }); // options - 30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type + 33 | mock.method(obj, "value", { times: 1 }); // options + 34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type | ^~~~~~~~~~~ - 31 | other(); + 35 | other(); hint: give the implementation the method's parameter and return types, or discard the result -mock-fences.ts:35:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:39:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet - 34 | t.mock.method(obj, "value"); // lowers - 35 | t.mock.method(obj, "value", { times: 2 }); // options + 38 | t.mock.method(obj, "value"); // lowers + 39 | t.mock.method(obj, "value", { times: 2 }); // options | ^~~~~~~~~~~~ - 36 | }); + 40 | }); hint: mock options (times, getter and setter) have no lowering -mock-fences.ts:38:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:42:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet - 37 | - 38 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined + 41 | + 42 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 39 | pick(true); + 43 | pick(true); hint: a union return type needs an undefined arm here (the calls log records undefined for calls that throw) \ No newline at end of file diff --git a/tests/harness/__snapshots__/object-assign-class-copy.ts.txt b/tests/harness/__snapshots__/object-assign-class-copy.ts.txt new file mode 100644 index 000000000..ce25cc38f --- /dev/null +++ b/tests/harness/__snapshots__/object-assign-class-copy.ts.txt @@ -0,0 +1,6 @@ +object-assign-class-copy.ts:10:32 - error SC1090: Object.assign copying a 'Config' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need) is not supported yet + + 9 | const config = new Config(); + 10 | const copy = Object.assign({}, config); + | ^~~~~~ + 11 | console.log(JSON.stringify(copy), Object.keys(copy).join(",")); \ No newline at end of file diff --git a/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt b/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt new file mode 100644 index 000000000..89bc092aa --- /dev/null +++ b/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt @@ -0,0 +1,24 @@ +object-literal-accessor-values.ts:18:28 - error SC1090: JSON.stringify of '{ get doubled(): number; count: number }' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet + + 17 | // JSON.stringify of the typed value would omit the getter's property. + 18 | console.log(JSON.stringify(counter)); + | ^~~~~~~ + 19 | + +object-literal-accessor-values.ts:21:6 - error SC1101: converting typed values to 'unknown' is not supported yet + + 20 | // Converting the typed value to `unknown` or `any` would lose the getter. + 21 | hold(counter); + | ^~~~~~~ + 22 | const viaAny: any = counter; + + hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet + +object-literal-accessor-values.ts:22:21 - error SC1101: converting typed values to 'unknown' is not supported yet + + 21 | hold(counter); + 22 | const viaAny: any = counter; + | ^~~~~~~ + 23 | console.log(viaAny); + + hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet \ No newline at end of file diff --git a/tests/harness/__snapshots__/thread-slots-fences.ts.txt b/tests/harness/__snapshots__/thread-slots-fences.ts.txt index 566ddfba6..9d4648c27 100644 --- a/tests/harness/__snapshots__/thread-slots-fences.ts.txt +++ b/tests/harness/__snapshots__/thread-slots-fences.ts.txt @@ -59,4 +59,13 @@ thread-slots-fences.ts:28:16 - error SC2020: 'ThreadSlots.get (@scriptc/experime | ^~~~~~~~~ 29 | console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); - hint: call slots.get(...) directly \ No newline at end of file + hint: call slots.get(...) directly + +thread-slots-fences.ts:34:13 - error SC2003: union conversion has no unambiguous layout mapping: expected 'State | undefined', got '{ count: number } | undefined' (each source arm needs an identical destination arm, a unique compatible record/array layout, or a shared literal discriminant selecting compatible layouts) + + 33 | const records: ThreadSlots = new ThreadSlots("fences.records"); + 34 | console.log(records.get(new Key())); + | ^~~~~~~~~~~~~~~~~~~~~~ + 35 | + + hint: narrow the value to a SINGLE arm first (a discriminant check, or '!== undefined'/'!== null' for unit arms); a plain arm value widens into any union that contains it, and a whole union widens into any union whose arms include the source's \ No newline at end of file diff --git a/tests/harness/ci-test-plan.test.ts b/tests/harness/ci-test-plan.test.ts index 3b8c6d4e1..2343407df 100644 --- a/tests/harness/ci-test-plan.test.ts +++ b/tests/harness/ci-test-plan.test.ts @@ -10,6 +10,7 @@ import { fileExclusions, caseShardedSideFiles, isolatedFiles, + leakSuppressions, testCosts, } from "../../scripts/ci-test-plan.mjs"; import { runCiTestPlan } from "../../scripts/ci-test.mjs"; @@ -66,6 +67,20 @@ test.for([1, 3, 5, 10, 20])( }, ); +test("LeakSanitizer runs over the sanitized corpus on Linux only", () => { + const plan = (flavor: "plain" | "san", platform: string) => + ciTestPlan({ shard: "1/20", flavor, workers: 4, platform }); + const linuxSan = plan("san", "linux").corpus.env; + expect(linuxSan.ASAN_OPTIONS).toBe("detect_leaks=1"); + expect(linuxSan.LSAN_OPTIONS).toBe(`suppressions=${leakSuppressions}:print_suppressions=0`); + expect(existsSync(leakSuppressions)).toBe(true); + for (const env of [plan("plain", "linux").corpus.env, plan("san", "darwin").corpus.env]) { + expect(env.ASAN_OPTIONS).toBeUndefined(); + expect(env.LSAN_OPTIONS).toBeUndefined(); + } + for (const task of plan("san", "linux").side) expect(task.env.ASAN_OPTIONS).toBeUndefined(); +}); + test("the side queue balances measured files across the twenty Linux shards", () => { for (const flavor of ["plain", "san"] as const) { const costs = testCosts[flavor] as Record; diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 30899d72b..85e4ba4aa 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1886,6 +1886,7 @@ "5464-locale-compare-fractions.ts": 2833, "5465-dyn-index-read-arrays.ts": 3603, "5466-dyn-index-read-special.ts": 2823, + "5469-typed-array-loop-initializer-rebinding.ts": 1453, "5470-strip-vt-osc.cjs": 2497, "5471-strip-vt-csi.cjs": 2651, "5472-strip-vt-random.cjs": 2375, @@ -2129,6 +2130,9 @@ "5873-readline-promises-namespace.cjs": 2023, "5874-worker-stdio-streams.ts": 2839, "5875-worker-stdout-pipe.cjs": 2740, + "5876-readline-dispose-instanceof.ts": 2465, + "5877-readline-question-query.cjs": 3233, + "5878-readline-dynamic-streams.mjs": 2925, "5879-worker-stdin.ts": 3158, "5880-emitter-leak-warning-delivery.ts": 3418, "5881-inspect-weak-collections.ts": 2615, @@ -2186,9 +2190,6 @@ "5940-callback-helper-specialization.ts": 3109, "5941-callback-specialization-recursion.ts": 3112, "5942-callback-specialization-throws.ts": 3221, - "5942-local-class-observed-methods.ts": 5063, - "5943-fields-read-before-assignment.ts": 6856, - "5944-js-fields-read-before-assignment.js": 4247, "5945-local-class-observed-methods.ts": 5063, "5946-nested-record-field-optional-writes.ts": 6045, "5950-tuple-predicate-methods.ts": 5990, @@ -2230,6 +2231,8 @@ "6001-json-stringify-js-class-instances.js": 4668, "6002-inspect-class-added-properties.ts": 6044, "6003-inspect-js-class-properties.js": 2752, + "6004-class-instance-integrity.ts": 10548, + "6005-class-instance-integrity-sloppy.cjs": 7466, "601-closures-loops.ts": 1788, "6010-match-metadata.ts": 3990, "6011-match-metadata-js.js": 3660, @@ -4980,6 +4983,7 @@ "5464-locale-compare-fractions.ts": 3312, "5465-dyn-index-read-arrays.ts": 3798, "5466-dyn-index-read-special.ts": 4384, + "5469-typed-array-loop-initializer-rebinding.ts": 2058, "5470-strip-vt-osc.cjs": 2708, "5471-strip-vt-csi.cjs": 3038, "5472-strip-vt-random.cjs": 2850, @@ -5223,6 +5227,9 @@ "5873-readline-promises-namespace.cjs": 3811, "5874-worker-stdio-streams.ts": 27801, "5875-worker-stdout-pipe.cjs": 31391, + "5876-readline-dispose-instanceof.ts": 29361, + "5877-readline-question-query.cjs": 170940, + "5878-readline-dynamic-streams.mjs": 25503, "5879-worker-stdin.ts": 33232, "5880-emitter-leak-warning-delivery.ts": 4319, "5881-inspect-weak-collections.ts": 2915, @@ -5280,9 +5287,6 @@ "5940-callback-helper-specialization.ts": 4548, "5941-callback-specialization-recursion.ts": 4448, "5942-callback-specialization-throws.ts": 3443, - "5942-local-class-observed-methods.ts": 6212, - "5943-fields-read-before-assignment.ts": 72293, - "5944-js-fields-read-before-assignment.js": 40310, "5945-local-class-observed-methods.ts": 6212, "5946-nested-record-field-optional-writes.ts": 68117, "5950-tuple-predicate-methods.ts": 7790, @@ -5324,6 +5328,8 @@ "6001-json-stringify-js-class-instances.js": 7469, "6002-inspect-class-added-properties.ts": 9670, "6003-inspect-js-class-properties.js": 4403, + "6004-class-instance-integrity.ts": 16877, + "6005-class-instance-integrity-sloppy.cjs": 11946, "601-closures-loops.ts": 2297, "6010-match-metadata.ts": 5450, "6011-match-metadata-js.js": 4890, diff --git a/tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json b/tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json new file mode 100644 index 000000000..444a9b634 --- /dev/null +++ b/tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json @@ -0,0 +1 @@ +{ "plain": 2849, "san": 4164 } diff --git a/tests/harness/corpus-costs/5775-md5-lengths.ts.json b/tests/harness/corpus-costs/5775-md5-lengths.ts.json new file mode 100644 index 000000000..70f936cec --- /dev/null +++ b/tests/harness/corpus-costs/5775-md5-lengths.ts.json @@ -0,0 +1 @@ +{"plain":2850,"san":3660} diff --git a/tests/harness/corpus-costs/5816-fixed-into-unknown-rest.ts.json b/tests/harness/corpus-costs/5816-fixed-into-unknown-rest.ts.json new file mode 100644 index 000000000..2b149eb6a --- /dev/null +++ b/tests/harness/corpus-costs/5816-fixed-into-unknown-rest.ts.json @@ -0,0 +1 @@ +{ "plain": 3028, "san": 5470 } diff --git a/tests/harness/corpus-costs/5916-dyn-error-stack-property.ts.json b/tests/harness/corpus-costs/5916-dyn-error-stack-property.ts.json new file mode 100644 index 000000000..3ed0acb36 --- /dev/null +++ b/tests/harness/corpus-costs/5916-dyn-error-stack-property.ts.json @@ -0,0 +1 @@ +{ "plain": 3232, "san": 4298 } diff --git a/tests/harness/corpus-costs/5983-runtime-constructor.ts.json b/tests/harness/corpus-costs/5983-runtime-constructor.ts.json new file mode 100644 index 000000000..5506cd9a8 --- /dev/null +++ b/tests/harness/corpus-costs/5983-runtime-constructor.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6900 } diff --git a/tests/harness/corpus-costs/6058-class-view-asserted-bindings.ts.json b/tests/harness/corpus-costs/6058-class-view-asserted-bindings.ts.json new file mode 100644 index 000000000..80e35e544 --- /dev/null +++ b/tests/harness/corpus-costs/6058-class-view-asserted-bindings.ts.json @@ -0,0 +1 @@ +{ "plain": 6139, "san": 10783 } diff --git a/tests/harness/corpus-costs/6061-type-only-class-fields/main.ts.json b/tests/harness/corpus-costs/6061-type-only-class-fields/main.ts.json new file mode 100644 index 000000000..546e4a9e8 --- /dev/null +++ b/tests/harness/corpus-costs/6061-type-only-class-fields/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6644, "san": 11367 } diff --git a/tests/harness/corpus-costs/6063-union-class-field-checks.ts.json b/tests/harness/corpus-costs/6063-union-class-field-checks.ts.json new file mode 100644 index 000000000..bba381595 --- /dev/null +++ b/tests/harness/corpus-costs/6063-union-class-field-checks.ts.json @@ -0,0 +1 @@ +{ "plain": 4152, "san": 6474 } diff --git a/tests/harness/corpus-costs/6342-throw-builtin-identity.ts.json b/tests/harness/corpus-costs/6342-throw-builtin-identity.ts.json new file mode 100644 index 000000000..e2254bc18 --- /dev/null +++ b/tests/harness/corpus-costs/6342-throw-builtin-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3900 } diff --git a/tests/harness/corpus-costs/6345-inspect-dyn-holes.ts.json b/tests/harness/corpus-costs/6345-inspect-dyn-holes.ts.json new file mode 100644 index 000000000..d80769f29 --- /dev/null +++ b/tests/harness/corpus-costs/6345-inspect-dyn-holes.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2300 } diff --git a/tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json b/tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json new file mode 100644 index 000000000..2b1382076 --- /dev/null +++ b/tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2200 } diff --git a/tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json b/tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json new file mode 100644 index 000000000..e053739a0 --- /dev/null +++ b/tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 2000 } diff --git a/tests/harness/corpus-costs/6348-errno-code-own-field.ts.json b/tests/harness/corpus-costs/6348-errno-code-own-field.ts.json new file mode 100644 index 000000000..28b033d9b --- /dev/null +++ b/tests/harness/corpus-costs/6348-errno-code-own-field.ts.json @@ -0,0 +1 @@ +{ "plain": 1200, "san": 1500 } diff --git a/tests/harness/corpus-costs/6356-format-integer-dyn.ts.json b/tests/harness/corpus-costs/6356-format-integer-dyn.ts.json new file mode 100644 index 000000000..8bcdf4a18 --- /dev/null +++ b/tests/harness/corpus-costs/6356-format-integer-dyn.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2000 } diff --git a/tests/harness/corpus-costs/6400-dyn-error-stack-property-js.js.json b/tests/harness/corpus-costs/6400-dyn-error-stack-property-js.js.json new file mode 100644 index 000000000..c51a3475f --- /dev/null +++ b/tests/harness/corpus-costs/6400-dyn-error-stack-property-js.js.json @@ -0,0 +1 @@ +{ "plain": 3172, "san": 4044 } diff --git a/tests/harness/corpus-costs/6401-dyn-error-own-property-names.ts.json b/tests/harness/corpus-costs/6401-dyn-error-own-property-names.ts.json new file mode 100644 index 000000000..f85c4901a --- /dev/null +++ b/tests/harness/corpus-costs/6401-dyn-error-own-property-names.ts.json @@ -0,0 +1 @@ +{ "plain": 3175, "san": 4362 } diff --git a/tests/harness/corpus-costs/6402-dyn-error-own-property-names-js.js.json b/tests/harness/corpus-costs/6402-dyn-error-own-property-names-js.js.json new file mode 100644 index 000000000..55012b442 --- /dev/null +++ b/tests/harness/corpus-costs/6402-dyn-error-own-property-names-js.js.json @@ -0,0 +1 @@ +{ "plain": 3056, "san": 4620 } diff --git a/tests/harness/corpus-costs/6407-dyn-bytes-in.ts.json b/tests/harness/corpus-costs/6407-dyn-bytes-in.ts.json new file mode 100644 index 000000000..4b769b3a1 --- /dev/null +++ b/tests/harness/corpus-costs/6407-dyn-bytes-in.ts.json @@ -0,0 +1 @@ +{ "plain": 3590, "san": 4401 } diff --git a/tests/harness/corpus-costs/6408-dyn-bytes-in-js.js.json b/tests/harness/corpus-costs/6408-dyn-bytes-in-js.js.json new file mode 100644 index 000000000..12ffbeef1 --- /dev/null +++ b/tests/harness/corpus-costs/6408-dyn-bytes-in-js.js.json @@ -0,0 +1 @@ +{ "plain": 3076, "san": 4009 } diff --git a/tests/harness/corpus-costs/6445-rest-function-fixed-slot.ts.json b/tests/harness/corpus-costs/6445-rest-function-fixed-slot.ts.json new file mode 100644 index 000000000..858c662a8 --- /dev/null +++ b/tests/harness/corpus-costs/6445-rest-function-fixed-slot.ts.json @@ -0,0 +1 @@ +{ "plain": 2526, "san": 3450 } diff --git a/tests/harness/corpus-costs/6446-rest-function-nested-declaration.ts.json b/tests/harness/corpus-costs/6446-rest-function-nested-declaration.ts.json new file mode 100644 index 000000000..39f698b22 --- /dev/null +++ b/tests/harness/corpus-costs/6446-rest-function-nested-declaration.ts.json @@ -0,0 +1 @@ +{ "plain": 2222, "san": 3035 } diff --git a/tests/harness/corpus-costs/cjs-class-hierarchy-npm/main.ts.json b/tests/harness/corpus-costs/cjs-class-hierarchy-npm/main.ts.json new file mode 100644 index 000000000..6f2c07c70 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-class-hierarchy-npm/main.ts.json @@ -0,0 +1 @@ +{ "plain": 5788, "san": 8587 } diff --git a/tests/harness/corpus-costs/cjs-open-options-npm/main.ts.json b/tests/harness/corpus-costs/cjs-open-options-npm/main.ts.json new file mode 100644 index 000000000..bc8f1df37 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-open-options-npm/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3636, "san": 4211 } diff --git a/tests/harness/corpus-costs/cjs-strict-arguments.cjs.json b/tests/harness/corpus-costs/cjs-strict-arguments.cjs.json new file mode 100644 index 000000000..82b947fc7 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-strict-arguments.cjs.json @@ -0,0 +1 @@ +{ "plain": 2381, "san": 3148 } diff --git a/tests/harness/corpus-costs/cjs-symbol-key-collisions/main.js.json b/tests/harness/corpus-costs/cjs-symbol-key-collisions/main.js.json new file mode 100644 index 000000000..682d630d5 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-symbol-key-collisions/main.js.json @@ -0,0 +1 @@ +{ "plain": 2258, "san": 3272 } diff --git a/tests/harness/corpus-costs/cjs-symbol-key-table/main.js.json b/tests/harness/corpus-costs/cjs-symbol-key-table/main.js.json new file mode 100644 index 000000000..5bdd1fc52 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-symbol-key-table/main.js.json @@ -0,0 +1 @@ +{ "plain": 3327, "san": 4728 } diff --git a/tests/harness/corpus-costs/cjs-unmapped-arguments.cjs.json b/tests/harness/corpus-costs/cjs-unmapped-arguments.cjs.json new file mode 100644 index 000000000..7a5c1ebd8 --- /dev/null +++ b/tests/harness/corpus-costs/cjs-unmapped-arguments.cjs.json @@ -0,0 +1 @@ +{ "plain": 2377, "san": 3264 } diff --git a/tests/harness/corpus-costs/function-local-detached-methods.js.json b/tests/harness/corpus-costs/function-local-detached-methods.js.json new file mode 100644 index 000000000..f839807c5 --- /dev/null +++ b/tests/harness/corpus-costs/function-local-detached-methods.js.json @@ -0,0 +1 @@ +{ "plain": 3316, "san": 4688 } diff --git a/tests/harness/corpus-costs/function-local-detached-methods.ts.json b/tests/harness/corpus-costs/function-local-detached-methods.ts.json new file mode 100644 index 000000000..19907d4e4 --- /dev/null +++ b/tests/harness/corpus-costs/function-local-detached-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2314, "san": 3117 } diff --git a/tests/harness/corpus-costs/function-local-private-evaluations.js.json b/tests/harness/corpus-costs/function-local-private-evaluations.js.json new file mode 100644 index 000000000..c75974ffd --- /dev/null +++ b/tests/harness/corpus-costs/function-local-private-evaluations.js.json @@ -0,0 +1 @@ +{ "plain": 6264, "san": 10866 } diff --git a/tests/harness/corpus-costs/function-local-private-fields.js.json b/tests/harness/corpus-costs/function-local-private-fields.js.json new file mode 100644 index 000000000..e01fe4c5f --- /dev/null +++ b/tests/harness/corpus-costs/function-local-private-fields.js.json @@ -0,0 +1 @@ +{ "plain": 2964, "san": 5827 } diff --git a/tests/harness/corpus-costs/private-in-checked-primitives.js.json b/tests/harness/corpus-costs/private-in-checked-primitives.js.json new file mode 100644 index 000000000..ce2fa848e --- /dev/null +++ b/tests/harness/corpus-costs/private-in-checked-primitives.js.json @@ -0,0 +1 @@ +{ "plain": 2316, "san": 2225 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 26dfd3a5c..1f3d9729b 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1150,6 +1150,244 @@ export function plain(x) { return x + 1; } /^Uncaught Error: calling a module export that reads 'this' through its module binding \('who': .*\) is not supported yet \[SC1090 at .*namespace-member-this\.mjs:3\]\n$/, ); }); + + test("a namespace import's export that passes this on with apply or call compiles", async () => { + // Effect 4's provideService: `dual(...).apply(this, arguments)` hands the + // receiver to a function that `dual` returns, which reads it only to + // pass it on to an arrow, as here. + const r = await compileAndRun( + "namespace-member-forwards-this", + `import * as ns from './lib.mjs'; +export const added = ns.provide(1, 2, 3); +console.log(added, ns.sum(3, 4)); +`, + "mjs", + { + "lib.mjs": `const dual = function (arity, body) { + switch (arity) { + case 2: + return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); }; + default: + return function () { return body.apply(this, arguments); }; + } +}; +export const provide = function () { return dual(3, (a, b, c) => a + b + c).apply(this, arguments); }; +const twice = function (a, b) { return (a + b) * 2; }; +export const sum = function (a, b) { return twice.call(this, a, b); }; +`, + }, + ); + expect(r).toEqual({ stdout: "6 14\n", stderr: "", exitCode: 0 }); + }); + + test("a require binding's export that passes this on with apply compiles", async () => { + const r = await compileAndRun( + "require-member-forwards-this", + `'use strict'; +const m = require('./lib.cjs'); +console.log(m.scale(5)); +`, + "cjs", + { + "lib.cjs": `'use strict'; +const triple = function (x) { return x * 3; }; +exports.scale = function () { return triple.apply(this, arguments); }; +`, + }, + ); + expect(r).toEqual({ stdout: "15\n", stderr: "", exitCode: 0 }); + }); + + test.each([ + [ + "a namespace import's export that passes this on to a function that reads it", + "namespace-member-forwards-to-reader", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaCall('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +function read(suffix) { return this.tag + suffix; } +export function viaCall(suffix) { return read.call(this, suffix); } +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaCall", + 3, + ], + [ + "a namespace import's export that passes this on through a factory to a function that reads it", + "namespace-member-forwards-through-factory", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaDual('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const dual = function (arity, body) { + switch (arity) { + case 2: + return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); }; + default: + return function () { return body.apply(this, arguments); }; + } +}; +export const viaDual = function () { return dual(9, function (s) { return this.tag + s; }).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaDual", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that reassigns its parameter", + "namespace-member-forwards-reassigned", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + body = function (s) { return this.tag + s; }; + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that replaces its parameter as a destructuring", + "namespace-member-forwards-destructured", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + [body] = [function (s) { return this.tag + s; }]; + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that replaces its parameter as a object destructuring", + "namespace-member-forwards-object-destructured", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + ({ body } = { body: function (s) { return this.tag + s; } }); + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that replaces its parameter as a for-of target", + "namespace-member-forwards-for-of", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + for (body of [function (s) { return this.tag + s; }]); + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a require binding's export that passes this on to a function that reads it", + "require-member-forwards-to-reader", + "cjs", + `'use strict'; +const m = require('./lib.cjs'); +console.log(m.plain(21)); +console.log(m.viaApply('!')); +`, + { + "lib.cjs": `'use strict'; +exports.name = 'lib'; +const read = function (suffix) { return this.name + suffix; }; +exports.viaApply = function () { return read.apply(this, arguments); }; +exports.plain = function (x) { return x * 2; }; +`, + }, + "42\n", + "viaApply", + 4, + ], + ] as const)( + "%s is a named deferred fence", + async (_, name, ext, source, files, stdout, member, line) => { + // Node passes the module object on to `read`, which reads it; the + // direct call has none to pass. + const r = await compileAndRun(name, source, ext, files); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(stdout); + expect(r.stderr).toMatch( + new RegExp( + `^Uncaught Error: calling a module export that reads 'this' through its module binding \\('${member}': .*\\) is not supported yet \\[SC1090 at .*${name}\\.${ext}:${line}\\]\\n$`, + ), + ); + }, + ); + + test("an export that also reads this beside passing it on is a named deferred fence", async () => { + const r = await compileAndRun( + "namespace-member-forwards-and-reads-this", + `import * as ns from './lib.mjs'; +console.log(ns.tag('x')); +`, + "mjs", + { + "lib.mjs": `const join = function (a, b) { return a + ':' + b; }; +export function tag(x) { return join.call(this, x, typeof this); } +`, + }, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(""); + expect(r.stderr).toMatch( + /^Uncaught Error: calling a module export that reads 'this' through its module binding \('tag': .*\) is not supported yet \[SC1090 at .*namespace-member-forwards-and-reads-this\.mjs:2\]\n$/, + ); + }); }); // CommonJS forms around the module's exports that keep a refusal name the @@ -1174,7 +1412,7 @@ console.log(keys(exports)); [ "exports-rebinding", `console.log("before"); -exports = module.exports = function hash(x) { +exports = function hash(x) { return x; }; exports.extra = 1; @@ -1182,7 +1420,7 @@ exports.extra = 1; {}, "before\n", 2, - "rebinding 'exports' \\(exports = module\\.exports = value\\)", + "rebinding 'exports' without module\\.exports \\(exports = value\\)", ], [ "require-computed", diff --git a/tests/harness/esm-path-globals.test.ts b/tests/harness/esm-path-globals.test.ts new file mode 100644 index 000000000..546c924a9 --- /dev/null +++ b/tests/harness/esm-path-globals.test.ts @@ -0,0 +1,38 @@ +/* __dirname / __filename in TypeScript files that Node's strip-types rule + * loads as ES modules: scriptc compiles them to the module's source path, + * the CommonJS-bundle reading. Node itself has no such globals in an ES + * module, so the oracle runs a twin of each source with the globals spelled + * import.meta.dirname / import.meta.filename, at the same paths. */ +import { execFile } from "node:child_process"; +import { cpSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const fixture = join(import.meta.dirname, "../fixtures/esm-path-globals"); + +test("__dirname and __filename in TypeScript ES modules read the module's source path", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-esm-path-globals-")); + cpSync(fixture, join(dir, "src"), { recursive: true }); + const entry = join(dir, "src", "main.ts"); + const result = await compile(entry, { + outPath: join(dir, "main"), + outDir: dir, + sanitize: process.env["SCRIPTC_SAN"] === "1", + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const native = await execFileAsync(result.binaryPath); + for (const file of [entry, join(dir, "src", "lib", "helper.ts")]) { + const text = readFileSync(file, "utf8") + .replaceAll("__dirname", "import.meta.dirname") + .replaceAll("__filename", "import.meta.filename"); + writeFileSync(file, text); + } + const node = await execFileAsync(process.execPath, [entry]); + expect(native.stdout).toBe(node.stdout); + expect(native.stdout).toContain("string string\nsrc main.ts\ntrue true\ntrue helper.ts\n"); +}); diff --git a/tests/harness/lsan.supp b/tests/harness/lsan.supp new file mode 100644 index 000000000..41125589f --- /dev/null +++ b/tests/harness/lsan.supp @@ -0,0 +1,4 @@ +# LeakSanitizer suppressions for the sanitized corpus on Linux CI +# (scripts/ci-test-plan.mjs). One `leak:` line per intentional +# leak, under a comment that gives the reason. Empty: no corpus program +# leaks. diff --git a/tests/harness/npm-cases.ts b/tests/harness/npm-cases.ts index 4dda0b9ad..0830d4162 100644 --- a/tests/harness/npm-cases.ts +++ b/tests/harness/npm-cases.ts @@ -19,12 +19,12 @@ export function npmCases(fixturesRoot: string): NpmCase[] { return [ ...globSync(join(fixturesRoot, "npm/cases/*/main.ts")) .sort() - // 2465-2469 and 2556-2557 are the --npm-static bundler-emitted-CJS + // 2465-2469, 2556-2566 and 2571 are the --npm-static bundler-emitted-CJS // cases (npm-static.test.ts drives them with the opt-in): their // consumers exercise the static rewrite's surface (lexer-visible // names the shipped .d.ts never declares, the __toESM interop // family), so they stay out of the flagless island lane by design. - .filter((entry) => !/\/(246[5-9]|255[67])-[^/]+\/main\.ts$/.test(entry)) + .filter((entry) => !/\/(246[5-9]|255[6-9]|256[0-24-6]|2571)-[^/]+\/main\.ts$/.test(entry)) .map((entry) => ({ name: entry.split("/").at(-2)!, entry, diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 356d2fa6d..8325e2998 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -76,8 +76,8 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise ...globSync( join(fixturesRoot, "npm/node_modules/cryptozoo/**/*.{js,mjs,cjs,json,d.ts}"), ).sort(), - // the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2557) - ...globSync(join(fixturesRoot, "npm/node_modules/gt*/**/*.{js,json}")).sort(), + // the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2566, 2571) + ...globSync(join(fixturesRoot, "npm/node_modules/gt*/**/*.{js,cjs,json}")).sort(), ]; for (const f of inputs) hash.update(f).update(readFileSync(f)); const key = hash @@ -1389,6 +1389,138 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 120_000); + /* ── 2558-2561: esbuild's bundled CommonJS chunks and per-file CommonJS + * cycles ─ every __commonJS chunk compiles as its own CommonJS module + * (evaluated once, at its first require); a require cycle between + * esbuild outputs whose live getter tables install before their first + * require initializes exactly like Node, and a cycle that reads a + * binding in its temporal dead zone stays refused. */ + async function expectNodeParity(entry: string, packages: string[]): Promise { + const { coverage } = analyze(entry, { npmStatic: packages }); + expect(coverage.npmStatic).toEqual(packages.map((pkg) => ({ package: pkg, status: "static" }))); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, packages); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + // Node's stderr carries its type-stripping warning; the programs write + // only to stdout. + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(comparableStderr(nativeRes.stderr).toString("utf8")).toBe(""); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + expect(nativeRes.exitCode).toBe(0); + } + + // 2558: esbuild 0.25 (plain and whitespace/syntax minified) and 0.17 + // CommonJS bundles of one source, plus a .cjs bundle in a type-module + // package with the node-mode interop. + test("bundled CommonJS chunks 2558-bundled-chunks compile statically and byte-match Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2558-bundled-chunks/main.ts"), [ + "gtchunk", + "gtchunkws", + "gtchunk17", + "gtcjsdist", + "gtcore", + ]); + }, 180_000); + + // A whitespace/syntax-minified bundle whose helpers keep their names is + // judged by the recognizers, not refused up front as minified. + test("--npm-static=auto opts a minified bundle with named helpers in", () => { + const entry = join(fixturesRoot, "npm/cases/2558-bundled-chunks/main.ts"); + const { coverage } = analyze(entry, { npmStatic: "auto" }); + expect(coverage.npmStatic).toContainEqual({ package: "gtchunkws", status: "static" }); + }, 120_000); + + test.each(["2559-cjs-cycle", "2560-cjs-cycle-partner-first"])( + "the esbuild CommonJS cycle %s compiles statically and byte-matches Node", + async (name) => { + await expectNodeParity(join(fixturesRoot, `npm/cases/${name}/main.ts`), ["gtcycle"]); + }, + 180_000, + ); + + // 2561: Node throws a ReferenceError there; the cycle keeps SC1016. + test("a CommonJS cycle that reads a binding before its declaration stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2561-cjs-cycle-early-read/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcyclebad"] }); + expect(coverage.npmStatic).toEqual([ + { + package: "gtcyclebad", + status: "refused", + detail: expect.stringMatching( + /circular imports .*can use its binding 'import_index' of a module of the cycle/, + ) as string, + }, + ]); + }, 120_000); + + // 2562: a file with unverifiable interop that no compiled code can load + // (the program imports a leaf data module and only a type from the + // main entry) leaves its package static. + test("a package is judged by the files its imported subpaths can load", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2562-subpath-leaf/main.ts"), [ + "gtsubpath", + ]); + }, 180_000); + + // 2566: the same early read through esbuild's `(0, binding.fn)()` call + // spelling also keeps SC1016. + test("a CommonJS cycle that calls an export during initialization stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2566-cjs-cycle-indirect-call/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcyclecall"] }); + expect(coverage.npmStatic).toEqual([ + { + package: "gtcyclecall", + status: "refused", + detail: expect.stringMatching( + /circular imports .*can use its binding 'import_index' of a module of the cycle/, + ) as string, + }, + ]); + }, 120_000); + + // 2571: the partner stores the export (in a const, or in an object + // literal) and calls it while the cycle initializes; both keep SC1016. + test("a CommonJS cycle that stores an export and calls it stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2571-cjs-cycle-stored-function/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcyclealias", "gtcyclebox"] }); + for (const pkg of ["gtcyclealias", "gtcyclebox"]) + expect(coverage.npmStatic).toContainEqual({ + package: pkg, + status: "refused", + detail: expect.stringMatching( + /circular imports .*can use its binding 'import_index' of a module of the cycle/, + ) as string, + }); + }, 120_000); + + // 2564: a chunk reads a top-level function whose name a nested scope of + // the chunk also declares; the function still travels with the chunk. + test("a chunk carries a top-level function its nested scopes shadow", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2564-chunk-helper-shadowing/main.ts"), [ + "gtchunkshadow", + ]); + }, 180_000); + + // 2565: Node shares one function between the chunks that export it; a + // per-chunk copy would not, so the package stays refused. + test("chunks that export a shared top-level function stay refused", () => { + const entry = join(fixturesRoot, "npm/cases/2565-chunk-shared-function/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtchunkshared"] }); + expect(coverage.npmStatic).toEqual([ + { + package: "gtchunkshared", + status: "refused", + detail: expect.stringMatching( + /uses the bundle's top-level function 'tally' as a value, which its chunks cannot share/, + ) as string, + }, + ]); + }, 120_000); + test("published type-only declarations coexist with a static runtime bundle", async () => { const entry = join(fixturesRoot, "npm/cases/2469-bundle-offender/main.ts"); const { coverage } = analyze(entry, { npmStatic: ["gtghost"] }); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 802fbd3e1..14e4c27af 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -200,6 +200,27 @@ test("node-types: socket.address and resetAndDestroy under @types/node", async ( expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: TLSSocket.isSessionReused and keyed ??= timings under @types/node", async () => { + const outDir = outDirFor("tls-session-timings"); + const entry = join(nodeTypesDir, "tls-session-timings.ts"); + const result = await compile(entry, { + outPath: join( + outDir, + process.platform === "win32" ? "tls-session-timings.exe" : "tls-session-timings", + ), + 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: net.SocketAddress and net.BlockList under @types/node", async () => { const outDir = outDirFor("net-blocklist"); const entry = join(nodeTypesDir, "net-blocklist.ts"); @@ -404,6 +425,42 @@ test("node-types: promisified fs callback functions lower as their fs/promises c expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: util.promisify over typed callback-last functions settles like Node", async () => { + const outDir = outDirFor("promisify-callback-functions"); + const entry = join(nodeTypesDir, "promisify-callback-functions.ts"); + const result = await compile(entry, { + outPath: join(outDir, "promisify-callback-functions"), + 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: stream subclasses extend the @types/node stream classes", async () => { + const outDir = outDirFor("stream-subclass-hooks"); + const entry = join(nodeTypesDir, "stream-subclass-hooks.ts"); + const result = await compile(entry, { + outPath: join(outDir, "stream-subclass-hooks"), + 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: promisified setTimeout and setImmediate lower as timers/promises", async () => { const outDir = outDirFor("promisify-timers"); const entry = join(nodeTypesDir, "promisify-timers.ts"); diff --git a/tests/harness/runtime-tree-shaking.test.ts b/tests/harness/runtime-tree-shaking.test.ts index 7d85a48c3..b33a0d2f9 100644 --- a/tests/harness/runtime-tree-shaking.test.ts +++ b/tests/harness/runtime-tree-shaking.test.ts @@ -172,6 +172,33 @@ nativeToolchainTest( }, ); +nativeToolchainTest( + "the event loop links the child-process unit only for programs that spawn", + async () => { + const timer = await build( + "async-timer", + `await new Promise((resolve) => setTimeout(resolve, 1)); +console.log("timer"); +`, + ); + await expectNodeParity(timer.sourcePath, timer.binaryPath); + const timerSymbols = await symbols(timer.binaryPath); + for (const family of ["scr_children_", "scr_child_", "scr_ipc_"]) { + expect(timerSymbols, `the timer program retains ${family}`).not.toContain(family); + } + + const spawned = await build( + "async-spawn", + `import { spawn } from "node:child_process"; +const child = spawn("/bin/echo", ["child"], { stdio: "ignore" }); +child.on("close", (code) => console.log("close", code)); +`, + ); + await expectNodeParity(spawned.sourcePath, spawned.binaryPath); + expect(await symbols(spawned.binaryPath)).toContain("scr_children_poll"); + }, +); + nativeToolchainTest("fetch response JSON retains the URL and parser runtime", async () => { const server = createServer((_request, response) => { response.setHeader("content-type", "application/json");