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 #