mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -8,7 +8,9 @@ node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
!tests/fixtures/npm/cases/*/node_modules/
|
||||
!tests/library-mode/*/node_modules/
|
||||
@@ -32,6 +34,7 @@ output/
|
||||
!tests/fixtures/npm/cases/workspace-copied/members/*/dist/
|
||||
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
|
||||
!tests/fixtures/provenance/node_modules/*/dist/
|
||||
!tests/fixtures/npm-static/node_modules/*/dist/
|
||||
.scriptc/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"packages/compiler/src/backend/llvm/**",
|
||||
"packages/compiler/src/backend/loop-polls.test.ts",
|
||||
"packages/compiler/src/backend/may-throw.test.ts",
|
||||
"packages/compiler/src/backend/stack-observations.test.ts",
|
||||
"packages/compiler/src/ir/**",
|
||||
"packages/compiler/src/library/*.test.ts",
|
||||
"packages/compiler/test/**",
|
||||
|
||||
@@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
|
||||
|
||||
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
|
||||
|
||||
Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/<program>.json`, not in `tests/harness/corpus-costs.json`; see the README there.
|
||||
|
||||
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
|
||||
|
||||
A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=<regex>` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared:
|
||||
@@ -140,7 +142,6 @@ Never hand-edit these files:
|
||||
- `internal/compatibility/generated/node-v24-internal.json`
|
||||
- `internal/compatibility/generated/node-v24-backlog.json`
|
||||
- `docs/src/generated/node-v24-compatibility.json`
|
||||
- `docs/src/generated/node-v24-compatibility-meta.json`
|
||||
|
||||
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
@@ -127,6 +127,26 @@
|
||||
"100000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "thread-slots",
|
||||
"entry": "workloads/thread-slots.ts",
|
||||
"args": [
|
||||
"slots",
|
||||
"8",
|
||||
"50000",
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "thread-slots-cache",
|
||||
"entry": "workloads/thread-slots.ts",
|
||||
"args": [
|
||||
"cache",
|
||||
"8",
|
||||
"50000",
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atomics-contended",
|
||||
"entry": "workloads/atomics-contended.ts",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Per-thread state for shared objects: worker threads read the same
|
||||
// published type graph from an InternTable and keep per-thread state for
|
||||
// each type they visit (a resolved flag and a visit count), then walk the
|
||||
// graph many times. Arguments: the store ("slots" for a ThreadSlots family,
|
||||
// "cache" for a direct-mapped cache by id in front of a Map by id, the
|
||||
// layout programs use without slots), the thread count, the type count and
|
||||
// the number of walks. Under Node.js both stores are per-thread.
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
import { InternTable, ThreadSlots } from "@scriptc/experimental";
|
||||
|
||||
class Type {
|
||||
readonly id: number;
|
||||
readonly members: readonly Type[];
|
||||
constructor(id: number, members: readonly Type[]) {
|
||||
this.id = id;
|
||||
this.members = members;
|
||||
}
|
||||
}
|
||||
|
||||
class TypeState {
|
||||
resolved = false;
|
||||
visits = 0;
|
||||
}
|
||||
|
||||
const types = new InternTable<number, Type>("bench.slot-types");
|
||||
const states = new ThreadSlots<Type, TypeState>("bench.type-states");
|
||||
|
||||
const CACHE = 1024;
|
||||
const cacheIds: number[] = new Array<number>(CACHE).fill(-1);
|
||||
const cacheStates: (TypeState | undefined)[] = new Array<TypeState | undefined>(CACHE).fill(
|
||||
undefined,
|
||||
);
|
||||
const byId = new Map<number, TypeState>();
|
||||
|
||||
function slotState(t: Type): TypeState {
|
||||
let state = states.get(t);
|
||||
if (state === undefined) {
|
||||
state = new TypeState();
|
||||
states.set(t, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function cachedState(t: Type): TypeState {
|
||||
const slot = t.id & (CACHE - 1);
|
||||
if (cacheIds[slot] === t.id) return cacheStates[slot]!;
|
||||
let state = byId.get(t.id);
|
||||
if (state === undefined) {
|
||||
state = new TypeState();
|
||||
byId.set(t.id, state);
|
||||
}
|
||||
cacheIds[slot] = t.id;
|
||||
cacheStates[slot] = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
function typeOf(id: number): Type {
|
||||
return types.getOrInsertComputed(id, () => {
|
||||
const members: Type[] = [];
|
||||
if (id >= 3) members.push(typeOf(Math.floor(id / 3)));
|
||||
if (id >= 7) members.push(typeOf(id - 7));
|
||||
return new Type(id, members);
|
||||
});
|
||||
}
|
||||
|
||||
function run(useSlots: boolean, count: number, walks: number): string {
|
||||
const all: Type[] = [];
|
||||
for (let id = 0; id < count; id++) all.push(typeOf(id));
|
||||
// Visits follow member edges from a pseudo-random start, as a checker's
|
||||
// relation checks do: neighbours are often visited together.
|
||||
let seed = 1;
|
||||
let resolved = 0;
|
||||
let visits = 0;
|
||||
for (let w = 0; w < walks; w++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
seed = (seed * 48271) % 2147483647;
|
||||
let t = all[seed % count]!;
|
||||
for (let depth = 0; depth < 4; depth++) {
|
||||
const state = useSlots ? slotState(t) : cachedState(t);
|
||||
if (!state.resolved) {
|
||||
state.resolved = true;
|
||||
resolved++;
|
||||
}
|
||||
state.visits++;
|
||||
visits++;
|
||||
if (t.members.length === 0) break;
|
||||
t = t.members[depth % t.members.length]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${resolved} resolved, ${visits} visits`;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const store = process.argv[2] ?? "slots";
|
||||
const threads = Number(process.argv[3] ?? "8");
|
||||
const count = Number(process.argv[4] ?? "50000");
|
||||
const walks = Number(process.argv[5] ?? "20");
|
||||
const results: string[] = [];
|
||||
let pending = threads;
|
||||
for (let i = 0; i < threads; i++) {
|
||||
const worker = new Worker(new URL(import.meta.url), {
|
||||
workerData: { useSlots: store === "slots", count, walks },
|
||||
});
|
||||
worker.on("message", (text: string) => {
|
||||
results[i] = text;
|
||||
});
|
||||
worker.on("exit", () => {
|
||||
if (--pending === 0) {
|
||||
const same = results.every((r) => r === results[0]);
|
||||
console.log(`${threads} threads: ${results[0]}, same in every thread ${same}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { useSlots, count, walks } = workerData as {
|
||||
useSlots: boolean;
|
||||
count: number;
|
||||
walks: number;
|
||||
};
|
||||
parentPort!.postMessage(run(useSlots, count, walks));
|
||||
}
|
||||
@@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dd>On Linux, when the program calls <code>process.exit()</code>, hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing <code>...</code>, as in <code>fs-extra...</code>, also names every package that package depends on. <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
|
||||
<dt><code>--provenance-sources</code></dt>
|
||||
<dd>Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.</dd>
|
||||
@@ -133,6 +133,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--wasm <file.wasm></code></dt>
|
||||
<dd>Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ Your callback and the commander implementation both compile to native code. No e
|
||||
|
||||
scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources.
|
||||
|
||||
Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js.
|
||||
|
||||
## Which packages compile
|
||||
|
||||
scriptc compiles a package automatically when it:
|
||||
@@ -53,7 +55,7 @@ scriptc compiles a package automatically when it:
|
||||
- ships its own type declarations, and
|
||||
- ships readable, unminified JavaScript.
|
||||
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
|
||||
|
||||
`graceful-fs` is the exception: it compiles as `node:fs` instead of from its JavaScript, with one [documented difference](/docs/limitations#graceful-fs).
|
||||
|
||||
@@ -69,7 +71,7 @@ Some packages rely on separately installed declarations, such as an `@types/` pa
|
||||
$ scriptc build app.ts --npm-static=three -o app >/dev/null
|
||||
```
|
||||
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
|
||||
## CommonJS packages
|
||||
|
||||
|
||||
@@ -238,8 +238,99 @@ No lock is held while a callback runs. Two threads that miss the same key at the
|
||||
2. It does not compare identities or ids across threads.
|
||||
3. A thread does not depend on another thread's inserts: `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js.
|
||||
|
||||
State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.
|
||||
State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a [`ThreadSlots`](#threadslots) family, a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.
|
||||
|
||||
### Memory
|
||||
|
||||
Lookups take no lock and write nothing. Inserts lock one of 64 segments of the table for a probe and a few stores. Stored values are immortal in scriptc builds: they stay allocated until the process exits, including a value a callback computed for a key that another thread stored first. Programs that never construct an `InternTable` do not run any of its code.
|
||||
|
||||
## ThreadSlots
|
||||
|
||||
Threads that share values through an `InternTable` often compute more about each shared value later: resolved members, cached results, flags. That state differs per thread, so it cannot live in the shared value, which is immutable. A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares.
|
||||
|
||||
```ts:slots.ts
|
||||
import { Worker, isMainThread, parentPort, threadId } from "node:worker_threads";
|
||||
import { InternTable, ThreadSlots } from "@scriptc/experimental";
|
||||
|
||||
class Shape {
|
||||
readonly name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class ShapeState {
|
||||
uses = 0;
|
||||
readonly thread: number;
|
||||
constructor(thread: number) {
|
||||
this.thread = thread;
|
||||
}
|
||||
}
|
||||
|
||||
const shapes = new InternTable<string, Shape>("example.slot-shapes");
|
||||
// Module scope: every thread that evaluates this line opens the same family.
|
||||
const states = new ThreadSlots<Shape, ShapeState>("example.shape-states");
|
||||
|
||||
function use(name: string): ShapeState {
|
||||
const shape = shapes.getOrInsertComputed(name, () => new Shape(name));
|
||||
let state = states.get(shape);
|
||||
if (state === undefined) {
|
||||
state = new ShapeState(threadId);
|
||||
states.set(shape, state);
|
||||
}
|
||||
state.uses++;
|
||||
return state;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text));
|
||||
}
|
||||
} else {
|
||||
use("square");
|
||||
const state = use("square");
|
||||
parentPort!.postMessage(`worker: ${state.uses} uses, own state ${state.thread === threadId}`);
|
||||
}
|
||||
```
|
||||
|
||||
In the scriptc executable both workers read the same `Shape` object from the table, and each keeps its own `ShapeState` for it. Both runtimes print the same:
|
||||
|
||||
```console
|
||||
$ node slots.ts
|
||||
worker: 2 uses, own state true
|
||||
worker: 2 uses, own state true
|
||||
$ scriptc build slots.ts -o slots >/dev/null
|
||||
$ ./slots
|
||||
worker: 2 uses, own state true
|
||||
worker: 2 uses, own state true
|
||||
```
|
||||
|
||||
### Slot methods
|
||||
|
||||
<dl>
|
||||
<dt><code>new ThreadSlots<K, V>(name)</code></dt>
|
||||
<dd>Opens the family called <code>name</code>. Within one thread every construction with the same name refers to the same family.</dd>
|
||||
<dt><code>slots.get(key)</code>, <code>slots.has(key)</code></dt>
|
||||
<dd>Read the calling thread's value for <code>key</code>.</dd>
|
||||
<dt><code>slots.set(key, value)</code></dt>
|
||||
<dd>Sets the calling thread's value for <code>key</code> and returns the slots. It never writes the key, so it works on published keys.</dd>
|
||||
<dt><code>slots.delete(key)</code></dt>
|
||||
<dd>Removes the calling thread's value for <code>key</code> and returns whether there was one.</dd>
|
||||
</dl>
|
||||
|
||||
Under Node.js each family is a `WeakMap` per thread.
|
||||
|
||||
### Keys and values
|
||||
|
||||
scriptc requires:
|
||||
|
||||
- the name to be a string literal, and every construction of one name to use the same type arguments;
|
||||
- the key type to be a class declared in the program; keys may be instances of its subclasses;
|
||||
- the value type to be a class, an object type, an array, a `Map` or a `Set`;
|
||||
- the methods to be called through a module-level `const` initialized with `new ThreadSlots<K, V>("name")`, as in the example. A family's storage is found by its name when the program is compiled.
|
||||
|
||||
### Slot cost
|
||||
|
||||
Each instance of a key class, and of every class in its hierarchy, carries a 4-byte slot index, 0 until a value is set for it. The first `set` for a key assigns the index; indices are never reused. `get` and `has` read the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family stores values in pages of 1024 entries, allocated as the thread sets values in them.
|
||||
|
||||
A family holds a reference to each of the thread's values. A value is released when it is replaced or deleted, when its key is freed, and when the thread exits. Keys that are freed with a [`LifetimeRegion`](#lifetimeregion) keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. Programs that never construct a `ThreadSlots` do not run any of its code, and their classes have no slot index.
|
||||
|
||||
@@ -64,6 +64,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/eventemitter.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/asynclocalstorage.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/streams-and-readline.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/commonjs-metadata.mdx</include>
|
||||
@@ -76,6 +78,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/legacy-url-api.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/deprecated-modules.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/copy.mdx</include>
|
||||
@@ -120,6 +124,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/synchronous-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/iterator-helpers.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/regular-expressions.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/object-freezing.mdx</include>
|
||||
@@ -242,6 +248,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/native-addons.mdx</include>
|
||||
|
||||
## WebAssembly modules
|
||||
|
||||
<include>../limitations/webassembly-modules.mdx</include>
|
||||
|
||||
## WASI target
|
||||
|
||||
<include>../limitations/wasi-target.mdx</include>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started.
|
||||
@@ -1 +1 @@
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
|
||||
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **Deprecated `punycode` and `constants` modules:** `punycode.encode()`, `decode()`, `toASCII()`, `toUnicode()`, `ucs2.decode()`, `ucs2.encode()`, and `version` compile, with the `DEP0040` warning Node.js reports when the module loads. A `require("punycode")` made from inside `node_modules` doesn't warn; a `require()` used inside a larger expression, such as `require("punycode").toASCII(name)`, doesn't warn either. The `punycode.ucs2` object itself is unsupported as a value. The `constants` module is the frozen object Node.js builds from `os.constants` and `fs.constants`, plus the `crypto.constants` of Node.js 24.22.0.
|
||||
@@ -0,0 +1 @@
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped generators and array, string, Map and Set iterators answer the same methods, and array destructuring of untyped values steps generators, iterators, Maps and Sets the way Node does. Helper methods on untyped async generators are unsupported. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported.
|
||||
@@ -1 +1 @@
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity. Map values may also be functions. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
- **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is.
|
||||
- **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`.
|
||||
- **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named `<root>`. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged.
|
||||
- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started.
|
||||
- **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it.
|
||||
- **Test names:** a test without a name, or with an empty one, takes its function's name or `<anonymous>`, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function. A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name. `test(fn)` and `test("", fn)` with such a function throw `SC2020`.
|
||||
- **Current context:** `getTestContext()` returns the context of the test, suite or hook whose code is running, as in Node: `undefined` at module top level, the root's context in module-level hooks, the suite's in a suite body and in the suite's hooks, and the test's in its body, in the code it awaits, and in the timers, immediates, `process.nextTick` and `queueMicrotask` callbacks it schedules. Node returns, for a suite, a `TestContext` that is a different object from the `SuiteContext` passed to the suite body and its `before` and `after` hooks; in a compiled program they are the same object, so comparing them with `===` gives `true`. Completion callbacks of callback-style I/O APIs, such as `fs.readFile`, get `undefined`, as AsyncLocalStorage stores do.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms.
|
||||
|
||||
Local calendar getters use the operating system's timezone database. Historical results can differ from Node.js when timezone data differs. For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule.
|
||||
|
||||
`toString`, `toTimeString`, `String(date)` and template literals print the zone's long name after the GMT offset, as Node does (`GMT-0500 (Central Daylight Time)`). scriptc identifies the zone the way Node does, from `TZ` when it names a zone, else from `/etc/localtime`. Outside Windows, a write to `process.env.TZ` changes the zone for later conversions and getters. The names are the en-US names for the IANA zones; Node prints them in the language of the locale that `LC_ALL`, `LC_MESSAGES` or `LANG` selects, so with a non-English locale the name differs from Node's. A `TZ` value that is a file path or a POSIX rule string (such as `EST5EDT,M3.2.0,M11.1.0`) gives offsets and names that differ from Node's, which ignores those forms. On Windows, the name is the `GMT+hh:mm` form unless `TZ` names a zone. On `wasm32-wasi`, local time is UTC and the name is `Coordinated Universal Time`.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. They raise an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values.
|
||||
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. `JSON.stringify` writes Date fields as their ISO text and Map and Set fields as `{}`, as Node.js does, and refuses classes with RegExp or Promise fields with `SC1090`. `Object.keys` raises an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
|
||||
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code` as typed members. The other own properties Node gives them, such as a system error's `errno`, `syscall`, and `path`, a child process error's `status`, `stdout`, and `stderr`, or an assertion error's `actual` and `expected`, are readable through an untyped view of the error (for example a cast of a caught value) and appear in `util.inspect` output.
|
||||
|
||||
Errors carry no stack frames. `console.log` and `util.inspect` print an Error in Node's stackless form, `[TypeError: message]`, where Node prints `TypeError: message` followed by stack frames; the two agree when `Error.stackTraceLimit` is `0`.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Local class expressions:** Static members, decorators, private brands, generic members, explicitly generic factories returning local constructors, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported.
|
||||
- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union.
|
||||
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union. A union may hold one promise beside other members, such as `T | Promise<T>` or `Promise<R | void> | R | void`: `await`, an async function's `return`, `Promise.resolve`, and `instanceof Promise` act on the member it holds, with Node's microtask timing. A promise member beside a member that declares `then`, two promise members in one union, and `console.log` of a union holding a promise are unsupported.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Compiled programs can't load WebAssembly modules yet. Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js. Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`. TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
|
||||
|
||||
`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use. It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
|
||||
@@ -1,10 +1,21 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createGeistdocs } from "@vercel/geistdocs/next";
|
||||
|
||||
const withGeistdocs = createGeistdocs();
|
||||
const legacySlugs = ["quickstart", "coverage", "dependencies", "ffi", "native-objects", "wasm", "platforms", "cli", "how-it-works", "limitations"];
|
||||
|
||||
// The compatibility page fetches its data under a content hash of the
|
||||
// committed artifact, so a deploy never serves a cached older copy, and
|
||||
// checks the row count it expects. Both are derived here at build time.
|
||||
const compatibilityArtifact = readFileSync(new URL("./src/generated/node-v24-compatibility.json", import.meta.url), "utf8");
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
env: {
|
||||
COMPATIBILITY_ARTIFACT_VERSION: createHash("sha256").update(compatibilityArtifact).digest("hex").slice(0, 20),
|
||||
COMPATIBILITY_ROW_COUNT: String(JSON.parse(compatibilityArtifact).rows.length),
|
||||
},
|
||||
agentRules: false,
|
||||
allowedDevOrigins: ["127.0.0.1"],
|
||||
distDir: process.env.NEXT_DIST_DIR || ".next",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
// @refresh reset
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import compatibilityMeta from "@/generated/node-v24-compatibility-meta.json";
|
||||
|
||||
// Set by next.config.mjs from the committed artifact.
|
||||
const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION;
|
||||
const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT);
|
||||
|
||||
type Status =
|
||||
| "supported"
|
||||
@@ -211,14 +214,14 @@ export function NodeCompatibilityMatrix() {
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(`/compatibility/data?v=${compatibilityMeta.artifactVersion}`, { cache: "no-store", signal: controller.signal })
|
||||
fetch(`/compatibility/data?v=${artifactVersion}`, { cache: "no-store", signal: controller.signal })
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`compatibility data: ${response.status}`);
|
||||
return response.json() as Promise<Snapshot>;
|
||||
})
|
||||
.then((data) => {
|
||||
if (!Array.isArray(data.rows) || data.rows.length !== compatibilityMeta.rowCount) {
|
||||
throw new Error(`compatibility data is stale or malformed (expected ${compatibilityMeta.rowCount} rows)`);
|
||||
if (!Array.isArray(data.rows) || data.rows.length !== rowCount) {
|
||||
throw new Error(`compatibility data is stale or malformed (expected ${rowCount} rows)`);
|
||||
}
|
||||
setSnapshot(data);
|
||||
})
|
||||
@@ -281,7 +284,7 @@ export function NodeCompatibilityMatrix() {
|
||||
return <div className="flex h-full items-center justify-center p-6"><div className="rounded-xl border border-red-500/40 bg-red-100 p-4 text-sm text-red-900">The compatibility dataset could not be loaded.</div></div>;
|
||||
}
|
||||
if (!snapshot) {
|
||||
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {compatibilityMeta.rowCount.toLocaleString()} Node.js API rows…</div>;
|
||||
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {rowCount.toLocaleString()} Node.js API rows…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "d9da17b113d39b0cf1b5",
|
||||
"rowCount": 3880
|
||||
}
|
||||
@@ -330,8 +330,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -584,9 +584,9 @@
|
||||
"apiEntries": 9,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 9,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -689,9 +689,9 @@
|
||||
"apiEntries": 117,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 64,
|
||||
"partial": 65,
|
||||
"refused": 0,
|
||||
"not-implemented": 53,
|
||||
"not-implemented": 52,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -36590,9 +36590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58489,7 +58489,7 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"status": "partial",
|
||||
"detail": "Derived from the exact API rows in this chapter.",
|
||||
"verification": "derived"
|
||||
}
|
||||
@@ -58512,9 +58512,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58535,9 +58535,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58558,9 +58558,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58581,9 +58581,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58627,9 +58627,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58650,9 +58650,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58673,9 +58673,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67344,9 +67344,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+16
@@ -300,6 +300,22 @@
|
||||
{
|
||||
"text": "Memory",
|
||||
"id": "memory"
|
||||
},
|
||||
{
|
||||
"text": "ThreadSlots",
|
||||
"id": "threadslots"
|
||||
},
|
||||
{
|
||||
"text": "Slot methods",
|
||||
"id": "slot-methods"
|
||||
},
|
||||
{
|
||||
"text": "Keys and values",
|
||||
"id": "keys-and-values"
|
||||
},
|
||||
{
|
||||
"text": "Slot cost",
|
||||
"id": "slot-cost"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -151,8 +151,11 @@ test("compatibility page and data endpoints keep their existing contracts", asyn
|
||||
const response = await get("/compatibility/data");
|
||||
assert.equal(response.status, 200);
|
||||
const dataset = await response.json();
|
||||
const metadata = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility-meta.json", import.meta.url), "utf8"));
|
||||
assert.equal(dataset.rows.length, metadata.rowCount);
|
||||
const artifact = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility.json", import.meta.url), "utf8"));
|
||||
assert.equal(dataset.rows.length, artifact.rows.length);
|
||||
// The row count next.config.mjs derives reaches the page (React separates
|
||||
// adjacent text nodes with empty comments).
|
||||
assert.ok(html.replaceAll("<!-- -->", "").includes(`Loading ${artifact.rows.length.toLocaleString()} Node.js API rows`));
|
||||
assert.equal((await get("/node-compatibility")).status, 308);
|
||||
assert.equal((await get("/node-compatibility/data")).status, 308);
|
||||
});
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"tierItemCount": 3457,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1166,
|
||||
"verify-gap": 2164,
|
||||
"replace-refusal": 40,
|
||||
"audit-partial": 1174,
|
||||
"verify-gap": 2155,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
}
|
||||
@@ -880,7 +880,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:AsyncLocalStorage",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -932,7 +933,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:asyncLocalStorage.disable",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -959,7 +961,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:asyncLocalStorage.getStore",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -986,7 +989,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:asyncLocalStorage.enterWith",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1013,7 +1017,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:asyncLocalStorage.run",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1040,7 +1045,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:asyncLocalStorage.exit",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1771,7 +1777,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:AsyncLocalStorage",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6736,6 +6743,8 @@
|
||||
"source": "compiler-dedicated:crypto.createHash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -6774,6 +6783,8 @@
|
||||
"source": "compiler-dedicated:crypto.createHmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -7396,6 +7407,8 @@
|
||||
"source": "compiler-dedicated:crypto.hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -7519,6 +7532,8 @@
|
||||
"source": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -7557,6 +7572,8 @@
|
||||
"source": "compiler-dedicated:crypto.pbkdf2Sync",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9037,6 +9054,8 @@
|
||||
"source": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9075,6 +9094,8 @@
|
||||
"source": "compiler-dedicated:hash.copy",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9113,6 +9134,8 @@
|
||||
"source": "compiler-dedicated:hash.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9151,6 +9174,8 @@
|
||||
"source": "compiler-dedicated:hash.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9189,6 +9214,8 @@
|
||||
"source": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9227,6 +9254,8 @@
|
||||
"source": "compiler-dedicated:hmac.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -9265,6 +9294,8 @@
|
||||
"source": "compiler-dedicated:hmac.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -34211,11 +34242,11 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"status": "refused",
|
||||
"action": "replace-refusal",
|
||||
"verification": "explicit-refusal",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:unsupported:stdlib.webassembly",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
@@ -58592,12 +58623,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58617,12 +58652,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58642,12 +58681,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.toASCII",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58667,12 +58710,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.toUnicode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58717,12 +58764,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.ucs2.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58742,12 +58793,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.ucs2.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58767,12 +58822,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:punycode",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.punycode.version",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68856,12 +68915,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.getTestContext",
|
||||
"tests": [
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -342,8 +342,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -611,9 +611,9 @@
|
||||
"apiEntries": 9,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 9,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -746,9 +746,9 @@
|
||||
"apiEntries": 117,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 64,
|
||||
"partial": 65,
|
||||
"refused": 0,
|
||||
"not-implemented": 53,
|
||||
"not-implemented": 52,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -1953,7 +1953,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -2002,7 +2003,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:AsyncLocalStorage",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -2074,7 +2076,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.disable",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -2100,7 +2103,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.getStore",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -2126,7 +2130,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.enterWith",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -2152,7 +2157,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.run",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -2178,7 +2184,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.exit",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -2481,6 +2488,7 @@
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -3042,7 +3050,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:AsyncLocalStorage",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs"
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -19861,6 +19870,8 @@
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
"tests/corpus/node24-crypto-kdf-options.ts",
|
||||
"tests/corpus/node24-crypto-kdf.ts",
|
||||
@@ -20402,6 +20413,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.createHash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -20439,6 +20452,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.createHmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -21040,6 +21055,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -21158,6 +21175,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -21195,6 +21214,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2Sync",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23095,6 +23116,8 @@
|
||||
"evidence": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23132,6 +23155,8 @@
|
||||
"evidence": "compiler-dedicated:hash.copy",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23169,6 +23194,8 @@
|
||||
"evidence": "compiler-dedicated:hash.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23206,6 +23233,8 @@
|
||||
"evidence": "compiler-dedicated:hash.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23243,6 +23272,8 @@
|
||||
"evidence": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23280,6 +23311,8 @@
|
||||
"evidence": "compiler-dedicated:hmac.digest",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -23317,6 +23350,8 @@
|
||||
"evidence": "compiler-dedicated:hmac.update",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -25031,6 +25066,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -28114,6 +28151,8 @@
|
||||
"evidence": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -28164,6 +28203,8 @@
|
||||
"evidence": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
@@ -53570,8 +53611,8 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -81994,8 +82035,13 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "derived:descendants"
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
},
|
||||
@@ -82017,8 +82063,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82040,8 +82091,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82063,8 +82119,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.toASCII",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82086,8 +82147,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.toUnicode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82132,8 +82198,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.ucs2.decode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82155,8 +82226,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.ucs2.encode",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -82178,8 +82254,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:punycode"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.punycode.version",
|
||||
"tests": [
|
||||
"tests/corpus/6122-punycode-conversions.ts",
|
||||
"tests/corpus/6124-punycode-require-warnings.cjs",
|
||||
"tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
},
|
||||
@@ -91023,6 +91104,8 @@
|
||||
"tests/fixtures/node-test/cases/ctx-hooks.ts",
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"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.ts",
|
||||
"tests/fixtures/node-test/cases/mock-method.ts",
|
||||
"tests/fixtures/node-test/cases/mock-timers-date.ts",
|
||||
@@ -94771,8 +94854,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.getTestContext",
|
||||
"tests": [
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,6 @@ const root = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`;
|
||||
const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`;
|
||||
const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`;
|
||||
const publicMetaOutputPath = `${root}docs/src/generated/node-v24-compatibility-meta.json`;
|
||||
const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`;
|
||||
const compilerCompatPath = `${packageRoot}static-support.json`;
|
||||
const pinPath = `${packageRoot}node-v24.json`;
|
||||
@@ -203,6 +202,10 @@ function featureOf(row, entries, parentSignature) {
|
||||
|
||||
function symbolCandidates(row) {
|
||||
const result = new Set([row.apiSymbol]);
|
||||
// A global's own row (`WebAssembly`) matches the compiler's
|
||||
// `globalThis.<name>` entries.
|
||||
if (row.chapter === "globals" && row.apiSymbol && !row.apiSymbol.includes("."))
|
||||
result.add(`globalThis.${row.apiSymbol}`);
|
||||
const aliases = [
|
||||
["fsPromises.", "fs/promises."],
|
||||
["dnsPromises.", "dns/promises."],
|
||||
@@ -1005,17 +1008,6 @@ function publicSnapshot(snapshot) {
|
||||
};
|
||||
}
|
||||
|
||||
function publicMetadata(publicRendered, snapshot) {
|
||||
const matrix = projectPublicMatrix(snapshot);
|
||||
return {
|
||||
schemaVersion: 3,
|
||||
nodeVersion: snapshot.nodeVersion,
|
||||
nodeCommit: snapshot.nodeCommit,
|
||||
artifactVersion: sha(publicRendered).slice(0, 20),
|
||||
rowCount: matrix.rows.length,
|
||||
};
|
||||
}
|
||||
|
||||
function checkLocal() {
|
||||
const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8"));
|
||||
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
|
||||
@@ -1067,11 +1059,8 @@ function checkLocal() {
|
||||
if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) {
|
||||
throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
if (
|
||||
readFileSync(publicMetaOutputPath, "utf8") !== render(publicMetadata(publicRendered, snapshot))
|
||||
) {
|
||||
throw new Error("the docs compatibility metadata is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) {
|
||||
throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
for (const row of snapshot.rows) {
|
||||
for (const key of ["static"]) {
|
||||
@@ -1137,7 +1126,6 @@ async function main() {
|
||||
const internalRendered = render(snapshot);
|
||||
const backlogRendered = render(buildBacklog(snapshot));
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
const publicMetaRendered = render(publicMetadata(publicRendered, snapshot));
|
||||
|
||||
if (args.has("--check")) {
|
||||
if (readFileSync(internalOutputPath, "utf8") !== internalRendered)
|
||||
@@ -1146,8 +1134,6 @@ async function main() {
|
||||
throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== publicRendered)
|
||||
throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicMetaOutputPath, "utf8") !== publicMetaRendered)
|
||||
throw new Error("public Node compatibility metadata is stale; run 'pnpm node-compat'");
|
||||
console.log(
|
||||
`Node compatibility snapshot matches ${pin.tag} (${snapshot.rows.length} API rows, commit ${pin.commit.slice(0, 12)})`,
|
||||
);
|
||||
@@ -1156,7 +1142,6 @@ async function main() {
|
||||
writeFileSync(internalOutputPath, internalRendered);
|
||||
writeFileSync(backlogOutputPath, backlogRendered);
|
||||
writeFileSync(publicOutputPath, publicRendered);
|
||||
writeFileSync(publicMetaOutputPath, publicMetaRendered);
|
||||
console.log(
|
||||
`wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"syntax.for-await-readable-streams": ["tests/corpus/2861-readable-iterator-close.ts", "tests/corpus/2862-web-readable-async-iteration.ts"],
|
||||
"syntax.for-await-class-async-iterators": ["tests/corpus/2863-class-async-iterator.ts"],
|
||||
"node-builtin.assert": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1609-assert-async.ts"],
|
||||
"node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs"],
|
||||
"node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"],
|
||||
"node-builtin.buffer": ["tests/corpus/1402-buffer-encodings.ts", "tests/corpus/1660-buffer-read-write-num.ts", "tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"],
|
||||
"node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"],
|
||||
"node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"],
|
||||
@@ -151,6 +151,7 @@
|
||||
"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.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"],
|
||||
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
|
||||
@@ -369,6 +370,7 @@
|
||||
{ "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"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "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": "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"] },
|
||||
@@ -393,7 +395,7 @@
|
||||
{ "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] },
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] },
|
||||
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
|
||||
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
|
||||
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
|
||||
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
|
||||
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
@@ -401,7 +403,7 @@
|
||||
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
|
||||
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
|
||||
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] },
|
||||
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] },
|
||||
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] },
|
||||
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
|
||||
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
|
||||
{ "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
|
||||
|
||||
@@ -418,7 +418,12 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
|
||||
// An internal function referenced by exactly one other function, and by
|
||||
// no global, is a whole-program inlining candidate; keep it with that
|
||||
// function so its partition can inline it without importing.
|
||||
// function so its partition can inline it without importing. A function
|
||||
// body larger than this is past LLVM's inline cost limit even for the only
|
||||
// call to a local function, so it is placed by its own name. Otherwise a
|
||||
// program's module initializers, which only the entry point calls, would
|
||||
// all share the entry point's partition.
|
||||
constexpr size_t MaxInlinedBodyBytes = 256 * 1024;
|
||||
constexpr unsigned Shared = ~1u;
|
||||
std::vector<unsigned> Referrer(Symbols.size(), NoPartition);
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
@@ -441,7 +446,8 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
for (unsigned Steps = 0; Steps < 1024; ++Steps) {
|
||||
const Symbol &S = Symbols[Root];
|
||||
unsigned Parent = Referrer[Root];
|
||||
if (!S.Local || Parent == NoPartition || Parent == Shared ||
|
||||
if (!S.Local || S.Body.size() > MaxInlinedBodyBytes ||
|
||||
Parent == NoPartition || Parent == Shared ||
|
||||
Symbols[Parent].Kind != SymbolKind::Function || Parent == I)
|
||||
break;
|
||||
Root = Parent;
|
||||
|
||||
@@ -22,8 +22,8 @@ struct ProgramPartitions {
|
||||
// Divides the LLVM assembly scriptc emits into Count modules without parsing
|
||||
// it, so the partitions can be parsed concurrently. Functions are placed by a
|
||||
// stable hash of their name; an internal function referenced by exactly one
|
||||
// other function stays with that function, and a global stays with the first
|
||||
// function that references it. Returns nullopt for any construct outside the
|
||||
// other function stays with that function unless its body is too large to
|
||||
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
|
||||
// emitter's narrow top-level grammar; callers then split a parsed module.
|
||||
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
|
||||
unsigned Count);
|
||||
|
||||
+82
-20
@@ -3412,8 +3412,10 @@ declare module "node:test" {
|
||||
readonly tags: readonly string[];
|
||||
readonly assert: TestContextAssert;
|
||||
readonly mock: MockTracker;
|
||||
test(name: string, fn?: TestFn): Promise<void>;
|
||||
test(name: string, options: TestOptions, fn?: TestFn): Promise<void>;
|
||||
test(name?: string, fn?: TestFn): Promise<void>;
|
||||
test(name?: string, options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
test(options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
test(fn?: TestFn): Promise<void>;
|
||||
skip(message?: string): void;
|
||||
todo(message?: string): void;
|
||||
diagnostic(message: string): void;
|
||||
@@ -3443,23 +3445,44 @@ declare module "node:test" {
|
||||
match(value: string, regExp: RegExp, message?: string): void;
|
||||
doesNotMatch(value: string, regExp: RegExp, message?: string): void;
|
||||
}
|
||||
function test(name: string, fn?: TestFn): Promise<void>;
|
||||
function test(name: string, options: TestOptions, fn?: TestFn): Promise<void>;
|
||||
/* The name, options and function are each optional, as in Node: a test
|
||||
* without a name takes the function's name, or "<anonymous>". */
|
||||
function test(name?: string, fn?: TestFn): Promise<void>;
|
||||
function test(name?: string, options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function test(options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function test(fn?: TestFn): Promise<void>;
|
||||
namespace test {
|
||||
function skip(name: string, fn?: TestFn): Promise<void>;
|
||||
function skip(name: string, options: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function todo(name: string, fn?: TestFn): Promise<void>;
|
||||
function todo(name: string, options: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function only(name: string, fn?: TestFn): Promise<void>;
|
||||
function only(name: string, options: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function skip(name?: string, fn?: TestFn): Promise<void>;
|
||||
function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function skip(options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function skip(fn?: TestFn): Promise<void>;
|
||||
function todo(name?: string, fn?: TestFn): Promise<void>;
|
||||
function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function todo(options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function todo(fn?: TestFn): Promise<void>;
|
||||
function only(name?: string, fn?: TestFn): Promise<void>;
|
||||
function only(name?: string, options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function only(options?: TestOptions, fn?: TestFn): Promise<void>;
|
||||
function only(fn?: TestFn): Promise<void>;
|
||||
}
|
||||
const it: typeof test;
|
||||
function describe(name: string, fn?: SuiteFn): void;
|
||||
function describe(name: string, options: TestOptions, fn?: SuiteFn): void;
|
||||
function describe(name?: string, fn?: SuiteFn): void;
|
||||
function describe(name?: string, options?: TestOptions, fn?: SuiteFn): void;
|
||||
function describe(options?: TestOptions, fn?: SuiteFn): void;
|
||||
function describe(fn?: SuiteFn): void;
|
||||
namespace describe {
|
||||
function skip(name: string, fn?: SuiteFn): void;
|
||||
function todo(name: string, fn?: SuiteFn): void;
|
||||
function only(name: string, fn?: SuiteFn): void;
|
||||
function skip(name?: string, fn?: SuiteFn): void;
|
||||
function skip(name?: string, options?: TestOptions, fn?: SuiteFn): void;
|
||||
function skip(options?: TestOptions, fn?: SuiteFn): void;
|
||||
function skip(fn?: SuiteFn): void;
|
||||
function todo(name?: string, fn?: SuiteFn): void;
|
||||
function todo(name?: string, options?: TestOptions, fn?: SuiteFn): void;
|
||||
function todo(options?: TestOptions, fn?: SuiteFn): void;
|
||||
function todo(fn?: SuiteFn): void;
|
||||
function only(name?: string, fn?: SuiteFn): void;
|
||||
function only(name?: string, options?: TestOptions, fn?: SuiteFn): void;
|
||||
function only(options?: TestOptions, fn?: SuiteFn): void;
|
||||
function only(fn?: SuiteFn): void;
|
||||
}
|
||||
const suite: typeof describe;
|
||||
function before(fn: HookFn): void;
|
||||
@@ -3534,6 +3557,9 @@ declare module "node:test" {
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
const mock: MockTracker;
|
||||
/* The context of the test, suite or hook running in the current async
|
||||
* context; undefined outside one. */
|
||||
function getTestContext(): TestContext | SuiteContext | undefined;
|
||||
/* Declared for typechecking only — every use fences (no lowering).
|
||||
* `assert` is the module-level custom-assertion registry (v24's
|
||||
* `const { assert } = require('node:test')`), not TestContext.assert. */
|
||||
@@ -3551,6 +3577,7 @@ declare module "node:test" {
|
||||
afterEach,
|
||||
mock,
|
||||
assert,
|
||||
getTestContext,
|
||||
run,
|
||||
snapshot,
|
||||
TestContext,
|
||||
@@ -3769,6 +3796,39 @@ declare module "node:querystring" {
|
||||
export * from "querystring";
|
||||
}
|
||||
|
||||
/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and
|
||||
* toUnicode throw its RangeErrors on malformed input; ucs2 converts
|
||||
* between strings and UTF-16 code point arrays. */
|
||||
declare module "punycode" {
|
||||
export function decode(string: string): string;
|
||||
export function encode(string: string): string;
|
||||
export function toUnicode(domain: string): string;
|
||||
export function toASCII(domain: string): string;
|
||||
export namespace ucs2 {
|
||||
function decode(string: string): number[];
|
||||
function encode(codePoints: readonly number[]): string;
|
||||
}
|
||||
export const version: string;
|
||||
}
|
||||
declare module "node:punycode" {
|
||||
export * from "punycode";
|
||||
}
|
||||
|
||||
/* node:constants (deprecated, DEP0008) — one frozen object: os.constants'
|
||||
* dlopen, errno, priority and signals groups, fs.constants, and
|
||||
* crypto.constants. A platform's missing constant reads as undefined. */
|
||||
declare module "constants" {
|
||||
const constants: { readonly [name: string]: number } & {
|
||||
readonly defaultCoreCipherList: string;
|
||||
readonly defaultCipherList: string;
|
||||
};
|
||||
export = constants;
|
||||
}
|
||||
declare module "node:constants" {
|
||||
import constants = require("constants");
|
||||
export = constants;
|
||||
}
|
||||
|
||||
/* node:readline — the question/close slice: createInterface over exactly
|
||||
* { input: process.stdin, output: process.stdout }, question(query, cb)
|
||||
* (the query writes to stdout, the callback gets the next line's text),
|
||||
@@ -4842,17 +4902,16 @@ declare module "http" {
|
||||
/* The Agent lowers to a checked-dynamic HANDLE: option parsing at the
|
||||
* literal construction site, getName, destroy, the sockets/requests/
|
||||
* freeSockets snapshots, and REAL maxSockets accounting (over-limit
|
||||
* requests defer their dial and queue). keepAlive: true throws the
|
||||
* named pooling fence — this client dials one connection per request
|
||||
* and closes it with the response, so freeSockets is always
|
||||
* empty. */
|
||||
* requests defer their dial and queue). keepAlive: true keeps finished
|
||||
* connections in freeSockets for the next request to the same name,
|
||||
* taken in `scheduling` order. */
|
||||
export interface AgentOptions {
|
||||
keepAlive?: boolean;
|
||||
keepAliveMsecs?: number;
|
||||
maxSockets?: number;
|
||||
maxFreeSockets?: number;
|
||||
maxTotalSockets?: number;
|
||||
scheduling?: string;
|
||||
scheduling?: "fifo" | "lifo";
|
||||
timeout?: number;
|
||||
port?: number;
|
||||
[option: string]: unknown;
|
||||
@@ -4882,6 +4941,7 @@ declare module "http" {
|
||||
readonly freeSockets: { [key: string]: unknown[] };
|
||||
readonly requests: { [key: string]: unknown[] };
|
||||
}
|
||||
/* Node's keepAlive global agent for requests without an agent option. */
|
||||
export const globalAgent: Agent;
|
||||
export interface ClientRequest {
|
||||
readonly destroyed: boolean;
|
||||
@@ -5085,6 +5145,8 @@ declare module "https" {
|
||||
import { Server } from "http";
|
||||
import { Agent, ClientRequest, IncomingMessage, ServerResponse } from "http";
|
||||
export { Agent, Server };
|
||||
/* Node's keepAlive global agent for requests without an agent option. */
|
||||
export const globalAgent: Agent;
|
||||
export interface ServerOptions {
|
||||
cert: string | Uint8Array;
|
||||
key: string | Uint8Array;
|
||||
|
||||
+134
@@ -13,6 +13,9 @@
|
||||
* they MERGE as overloads instead of colliding — the Timeout handle maps
|
||||
* to the numeric timer id either way, and .unref()/.ref()/.hasRef() are
|
||||
* loop-liveness bookkeeping).
|
||||
* 4. The WebAssembly namespace and BufferSource, which the DOM library
|
||||
* supplies in ordinary TypeScript projects (types only; every reached
|
||||
* use of the API is refused at its site).
|
||||
*
|
||||
* This file ships to EVERY program scriptc builds — the lowering program
|
||||
* and preflight's project-world second-chance program alike (a project
|
||||
@@ -108,3 +111,134 @@ declare function clearTimeout(handle?: Timeout | number | null | undefined): voi
|
||||
* baked result is exactly what Node would compute at runtime (the
|
||||
* differential harness verifies this with a `comptime = (f) => f()` shim). */
|
||||
declare function comptime<T>(compute: () => T): T;
|
||||
|
||||
/* The JavaScript WebAssembly API, as the DOM library declares it (scriptc's
|
||||
* ES-only type world never loads lib.dom, and @types/node leaves it to
|
||||
* lib.dom). Packages whose declarations name these types (quickjs-wasi,
|
||||
* photon-node, ...) typecheck against them. These are types only: the
|
||||
* type mapper gives every type declared here the checked-dynamic
|
||||
* representation, and the lowerer refuses each reached use of the API with
|
||||
* a located diagnostic (frontend/lowering/builtins/webassembly.ts).
|
||||
* BufferSource admits every ArrayBufferView, as lib.dom did before
|
||||
* TypeScript 5.7: Node's Buffer types are views over ArrayBufferLike. */
|
||||
type BufferSource = ArrayBufferView | ArrayBuffer;
|
||||
declare namespace WebAssembly {
|
||||
interface CompileError extends Error {}
|
||||
var CompileError: {
|
||||
prototype: CompileError;
|
||||
new (message?: string): CompileError;
|
||||
(message?: string): CompileError;
|
||||
};
|
||||
interface Global<T extends ValueType = ValueType> {
|
||||
value: ValueTypeMap[T];
|
||||
valueOf(): ValueTypeMap[T];
|
||||
}
|
||||
var Global: {
|
||||
prototype: Global;
|
||||
new <T extends ValueType = ValueType>(
|
||||
descriptor: GlobalDescriptor<T>,
|
||||
v?: ValueTypeMap[T],
|
||||
): Global<T>;
|
||||
};
|
||||
interface Instance {
|
||||
readonly exports: Exports;
|
||||
}
|
||||
var Instance: {
|
||||
prototype: Instance;
|
||||
new (module: Module, importObject?: Imports): Instance;
|
||||
};
|
||||
interface LinkError extends Error {}
|
||||
var LinkError: {
|
||||
prototype: LinkError;
|
||||
new (message?: string): LinkError;
|
||||
(message?: string): LinkError;
|
||||
};
|
||||
interface Memory {
|
||||
readonly buffer: ArrayBuffer;
|
||||
grow(delta: number): number;
|
||||
}
|
||||
var Memory: {
|
||||
prototype: Memory;
|
||||
new (descriptor: MemoryDescriptor): Memory;
|
||||
};
|
||||
interface Module {}
|
||||
var Module: {
|
||||
prototype: Module;
|
||||
new (bytes: BufferSource): Module;
|
||||
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
|
||||
exports(moduleObject: Module): ModuleExportDescriptor[];
|
||||
imports(moduleObject: Module): ModuleImportDescriptor[];
|
||||
};
|
||||
interface RuntimeError extends Error {}
|
||||
var RuntimeError: {
|
||||
prototype: RuntimeError;
|
||||
new (message?: string): RuntimeError;
|
||||
(message?: string): RuntimeError;
|
||||
};
|
||||
interface Table {
|
||||
readonly length: number;
|
||||
get(index: number): any;
|
||||
grow(delta: number, value?: any): number;
|
||||
set(index: number, value?: any): void;
|
||||
}
|
||||
var Table: {
|
||||
prototype: Table;
|
||||
new (descriptor: TableDescriptor, value?: any): Table;
|
||||
};
|
||||
interface GlobalDescriptor<T extends ValueType = ValueType> {
|
||||
mutable?: boolean;
|
||||
value: T;
|
||||
}
|
||||
interface MemoryDescriptor {
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
shared?: boolean;
|
||||
}
|
||||
interface ModuleExportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
name: string;
|
||||
}
|
||||
interface ModuleImportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
module: string;
|
||||
name: string;
|
||||
}
|
||||
interface TableDescriptor {
|
||||
element: TableKind;
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
}
|
||||
interface ValueTypeMap {
|
||||
anyfunc: Function;
|
||||
externref: any;
|
||||
f32: number;
|
||||
f64: number;
|
||||
i32: number;
|
||||
i64: bigint;
|
||||
v128: never;
|
||||
}
|
||||
interface WebAssemblyInstantiatedSource {
|
||||
instance: Instance;
|
||||
module: Module;
|
||||
}
|
||||
type ImportExportKind = "function" | "global" | "memory" | "table";
|
||||
type TableKind = "anyfunc" | "externref";
|
||||
type ExportValue = Function | Global | Memory | Table;
|
||||
type Exports = Record<string, ExportValue>;
|
||||
type ImportValue = ExportValue | number;
|
||||
type Imports = Record<string, ModuleImports>;
|
||||
type ModuleImports = Record<string, ImportValue>;
|
||||
type ValueType = keyof ValueTypeMap;
|
||||
function compile(bytes: BufferSource): Promise<Module>;
|
||||
function compileStreaming(source: Response | PromiseLike<Response>): Promise<Module>;
|
||||
function instantiate(
|
||||
bytes: BufferSource,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function instantiate(moduleObject: Module, importObject?: Imports): Promise<Instance>;
|
||||
function instantiateStreaming(
|
||||
source: Response | PromiseLike<Response>,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function validate(bytes: BufferSource): boolean;
|
||||
}
|
||||
|
||||
@@ -97,16 +97,20 @@ export function computeCycleMutability(
|
||||
// can store; a commit rewrites every field, reference fields possibly
|
||||
// with fresh conversions). Without any such store in the program,
|
||||
// crossing into checked-dynamic code writes nothing. FFI calls and
|
||||
// intrinsics other than publish (which only marks objects immortal) and
|
||||
// the intern-table operations (which store published values) stay
|
||||
// conservative.
|
||||
// intrinsics other than publish (which only marks objects immortal), the
|
||||
// intern-table operations (which store published values) and ThreadSlots
|
||||
// reads stay conservative.
|
||||
const dynStores = dynamicFieldStores(mod);
|
||||
const dynamicStores = dynStores.all || dynStores.names.size > 0;
|
||||
result.emptyPropertyBags = !dynamicStores;
|
||||
const boundaryWrites = (e: IrExpr): boolean =>
|
||||
dynamicStores ||
|
||||
e.kind === "ffiCall" ||
|
||||
(e.kind === "intrinsic" && e.name !== "threads.publish" && !e.name.startsWith("itable."));
|
||||
(e.kind === "intrinsic" &&
|
||||
e.name !== "threads.publish" &&
|
||||
!e.name.startsWith("itable.") &&
|
||||
e.name !== "slots.get" &&
|
||||
e.name !== "slots.has");
|
||||
|
||||
// Types that can become checked-dynamic values: mark their whole closure.
|
||||
const exposedTypes = new Set<string>();
|
||||
|
||||
@@ -3,11 +3,13 @@ import { type IrModule } from "../ir/ir.js";
|
||||
import { moduleRuntimeFeatures } from "../ir/runtime-features.js";
|
||||
import { hasForeignFfiCallback } from "./ffi-callbacks.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import { youngNurseryRequested } from "./young-nursery.js";
|
||||
|
||||
export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
const features = moduleRuntimeFeatures(mod);
|
||||
return {
|
||||
...(mod.workers ? { workers: true } : {}),
|
||||
...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}),
|
||||
regex: features.regex,
|
||||
copying: features.copying,
|
||||
textDecoderLegacy: features.legacyTextDecoder,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { type AllocHost, emitObjectAlloc, emitObjectFree, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
type AllocHost,
|
||||
disposeHookLoadLines,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
inlineAllocSupported,
|
||||
} from "./alloc.js";
|
||||
|
||||
function host(inlineAlloc: boolean): AllocHost & { decls: Set<string>; oom: boolean } {
|
||||
const h = {
|
||||
@@ -63,7 +69,12 @@ describe("emitted object allocation", () => {
|
||||
]);
|
||||
expect(emitObjectFree(h, false)).toEqual([
|
||||
" call void @scr_obj_free_note()",
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.observe, label %sf.block",
|
||||
"sf.observe:",
|
||||
" call void @scr_weak_dispose(ptr %o)",
|
||||
" br label %sf.block",
|
||||
"sf.block:",
|
||||
" call void @scr_rt_free(ptr %o)",
|
||||
]);
|
||||
expect([...h.decls].some((d) => d.includes("@scr_sa"))).toBe(false);
|
||||
@@ -167,4 +178,30 @@ describe("emitted object allocation", () => {
|
||||
expect(lines.at(-1)).toBe("sf.done:");
|
||||
}
|
||||
});
|
||||
|
||||
test("frees test the dispose hook only where the program can observe them", () => {
|
||||
const h = host(true);
|
||||
const weak = emitObjectFree(h, false);
|
||||
expectSingleDefinitions(weak);
|
||||
expect(weak.slice(0, 2)).toEqual([
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
const records = emitObjectFree(h, true, "weak-or-source");
|
||||
expectSingleDefinitions(records);
|
||||
expect(records.slice(0, 4)).toEqual([
|
||||
" %sf.obsw = load i1, ptr @sc_weak_keys",
|
||||
" %sf.obss = load i1, ptr @sc_record_sources",
|
||||
" %sf.obs = or i1 %sf.obsw, %sf.obss",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
expect(records).toContain(" call void %sf.hook(ptr %o)");
|
||||
});
|
||||
|
||||
test("loads the dispose hook from emitted code only with the inline allocator", () => {
|
||||
expect(disposeHookLoadLines(host(false), "%t1")).toBeNull();
|
||||
expect(disposeHookLoadLines(host(true), "%t1")).toEqual([
|
||||
" %t1 = load ptr, ptr @scr_weak_dispose_hook",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,17 @@ function threadLocalState(host: AllocHost, prefix: string, lines: string[]): str
|
||||
return [...preamble, ...text.split("\n")];
|
||||
}
|
||||
|
||||
/** Lines that load the runtime's dispose hook into `dest`, or null on
|
||||
* targets without the inline allocator, where the hook's storage is not
|
||||
* addressed from emitted code. The hook is NULL until a weak collection
|
||||
* or a kept record source installs it, and then never NULL again. */
|
||||
export function disposeHookLoadLines(host: AllocHost, dest: string): string[] | null {
|
||||
if (host.inlineAlloc !== true) return null;
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const prefix = dest.replace(/^%/, "").replace(/[^-$._A-Za-z0-9]/g, "_");
|
||||
return threadLocalState(host, prefix, [` ${dest} = load ptr, ptr @scr_weak_dispose_hook`]);
|
||||
}
|
||||
|
||||
/** Worker executables: the calling script thread's ScrThreadHot block into
|
||||
* `dest`. Its address also identifies the thread (the owner word of a
|
||||
* lifetime-region chunk; llvm/regions.ts). */
|
||||
@@ -141,6 +152,97 @@ function declareGlobal(host: AllocHost, decl: string): void {
|
||||
if (host.threadLocalAlloc !== true) host.declare(decl);
|
||||
}
|
||||
|
||||
/** `scr_cyc_buffers` (scr_rc.h ScrCycBuffers): the three candidate buffers
|
||||
* `{ v, n, cap, used }`, the inline buffering window and the allocation
|
||||
* trigger. */
|
||||
const CYC_BUFFERS_TYPE = "{ [3 x { ptr, i64, i64, i64 }], i64, i64 }";
|
||||
/** Byte offsets in it (asserted in scr_rc.h). */
|
||||
const CYC_NURSERY_COUNT = 8;
|
||||
const CYC_WINDOW = 96;
|
||||
const CYC_ALLOC_TRIGGER = 104;
|
||||
|
||||
/** Whether emitted release paths can append nursery candidates to the
|
||||
* collector's buffer themselves (through {@link cycleAppendHelper}): where
|
||||
* the runtime keeps that state in ordinary globals (executables without
|
||||
* workers and classic libraries, on the 64-bit targets with inline
|
||||
* allocation). */
|
||||
export function inlineBufferingSupported(host: AllocHost): boolean {
|
||||
return host.inlineAlloc === true && host.threadLocalAlloc !== true && host.sizeType === "i64";
|
||||
}
|
||||
|
||||
/** The emitted symbol of {@link cycleAppendHelper}. */
|
||||
export const CYC_APPEND = "sc_cyc_append";
|
||||
|
||||
/** The runtime symbols {@link cycleAppendHelper} uses. The helper is
|
||||
* defined after the module's extern block, so its caller declares them. */
|
||||
export function cycleAppendDecls(): string[] {
|
||||
return [
|
||||
`@scr_cyc_buffers = external global ${CYC_BUFFERS_TYPE}`,
|
||||
`@scr_cyc_live = external global i64`,
|
||||
`declare void @scr_cyc_on_release(ptr)`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The candidate-buffering step of every emitted release that leaves a
|
||||
* headered object alive and unbuffered: scr_rc.h's scr_cyc_append, then
|
||||
* scr_cyc_on_release when it declines. A nursery object goes onto the
|
||||
* nursery buffer while the buffer holds fewer entries than the runtime's
|
||||
* window and the live count is below the allocation trigger.
|
||||
*
|
||||
* One out-of-line definition per module keeps the release fast paths, which
|
||||
* LLVM inlines into their callers, as small as the plain call they had. It
|
||||
* uses preserve_mostcc, so the call clobbers almost no caller registers:
|
||||
* the callers' code around a release stays what it was, and the append
|
||||
* itself saves only the few registers it uses. Its slow path enters the
|
||||
* runtime through a second preserve_mostcc function, which saves the rest. */
|
||||
export function cycleAppendHelper(): string[] {
|
||||
return [
|
||||
`define internal preserve_mostcc void @${CYC_APPEND}(ptr %o) noinline #0 { ; scr_cyc_append, else scr_cyc_on_release`,
|
||||
`entry:`,
|
||||
` %genp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_GEN}`,
|
||||
` %gen = load i8, ptr %genp ; ScrCycHdr.gen`,
|
||||
` %young = icmp eq i8 %gen, 0`,
|
||||
` br i1 %young, label %window, label %call`,
|
||||
`window:`,
|
||||
` %np = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_NURSERY_COUNT}`,
|
||||
` %n = load i64, ptr %np ; nursery buffer entries`,
|
||||
` %wp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_WINDOW}`,
|
||||
` %w = load i64, ptr %wp ; window`,
|
||||
` %room = icmp ult i64 %n, %w`,
|
||||
` br i1 %room, label %live, label %call`,
|
||||
`live:`,
|
||||
` %lv = load i64, ptr @scr_cyc_live`,
|
||||
` %tp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_ALLOC_TRIGGER}`,
|
||||
` %t = load i64, ptr %tp ; allocation trigger`,
|
||||
` %due = icmp uge i64 %lv, %t`,
|
||||
` br i1 %due, label %call, label %append`,
|
||||
`append:`,
|
||||
` %v = load ptr, ptr @scr_cyc_buffers`,
|
||||
` %slot = getelementptr inbounds ptr, ptr %v, i64 %n`,
|
||||
` store ptr %o, ptr %slot`,
|
||||
` %n1 = add i64 %n, 1`,
|
||||
` store i64 %n1, ptr %np`,
|
||||
` %i = trunc i64 %n to i32`,
|
||||
` %b = or i32 %i, -2147483648 ; SCR_CYC_BUFFERED_BIT | position`,
|
||||
` %bufp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_BUF}`,
|
||||
` store i32 %b, ptr %bufp`,
|
||||
` ret void`,
|
||||
`call:`,
|
||||
` tail call preserve_mostcc void @${CYC_APPEND}_call(ptr %o)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
// The runtime call clobbers every caller-saved register, which this
|
||||
// preserve_mostcc function must restore: saving them here keeps those
|
||||
// saves out of the append path above.
|
||||
`define internal preserve_mostcc void @${CYC_APPEND}_call(ptr %o) noinline #0 {`,
|
||||
`entry:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
/** A collector header to install: the object type's constant ScrCycType
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). */
|
||||
export interface CycHeader {
|
||||
@@ -325,20 +427,56 @@ function allocLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
/* ── dispose observers ────────────────────────────────────────────────
|
||||
* The runtime's dispose hook (scr_weak_dispose_hook) has two clients: weak
|
||||
* collections, whose keys can be any object, and the kept sources of
|
||||
* checked records (scr_record_source_note and the JSON schema parser),
|
||||
* keyed by closed records. A program without either can never install the
|
||||
* hook, so its frees need not test it. Whether the program has them is
|
||||
* known only once every helper is emitted (dynCheck builders and JSON
|
||||
* schemas are requested lazily), so the emitted frees and record writers
|
||||
* test these two module constants, which the emitter defines last
|
||||
* (LlvmEmitter.emitParts) and LLVM folds away. */
|
||||
|
||||
/** True when the program constructs a WeakMap or WeakSet. */
|
||||
export const WEAK_KEYS_GATE = "@sc_weak_keys";
|
||||
/** True when a checked conversion can keep a record's undeclared source. */
|
||||
export const RECORD_SOURCES_GATE = "@sc_record_sources";
|
||||
|
||||
/** Which dispose observers an object type can have: weak-collection keys
|
||||
* only, or (closed record layouts) kept record sources too. */
|
||||
export type FreeObservers = "weak" | "weak-or-source";
|
||||
|
||||
/**
|
||||
* Lines that free the object `%o` after its members were released: the
|
||||
* RC-audit free note, the weak-collection dispose hook, and the block —
|
||||
* `scr_cyc_free` for collector-headered objects, `scr_rt_free` otherwise.
|
||||
* Labels use the `sf.` prefix; the lines end in an open block.
|
||||
* RC-audit free note, the dispose hook (when the program can observe this
|
||||
* type's disposals), and the block — `scr_cyc_free` for collector-headered
|
||||
* objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines
|
||||
* end in an open block.
|
||||
*/
|
||||
export function emitObjectFree(host: AllocHost, cyc: boolean): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc));
|
||||
export function emitObjectFree(
|
||||
host: AllocHost,
|
||||
cyc: boolean,
|
||||
observers: FreeObservers = "weak",
|
||||
): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers));
|
||||
if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE);
|
||||
host.declare(`declare void @scr_obj_free_note()`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
/** Lines that leave in `%<prefix>.obs` whether the program can observe a
|
||||
* disposal of this type (folds to a constant). */
|
||||
function observedLines(prefix: string, observers: FreeObservers): string[] {
|
||||
if (observers === "weak") return [` %${prefix}.obs = load i1, ptr ${WEAK_KEYS_GATE}`];
|
||||
return [
|
||||
` %${prefix}.obsw = load i1, ptr ${WEAK_KEYS_GATE}`,
|
||||
` %${prefix}.obss = load i1, ptr ${RECORD_SOURCES_GATE}`,
|
||||
` %${prefix}.obs = or i1 %${prefix}.obsw, %${prefix}.obss`,
|
||||
];
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] {
|
||||
if (host.inlineAlloc !== true) {
|
||||
if (cyc) {
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
@@ -348,7 +486,12 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_weak_dispose(ptr)`);
|
||||
return [
|
||||
` call void @scr_obj_free_note()`,
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.observe, label %sf.block`,
|
||||
`sf.observe:`,
|
||||
` call void @scr_weak_dispose(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
` call void @scr_rt_free(ptr %o)`,
|
||||
];
|
||||
}
|
||||
@@ -356,11 +499,14 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_rt_free(ptr)`);
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const lines = [
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.hooked, label %sf.block`,
|
||||
`sf.hooked:`,
|
||||
` %sf.hook = load ptr, ptr @scr_weak_dispose_hook`,
|
||||
` %sf.has = icmp ne ptr %sf.hook, null`,
|
||||
` %sf.hasx = call i1 @llvm.expect.i1(i1 %sf.has, i1 false)`,
|
||||
` br i1 %sf.hasx, label %sf.observe, label %sf.block`,
|
||||
`sf.observe: ; weak collections observe the disposal`,
|
||||
`sf.observe: ; a weak collection or a kept record source observes the disposal`,
|
||||
` call void %sf.hook(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
|
||||
@@ -42,5 +42,10 @@ export function borrowsJsonInputs(fn: string): boolean {
|
||||
* loop. Runtime steps borrow that state but may invoke indexed getters, so
|
||||
* this permits stable-local borrowing without preserving reference edges. */
|
||||
export function borrowsIteratorInputs(fn: string): boolean {
|
||||
return fn === "dyn.iteratorCanStep" || fn === "dyn.iteratorStep" || fn === "dyn.iteratorStepDone";
|
||||
return (
|
||||
fn === "dyn.iteratorCanStep" ||
|
||||
fn === "dyn.iteratorStep" ||
|
||||
fn === "dyn.iteratorStepDone" ||
|
||||
fn === "dyn.collectionIteratorIndex"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
|
||||
import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
|
||||
import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper } from "./classes.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import {
|
||||
@@ -316,3 +316,52 @@ test("unconstructed narrowing layouts may omit abstract slots while live classes
|
||||
});
|
||||
expect(() => emitLlvmModule(mod)).toThrow("no implementation of run for Dormant");
|
||||
});
|
||||
|
||||
test("the young-in nursery flag adds the nursery entry to traced constructors only", () => {
|
||||
const linked = { kind: "object" as const, className: "Linked" };
|
||||
const mod: IrModule = {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Scalar", fields: [{ name: "value", type: F64 }], methods: [], loc },
|
||||
// A dyn field can reach anything: the class carries a collector header.
|
||||
{
|
||||
name: "Linked",
|
||||
fields: [
|
||||
{ name: "next", type: linked },
|
||||
{ name: "hook", type: DYN },
|
||||
],
|
||||
methods: [],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
};
|
||||
const helper = (llvm: string, name: string): string => {
|
||||
const start = llvm.indexOf(`define internal ptr @${name}(`);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
return llvm.slice(start, llvm.indexOf("\n}\n", start));
|
||||
};
|
||||
const saved = process.env["SCRIPTC_YOUNG_NURSERY"];
|
||||
try {
|
||||
delete process.env["SCRIPTC_YOUNG_NURSERY"];
|
||||
const off = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
expect(off).not.toContain("scr_yin");
|
||||
|
||||
process.env["SCRIPTC_YOUNG_NURSERY"] = "1";
|
||||
const on = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const linkedNew = helper(on, mangleClassNew("Linked"));
|
||||
expect(linkedNew).toContain("@scr_yin_skip");
|
||||
expect(linkedNew).toContain("call void @scr_yin_enter_slow(ptr %o)");
|
||||
expect(linkedNew).toContain("store i8 3, ptr %ye.genp");
|
||||
// An acyclic class has no collector header and never enters.
|
||||
expect(helper(on, mangleClassNew("Scalar"))).not.toContain("scr_yin");
|
||||
// Targets without the inline allocator call the whole entry.
|
||||
const narrow = emitLlvmModule(mod, { pointerBits: 32 });
|
||||
expect(helper(narrow, mangleClassNew("Linked"))).toContain("call void @scr_yin_enter(ptr %o)");
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["SCRIPTC_YOUNG_NURSERY"];
|
||||
else process.env["SCRIPTC_YOUNG_NURSERY"] = saved;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RUNTIME_EVENT_TARGET_CLASS,
|
||||
RUNTIME_ERROR_CLASSES,
|
||||
RUNTIME_STREAM_CLASSES,
|
||||
classValueConstructor,
|
||||
} from "../../ir/ir.js";
|
||||
import { streamRooted, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import {
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
retainBody,
|
||||
traceAdapter,
|
||||
type ShapeHost,
|
||||
youngEntryLines,
|
||||
} from "./shapes.js";
|
||||
|
||||
/** One virtual method slot of a hierarchy: the ROOT-MOST declaring class
|
||||
@@ -75,6 +77,10 @@ export class LlClassMeta {
|
||||
membership: { pre: number; post: number }[] = [];
|
||||
/** Type-only references retain layouts without retaining construction. */
|
||||
constructorRetained = false;
|
||||
/** The hierarchy holds ThreadSlots keys (IrModule.threadSlotKeys): every
|
||||
* member's struct carries the i32 slot word before its fields
|
||||
* (classSlotWordIndex). */
|
||||
threadSlot = false;
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
@@ -116,6 +122,12 @@ export function buildClassGraph(
|
||||
if (meta.base === null) number(meta, meta);
|
||||
meta.hierarchy = meta.base !== null || meta.children.length > 0;
|
||||
}
|
||||
const slotRoots = new Set<LlClassMeta>();
|
||||
for (const key of mod.threadSlotKeys ?? []) {
|
||||
const meta = metaMap.get(key);
|
||||
if (meta !== undefined) slotRoots.add(meta.root);
|
||||
}
|
||||
for (const meta of metaMap.values()) meta.threadSlot = slotRoots.has(meta.root);
|
||||
const declares = (m: LlClassMeta, method: string): boolean =>
|
||||
m.def.methods?.includes(method) ?? false;
|
||||
const declaredBelow = (m: LlClassMeta, method: string): boolean =>
|
||||
@@ -323,16 +335,31 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null {
|
||||
return RUNTIME_PREFIXES.get(meta.root.def.name) ?? null;
|
||||
}
|
||||
|
||||
/** The GEP index where a class's own fields start: rc at 0, the vtable
|
||||
* word at 1 on hierarchy members, then a runtime prefix (state + display
|
||||
* name) on classes rooted at a runtime base, then the stream-state slot
|
||||
* on stream-rooted ones. */
|
||||
function fieldBase(meta: LlClassMeta): number {
|
||||
/** The GEP index after a class's header words: rc at 0, the vtable word
|
||||
* at 1 on hierarchy members, then a runtime prefix (state + display name)
|
||||
* on classes rooted at a runtime base, then the stream-state slot on
|
||||
* stream-rooted ones. */
|
||||
function headerEnd(meta: LlClassMeta): number {
|
||||
if (!meta.hierarchy) return 1;
|
||||
if (streamRooted(meta)) return 5;
|
||||
return runtimePrefixOf(meta) ? 4 : 2;
|
||||
}
|
||||
|
||||
/** The GEP index of the ThreadSlots slot word (an i32) in a class whose
|
||||
* hierarchy holds ThreadSlots keys: right after the header words, so every
|
||||
* member of the hierarchy has it at the same offset. */
|
||||
export function classSlotWordIndex(meta: LlClassMeta): number {
|
||||
if (!meta.threadSlot)
|
||||
throw new InternalCompilerError(`llvm emitter bug: class ${meta.def.name} has no slot word`);
|
||||
return headerEnd(meta);
|
||||
}
|
||||
|
||||
/** The GEP index where a class's own fields start: after the header words
|
||||
* and the slot word. */
|
||||
export function classFieldBase(meta: LlClassMeta): number {
|
||||
return headerEnd(meta) + (meta.threadSlot ? 1 : 0);
|
||||
}
|
||||
|
||||
/** Append the lexical environment after inherited fields to preserve base layouts. */
|
||||
export function classEnvironmentIndex(meta: LlClassMeta): number {
|
||||
return classFieldIndex(meta, `%classEnvironment:${meta.def.name}`).index;
|
||||
@@ -346,7 +373,28 @@ export function classFieldIndex(meta: LlClassMeta, field: string): { index: numb
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`,
|
||||
);
|
||||
return { index: fieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
return { index: classFieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
}
|
||||
|
||||
/** Lines that release the ThreadSlots values of the object `%o` before it
|
||||
* is freed: its slot word is 0 unless a value was ever set for it. */
|
||||
function slotForgetLines(
|
||||
host: ClassHost,
|
||||
struct: string,
|
||||
meta: LlClassMeta,
|
||||
tag: string,
|
||||
): string[] {
|
||||
host.declare(`declare void @scr_slots_forget(i32)`);
|
||||
return [
|
||||
` %${tag}p = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${classSlotWordIndex(meta)}`,
|
||||
` %${tag} = load i32, ptr %${tag}p, align 4`,
|
||||
` %${tag}.set = icmp ne i32 %${tag}, 0`,
|
||||
` br i1 %${tag}.set, label %${tag}.forget, label %${tag}.done`,
|
||||
`${tag}.forget: ; release its ThreadSlots values`,
|
||||
` call void @scr_slots_forget(i32 %${tag})`,
|
||||
` br label %${tag}.done`,
|
||||
`${tag}.done:`,
|
||||
];
|
||||
}
|
||||
|
||||
/** What class emission needs beyond ShapeHost: interned unit instances for
|
||||
@@ -483,7 +531,7 @@ export function emitClassShapes(
|
||||
? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const members = [...prefix, ...fieldTys];
|
||||
const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys];
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
|
||||
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
|
||||
@@ -569,7 +617,7 @@ export function emitClassShapes(
|
||||
const traced = host.tracedShapes.has(`object:${cls.name}`);
|
||||
const runtimePrefix = runtimePrefixOf(meta);
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
const fieldIndex = (i: number): number => fieldBase(meta) + i;
|
||||
const fieldIndex = (i: number): number => classFieldBase(meta) + i;
|
||||
// Teardown, trace and gcFree see each slot's storage type: a nullable
|
||||
// class field holds the instance pointer (NULL for the unit arm), and
|
||||
// every class retain/release/visit entry point is NULL-tolerant.
|
||||
@@ -640,6 +688,7 @@ export function emitClassShapes(
|
||||
});
|
||||
if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, ""));
|
||||
if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", ""));
|
||||
if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw"));
|
||||
lines.push(...emitObjectFree(host, traced));
|
||||
};
|
||||
|
||||
@@ -803,6 +852,11 @@ export function emitClassShapes(
|
||||
);
|
||||
}
|
||||
nw.push(...undefFieldInits(host, meta, nullable));
|
||||
// Young instances: program-owned traced classes. Classes rooted at a
|
||||
// runtime base (errors, emitters, events, streams) carry runtime state
|
||||
// and stay with trial deletion, like every runtime family.
|
||||
if (traced && !lifetime && (!meta.hierarchy || meta.root.def.runtime !== true))
|
||||
nw.push(...youngEntryLines(host));
|
||||
nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
|
||||
@@ -841,6 +895,7 @@ export function emitClassShapes(
|
||||
` call void ${releaseSym(host, f.type)}(ptr %v${i}) ; ${llvmCommentText(f.name)} (acyclic)`,
|
||||
);
|
||||
});
|
||||
if (meta.threadSlot) gf.push(...slotForgetLines(host, struct, meta, "gfw"));
|
||||
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
@@ -878,14 +933,32 @@ export function emitClassObjDefs(
|
||||
`llvm emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`,
|
||||
);
|
||||
}
|
||||
const ctor = fnByName.get(`%${className}.constructor`);
|
||||
const valueCtor = classValueConstructor(meta.def);
|
||||
const ctor = fnByName.get(valueCtor.fn);
|
||||
if (!ctor)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: class object for ${className} without a constructor`,
|
||||
);
|
||||
const params = ctor.params.slice(1);
|
||||
const params = ctor.params.slice(valueCtor.firstArg);
|
||||
const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", ");
|
||||
const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`);
|
||||
const classObj =
|
||||
`@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` +
|
||||
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`;
|
||||
if (meta.def.runtime) {
|
||||
// A runtime-provided class: %C.construct allocates through the
|
||||
// runtime and returns the instance.
|
||||
out.push(
|
||||
`define internal ptr @${mangleCtorThunk(className)}(ptr %class${paramDecls ? ", " + paramDecls : ""}) ${FN_ATTRS} { ; construct thunk ${className}`,
|
||||
`entry:`,
|
||||
` %o = call ptr @${mangleFunction(valueCtor.fn)}(${ctorArgs.join(", ")})`,
|
||||
` ret ptr %o`,
|
||||
`}`,
|
||||
classObj,
|
||||
``,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (meta.def.localCaptures !== undefined)
|
||||
host.declare(`declare ptr @scr_classobj_retain_v(ptr)`);
|
||||
const localBaseInits: string[] = [];
|
||||
@@ -944,8 +1017,7 @@ export function emitClassObjDefs(
|
||||
` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`,
|
||||
` ret ptr %o`,
|
||||
`}`,
|
||||
`@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` +
|
||||
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? 0}, ptr null, ptr null } ; class ${className}`,
|
||||
classObj,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ export function emitDenseSlotUnlink(
|
||||
B.condBr(same, swap, done);
|
||||
B.startBlock(swap);
|
||||
B.line(`store ptr ${placeholder}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
host.releaseValue(current, element);
|
||||
host.releaseHeap(current, element);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
}
|
||||
@@ -315,7 +315,7 @@ export function emitDenseArrayGet(
|
||||
fastLabel = B.newLabel("arr.get.ref");
|
||||
B.condBr(nonnull, fastLabel, slow);
|
||||
B.startBlock(fastLabel);
|
||||
fast = retain ? host.retainValue(raw, element) : raw;
|
||||
fast = retain ? host.retainFrame(raw, element) : raw;
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
@@ -493,7 +493,7 @@ export function emitDenseArraySet(
|
||||
const releaseLabel = B.newLabel("arr.set.release");
|
||||
B.condBr(hadValue, releaseLabel, join);
|
||||
B.startBlock(releaseLabel);
|
||||
host.releaseValue(old, element);
|
||||
host.releaseHeap(old, element);
|
||||
}
|
||||
B.br(join);
|
||||
// Below the bound the slot holds a present value: replace it, leaving
|
||||
@@ -508,7 +508,7 @@ export function emitDenseArraySet(
|
||||
);
|
||||
}
|
||||
storeValue(filledValuePtr);
|
||||
if (filledOld) host.releaseValue(filledOld, element);
|
||||
if (filledOld) host.releaseHeap(filledOld, element);
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
call(" cold");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN_HANDLE_KINDS } from "../../ir/ir.js";
|
||||
import { DYN_HANDLE_TAG_NUM } from "./dyn.js";
|
||||
|
||||
// ScrDynHandleTag in scr_dyn.h lists its tags by name, each with an explicit
|
||||
// value, so changes that add tags edit different lines. These checks stand
|
||||
// in for what the enum's order used to guarantee.
|
||||
const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8");
|
||||
const body = /typedef enum \{\n([\s\S]*?)\} ScrDynHandleTag;/.exec(header)![1]!;
|
||||
const tags = [...body.matchAll(/^\s*(SCR_DYNH_[A-Z0-9_]+) = (\d+),/gm)].map(([, name, value]) => ({
|
||||
name: name!,
|
||||
value: Number(value),
|
||||
}));
|
||||
const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]);
|
||||
|
||||
test("every ScrDynHandleTag has an explicit value, in name order", () => {
|
||||
const lines = body.split("\n").filter((line) => line.trim().startsWith("SCR_DYNH_"));
|
||||
expect(tags.length).toBe(lines.length);
|
||||
expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted());
|
||||
});
|
||||
|
||||
test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => {
|
||||
const owners = new Map<number, string>();
|
||||
for (const { name, value } of tags) {
|
||||
expect(owners.get(value), `${name} = ${value}`).toBeUndefined();
|
||||
owners.set(value, name);
|
||||
expect(value, name).toBeLessThan(capacity);
|
||||
}
|
||||
});
|
||||
|
||||
test("the backend's handle tag numbers match scr_dyn.h", () => {
|
||||
const values = new Map(tags.map((tag) => [tag.name, tag.value]));
|
||||
for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) {
|
||||
const tag = DYN_HANDLE_KINDS.get(kind)?.tag;
|
||||
expect(tag, kind).toBeDefined();
|
||||
expect(values.get(tag!), `${kind} (${tag})`).toBe(num);
|
||||
}
|
||||
});
|
||||
@@ -72,8 +72,8 @@ export function recordDynKeepsIdentity(shape: IrRecordShape): boolean {
|
||||
}
|
||||
|
||||
/** ScrDynKind values (scr_runtime.h). */
|
||||
/** ScrDynHandleTag numeric values (scr_runtime.h's enum order). */
|
||||
const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
/** ScrDynHandleTag values (scr_dyn.h's explicit values; dyn-handle-tags.test.ts checks them). */
|
||||
export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
httpReq: 0,
|
||||
httpRes: 1,
|
||||
netSocket: 2,
|
||||
@@ -84,6 +84,16 @@ const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
child: 16,
|
||||
fileHandle: 17,
|
||||
dir: 44,
|
||||
cryptoHash: 52,
|
||||
cryptoHmac: 53,
|
||||
};
|
||||
|
||||
/** Handle kinds whose runtime unit boxes them through its own function,
|
||||
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
||||
* never box one of these values do not link its dynamic members. */
|
||||
const DYN_HANDLE_BOX_FN: Record<string, string> = {
|
||||
cryptoHash: "scr_crypto_hash_to_dyn",
|
||||
cryptoHmac: "scr_crypto_hash_to_dyn",
|
||||
};
|
||||
|
||||
export const DYN_KIND = {
|
||||
@@ -1106,6 +1116,10 @@ export class LlDyn {
|
||||
private readonly jsonSchemas = new Map<string, string>();
|
||||
private readonly jsonParsers = new Map<string, string>();
|
||||
private readonly recordNameTables = new Map<string, { names: string; lens: string }>();
|
||||
/** Some emitted helper can keep a record's undeclared source: a checked
|
||||
* record builder that notes it, or a JSON schema of a closed record
|
||||
* (alloc.ts RECORD_SOURCES_GATE). */
|
||||
keepsRecordSources = false;
|
||||
|
||||
/** Targets the one-pass parser (scr_json_parse_schema) can build
|
||||
* directly: number, boolean and string leaves inside arrays and records
|
||||
@@ -1221,6 +1235,7 @@ export class LlDyn {
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
const field = shape.fields[index.get(fieldName)! - base]!;
|
||||
@@ -1981,6 +1996,7 @@ export class LlDyn {
|
||||
// that has them stays beside the record so its reflective
|
||||
// surfaces still observe them (scr_record_source_note).
|
||||
if (recordMayKeepSource(shape)) {
|
||||
this.keepsRecordSources = true;
|
||||
const names = this.recordNameTable(t.shapeId, shape);
|
||||
host.declare(
|
||||
`declare void @scr_record_source_note(ptr, ptr, ptr, ptr, ${host.sizeType})`,
|
||||
@@ -2945,9 +2961,17 @@ export class LlDyn {
|
||||
// tag's installed ops.
|
||||
const h = DYN_HANDLE_KINDS.get(t.kind);
|
||||
if (h) {
|
||||
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
||||
const box = DYN_HANDLE_BOX_FN[t.kind];
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`);
|
||||
if (box) {
|
||||
host.declare(`declare ptr @${box}(ptr)`);
|
||||
B.line(`${r} = call ptr @${box}(ptr %v)`);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
@@ -3990,14 +4014,9 @@ export class LlDyn {
|
||||
const name = "sc_dyn_iter_n";
|
||||
this.dynBuilders.set(memoKey, name);
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
||||
host.declare(`declare void @scr_jb_puts(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_throw_error(i32, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
||||
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
||||
host.declare(`declare ptr @scr_f64_to_scrstr(double)`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const B = new BlockBuilder();
|
||||
const kd = this.kindOf(B, "%d");
|
||||
@@ -4009,6 +4028,21 @@ export class LlDyn {
|
||||
const lNext = B.newLabel("din.nt");
|
||||
B.condBr(isTyped, lTyped, lNext);
|
||||
B.startBlock(lTyped);
|
||||
// Compiled generators step through the iterator protocol.
|
||||
host.declare(`declare i1 @scr_dyn_iterates_by_protocol(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`);
|
||||
const protocol = B.tmp();
|
||||
const lProtocol = B.newLabel("din.pr");
|
||||
const lView = B.newLabel("din.vw");
|
||||
B.line(`${protocol} = call i1 @scr_dyn_iterates_by_protocol(ptr %d)`);
|
||||
B.condBr(protocol, lProtocol, lView);
|
||||
B.startBlock(lProtocol);
|
||||
const stepped = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`,
|
||||
);
|
||||
B.terminate(`ret ptr ${stepped}`);
|
||||
B.startBlock(lView);
|
||||
host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const materialized = B.tmp();
|
||||
@@ -4033,71 +4067,13 @@ export class LlDyn {
|
||||
const lGo = B.newLabel("din.g");
|
||||
const lThrow = B.newLabel("din.t");
|
||||
B.condBr(ok, lGo, lThrow);
|
||||
// V8's exact not-iterable TypeError.
|
||||
// Other iterables step through the iterator protocol; non-iterables
|
||||
// throw V8's exact not-iterable TypeError.
|
||||
B.startBlock(lThrow);
|
||||
const buf = "%dib";
|
||||
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`);
|
||||
B.line(`call void @scr_jb_init(ptr ${buf})`);
|
||||
const hasSpelling = B.tmp();
|
||||
B.line(`${hasSpelling} = icmp ne ptr %spelling, null`);
|
||||
const named = B.newLabel("din.named"),
|
||||
unnamed = B.newLabel("din.unnamed");
|
||||
B.condBr(hasSpelling, named, unnamed);
|
||||
B.startBlock(named);
|
||||
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spelling)`);
|
||||
const namedMessage = B.tmp();
|
||||
B.line(`${namedMessage} = call ptr @scr_jb_finish(ptr ${buf})`);
|
||||
B.line(`call void @scr_throw_error(i32 1, ptr ${namedMessage})`);
|
||||
B.terminate(`ret ptr null`);
|
||||
B.startBlock(unnamed);
|
||||
const lU = B.newLabel("din.u");
|
||||
const lN = B.newLabel("din.n");
|
||||
const lB2 = B.newLabel("din.b");
|
||||
const lNum = B.newLabel("din.m");
|
||||
const lF = B.newLabel("din.f");
|
||||
const lDef = B.newLabel("din.d");
|
||||
const lTail = B.newLabel("din.e");
|
||||
B.terminate(
|
||||
`switch i32 ${kd}, label %${lDef} [ i32 ${DYN_KIND.UNDEF}, label %${lU} i32 ${DYN_KIND.NULL}, label %${lN} i32 ${DYN_KIND.BOOL}, label %${lB2} i32 ${DYN_KIND.NUM}, label %${lNum} i32 ${DYN_KIND.FUNC}, label %${lF} ]`,
|
||||
);
|
||||
B.startBlock(lU);
|
||||
this.puts(B, buf, "undefined");
|
||||
B.br(lTail);
|
||||
B.startBlock(lN);
|
||||
this.puts(B, buf, "object null");
|
||||
B.br(lTail);
|
||||
B.startBlock(lB2);
|
||||
{
|
||||
const bv = this.boolOf(B, "%d");
|
||||
const s = B.tmp();
|
||||
B.line(
|
||||
`${s} = select i1 ${bv}, ptr ${host.cstr("boolean true")}, ptr ${host.cstr("boolean false")}`,
|
||||
);
|
||||
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr ${s})`);
|
||||
B.br(lTail);
|
||||
}
|
||||
B.startBlock(lNum);
|
||||
{
|
||||
this.puts(B, buf, "number ");
|
||||
const x = this.payloadOf(B, "%d", "double");
|
||||
const s = B.tmp();
|
||||
B.line(`${s} = call ptr @scr_f64_to_scrstr(double ${x})`);
|
||||
this.putScrStr(B, buf, s);
|
||||
B.line(`call void @scr_str_release(ptr ${s})`);
|
||||
B.br(lTail);
|
||||
}
|
||||
B.startBlock(lF);
|
||||
this.puts(B, buf, "function");
|
||||
B.br(lTail);
|
||||
B.startBlock(lDef);
|
||||
this.puts(B, buf, "object");
|
||||
B.br(lTail);
|
||||
B.startBlock(lTail);
|
||||
this.puts(B, buf, " is not iterable (cannot read property Symbol(Symbol.iterator))");
|
||||
const msg = B.tmp();
|
||||
B.line(`${msg} = call ptr @scr_jb_finish(ptr ${buf})`);
|
||||
B.line(`call void @scr_throw_error(i32 1, ptr ${msg}) ; SCR_ERR_TYPE`);
|
||||
B.terminate(`ret ptr null`);
|
||||
host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`);
|
||||
const taken = B.tmp();
|
||||
B.line(`${taken} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`);
|
||||
B.terminate(`ret ptr ${taken}`);
|
||||
B.startBlock(lGo);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { emitThreadSlotsIntrinsic, threadSlotsBlockDefinitions } from "./thread-slots.js";
|
||||
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
|
||||
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
CYC_BUF,
|
||||
CYC_HDR,
|
||||
FIRST_EMITTED_CYC_KIND,
|
||||
inlineAllocSupported,
|
||||
RECORD_SOURCES_GATE,
|
||||
WEAK_KEYS_GATE,
|
||||
} from "./alloc.js";
|
||||
import { youngNurseryRequested } from "../young-nursery.js";
|
||||
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -48,8 +57,10 @@ import {
|
||||
typeKey,
|
||||
STRING,
|
||||
VOID,
|
||||
classJsName,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import {
|
||||
matchIntegerArrayForLoop,
|
||||
matchIntegerBytesForLoop,
|
||||
@@ -107,6 +118,7 @@ import {
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
@@ -158,6 +170,7 @@ import {
|
||||
mangleFnClosure,
|
||||
mangleFunction,
|
||||
mangleGenDrop,
|
||||
mangleGenSourceClose,
|
||||
mangleGenSpawn,
|
||||
mangleGlobal,
|
||||
mangleLocal,
|
||||
@@ -505,7 +518,9 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
@@ -533,7 +548,9 @@ export function emitLlvmModuleSource(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
@@ -900,6 +917,8 @@ export class LlEmitter {
|
||||
/** Constant-key reads on checked-dynamic objects, each with its own
|
||||
* zero-initialized ScrDynReadIc (scr_runtime.h). */
|
||||
private dynReadIcSites = 0;
|
||||
/** ThreadSlots blocks by family name (thread-slots.ts). */
|
||||
private readonly threadSlotsBlocks = new Map<string, string>();
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
* checks are emitted only after calls that can actually raise. */
|
||||
readonly mayThrow: Set<string>;
|
||||
@@ -951,6 +970,14 @@ export class LlEmitter {
|
||||
/** The program calls mock.timers members (moduleUsesMockTimers): timer
|
||||
* calls emit scr_test.c's mock-aware entry points. */
|
||||
readonly mockTimers: boolean;
|
||||
private schedulesInContextValue: boolean | undefined = undefined;
|
||||
/** Whether timer and tick scheduling goes through the *_als entry
|
||||
* points (RuntimeFeatures asyncContext). */
|
||||
get schedulesInContext(): boolean {
|
||||
if (this.schedulesInContextValue === undefined)
|
||||
this.schedulesInContextValue = moduleRuntimeFeatures(this.mod).asyncContext;
|
||||
return this.schedulesInContextValue;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunks for new Promise, interned per inner
|
||||
* typeKey → thunk symbol. */
|
||||
readonly resolveThunks = new Map<string, string>();
|
||||
@@ -1172,18 +1199,14 @@ export class LlEmitter {
|
||||
readonly chainSlots = new Map<string, LlValue>();
|
||||
logArgSlots = 0;
|
||||
private readonly stackTraces: boolean;
|
||||
/** Stack-trace mode: functions no capture can include (no frame push). */
|
||||
private readonly unframedFunctions: ReadonlySet<string>;
|
||||
|
||||
constructor(
|
||||
readonly mod: IrModule,
|
||||
options: LlvmTargetOptions,
|
||||
) {
|
||||
this.stackTraces = mod.functions.some(
|
||||
(fn) =>
|
||||
!everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
|
||||
}),
|
||||
);
|
||||
this.stackTraces = observesStacks(mod);
|
||||
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
|
||||
this.mockTimers = moduleUsesMockTimers(mod);
|
||||
this.debug =
|
||||
@@ -1265,6 +1288,9 @@ export class LlEmitter {
|
||||
// which Windows lacks.
|
||||
this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
|
||||
this.publishedTypes = computePublishedTypes(mod);
|
||||
this.unframedFunctions = this.stackTraces
|
||||
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
|
||||
: new Set();
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
this.indirectMayThrow = mt.indirect || mod.workers === true;
|
||||
@@ -1438,6 +1464,7 @@ export class LlEmitter {
|
||||
...(this.rcMode !== undefined ? { rcMode: this.rcMode } : {}),
|
||||
...(this.rcHelpersNoinline ? { rcHelpersNoinline: true } : {}),
|
||||
nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++,
|
||||
...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}),
|
||||
unionsById: this.unionsById,
|
||||
declare: (decl) => this.declare(decl),
|
||||
needOom: () => this.needOom(),
|
||||
@@ -1964,7 +1991,10 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
emitParts(): string[] {
|
||||
const parts = limitInlineDynReleases(this.emitModuleParts());
|
||||
const parts = [
|
||||
...limitInlineDynReleases(this.emitModuleParts()),
|
||||
...this.disposeObserverGates(),
|
||||
];
|
||||
if (!this.executableTls) return parts;
|
||||
// A worker executable's thread-locals all live in the executable's own
|
||||
// TLS block: the program's internal ones, and the runtime's external
|
||||
@@ -1987,6 +2017,21 @@ export class LlEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
* (alloc.ts): defined after every helper is emitted, because dynCheck
|
||||
* builders, JSON schemas and weak collection constructors are requested
|
||||
* lazily. */
|
||||
private disposeObserverGates(): string[] {
|
||||
const weak = [...this.decls].some(
|
||||
(d) => d.includes("@scr_weak_map_new(") || d.includes("@scr_weak_set_new("),
|
||||
);
|
||||
return [
|
||||
`${WEAK_KEYS_GATE} = internal constant i1 ${weak}`,
|
||||
`${RECORD_SOURCES_GATE} = internal constant i1 ${this.dyn.keepsRecordSources}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
private emitModuleParts(): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
@@ -2476,6 +2521,7 @@ export class LlEmitter {
|
||||
{
|
||||
const tl =
|
||||
this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
|
||||
out.push(...threadSlotsBlockDefinitions(this.threadSlotsBlocks, tl !== ""));
|
||||
for (let site = 0; site < this.dynReadIcSites; site++)
|
||||
out.push(
|
||||
`@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${this.sizeType}, ${this.sizeType}, ptr } zeroinitializer`,
|
||||
@@ -4598,6 +4644,28 @@ export class LlEmitter {
|
||||
dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``);
|
||||
for (const line of dr) out.push(line);
|
||||
|
||||
// An iterator helper's return() before its first next() closes the
|
||||
// source generator in its first argument slot.
|
||||
const closesSource = fn.generator.closesSource === true;
|
||||
if (fn.generator.tag !== undefined) this.declare(`declare void @scr_gen_set_tag(ptr, ptr)`);
|
||||
if (fn.generator.inheritsTag === true)
|
||||
this.declare(`declare void @scr_gen_copy_tag(ptr, ptr)`);
|
||||
if (closesSource) {
|
||||
const field = lifted ? 1 : 0;
|
||||
this.declare(`declare void @scr_gen_close(ptr)`);
|
||||
this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`);
|
||||
out.push(
|
||||
`define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`,
|
||||
` %sv = load ptr, ptr %sp`,
|
||||
` call void @scr_gen_close(ptr %sv)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn wrapper: pack the args (+1 moves in), allocate the
|
||||
// SUSPENDED fiber — nothing runs until the first .next().
|
||||
if (lifted) {
|
||||
@@ -4614,6 +4682,17 @@ export class LlEmitter {
|
||||
settleAsync === null
|
||||
? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`
|
||||
: ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`,
|
||||
...(fn.generator.tag !== undefined
|
||||
? [` call void @scr_gen_set_tag(ptr %gg, ptr ${this.cstr(fn.generator.tag)})`]
|
||||
: []),
|
||||
...(fn.generator.inheritsTag === true
|
||||
? [` call void @scr_gen_copy_tag(ptr %gg, ptr %a${lifted ? 1 : 0})`]
|
||||
: []),
|
||||
...(closesSource
|
||||
? [
|
||||
` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`,
|
||||
]
|
||||
: []),
|
||||
` ret ptr %gg`,
|
||||
`}`,
|
||||
``,
|
||||
@@ -4886,8 +4965,8 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Strike a refcounted temp from its frame: ownership is being moved. */
|
||||
moveTemp(v: LlValue): void {
|
||||
/** Strike a refcounted temp from its frame. */
|
||||
private disownTemp(v: LlValue): void {
|
||||
if (!isRefCounted(v.type) || this.isImmortal(v.name)) return;
|
||||
for (let i = this.frames.length - 1; i >= 0; i--) {
|
||||
const idx = this.frames[i]!.findIndex((e) => e.name === v.name);
|
||||
@@ -4901,23 +4980,69 @@ export class LlEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/* Ownership transfers name their destination. A heap reference is one
|
||||
* that heap memory holds: a field, an array or map element, a capture
|
||||
* box, a union box, a module global, a promise or generator slot, the
|
||||
* exception cell, or any other runtime structure. A frame reference is
|
||||
* one that a local, a parameter, a return value, a stack object or a
|
||||
* temporary holds. Both forms of each operation emit the same code. */
|
||||
|
||||
/** Ends a temp's frame ownership: its reference moves into heap memory. */
|
||||
toHeap(v: LlValue): void {
|
||||
this.disownTemp(v);
|
||||
}
|
||||
|
||||
/** Ends a temp's frame ownership: its reference moves to another frame
|
||||
* owner (a local or parameter slot, a callee's parameter, a return
|
||||
* value, a stack object, another temp). */
|
||||
toFrame(v: LlValue): void {
|
||||
this.disownTemp(v);
|
||||
}
|
||||
|
||||
/** The retained (+1) read of a refcounted value — type-directed through
|
||||
* the `_v` table (immortals skip, exactly the C retain calls). */
|
||||
retainValue(name: string, type: IrType): string {
|
||||
private retainCall(name: string, type: IrType): string {
|
||||
if (this.isImmortal(name)) return name;
|
||||
const t = this.B.tmp();
|
||||
this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** A +1 of `name` that heap memory keeps. */
|
||||
retainHeap(name: string, type: IrType): string {
|
||||
return this.retainCall(name, type);
|
||||
}
|
||||
|
||||
/** A +1 of `name` that a frame owns. */
|
||||
retainFrame(name: string, type: IrType): string {
|
||||
return this.retainCall(name, type);
|
||||
}
|
||||
|
||||
/** The release call for one owned refcounted value — type-directed like
|
||||
* releaseCallC (all runtime releases are NULL-tolerant). */
|
||||
releaseValue(name: string, type: IrType): void {
|
||||
private releaseCall(name: string, type: IrType): void {
|
||||
if (this.isImmortal(name)) return;
|
||||
this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`);
|
||||
}
|
||||
|
||||
releaseFrame(frame: LlValue[]): void {
|
||||
/** Releases a reference that heap memory held. */
|
||||
releaseHeap(name: string, type: IrType): void {
|
||||
this.releaseCall(name, type);
|
||||
}
|
||||
|
||||
/** Releases a reference that a frame owned. */
|
||||
releaseFrame(name: string, type: IrType): void {
|
||||
this.releaseCall(name, type);
|
||||
}
|
||||
|
||||
/** A +1 that the runtime made with the heap retain (a box, generator or
|
||||
* await read) becomes a frame value. Emits nothing; answers `name`. */
|
||||
adoptHeap(name: string, _type: IrType): string {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Releases a statement frame's temps and owned slots. */
|
||||
releaseTemps(frame: LlValue[]): void {
|
||||
for (const v of frame) {
|
||||
if (v.slot) {
|
||||
const t = this.B.tmp();
|
||||
@@ -4925,10 +5050,10 @@ export class LlEmitter {
|
||||
if (v.boxed) {
|
||||
this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
|
||||
} else {
|
||||
this.releaseValue(t, v.type);
|
||||
this.releaseFrame(t, v.type);
|
||||
}
|
||||
} else {
|
||||
this.releaseValue(v.name, v.type);
|
||||
this.releaseFrame(v.name, v.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4953,7 +5078,7 @@ export class LlEmitter {
|
||||
} else if (e.boxed) {
|
||||
this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`);
|
||||
} else {
|
||||
this.releaseValue(t, e.type); // runtime releases are NULL-tolerant
|
||||
this.releaseFrame(t, e.type); // runtime releases are NULL-tolerant
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4963,7 +5088,7 @@ export class LlEmitter {
|
||||
* everything whose normal fall-through releases the jump bypasses.
|
||||
* Release every scope crossed by the control-flow edge. */
|
||||
private releaseForJump(frameDepth: number, scopeDepth: number): void {
|
||||
for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!);
|
||||
for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!);
|
||||
for (let i = this.scopes.length - 1; i >= scopeDepth; i--) this.releaseScope(this.scopes[i]!);
|
||||
}
|
||||
|
||||
@@ -5036,7 +5161,7 @@ export class LlEmitter {
|
||||
const target = this.tryStack[this.tryStack.length - 1];
|
||||
const frameDepth = target?.frameDepth ?? 0;
|
||||
const scopeDepth = target?.scopeDepth ?? 0;
|
||||
for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!);
|
||||
for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!);
|
||||
let terminator: string;
|
||||
if (target) {
|
||||
target.used = true;
|
||||
@@ -5566,7 +5691,7 @@ export class LlEmitter {
|
||||
if (target.value !== null) {
|
||||
this.frames.push([]);
|
||||
for (const read of speculativeReads(target.value)) reads.set(read, this.emitExpr(read));
|
||||
this.releaseFrame(this.frames.pop()!);
|
||||
this.releaseTemps(this.frames.pop()!);
|
||||
}
|
||||
this.frames.push([]);
|
||||
let result: string;
|
||||
@@ -5890,7 +6015,7 @@ export class LlEmitter {
|
||||
unionExtract(uName: string, unionId: string, arm: IrType): string {
|
||||
if (arm.kind === "f64" || arm.kind === "procStream") return this.unionGetF64(uName);
|
||||
if (arm.kind === "bool") return this.unionGetBool(uName);
|
||||
return this.retainValue(this.unionPeek(uName, unionId), arm);
|
||||
return this.retainFrame(this.unionPeek(uName, unionId), arm);
|
||||
}
|
||||
|
||||
/** A scalar f64 arm's payload (scr_union_get_f64 inlined). Inline loads
|
||||
@@ -6001,7 +6126,7 @@ export class LlEmitter {
|
||||
classObjSym(className: string): string {
|
||||
if (!this.classObjs.has(className)) {
|
||||
const meta = this.classMetaOf(className);
|
||||
this.classObjs.set(className, { nameSym: this.internLiteral(meta.def.jsName ?? "") });
|
||||
this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) });
|
||||
}
|
||||
return mangleClassObj(className);
|
||||
}
|
||||
@@ -6132,11 +6257,11 @@ export class LlEmitter {
|
||||
* in, and the old value is released AFTER the field is overwritten. */
|
||||
replaceField(ptr: string, type: IrType, inline: boolean, v: LlValue): void {
|
||||
if (!inline && isRefCounted(type)) {
|
||||
this.moveTemp(v);
|
||||
this.toHeap(v);
|
||||
const old = this.B.tmp();
|
||||
this.B.line(`${old} = load ptr, ptr ${ptr}`);
|
||||
this.storeField(ptr, type, v.name, false);
|
||||
this.releaseValue(old, type);
|
||||
this.releaseHeap(old, type);
|
||||
return;
|
||||
}
|
||||
this.storeField(ptr, type, v.name, inline);
|
||||
@@ -6144,7 +6269,7 @@ export class LlEmitter {
|
||||
|
||||
/** Stores `v` (from emitFieldValue) into a fresh field slot. */
|
||||
initField(ptr: string, type: IrType, inline: boolean, v: LlValue): void {
|
||||
if (!inline && isRefCounted(type)) this.moveTemp(v);
|
||||
if (!inline && isRefCounted(type)) this.toHeap(v);
|
||||
this.storeField(ptr, type, v.name, inline);
|
||||
}
|
||||
|
||||
@@ -6310,7 +6435,7 @@ export class LlEmitter {
|
||||
if (!stack) B.line(`${box} = ${boxNewCall(this.shapeHost, type)}`);
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = load ${this.llType(type)}, ptr %${mangleLocal(localId)}`);
|
||||
this.boxSet(box, type, isRefCounted(type) ? this.retainValue(value, type) : value);
|
||||
this.boxSet(box, type, isRefCounted(type) ? this.retainHeap(value, type) : value);
|
||||
B.line(`store ptr ${box}, ptr ${cache}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
@@ -6456,7 +6581,7 @@ export class LlEmitter {
|
||||
this.boxSet(
|
||||
box,
|
||||
local.type,
|
||||
borrowed && isRefCounted(local.type) ? this.retainValue(value, local.type) : value,
|
||||
borrowed && isRefCounted(local.type) ? this.retainHeap(value, local.type) : value,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7125,13 +7250,39 @@ export class LlEmitter {
|
||||
// Stackful native fibers and native coroutine frames retain these frames
|
||||
// while suspended. The active exception context owns the chain, so
|
||||
// concurrent fibers isolate it.
|
||||
if (this.stackTraces && !this.wasi && fn.sourceName !== undefined) {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
if (
|
||||
this.stackTraces &&
|
||||
!this.wasi &&
|
||||
fn.sourceName !== undefined &&
|
||||
!this.unframedFunctions.has(fn.name)
|
||||
) {
|
||||
B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`);
|
||||
const frame = ` at ${fn.sourceName} (${fn.loc.file})`;
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
const frame = this.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
|
||||
if (
|
||||
B.excCellInvariant &&
|
||||
this.mod.lib === undefined &&
|
||||
this.mod.workers !== true &&
|
||||
this.sizeType === "i64"
|
||||
) {
|
||||
// Executables link the active-cell pointer as a plain global, and a
|
||||
// synchronous body sees one cell from entry to every return: push
|
||||
// and pop the frame inline (ScrExcCell.stack is at byte 56 on
|
||||
// 64-bit targets) instead of calling the runtime twice per call.
|
||||
this.declare(`@scr_exc_active = external global ptr`);
|
||||
B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`);
|
||||
B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`);
|
||||
B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`);
|
||||
B.line(`store ptr %source_frame.previous, ptr %source_frame`);
|
||||
B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`);
|
||||
B.line(`store ptr ${frame}, ptr %source_frame.text`);
|
||||
B.line(`store ptr %source_frame, ptr %source_frame.chain`);
|
||||
B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`;
|
||||
} else {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
}
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true);
|
||||
@@ -7455,14 +7606,14 @@ export class LlEmitter {
|
||||
B.line(`${present} = icmp ne ptr %result, null`);
|
||||
B.condBr(present, own, done);
|
||||
B.startBlock(own);
|
||||
B.line(`store ptr ${this.retainValue("%result", fn.returnType)}, ptr ${slot}`);
|
||||
B.line(`store ptr ${this.retainFrame("%result", fn.returnType)}, ptr ${slot}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
result = B.tmp();
|
||||
B.line(`${result} = load ptr, ptr ${slot}`);
|
||||
}
|
||||
for (const index of borrowed)
|
||||
if (!kept.has(index)) this.releaseValue(`%p${index}`, fn.params[index]!.type);
|
||||
if (!kept.has(index)) this.releaseFrame(`%p${index}`, fn.params[index]!.type);
|
||||
B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
|
||||
const symbol =
|
||||
kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
|
||||
@@ -7623,7 +7774,7 @@ export class LlEmitter {
|
||||
private emitAdoptingReturn(s: IrStmt & { kind: "return" }, adopted: ExprOf<"awaitExpr">): void {
|
||||
const B = this.B;
|
||||
const promise = this.emitExpr(adopted.value);
|
||||
this.moveTemp(promise);
|
||||
this.toFrame(promise);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr ; adopted return promise`);
|
||||
B.line(`store ptr ${promise.name}, ptr ${slot}`);
|
||||
@@ -7657,7 +7808,8 @@ export class LlEmitter {
|
||||
const v = this.emitExpr(s.value!);
|
||||
if (this.adoptedOperand !== null)
|
||||
throw new InternalCompilerError("llvm emitter bug: adopted return promise was not awaited");
|
||||
this.moveTemp(v);
|
||||
if (this.currentCoro !== null) this.toHeap(v);
|
||||
else this.toFrame(v);
|
||||
this.releaseForJump(0, 0);
|
||||
if (!B.isTerminated()) {
|
||||
if (this.currentCoro !== null) {
|
||||
@@ -8002,7 +8154,7 @@ export class LlEmitter {
|
||||
const union = emitStackUnion(this, localUnion);
|
||||
B.line(`store ptr ${union.value.name}, ptr ${b.slot}`);
|
||||
if (union.payload) {
|
||||
this.moveTemp(union.payload);
|
||||
this.toFrame(union.payload);
|
||||
this.scopes[this.scopes.length - 1]!.push({
|
||||
slot: union.payloadSlot,
|
||||
type: union.payload.type,
|
||||
@@ -8049,7 +8201,7 @@ export class LlEmitter {
|
||||
if (stack.owner) this.scopes[this.scopes.length - 1]!.push(stack.owner);
|
||||
if (s.init) {
|
||||
const value = this.emitExpr(s.init);
|
||||
if (isRefCounted(value.type)) this.moveTemp(value);
|
||||
if (isRefCounted(value.type)) this.toFrame(value);
|
||||
this.boxSet(stack.box, b.type, value.name);
|
||||
}
|
||||
break;
|
||||
@@ -8076,7 +8228,7 @@ export class LlEmitter {
|
||||
break;
|
||||
}
|
||||
const v = this.emitExpr(s.init);
|
||||
if (isRefCounted(v.type)) this.moveTemp(v); // the box takes ownership
|
||||
if (isRefCounted(v.type)) this.toHeap(v); // the box takes ownership
|
||||
if (b.local!.tdz === true && boxAccess(b.type) !== "ref") {
|
||||
this.tdzScalarInit(box, b.type, v.name);
|
||||
} else {
|
||||
@@ -8134,7 +8286,8 @@ export class LlEmitter {
|
||||
break;
|
||||
}
|
||||
const v = this.emitExpr(s.init);
|
||||
this.moveTemp(v);
|
||||
if (b.kind === "global") this.toHeap(v);
|
||||
else this.toFrame(v);
|
||||
B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`);
|
||||
this.storeIntegerView(s.localId, v, s.init);
|
||||
if (isRefCounted(b.type)) {
|
||||
@@ -8157,10 +8310,10 @@ export class LlEmitter {
|
||||
const param = this.binding(s.localId);
|
||||
const slot = param.slot;
|
||||
const v = this.emitExpr(s.value);
|
||||
this.moveTemp(v);
|
||||
this.toFrame(v);
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${owner}`);
|
||||
this.releaseValue(old, param.type);
|
||||
this.releaseFrame(old, param.type);
|
||||
B.line(`store ptr ${v.name}, ptr ${slot}`);
|
||||
B.line(`store ptr ${v.name}, ptr ${owner}`);
|
||||
break;
|
||||
@@ -8187,7 +8340,7 @@ export class LlEmitter {
|
||||
B.line(`${bits} = load ${struct}, ptr ${value.name}`);
|
||||
B.line(`store ${struct} ${bits}, ptr ${target}`);
|
||||
this.disownSlots(owned);
|
||||
for (const field of old) this.releaseValue(field.name, field.type);
|
||||
for (const field of old) this.releaseFrame(field.name, field.type);
|
||||
break;
|
||||
}
|
||||
const localStorage = this.localUnionStorage.get(s.localId);
|
||||
@@ -8211,14 +8364,16 @@ export class LlEmitter {
|
||||
if (b.kind === "boxed") {
|
||||
this.writeBindingBox(this.loadBox(b.slot), b.local!, v.name, s.initializes);
|
||||
// The RHS remains frame-owned until a possible TDZ throw passes.
|
||||
if (isRefCounted(v.type)) this.moveTemp(v);
|
||||
if (isRefCounted(v.type)) this.toHeap(v);
|
||||
break;
|
||||
}
|
||||
this.moveTemp(v);
|
||||
if (b.kind === "global") this.toHeap(v);
|
||||
else this.toFrame(v);
|
||||
if (isRefCounted(b.type)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${b.slot}`);
|
||||
this.releaseValue(old, b.type);
|
||||
if (b.kind === "global") this.releaseHeap(old, b.type);
|
||||
else this.releaseFrame(old, b.type);
|
||||
}
|
||||
B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`);
|
||||
this.storeIntegerView(s.localId, v, s.value);
|
||||
@@ -8269,7 +8424,7 @@ export class LlEmitter {
|
||||
throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") this.moveTemp(v);
|
||||
if (acc === "ref") this.toHeap(v);
|
||||
emitDenseArraySet(
|
||||
this,
|
||||
arr.name,
|
||||
@@ -8334,7 +8489,7 @@ export class LlEmitter {
|
||||
if (acc === "ref" && isRefCounted(elem)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
|
||||
this.releaseValue(old, elem);
|
||||
this.releaseHeap(old, elem);
|
||||
}
|
||||
B.br(done);
|
||||
B.startBlock(call);
|
||||
@@ -8484,7 +8639,7 @@ export class LlEmitter {
|
||||
const v = this.emitExpr(s.value);
|
||||
if (this.publishedTypes?.recordGuarded(s.shapeId) === true)
|
||||
this.emitPublishedFieldGuard(obj.name, null, "[key]");
|
||||
if (isRefCounted(v.type)) this.moveTemp(v);
|
||||
if (isRefCounted(v.type)) this.toHeap(v);
|
||||
const shape = this.recordShape(s.shapeId);
|
||||
// Signature-free shapes dispatch over their (one-typed) declared
|
||||
// fields and TRAP on a miss (scr_record_key_miss — JS would add
|
||||
@@ -8550,12 +8705,12 @@ export class LlEmitter {
|
||||
if (inline) {
|
||||
// The slot copies the checked value's bits; nv is released below.
|
||||
this.storeField(ptr, type, nv, true);
|
||||
this.releaseValue(nv, type);
|
||||
this.releaseFrame(nv, type);
|
||||
} else if (isRefCounted(type)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${ptr}`);
|
||||
this.storeField(ptr, type, nv, false);
|
||||
this.releaseValue(old, type);
|
||||
this.releaseHeap(old, type);
|
||||
} else {
|
||||
this.storeField(ptr, type, nv, false);
|
||||
}
|
||||
@@ -8585,12 +8740,12 @@ export class LlEmitter {
|
||||
if (inline) {
|
||||
// The write owns the moved-in value; the slot copies its bits.
|
||||
this.storeField(ptr, type, v.name, true);
|
||||
this.releaseValue(v.name, type);
|
||||
this.releaseHeap(v.name, type);
|
||||
} else if (isRefCounted(type)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${ptr}`);
|
||||
this.storeField(ptr, type, v.name, false);
|
||||
this.releaseValue(old, type);
|
||||
this.releaseHeap(old, type);
|
||||
} else {
|
||||
this.storeField(ptr, type, v.name, false);
|
||||
}
|
||||
@@ -8601,7 +8756,7 @@ export class LlEmitter {
|
||||
// The MISS on a fixed shape: release the moved-in value, throw
|
||||
// the catchable TypeError naming the key (scr_record_key_miss —
|
||||
// JS would add the property, the documented divergence).
|
||||
if (isRefCounted(iv)) this.releaseValue(v.name, iv);
|
||||
if (isRefCounted(iv)) this.releaseHeap(v.name, iv);
|
||||
this.declare(`declare void @scr_record_key_miss(ptr)`);
|
||||
B.line(`call void @scr_record_key_miss(ptr ${key.name})`);
|
||||
B.br(join);
|
||||
@@ -9244,7 +9399,8 @@ export class LlEmitter {
|
||||
} else v = this.emitReadReceiver(s.value);
|
||||
} else if (s.value !== null) {
|
||||
v = this.emitExpr(s.value);
|
||||
this.moveTemp(v);
|
||||
if (this.currentCoro !== null) this.toHeap(v);
|
||||
else this.toFrame(v);
|
||||
}
|
||||
// An async generator's `return v` awaits v before any crossed
|
||||
// finally runs. Past finally regions that turn is taken here and
|
||||
@@ -9313,7 +9469,7 @@ export class LlEmitter {
|
||||
// unwind unconditionally (the innermost try handler, or out of the
|
||||
// function) — the same release path as return/break/continue.
|
||||
const v = this.emitExpr(s.value);
|
||||
if (isRefCounted(s.value.type)) this.moveTemp(v); // the cell takes ownership
|
||||
if (isRefCounted(s.value.type)) this.toHeap(v); // the cell takes ownership
|
||||
this.emitThrowValue({ name: v.name, type: s.value.type });
|
||||
this.emitUnwind();
|
||||
break;
|
||||
@@ -9359,7 +9515,7 @@ export class LlEmitter {
|
||||
s.kind !== "rethrow" &&
|
||||
s.kind !== "runtimeFence"
|
||||
) {
|
||||
this.releaseFrame(frame);
|
||||
this.releaseTemps(frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9492,14 +9648,15 @@ export class LlEmitter {
|
||||
B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`);
|
||||
B.line(`store ptr ${stash}, ptr ${stashSlot}`);
|
||||
this.scopes.push([{ slot: stashSlot, type: CAUGHT }]);
|
||||
const suppressHandler = s.suppressFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
const suppressHandler =
|
||||
s.suppressFinallyErrors || s.dropFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
if (suppressHandler) this.tryStack.push(suppressHandler);
|
||||
this.emitBlock(s.finallyBody!);
|
||||
if (suppressHandler) this.tryStack.pop();
|
||||
@@ -9509,8 +9666,9 @@ export class LlEmitter {
|
||||
this.emitUnwind();
|
||||
if (suppressHandler?.used) {
|
||||
B.startBlock(suppressHandler.label);
|
||||
this.declare(`declare void @scr_exc_suppress(ptr)`);
|
||||
B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`);
|
||||
const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress";
|
||||
this.declare(`declare void @${cleanup}(ptr)`);
|
||||
B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`);
|
||||
this.emitUnwind();
|
||||
}
|
||||
}
|
||||
@@ -9569,7 +9727,7 @@ export class LlEmitter {
|
||||
} else {
|
||||
B.line(`${hit} = fcmp oeq double ${disc.name}, ${t.name}`);
|
||||
}
|
||||
this.releaseFrame(this.frames.pop()!);
|
||||
this.releaseTemps(this.frames.pop()!);
|
||||
const next = B.newLabel("sw.t");
|
||||
B.condBr(hit, caseLabels[i]!, next);
|
||||
B.startBlock(next);
|
||||
@@ -9610,7 +9768,7 @@ export class LlEmitter {
|
||||
private emitCondition(cond: IrExpr): string {
|
||||
const v = this.emitExpr(cond);
|
||||
const frame = this.currentFrame();
|
||||
this.releaseFrame(frame);
|
||||
this.releaseTemps(frame);
|
||||
frame.length = 0;
|
||||
return v.name;
|
||||
}
|
||||
@@ -9622,11 +9780,11 @@ export class LlEmitter {
|
||||
emitBranchInto(slot: string, expr: IrExpr, integerSlot?: string): boolean {
|
||||
this.frames.push([]);
|
||||
const v = this.emitExpr(expr);
|
||||
this.moveTemp(v);
|
||||
this.toFrame(v);
|
||||
this.B.line(`store ${this.llType(expr.type)} ${v.name}, ptr ${slot}`);
|
||||
if (integerSlot !== undefined && v.uint32 !== undefined)
|
||||
this.B.line(`store i32 ${v.uint32}, ptr ${integerSlot}`);
|
||||
this.releaseFrame(this.frames.pop()!);
|
||||
this.releaseTemps(this.frames.pop()!);
|
||||
return v.uint32 !== undefined;
|
||||
}
|
||||
|
||||
@@ -9780,12 +9938,13 @@ export class LlEmitter {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${b.slot}`);
|
||||
B.line(`store ptr null, ptr ${b.slot}`);
|
||||
this.releaseValue(old, b.type);
|
||||
if (b.kind === "global") this.releaseHeap(old, b.type);
|
||||
else this.releaseFrame(old, b.type);
|
||||
}
|
||||
let raw: string;
|
||||
if (charCode !== null) {
|
||||
// The snapshot moves into the consuming character append.
|
||||
this.moveTemp(snapshot);
|
||||
this.toFrame(snapshot);
|
||||
this.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`);
|
||||
raw = B.tmp();
|
||||
B.line(
|
||||
@@ -9800,11 +9959,15 @@ export class LlEmitter {
|
||||
}
|
||||
const result = this.own({ name: raw, type: left.type });
|
||||
if (retainForYield) {
|
||||
const stored = this.retainValue(result.name, result.type);
|
||||
const stored =
|
||||
b.kind === "local"
|
||||
? this.retainFrame(result.name, result.type)
|
||||
: this.retainHeap(result.name, result.type);
|
||||
if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, stored);
|
||||
else B.line(`store ptr ${stored}, ptr ${b.slot}`);
|
||||
} else {
|
||||
this.moveTemp(result);
|
||||
if (b.kind === "local") this.toFrame(result);
|
||||
else this.toHeap(result);
|
||||
if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, result.name);
|
||||
else B.line(`store ptr ${result.name}, ptr ${b.slot}`);
|
||||
}
|
||||
@@ -9867,7 +10030,7 @@ export class LlEmitter {
|
||||
if (owned) {
|
||||
const old = this.loadField(ptr, fieldType, false);
|
||||
this.storeField(ptr, fieldType, "null", false);
|
||||
this.releaseValue(old, fieldType);
|
||||
this.releaseHeap(old, fieldType);
|
||||
}
|
||||
let raw: string;
|
||||
if (right === null) {
|
||||
@@ -10096,9 +10259,20 @@ export class LlEmitter {
|
||||
)
|
||||
return this.emitRegionIntrinsic(e);
|
||||
if (e.name.startsWith("itable.")) return this.emitInternTableIntrinsic(e);
|
||||
if (e.name.startsWith("slots.")) return emitThreadSlotsIntrinsic(this, e);
|
||||
return emitIntrinsicExpr(this, e);
|
||||
}
|
||||
|
||||
/** The ScrSlots block of the ThreadSlots family `name`. */
|
||||
threadSlotsBlock(name: string): string {
|
||||
let sym = this.threadSlotsBlocks.get(name);
|
||||
if (sym === undefined) {
|
||||
sym = `@sc_slots_${this.threadSlotsBlocks.size}`;
|
||||
this.threadSlotsBlocks.set(name, sym);
|
||||
}
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** InternTable's runtime calls (scr_itable.c). Table and entry handles
|
||||
* are f64 values carrying pointer bits; entry values are 64-bit slots
|
||||
* (double bits, a bool, or a pointer to an immortal value) that the
|
||||
@@ -10301,7 +10475,7 @@ export class LlEmitter {
|
||||
}
|
||||
// The result IS the operand: ownership moves to the result temp.
|
||||
if (!isRefCounted(value.type)) return { name: value.name, type: e.type };
|
||||
this.moveTemp(value);
|
||||
this.toFrame(value);
|
||||
return this.own({ name: value.name, type: e.type });
|
||||
}
|
||||
|
||||
@@ -10554,7 +10728,7 @@ export class LlEmitter {
|
||||
this.frames.push([]);
|
||||
const result = this.emitReadReceiver(value);
|
||||
B.line(`store ptr ${result.name}, ptr ${slot}`);
|
||||
this.releaseFrame(this.frames.pop()!);
|
||||
this.releaseTemps(this.frames.pop()!);
|
||||
B.br(join);
|
||||
}
|
||||
B.startBlock(join);
|
||||
@@ -10650,7 +10824,7 @@ export class LlEmitter {
|
||||
const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field);
|
||||
const p = this.B.tmp();
|
||||
this.B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
|
||||
const owner = this.own({ name: this.retainValue(p, nullable.arm), type: nullable.arm });
|
||||
const owner = this.own({ name: this.retainFrame(p, nullable.arm), type: nullable.arm });
|
||||
return { box: this.stackNullableBox(owner.name, nullable), owner };
|
||||
}
|
||||
|
||||
@@ -10689,7 +10863,7 @@ export class LlEmitter {
|
||||
B.line(`store ptr ${this.unitInstanceRef(nullable.unionId, nullable.unitTag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ref);
|
||||
const owned = this.retainValue(p, nullable.arm);
|
||||
const owned = this.retainHeap(p, nullable.arm);
|
||||
const box = this.unionNewOwned(nullable.unionId, nullable.refTag, {
|
||||
name: owned,
|
||||
type: nullable.arm,
|
||||
@@ -10712,7 +10886,7 @@ export class LlEmitter {
|
||||
B.line(`${isRef} = icmp eq i32 ${tag}, ${nullable.refTag}`);
|
||||
const payload = this.unionPeek(u, nullable.unionId);
|
||||
B.line(`${sel} = select i1 ${isRef}, ptr ${payload}, ptr null`);
|
||||
return this.retainValue(sel, nullable.arm);
|
||||
return this.retainHeap(sel, nullable.arm);
|
||||
}
|
||||
|
||||
/** `obj.f = value` for a nullable-pointer field: the new instance pointer
|
||||
@@ -10737,7 +10911,7 @@ export class LlEmitter {
|
||||
) {
|
||||
if (value.tag === nullable.refTag) {
|
||||
const inner = this.emitExpr(value.value);
|
||||
this.moveTemp(inner);
|
||||
this.toHeap(inner);
|
||||
next = inner.name;
|
||||
} else {
|
||||
// Unit payloads carry nothing; a void payload runs for effects.
|
||||
@@ -10753,7 +10927,7 @@ export class LlEmitter {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${ptr}`);
|
||||
B.line(`store ptr ${next}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
|
||||
this.releaseValue(old, nullable.arm);
|
||||
this.releaseHeap(old, nullable.arm);
|
||||
}
|
||||
|
||||
/** A union binding whose slot may hold a private stack box: stack locals
|
||||
|
||||
@@ -162,7 +162,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = call ptr @scr_promise_new()`);
|
||||
const out = host.own({ name: p, type: e.type });
|
||||
host.moveTemp(reason); // the cell takes ownership
|
||||
host.toHeap(reason); // the cell takes ownership
|
||||
host.emitThrowValue({ name: reason.name, type: reasonT });
|
||||
B.line(`call void @scr_promise_reject_pending(ptr ${p})`);
|
||||
return out;
|
||||
@@ -191,12 +191,12 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
|
||||
host.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`);
|
||||
B.line(`call void @scr_promise_fulfill_bool(ptr ${p}, i1 ${v.name})`);
|
||||
} else if (t.kind === "string") {
|
||||
host.moveTemp(v);
|
||||
host.toHeap(v);
|
||||
host.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
|
||||
B.line(`call void @scr_promise_fulfill_str(ptr ${p}, ptr ${v.name})`);
|
||||
} else {
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
host.moveTemp(v);
|
||||
host.toHeap(v);
|
||||
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_promise_fulfill_ref(ptr ${p}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
|
||||
@@ -537,7 +537,7 @@ export function emitAsyncExpr(
|
||||
B.line(`call void @scr_gen_yield_bool(i1 ${v.name})`);
|
||||
}
|
||||
} else {
|
||||
host.moveTemp(v); // the OUT slot takes ownership
|
||||
host.toHeap(v); // the OUT slot takes ownership
|
||||
if (host.currentCoro !== null) {
|
||||
const coro = host.currentCoro;
|
||||
const release = vAdapters(host.shapeHost, yt).release;
|
||||
@@ -601,7 +601,7 @@ export function emitAsyncExpr(
|
||||
// Refcounted channels: the slot's +1 moves out.
|
||||
host.declare(`declare ptr @scr_gen_take_in_ref()`);
|
||||
B.line(`${t} = call ptr @scr_gen_take_in_ref()`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
return host.own({ name: host.adoptHeap(t, e.type), type: e.type });
|
||||
}
|
||||
case "genResume": {
|
||||
// One consumer resume: park the sent value (typed per mode), hop
|
||||
@@ -731,7 +731,7 @@ export function emitAsyncExpr(
|
||||
);
|
||||
host.declare(`declare ptr @scr_gen_take_out_ref(ptr)`);
|
||||
B.line(`${t} = call ptr @scr_gen_take_out_ref(ptr ${gb.name})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
return host.own({ name: host.adoptHeap(t, e.type), type: e.type });
|
||||
}
|
||||
// Valueless: an empty IN slot reads as undefined on any channel.
|
||||
host.declare(`declare zeroext i1 @scr_gen_step(ptr)`);
|
||||
@@ -748,7 +748,7 @@ export function emitAsyncExpr(
|
||||
const g = host.emitExpr(e.gen); // borrowed for the calls below
|
||||
const sendArg = (store: (aName: string, t: IrType) => void): void => {
|
||||
const a = host.emitExpr(e.arg!);
|
||||
if (isRefCounted(e.arg!.type)) host.moveTemp(a); // the slot takes ownership
|
||||
if (isRefCounted(e.arg!.type)) host.toHeap(a); // the slot takes ownership
|
||||
store(a.name, e.arg!.type);
|
||||
};
|
||||
const parkIn = (name: string, t: IrType): void => {
|
||||
@@ -786,7 +786,7 @@ export function emitAsyncExpr(
|
||||
} else {
|
||||
const a = host.emitExpr(e.arg);
|
||||
const t = e.arg.type;
|
||||
if (isRefCounted(t)) host.moveTemp(a);
|
||||
if (isRefCounted(t)) host.toHeap(a);
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`);
|
||||
call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`;
|
||||
@@ -805,7 +805,7 @@ export function emitAsyncExpr(
|
||||
} else {
|
||||
const a = host.emitExpr(e.arg);
|
||||
const t = e.arg.type;
|
||||
if (isRefCounted(t)) host.moveTemp(a);
|
||||
if (isRefCounted(t)) host.toHeap(a);
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`);
|
||||
call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`;
|
||||
@@ -823,7 +823,7 @@ export function emitAsyncExpr(
|
||||
"llvm emitter bug: async genResume throw with no payload",
|
||||
);
|
||||
const a = host.emitExpr(e.arg);
|
||||
if (isRefCounted(e.arg.type)) host.moveTemp(a);
|
||||
if (isRefCounted(e.arg.type)) host.toHeap(a);
|
||||
host.emitThrowValue({ name: a.name, type: e.arg.type });
|
||||
host.declare(`declare ptr @scr_async_gen_throw(ptr)`);
|
||||
call = `call ptr @scr_async_gen_throw(ptr ${g.name})`;
|
||||
@@ -873,7 +873,7 @@ export function emitAsyncExpr(
|
||||
if (e.arg === null)
|
||||
throw new InternalCompilerError("llvm emitter bug: genResume throw with no payload");
|
||||
const a = host.emitExpr(e.arg);
|
||||
if (isRefCounted(e.arg.type)) host.moveTemp(a); // the cell takes ownership
|
||||
if (isRefCounted(e.arg.type)) host.toHeap(a); // the cell takes ownership
|
||||
host.emitThrowValue({ name: a.name, type: e.arg.type });
|
||||
host.declare(`declare void @scr_gen_resume_throw(ptr)`);
|
||||
B.line(`call void @scr_gen_resume_throw(ptr ${g.name})`);
|
||||
@@ -955,7 +955,7 @@ export function emitAsyncExpr(
|
||||
host.declare(`declare ptr @${refAwait}(ptr)`);
|
||||
B.line(`${t} = call ptr @${refAwait}(ptr ${pr.name})`);
|
||||
}
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
const out = host.own({ name: host.adoptHeap(t, e.type), type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
typeKey,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
classValueConstructor,
|
||||
} from "../../ir/ir.js";
|
||||
import { liveRefView } from "./expr-stream-bridges.js";
|
||||
import { typedRefConstructor, vAdapters } from "./shapes.js";
|
||||
@@ -165,7 +166,7 @@ export function emitCallExpr(
|
||||
// This also preserves snapshots when a later argument mutates its
|
||||
// source, and releases earlier arguments if evaluation throws.
|
||||
args.forEach((a, index) => {
|
||||
if (!borrowed?.has(index)) host.moveTemp(a);
|
||||
if (!borrowed?.has(index)) host.toFrame(a);
|
||||
});
|
||||
const abi = awaitEntry ? undefined : host.int32Abis.get(e.callee);
|
||||
const argList = args
|
||||
@@ -224,14 +225,14 @@ export function emitCallExpr(
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseFrame(host.frames.pop()!);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (abi?.union) {
|
||||
B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseFrame(host.frames.pop()!);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
if (stackResult) return host.stackScalarUnion(t, e.type);
|
||||
return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type });
|
||||
}
|
||||
@@ -239,7 +240,7 @@ export function emitCallExpr(
|
||||
B.line(`${t} = call i32 ${target}(${argList})`);
|
||||
const out = host.int32Result(t, e.type);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (borrowed) host.releaseFrame(host.frames.pop()!);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return out;
|
||||
}
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`);
|
||||
@@ -249,14 +250,14 @@ export function emitCallExpr(
|
||||
// test the exception first, then own the result before the
|
||||
// argument snapshots that keep it reachable are released.
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
out = host.own({ name: host.retainValue(t, e.type), type: e.type });
|
||||
out = host.own({ name: host.retainFrame(t, e.type), type: e.type });
|
||||
} else {
|
||||
out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
}
|
||||
if (borrowed) {
|
||||
host.moveTemp(out);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.toFrame(out);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
return host.own(out);
|
||||
}
|
||||
return out;
|
||||
@@ -718,7 +719,7 @@ export function emitCallExpr(
|
||||
// every mention yields the same pointer, so `f === f` is true.
|
||||
host.fnValues.add(e.fnName);
|
||||
return host.own({
|
||||
name: host.retainValue(`@${mangleFnClosure(e.fnName)}`, e.type),
|
||||
name: host.retainFrame(`@${mangleFnClosure(e.fnName)}`, e.type),
|
||||
type: e.type,
|
||||
});
|
||||
}
|
||||
@@ -880,7 +881,7 @@ export function emitCallExpr(
|
||||
lent?.has(i) && lend[i] ? host.emitReadReceiver(a) : host.emitExpr(a),
|
||||
);
|
||||
args.forEach((a, i) => {
|
||||
if (!lent?.has(i)) host.moveTemp(a);
|
||||
if (!lent?.has(i)) host.toFrame(a);
|
||||
});
|
||||
const argListOf = (values: readonly LlValue[]): string =>
|
||||
[
|
||||
@@ -891,7 +892,7 @@ export function emitCallExpr(
|
||||
const ownedArgs = (): LlValue[] =>
|
||||
lent
|
||||
? args.map((a, i) =>
|
||||
lent.has(i) ? { name: host.retainValue(a.name, a.type), type: a.type } : a,
|
||||
lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a,
|
||||
)
|
||||
: args;
|
||||
const invoke = (target: string, needsReceiver: boolean, values = args): string => {
|
||||
@@ -978,7 +979,7 @@ export function emitCallExpr(
|
||||
}
|
||||
if (e.type.kind === "void") {
|
||||
if (host.indirectMayThrow) host.emitPendingCheck();
|
||||
if (lent) host.releaseFrame(host.frames.pop()!);
|
||||
if (lent) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
// The check runs AFTER the result temp joins the frame: an unwind
|
||||
@@ -986,15 +987,15 @@ export function emitCallExpr(
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
if (host.indirectMayThrow) host.emitPendingCheck();
|
||||
if (lent) {
|
||||
host.moveTemp(out);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.toFrame(out);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
return host.own(out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case "selfRef":
|
||||
// The running closure itself (env is borrowed; the result owned).
|
||||
return host.own({ name: host.retainValue("%sc_env", e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame("%sc_env", e.type), type: e.type });
|
||||
case "new": {
|
||||
// Allocate (fields zeroed, vt stamped), then run the ctor. The
|
||||
// ctor owns and releases its `this` param like any callee, so it
|
||||
@@ -1034,7 +1035,7 @@ export function emitCallExpr(
|
||||
});
|
||||
return host.own({ name: value, type: e.type });
|
||||
}
|
||||
return host.own({ name: host.retainValue(`@${sym}`, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(`@${sym}`, e.type), type: e.type });
|
||||
}
|
||||
case "newValue": {
|
||||
// Construction through a class VALUE: call the class object's
|
||||
@@ -1043,20 +1044,21 @@ export function emitCallExpr(
|
||||
if (e.callee.type.kind !== "classval")
|
||||
throw new InternalCompilerError("llvm emitter bug: newValue on non-classval callee");
|
||||
const cls = e.callee.type.className;
|
||||
const ctor = host.fnByName.get(`%${cls}.constructor`);
|
||||
const valueCtor = classValueConstructor(host.classMetaOf(cls).def);
|
||||
const ctor = host.fnByName.get(valueCtor.fn);
|
||||
if (!ctor)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: newValue on ${cls} without a constructor`,
|
||||
);
|
||||
const callee = host.emitExpr(e.callee);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
for (const a of args) host.moveTemp(a); // the constructor owns its params
|
||||
for (const a of args) host.toFrame(a); // the constructor owns its params
|
||||
const ctorp = B.tmp();
|
||||
const thunk = B.tmp();
|
||||
B.line(`${ctorp} = getelementptr inbounds %ScrClassObj, ptr ${callee.name}, i64 0, i32 3`);
|
||||
B.line(`${thunk} = load ptr, ptr ${ctorp}`);
|
||||
const argList = args
|
||||
.map((a, i) => `${host.llType(ctor.params[i + 1]!.type)} ${a.name}`)
|
||||
.map((a, i) => `${host.llType(ctor.params[i + valueCtor.firstArg]!.type)} ${a.name}`)
|
||||
.join(", ");
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr ${thunk}(ptr ${callee.name}${argList ? ", " + argList : ""})`);
|
||||
@@ -1121,7 +1123,7 @@ export function emitCallExpr(
|
||||
// One ScrPromise* either way — ownership transfers, type-only
|
||||
//.
|
||||
const v = host.emitExpr(e.value);
|
||||
host.moveTemp(v);
|
||||
host.toFrame(v);
|
||||
return host.own({ name: v.name, type: e.type });
|
||||
}
|
||||
case "upcast":
|
||||
@@ -1134,7 +1136,7 @@ export function emitCallExpr(
|
||||
const v = host.emitExpr(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object" && v.type.kind === "object")
|
||||
emitDowncastGuard(host, v.name, v.type.className, e.type.className);
|
||||
if (isRefCounted(v.type)) host.moveTemp(v);
|
||||
if (isRefCounted(v.type)) host.toFrame(v);
|
||||
return host.own({ name: v.name, type: e.type });
|
||||
}
|
||||
case "instanceOf": {
|
||||
@@ -1188,7 +1190,7 @@ export function emitCallExpr(
|
||||
borrowed.has(index) && inputs[index] ? host.emitReadReceiver(a) : host.emitExpr(a),
|
||||
);
|
||||
args.forEach((a, index) => {
|
||||
if (!borrowed.has(index)) host.moveTemp(a);
|
||||
if (!borrowed.has(index)) host.toFrame(a);
|
||||
});
|
||||
const recv = args[0]!.name;
|
||||
const vtp = B.tmp();
|
||||
@@ -1239,7 +1241,7 @@ export function emitCallExpr(
|
||||
B.startBlock(done);
|
||||
if (e.type.kind === "void") {
|
||||
if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck();
|
||||
if (borrowed.size > 0) host.releaseFrame(host.frames.pop()!);
|
||||
if (borrowed.size > 0) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
@@ -1247,8 +1249,8 @@ export function emitCallExpr(
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck();
|
||||
if (borrowed.size > 0) {
|
||||
host.moveTemp(out);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.toFrame(out);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
return host.own(out);
|
||||
}
|
||||
return out;
|
||||
@@ -1256,7 +1258,7 @@ export function emitCallExpr(
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${fn}(${argList})`);
|
||||
if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck();
|
||||
if (borrowed.size > 0) host.releaseFrame(host.frames.pop()!);
|
||||
if (borrowed.size > 0) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
@@ -1264,8 +1266,8 @@ export function emitCallExpr(
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck();
|
||||
if (borrowed.size > 0) {
|
||||
host.moveTemp(out);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.toFrame(out);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
return host.own(out);
|
||||
}
|
||||
return out;
|
||||
@@ -1392,7 +1394,7 @@ export function emitConstruction(
|
||||
return host.emitExpr(a);
|
||||
});
|
||||
args.forEach((a, i) => {
|
||||
if (!borrowed.has(i + 1)) host.moveTemp(a);
|
||||
if (!borrowed.has(i + 1)) host.toFrame(a);
|
||||
});
|
||||
const self = borrowed.has(0)
|
||||
? o
|
||||
@@ -1408,11 +1410,11 @@ export function emitConstruction(
|
||||
`call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
|
||||
);
|
||||
if (host.mayThrow.has(ctorName)) host.emitPendingCheck();
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
return;
|
||||
}
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
for (const a of args) host.moveTemp(a);
|
||||
for (const a of args) host.toFrame(a);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
|
||||
const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", ");
|
||||
@@ -1454,8 +1456,8 @@ function emitNumericIndexRead(
|
||||
B.startBlock(slow);
|
||||
host.frames.push([]);
|
||||
const read = emitCallExpr(host, e, false, false, undefined, true);
|
||||
host.moveTemp(read);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.toFrame(read);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
const slowLabel = B.currentLabel();
|
||||
B.terminate(`br label %${join}`);
|
||||
B.startBlock(join);
|
||||
|
||||
@@ -118,7 +118,7 @@ export function emitArrayValues(
|
||||
sharedBuffer?: string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
if (acc === "ref") values.forEach((value) => host.moveTemp(value));
|
||||
if (acc === "ref") values.forEach((value) => host.toHeap(value));
|
||||
if (values.length === 0) {
|
||||
host.declare("declare double @scr_arr_len(ptr)");
|
||||
const result = B.tmp();
|
||||
@@ -219,7 +219,7 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void {
|
||||
B.startBlock(label);
|
||||
host.frames.push([]);
|
||||
emitDiscarded(host, arm);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
B.br(lj);
|
||||
}
|
||||
B.startBlock(lj);
|
||||
@@ -235,7 +235,7 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void {
|
||||
const start = host.emitExpr(e.args[0]!);
|
||||
const count = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF;
|
||||
const values = items.map((item) => host.emitExpr(item));
|
||||
if (acc === "ref") values.forEach((value) => host.moveTemp(value));
|
||||
if (acc === "ref") values.forEach((value) => host.toHeap(value));
|
||||
let buffer = "null";
|
||||
if (values.length > 0) {
|
||||
buffer = B.tmp();
|
||||
@@ -1069,7 +1069,7 @@ export function emitMapNew(host: LlvmEmitterContext, e: IrExpr & { kind: "mapNew
|
||||
for (const pair of e.seed ?? []) {
|
||||
const k = emitBorrowedInput(host, pair.key);
|
||||
const v = host.emitExpr(pair.value);
|
||||
if (vAcc === "ref") host.moveTemp(v); // the value MOVES in
|
||||
if (vAcc === "ref") host.toHeap(v); // the value MOVES in
|
||||
host.mapSet(m, kAcc, vAcc, k.name, v.name);
|
||||
}
|
||||
return out;
|
||||
@@ -1370,7 +1370,7 @@ export function emitMapLikeIntrinsic(
|
||||
const k = emitBorrowedInput(host, e.args[0]!);
|
||||
const v = host.emitExpr(e.args[1]!);
|
||||
const vAcc = elemAccess(receiverType.value);
|
||||
if (vAcc === "ref") host.moveTemp(v);
|
||||
if (vAcc === "ref") host.toHeap(v);
|
||||
const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name);
|
||||
if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
const bits = B.tmp();
|
||||
|
||||
@@ -58,7 +58,7 @@ export function emitControlExpr(
|
||||
// and the right runs in its own frame.
|
||||
const ty = host.llType(e.type);
|
||||
const l = host.emitExpr(e.left);
|
||||
host.moveTemp(l);
|
||||
host.toFrame(l);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
@@ -73,7 +73,7 @@ export function emitControlExpr(
|
||||
if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel);
|
||||
else B.condBr(truthy, joinLabel, rightLabel);
|
||||
B.startBlock(rightLabel);
|
||||
if (isRefCounted(e.type)) host.releaseValue(l.name, e.type);
|
||||
if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type);
|
||||
const rightInteger = host.emitBranchInto(slot, e.right, integerSlot);
|
||||
B.br(joinLabel);
|
||||
B.startBlock(joinLabel);
|
||||
@@ -157,7 +157,7 @@ export function emitControlExpr(
|
||||
host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true });
|
||||
host.frames.push([]);
|
||||
host.emitExpr(e.body);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
@@ -203,7 +203,7 @@ export function emitControlExpr(
|
||||
bindPresent();
|
||||
host.frames.push([]);
|
||||
host.emitExpr(e.body);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(e.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
@@ -272,7 +272,7 @@ export function emitControlExpr(
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`);
|
||||
if (!isRefCounted(e.type)) return { name: v, type: e.type };
|
||||
return host.own({ name: host.retainValue(v, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(v, e.type), type: e.type });
|
||||
}
|
||||
case "orDefault": {
|
||||
// `u || d` narrowed to the single non-unit arm: nullish's dance
|
||||
@@ -309,7 +309,7 @@ export function emitControlExpr(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
host.moveTemp(l);
|
||||
host.toFrame(l);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
@@ -337,11 +337,11 @@ export function emitControlExpr(
|
||||
B.line(
|
||||
`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`,
|
||||
);
|
||||
host.releaseValue(l.name, e.left.type);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(lf);
|
||||
host.releaseValue(l.name, e.left.type);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
@@ -359,7 +359,7 @@ export function emitControlExpr(
|
||||
// The native checked-value kind decides whether the left is
|
||||
// nullish; the right runs lazily in its branch.
|
||||
const l = host.emitExpr(e.left);
|
||||
host.moveTemp(l);
|
||||
host.toFrame(l);
|
||||
host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
@@ -370,7 +370,7 @@ export function emitControlExpr(
|
||||
const lj = B.newLabel("nuld.j");
|
||||
B.condBr(isN, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.releaseValue(l.name, e.left.type);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
@@ -426,7 +426,7 @@ export function emitControlExpr(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
host.moveTemp(l);
|
||||
host.toFrame(l);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
@@ -436,7 +436,7 @@ export function emitControlExpr(
|
||||
const lj = B.newLabel("nul.j");
|
||||
B.condBr(isUnit, lu, lv);
|
||||
B.startBlock(lu);
|
||||
host.releaseValue(l.name, e.left.type);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
@@ -445,7 +445,7 @@ export function emitControlExpr(
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`);
|
||||
host.releaseValue(l.name, e.left.type);
|
||||
host.releaseFrame(l.name, e.left.type);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
@@ -533,7 +533,7 @@ export function emitOptionalChainReceiver(
|
||||
const isUnit = host.tagInSet(r.name, def.id, unitTags);
|
||||
const extract = (): string =>
|
||||
multiple
|
||||
? host.retainValue(r.name, narrowed)
|
||||
? host.retainFrame(r.name, narrowed)
|
||||
: borrowBind
|
||||
? host.unionPeek(r.name, def.id)
|
||||
: host.unionExtract(r.name, def.id, narrowed);
|
||||
|
||||
@@ -161,7 +161,7 @@ export function emitDynamicExpr(
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
} else {
|
||||
host.moveTemp(v);
|
||||
host.toHeap(v);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${pack}, ptr ${v.name})`);
|
||||
}
|
||||
});
|
||||
@@ -253,7 +253,7 @@ export function emitDynamicExpr(
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
for (const el of e.elems) {
|
||||
const v = host.emitExpr(el);
|
||||
host.moveTemp(v);
|
||||
host.toHeap(v);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${arr}, ptr ${v.name})`);
|
||||
}
|
||||
return out;
|
||||
@@ -296,7 +296,7 @@ export function emitDynamicExpr(
|
||||
return host.own({ name: host.unitInstanceRef(e.unionId, e.tag), type: e.type });
|
||||
}
|
||||
const v = host.emitExpr(e.value);
|
||||
if (isRefCounted(arm)) host.moveTemp(v);
|
||||
if (isRefCounted(arm)) host.toHeap(v);
|
||||
return host.own({ name: host.unionNewOwned(e.unionId, e.tag, v), type: e.type });
|
||||
}
|
||||
case "unionNarrow": {
|
||||
@@ -353,7 +353,7 @@ export function emitDynamicExpr(
|
||||
const value = nullable
|
||||
? host.nullableToOwnedUnion(v, nullable)
|
||||
: isRefCounted(e.type)
|
||||
? host.retainValue(v, e.type)
|
||||
? host.retainFrame(v, e.type)
|
||||
: v;
|
||||
B.line(`store ${ty} ${value}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
@@ -465,7 +465,7 @@ export function emitDynamicExpr(
|
||||
const owned = inline
|
||||
? host.inlineFieldUnion(ptr, ft)
|
||||
: isRefCounted(ft)
|
||||
? host.retainValue(v, ft)
|
||||
? host.retainFrame(v, ft)
|
||||
: v;
|
||||
B.line(`store ${ty} ${owned}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
@@ -487,7 +487,7 @@ export function emitDynamicExpr(
|
||||
ft.kind === "f64" || ft.kind === "bool"
|
||||
? host.unionNewOwned(e.type.unionId, tag, { name: v, type: ft })
|
||||
: host.unionNewOwned(e.type.unionId, tag, {
|
||||
name: host.retainValue(v, ft),
|
||||
name: host.retainHeap(v, ft),
|
||||
type: ft,
|
||||
});
|
||||
B.line(`store ptr ${wrapped}, ptr ${slot}`);
|
||||
@@ -1087,7 +1087,7 @@ export function emitDynamicExpr(
|
||||
B.line(`${pp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`);
|
||||
B.line(`${payload} = load ptr, ptr ${pp}`);
|
||||
if (e.type.kind === "string") {
|
||||
return host.own({ name: host.retainValue(payload, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(payload, e.type), type: e.type });
|
||||
}
|
||||
if (e.type.kind === "object") {
|
||||
// Retain through the snapshot's own entry point (the payload's
|
||||
|
||||
@@ -47,11 +47,11 @@ export function emitLiteralExpr(
|
||||
return { name: e.value ? "true" : "false", type: e.type };
|
||||
case "strLit": {
|
||||
const sym = host.internLiteral(e.value);
|
||||
return host.own({ name: host.retainValue(sym, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(sym, e.type), type: e.type });
|
||||
}
|
||||
case "moduleNsRef": {
|
||||
const sym = host.internLiteral(`module:${e.moduleId}`);
|
||||
return host.own({ name: host.retainValue(sym, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(sym, e.type), type: e.type });
|
||||
}
|
||||
case "unitLit":
|
||||
// unitLits are consumed inline by the unionWrap case (a unit arm is
|
||||
@@ -110,10 +110,10 @@ export function emitLiteralExpr(
|
||||
const box = host.loadBox(b.slot);
|
||||
if (b.local!.tdz === true) {
|
||||
const v = host.tdzBoxRead(box, e.type, b.local!.name);
|
||||
return host.own({ name: v, type: e.type });
|
||||
return host.own({ name: host.adoptHeap(v, e.type), type: e.type });
|
||||
}
|
||||
const v = host.boxGet(box, e.type);
|
||||
return host.own({ name: v, type: e.type });
|
||||
return host.own({ name: host.adoptHeap(v, e.type), type: e.type });
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}`);
|
||||
@@ -131,7 +131,7 @@ export function emitLiteralExpr(
|
||||
return host.markImmortal({ name: t, type: e.type });
|
||||
// The binding's last read takes over its reference (lastUseRead).
|
||||
if (host.takeLastUseMove(e, b.slot, b.type)) return host.own({ name: t, type: e.type });
|
||||
return host.own({ name: host.retainValue(t, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(t, e.type), type: e.type });
|
||||
}
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -488,8 +488,11 @@ export function emitOperatorExpr(
|
||||
if (isRefCounted(b.type)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${b.slot}`);
|
||||
host.releaseValue(old, b.type);
|
||||
B.line(`store ptr ${host.retainValue(v.name, v.type)}, ptr ${b.slot}`);
|
||||
if (b.kind === "global") host.releaseHeap(old, b.type);
|
||||
else host.releaseFrame(old, b.type);
|
||||
B.line(
|
||||
`store ptr ${b.kind === "global" ? host.retainHeap(v.name, v.type) : host.retainFrame(v.name, v.type)}, ptr ${b.slot}`,
|
||||
);
|
||||
} else {
|
||||
B.line(`store ${host.llType(b.type)} ${v.name}, ptr ${b.slot}`);
|
||||
host.storeIntegerView(e.localId, v, e.value);
|
||||
@@ -632,12 +635,12 @@ export function emitStringExpr(
|
||||
case "undefinedT":
|
||||
case "nullT": {
|
||||
const lit = host.internLiteral(arm.kind === "undefinedT" ? "undefined" : "null");
|
||||
B.line(`store ptr ${host.retainValue(lit, e.type)}, ptr ${slot}`);
|
||||
B.line(`store ptr ${host.retainFrame(lit, e.type)}, ptr ${slot}`);
|
||||
break;
|
||||
}
|
||||
case "string":
|
||||
B.line(
|
||||
`store ptr ${host.retainValue(host.unionPeek(v.name, def.id), e.type)}, ptr ${slot}`,
|
||||
`store ptr ${host.retainFrame(host.unionPeek(v.name, def.id), e.type)}, ptr ${slot}`,
|
||||
);
|
||||
break;
|
||||
case "f64": {
|
||||
@@ -691,7 +694,7 @@ export function emitStringExpr(
|
||||
// String(record) / `${record}`: Object.prototype.toString's
|
||||
// constant — the interned literal, retained like a strLit.
|
||||
const sym = host.internLiteral("[object Object]");
|
||||
return host.own({ name: host.retainValue(sym, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(sym, e.type), type: e.type });
|
||||
}
|
||||
if (v.type.kind === "caught") {
|
||||
// String(e) / `${e}` on a catch binding: JS's String() over the
|
||||
@@ -748,7 +751,7 @@ export function emitStringExpr(
|
||||
};
|
||||
host.templateStringsInstances.set(e.key, inst);
|
||||
}
|
||||
return host.own({ name: host.retainValue(`@${inst.sym}`, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type });
|
||||
}
|
||||
case "regexIntrinsic":
|
||||
return host.emitRegexIntrinsic(e);
|
||||
@@ -975,7 +978,7 @@ export function emitRecordExpr(
|
||||
}
|
||||
const v = host.loadField(ptr, type, false);
|
||||
if (isRefCounted(e.type))
|
||||
return host.own({ name: host.retainValue(v, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(v, e.type), type: e.type });
|
||||
return { name: v, type: e.type };
|
||||
}
|
||||
case "recordGet": {
|
||||
@@ -984,7 +987,7 @@ export function emitRecordExpr(
|
||||
if (inline) return host.own({ name: host.inlineFieldUnion(ptr, type), type: e.type });
|
||||
const v = host.loadRecordField(ptr, type, false);
|
||||
if (isRefCounted(e.type))
|
||||
return host.own({ name: host.retainValue(v, e.type), type: e.type });
|
||||
return host.own({ name: host.retainFrame(v, e.type), type: e.type });
|
||||
return { name: v, type: e.type };
|
||||
}
|
||||
case "recordLit": {
|
||||
@@ -1090,7 +1093,7 @@ export function emitRecordLitFields(
|
||||
if (f.overflow) {
|
||||
const lit = host.internLiteral(f.name);
|
||||
const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
|
||||
if (acc === "ref") host.moveTemp(v);
|
||||
if (acc === "ref") host.toHeap(v);
|
||||
const ovf = host.recordOvfPtr(rec, shapeId);
|
||||
const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr";
|
||||
host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function emitUnionWiden(
|
||||
B.br(join);
|
||||
});
|
||||
B.startBlock(join);
|
||||
if (owned) host.releaseValue(value, { kind: "union", unionId: fromId });
|
||||
if (owned) host.releaseFrame(value, { kind: "union", unionId: fromId });
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = load ptr, ptr ${slot}`);
|
||||
return result;
|
||||
@@ -58,7 +58,7 @@ export function emitUnionRetype(
|
||||
e: IrExpr & { kind: "unionRetype" },
|
||||
): LlValue {
|
||||
const value = host.emitExpr(e.value);
|
||||
host.moveTemp(value);
|
||||
host.toFrame(value);
|
||||
const from = host.nullableUnions.get(e.fromId);
|
||||
const to = host.nullableUnions.get(e.unionId);
|
||||
if (from && to) {
|
||||
@@ -131,7 +131,7 @@ function rebuildRetype(
|
||||
B.br(join);
|
||||
});
|
||||
B.startBlock(join);
|
||||
host.releaseValue(value, { kind: "union", unionId: e.fromId });
|
||||
host.releaseFrame(value, { kind: "union", unionId: e.fromId });
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = load ptr, ptr ${slot}`);
|
||||
return result;
|
||||
@@ -147,7 +147,9 @@ function directRegexOperandMethod(method: string): boolean {
|
||||
case "match":
|
||||
case "search":
|
||||
case "matchAll":
|
||||
case "matchAllCaptures":
|
||||
case "matchAllInto":
|
||||
case "matchAllCapturesInto":
|
||||
case "replace":
|
||||
case "replaceAll":
|
||||
case "split":
|
||||
@@ -192,7 +194,7 @@ export function emitRegexIntrinsic(
|
||||
} else {
|
||||
values = preservesRegexInputs(method)
|
||||
? emitBorrowedInputs(host, inputs)
|
||||
: method === "matchAllInto"
|
||||
: method === "matchAllInto" || method === "matchAllCapturesInto"
|
||||
? inputs.map((a) => emitBorrowedInput(host, a))
|
||||
: inputs.map((a) => host.emitExpr(a));
|
||||
}
|
||||
@@ -216,6 +218,9 @@ export function emitRegexIntrinsic(
|
||||
// Every match drained eagerly into a fresh +1 string[][]; throws
|
||||
// Node's TypeError on a non-global regex (catchable).
|
||||
return fallible("scr_regex_match_all", `ptr ${r.name}, ptr ${args[0]!.name}`);
|
||||
case "matchAllCaptures":
|
||||
// matchAll whose rows carry no index, input or groups properties.
|
||||
return fallible("scr_regex_match_all_captures", `ptr ${r.name}, ptr ${args[0]!.name}`);
|
||||
case "matchAllInto":
|
||||
// matchAll's companion-index form: args[1] (a number[]) also
|
||||
// receives each match's UTF-16 start index.
|
||||
@@ -223,6 +228,11 @@ export function emitRegexIntrinsic(
|
||||
"scr_regex_match_all_into",
|
||||
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
|
||||
);
|
||||
case "matchAllCapturesInto":
|
||||
return fallible(
|
||||
"scr_regex_match_all_captures_into",
|
||||
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
|
||||
);
|
||||
case "replaceAll":
|
||||
return fallible(
|
||||
"scr_regex_replace_all",
|
||||
@@ -476,10 +486,10 @@ export function keyedRecordReadInto(
|
||||
if (!typeEquals(vt, resultType)) {
|
||||
throw new LlvmUnsupportedError(`recordKeyGet:narrow:${vt.kind}`, loc);
|
||||
}
|
||||
return owned || !isRefCounted(vt) ? expr : host.retainValue(expr, vt);
|
||||
return owned || !isRefCounted(vt) ? expr : host.retainFrame(expr, vt);
|
||||
}
|
||||
if (typeEquals(vt, resultType)) {
|
||||
return owned ? expr : host.retainValue(expr, vt);
|
||||
return owned ? expr : host.retainFrame(expr, vt);
|
||||
}
|
||||
if (vt.kind === "union" && resultType.kind === "union") {
|
||||
return emitUnionWiden(host, expr, vt.unionId, resultType.unionId, owned);
|
||||
@@ -490,7 +500,7 @@ export function keyedRecordReadInto(
|
||||
if (vt.kind === "f64" || vt.kind === "bool") {
|
||||
return host.unionNewOwned(def.id, tag, { name: expr, type: vt });
|
||||
}
|
||||
const payload = owned ? expr : host.retainValue(expr, vt);
|
||||
const payload = owned ? expr : host.retainHeap(expr, vt);
|
||||
return host.unionNewOwned(def.id, tag, { name: payload, type: vt });
|
||||
};
|
||||
for (const f of overflowOnly ? [] : shape.fields) {
|
||||
|
||||
@@ -708,6 +708,10 @@ export function streamTypedRefMaterializeAdapter(
|
||||
}
|
||||
// Publish both symbols before their bodies: class-array and callback
|
||||
// converters can request this same adapter while emitting the commit.
|
||||
// A recursive field type binds capsules of this class there too, so the
|
||||
// class view ops are published first.
|
||||
const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined;
|
||||
if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`;
|
||||
adapter.commit = `@${snapshot}_commit`;
|
||||
adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot);
|
||||
const B = new BlockBuilder();
|
||||
@@ -719,10 +723,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
`llvm emitter bug: typed-ref materialize of unknown class ${t.className}`,
|
||||
);
|
||||
}
|
||||
if (liveClassOpsEligible(meta)) {
|
||||
adapter.classOps = `@${snapshot}_cops`;
|
||||
liveClassOps(host, snapshot, meta);
|
||||
}
|
||||
if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta);
|
||||
if (meta.hierarchy && meta.children.length) {
|
||||
const vtSlot = B.tmp(),
|
||||
vt = B.tmp(),
|
||||
|
||||
@@ -85,7 +85,7 @@ export function callbackAdapter(
|
||||
symbol: string,
|
||||
params: string[],
|
||||
): AbiArgument[] {
|
||||
host.moveTemp(value);
|
||||
host.toHeap(value);
|
||||
host.declare(`declare void @${symbol}(${params.join(", ")})`);
|
||||
return [ptr(value.name), ptr(`@${symbol}`)];
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
type = callbackType(cb),
|
||||
arity = type.params.length,
|
||||
second = type.params[1];
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
let h1: string,
|
||||
h2 = "null";
|
||||
if (second?.kind === "union") {
|
||||
|
||||
@@ -131,7 +131,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
case "dgram.bindCb":
|
||||
case "dgram.connect":
|
||||
case "dgram.connectCb":
|
||||
if (args[3] !== undefined) host.moveTemp(args[3]);
|
||||
if (args[3] !== undefined) host.toHeap(args[3]);
|
||||
return finishRuntimeCall(
|
||||
host,
|
||||
e,
|
||||
@@ -140,7 +140,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
);
|
||||
case "dgram.close":
|
||||
case "dgram.closeCb":
|
||||
if (args[1] !== undefined) host.moveTemp(args[1]);
|
||||
if (args[1] !== undefined) host.toHeap(args[1]);
|
||||
return finishRuntimeCall(host, e, "scr_dgram_close", [a[0]!, a[1] ?? ptr()]);
|
||||
case "dgram.address": {
|
||||
const type = e.type;
|
||||
@@ -151,7 +151,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
const record = host.B.tmp();
|
||||
host.B.line(`${record} = call ptr @${mangleRecordNew(type.shapeId)}()`);
|
||||
const out = host.own({ name: record, type });
|
||||
host.moveTemp(ip);
|
||||
host.toHeap(ip);
|
||||
const shape = host.recordsById.get(type.shapeId);
|
||||
if (shape === undefined) throw new InternalCompilerError("missing datagram address shape");
|
||||
for (const field of ["address", "family", "port"]) {
|
||||
@@ -185,7 +185,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
? "scr_dgram_msg_thunk1"
|
||||
: messageAdapter(host, type.params[1]!);
|
||||
// Program-specific adapters are definitions, so do not redeclare them.
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
if (type.params.length < 2)
|
||||
host.declare(`declare void @${adapter}(ptr, ptr, ptr, ptr, double, double)`);
|
||||
return finishRuntimeCall(host, e, "scr_dgram_on_message", [
|
||||
@@ -213,7 +213,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
case "dgram.onListening":
|
||||
case "dgram.onClose":
|
||||
case "dgram.onConnect":
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
return finishRuntimeCall(
|
||||
host,
|
||||
e,
|
||||
@@ -227,7 +227,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
case "dns.lookup": {
|
||||
const cb = args[2]!,
|
||||
symbol = lookupAdapter(host, cb);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
if (callbackType(cb).params.length === 0)
|
||||
host.declare("declare void @scr_dns_thunk0(ptr, ptr, ptr, double)");
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup", [...a, ptr("@" + symbol)]);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { emitTestLibCall } from "./lib-test.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import { isDynUndefined } from "../../ir/runtime-features.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { rawBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
|
||||
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -90,7 +92,7 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func");
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
const adapter =
|
||||
cbT.params.length === 0
|
||||
? "scr_rl_answer_thunk0"
|
||||
@@ -163,7 +165,7 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
const ev = host.emitExpr(evArg);
|
||||
const cb = host.emitExpr(e.args[2]!);
|
||||
const once = host.emitExpr(e.args[3]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
let adapter = "null";
|
||||
if (evArg.value === 0 || evArg.value === 4) {
|
||||
const param = cbT.params[0];
|
||||
@@ -285,7 +287,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const cbT = e.args[4]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: randomFill callback not a func");
|
||||
host.moveTemp(args[4]!);
|
||||
host.toHeap(args[4]!);
|
||||
const dyn = e.fn === "crypto.randomFillCbDyn";
|
||||
const adapter = host.cryptoBytesThunkFor(cbT, false, dyn ? "dyn" : "view");
|
||||
const symbol = dyn ? "scr_crypto_random_fill_async_dyn" : "scr_crypto_random_fill_async";
|
||||
@@ -303,7 +305,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: generateKey callback not a func");
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
const adapter = host.cryptoBytesThunkFor(cbT, false, "dyn");
|
||||
host.declare(`declare void @scr_crypto_generate_key_async(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -318,7 +320,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const cbT = e.args[3]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: randomInt callback not a func");
|
||||
host.moveTemp(args[3]!);
|
||||
host.toHeap(args[3]!);
|
||||
const adapter = host.cryptoIntThunkFor(cbT);
|
||||
host.declare(`declare void @scr_crypto_random_int_async(double, double, double, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -427,7 +429,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const cbT = e.args[cbIndex]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: crypto callback not a func");
|
||||
host.moveTemp(args[cbIndex]!);
|
||||
host.toHeap(args[cbIndex]!);
|
||||
const adapter = host.cryptoBytesThunkFor(cbT, e.fn === "crypto.hkdfCb");
|
||||
if (e.fn === "crypto.randomBytesCb") {
|
||||
host.declare(`declare void @scr_crypto_random_bytes_async(double, ptr, ptr)`);
|
||||
@@ -484,7 +486,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const cbT = e.args[cbIdx]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
host.moveTemp(args[cbIdx]!);
|
||||
host.toHeap(args[cbIdx]!);
|
||||
cb = args[cbIdx]!.name;
|
||||
const sym = cbT.params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res";
|
||||
host.declare(`declare void @${sym}(ptr, ptr)`);
|
||||
@@ -524,7 +526,13 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
|
||||
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
}
|
||||
const sym = mockTimerSym(host, e) ?? LIB_FN_SYMS[e.fn];
|
||||
// queueMicrotask's checked-dynamic form carries the AsyncLocalStorage
|
||||
// context like the static one (lib-process.ts).
|
||||
const sym =
|
||||
mockTimerSym(host, e) ??
|
||||
(e.fn === "timers.queueMicrotaskDyn" && host.schedulesInContext
|
||||
? "scr_queue_microtask_dyn_als"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
@@ -642,8 +650,66 @@ function emitAssertFailing(
|
||||
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
||||
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
||||
|
||||
/** The parameter count of a replace callback the runtime can call through
|
||||
* the closure ABI (scr_regex_replace_fn), or null when it needs the dyn
|
||||
* box. Each declared parameter must receive a value of its declared kind
|
||||
* in every match: the match string at 0; with more than one parameter the
|
||||
* pattern must be a literal whose captures are known, so position i is a
|
||||
* capture that every match sets (a string), the offset (a number) right
|
||||
* after the captures, or the subject (a string) after that. */
|
||||
function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
||||
if (callback.ret.kind !== "string") return null;
|
||||
const params = callback.params;
|
||||
if (params.length > 4) return null;
|
||||
if (params.length === 0) return 0;
|
||||
if (params[0]!.kind !== "string") return null;
|
||||
if (params.length === 1) return 1;
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
const captures = regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
for (let i = 1; i < params.length; i++) {
|
||||
const kind = params[i]!.kind;
|
||||
const ok =
|
||||
i < captures.count
|
||||
? kind === "string" && captures.always[i] === true
|
||||
: i === captures.count
|
||||
? kind === "f64"
|
||||
: i === captures.count + 1 && kind === "string";
|
||||
if (!ok) return null;
|
||||
}
|
||||
return params.length;
|
||||
}
|
||||
|
||||
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
||||
* call directly: the dyn box the IR wraps it in is never observable (the
|
||||
* runtime only calls it, with `this` undefined), so the closure goes to
|
||||
* scr_regex_replace_fn as it is. Null keeps the generic path. */
|
||||
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const [subject, pattern, callback, all] = e.args;
|
||||
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
||||
const arity = typedReplacerArity(callback.value.type, pattern);
|
||||
if (arity === null) return null;
|
||||
const B = host.B;
|
||||
const s = host.emitExpr(subject);
|
||||
const re = host.emitExpr(pattern);
|
||||
const fn = host.emitExpr(callback.value);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`,
|
||||
);
|
||||
const output = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return output;
|
||||
}
|
||||
|
||||
export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true;
|
||||
if (e.fn === "regex.replaceCallback") {
|
||||
const typed = emitTypedRegexReplace(host, e);
|
||||
if (typed) return typed;
|
||||
}
|
||||
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
||||
if (
|
||||
e.fn === "sp.pipeline" ||
|
||||
@@ -656,7 +722,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
// Literals (`123n`) and constant BigInt("...") texts are immortal
|
||||
// statics instead of a parse at every evaluation.
|
||||
const sym = host.internBigIntLiteral(e.args[0].value);
|
||||
if (sym) return host.own({ name: host.retainValue(sym, e.type), type: e.type });
|
||||
if (sym) return host.own({ name: host.retainFrame(sym, e.type), type: e.type });
|
||||
}
|
||||
if (e.fn.startsWith("bigint.")) {
|
||||
// Small-value operator trees run in 64-bit registers (bigint-wrap.ts).
|
||||
@@ -774,6 +840,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "blob":
|
||||
case "record":
|
||||
case "disposable":
|
||||
case "punycode":
|
||||
case "constants":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "test":
|
||||
return emitTestLibCall(host, e);
|
||||
|
||||
@@ -193,7 +193,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: zlib callback not a func");
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const adapter = host.zlibBytesThunkFor(cbT);
|
||||
const compressing =
|
||||
e.fn === "zlib.deflateCb" || e.fn === "zlib.deflateRawCb" || e.fn === "zlib.gzipCb";
|
||||
@@ -246,7 +246,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const cbT = e.args[3]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: zlib callback not a func");
|
||||
host.moveTemp(args[3]!);
|
||||
host.toHeap(args[3]!);
|
||||
const adapter = host.zlibBytesThunkFor(cbT);
|
||||
const sym =
|
||||
e.fn === "zlib.cbOpts" ? "scr_zlib_codec_async_opts" : "scr_zlib_codec_async_opts_dyn";
|
||||
@@ -293,7 +293,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const operands = args.slice(0, withCb ? -1 : undefined).map((a) => `ptr ${a.name}`);
|
||||
let cb = "null";
|
||||
if (withCb) {
|
||||
host.moveTemp(args[args.length - 1]!);
|
||||
host.toHeap(args[args.length - 1]!);
|
||||
cb = args[args.length - 1]!.name;
|
||||
}
|
||||
const sym =
|
||||
@@ -356,7 +356,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.rename callback not a func");
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
const adapter = host.fsRenameThunkFor(cbT);
|
||||
host.declare(`declare void @scr_fs_rename_async(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -403,7 +403,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const countField =
|
||||
e.fn === "fileHandle.read" || e.fn === "fileHandle.readv" ? "bytesRead" : "bytesWritten";
|
||||
host.storeField(host.recordFieldPtr(rec, inner.shapeId, countField).ptr, F64, count, false);
|
||||
const payload = host.retainValue(args[1]!.name, e.args[1]!.type);
|
||||
const payload = host.retainHeap(args[1]!.name, e.args[1]!.type);
|
||||
B.line(
|
||||
`store ptr ${payload}, ptr ${host.recordFieldPtr(rec, inner.shapeId, vector ? "buffers" : "buffer").ptr}`,
|
||||
);
|
||||
@@ -465,7 +465,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watchCb callback not a func");
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`);
|
||||
@@ -618,7 +618,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
B.line(`${nm} = call ptr @scr_fs_scandir_name(ptr ${snap}, ${host.sizeType} ${i}) ; +1`);
|
||||
B.line(`store ptr ${nm}, ptr ${host.recordFieldPtr(row, recT.shapeId, "name").ptr}`);
|
||||
B.line(
|
||||
`store ptr ${host.retainValue(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`,
|
||||
`store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`,
|
||||
);
|
||||
host.arrPush(arr, "ref", row); // push takes ownership of the row
|
||||
const i2 = B.tmp();
|
||||
@@ -630,7 +630,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
if (!promiseForm) return arrayOut;
|
||||
const rc = vAdapters(host.shapeHost, arrayT);
|
||||
host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`);
|
||||
host.moveTemp(arrayOut); // promise fulfillment owns the result array
|
||||
host.toHeap(arrayOut); // promise fulfillment owns the result array
|
||||
const promise = B.tmp();
|
||||
B.line(
|
||||
`${promise} = call ptr @scr_promise_settled_ref(ptr ${arr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, arrayT)})`,
|
||||
@@ -696,7 +696,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
if (!promiseForm) return value;
|
||||
const rc = vAdapters(host.shapeHost, unionT);
|
||||
host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`);
|
||||
host.moveTemp(value);
|
||||
host.toHeap(value);
|
||||
const promise = B.tmp();
|
||||
B.line(
|
||||
`${promise} = call ptr @scr_promise_settled_ref(ptr ${value.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, unionT)})`,
|
||||
@@ -941,7 +941,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const retained = B.tmp();
|
||||
B.line(`${retained} = call ptr @scr_arr_retain_v(ptr ${peeked})`);
|
||||
B.line(`store ptr ${retained}, ptr ${rowsSlot}`);
|
||||
host.releaseValue(cell, iv);
|
||||
host.releaseFrame(cell, iv);
|
||||
B.br(lj);
|
||||
B.startBlock(lm);
|
||||
const fresh = B.tmp();
|
||||
|
||||
@@ -170,7 +170,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
|
||||
type = arms[tag]!;
|
||||
value = host.unwrapNullableClosure(value, tag);
|
||||
} else host.moveTemp(cb);
|
||||
} else host.toHeap(cb);
|
||||
if (type.kind !== "func" || type.params[1] === undefined)
|
||||
throw new InternalCompilerError("SNI callback lacks its answer function");
|
||||
sni = [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))];
|
||||
@@ -207,7 +207,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
case "http2.streamCloseCb": {
|
||||
const stream = e.fn.startsWith("http2.stream"),
|
||||
cbIndex = stream ? 2 : 1;
|
||||
if (args[cbIndex] !== undefined) host.moveTemp(args[cbIndex]!);
|
||||
if (args[cbIndex] !== undefined) host.toHeap(args[cbIndex]!);
|
||||
return finishRuntimeCall(
|
||||
host,
|
||||
e,
|
||||
@@ -310,7 +310,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
default:
|
||||
throw new InternalCompilerError(`unhandled HTTP/2 listener ${e.fn}`);
|
||||
}
|
||||
if (thunk === null) host.moveTemp(cb);
|
||||
if (thunk === null) host.toHeap(cb);
|
||||
const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params);
|
||||
return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
);
|
||||
}
|
||||
host.usesTimers = true;
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
return finishRuntimeCall(host, e, "scr_net_connect_lookup", [
|
||||
...args.map((arg) => abiValue(host, arg)),
|
||||
ptr("@" + symbol),
|
||||
|
||||
@@ -42,7 +42,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (e.fn === "http.requestConn" || e.fn === "http.requestConnCb") {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
host.usesTimers = true;
|
||||
host.moveTemp(args[0]!);
|
||||
host.toHeap(args[0]!);
|
||||
const cb = args[6];
|
||||
const handler =
|
||||
cb === undefined
|
||||
@@ -72,7 +72,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const cbT = e.args[0]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: net.createServerCb handler not a func");
|
||||
host.moveTemp(args[0]!);
|
||||
host.toHeap(args[0]!);
|
||||
cb = args[0]!.name;
|
||||
adapter = cbT.params.length === 0 ? "@scr_net_conn_thunk0" : "@scr_net_conn_thunk_sock";
|
||||
host.declare(`declare void ${adapter}(ptr, ptr)`);
|
||||
@@ -86,7 +86,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
let cb = "null";
|
||||
if (e.fn === "net.listenCb") {
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
cb = args[2]!.name;
|
||||
}
|
||||
host.declare(`declare void @scr_net_listen(ptr, double, ptr)`);
|
||||
@@ -109,7 +109,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (withCallback) {
|
||||
const cbT = e.args[cbIndex]!.type;
|
||||
if (cbT.kind === "func") {
|
||||
host.moveTemp(args[cbIndex]!);
|
||||
host.toHeap(args[cbIndex]!);
|
||||
cb = args[cbIndex]!.name;
|
||||
} else {
|
||||
if (cbT.kind !== "union")
|
||||
@@ -192,7 +192,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
let cb = "null";
|
||||
if (e.fn === "net.serverCloseCb") {
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
cb = args[1]!.name;
|
||||
}
|
||||
host.declare(`declare void @scr_net_server_close(ptr, ptr)`);
|
||||
@@ -233,7 +233,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
throw new InternalCompilerError("llvm emitter bug: close override not a func");
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const wrapSym = host.closeOverrideWrapFor(cbT.params[0]!, cbT.ret.kind === "netServer");
|
||||
host.moveTemp(args[1]!); // ownership moves into the wrapper's env box
|
||||
host.toHeap(args[1]!); // ownership moves into the wrapper's env box
|
||||
const wrap = host.wrapEmitterListener(args[1]!.name, wrapSym);
|
||||
host.declare(`declare void @scr_net_server_set_close_override(ptr, ptr)`);
|
||||
B.line(`call void @scr_net_server_set_close_override(ptr ${args[0]!.name}, ptr ${wrap})`);
|
||||
@@ -244,7 +244,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
||||
const entry =
|
||||
e.fn === "net.serverOnError" ? "scr_net_server_on_error" : "scr_net_sock_on_error";
|
||||
@@ -295,7 +295,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
"http.clientOnContinue": "scr_http_client_on_continue",
|
||||
}[e.fn]!;
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
host.declare(`declare void @${entry}(ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, i1 ${args[2]!.name})`);
|
||||
return { name: "", type: e.type };
|
||||
@@ -312,7 +312,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[cbIndex]!);
|
||||
host.toHeap(args[cbIndex]!);
|
||||
const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
if (e.fn === "http.serverSetTimeoutCb") {
|
||||
@@ -340,7 +340,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
let cb = "null";
|
||||
if (e.fn === "net.connectCb") {
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
cb = args[2]!.name;
|
||||
}
|
||||
host.declare(`declare ptr @scr_net_connect_checked(double, ptr, ptr)`);
|
||||
@@ -357,7 +357,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const adapter =
|
||||
cbT.params.length === 0
|
||||
? "scr_net_data_thunk0"
|
||||
@@ -533,7 +533,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const cbT = e.args[0]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: http.createServer handler not a func");
|
||||
host.moveTemp(args[0]!);
|
||||
host.toHeap(args[0]!);
|
||||
cb = args[0]!.name;
|
||||
const sym =
|
||||
cbT.params.length === 2
|
||||
@@ -554,7 +554,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} handler not a func`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const adapter =
|
||||
cbT.params.length === 2
|
||||
? "scr_http_handler_thunk2"
|
||||
@@ -580,7 +580,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const adapter =
|
||||
cbT.params.length === 3
|
||||
? "scr_http_upgrade_thunk3"
|
||||
@@ -607,7 +607,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const adapter =
|
||||
e.fn === "http.clientOnResponse"
|
||||
? cbT.params.length === 0
|
||||
@@ -630,7 +630,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
}
|
||||
if (e.fn === "http.resOnFinish" || e.fn === "http.resOnEndCallback") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
const entry =
|
||||
e.fn === "http.resOnFinish" ? "scr_http_res_on_finish" : "scr_http_res_on_end_callback";
|
||||
host.declare(`declare void @${entry}(ptr, ptr)`);
|
||||
@@ -639,7 +639,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
}
|
||||
if (e.fn === "http.resSetTimeoutCb" || e.fn === "http.reqSetTimeoutCb") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
const entry =
|
||||
e.fn === "http.resSetTimeoutCb" ? "scr_http_res_set_timeout" : "scr_http_req_set_timeout";
|
||||
host.declare(`declare void @${entry}(ptr, double, ptr)`);
|
||||
@@ -650,7 +650,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
}
|
||||
if (e.fn === "http.clientSetTimeoutCb") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
host.declare("declare void @scr_http_client_set_timeout_cb(ptr, double, ptr)");
|
||||
B.line(
|
||||
`call void @scr_http_client_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`,
|
||||
@@ -659,21 +659,21 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
}
|
||||
if (e.fn === "net.sockOnFinish") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
host.declare(`declare void @scr_net_sock_on_finish(ptr, ptr)`);
|
||||
B.line(`call void @scr_net_sock_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "net.sockOnWriteFlush") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
host.declare(`declare void @scr_net_sock_on_write_flush(ptr, ptr)`);
|
||||
B.line(`call void @scr_net_sock_on_write_flush(ptr ${args[0]!.name}, ptr ${args[1]!.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "net.sockSetTimeoutCb") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
host.declare("declare void @scr_net_sock_set_timeout_cb(ptr, double, ptr)");
|
||||
B.line(
|
||||
`call void @scr_net_sock_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`,
|
||||
|
||||
@@ -18,7 +18,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const cmd = host.emitExpr(e.args[0]!);
|
||||
const argv = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(callbackArg);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
const shell = host.emitExpr(e.args[3]!);
|
||||
const adapter = host.execFileThunkFor(cbT);
|
||||
host.usesTimers = true;
|
||||
@@ -72,7 +72,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
if (e.fn === "child.onSpawn") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
host.declare("declare void @scr_child_on_spawn(ptr, ptr)");
|
||||
B.line(`call void @scr_child_on_spawn(ptr ${child.name}, ptr ${cb.name})`);
|
||||
return { name: "", type: e.type };
|
||||
@@ -87,7 +87,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
let adapter: string;
|
||||
if (cbT.params.length === 0) {
|
||||
adapter = "scr_child_exit_thunk0";
|
||||
@@ -110,7 +110,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
throw new InternalCompilerError("llvm emitter bug: child.onError callback not a func");
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_child_on_error(ptr, ptr, ptr)`);
|
||||
@@ -136,7 +136,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: child.send callback not a func");
|
||||
const cb = host.emitExpr(e.args[2]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
suffix = `, ptr ${cb.name}, ptr @${host.ipcSendThunkFor(cbT)}`;
|
||||
decl += ", ptr, ptr";
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
const once = host.emitExpr(e.args[2]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
if (e.fn === "child.onMessage") {
|
||||
host.declare(`declare void @scr_child_ipc_on_message(ptr, ptr, ptr, i1 zeroext)`);
|
||||
B.line(
|
||||
@@ -337,7 +337,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
const once = host.emitExpr(e.args[2]!);
|
||||
if (e.fn === "writer.onError") {
|
||||
const adapter =
|
||||
@@ -373,17 +373,19 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
// main runs the loop.
|
||||
host.usesTimers = true;
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
// Programs that enter AsyncLocalStorage stores schedule through the
|
||||
// *_als entry points, which carry the active stores into the callback.
|
||||
const sym =
|
||||
mockTimerSym(host, e) ??
|
||||
{
|
||||
"timers.setTimeout": "scr_set_timeout",
|
||||
"timers.setInterval": "scr_set_interval",
|
||||
"timers.setTimeoutHandle": "scr_set_timeout_handle",
|
||||
"timers.setImmediate": "scr_set_immediate",
|
||||
"process.nextTick": "scr_next_tick",
|
||||
"timers.queueMicrotask": "scr_queue_microtask",
|
||||
}[e.fn];
|
||||
(mockTimerSym(host, e) ??
|
||||
{
|
||||
"timers.setTimeout": "scr_set_timeout",
|
||||
"timers.setInterval": "scr_set_interval",
|
||||
"timers.setTimeoutHandle": "scr_set_timeout_handle",
|
||||
"timers.setImmediate": "scr_set_immediate",
|
||||
"process.nextTick": "scr_next_tick",
|
||||
"timers.queueMicrotask": "scr_queue_microtask",
|
||||
}[e.fn]!) + (host.schedulesInContext ? "_als" : "");
|
||||
const rest = e.args.slice(1).map((a) => host.emitExpr(a));
|
||||
const argList = [`ptr ${cb.name}`, ...rest.map((a) => `${host.llType(a.type)} ${a.name}`)].join(
|
||||
", ",
|
||||
@@ -464,7 +466,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: process.send callback not a func");
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
suffix = `, ptr ${cb.name}, ptr @${host.ipcSendThunkFor(cbT)}`;
|
||||
decl += ", ptr, ptr";
|
||||
}
|
||||
@@ -486,7 +488,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
if (e.fn === "process.onMessage") {
|
||||
host.declare(`declare void @scr_process_ipc_on_message(ptr, ptr, i1 zeroext)`);
|
||||
B.line(
|
||||
@@ -507,7 +509,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: process write callback not a func");
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
const adapter = host.fsRenameThunkFor(cbT);
|
||||
const write =
|
||||
e.fn === "process.stdoutWriteBytesCb"
|
||||
@@ -538,7 +540,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
if (e.fn === "stdin.onEnd") {
|
||||
host.declare(`declare void @scr_stdin_on_end(ptr, i1 zeroext)`);
|
||||
B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`);
|
||||
@@ -586,7 +588,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
throw new InternalCompilerError("llvm emitter bug: process.onExit callback not a func");
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code";
|
||||
host.declare(`declare void @${adapter}(ptr, double)`);
|
||||
host.declare(`declare void @scr_process_on_exit(ptr, ptr, i1 zeroext)`);
|
||||
@@ -741,7 +743,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const cbr = B.tmp();
|
||||
B.line(`${cbr} = call ptr @scr_closure_retain_v(ptr ${args[2]!.name})`);
|
||||
const wrapped = host.wrapEmitterListener(cbr, adapterFn);
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
host.declare(
|
||||
`declare ptr @scr_emitter_on_via(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`,
|
||||
);
|
||||
@@ -765,7 +767,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
throw new InternalCompilerError("llvm emitter bug: emitter.onDyn adapter not a func");
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const { fn: adapterFn, shim } = host.emitterFixedAdapter(adT);
|
||||
host.moveTemp(args[3]!); // the frame's +1 moves into the wrapper's box
|
||||
host.toHeap(args[3]!); // the frame's +1 moves into the wrapper's box
|
||||
const wrapped = host.wrapEmitterListener(args[3]!.name, adapterFn);
|
||||
host.declare(
|
||||
`declare ptr @scr_emitter_on_dyn(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`,
|
||||
@@ -864,7 +866,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
if (isDyn) {
|
||||
host.moveTemp(args[3]!);
|
||||
host.toHeap(args[3]!);
|
||||
const wrapped = host.wrapEmitterListener(args[3]!.name, adapterFn);
|
||||
host.declare(
|
||||
`declare ptr @scr_emitter_on_dyn(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`,
|
||||
@@ -878,7 +880,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const cbr = B.tmp();
|
||||
B.line(`${cbr} = call ptr @scr_closure_retain_v(ptr ${args[2]!.name})`);
|
||||
const wrapped = host.wrapEmitterListener(cbr, adapterFn);
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
host.declare(
|
||||
`declare ptr @scr_emitter_on_via(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`,
|
||||
);
|
||||
|
||||
@@ -144,6 +144,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.toFixed0": "scr_num_to_fixed0",
|
||||
"num.toFixed": "scr_num_to_fixed",
|
||||
"num.toStringRadix": "scr_num_to_string_radix",
|
||||
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
|
||||
"num.sameValue": "scr_num_same_value",
|
||||
"intl.numFormatEnUs": "scr_intl_num_format_en_us",
|
||||
"number.isFinite": "scr_num_is_finite",
|
||||
@@ -194,6 +195,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"os.cpusModel": "scr_os_cpus_model",
|
||||
"os.cpusField": "scr_os_cpus_field",
|
||||
"os.constants": "scr_os_constants",
|
||||
"constants.object": "scr_node_constants",
|
||||
"os.release": "scr_os_release",
|
||||
"os.userName": "scr_os_user_name",
|
||||
"os.userShell": "scr_os_user_shell",
|
||||
@@ -314,6 +316,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getUTCDate": "scr_date_get_date_utc",
|
||||
"date.getDay": "scr_date_get_day_local",
|
||||
"date.getUTCDay": "scr_date_get_day_utc",
|
||||
"date.toUTCString": "scr_date_to_utc_string",
|
||||
"date.getHours": "scr_date_get_hours_local",
|
||||
"date.getUTCHours": "scr_date_get_hours_utc",
|
||||
"date.getMinutes": "scr_date_get_minutes_local",
|
||||
@@ -323,6 +326,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getMilliseconds": "scr_date_get_milliseconds",
|
||||
"date.getUTCMilliseconds": "scr_date_get_milliseconds",
|
||||
"date.getTimezoneOffset": "scr_date_get_timezone_offset",
|
||||
"date.toString": "scr_date_to_string",
|
||||
"date.toDateString": "scr_date_to_date_string",
|
||||
"date.toTimeString": "scr_date_to_time_string",
|
||||
"fs.existsSync": "scr_fs_exists",
|
||||
// ── the throwing slice (MAY_THROW_LIB_FNS members): the generic path
|
||||
// emits the standard pending check after each — exprs.ts's finish.
|
||||
@@ -405,6 +411,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.fromDyn": "scr_buffer_from_dyn",
|
||||
"buffer.concat": "scr_buffer_concat",
|
||||
"buffer.concatLen": "scr_buffer_concat_len",
|
||||
"buffer.concatDyn": "scr_buffer_concat_dyn",
|
||||
"buffer.concatLenDyn": "scr_buffer_concat_len_dyn",
|
||||
"buffer.alloc": "scr_buffer_alloc",
|
||||
"buffer.allocUnsafe": "scr_buffer_alloc_unsafe",
|
||||
"buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow",
|
||||
@@ -638,6 +646,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"url.legacyFormat": "scr_url_legacy_format",
|
||||
"url.legacyResolve": "scr_url_legacy_resolve",
|
||||
"url.domainToASCII": "scr_url_domain_to_ascii",
|
||||
"punycode.decode": "scr_punycode_decode",
|
||||
"punycode.encode": "scr_punycode_encode",
|
||||
"punycode.toASCII": "scr_punycode_to_ascii",
|
||||
"punycode.toUnicode": "scr_punycode_to_unicode",
|
||||
"punycode.ucs2Decode": "scr_punycode_ucs2_decode",
|
||||
"punycode.ucs2Encode": "scr_str_from_code_point",
|
||||
"punycode.warn": "scr_punycode_warn",
|
||||
"url.domainToUnicode": "scr_url_domain_to_unicode",
|
||||
"url.pathToFileURL": "scr_url_from_path",
|
||||
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
|
||||
@@ -809,6 +824,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// ambient-this read never throw. The fs dyn read is the sync-fs story.
|
||||
"fetch.start": "scr_fetch_static",
|
||||
"fetch.responseNew": "scr_fetch_response_new",
|
||||
"fetch.responseJsonStatic": "scr_fetch_response_json_static",
|
||||
"fetch.input": "scr_fetch_input",
|
||||
"abort.controllerNew": "scr_fetch_abort_controller_new",
|
||||
"event.targetNew": "scr_event_target_new",
|
||||
@@ -870,12 +886,16 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
|
||||
"dyn.classIs": "scr_dyn_class_is_key",
|
||||
"dyn.iterPack": "scr_dyn_iter_pack",
|
||||
"dyn.destructureRest": "scr_dyn_destructure_rest",
|
||||
"dyn.iterator": "scr_dyn_iterator",
|
||||
"dyn.arrayFromIterator": "scr_dyn_array_from_iterator",
|
||||
"dyn.iteratorResult": "scr_dyn_iterator_result",
|
||||
"dyn.iteratorCanStep": "scr_dyn_iterator_can_step",
|
||||
"dyn.iteratorStep": "scr_dyn_iterator_step",
|
||||
"dyn.iteratorStepDone": "scr_dyn_iterator_step_done",
|
||||
"dyn.setGeneratorMembers": "scr_dyn_set_generator_members",
|
||||
"dyn.iteratorHelperMethod": "scr_dyn_iterator_helper_method",
|
||||
"dyn.collectionIteratorIndex": "scr_dyn_collection_iterator_index",
|
||||
"dyn.mapSeedEntries": "scr_dyn_map_seed_entries",
|
||||
"dyn.mapSeedEntry": "scr_dyn_map_seed_entry",
|
||||
"dyn.arrLen": "scr_dyn_arr_len",
|
||||
@@ -1231,9 +1251,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"http.reqSetTimeout": "scr_http_req_set_timeout_plain",
|
||||
"http.clientDestroy": "scr_http_client_destroy",
|
||||
"http.clientDestroyError": "scr_http_client_destroy_error",
|
||||
"http.globalAgent": "scr_http_global_agent_value",
|
||||
"http.serverMaxHeaderSize": "scr_http_server_max_header_size",
|
||||
"http.clientMaxHeaderSize": "scr_http_client_max_header_size",
|
||||
"https.clientServername": "scr_https_client_servername",
|
||||
"http.clientSignalCheck": "scr_http_client_signal_check",
|
||||
"https.nextServername": "scr_https_set_next_servername",
|
||||
"http.clientAbort": "scr_http_client_abort",
|
||||
"http.clientAborted": "scr_http_client_aborted",
|
||||
"http.clientDestroyed": "scr_http_client_destroyed",
|
||||
@@ -1394,6 +1416,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"regex.groupEquals": "scr_regex_group_eq",
|
||||
"regex.groupNumber": "scr_regex_group_number",
|
||||
"regex.groupParseInt": "scr_regex_group_parse_int",
|
||||
"regex.resultProp": "scr_regex_result_prop",
|
||||
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
|
||||
// throws synchronously), the minted setImmediate value, and the
|
||||
// timers/promises immediate — all mark the loop live.
|
||||
|
||||
@@ -48,7 +48,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const s0 = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
const once = host.emitExpr(e.args[2]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
const param = cbT.params[0];
|
||||
let adapter: string;
|
||||
if (param === undefined) {
|
||||
@@ -70,7 +70,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const s0 = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
const once = host.emitExpr(e.args[2]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
host.declare(`declare void @scr_child_stream_on_end(ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @scr_child_stream_on_end(ptr ${s0.name}, ptr ${cb.name}, i1 ${once.name})`);
|
||||
return { name: "", type: e.type };
|
||||
@@ -86,7 +86,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const s0 = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
const once = host.emitExpr(e.args[2]!);
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`);
|
||||
B.line(
|
||||
@@ -127,7 +127,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
}
|
||||
const cb = args[at]!;
|
||||
const cbT = e.args[at]!.type;
|
||||
host.moveTemp(cb); // the callback closure MOVES into the stream
|
||||
host.toHeap(cb); // the callback closure MOVES into the stream
|
||||
cbArgs.push(`ptr ${cb.name}`, `ptr @${host.streamCbThunkFor(canonical[i]!, cbT)}`);
|
||||
at++;
|
||||
}
|
||||
@@ -182,7 +182,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const recv = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
const cbT = e.args[1]!.type;
|
||||
host.moveTemp(cb); // the callback closure MOVES into the stream
|
||||
host.toHeap(cb); // the callback closure MOVES into the stream
|
||||
const sym = symOf[e.fn]!;
|
||||
host.declare(`declare void @${sym}(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -210,7 +210,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
}
|
||||
const cb = args[at]!;
|
||||
const cbT = e.args[at]!.type;
|
||||
host.moveTemp(cb);
|
||||
host.toHeap(cb);
|
||||
cbArgs.push(`ptr ${cb.name}`, `ptr @${host.streamCbThunkFor(canonical[i]!, cbT)}`);
|
||||
at++;
|
||||
}
|
||||
@@ -321,7 +321,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
B.line(`${raw} = call ptr @scr_stream_read(ptr ${args[0]!.name}, double ${args[1]!.name})`);
|
||||
const b = host.own({ name: raw, type: def!.arms[bytesTag]! });
|
||||
host.emitPendingCheck();
|
||||
host.moveTemp(b); // moves into the union arm when present
|
||||
host.toHeap(b); // moves into the union arm when present
|
||||
return host.wrapNullable(raw, raw, def!.arms[bytesTag]!, bytesTag, e.type, nullTag);
|
||||
}
|
||||
if (e.fn === "readable.flowing") {
|
||||
@@ -383,7 +383,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
let cb = "null";
|
||||
if (e.fn !== "writable.writeDyn" && e.args.length > 2) {
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
cb = args[2]!.name;
|
||||
}
|
||||
host.declare(`declare zeroext i1 @${entry}(ptr, ptr, ptr)`);
|
||||
@@ -412,7 +412,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
else if (flags & 8) chunkD = args[at++]!.name;
|
||||
let cbName = "null";
|
||||
if (flags & 4) {
|
||||
host.moveTemp(args[at]!);
|
||||
host.toHeap(args[at]!);
|
||||
cbName = args[at]!.name;
|
||||
}
|
||||
if (chunkD !== null) {
|
||||
@@ -490,7 +490,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: stream.finished callback not a func");
|
||||
host.moveTemp(args[1]!); // the watcher closure MOVES into the stream
|
||||
host.toHeap(args[1]!); // the watcher closure MOVES into the stream
|
||||
const thunk = host.streamCbThunkFor("e", cbT);
|
||||
host.declare(`declare ptr @scr_stream_finished(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -599,7 +599,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: stream.finishedOpts callback not a func",
|
||||
);
|
||||
host.moveTemp(args[cbIdx]!);
|
||||
host.toHeap(args[cbIdx]!);
|
||||
const thunk = host.streamCbThunkFor("e", cbT);
|
||||
host.declare(
|
||||
`declare ptr @scr_stream_finished_opts(ptr, ptr, ptr, double, double, i1, ptr, ptr)`,
|
||||
@@ -696,7 +696,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: stream.pipelineStages callback not a func",
|
||||
);
|
||||
host.moveTemp(args[args.length - 1]!);
|
||||
host.toHeap(args[args.length - 1]!);
|
||||
const thunk = host.streamCbThunkFor("e", cbT);
|
||||
host.declare(`declare ptr @scr_stream_pipeline_stages(${params}, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -732,7 +732,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const cbT = e.args[1 + n]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: stream.pipeline callback not a func");
|
||||
host.moveTemp(args[1 + n]!);
|
||||
host.toHeap(args[1 + n]!);
|
||||
const thunk = host.streamCbThunkFor("e", cbT);
|
||||
host.declare(`declare ptr @scr_stream_pipeline(double, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
|
||||
@@ -8,7 +8,7 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
|
||||
host.usesTimers = true;
|
||||
switch (e.fn) {
|
||||
case "test.register":
|
||||
host.moveTemp(args[3]!);
|
||||
host.toHeap(args[3]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_register", a);
|
||||
case "test.registerEmpty":
|
||||
return finishRuntimeCall(host, e, "scr_test_register", [
|
||||
@@ -17,13 +17,13 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
|
||||
...a.slice(3),
|
||||
]);
|
||||
case "test.suite":
|
||||
host.moveTemp(args[3]!);
|
||||
host.toHeap(args[3]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_suite", a);
|
||||
case "test.hook":
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_hook", a);
|
||||
case "test.sub":
|
||||
host.moveTemp(args[4]!);
|
||||
host.toHeap(args[4]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_sub", a);
|
||||
case "test.subEmpty": {
|
||||
const promise = callRuntime(host, "scr_test_sub", "ptr", [
|
||||
@@ -49,22 +49,26 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_passed", a);
|
||||
case "test.workerId":
|
||||
return finishRuntimeCall(host, e, "scr_test_worker_id", a);
|
||||
case "test.hasContext":
|
||||
return finishRuntimeCall(host, e, "scr_test_has_context", a);
|
||||
case "test.context":
|
||||
return finishRuntimeCall(host, e, "scr_test_context", a);
|
||||
case "test.ctxHook":
|
||||
host.moveTemp(args[2]!);
|
||||
host.toHeap(args[2]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_hook", a);
|
||||
case "test.ctxPlan":
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_plan", a);
|
||||
case "test.ctxCount":
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_count", a);
|
||||
case "test.mockTrack":
|
||||
host.moveTemp(args[0]!);
|
||||
host.toHeap(args[0]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_mock_track", a);
|
||||
case "test.mockRestoreAll":
|
||||
return finishRuntimeCall(host, e, "scr_test_mock_restore_all", a);
|
||||
case "test.mockReset":
|
||||
return finishRuntimeCall(host, e, "scr_test_mock_reset", a);
|
||||
case "test.ctxMockTrack":
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_mock_track", a);
|
||||
case "test.ctxMockRestoreAll":
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a);
|
||||
|
||||
@@ -94,10 +94,10 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
}
|
||||
case "tls.connect":
|
||||
case "tls.connectCb":
|
||||
if (args[3] !== undefined) host.moveTemp(args[3]);
|
||||
if (args[3] !== undefined) host.toHeap(args[3]);
|
||||
return finishRuntimeCall(host, e, "scr_tls_connect_dyn", [...a.slice(0, 3), a[3] ?? ptr()]);
|
||||
case "tls.sockOnSecureConnect":
|
||||
host.moveTemp(args[1]!);
|
||||
host.toHeap(args[1]!);
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a);
|
||||
case "tls.sockOnSession": {
|
||||
const cb = args[1]!,
|
||||
@@ -164,13 +164,8 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
]);
|
||||
B.br(done);
|
||||
B.startBlock(plain);
|
||||
const insecure = callRuntime(host, "scr_http_request_ex", "ptr", [
|
||||
...a.slice(1, 8),
|
||||
...handler,
|
||||
{ type: "i32", name: "80" },
|
||||
ptr(),
|
||||
ptr(),
|
||||
]);
|
||||
// The plain arm is http.request: Node's global agent serves it.
|
||||
const insecure = callRuntime(host, "scr_http_request", "ptr", [...a.slice(1, 8), ...handler]);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const result = B.tmp();
|
||||
|
||||
@@ -629,7 +629,7 @@ export function emitLocalArrayRead(
|
||||
// A slot of a nullable-union array may hold NULL even when present.
|
||||
const owned = read.nullableElement ? read.type : read.element;
|
||||
const store = (value: string): void => {
|
||||
B.line(`store ptr ${read.borrow ? value : host.retainValue(value, owned)}, ptr ${localSlot}`);
|
||||
B.line(`store ptr ${read.borrow ? value : host.retainFrame(value, owned)}, ptr ${localSlot}`);
|
||||
};
|
||||
if (inline)
|
||||
emitDenseReferenceArrayRead(host, array, index, integerIndex, store, no, slow, join, "ptr");
|
||||
@@ -676,7 +676,7 @@ export function emitLocalArrayRead(
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
} else {
|
||||
B.line(
|
||||
`store ptr ${read.borrow ? value : host.retainValue(value, read.element)}, ptr ${payload}`,
|
||||
`store ptr ${read.borrow ? value : host.retainFrame(value, read.element)}, ptr ${payload}`,
|
||||
);
|
||||
}
|
||||
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
|
||||
|
||||
@@ -202,7 +202,7 @@ export function storeLocalUnion(
|
||||
B.startBlock(label);
|
||||
host.frames.push([]);
|
||||
storeLocalUnion(host, storage, value);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
B.br(join);
|
||||
}
|
||||
B.startBlock(join);
|
||||
@@ -241,7 +241,7 @@ export function storeLocalUnion(
|
||||
const copied = host.emitOwnedNullableStack(expr);
|
||||
source = copied.box;
|
||||
owner = copied.owner.name;
|
||||
host.moveTemp(copied.owner);
|
||||
host.toFrame(copied.owner);
|
||||
} else if (expr.kind === "varRef" && !canStackUnion(expr, host.boxedUnionsById)) {
|
||||
// A copy of another binding: snapshot the tag and bits, and retain the
|
||||
// reference payload (scalar and unit arms own nothing).
|
||||
@@ -260,7 +260,7 @@ export function storeLocalUnion(
|
||||
B.line(`${isRef} = icmp eq i32 ${tag}, ${refTag}`);
|
||||
B.condBr(isRef, yes, join);
|
||||
B.startBlock(yes);
|
||||
const retained = host.retainValue(host.unionPeek(source, unionId), storage.ownerType);
|
||||
const retained = host.retainFrame(host.unionPeek(source, unionId), storage.ownerType);
|
||||
B.line(`store ptr ${retained}, ptr ${held}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
@@ -272,7 +272,7 @@ export function storeLocalUnion(
|
||||
source = wrapped.value.name;
|
||||
if (wrapped.payload) {
|
||||
owner = wrapped.payload.name;
|
||||
host.moveTemp(wrapped.payload);
|
||||
host.toFrame(wrapped.payload);
|
||||
}
|
||||
} else {
|
||||
const read = matchMapRead(expr, host.boxedUnionsById)!;
|
||||
@@ -294,7 +294,7 @@ export function storeLocalUnion(
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${storage.owner}`);
|
||||
B.line(`store ptr ${owner}, ptr ${storage.owner}`);
|
||||
host.releaseValue(old, storage.ownerType);
|
||||
host.releaseFrame(old, storage.ownerType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ export function emitScalarOptional(
|
||||
bindPresent();
|
||||
host.frames.push([]);
|
||||
storeLocalUnion(host, storage, expr.body);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
host.chainSlots.delete(expr.id);
|
||||
B.br(join);
|
||||
} else {
|
||||
@@ -339,7 +339,7 @@ export function emitScalarOptional(
|
||||
B.startBlock(fallback);
|
||||
host.frames.push([]);
|
||||
storeLocalUnion(host, storage, expr.right);
|
||||
host.releaseFrame(host.frames.pop()!);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
B.br(join);
|
||||
}
|
||||
B.startBlock(join);
|
||||
|
||||
@@ -105,7 +105,7 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy
|
||||
join = B.newLabel("mapn.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const retained = host.retainValue(emitNumberMapSlot(host, entry, "ptr"), value);
|
||||
const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value);
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
@@ -251,7 +251,7 @@ export function emitMapIterSlot(
|
||||
B.line(`${loaded} = icmp ne i64 ${bits}, 0`);
|
||||
} else {
|
||||
B.line(`${loaded} = load ${llTy}, ptr ${slot}`);
|
||||
if (access === "ref") loaded = host.retainValue(loaded, type);
|
||||
if (access === "ref") loaded = host.retainFrame(loaded, type);
|
||||
}
|
||||
const hitEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
|
||||
@@ -29,6 +29,7 @@ export function preservesRegexInputs(method: string): boolean {
|
||||
case "match":
|
||||
case "search":
|
||||
case "matchAll":
|
||||
case "matchAllCaptures":
|
||||
case "replace":
|
||||
case "replaceAll":
|
||||
case "split":
|
||||
|
||||
@@ -104,6 +104,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"regex.groupEquals",
|
||||
"regex.groupNumber",
|
||||
"regex.groupParseInt",
|
||||
"regex.resultProp",
|
||||
"process.cwd",
|
||||
"process.argv",
|
||||
"process.execArgv",
|
||||
@@ -469,11 +470,14 @@ export class ReferenceWrites {
|
||||
return;
|
||||
case "intrinsic":
|
||||
// A TDZ test reads; publication only makes objects immortal; intern
|
||||
// tables store published values and write no script object.
|
||||
// tables store published values and write no script object;
|
||||
// ThreadSlots reads write nothing.
|
||||
if (
|
||||
e.name !== "module.tdzCheck" &&
|
||||
e.name !== "threads.publish" &&
|
||||
!e.name.startsWith("itable.")
|
||||
!e.name.startsWith("itable.") &&
|
||||
e.name !== "slots.get" &&
|
||||
e.name !== "slots.has"
|
||||
)
|
||||
sink.all();
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
compileRegexMatcher,
|
||||
regexLiteralBailReason,
|
||||
regexLiteralCaptures,
|
||||
regexLiteralCompiles,
|
||||
} from "./regex-matchers.js";
|
||||
|
||||
@@ -11,6 +12,30 @@ function body(pattern: string, flags = "", vector = true): string {
|
||||
return code.lines.join("\n");
|
||||
}
|
||||
|
||||
describe("regex literal captures", () => {
|
||||
test("counts captures with the whole match and marks the ones every match sets", () => {
|
||||
expect(regexLiteralCaptures("[&<>]", "g")).toEqual({ count: 1, always: [true] });
|
||||
expect(regexLiteralCaptures("(\\w+)=(\\d)+", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, true, true],
|
||||
});
|
||||
expect(regexLiteralCaptures("(?<k>a)(b)?|(c)", "")).toEqual({
|
||||
count: 4,
|
||||
always: [true, false, false, false],
|
||||
});
|
||||
expect(regexLiteralCaptures("x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", "")).toEqual({
|
||||
count: 6,
|
||||
always: [true, false, false, false, false, true],
|
||||
});
|
||||
});
|
||||
|
||||
test("answers null outside the parsed subset", () => {
|
||||
expect(regexLiteralCaptures("(?=a)(b)", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("(a)\\1", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("a", "v")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("regex literal matcher subset", () => {
|
||||
test.each([
|
||||
['^(\\d+\\.\\d+\\.\\d+\\.\\d+) - - \\[([^\\]]+)\\] "(\\w+) ([^ ?"]+)(?:\\?([^ "]*))?$', ""],
|
||||
|
||||
@@ -1639,6 +1639,40 @@ function parseSubset(pattern: string, flags: string): { root: RxNode; ncap: numb
|
||||
}
|
||||
}
|
||||
|
||||
/** The capture count of a regex literal (the whole match included) and,
|
||||
* per capture, whether every match sets it: no alternation of several
|
||||
* branches and no repetition that can run zero times encloses it. Null
|
||||
* when the pattern is outside the parsed subset. */
|
||||
export function regexLiteralCaptures(
|
||||
pattern: string,
|
||||
flags: string,
|
||||
): { count: number; always: boolean[] } | null {
|
||||
const parsed = parseSubset(pattern, flags);
|
||||
if (!parsed) return null;
|
||||
const always = Array.from({ length: parsed.ncap }, (_, i) => i === 0);
|
||||
const walk = (node: RxNode, sure: boolean): void => {
|
||||
switch (node.kind) {
|
||||
case "cap":
|
||||
always[node.index] = sure;
|
||||
walk(node.body, sure);
|
||||
return;
|
||||
case "seq":
|
||||
for (const item of node.items) walk(item, sure);
|
||||
return;
|
||||
case "alt":
|
||||
for (const alt of node.alts) walk(alt, sure && node.alts.length === 1);
|
||||
return;
|
||||
case "rep":
|
||||
walk(node.body, sure && node.min > 0);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
walk(parsed.root, true);
|
||||
return { count: parsed.ncap, always };
|
||||
}
|
||||
|
||||
/** Whether a literal compiles to a native matcher (tests and size caps). */
|
||||
export function regexLiteralCompiles(pattern: string, flags: string): boolean {
|
||||
return parseSubset(pattern, flags) !== null;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
mangleRecordStruct,
|
||||
} from "../mangle.js";
|
||||
import { threadHotAddressLines } from "./alloc.js";
|
||||
import type { LlClassMeta } from "./classes.js";
|
||||
import { classFieldBase, type LlClassMeta } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import type { NullableRefFields } from "./nullable-fields.js";
|
||||
import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js";
|
||||
@@ -84,7 +84,7 @@ export class LifetimeRegions {
|
||||
|
||||
/** The reference-holding fields; inline scalar unions own nothing. */
|
||||
private classFields(def: IrClassDef, meta: LlClassMeta): Field[] {
|
||||
const base = meta.hierarchy ? 2 : 1;
|
||||
const base = classFieldBase(meta);
|
||||
return def.fields.flatMap((f, i) =>
|
||||
this.host.inlineUnions.classField(def.name, f.name)
|
||||
? []
|
||||
|
||||
@@ -205,6 +205,43 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(fn("sc_rc_retain_promise")).not.toContain("store i32");
|
||||
});
|
||||
|
||||
test("optimized builds buffer candidates through the module's append helper", () => {
|
||||
const emit = (inlineRc: true | "release", extra: Partial<ShapeHost>) => {
|
||||
const { host } = declarationHost(64, inlineRc);
|
||||
const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra };
|
||||
releaseSym(configured, { kind: "dyn" });
|
||||
return {
|
||||
ir: emitInlineRcHelpers(configured).join("\n"),
|
||||
decls: inlineRcDecls(configured),
|
||||
};
|
||||
};
|
||||
for (const mode of [true, "release"] as const) {
|
||||
const { ir, decls } = emit(mode, {});
|
||||
expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)");
|
||||
expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline");
|
||||
// The young test reads the generation byte at obj-5; the append fills
|
||||
// scr_cyc_buffers' nursery buffer and the header's position at obj-4.
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i64 -5");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104");
|
||||
expect(ir).toContain("load i64, ptr @scr_cyc_live");
|
||||
expect(ir).toContain("or i32 %i, -2147483648");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls).toContain(
|
||||
"@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }",
|
||||
);
|
||||
expect(decls).toContain("@scr_cyc_live = external global i64");
|
||||
}
|
||||
// Worker executables and targets without inline allocation call
|
||||
// scr_cyc_on_release, which tries the same append first.
|
||||
for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) {
|
||||
const { ir, decls } = emit(true, extra);
|
||||
expect(ir).not.toContain("sc_cyc_append");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls.join("\n")).not.toContain("scr_cyc_buffers");
|
||||
}
|
||||
});
|
||||
|
||||
test("uses the wasm32 header offsets", () => {
|
||||
const { host } = declarationHost(32, true);
|
||||
releaseSym(host, { kind: "union", unionId: "u0" } as never);
|
||||
|
||||
@@ -28,7 +28,16 @@ import {
|
||||
mangleRecordStruct,
|
||||
mangleRecordTrace,
|
||||
} from "../mangle.js";
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import {
|
||||
CYC_HDR,
|
||||
cycTypeDescriptor,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
CYC_APPEND,
|
||||
cycleAppendDecls,
|
||||
cycleAppendHelper,
|
||||
inlineBufferingSupported,
|
||||
} from "./alloc.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import {
|
||||
@@ -47,69 +56,135 @@ import {
|
||||
stackUnionBox,
|
||||
type InlineUnionFields,
|
||||
} from "./inline-unions.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
/** SCR_CYC_MAX_TYPES (scr_rc.h): the registry's size, and the length of
|
||||
* the young-in nursery's per-id skip countdown. */
|
||||
const CYC_MAX_TYPES = 32768;
|
||||
|
||||
/** What the tables need from the emitter: the extern-declaration ledger
|
||||
* and the module-wide cycle/shape indexes. */
|
||||
export interface ShapeHost {
|
||||
declare(decl: string): void;
|
||||
/** The hierarchy root of a class (a program class or a runtime base),
|
||||
* or undefined for classes outside the class graph. */
|
||||
classRootOf?(className: string): string | undefined;
|
||||
/** Request the shared OOM abort helper (@sc_oom) — emitted once. */
|
||||
needOom(): void;
|
||||
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
|
||||
readonly cycleBufOffset: number;
|
||||
declare(decl: string): void;
|
||||
/** Inline the small-object allocator's fast paths (llvm/alloc.ts). */
|
||||
readonly inlineAlloc?: boolean;
|
||||
/** Record and class fields that store a scalar union's tag and bits
|
||||
* inline (inline-unions.ts): they own nothing. */
|
||||
readonly inlineUnions: InlineUnionFields;
|
||||
/** The immutable ScrMapOps descriptor (scr_map.h) of a Map or Set type:
|
||||
* a module constant holding the LLVM operands { key retain, key release,
|
||||
* key trace, value retain, value release, value trace, union key kinds },
|
||||
* or `null` when every operand is null. */
|
||||
mapOps(operands: readonly string[]): string;
|
||||
/** The target C ABI's `size_t` / refcount integer type. */
|
||||
readonly sizeType: "i32" | "i64";
|
||||
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
|
||||
readonly cycleBufOffset: number;
|
||||
readonly tracedShapes: Set<string>;
|
||||
readonly tracedUnions: Set<string>;
|
||||
/** Request the shared OOM abort helper (@sc_oom) — emitted once. */
|
||||
needOom(): void;
|
||||
/** The next collector statistics index for an emitted ScrCycType
|
||||
* descriptor (cycTypeDescriptor), counting from FIRST_EMITTED_CYC_KIND
|
||||
* in emission order.
|
||||
* Absent: every descriptor gets 0, which keeps no statistics. */
|
||||
nextCycKind?(): number;
|
||||
/** Unions represented as a nullable arm pointer (nullable-unions.ts):
|
||||
* their RC and trace entry points are the arm's NULL-tolerant ones. */
|
||||
readonly nullableUnions: NullableUnions;
|
||||
/** Record and class fields that store a scalar union's tag and bits
|
||||
* inline (inline-unions.ts): they own nothing. */
|
||||
readonly inlineUnions: InlineUnionFields;
|
||||
/** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */
|
||||
unionNoneRef(): string;
|
||||
readonly recordsById: Map<string, IrRecordShape>;
|
||||
readonly recordCloneShapes: ReadonlySet<string>;
|
||||
/** The program constructs lifetime regions: the ProgramLifetime classes
|
||||
* allocate in regions and count references in their region chunk instead
|
||||
* of being empty (llvm/regions.ts). */
|
||||
regions?: RegionHooks | null;
|
||||
/** The alias tag of a runtime word that lifetime-region code reads and
|
||||
* writes outside every field: the chunk count, the chunk owner, the class
|
||||
* cursors and scr_region_counting (", !tbaa !N", or "" in debug builds). */
|
||||
regionAliasTag?(word: "count" | "owner" | "cursor" | "counting"): string;
|
||||
/** Emit the live-object audit notes (`scr_obj_alloc_note` /
|
||||
* `scr_obj_free_note`) in emitted new/free helpers. Only runtimes built
|
||||
* with SCR_RC_AUDIT (the sanitized lane) count them; release and dev
|
||||
* runtime packs define them empty, so plain builds omit the calls.
|
||||
* Absent means emit. */
|
||||
readonly objectAudit?: boolean;
|
||||
/** Inline the small-object allocator's fast paths (llvm/alloc.ts). */
|
||||
readonly inlineAlloc?: boolean;
|
||||
/** Worker executables: the inline allocator state is thread-local. */
|
||||
readonly threadLocalAlloc?: boolean;
|
||||
/** Inline RC fast-path helpers requested so far ("<family>:<op>"); the
|
||||
* emitter defines each once through {@link emitInlineRcHelpers}. Null
|
||||
* keeps every retain/release a runtime call. */
|
||||
readonly rcHelpers: Set<string> | null;
|
||||
/** Which fast paths are inlined (see {@link InlineRcMode}). */
|
||||
readonly rcMode?: InlineRcMode;
|
||||
/** Define the fast-path helpers `noinline` (see {@link inlineRcOption}). */
|
||||
readonly rcHelpersNoinline?: boolean;
|
||||
/** The next collector statistics index for an emitted ScrCycType
|
||||
* descriptor (cycTypeDescriptor), counting from FIRST_EMITTED_CYC_KIND
|
||||
* in emission order.
|
||||
* Absent: every descriptor gets 0, which keeps no statistics. */
|
||||
nextCycKind?(): number;
|
||||
/** Which fast paths are inlined (see {@link InlineRcMode}). */
|
||||
readonly rcMode?: InlineRcMode;
|
||||
readonly recordCloneShapes: ReadonlySet<string>;
|
||||
readonly recordsById: Map<string, IrRecordShape>;
|
||||
/** The alias tag of a runtime word that lifetime-region code reads and
|
||||
* writes outside every field: the chunk count, the chunk owner, the class
|
||||
* cursors and scr_region_counting (", !tbaa !N", or "" in debug builds). */
|
||||
regionAliasTag?(word: "count" | "owner" | "cursor" | "counting"): string;
|
||||
/** The program constructs lifetime regions: the ProgramLifetime classes
|
||||
* allocate in regions and count references in their region chunk instead
|
||||
* of being empty (llvm/regions.ts). */
|
||||
regions?: RegionHooks | null;
|
||||
/** The target C ABI's `size_t` / refcount integer type. */
|
||||
readonly sizeType: "i32" | "i64";
|
||||
/** Worker executables: the inline allocator state is thread-local. */
|
||||
readonly threadLocalAlloc?: boolean;
|
||||
readonly tracedShapes: Set<string>;
|
||||
readonly tracedUnions: Set<string>;
|
||||
/** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */
|
||||
unionNoneRef(): string;
|
||||
/** The young-in nursery (backend/young-nursery.ts): `new` hands each
|
||||
* traced program-owned instance to scr_yin_enter. Absent: off. */
|
||||
readonly youngNursery?: boolean;
|
||||
}
|
||||
|
||||
/** The nursery entry of a new traced instance in `%o`, after its count is
|
||||
* stored; nothing when the nursery is off. Where the allocator is inlined
|
||||
* (64-bit, process-global state) the entry is inline too: a type that is
|
||||
* skipping the nursery (its scr_yin_skip countdown is nonzero) counts down
|
||||
* and stays out; otherwise, while the nursery has room (n < lim in ScrYin,
|
||||
* scr_rc.h), list the object at index n and mark its header young
|
||||
* (generation SCR_CYC_YOUNG, `buf` the buffered bit and the index), and
|
||||
* call scr_yin_enter_slow when it has none (it collects the nursery
|
||||
* first). Other targets call scr_yin_enter, which does all of it.
|
||||
* Labels use the `ye.` prefix; the lines end in an open block. */
|
||||
export function youngEntryLines(host: ShapeHost): string[] {
|
||||
if (host.youngNursery !== true) return [];
|
||||
if (host.inlineAlloc !== true || host.threadLocalAlloc === true) {
|
||||
host.declare(`declare void @scr_yin_enter(ptr)`);
|
||||
return [` call void @scr_yin_enter(ptr %o) ; the young-in nursery`];
|
||||
}
|
||||
host.declare(`@scr_yin = external global { ptr, i64, i64, i64 }`);
|
||||
host.declare(`@scr_yin_skip = external global [${CYC_MAX_TYPES} x i32]`);
|
||||
host.declare(`declare void @scr_yin_enter_slow(ptr)`);
|
||||
return [
|
||||
` %ye.idp = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR} ; the young-in nursery`,
|
||||
` %ye.id16 = load i16, ptr %ye.idp ; ScrCycHdr.id`,
|
||||
` %ye.id = zext i16 %ye.id16 to i64`,
|
||||
` %ye.sp = getelementptr inbounds [${CYC_MAX_TYPES} x i32], ptr @scr_yin_skip, i64 0, i64 %ye.id`,
|
||||
` %ye.s = load i32, ptr %ye.sp`,
|
||||
` %ye.skip = icmp ne i32 %ye.s, 0`,
|
||||
` br i1 %ye.skip, label %ye.skipped, label %ye.enter`,
|
||||
`ye.skipped: ; a type that does not die young: counted down, stays out`,
|
||||
` %ye.s1 = sub i32 %ye.s, 1`,
|
||||
` store i32 %ye.s1, ptr %ye.sp`,
|
||||
` br label %ye.done`,
|
||||
`ye.enter:`,
|
||||
` %ye.v = load ptr, ptr @scr_yin`,
|
||||
` %ye.np = getelementptr inbounds i8, ptr @scr_yin, i64 8`,
|
||||
` %ye.n = load i64, ptr %ye.np`,
|
||||
` %ye.lp = getelementptr inbounds i8, ptr @scr_yin, i64 16`,
|
||||
` %ye.l = load i64, ptr %ye.lp`,
|
||||
` %ye.room = icmp ult i64 %ye.n, %ye.l`,
|
||||
` br i1 %ye.room, label %ye.fast, label %ye.slow`,
|
||||
`ye.fast:`,
|
||||
` %ye.slot = getelementptr inbounds ptr, ptr %ye.v, i64 %ye.n`,
|
||||
` store ptr %o, ptr %ye.slot`,
|
||||
` %ye.n1 = add i64 %ye.n, 1`,
|
||||
` store i64 %ye.n1, ptr %ye.np`,
|
||||
` %ye.genp = getelementptr inbounds i8, ptr %o, i64 -5`,
|
||||
` store i8 3, ptr %ye.genp ; ScrCycHdr.gen = SCR_CYC_YOUNG`,
|
||||
` %ye.bufp = getelementptr inbounds i8, ptr %o, i64 -4`,
|
||||
` %ye.i = trunc i64 %ye.n to i32`,
|
||||
` %ye.buf = or i32 %ye.i, -2147483648`,
|
||||
` store i32 %ye.buf, ptr %ye.bufp ; ScrCycHdr.buf = buffered bit | index`,
|
||||
` br label %ye.done`,
|
||||
`ye.slow:`,
|
||||
` call void @scr_yin_enter_slow(ptr %o)`,
|
||||
` br label %ye.done`,
|
||||
`ye.done:`,
|
||||
];
|
||||
}
|
||||
|
||||
/** What class emission asks of lifetime regions (LifetimeRegions in
|
||||
@@ -413,6 +488,10 @@ export const INLINE_RC_FAMILIES: Readonly<Record<string, InlineRcFamily>> = {
|
||||
|
||||
type RcOp = "retain" | "release";
|
||||
|
||||
/** The helper key (with the "family:op" keys) of the module's candidate
|
||||
* append helper (alloc.ts cycleAppendHelper). */
|
||||
const CYC_APPEND_KEY = "cyc:append";
|
||||
|
||||
/** The mirrored family for an RC operation on a runtime stem (`scr_str`),
|
||||
* or null when it stays a call. */
|
||||
export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineRcFamily | null {
|
||||
@@ -474,6 +553,10 @@ function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string {
|
||||
}
|
||||
host.rcHelpers.add(`${family}:${op}`);
|
||||
for (const decl of rcHelperDecls(family, op)) host.declare(decl);
|
||||
// Its body is written last (emitInlineRcHelpers), after the requests are
|
||||
// collected: a release that can buffer a candidate requests the append
|
||||
// helper now.
|
||||
if (op === "release" && INLINE_RC_FAMILIES[family]!.cycle === "always") requestCycleAppend(host);
|
||||
return `@${inlineRcName(family, op)}`;
|
||||
}
|
||||
|
||||
@@ -571,6 +654,10 @@ function rcHelperKeys(host: ShapeHost): string[] {
|
||||
export function inlineRcDecls(host: ShapeHost): string[] {
|
||||
const decls = new Set<string>();
|
||||
for (const key of rcHelperKeys(host)) {
|
||||
if (key === CYC_APPEND_KEY) {
|
||||
for (const decl of cycleAppendDecls()) decls.add(decl);
|
||||
continue;
|
||||
}
|
||||
const [family, op] = key.split(":") as [string, RcOp];
|
||||
for (const decl of rcHelperDecls(family, op)) decls.add(decl);
|
||||
}
|
||||
@@ -716,6 +803,10 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st
|
||||
export function emitInlineRcHelpers(host: ShapeHost): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of rcHelperKeys(host).sort()) {
|
||||
if (key === CYC_APPEND_KEY) {
|
||||
out.push(...cycleAppendHelper());
|
||||
continue;
|
||||
}
|
||||
const [family, op] = key.split(":") as [string, RcOp];
|
||||
const row = INLINE_RC_FAMILIES[family]!;
|
||||
out.push(
|
||||
@@ -1368,12 +1459,6 @@ export function releaseFastPath(
|
||||
];
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object) and call the runtime only to enqueue a new candidate.
|
||||
* Every emitted release of a headered object shares this sequence
|
||||
* (scr_runtime.h asserts the offset); it ends by branching to `done`. */
|
||||
/** One traced field of an emitted trace function: load the slot and visit
|
||||
* it unless it is null. Every collector visitor ignores null children, and
|
||||
* most traced slots of a typical heap are empty (optional links, unset
|
||||
@@ -1397,16 +1482,39 @@ export function traceFieldVisitLines(
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether candidate buffering goes through the module's append helper
|
||||
* (alloc.ts cycleAppendHelper), requesting its definition if so: in
|
||||
* optimized builds (those with inline RC helpers) that support it. */
|
||||
function requestCycleAppend(host: ShapeHost): boolean {
|
||||
if (host.rcHelpers === null || !inlineBufferingSupported(host)) return false;
|
||||
if (!host.rcHelpers.has(CYC_APPEND_KEY)) {
|
||||
host.rcHelpers.add(CYC_APPEND_KEY);
|
||||
for (const decl of cycleAppendDecls()) host.declare(decl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object), and buffer a new candidate: through the module's
|
||||
* cycleAppendHelper in optimized builds that support it, through
|
||||
* scr_cyc_on_release otherwise. Every emitted release of a headered object
|
||||
* shares this sequence (scr_runtime.h asserts the offsets); it ends by
|
||||
* branching to `done`. */
|
||||
export function cycleRootLines(host: ShapeHost, done: string): string[] {
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
const append = requestCycleAppend(host);
|
||||
return [
|
||||
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %buf = load i32, ptr %bufp`,
|
||||
` %queued = icmp slt i32 %buf, 0`,
|
||||
` br i1 %queued, label %${done}, label %enqueue`,
|
||||
`enqueue:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
append
|
||||
? ` call preserve_mostcc void @${CYC_APPEND}(ptr %o) ; buffer the candidate; may collect`
|
||||
: ` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
` br label %${done}`,
|
||||
];
|
||||
}
|
||||
@@ -1524,7 +1632,9 @@ export function emitRecordShapes(
|
||||
}
|
||||
t++;
|
||||
}
|
||||
freeBody.push(...emitObjectFree(host, traced));
|
||||
// Closed layouts can carry a kept record source (record-sources.ts).
|
||||
const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak";
|
||||
freeBody.push(...emitObjectFree(host, traced, observers));
|
||||
defs.push(
|
||||
...releaseBody(
|
||||
host,
|
||||
@@ -1578,6 +1688,7 @@ export function emitRecordShapes(
|
||||
` store ptr %ovf, ptr %ovfp`,
|
||||
);
|
||||
}
|
||||
if (traced) nw.push(...youngEntryLines(host));
|
||||
nw.push(` ret ptr %o`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
|
||||
@@ -1639,7 +1750,7 @@ export function emitRecordShapes(
|
||||
` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)} (acyclic)`,
|
||||
);
|
||||
});
|
||||
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
|
||||
gf.push(...emitObjectFree(host, true, observers), ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ export function storeSplitSnapshot(
|
||||
host.B.line(`store ptr ${snapshot.source}, ptr ${sourceSlot}`);
|
||||
host.B.line(`store ptr ${snapshot.separator}, ptr ${separatorSlot}`);
|
||||
host.B.line(`store i32 ${snapshot.limit}, ptr ${limitSlot}`);
|
||||
host.moveTemp({ name: snapshot.source, type: STRING });
|
||||
host.moveTemp({ name: snapshot.separator, type: STRING });
|
||||
host.toFrame({ name: snapshot.source, type: STRING });
|
||||
host.toFrame({ name: snapshot.separator, type: STRING });
|
||||
return { sourceSlot, separatorSlot, limitSlot };
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => voi
|
||||
* (stackObjectFields). Published shapes keep heap storage: their field
|
||||
* guards treat the immortal header as a frozen object. */
|
||||
export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource {
|
||||
// A ThreadSlots key keeps its slot index for its lifetime and has its
|
||||
// values released when it is freed: it lives on the heap.
|
||||
if (e.type.kind === "object" && host.classMeta.get(e.type.className)?.threadSlot === true)
|
||||
return false;
|
||||
if (e.type.kind === "record" && host.publishedTypes?.recordGuarded(e.type.shapeId) === true)
|
||||
return false;
|
||||
// A producer with a result-slot entry builds its fresh result in the
|
||||
@@ -232,7 +236,7 @@ function emitRecordCloneFields(
|
||||
const fieldTy = llFieldType(from.type, from.inline);
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = load ${fieldTy}, ptr ${from.ptr}`);
|
||||
const stored = isRefCounted(from.type) && !from.inline ? host.retainValue(v, from.type) : v;
|
||||
const stored = isRefCounted(from.type) && !from.inline ? host.retainFrame(v, from.type) : v;
|
||||
B.line(`store ${fieldTy} ${stored}, ptr ${to.ptr}`);
|
||||
}
|
||||
const initialized = new Set<string>();
|
||||
|
||||
@@ -91,7 +91,7 @@ export function emitStringSliceSnapshot(
|
||||
B.line(
|
||||
`call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`,
|
||||
);
|
||||
host.moveTemp(source);
|
||||
host.toFrame(source);
|
||||
return { source: sourceSlot, range };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { mangleClassStruct } from "../mangle.js";
|
||||
|
||||
const loc = { file: "thread-slots.ts", start: 0, end: 1 };
|
||||
const slotsType: IrType = { kind: "object", className: "%ThreadSlots" };
|
||||
const node: IrType = { kind: "object", className: "Node" };
|
||||
const leaf: IrType = { kind: "object", className: "Leaf" };
|
||||
const plain: IrType = { kind: "object", className: "Plain" };
|
||||
const state: IrType = { kind: "object", className: "State" };
|
||||
const maybeState: IrType = { kind: "union", unionId: "u0" };
|
||||
|
||||
/** A key hierarchy (Node, Leaf), a class that is not a key (Plain), and the
|
||||
* value class (State), with `slots` a module global of the slots class. */
|
||||
function module(body: IrExpr[]): IrModule {
|
||||
const cls = (name: string, fields: [string, IrType][], base?: string) => ({
|
||||
name,
|
||||
...(base !== undefined ? { base } : {}),
|
||||
fields: fields.map(([field, type]) => ({ name: field, type })),
|
||||
methods: [],
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
threadSlotKeys: ["Node"],
|
||||
globals: [
|
||||
["slots", slotsType],
|
||||
["node", node],
|
||||
["leaf", leaf],
|
||||
["plain", plain],
|
||||
["state", state],
|
||||
].map(([name, type]) => ({ id: `%g.${name as string}`, name, type, mutable: false })),
|
||||
functions: [
|
||||
{
|
||||
name: "main",
|
||||
params: [],
|
||||
returnType: VOID,
|
||||
locals: [],
|
||||
body: body.map((expr) => ({ kind: "exprStmt" as const, expr, loc })),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
classes: [
|
||||
cls("%ThreadSlots", []),
|
||||
cls("Node", [["name", STRING]]),
|
||||
cls(
|
||||
"Leaf",
|
||||
[
|
||||
["name", STRING],
|
||||
["weight", F64],
|
||||
],
|
||||
"Node",
|
||||
),
|
||||
cls("Plain", [["name", STRING]]),
|
||||
cls("State", [["count", F64]]),
|
||||
],
|
||||
unions: [{ id: "u0", arms: [state, { kind: "undefinedT" }] }],
|
||||
} as unknown as IrModule;
|
||||
}
|
||||
|
||||
function body(llvm: string, name: string): string {
|
||||
const start = llvm.indexOf(`@${name}(`);
|
||||
return start < 0 ? "" : llvm.slice(start, llvm.indexOf("\n}", start));
|
||||
}
|
||||
|
||||
const intrinsic = (name: string, args: IrExpr[], type: IrType): IrExpr =>
|
||||
({ kind: "intrinsic", name, args, type, loc }) as IrExpr;
|
||||
const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
|
||||
const global = (id: string, type: IrType): IrExpr =>
|
||||
({ kind: "varRef", localId: `%g.${id}`, type, loc }) as IrExpr;
|
||||
const slots = global("slots", slotsType);
|
||||
const fresh = (type: IrType & { kind: "object" }): IrExpr =>
|
||||
global(type.className.toLowerCase(), type);
|
||||
|
||||
test("slots.get reads the slot word, the page and the entry inline", () => {
|
||||
const calls = [
|
||||
intrinsic("slots.get", [str("s"), slots, fresh(leaf)], maybeState),
|
||||
intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL),
|
||||
intrinsic("slots.set", [str("s"), slots, fresh(leaf), fresh(state)], VOID),
|
||||
intrinsic("slots.delete", [str("t"), slots, fresh(node)], BOOL),
|
||||
];
|
||||
expect(validateModule(module(calls))).toEqual([]);
|
||||
const llvm = emitLlvmModule(module(calls), { pointerBits: 64 });
|
||||
// The slot word sits after the header words of every hierarchy member.
|
||||
expect(llvm).toMatch(
|
||||
new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`),
|
||||
);
|
||||
expect(llvm).toMatch(
|
||||
new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`),
|
||||
);
|
||||
expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`));
|
||||
// One block per family name.
|
||||
expect(llvm).toMatch(
|
||||
/@sc_slots_0 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/,
|
||||
);
|
||||
expect(llvm).toMatch(
|
||||
/@sc_slots_1 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/,
|
||||
);
|
||||
const main = body(llvm, "sc_f_main");
|
||||
expect(main).toMatch(
|
||||
/getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/,
|
||||
);
|
||||
expect(main).not.toMatch(/call ptr @scr_slots_get/);
|
||||
expect(main).toMatch(
|
||||
/call void @scr_slots_set\(ptr @sc_slots_0, ptr %\S+, ptr %\S+, ptr %\S+, ptr @\S+\)/,
|
||||
);
|
||||
expect(main).toMatch(/call zeroext i1 @scr_slots_delete\(ptr @sc_slots_1, ptr %\S+\)/);
|
||||
});
|
||||
|
||||
test("freeing a key releases its values when its slot word is set", () => {
|
||||
const llvm = emitLlvmModule(
|
||||
module([intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL)]),
|
||||
{ pointerBits: 64 },
|
||||
);
|
||||
expect(llvm).toMatch(/icmp ne i32 %tdw, 0\n {2}br i1 %tdw\.set, label %tdw\.forget/);
|
||||
expect(llvm).toMatch(/call void @scr_slots_forget\(i32 %tdw\)/);
|
||||
});
|
||||
|
||||
test("the validator checks ThreadSlots intrinsic operands", () => {
|
||||
const bad = [
|
||||
intrinsic("slots.get", [str("s"), slots], maybeState),
|
||||
intrinsic("slots.has", [slots, slots, fresh(node)], BOOL),
|
||||
intrinsic("slots.has", [str("s"), slots, fresh(plain)], BOOL),
|
||||
intrinsic(
|
||||
"slots.set",
|
||||
[str("s"), slots, fresh(node), { kind: "numLit", value: 1, type: F64, loc }],
|
||||
VOID,
|
||||
),
|
||||
intrinsic("slots.get", [str("s"), slots, fresh(node)], state),
|
||||
];
|
||||
expect(validateModule(module(bad)).map((e) => e.message)).toEqual([
|
||||
"in main: slots.get takes 3 arguments",
|
||||
"in main: slots.has takes a name literal and a %ThreadSlots value",
|
||||
"in main: slots.has key object:Plain is not a ThreadSlots key class",
|
||||
"in main: slots.set value f64 is not a ThreadSlots value type",
|
||||
"in main: slots.get has a V | undefined union type",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/* ThreadSlots (@scriptc/experimental): the slots.* intrinsics (scr_slots.c,
|
||||
* frontend/lowering/thread-slots.ts).
|
||||
*
|
||||
* Each family name has one `{ ptr, i32, i32, ptr, ptr }` ScrSlots block,
|
||||
* an internal global that is thread-local where module globals are. A key
|
||||
* class's hierarchy carries an i32 slot word after its header words
|
||||
* (classSlotWordIndex). A read is inline: the slot word, then the page
|
||||
* and the entry,
|
||||
*
|
||||
* page = word >> 10 < block.npages ? block.pages[word >> 10] : none
|
||||
* value = page[word & 1023]
|
||||
*
|
||||
* and absent pages are a shared page of NULL entries (scr_slots_none), so
|
||||
* a key without a value reads NULL, the undefined of `V | undefined`. Set
|
||||
* and delete call the runtime, which holds one reference to each value and
|
||||
* releases the value it replaces or removes. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { isUnitType, typeKey, type IrType } from "../../ir/ir.js";
|
||||
import { classSlotWordIndex, classStructSym } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { releaseSym } from "./shapes.js";
|
||||
|
||||
const BLOCK = "{ ptr, i32, i32, ptr, ptr }";
|
||||
|
||||
/** The slots.* intrinsics. */
|
||||
export function emitThreadSlotsIntrinsic(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"intrinsic">,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const [nameArg, slotsArg, keyArg, valueArg] = e.args;
|
||||
if (nameArg?.kind !== "strLit" || slotsArg === undefined || keyArg === undefined)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.name} operands`);
|
||||
const block = host.threadSlotsBlock(nameArg.value);
|
||||
// The slots value is read for its effects (a module const's TDZ check).
|
||||
const slots = host.emitReadReceiver(slotsArg);
|
||||
const key = host.emitReadReceiver(keyArg);
|
||||
const value = valueArg !== undefined ? host.emitExpr(valueArg) : null;
|
||||
if (B.isTerminated()) return { name: "", type: e.type };
|
||||
if (keyArg.type.kind !== "object")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.name} key of kind ${keyArg.type.kind}`);
|
||||
const className = keyArg.type.className;
|
||||
const word = B.tmp();
|
||||
B.line(
|
||||
`${word} = getelementptr inbounds %${classStructSym(className)}, ptr ${key.name}, i64 0, i32 ${classSlotWordIndex(host.classMetaOf(className))}`,
|
||||
);
|
||||
switch (e.name) {
|
||||
case "slots.get": {
|
||||
const read = readSlot(host, block, word);
|
||||
if (host.nullableUnions.of(e.type) !== null)
|
||||
return host.own({ name: host.retainFrame(read, e.type), type: e.type });
|
||||
return boxedRead(host, read, e.type);
|
||||
}
|
||||
case "slots.has": {
|
||||
const read = readSlot(host, block, word);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = icmp ne ptr ${read}, null`);
|
||||
return { name: has, type: e.type };
|
||||
}
|
||||
case "slots.delete": {
|
||||
host.declare(`declare zeroext i1 @scr_slots_delete(ptr, ptr)`);
|
||||
const had = B.tmp();
|
||||
B.line(`${had} = call zeroext i1 @scr_slots_delete(ptr ${block}, ptr ${word})`);
|
||||
return { name: had, type: e.type };
|
||||
}
|
||||
case "slots.set": {
|
||||
if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value");
|
||||
// The value moves into the block.
|
||||
host.toHeap(value);
|
||||
host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`,
|
||||
);
|
||||
if (e.type.kind === "void") return { name: "", type: e.type };
|
||||
return host.own({ name: host.retainFrame(slots.name, e.type), type: e.type });
|
||||
}
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** A read as a tagged union box, for `V | undefined` types that the module
|
||||
* keeps boxed (Map keys and Set elements of that type): the value's arm, or
|
||||
* the interned undefined instance for NULL. */
|
||||
function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValue {
|
||||
const B = host.B;
|
||||
const def = type.kind === "union" ? host.unionsById.get(type.unionId) : undefined;
|
||||
const refTag = def?.arms.findIndex((a) => !isUnitType(a)) ?? -1;
|
||||
const unitTag = def?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1;
|
||||
if (def === undefined || refTag < 0 || unitTag < 0 || def.arms.length !== 2)
|
||||
throw new InternalCompilerError(`llvm emitter bug: slots.get of type ${typeKey(type)}`);
|
||||
const result = B.slot();
|
||||
B.entryAllocas.push(`${result} = alloca ptr`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = icmp ne ptr ${read}, null`);
|
||||
const some = B.newLabel("slots.some"),
|
||||
none = B.newLabel("slots.none"),
|
||||
done = B.newLabel("slots.box");
|
||||
B.condBr(has, some, none);
|
||||
B.startBlock(some);
|
||||
const arm = def.arms[refTag]!;
|
||||
const box = host.unionNewOwned(def.id, refTag, { name: host.retainHeap(read, arm), type: arm });
|
||||
B.line(`store ptr ${box}, ptr ${result}`);
|
||||
B.br(done);
|
||||
B.startBlock(none);
|
||||
B.line(`store ptr ${host.unitInstanceRef(def.id, unitTag)}, ptr ${result}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = load ptr, ptr ${result}`);
|
||||
return host.own({ name: out, type });
|
||||
}
|
||||
|
||||
/** This thread's value for the slot word at `word`, borrowed, or NULL. */
|
||||
function readSlot(host: LlvmEmitterContext, block: string, word: string): string {
|
||||
const B = host.B;
|
||||
host.declare(`@scr_slots_none = external constant [1024 x ptr]`);
|
||||
const index = B.tmp(),
|
||||
page = B.tmp(),
|
||||
pages = B.tmp(),
|
||||
count = B.tmp(),
|
||||
inRange = B.tmp();
|
||||
B.line(`${index} = load atomic i32, ptr ${word} monotonic, align 4`);
|
||||
B.line(`${page} = lshr i32 ${index}, 10`);
|
||||
B.line(`${count} = getelementptr inbounds ${BLOCK}, ptr ${block}, i64 0, i32 1`);
|
||||
const npages = B.tmp();
|
||||
B.line(`${npages} = load i32, ptr ${count}`);
|
||||
B.line(`${inRange} = icmp ult i32 ${page}, ${npages}`);
|
||||
const result = B.slot();
|
||||
B.entryAllocas.push(`${result} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${result}`);
|
||||
const found = B.newLabel("slots.page"),
|
||||
done = B.newLabel("slots.j");
|
||||
B.condBr(inRange, found, done);
|
||||
B.startBlock(found);
|
||||
B.line(`${pages} = load ptr, ptr ${block}`);
|
||||
const pageIndex = B.tmp(),
|
||||
pagePtr = B.tmp(),
|
||||
entries = B.tmp(),
|
||||
entryIndex = B.tmp(),
|
||||
entryWide = B.tmp(),
|
||||
entry = B.tmp(),
|
||||
v = B.tmp();
|
||||
B.line(`${pageIndex} = zext i32 ${page} to i64`);
|
||||
B.line(`${pagePtr} = getelementptr inbounds ptr, ptr ${pages}, i64 ${pageIndex}`);
|
||||
B.line(`${entries} = load ptr, ptr ${pagePtr}`);
|
||||
B.line(`${entryIndex} = and i32 ${index}, 1023`);
|
||||
B.line(`${entryWide} = zext i32 ${entryIndex} to i64`);
|
||||
B.line(`${entry} = getelementptr inbounds ptr, ptr ${entries}, i64 ${entryWide}`);
|
||||
B.line(`${v} = load ptr, ptr ${entry}`);
|
||||
B.line(`store ptr ${v}, ptr ${result}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = load ptr, ptr ${result}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The ScrSlots block definitions of the families a module uses. */
|
||||
export function threadSlotsBlockDefinitions(
|
||||
blocks: ReadonlyMap<string, string>,
|
||||
threadLocal: boolean,
|
||||
): string[] {
|
||||
const tl = threadLocal ? "thread_local " : "";
|
||||
return [...blocks].map(
|
||||
([name, sym]) =>
|
||||
`${sym} = internal ${tl}global ${BLOCK} zeroinitializer ; ThreadSlots ${llvmCommentText(JSON.stringify(name))}`,
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
traceAdapter,
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
import { disposeHookLoadLines, RECORD_SOURCES_GATE } from "./alloc.js";
|
||||
import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
@@ -397,6 +398,24 @@ export class LlWalkers {
|
||||
const writer = this.recordFieldWriter(shapeId);
|
||||
const lSource = B.newLabel("jw.source");
|
||||
const lNative = B.newLabel("jw.native");
|
||||
// Only a program that can keep record sources looks one up (the gate
|
||||
// folds to a constant), and only once one was kept: keeping a source
|
||||
// installs the dispose hook, so a NULL hook means the table is empty.
|
||||
const lHooked = B.newLabel("jw.hooked");
|
||||
const kept = B.tmp();
|
||||
B.line(`${kept} = load i1, ptr ${RECORD_SOURCES_GATE}`);
|
||||
B.condBr(kept, lHooked, lNative);
|
||||
B.startBlock(lHooked);
|
||||
const hook = B.tmp();
|
||||
const hookLines = disposeHookLoadLines(host, hook);
|
||||
if (hookLines !== null) {
|
||||
const lProbe = B.newLabel("jw.probe");
|
||||
for (const line of hookLines) B.line(line.trimStart());
|
||||
const installed = B.tmp();
|
||||
B.line(`${installed} = icmp ne ptr ${hook}, null`);
|
||||
B.condBr(installed, lProbe, lNative);
|
||||
B.startBlock(lProbe);
|
||||
}
|
||||
if (traceAdapter(host, { kind: "record", shapeId }) === null) {
|
||||
// One lookup: the runtime writes the record when it kept a source.
|
||||
host.declare(`declare zeroext i1 @scr_jb_try_record_source(ptr, ptr, ptr)`);
|
||||
|
||||
@@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string {
|
||||
export function mangleGenDrop(fnName: string): string {
|
||||
return `sc_gd_${sanitize(fnName)}`;
|
||||
}
|
||||
/** The never-started return() hook of a generator that closes its source
|
||||
* argument (an iterator helper). */
|
||||
export function mangleGenSourceClose(fnName: string): string {
|
||||
return `sc_gc_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
|
||||
export function mangleResolveThunk(n: number): string {
|
||||
return `sc_rslv_${n}`;
|
||||
|
||||
@@ -297,7 +297,9 @@ export function computeMayThrow(
|
||||
rec.method === "replaceAll" ||
|
||||
rec.method === "split" ||
|
||||
rec.method === "matchAll" ||
|
||||
rec.method === "matchAllInto"
|
||||
rec.method === "matchAllCaptures" ||
|
||||
rec.method === "matchAllInto" ||
|
||||
rec.method === "matchAllCapturesInto"
|
||||
) {
|
||||
f.throws = true;
|
||||
}
|
||||
@@ -580,7 +582,7 @@ function breaksTo(
|
||||
|
||||
/** Statements no execution reaches: those after a statement that cannot
|
||||
* complete normally in the same statement list. */
|
||||
function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
export function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
const dead = new Set<IrStmt>();
|
||||
const { statement } = completion();
|
||||
const list = (stmts: readonly IrStmt[]): void => {
|
||||
|
||||
@@ -16,6 +16,9 @@ import type { NativeTargetSpec } from "./targets.js";
|
||||
|
||||
export interface NativeLinkFeatures {
|
||||
workers?: boolean;
|
||||
/** The young-in nursery (backend/young-nursery.ts): the SCR_YIN
|
||||
* variant of the collector unit. Absent when off. */
|
||||
youngNursery?: boolean;
|
||||
regex: boolean;
|
||||
copying: boolean;
|
||||
textDecoderLegacy: boolean;
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface CcOptions {
|
||||
frameworks?: readonly string[];
|
||||
/** Isolated native Worker contexts; every runtime object must use TLS. */
|
||||
workers?: boolean;
|
||||
/** Compile the runtime with SCR_YIN (backend/young-nursery.ts). */
|
||||
youngNursery?: boolean;
|
||||
|
||||
regex?: boolean;
|
||||
/** The program uses one of the copying/typed-array bridge intrinsics
|
||||
|
||||
@@ -445,6 +445,7 @@ export async function compileCInternal(
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
"-fno-math-errno",
|
||||
// The runtime object model uses type-punned C: a hierarchy
|
||||
// upcast is a raw pointer cast, so one object's header (rc, vt) and
|
||||
@@ -644,6 +645,7 @@ export async function compileCInternal(
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
"-fno-math-errno",
|
||||
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
|
||||
"-Wno-deprecated-declarations",
|
||||
|
||||
@@ -70,6 +70,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_publish.c",
|
||||
"scr_region.c",
|
||||
"scr_itable.c",
|
||||
"scr_slots.c",
|
||||
"scr_alloc.c",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface RuntimePackManifest {
|
||||
|
||||
export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
workers: boolean;
|
||||
youngNursery: boolean;
|
||||
eventTarget: boolean;
|
||||
nativeFetch: boolean;
|
||||
netEffective: boolean;
|
||||
@@ -266,6 +267,7 @@ export function effectiveRuntimeFeatures(
|
||||
return {
|
||||
...features,
|
||||
workers: features.workers === true,
|
||||
youngNursery: features.youngNursery === true,
|
||||
eventTarget: features.eventTarget === true,
|
||||
nativeFetch,
|
||||
netEffective,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "../ir/ir.js";
|
||||
import { captureFreeFunctions, observesStacks } from "./stack-observations.js";
|
||||
|
||||
const loc = { file: "stack-observations.ts", start: 0, end: 0 };
|
||||
const ERROR = { kind: "object", className: "%Error" } as const;
|
||||
const err: IrExpr = { loc, kind: "varRef", localId: "e", type: ERROR };
|
||||
const stackRead: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.stack", args: [err], type: STRING },
|
||||
};
|
||||
const call = (callee: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee, args: [], type: VOID },
|
||||
});
|
||||
const throwStmt: IrStmt = { loc, kind: "throw", value: err };
|
||||
const ret: IrStmt = { loc, kind: "return", value: null };
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const mod = (functions: IrFunction[], extra: Partial<IrModule> = {}): IrModule => ({
|
||||
irVersion: 16,
|
||||
sourceFile: "stack-observations.ts",
|
||||
entry: "%main",
|
||||
functions,
|
||||
classes: [],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("a reachable stack read is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("reads after an unconditional throw or return never run", () => {
|
||||
expect(observesStacks(mod([fn("%main", [throwStmt, stackRead])]))).toBe(false);
|
||||
expect(observesStacks(mod([fn("%main", [ret, call("parse")]), fn("parse", [stackRead])]))).toBe(
|
||||
false,
|
||||
);
|
||||
// A terminator ends only its own list: the outer list continues.
|
||||
const cond: IrExpr = { loc, kind: "numLit", value: 1, type: F64 };
|
||||
expect(
|
||||
observesStacks(
|
||||
mod([fn("%main", [{ kind: "if", cond, then: [ret], else_: null, loc }, stackRead])]),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("functions referenced only from dead code are unreached", () => {
|
||||
// The production React shape: a DEV-only body behind a throw stub calls
|
||||
// the stack parser, and a closure created after the stub reaches it too.
|
||||
const closure: IrStmt = {
|
||||
loc,
|
||||
kind: "assign",
|
||||
localId: "replacer",
|
||||
value: {
|
||||
loc,
|
||||
kind: "closure",
|
||||
fnName: "replacer",
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: VOID },
|
||||
},
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [throwStmt, closure, call("parse")]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(program)).toBe(false);
|
||||
// The same closure created before the stub keeps the read reachable.
|
||||
const live = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [closure, throwStmt]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(live)).toBe(true);
|
||||
});
|
||||
|
||||
test("class methods are roots", () => {
|
||||
const program = mod([fn("%main", [ret]), fn("%Parser.read", [stackRead])], {
|
||||
classes: [
|
||||
{ name: "Parser", fields: [], methods: ["read"] } as unknown as NonNullable<
|
||||
IrModule["classes"]
|
||||
>[number],
|
||||
],
|
||||
});
|
||||
expect(observesStacks(program)).toBe(true);
|
||||
});
|
||||
|
||||
test("leaf functions that cannot create an Error need no frame", () => {
|
||||
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
|
||||
const add: IrStmt = {
|
||||
loc,
|
||||
kind: "return",
|
||||
value: { loc, kind: "bin", op: "+", left: num(1), right: num(2), type: F64 },
|
||||
};
|
||||
const construct: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.new", args: [], type: ERROR },
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("leaf"), call("caller"), call("creates"), call("viaCreates")]),
|
||||
fn("leaf", [add]),
|
||||
fn("caller", [call("leaf"), add]),
|
||||
fn("creates", [construct]),
|
||||
fn("viaCreates", [call("creates")]),
|
||||
fn("recursive", [call("recursive")]),
|
||||
]);
|
||||
const free = captureFreeFunctions(program, new Set(["recursive"]), null);
|
||||
expect([...free].sort()).toEqual(["caller", "leaf"]);
|
||||
// A worker program keeps every frame.
|
||||
expect(captureFreeFunctions({ ...program, workers: true }, null, null).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Whole-program stack-trace gate (see observesStacks). Pure function of the
|
||||
* IR module; the emitter keeps a source frame in every function only when
|
||||
* it answers true. */
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
|
||||
import { unreachableStatements } from "./may-throw.js";
|
||||
import type { PublishedTypes } from "./publication.js";
|
||||
import { writesPublished } from "./publication.js";
|
||||
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
function readsStack(expr: IrExpr): boolean {
|
||||
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) });
|
||||
}
|
||||
|
||||
/** Visit every expression of `body` that execution can reach: statements
|
||||
* after one that cannot complete normally in the same list are skipped. */
|
||||
function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
const dead = unreachableStatements(body);
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
visit(node);
|
||||
return everyExprChild(node, expr, stmt);
|
||||
};
|
||||
const stmt = (node: IrStmt): boolean => dead.has(node) || everyStmtChild(node, expr, stmt);
|
||||
body.every(stmt);
|
||||
}
|
||||
|
||||
/** Whether the program can read a captured stack trace, which is what makes
|
||||
* every function keep a source frame. A read counts when it can execute:
|
||||
* it sits in live code of a function reachable from the module's roots (its
|
||||
* entry, library exports, class prototype helpers and every class's
|
||||
* methods) through live code. Code after a statement that cannot complete
|
||||
* normally is not live: production builds of libraries such as React's
|
||||
* Flight server keep development-only stack parsing behind `throw` stubs,
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
if (!mod.functions.some(containsStackRead)) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
// Class methods, accessors and constructors are reachable through
|
||||
// dispatch the IR does not name per site: all of them are roots.
|
||||
for (const fn of mod.functions) {
|
||||
if (!fn.name.startsWith("%")) continue;
|
||||
for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
|
||||
if (classNames.has(fn.name.slice(1, dot))) {
|
||||
pending.push(fn.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const reached = new Set<string>();
|
||||
let observed = false;
|
||||
while (pending.length > 0 && !observed) {
|
||||
const name = pending.pop()!;
|
||||
if (reached.has(name)) continue;
|
||||
reached.add(name);
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
});
|
||||
}
|
||||
return observed;
|
||||
}
|
||||
|
||||
/** Expressions that neither create an Error nor throw one by themselves.
|
||||
* Calls, `new` and virtual calls are capture-free when their callees are. */
|
||||
const CAPTURE_FREE_EXPRS: ReadonlySet<IrExpr["kind"]> = new Set([
|
||||
"numLit",
|
||||
"boolLit",
|
||||
"strLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"incDec",
|
||||
"assignExpr",
|
||||
"bin",
|
||||
"unary",
|
||||
"logical",
|
||||
"ternary",
|
||||
"nullish",
|
||||
"toBool",
|
||||
"fieldGet",
|
||||
"recordGet",
|
||||
"arrayGet",
|
||||
"arrayHas",
|
||||
"arrayState",
|
||||
"strCmp",
|
||||
"strEq",
|
||||
"strConcat",
|
||||
"upcast",
|
||||
"unionDisc",
|
||||
"unionIsTag",
|
||||
"unionEq",
|
||||
"instanceOf",
|
||||
"recordLit",
|
||||
"arrayLit",
|
||||
"unionWrap",
|
||||
"unionNarrow",
|
||||
"libCall",
|
||||
"arrIntrinsic",
|
||||
"call",
|
||||
"new",
|
||||
"virtualCall",
|
||||
]);
|
||||
|
||||
/** Statements that neither create an Error nor throw one by themselves. */
|
||||
const CAPTURE_FREE_STMTS: ReadonlySet<IrStmt["kind"]> = new Set([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"while",
|
||||
"doWhile",
|
||||
"for",
|
||||
"switch",
|
||||
"block",
|
||||
"return",
|
||||
"break",
|
||||
"continue",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
"arraySet",
|
||||
]);
|
||||
|
||||
/** Functions whose frame no stack capture can ever include: a capture runs
|
||||
* where an Error is created (by the program or by a throwing runtime
|
||||
* operation), and these functions only perform operations from a fixed
|
||||
* list that never create one and only call functions of the same kind.
|
||||
* Their frames are therefore never observable and the emitter omits them,
|
||||
* which keeps small leaf methods and helpers free of the per-call frame
|
||||
* push in programs that read stacks. Recursive functions (the stack guard
|
||||
* throws RangeError), async and generator bodies, and every function of a
|
||||
* worker program keep their frames. */
|
||||
export function captureFreeFunctions(
|
||||
mod: IrModule,
|
||||
stackChecks: ReadonlySet<string> | null,
|
||||
published: PublishedTypes | null,
|
||||
): Set<string> {
|
||||
const free = new Set<string>();
|
||||
if (mod.workers === true) return free;
|
||||
const methodImpls = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodImpls.get(m);
|
||||
if (!list) methodImpls.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
}
|
||||
const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id));
|
||||
const callees = new Map<string, string[]>();
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.async === true || fn.generator !== undefined || stackChecks?.has(fn.name)) continue;
|
||||
const tdz = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]);
|
||||
const calls: string[] = [];
|
||||
const bigint = (e: IrExpr): boolean => e.type.kind === "bigint";
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (!CAPTURE_FREE_EXPRS.has(e.kind)) return false;
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
if (tdz.has(e.localId)) return false;
|
||||
break;
|
||||
case "bin":
|
||||
// BigInt division and remainder by zero throw RangeError.
|
||||
if (bigint(e) || bigint(e.left) || bigint(e.right)) return false;
|
||||
break;
|
||||
case "unary":
|
||||
if (bigint(e) || bigint(e.operand)) return false;
|
||||
break;
|
||||
case "libCall":
|
||||
// Math functions only: they neither throw nor call back.
|
||||
if (!e.fn.startsWith("math.") || MAY_THROW_LIB_FNS.has(e.fn)) return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Methods that cannot throw, over numbers, booleans or strings
|
||||
// (no element conversion can run program code) and without a
|
||||
// callback argument.
|
||||
const elem = e.receiver.type.kind === "array" ? e.receiver.type.elem.kind : null;
|
||||
if (
|
||||
MAY_THROW_ARR_METHODS.has(e.method) ||
|
||||
(elem !== "f64" && elem !== "bool" && elem !== "string") ||
|
||||
e.args.some((arg) => arg.type.kind === "func")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "call":
|
||||
calls.push(e.callee);
|
||||
break;
|
||||
case "new":
|
||||
calls.push(`%${e.className}.constructor`);
|
||||
break;
|
||||
case "virtualCall": {
|
||||
const impls = methodImpls.get(e.method);
|
||||
if (impls === undefined) return false;
|
||||
calls.push(...impls);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (!CAPTURE_FREE_STMTS.has(s.kind)) return false;
|
||||
if (s.kind === "assign" && tdz.has(s.localId)) return false;
|
||||
if (published !== null && writesPublished(published, s)) return false;
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (fn.body.every(stmt)) callees.set(fn.name, calls);
|
||||
}
|
||||
// Drop candidates that call a function outside the set until none do.
|
||||
for (const name of callees.keys()) free.add(name);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [name, calls] of callees) {
|
||||
if (free.has(name) && !calls.every((callee) => free.has(callee))) {
|
||||
free.delete(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return free;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/** The young-in nursery (phase 1, experimental): compile-time opt-in with
|
||||
* SCRIPTC_YOUNG_NURSERY=1. Executables then allocate compiled class and
|
||||
* record instances young (the emitted constructors call scr_yin_enter) and
|
||||
* link the collector unit built with SCR_YIN, which collects young cycles
|
||||
* in nursery collections (scr_cycle.c, "young-in nursery"). Unset, nothing
|
||||
* the compiler emits or links changes. */
|
||||
export function youngNurseryRequested(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return env["SCRIPTC_YOUNG_NURSERY"] === "1";
|
||||
}
|
||||
@@ -285,6 +285,11 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const wasmArgs = values.wasm ?? [];
|
||||
if (wasmArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--wasm is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const wasmFiles = wasmArgs.map((file) => resolve(file));
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
@@ -364,6 +369,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
|
||||
});
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
|
||||
@@ -57,7 +57,8 @@ Options:
|
||||
compile the named npm packages' shipped JS statically as
|
||||
program modules (repeatable; "auto" opts in every eligible
|
||||
direct import: own .d.ts, unminified JS, no build-transform
|
||||
markers). Packages that fail preflight are refused with a
|
||||
markers; "pkg..." also names every package pkg depends
|
||||
on). Packages that fail preflight are refused with a
|
||||
diagnostic. Eligible dependencies compile automatically.
|
||||
--provenance-sources
|
||||
EXPERIMENTAL: compile npm dependencies from their
|
||||
@@ -79,6 +80,11 @@ Options:
|
||||
values and the bindings, fields, functions, classes and
|
||||
modules the dynamic tier would take over, with the
|
||||
refusal behind each; a name shows only that unit's chain
|
||||
--wasm <file.wasm>
|
||||
coverage only: report a WebAssembly module the program
|
||||
loads whose path its sources do not spell (repeatable;
|
||||
modules the sources name, inline modules and packaged
|
||||
.wasm files are found without it)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
@@ -102,6 +108,7 @@ export const CLI_OPTIONS = {
|
||||
"external-types": { type: "string", multiple: true },
|
||||
tier: { type: "string" },
|
||||
"explain-dynamic": { type: "string" },
|
||||
wasm: { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
@@ -518,6 +518,15 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
evidence: [fixture("static")],
|
||||
}),
|
||||
),
|
||||
{
|
||||
id: "stdlib.response.static.json",
|
||||
name: "Response.json",
|
||||
kind: "static-method",
|
||||
facets: ["webidl-conversion", "error-shape"],
|
||||
scope:
|
||||
"data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor",
|
||||
evidence: [fixture("static-response-json")],
|
||||
},
|
||||
...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({
|
||||
id: `stdlib.response.${member}`,
|
||||
name: `Response.${member}`,
|
||||
@@ -681,7 +690,8 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
),
|
||||
|
||||
staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"),
|
||||
...["error", "json", "redirect"].map((member) =>
|
||||
staticEntry("stdlib.response.static.json", "Response", "json", "static"),
|
||||
...["error", "redirect"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.response.static.${member}`,
|
||||
"Response",
|
||||
|
||||
@@ -171,6 +171,9 @@ export interface AnalyzeOptions {
|
||||
/** `--explain-dynamic`: attach the tier-assignment report
|
||||
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
|
||||
explainDynamic?: boolean;
|
||||
/** `--wasm <file>`: WebAssembly module files the program loads that its
|
||||
* sources do not name (absolute paths); reported with the discovered ones. */
|
||||
wasm?: readonly string[];
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user