Merge main

This commit is contained in:
Chris Tate
2026-10-19 08:31:43 -05:00
493 changed files with 18735 additions and 4583 deletions
-1
View File
@@ -140,7 +140,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.
+1
View File
@@ -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 |
+20
View File
@@ -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));
}
+92 -1
View File
@@ -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&lt;K, V&gt;(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.
+2
View File
@@ -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>
@@ -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 @@
- **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.
@@ -1 +1 @@
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `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. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
- **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 array, string, Map and Set iterators answer the same methods. `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.
@@ -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,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`.
+11
View File
@@ -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": "ef4244aca5b727e3683c",
"rowCount": 3880
}
@@ -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
@@ -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
View File
@@ -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"
}
]
},
+5 -2
View File
@@ -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,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
"audit-partial": 1175,
"verify-gap": 2153,
"audit-partial": 1176,
"verify-gap": 2152,
"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"
]
}
}
@@ -68887,12 +68894,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"
]
}
}
},
@@ -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"
@@ -91065,6 +91074,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",
@@ -94813,8 +94824,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"
},
+2 -21
View File
@@ -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`;
@@ -1009,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) {
@@ -1071,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"]) {
@@ -1141,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)
@@ -1150,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)})`,
);
@@ -1160,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})`,
);
+3 -2
View File
@@ -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"],
@@ -372,6 +372,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"] },
@@ -401,7 +402,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"] },
+42 -15
View File
@@ -3510,8 +3510,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;
@@ -3541,23 +3543,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;
@@ -3632,6 +3655,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. */
@@ -3649,6 +3675,7 @@ declare module "node:test" {
afterEach,
mock,
assert,
getTestContext,
run,
snapshot,
TestContext,
@@ -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,
@@ -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;
}
});
+84 -12
View File
@@ -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);
}
});
+2 -2
View File
@@ -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,
+150 -59
View File
@@ -1,6 +1,7 @@
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,
@@ -10,6 +11,7 @@ import {
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";
@@ -55,6 +57,7 @@ import {
typeKey,
STRING,
VOID,
classJsName,
} from "../../ir/ir.js";
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
@@ -914,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>;
@@ -965,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>();
@@ -1451,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(),
@@ -2507,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`,
@@ -4632,6 +4647,9 @@ export class LlEmitter {
// 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)`);
@@ -4664,6 +4682,12 @@ 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)})`,
@@ -4941,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);
@@ -4956,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();
@@ -4980,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);
}
}
}
@@ -5008,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
}
}
}
@@ -5018,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]!);
}
@@ -5091,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;
@@ -5621,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;
@@ -5945,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
@@ -6056,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);
}
@@ -6187,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);
@@ -6199,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);
}
@@ -6365,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);
@@ -6511,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,
);
}
@@ -7536,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);
@@ -7704,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}`);
@@ -7738,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) {
@@ -8083,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,
@@ -8130,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;
@@ -8157,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 {
@@ -8215,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)) {
@@ -8238,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;
@@ -8268,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);
@@ -8292,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);
@@ -8350,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,
@@ -8415,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);
@@ -8565,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
@@ -8631,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);
}
@@ -8666,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);
}
@@ -8682,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);
@@ -9325,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
@@ -9394,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;
@@ -9440,7 +9515,7 @@ export class LlEmitter {
s.kind !== "rethrow" &&
s.kind !== "runtimeFence"
) {
this.releaseFrame(frame);
this.releaseTemps(frame);
}
}
@@ -9652,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);
@@ -9693,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;
}
@@ -9705,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;
}
@@ -9863,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(
@@ -9883,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}`);
}
@@ -9950,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) {
@@ -10179,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
@@ -10384,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 });
}
@@ -10637,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);
@@ -10733,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 };
}
@@ -10772,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,
@@ -10795,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
@@ -10820,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.
@@ -10836,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)]);
@@ -92,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"
@@ -165,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];
@@ -287,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";
@@ -305,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(
@@ -320,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(
@@ -429,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)`);
@@ -486,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)`);
@@ -526,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)
@@ -716,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).
@@ -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)`,
);
@@ -316,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",
@@ -325,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.
@@ -825,6 +829,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",
@@ -892,6 +897,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.iteratorCanStep": "scr_dyn_iterator_can_step",
"dyn.iteratorStep": "scr_dyn_iterator_step",
"dyn.iteratorStepDone": "scr_dyn_iterator_step_done",
"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",
@@ -1412,6 +1418,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(
+11 -7
View File
@@ -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]!,
@@ -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;
@@ -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)
? []
+102 -35
View File
@@ -29,6 +29,7 @@ import {
mangleRecordTrace,
} from "../mangle.js";
import {
CYC_HDR,
cycTypeDescriptor,
emitObjectAlloc,
emitObjectFree,
@@ -58,67 +59,132 @@ import {
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
@@ -1622,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);
@@ -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))}`,
);
}
+3 -1
View File
@@ -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;
}
@@ -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,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";
}
+11 -1
View File
@@ -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",
@@ -49,6 +49,7 @@ export interface EarlyExecutableNativeFeatures {
/** Omitted is the historical release posture. */
optimization?: "dev" | "speed";
workers?: boolean;
youngNursery?: boolean;
regex: boolean;
copying: boolean;
textDecoderLegacy: boolean;
@@ -112,6 +113,8 @@ export interface EarlyExecutableCacheOptions {
windowsSubsystem?: "gui";
/** Omitted unless --detached-exit is set. */
detachedExit?: true;
/** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */
youngNursery?: true;
npmStatic: readonly string[] | "auto" | null;
/** Raw manifest identity: path and bytes. Native archives remain under the
* stricter native cache's independent dependency validation. */
@@ -202,7 +205,8 @@ function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeat
(native.optimization === undefined ||
native.optimization === "dev" ||
native.optimization === "speed") &&
(native.workers === undefined || typeof native.workers === "boolean"),
(native.workers === undefined || typeof native.workers === "boolean") &&
(native.youngNursery === undefined || typeof native.youngNursery === "boolean"),
);
}
@@ -223,6 +227,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
...(options.strip ? ["strip"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
...(options.detachedExit ? ["detached-exit"] : []),
...(options.youngNursery ? ["young-nursery"] : []),
options.npmStatic === null
? "<npm-static-off>"
: options.npmStatic === "auto"
@@ -262,6 +267,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
if (options.strip) hash.update("strip\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
if (options.detachedExit) hash.update("detached-exit\0");
if (options.youngNursery) hash.update("young-nursery\0");
hash
.update(
options.npmStatic === null
+31 -31
View File
@@ -56,8 +56,8 @@ function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
export function bareRequireSpecOf(e: ts.Node): string | null {
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return null;
if (!ts.isIdentifier(e.expression) || e.expression.text !== "require") return null;
if (e.arguments.length !== 1) return null;
const arg = e.arguments[0]!;
if (e.arguments.nodes.length !== 1) return null;
const arg = e.arguments.nodes[0]!;
return ts.isStringLiteral(arg) ? arg.text : null;
}
@@ -135,7 +135,7 @@ function scanTableLiteral(
reexports: string[],
): void {
const text = sf.text;
for (const prop of obj.properties) {
for (const prop of obj.properties.nodes) {
if (ts.isGetAccessor(prop)) {
// `get name() {...}` — merve's explicit early termination... unless
// the accessor's NAME is not a plain identifier, where the lexer
@@ -253,8 +253,8 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set
) {
return;
}
if (call.arguments.length !== 3) return;
const [recv, nameArg, desc] = call.arguments as unknown as [
if (call.arguments.nodes.length !== 3) return;
const [recv, nameArg, desc] = call.arguments.nodes as unknown as [
ts.Expression,
ts.Expression,
ts.Expression,
@@ -263,7 +263,7 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set
if (!ts.isStringLiteral(nameArg)) return;
if (!ts.isObjectLiteralExpression(desc)) return;
let i = 0;
const props = desc.properties;
const props = desc.properties.nodes;
const first = props[0];
if (
first !== undefined &&
@@ -287,8 +287,8 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set
// and the getter must be the descriptor's LAST property.
const body = getterBodyOf(p);
if (body === undefined || i !== props.length - 1) return;
if (body.statements.length !== 1) return;
const ret = body.statements[0]!;
if (body.statements.nodes.length !== 1) return;
const ret = body.statements.nodes[0]!;
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return;
const r = ret.expression;
const returnsIdentish =
@@ -313,7 +313,7 @@ function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined {
p.name.text === "get" &&
p.asteriskToken === undefined &&
(ts.getModifiers(p) ?? []).length === 0 &&
p.parameters.length === 0
p.parameters.nodes.length === 0
) {
return p.body;
}
@@ -322,7 +322,7 @@ function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined {
ts.isIdentifier(p.name) &&
p.name.text === "get" &&
ts.isFunctionExpression(p.initializer) &&
p.initializer.parameters.length === 0 &&
p.initializer.parameters.nodes.length === 0 &&
p.initializer.asteriskToken === undefined &&
(ts.getModifiers(p.initializer) ?? []).length === 0
) {
@@ -349,7 +349,7 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string |
if (nameNode === undefined) return null;
if (nameNode.text !== "__export" && nameNode.text !== "__exportStar") return null;
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
const arg0 = call.arguments[0];
const arg0 = call.arguments.nodes[0];
if (arg0 === undefined) return null;
if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
return leadingRequireOf(arg0, sf)?.spec ?? null;
@@ -369,7 +369,7 @@ function byteAdjacentParen(sf: ts.SourceFile, nameEnd: number, argStart: number)
* probed: a member-qualified helper or one space after its paren misses).
* Answers [ID, spec]. */
function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, string] | null {
const decl = stmt.declarationList.declarations[0];
const decl = stmt.declarationList.declarations.nodes[0];
if (decl === undefined || !ts.isIdentifier(decl.name) || !sourceSpellsIdentifier(decl.name, sf))
return null;
const init = decl.initializer;
@@ -381,9 +381,9 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s
init.questionDotToken === undefined &&
ts.isIdentifier(init.expression) &&
init.expression.text === "_interopRequireWildcard" &&
init.arguments.length >= 1
init.arguments.nodes.length >= 1
) {
const arg0 = init.arguments[0]!;
const arg0 = init.arguments.nodes[0]!;
if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
const spec = bareRequireSpecOf(arg0);
if (spec !== null) return [decl.name.text, spec];
@@ -434,8 +434,8 @@ function isHasOwnCall(e: ts.Expression, key: string): boolean {
)
cur = cur.expression;
if (!ts.isIdentifier(cur) || cur.text !== "Object") return false;
if (e.arguments.length !== 2) return false;
const [recv, k] = e.arguments as unknown as [ts.Expression, ts.Expression];
if (e.arguments.nodes.length !== 2) return false;
const [recv, k] = e.arguments.nodes as unknown as [ts.Expression, ts.Expression];
return ts.isIdentifier(recv) && ts.isIdentifier(k) && k.text === key;
}
@@ -448,9 +448,9 @@ function isDirectHasOwnCall(e: ts.Expression, key: string): boolean {
ts.isIdentifier(e.expression.name) &&
e.expression.name.text === "hasOwnProperty" &&
ts.isIdentifier(e.expression.expression) &&
e.arguments.length === 1 &&
ts.isIdentifier(e.arguments[0]!) &&
(e.arguments[0] as ts.Identifier).text === key
e.arguments.nodes.length === 1 &&
ts.isIdentifier(e.arguments.nodes[0]!) &&
(e.arguments.nodes[0] as ts.Identifier).text === key
);
}
@@ -477,16 +477,16 @@ function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean
) {
return false;
}
if (e.arguments.length !== 3) return false;
const [recv, nameArg, desc] = e.arguments as unknown as [
if (e.arguments.nodes.length !== 3) return false;
const [recv, nameArg, desc] = e.arguments.nodes as unknown as [
ts.Expression,
ts.Expression,
ts.Expression,
];
if (!isExportsTarget(recv)) return false;
if (!ts.isIdentifier(nameArg) || nameArg.text !== key) return false;
if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 2) return false;
const [en, get] = desc.properties as unknown as [
if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 2) return false;
const [en, get] = desc.properties.nodes as unknown as [
ts.ObjectLiteralElementLike,
ts.ObjectLiteralElementLike,
];
@@ -494,8 +494,8 @@ function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean
return false;
if (en.initializer.kind !== ts.SyntaxKind.TrueKeyword) return false;
const body = getterBodyOf(get);
if (body === undefined || body.statements.length !== 1) return false;
const ret = body.statements[0]!;
if (body === undefined || body.statements.nodes.length !== 1) return false;
const ret = body.statements.nodes[0]!;
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return false;
return isKeyedAccess(ret.expression, (x) => ts.isIdentifier(x) && x.text === id1, key);
}
@@ -541,16 +541,16 @@ function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null
) {
return null;
}
if (keysCall.arguments.length !== 1) return null;
const id1Node = keysCall.arguments[0]!;
if (keysCall.arguments.nodes.length !== 1) return null;
const id1Node = keysCall.arguments.nodes[0]!;
if (!ts.isIdentifier(id1Node) || !sourceSpellsIdentifier(id1Node, sf)) return null;
const id1 = id1Node.text;
const fn = call.arguments[0];
const fn = call.arguments.nodes[0];
if (fn === undefined || !ts.isFunctionExpression(fn)) return null; // arrows never match (probed)
if (fn.name !== undefined) return null; // a NAMED callback misses (probed)
if (fn.asteriskToken !== undefined || (ts.getModifiers(fn) ?? []).length > 0) return null;
if (fn.parameters.length !== 1) return null;
const keyParam = fn.parameters[0]!;
if (fn.parameters.nodes.length !== 1) return null;
const keyParam = fn.parameters.nodes[0]!;
if (
!ts.isIdentifier(keyParam.name) ||
keyParam.initializer !== undefined ||
@@ -558,7 +558,7 @@ function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null
)
return null;
const key = keyParam.name.text;
const stmts = fn.body.statements;
const stmts = fn.body.statements.nodes;
// Form B: the single `if (KEY !== 'default' [&& !hasOwn]) COPY` statement.
if (stmts.length === 1 && ts.isIfStatement(stmts[0]!) && stmts[0]!.elseStatement === undefined) {
@@ -262,7 +262,7 @@ function placeHeritage(
return unsupported(out, node, marker);
// `implements Acyclic<C>` would make the class implement C's own type,
// which the lowering treats as a protocol of C.
if ((node.typeArguments?.length ?? 0) > 0) return unsupported(out, node, marker);
if ((node.typeArguments?.nodes.length ?? 0) > 0) return unsupported(out, node, marker);
const note = (name: string): ContractNote => ({ marker, loc: locOf(node), name });
if (
(ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) &&
@@ -324,8 +324,8 @@ function place(
ts.isTypeReferenceNode(parent) &&
ts.isIdentifier(parent.typeName) &&
(parent.typeName.text === "Array" || parent.typeName.text === "ReadonlyArray") &&
parent.typeArguments?.length === 1 &&
parent.typeArguments[0] === ref
parent.typeArguments?.nodes.length === 1 &&
parent.typeArguments.nodes[0] === ref
) {
node = parent;
parent = parent.parent;
@@ -336,7 +336,7 @@ function place(
// contract of the function the NoThrow annotates.
if (marker !== "NoThrow" && parent !== undefined && ts.isTypeReferenceNode(parent)) {
const wrapper = markerOf(checker, parent);
if (wrapper === "NoThrow" && parent.typeArguments?.[0] === node) {
if (wrapper === "NoThrow" && parent.typeArguments?.nodes[0] === node) {
node = parent;
parent = parent.parent;
}
@@ -1,14 +1,15 @@
/* Provenance of the @scriptc/experimental exports. The package is scriptc's
* own (packages/experimental): Node.js runs its JavaScript implementation,
* scriptc builds lower its exports (lowering/lifetime-regions.ts,
* lowering/intern-tables.ts) and never compile its body. */
* lowering/intern-tables.ts, lowering/thread-slots.ts) and never compile
* its body. */
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import * as ts from "./ts7/adapter.js";
export const EXPERIMENTAL_MODULE = "@scriptc/experimental";
export type ExperimentalMember = "LifetimeRegion" | "InternTable";
export type ExperimentalMember = "LifetimeRegion" | "InternTable" | "ThreadSlots";
/** The @scriptc/experimental export an identifier is a named-import binding of. */
export function experimentalImportOf(
@@ -27,7 +28,9 @@ export function experimentalImportOf(
)
return null;
const member = decl.propertyName?.text ?? decl.name.text;
return member === "LifetimeRegion" || member === "InternTable" ? member : null;
return member === "LifetimeRegion" || member === "InternTable" || member === "ThreadSlots"
? member
: null;
}
/** Whether `sf` is the package's own declaration file: its directory's
@@ -114,3 +117,27 @@ export function internTableMemberOf(
? name
: null;
}
export type ThreadSlotsMember = "get" | "has" | "set" | "delete";
/** The ThreadSlots member a property access names (`slots.get`), by the
* declaration it resolves to. */
export function threadSlotsMemberOf(
program: ts.Program,
access: ts.PropertyAccessExpression,
known: Map<string, boolean>,
): ThreadSlotsMember | null {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(access.name);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
if (decl === undefined || !ts.isMethodDeclaration(decl)) return null;
const owner = decl.parent;
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "ThreadSlots" ||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
)
return null;
const name = access.name.text;
return name === "get" || name === "has" || name === "set" || name === "delete" ? name : null;
}
+10 -10
View File
@@ -158,13 +158,13 @@ function staticBoolean(
if (
ts.isCallExpression(current) &&
!current.questionDotToken &&
current.arguments.length === 1 &&
current.arguments.nodes.length === 1 &&
ts.isPropertyAccessExpression(current.expression) &&
!current.expression.questionDotToken &&
(current.expression.name.text === "endsWith" || current.expression.name.text === "startsWith")
) {
const receiver = staticString(program, current.expression.expression, seen);
const needle = staticString(program, current.arguments[0]!, seen);
const needle = staticString(program, current.arguments.nodes[0]!, seen);
if (receiver === null || needle === null) return null;
return current.expression.name.text === "endsWith"
? receiver.endsWith(needle)
@@ -194,7 +194,7 @@ function staticString(
if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href;
if (ts.isTemplateExpression(current)) {
let value = current.head.text;
for (const span of current.templateSpans) {
for (const span of current.templateSpans.nodes) {
const part = staticString(program, span.expression, seen);
if (part === null) return null;
value += part + span.literal.text;
@@ -219,11 +219,11 @@ function staticString(
if (
ts.isCallExpression(current) &&
!current.questionDotToken &&
current.arguments.length === 1 &&
current.arguments.nodes.length === 1 &&
ts.isIdentifier(current.expression) &&
isBuiltinMemberImport(program, current.expression, "url", "fileURLToPath")
) {
const href = staticString(program, current.arguments[0]!, seen);
const href = staticString(program, current.arguments.nodes[0]!, seen);
if (href === null) return null;
try {
return fileURLToPath(href);
@@ -237,7 +237,7 @@ function staticString(
(isBuiltinMemberImport(program, current.expression, "url", "URL") ||
(current.expression.text === "URL" && isNodeGlobal(program, current.expression)))
) {
const args = current.arguments ?? [];
const args = current.arguments?.nodes ?? [];
if (args.length < 1 || args.length > 2) return null;
const input = staticString(program, args[0]!, seen);
const base = args[1] === undefined ? null : staticString(program, args[1], seen);
@@ -261,7 +261,7 @@ export function staticForkModulePath(program: ts.Program, expr: ts.Expression):
const supported =
(ts.isCallExpression(current) &&
!current.questionDotToken &&
current.arguments.length === 1 &&
current.arguments.nodes.length === 1 &&
ts.isIdentifier(current.expression) &&
isBuiltinMemberImport(program, current.expression, "url", "fileURLToPath")) ||
(ts.isNewExpression(current) &&
@@ -279,7 +279,7 @@ export function staticForkModulePath(program: ts.Program, expr: ts.Expression):
}
export function forkCallModulePath(program: ts.Program, call: ts.CallExpression): string | null {
if (call.questionDotToken || call.arguments[0] === undefined) return null;
if (call.questionDotToken || call.arguments.nodes[0] === undefined) return null;
const callee = strip(call.expression);
const isFork =
(ts.isIdentifier(callee) && isBuiltinMemberImport(program, callee, "child_process", "fork")) ||
@@ -291,7 +291,7 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression)
if (!isFork) {
return null;
}
return staticForkModulePath(program, call.arguments[0]);
return staticForkModulePath(program, call.arguments.nodes[0]);
}
/** Source-order static fork targets in the supplied files, deduplicated by
@@ -304,7 +304,7 @@ export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFi
const calls = moduleSourceCandidates(program, sourceFile).calls;
const bindings: ts.Node[] = [];
for (const call of calls) {
if (call.questionDotToken || call.arguments[0] === undefined) continue;
if (call.questionDotToken || call.arguments.nodes[0] === undefined) continue;
const callee = strip(call.expression);
if (ts.isIdentifier(callee)) bindings.push(callee);
else if (
@@ -6,14 +6,14 @@ export function functionCanFallThrough(declaration: ts.Node): boolean {
if (
!ts.isFunctionLike(declaration) ||
declaration.asteriskToken ||
declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
declaration.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
)
return false;
const body = declaration.body as ts.Block | ts.Expression | undefined;
if (!body || !ts.isBlock(body)) return false;
const completes = (statement: ts.Statement): boolean => {
if (ts.isReturnStatement(statement) || ts.isThrowStatement(statement)) return false;
if (ts.isBlock(statement)) return sequence(statement.statements);
if (ts.isBlock(statement)) return sequence(statement.statements.nodes);
if (ts.isIfStatement(statement))
return (
completes(statement.thenStatement) ||
@@ -29,13 +29,13 @@ export function functionCanFallThrough(declaration: ts.Node): boolean {
}
if (ts.isSwitchStatement(statement))
return (
!statement.caseBlock.clauses.some(ts.isDefaultClause) ||
statement.caseBlock.clauses.some((clause) => sequence(clause.statements))
!statement.caseBlock.clauses.nodes.some(ts.isDefaultClause) ||
statement.caseBlock.clauses.nodes.some((clause) => sequence(clause.statements.nodes))
);
return true;
};
const sequence = (statements: readonly ts.Statement[]): boolean => statements.every(completes);
return sequence(body.statements);
return sequence(body.statements.nodes);
}
/** A documented JavaScript result must also represent explicit empty returns. */
@@ -44,7 +44,7 @@ export function functionCanReturnUndefined(declaration: ts.Node): boolean {
if (
!ts.isFunctionLike(declaration) ||
declaration.asteriskToken ||
declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
declaration.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
)
return false;
const body = declaration.body;
@@ -50,9 +50,10 @@ export function inferredJsDiagnosticSuppressed(
(ts.isFunctionDeclaration(declaration) ||
ts.isFunctionExpression(declaration) ||
ts.isMethodDeclaration(declaration)) &&
node.arguments.length > declaration.parameters.length &&
node.arguments[declaration.parameters.length]?.getStart(file) === diagnostic.pos &&
node.arguments[node.arguments.length - 1]?.getEnd() === diagnostic.end &&
node.arguments.nodes.length > declaration.parameters.nodes.length &&
node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) ===
diagnostic.pos &&
node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end &&
bodyReadsArguments(declaration)
) {
suppressed = true;
@@ -92,8 +93,8 @@ export function inferredJsDiagnosticSuppressed(
ts.isMethodDeclaration(declaration) &&
declaration.type === undefined &&
isJsSourceFileName(declaration.getSourceFile().fileName) &&
declaration.body?.statements.length === 1 &&
ts.isThrowStatement(declaration.body.statements[0]!)
declaration.body?.statements?.nodes.length === 1 &&
ts.isThrowStatement(declaration.body.statements.nodes[0]!)
) {
suppressed = true;
return "stop";
+24 -24
View File
@@ -141,7 +141,7 @@ function localTypeBindings(file: ts.SourceFile, bindings: TypeBindings): Set<str
const add = (name: ts.Identifier | undefined): void => {
if (name !== undefined && CONTRACT_GLOBAL_TYPES.has(name.text)) names.add(name.text);
};
for (const stmt of file.statements) {
for (const stmt of file.statements.nodes) {
if (
ts.isInterfaceDeclaration(stmt) ||
ts.isTypeAliasDeclaration(stmt) ||
@@ -160,7 +160,7 @@ function localTypeBindings(file: ts.SourceFile, bindings: TypeBindings): Set<str
add(clause.name);
if (clause.namedBindings === undefined) continue;
if (ts.isNamespaceImport(clause.namedBindings)) add(clause.namedBindings.name);
else for (const el of clause.namedBindings.elements) add(el.name);
else for (const el of clause.namedBindings.elements.nodes) add(el.name);
}
bindings.set(file, names);
return names;
@@ -176,7 +176,7 @@ function isUnshadowedGlobalType(
): boolean {
if (localTypeBindings(file, bindings).has(name)) return false;
for (let scope = node.parent; scope !== undefined && scope !== file; scope = scope.parent) {
const params = scope.typeParameters;
const params = scope.typeParameters?.nodes;
if (params?.some((p) => ts.isTypeParameterDeclaration(p) && p.name.text === name) === true)
return false;
}
@@ -254,7 +254,7 @@ function typeShape(
return { k: "array", elem: typeShape(file, node.elementType, bindings) };
if (ts.isTupleTypeNode(node)) {
const elems: ContractTypeShape[] = [];
for (const e of node.elements) {
for (const e of node.elements.nodes) {
// Named tuple members carry the type under `.type`.
const t = ts.isNamedTupleMember(e) ? e.type : (e as ts.TypeNode);
elems.push(typeShape(file, t, bindings));
@@ -262,13 +262,13 @@ function typeShape(
return { k: "tuple", elems };
}
if (ts.isUnionTypeNode(node)) {
return { k: "union", parts: node.types.map((t) => typeShape(file, t, bindings)) };
return { k: "union", parts: node.types.nodes.map((t) => typeShape(file, t, bindings)) };
}
if (ts.isTypeLiteralNode(node)) {
let bad: string | null = null;
const fields = shapeOfMembers(
file,
node.members,
node.members.nodes,
(text) => {
bad = text;
},
@@ -292,16 +292,16 @@ function typeShape(
const name = node.typeName.text;
const global =
CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings);
if (global && name === "Uint8Array" && (node.typeArguments?.length ?? 0) === 0)
if (global && name === "Uint8Array" && (node.typeArguments?.nodes.length ?? 0) === 0)
return { k: "bytes" };
if (
global &&
(name === "Array" || name === "ReadonlyArray") &&
node.typeArguments?.length === 1
node.typeArguments?.nodes.length === 1
) {
return { k: "array", elem: typeShape(file, node.typeArguments[0]!, bindings) };
return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) };
}
if ((node.typeArguments?.length ?? 0) > 0)
if ((node.typeArguments?.nodes.length ?? 0) > 0)
return { k: "unsupported", text: node.getText(file) };
return { k: "ref", name };
}
@@ -328,7 +328,7 @@ function stringArray(expr: ts.Expression): string[] | null {
const e = unwrapConst(expr);
if (!ts.isArrayLiteralExpression(e)) return null;
const out: string[] = [];
for (const el of e.elements) {
for (const el of e.elements.nodes) {
const v = unwrapConst(el as ts.Expression);
if (!ts.isStringLiteral(v)) return null;
out.push(v.text);
@@ -340,12 +340,12 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null
const e = unwrapConst(expr);
if (!ts.isArrayLiteralExpression(e)) return null;
const out: { env: string; msg: string }[] = [];
for (const el of e.elements) {
for (const el of e.elements.nodes) {
const v = unwrapConst(el as ts.Expression);
if (!ts.isObjectLiteralExpression(v)) return null;
let env: string | null = null;
let msg: string | null = null;
for (const prop of v.properties) {
for (const prop of v.properties.nodes) {
if (!ts.isPropertyAssignment(prop)) return null;
const name = propName(prop.name);
const val = unwrapConst(prop.initializer);
@@ -410,7 +410,7 @@ export function entryContractFacts(
* and the refusal names every site. */
const collectTypes = (file: ts.SourceFile): void => {
const exportedHere = new Set<string>();
for (const stmt of file.statements) {
for (const stmt of file.statements.nodes) {
if (
(ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) &&
isExported(stmt)
@@ -418,7 +418,7 @@ export function entryContractFacts(
exportedHere.add(stmt.name.text);
}
}
for (const stmt of file.statements) {
for (const stmt of file.statements.nodes) {
if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) {
const name = stmt.name.text;
const exported = isExported(stmt);
@@ -433,14 +433,14 @@ export function entryContractFacts(
let bad: string | null = null;
const fields = shapeOfMembers(
file,
stmt.members,
stmt.members.nodes,
(text) => {
bad = text;
},
bindings,
);
const generic = (stmt.typeParameters?.length ?? 0) > 0;
const heritage = (stmt.heritageClauses?.length ?? 0) > 0;
const generic = (stmt.typeParameters?.nodes.length ?? 0) > 0;
const heritage = (stmt.heritageClauses?.nodes.length ?? 0) > 0;
facts.types.push({
name,
form: "interface",
@@ -455,7 +455,7 @@ export function entryContractFacts(
loc: locOf(file, stmt),
});
} else {
const generic = (stmt.typeParameters?.length ?? 0) > 0;
const generic = (stmt.typeParameters?.nodes.length ?? 0) > 0;
facts.types.push({
name,
form: "alias",
@@ -473,7 +473,7 @@ export function entryContractFacts(
stmt.body !== undefined &&
ts.isModuleBlock(stmt.body)
) {
for (const inner of stmt.body.statements) {
for (const inner of stmt.body.statements.nodes) {
if (ts.isInterfaceDeclaration(inner) || ts.isTypeAliasDeclaration(inner)) {
addSite(inner.name.text, siteOf(file, inner));
}
@@ -494,25 +494,25 @@ export function entryContractFacts(
if (list.length > 1) facts.multiSiteTypes.push({ name, sites: list });
}
for (const stmt of entry.statements) {
for (const stmt of entry.statements.nodes) {
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) {
if (!isExported(stmt)) continue;
if (stmt.body === undefined && facts.functions.some((f) => f.name === stmt.name!.text))
continue; // overload signature
facts.functions.push({
name: stmt.name.text,
params: stmt.parameters.map((p) => ({
params: stmt.parameters.nodes.map((p) => ({
name: ts.isIdentifier(p.name) ? p.name.text : "<pattern>",
shape: p.type !== undefined ? typeShape(entry, p.type, bindings) : null,
})),
returns: stmt.type !== undefined ? typeShape(entry, stmt.type, bindings) : null,
generic: (stmt.typeParameters?.length ?? 0) > 0,
generic: (stmt.typeParameters?.nodes.length ?? 0) > 0,
loc: locOf(entry, stmt),
});
continue;
}
if (ts.isVariableStatement(stmt) && isExported(stmt)) {
for (const decl of stmt.declarationList.declarations) {
for (const decl of stmt.declarationList.declarations.nodes) {
if (!ts.isIdentifier(decl.name)) continue;
const name = decl.name.text;
if (!CONVENTION_CONSTS.has(name)) continue;
@@ -26,7 +26,7 @@ export interface EntryExportInfo {
* Overload signatures collapse onto one entry (same name, one body). */
export function entryFunctionExports(entry: ts.SourceFile): Map<string, EntryExportInfo> {
const out = new Map<string, EntryExportInfo>();
for (const stmt of entry.statements) {
for (const stmt of entry.statements.nodes) {
if (!ts.isFunctionDeclaration(stmt) || stmt.name === undefined) continue;
const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined;
const exported = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true;
@@ -36,7 +36,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map<string, EntryExp
out.set(name, {
name,
loc: { file: entry.fileName, start: stmt.getStart(), end: stmt.end },
generic: (stmt.typeParameters?.length ?? 0) > 0,
generic: (stmt.typeParameters?.nodes.length ?? 0) > 0,
async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true,
generator: stmt.asteriskToken !== undefined,
});
@@ -110,14 +110,14 @@ export function isAssertClassNew(lowerer: Lowerer, expr: ts.Expression): boolean
/** The options of `new Assert(<literal options>)`, or null when an option is
* not a literal or the argument list has another shape. */
export function literalAssertOptions(expr: ts.NewExpression): AssertInstanceOptions | null {
const args = expr.arguments ?? [];
const args = expr.arguments?.nodes ?? [];
const options: AssertInstanceOptions = { strict: true, diff: "simple", skipPrototype: false };
if (args.length === 0) return options;
if (args.length > 1) return null;
const arg = stripTypeCasts(args[0]!);
if (ts.isIdentifier(arg) && arg.text === "undefined") return options;
if (!ts.isObjectLiteralExpression(arg)) return null;
for (const prop of arg.properties) {
for (const prop of arg.properties.nodes) {
if (!ts.isPropertyAssignment(prop)) return null;
const name =
ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null;
@@ -24,7 +24,7 @@ export function lowerAddAbortListener(
expr: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const args = expr.arguments;
const args = expr.arguments.nodes;
if (args.length > 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(`addAbortListener with this argument list`, expr);
}
@@ -192,7 +192,7 @@ export function literalBoolOptions(
if (!ts.isObjectLiteralExpression(node)) return null;
const bools: Record<string, boolean> = {};
const exprs: Record<string, ts.Expression> = {};
for (const p of node.properties) {
for (const p of node.properties.nodes) {
if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) return null;
if (exprKeys.includes(p.name.text)) {
exprs[p.name.text] = p.initializer;
@@ -106,14 +106,14 @@ export function lowerDiagnosticsChannelMethodCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (name === "publish" && call.arguments.length === 1) {
if (name === "publish" && call.arguments.nodes.length === 1) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const msg = lowerDiagnosticsMessage(lowerer, call.arguments[0]!);
const msg = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!);
return { kind: "libCall", fn: "dc.publish", args: [receiver, msg], type: VOID, loc };
}
if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) {
if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!);
const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!);
return name === "subscribe"
? { kind: "libCall", fn: "dc.chanSubscribe", args: [receiver, cb], type: VOID, loc }
: { kind: "libCall", fn: "dc.chanUnsubscribe", args: [receiver, cb], type: BOOL, loc };
@@ -126,18 +126,18 @@ export function lowerDiagnosticsChannelMethodCall(
// this/arguments to fn exactly like the trace calls.
if (
(name === "bindStore" || name === "unbindStore") &&
call.arguments.length >= 1 &&
call.arguments.length <= (name === "bindStore" ? 2 : 1)
call.arguments.nodes.length >= 1 &&
call.arguments.nodes.length <= (name === "bindStore" ? 2 : 1)
) {
if (!isBuiltinClassInstance(lowerer, call.arguments[0]!, "AsyncLocalStorage")) {
if (!isBuiltinClassInstance(lowerer, call.arguments.nodes[0]!, "AsyncLocalStorage")) {
lowerer.noLowering(
`Channel.${name} with this store argument`,
call.arguments[0]!,
call.arguments.nodes[0]!,
"an AsyncLocalStorage instance (node:async_hooks) is the supported store",
);
}
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const store = lowerer.lowerExprExpecting(call.arguments[0]!, F64);
const store = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64);
if (name === "unbindStore") {
return {
kind: "libCall",
@@ -148,8 +148,8 @@ export function lowerDiagnosticsChannelMethodCall(
};
}
const transform: IrExpr =
call.arguments[1] !== undefined
? lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!)
call.arguments.nodes[1] !== undefined
? lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!)
: dynUndefinedExpr(loc);
return {
kind: "libCall",
@@ -159,15 +159,15 @@ export function lowerDiagnosticsChannelMethodCall(
loc,
};
}
if (name === "runStores" && call.arguments.length >= 2) {
if (name === "runStores" && call.arguments.nodes.length >= 2) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const data = lowerDiagnosticsMessage(lowerer, call.arguments[0]!);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!);
const data = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!);
const thisArg: IrExpr =
call.arguments[2] !== undefined
? lowerDiagnosticsMessage(lowerer, call.arguments[2]!)
call.arguments.nodes[2] !== undefined
? lowerDiagnosticsMessage(lowerer, call.arguments.nodes[2]!)
: dynUndefinedExpr(loc);
const rest = lowerTracingArguments(lowerer, call.arguments.slice(3), loc);
const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3), loc);
return {
kind: "libCall",
fn: "dc.chanRunStores",
@@ -198,7 +198,7 @@ export function lowerAsyncLocalStorageMethodCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (name === "getStore" && call.arguments.length === 0) {
if (name === "getStore" && call.arguments.nodes.length === 0) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
return asyncLocalStorageResult(lowerer, call, {
kind: "libCall",
@@ -208,11 +208,11 @@ export function lowerAsyncLocalStorageMethodCall(
loc,
});
}
if (name === "run" && call.arguments.length >= 2) {
if (name === "run" && call.arguments.nodes.length >= 2) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!);
const rest = lowerTracingArguments(lowerer, call.arguments.slice(2), loc);
const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!);
const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(2), loc);
return asyncLocalStorageResult(lowerer, call, {
kind: "libCall",
fn: "als.run",
@@ -221,10 +221,10 @@ export function lowerAsyncLocalStorageMethodCall(
loc,
});
}
if (name === "exit" && call.arguments.length >= 1) {
if (name === "exit" && call.arguments.nodes.length >= 1) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!);
const rest = lowerTracingArguments(lowerer, call.arguments.slice(1), loc);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!);
const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(1), loc);
return asyncLocalStorageResult(lowerer, call, {
kind: "libCall",
fn: "als.exitRun",
@@ -233,12 +233,12 @@ export function lowerAsyncLocalStorageMethodCall(
loc,
});
}
if (name === "enterWith" && call.arguments.length === 1) {
if (name === "enterWith" && call.arguments.nodes.length === 1) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!);
const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!);
return { kind: "libCall", fn: "als.enterWith", args: [receiver, value], type: VOID, loc };
}
if (name === "disable" && call.arguments.length === 0) {
if (name === "disable" && call.arguments.nodes.length === 0) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
return { kind: "libCall", fn: "als.disable", args: [receiver], type: VOID, loc };
}
@@ -301,13 +301,13 @@ function lowerTracingHandlers(lowerer: Lowerer, node: ts.Expression): IrExpr {
if (
ts.isObjectLiteralExpression(e) &&
lowerer.mapTypeOf(lowerer.typeOf(e))?.kind === "record" &&
e.properties.length > 0 &&
e.properties.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name))
e.properties.nodes.length > 0 &&
e.properties.nodes.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name))
) {
const loc = locOf(e);
return {
kind: "dynObjLit",
fields: e.properties.map((p) => {
fields: e.properties.nodes.map((p) => {
const pa = p as ts.PropertyAssignment;
return {
key: {
@@ -366,32 +366,32 @@ export function lowerTracingChannelMethodCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) {
if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const handlers = lowerTracingHandlers(lowerer, call.arguments[0]!);
const handlers = lowerTracingHandlers(lowerer, call.arguments.nodes[0]!);
return name === "subscribe"
? { kind: "libCall", fn: "dc.tcSubscribe", args: [receiver, handlers], type: VOID, loc }
: { kind: "libCall", fn: "dc.tcUnsubscribe", args: [receiver, handlers], type: BOOL, loc };
}
if (
(name === "traceSync" || name === "traceCallback" || name === "tracePromise") &&
call.arguments.length >= 1
call.arguments.nodes.length >= 1
) {
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!);
const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!);
// traceSync(fn, context?, thisArg?, ...args) /
// tracePromise(fn, context?, thisArg?, ...args) /
// traceCallback(fn, position?, context?, thisArg?, ...args)
const shift = name === "traceCallback" ? 1 : 0;
const ctxNode = call.arguments[1 + shift];
const thisNode = call.arguments[2 + shift];
const ctxNode = call.arguments.nodes[1 + shift];
const thisNode = call.arguments.nodes[2 + shift];
const ctx: IrExpr =
ctxNode !== undefined
? lowerDiagnosticsMessage(lowerer, ctxNode)
: { kind: "dynObjLit", fields: [], type: DYN, loc };
const thisArg: IrExpr =
thisNode !== undefined ? lowerDiagnosticsMessage(lowerer, thisNode) : dynUndefinedExpr(loc);
const rest = lowerTracingArguments(lowerer, call.arguments.slice(3 + shift), loc);
const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3 + shift), loc);
if (name === "traceSync") {
return {
kind: "libCall",
@@ -416,8 +416,8 @@ export function lowerTracingChannelMethodCall(
};
}
const pos: IrExpr =
call.arguments[1] !== undefined
? lowerer.lowerExprExpecting(call.arguments[1]!, F64)
call.arguments.nodes[1] !== undefined
? lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64)
: { kind: "numLit", value: -1, type: F64, loc };
return {
kind: "libCall",
@@ -473,29 +473,32 @@ export function lowerDiagnosticsChannelCall(
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr | null {
if (bi.member === "channel" && expr.arguments.length === 1) {
const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!);
if (bi.member === "channel" && expr.arguments.nodes.length === 1) {
const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!);
return { kind: "libCall", fn: "dc.channel", args: [name], type: F64, loc };
}
if ((bi.member === "subscribe" || bi.member === "unsubscribe") && expr.arguments.length === 2) {
const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!);
const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments[1]!);
if (
(bi.member === "subscribe" || bi.member === "unsubscribe") &&
expr.arguments.nodes.length === 2
) {
const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!);
const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments.nodes[1]!);
return bi.member === "subscribe"
? { kind: "libCall", fn: "dc.subscribe", args: [name, cb], type: VOID, loc }
: { kind: "libCall", fn: "dc.unsubscribe", args: [name, cb], type: BOOL, loc };
}
if (bi.member === "hasSubscribers" && expr.arguments.length === 1) {
const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!);
if (bi.member === "hasSubscribers" && expr.arguments.nodes.length === 1) {
const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!);
return { kind: "libCall", fn: "dc.hasSubscribers", args: [name], type: BOOL, loc };
}
// tracingChannel: the string form interns the five tracing:<name>:*
// channels; the collection form takes an object literal whose five
// event members are Channel-typed values (Node's TracingChannelCollection).
if (bi.member === "tracingChannel" && expr.arguments.length === 1) {
const a = expr.arguments[0]!;
if (bi.member === "tracingChannel" && expr.arguments.nodes.length === 1) {
const a = expr.arguments.nodes[0]!;
if (ts.isObjectLiteralExpression(a)) {
const byEvent = new Map<string, IrExpr>();
for (const p of a.properties) {
for (const p of a.properties.nodes) {
if (
!ts.isPropertyAssignment(p) ||
!ts.isIdentifier(p.name) ||
@@ -45,9 +45,9 @@ export function lowerAtomicsCall(
: 3;
const maximum = member === "wait" ? 4 : member === "notify" ? 3 : minimum;
if (
call.arguments.length < minimum ||
call.arguments.length > maximum ||
call.arguments.some(ts.isSpreadElement)
call.arguments.nodes.length < minimum ||
call.arguments.nodes.length > maximum ||
call.arguments.nodes.some(ts.isSpreadElement)
) {
lowerer.noLowering(
`Atomics.${member} arguments`,
@@ -55,7 +55,7 @@ export function lowerAtomicsCall(
`use ${minimum}${maximum > minimum ? `–${maximum}` : ""} arguments`,
);
}
const source = call.arguments[0]!;
const source = call.arguments.nodes[0]!;
// Generic constraints can remain unions in the checker while this body
// has a concrete typed-array binding. Validate the actual lowered storage.
const array = lowerer.lowerExpr(source);
@@ -75,8 +75,8 @@ export function lowerAtomicsCall(
}
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const arg = (index: number, fallback: number): IrExpr =>
call.arguments[index]
? lowerOptionalArgument(lowerer, call.arguments[index]!, F64, number(fallback))
call.arguments.nodes[index]
? lowerOptionalArgument(lowerer, call.arguments.nodes[index]!, F64, number(fallback))
: number(fallback);
const index = arg(1, 0);
if (member === "wait")
@@ -70,7 +70,7 @@ function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpr
export function lowerBlobNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
const cls = blobClassOf(lowerer, expr.expression);
if (cls === null) return null;
const args = expr.arguments ?? [];
const args = expr.arguments?.nodes ?? [];
if (args.some(ts.isSpreadElement)) return null;
const loc = locOf(expr);
const values = args.map((arg) =>
@@ -94,10 +94,10 @@ export function lowerStaticBlobCall(lowerer: Lowerer, call: ts.CallExpression):
const member = access.name.text;
if (access.questionDotToken || !BLOB_CALLS.has(member)) return null;
if (blobReceiverClass(lowerer, access.expression) === null) return null;
if (call.arguments.some(ts.isSpreadElement)) return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const loc = locOf(call);
const receiver = lowerer.lowerExprExpecting(access.expression, DYN);
const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
if (member === "slice") {
return {
kind: "libCall",
@@ -126,7 +126,7 @@ export function registerPromisifiedBuiltinDecl(
if (registerHttpClientFnBinding(lowerer, nameNode, init)) return true;
const e = isPromisifyCall(lowerer, init);
if (!e) return false;
const argNode = e.arguments.length === 1 ? e.arguments[0]! : null;
const argNode = e.arguments.nodes.length === 1 ? e.arguments.nodes[0]! : null;
// `promisify(exec)` over a named import, or `promisify(cp.exec)` over a
// namespace import.
const target = !argNode
@@ -42,9 +42,9 @@ export function lowerExecFileAsyncCall(
): IrExpr {
const what = shell ? "the promisified exec" : "the promisified execFile";
if (
expr.arguments.length < 1 ||
expr.arguments.length > (shell ? 2 : 3) ||
expr.arguments.some(ts.isSpreadElement)
expr.arguments.nodes.length < 1 ||
expr.arguments.nodes.length > (shell ? 2 : 3) ||
expr.arguments.nodes.some(ts.isSpreadElement)
) {
lowerer.noLowering(
`${what} with this argument shape`,
@@ -54,7 +54,7 @@ export function lowerExecFileAsyncCall(
: "the supported form is execFileAsync(file, args?, options?)",
);
}
const command = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const command = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
// exec: /bin/sh -c <command>, execSync's lowering (the command string
// rides as the argv[1] the failure display reads); execFile: file + its
// args list.
@@ -66,8 +66,8 @@ export function lowerExecFileAsyncCall(
type: arrayOf(STRING),
loc,
}
: lowerChildArgsArg(lowerer, expr.arguments[1], loc);
const optsNode = expr.arguments[shell ? 1 : 2];
: lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc);
const optsNode = expr.arguments.nodes[shell ? 1 : 2];
const emptyStr: IrExpr = { kind: "strLit", value: "", type: STRING, loc };
const helper = execFileAsyncHelper(lowerer, loc, shell);
@@ -93,7 +93,7 @@ export function lowerExecFileAsyncCall(
"pass the options inline so each member can be checked",
);
}
for (const p of optsNode.properties) {
for (const p of optsNode.properties.nodes) {
// The conditional-spread idiom carrying `env` — the portless
// opensslAsync shape: `...(c ? { env: { ...process.env, ...extra
// } } : {})` (either orientation). The condition evaluates ONCE
@@ -35,22 +35,22 @@ export function lowerChildMethodCall(
const loc = locOf(call);
if (name === "send") {
if (
call.arguments.length < 1 ||
call.arguments.length > 2 ||
call.arguments.some(ts.isSpreadElement)
call.arguments.nodes.length < 1 ||
call.arguments.nodes.length > 2 ||
call.arguments.nodes.some(ts.isSpreadElement)
) {
lowerer.noLowering(
`child.send with ${call.arguments.length} arguments`,
`child.send with ${call.arguments.nodes.length} arguments`,
call,
"send(message[, callback]) is supported; handle transfer and options are not",
);
}
const receiver = lowerer.lowerExpr(access.expression);
const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc);
if (call.arguments[1] === undefined) {
const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc);
if (call.arguments.nodes[1] === undefined) {
return { kind: "libCall", fn: "child.send", args: [receiver, json], type: BOOL, loc };
}
const callback = ipcSendCallback(lowerer, call.arguments[1]);
const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]);
return {
kind: "libCall",
fn: "child.sendCb",
@@ -59,7 +59,7 @@ export function lowerChildMethodCall(
loc,
};
}
if (name === "disconnect" && call.arguments.length === 0) {
if (name === "disconnect" && call.arguments.nodes.length === 0) {
if (!ts.isExpressionStatement(call.parent)) {
lowerer.unsupported(
"SC1090",
@@ -75,8 +75,8 @@ export function lowerChildMethodCall(
loc,
};
}
if ((name === "on" || name === "once") && call.arguments.length === 2) {
const evT = lowerer.typeOf(call.arguments[0]!);
if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) {
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
if (
event !== "spawn" &&
@@ -88,7 +88,7 @@ export function lowerChildMethodCall(
) {
lowerer.noLowering(
`child.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments[0]!,
call.arguments.nodes[0]!,
'"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)',
);
}
@@ -105,7 +105,7 @@ export function lowerChildMethodCall(
return {
kind: "libCall",
fn: "child.onMessage",
args: [receiver, ipcMessageListener(lowerer, call.arguments[1]!), once],
args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once],
type: VOID,
loc,
};
@@ -114,24 +114,24 @@ export function lowerChildMethodCall(
return {
kind: "libCall",
fn: "child.onDisconnect",
args: [receiver, ipcDisconnectListener(lowerer, call.arguments[1]!), once],
args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once],
type: VOID,
loc,
};
}
const listenerNode = stripTypeCasts(call.arguments[1]!);
const listenerNode = stripTypeCasts(call.arguments.nodes[1]!);
if (
isJsSourceFile(listenerNode.getSourceFile()) &&
(ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) &&
listenerNode.parameters.some((param) => param.dotDotDotToken) &&
listenerNode.parameters.every((param) => !param.type) &&
listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) &&
listenerNode.parameters.nodes.every((param) => !param.type) &&
!/@(?:param|type)\b/.test(
listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()),
)
) {
for (const param of listenerNode.parameters) lowerer.checkedCallbackParams.add(param);
for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param);
}
const cb = lowerer.lowerExpr(call.arguments[1]!);
const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
if (
(isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
(cb.type.kind === "func" &&
@@ -149,7 +149,7 @@ export function lowerChildMethodCall(
) {
lowerer.noLowering(
"child event listeners with non-representable parameters",
call.arguments[1]!,
call.arguments.nodes[1]!,
);
}
return {
@@ -168,7 +168,7 @@ export function lowerChildMethodCall(
if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") {
lowerer.noLowering(
"spawn event listeners with parameters or a returned value",
call.arguments[1]!,
call.arguments.nodes[1]!,
);
}
return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc };
@@ -176,7 +176,7 @@ export function lowerChildMethodCall(
if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
event !== "error"
? `${event} listeners with more than two parameters (use (code, signal), (code), or ())`
: "error listeners with more than one parameter (use (err) or ())",
@@ -188,7 +188,7 @@ export function lowerChildMethodCall(
// fenced instead of being called through the wrong ABI.
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
"listeners returning a value (make the callback body a block, or return nothing)",
);
}
@@ -206,7 +206,7 @@ export function lowerChildMethodCall(
if (!armsOk) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
`${event} listeners whose parameter is not 'number | null' (got '${lowerer.fmt(param!)}')`,
);
}
@@ -228,7 +228,7 @@ export function lowerChildMethodCall(
if (!sigOk) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
`${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`,
);
}
@@ -243,7 +243,7 @@ export function lowerChildMethodCall(
if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
`error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`,
);
}
@@ -256,11 +256,11 @@ export function lowerChildMethodCall(
// reaped or never spawned (Node's null-handle answer), and a successful
// send sets `killed`.
if (name === "kill") {
if (call.arguments.length > 1) {
lowerer.noLowering(`child.kill with ${call.arguments.length} arguments`, call);
if (call.arguments.nodes.length > 1) {
lowerer.noLowering(`child.kill with ${call.arguments.nodes.length} arguments`, call);
}
const receiver = lowerer.lowerExpr(access.expression);
const sigNode = call.arguments[0];
const sigNode = call.arguments.nodes[0];
if (!sigNode) {
const dflt: IrExpr = { kind: "strLit", value: "SIGTERM", type: STRING, loc };
return { kind: "libCall", fn: "child.kill", args: [receiver, dflt], type: BOOL, loc };
@@ -281,7 +281,7 @@ export function lowerChildMethodCall(
// child.unref(): drops the child from the event loop's keep-alive set
// (the process may exit while the child runs — Node's semantics; the
// child is still reaped while the loop runs for other reasons).
if ((name === "unref" || name === "ref") && call.arguments.length === 0) {
if ((name === "unref" || name === "ref") && call.arguments.nodes.length === 0) {
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "libCall",
@@ -320,9 +320,9 @@ export function lowerChildStreamMethodCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (name === "setEncoding" && call.arguments.length === 1) {
if (name === "setEncoding" && call.arguments.nodes.length === 1) {
const receiver = lowerer.lowerExprExpecting(access.expression, CHILDSTREAM_T);
const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments[0]!);
const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments.nodes[0]!);
return {
kind: "libCall",
fn: "stream.childSetEncoding",
@@ -331,13 +331,13 @@ export function lowerChildStreamMethodCall(
loc,
};
}
if ((name === "on" || name === "once") && call.arguments.length === 2) {
const evT = lowerer.typeOf(call.arguments[0]!);
if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) {
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
if (event !== "data" && event !== "end") {
lowerer.noLowering(
`stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments[0]!,
call.arguments.nodes[0]!,
'"data" and "end" are the supported child-stream events (as literals)',
);
}
@@ -349,7 +349,7 @@ export function lowerChildStreamMethodCall(
);
}
const receiver = lowerer.lowerExpr(access.expression);
const cb = lowerer.lowerExpr(call.arguments[1]!);
const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc };
if (
cb.type.kind !== "func" ||
@@ -358,7 +358,7 @@ export function lowerChildStreamMethodCall(
) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
event === "data"
? "data listeners with more than one parameter or a return value (use (chunk) or ())"
: "end listeners with parameters or a return value (use ())",
@@ -381,7 +381,7 @@ export function lowerChildStreamMethodCall(
) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
`data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
);
}
@@ -417,15 +417,15 @@ export function lowerChildWriterMethodCall(
const loc = locOf(call);
if (name === "write") {
if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) {
if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(
`child stdin write with ${call.arguments.length} arguments`,
`child stdin write with ${call.arguments.nodes.length} arguments`,
call,
"write(string | Uint8Array) with no encoding or callback is supported",
);
}
const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T);
const data = lowerer.lowerExpr(call.arguments[0]!);
const data = lowerer.lowerExpr(call.arguments.nodes[0]!);
if (data.type.kind === "string") {
return { kind: "libCall", fn: "writer.writeString", args: [receiver, data], type: BOOL, loc };
}
@@ -434,12 +434,12 @@ export function lowerChildWriterMethodCall(
}
lowerer.noLowering(
`child stdin write of '${lowerer.fmt(data.type)}'`,
call.arguments[0]!,
call.arguments.nodes[0]!,
"write one string, Buffer, or Uint8Array value (narrow unions first)",
);
}
if ((name === "end" || name === "destroy") && call.arguments.length === 0) {
if ((name === "end" || name === "destroy") && call.arguments.nodes.length === 0) {
if (!ts.isExpressionStatement(call.parent)) {
lowerer.unsupported(
"SC1090",
@@ -457,13 +457,13 @@ export function lowerChildWriterMethodCall(
};
}
if ((name === "on" || name === "once") && call.arguments.length === 2) {
const eventType = lowerer.typeOf(call.arguments[0]!);
if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) {
const eventType = lowerer.typeOf(call.arguments.nodes[0]!);
const event = eventType.isStringLiteralType() ? eventType.value : null;
if (event !== "drain" && event !== "finish" && event !== "error") {
lowerer.noLowering(
`child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments[0]!,
call.arguments.nodes[0]!,
'"drain", "finish", and "error" are the supported child stdin events',
);
}
@@ -471,7 +471,7 @@ export function lowerChildWriterMethodCall(
lowerer.unsupported("SC1090", call, "chaining child stdin listener registration");
}
const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T);
const cb = lowerer.lowerExpr(call.arguments[1]!);
const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
const maxParams = event === "error" ? 1 : 0;
if (
cb.type.kind !== "func" ||
@@ -480,7 +480,7 @@ export function lowerChildWriterMethodCall(
) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
event === "error"
? "error listeners with at most one Error parameter and no return value"
: `${event} listeners with no parameters or return value`,
@@ -493,7 +493,7 @@ export function lowerChildWriterMethodCall(
) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
call.arguments.nodes[1]!,
"child stdin error listeners whose parameter is Error",
);
}
@@ -48,15 +48,15 @@ export function lowerExecSyncCall(
shell: boolean,
loc: SrcLoc,
): IrExpr {
if (expr.arguments.some(ts.isSpreadElement)) {
if (expr.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`${shell ? "execSync" : "execFileSync"} with a spread call`, expr);
}
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
// execSync: /bin/sh -c <command>; execFileSync: file + its args list.
let argvExpr: IrExpr;
let optsNode: ts.Expression | undefined;
if (shell) {
if (expr.arguments.length > 2) {
if (expr.arguments.nodes.length > 2) {
lowerer.noLowering(
"execSync with this argument shape",
expr,
@@ -69,17 +69,17 @@ export function lowerExecSyncCall(
type: arrayOf(STRING),
loc,
};
optsNode = expr.arguments[1];
optsNode = expr.arguments.nodes[1];
} else {
if (expr.arguments.length > 3) {
if (expr.arguments.nodes.length > 3) {
lowerer.noLowering(
"execFileSync with this argument shape",
expr,
"the supported form is execFileSync(file, args?, options?)",
);
}
argvExpr = lowerChildArgsArg(lowerer, expr.arguments[1], loc);
optsNode = expr.arguments[2];
argvExpr = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc);
optsNode = expr.arguments.nodes[2];
}
// The shell command itself is /bin/sh; the "command" string rides as
// the argv[1] the display formatter reads.
@@ -102,6 +102,8 @@ export function lowerExecSyncCall(
let stdoutMode = 1;
let stderrMode = 0;
let stdinInherit = false;
// Without an encoding option, a failure's error carries Buffer outputs.
let encodingGiven = false;
// A conditional env spread (`...(c ? { env: ... } : {})`): the call
// itself splits into a ternary of the two env-nesses at the tail.
let condEnvSpread: { cond: IrExpr; pairs: IrExpr; whenTrue: boolean } | null = null;
@@ -134,6 +136,8 @@ export function lowerExecSyncCall(
runtime.timeout,
runtime.stdoutMode,
runtime.stderrMode,
// The runtime options carry a utf8 encoding (others throw).
boolLit(false, loc),
],
type: STRING,
loc,
@@ -182,8 +186,8 @@ export function lowerExecSyncCall(
modeOf(t.value, 2);
return;
}
if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) {
node.elements.forEach((el, i) => {
if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) {
node.elements.nodes.forEach((el, i) => {
const et = lowerer.typeOf(el);
if (!et.isStringLiteralType()) {
lowerer.noLowering(
@@ -202,7 +206,7 @@ export function lowerExecSyncCall(
);
};
for (const p of optsNode.properties) {
for (const p of optsNode.properties.nodes) {
// The conditional-spread idiom carrying `env` — the openssl-runner
// shape: `...(c ? { env: { ...process.env, ...extra } } : {})`
// (either orientation). The condition evaluates ONCE and picks
@@ -251,6 +255,7 @@ export function lowerExecSyncCall(
'outputs are captured as utf8 — pass { encoding: "utf8" }',
);
}
encodingGiven = true;
break;
}
case "cwd":
@@ -395,6 +400,7 @@ export function lowerExecSyncCall(
timeout,
numLit(stdoutMode + (stdinInherit ? 4 : 0), loc),
numLit(stderrMode, loc),
boolLit(!encodingGiven, loc),
],
type: STRING,
loc,
@@ -31,8 +31,8 @@ export function lowerChildArgsArg(
loc: SrcLoc,
): IrExpr {
if (!node) return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc };
if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) {
const elems = node.elements.map((el) => lowerer.lowerExprExpecting(el, STRING));
if (ts.isArrayLiteralExpression(node) && !node.elements.nodes.some(ts.isSpreadElement)) {
const elems = node.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, STRING));
return { kind: "arrayLit", elems, type: arrayOf(STRING), loc: locOf(node) };
}
return lowerer.lowerExprExpecting(node, arrayOf(STRING));
@@ -92,16 +92,16 @@ const NODE_SIGNAL_NAMES = new Set([
* fences by name. The bare `{ encoding: "utf8" }` shape keeps its
* historical cp.spawnSync lowering. */
export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) {
if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(
"spawnSync with this argument shape",
expr,
"the supported form is spawnSync(command, args?, options?)",
);
}
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const argv = lowerChildArgsArg(lowerer, expr.arguments[1], loc);
const optsNode = expr.arguments[2];
const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const argv = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc);
const optsNode = expr.arguments.nodes[2];
let timeout: IrExpr = numLit(0, loc);
let killSignal: IrExpr = { kind: "strLit", value: "", type: STRING, loc };
@@ -152,8 +152,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
modeOf(t.value, 2);
return;
}
if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) {
node.elements.forEach((el, i) => {
if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) {
node.elements.nodes.forEach((el, i) => {
const et = lowerer.typeOf(el);
if (!et.isStringLiteralType()) {
lowerer.noLowering("spawnSync stdio tuple entries beyond string literals", el);
@@ -182,7 +182,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
'stdio takes a "pipe"/"ignore"/"inherit" literal (or a value typed as a union of those), or a 3-tuple of literals',
);
};
for (const p of optsNode.properties) {
for (const p of optsNode.properties.nodes) {
const m = optionMember(p);
if (!m) {
lowerer.noLowering(
@@ -280,21 +280,21 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
* `windowsHide` (a POSIX no-op). The bare `{ stdio: "ignore" }` shape
* keeps its historical cp.spawn lowering. */
export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) {
if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(
"spawn with this argument shape",
expr,
'the supported form is spawn(command, args?, { stdio: "pipe" | "ignore" | "inherit", detached?, env?, cwd? })',
);
}
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
let argsNode: ts.Expression | undefined;
let optsNode: ts.Expression | undefined;
if (expr.arguments.length === 3) {
argsNode = expr.arguments[1];
optsNode = expr.arguments[2];
} else if (expr.arguments.length === 2) {
const second = expr.arguments[1]!;
if (expr.arguments.nodes.length === 3) {
argsNode = expr.arguments.nodes[1];
optsNode = expr.arguments.nodes[2];
} else if (expr.arguments.nodes.length === 2) {
const second = expr.arguments.nodes[1]!;
const secondType = lowerer.typeOf(second);
const mapped = lowerer.mapTypeOf(secondType);
if (
@@ -338,7 +338,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S
let plain = true; // exactly { stdio: "ignore" }: the historical libCall
if (optsNode && ts.isObjectLiteralExpression(optsNode)) {
for (const p of optsNode.properties) {
for (const p of optsNode.properties.nodes) {
// The conditional-spread idiom `...(isWindows ? {} : { detached:
// true })` (either orientation): the one carried member supported
// is `detached` with a boolean literal — the platform-conditional
@@ -393,7 +393,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S
// The 3-tuple form: every slot accepts a literal, including
// "pipe" (child.stdin/stdout/stderr); stdout/stderr also accept
// a number-typed fd (an openSync result — dup2'd into the child).
if (ts.isArrayLiteralExpression(m.value) && m.value.elements.length === 3) {
if (ts.isArrayLiteralExpression(m.value) && m.value.elements.nodes.length === 3) {
const slot = (el: ts.Expression, which: 0 | 1 | 2): void => {
const t = lowerer.typeOf(el);
if (t.isStringLiteralType()) {
@@ -434,7 +434,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S
errFd = fd;
}
};
m.value.elements.forEach((el, i) => slot(el, i as 0 | 1 | 2));
m.value.elements.nodes.forEach((el, i) => slot(el, i as 0 | 1 | 2));
sawStdio = true;
plain = false;
break;
@@ -512,7 +512,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S
}
if (shellEnabled && argsNode !== undefined) {
const shellArgs = stripTypeCasts(argsNode);
if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.length !== 0) {
if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.nodes.length !== 0) {
lowerer.noLowering(
"spawn with shell enabled and separate arguments",
argsNode,
@@ -551,9 +551,9 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S
* Node's default JSON IPC channel is always present. */
export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
if (
expr.arguments.length < 1 ||
expr.arguments.length > 3 ||
expr.arguments.some(ts.isSpreadElement)
expr.arguments.nodes.length < 1 ||
expr.arguments.nodes.length > 3 ||
expr.arguments.nodes.some(ts.isSpreadElement)
) {
lowerer.noLowering(
"fork with this argument shape",
@@ -561,24 +561,24 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
"use fork(staticModulePath[, stringArgs][, inlineOptions])",
);
}
const targetPath = staticForkModulePath(lowerer.program, expr.arguments[0]!);
const targetPath = staticForkModulePath(lowerer.program, expr.arguments.nodes[0]!);
const targetId =
targetPath === null ? undefined : lowerer.forkTargetIdByPath.get(tsgoPath(resolve(targetPath)));
if (targetId === undefined) {
lowerer.noLowering(
"fork with a runtime-valued module path",
expr.arguments[0]!,
expr.arguments.nodes[0]!,
'resolve a relative worker with new URL("./worker.ts", import.meta.url) or fileURLToPath(...) and bind it to a const if needed',
);
}
let argsNode: ts.Expression | undefined;
let optsNode: ts.Expression | undefined;
if (expr.arguments.length === 3) {
argsNode = expr.arguments[1];
optsNode = expr.arguments[2];
} else if (expr.arguments.length === 2) {
const second = expr.arguments[1]!;
if (expr.arguments.nodes.length === 3) {
argsNode = expr.arguments.nodes[1];
optsNode = expr.arguments.nodes[2];
} else if (expr.arguments.nodes.length === 2) {
const second = expr.arguments.nodes[1]!;
const secondType = lowerer.typeOf(second);
const mapped = lowerer.mapTypeOf(secondType);
if (
@@ -622,7 +622,7 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
};
if (optsNode && ts.isObjectLiteralExpression(optsNode)) {
for (const property of optsNode.properties) {
for (const property of optsNode.properties.nodes) {
const member = optionMember(property);
if (!member) {
lowerer.noLowering(
@@ -651,14 +651,14 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
break;
case "stdio": {
if (ts.isArrayLiteralExpression(member.value)) {
if (member.value.elements.length !== 4) {
if (member.value.elements.nodes.length !== 4) {
lowerer.noLowering(
"fork with this stdio tuple",
member.value,
'use [stdin, stdout, stderr, "ipc"] with exactly four literal entries',
);
}
const ipc = member.value.elements[3]!;
const ipc = member.value.elements.nodes[3]!;
const ipcType = lowerer.typeOf(ipc);
if (!ipcType.isStringLiteralType() || ipcType.value !== "ipc") {
lowerer.noLowering(
@@ -667,9 +667,9 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
'the fourth stdio entry must be "ipc"',
);
}
inMode = stdioMode(member.value.elements[0]!, "stdin");
outMode = stdioMode(member.value.elements[1]!, "stdout");
errMode = stdioMode(member.value.elements[2]!, "stderr");
inMode = stdioMode(member.value.elements.nodes[0]!, "stdin");
outMode = stdioMode(member.value.elements.nodes[1]!, "stdout");
errMode = stdioMode(member.value.elements.nodes[2]!, "stderr");
} else {
const mode = stdioMode(member.value, "scalar");
inMode = outMode = errMode = mode;
@@ -743,20 +743,20 @@ export function lowerExecFileCall(
): IrExpr {
const what = shell ? "exec" : "execFile";
if (
expr.arguments.some(ts.isSpreadElement) ||
expr.arguments.length < 2 ||
expr.arguments.length > (shell ? 3 : 4)
expr.arguments.nodes.some(ts.isSpreadElement) ||
expr.arguments.nodes.length < 2 ||
expr.arguments.nodes.length > (shell ? 3 : 4)
) {
lowerer.noLowering(
`${what} with ${expr.arguments.length} arguments`,
`${what} with ${expr.arguments.nodes.length} arguments`,
expr,
shell
? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)"
: "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
);
}
const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
const first = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const callbackNode = expr.arguments.nodes[expr.arguments.nodes.length - 1]!;
// exec: /bin/sh -c <command>, execSync's lowering (the runtime reads the
// command line back from argv[1] for failure messages); execFile: the
// file and its args list.
@@ -768,9 +768,15 @@ export function lowerExecFileCall(
type: arrayOf(STRING),
loc,
}
: lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc);
: lowerChildArgsArg(
lowerer,
expr.arguments.nodes.length >= 3 ? expr.arguments.nodes[1] : undefined,
loc,
);
const optionsNode =
expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined;
expr.arguments.nodes.length === (shell ? 3 : 4)
? expr.arguments.nodes[shell ? 1 : 2]
: undefined;
if (optionsNode) {
const options = optionsNode;
if (!ts.isObjectLiteralExpression(options)) {
@@ -792,7 +798,7 @@ export function lowerExecFileCall(
].includes(node.operatorToken.kind) &&
pureNumber(node.left) &&
pureNumber(node.right));
for (const property of options.properties) {
for (const property of options.properties.nodes) {
const member = optionMember(property);
if (!member)
lowerer.noLowering(
@@ -7,6 +7,7 @@ import { stripTypeCasts } from "./arguments.js";
import { streamClassAliasDecl } from "../lower-stream.js";
import { zlibClassName, zlibStreamModeOf } from "./compression.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import { RUNTIME_ERROR_CLASSES } from "../../../ir/ir.js";
/** A builtin class an expression names at compile time: the error
* classes, node:events EventEmitter, the node:stream classes (each a
@@ -123,6 +124,60 @@ export function registerBuiltinClassAlias(
return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol);
}
/** The runtime-provided class (EventEmitter, a stream class, an error
* class) `expr` names, when values of it construct (valueCtorParams), or
* null. */
export function runtimeClassValueOf(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null {
const ref = builtinClassRefOf(lowerer, expr);
if (ref === null || ref.kind === "zlib" || ref.kind === "collection") return null;
return ref.info.valueCtorParams !== undefined ? ref.info : null;
}
/** The runtime-provided class whose static side `type` is (`typeof
* EventEmitter`, `typeof Readable`, `ErrorConstructor`), when values of
* it construct, or null. Instance types have no construct signatures and
* answer null. A union of such static sides (`[Readable, Writable]`'s
* element type) is their nearest common ancestor's value when every arm
* constructs through the same parameters: a slot of the ancestor holds
* any of them. */
export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string | null {
if (type.isUnionType()) {
const arms: ClassInfo[] = [];
for (const part of ts.constituentTypes(type)) {
const name = runtimeClassValueOfType(lowerer, part);
const info = name !== null ? lowerer.classes.get(name) : undefined;
if (!info) return null;
arms.push(info);
}
const ancestor = commonRuntimeAncestor(arms);
if (ancestor === null) return null;
return arms.every((arm) => lowerer.ctorAbiEquals(arm, ancestor)) ? ancestor.def.name : null;
}
if (lowerer.checker.getConstructSignatures(type).length === 0) return null;
const symbol = type.getSymbol();
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol);
if (!info && symbol.name.endsWith("Constructor")) {
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null;
}
}
return info?.valueCtorParams !== undefined ? info.def.name : null;
}
/** The nearest class every one of `infos` is or extends, when it is a
* runtime class whose values construct, or null. */
function commonRuntimeAncestor(infos: ClassInfo[]): ClassInfo | null {
for (let candidate = infos[0] ?? null; candidate; candidate = candidate.base) {
const holdsAll = infos.every((info) => {
for (let c: ClassInfo | null = info; c; c = c.base) if (c === candidate) return true;
return false;
});
if (holdsAll) return candidate.def.runtime && candidate.valueCtorParams ? candidate : null;
}
return null;
}
/** True when two references name the same builtin class. */
export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean {
if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode;
@@ -197,7 +197,7 @@ export function lowerZlibStreamMethodCall(
): IrExpr {
const loc = locOf(call);
const member = access.name.text;
const args = call.arguments;
const args = call.arguments.nodes;
if (args.some(ts.isSpreadElement)) lowerer.noLowering(`${member} with spread arguments`, call);
const isFunc = (node: ts.Expression): boolean =>
lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "func";
@@ -304,25 +304,25 @@ export function lowerPromisifiedZlibCall(
member: string,
loc: SrcLoc,
): IrExpr {
if (expr.arguments.some(ts.isSpreadElement)) {
if (expr.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`the promisified zlib.${member} with spread arguments`, expr);
}
if (expr.arguments.length !== 1 && expr.arguments.length !== 2) {
if (expr.arguments.nodes.length !== 1 && expr.arguments.nodes.length !== 2) {
lowerer.noLowering(
`the promisified zlib.${member} with ${expr.arguments.length} arguments`,
`the promisified zlib.${member} with ${expr.arguments.nodes.length} arguments`,
expr,
`the supported forms are ${member}Async(data) and ${member}Async(data, options)`,
);
}
const input = zlibInput(lowerer, expr.arguments[0]!, loc);
const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc);
return {
kind: "libCall",
fn: input.dyn ? "zlib.promiseDyn" : "zlib.promise",
args: [
numLit(ZLIB_ONE_SHOT_MODES[member]!, loc),
input.value,
expr.arguments[1] !== undefined
? zlibOptionsValue(lowerer, expr.arguments[1], `zlib.${member}`, loc)
expr.arguments.nodes[1] !== undefined
? zlibOptionsValue(lowerer, expr.arguments.nodes[1], `zlib.${member}`, loc)
: dynUndefinedExpr(loc),
],
type: { kind: "promise", inner: BYTES_U8 },
@@ -346,19 +346,26 @@ export function lowerZlibModuleCall(
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr {
if (expr.arguments.some(ts.isSpreadElement)) {
if (expr.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`zlib.${bi.member} with spread arguments`, expr);
}
const streamMode = ZLIB_STREAM_FACTORIES[bi.member];
if (streamMode !== undefined) {
return lowerZlibStreamNew(lowerer, expr.arguments, streamMode, `zlib.${bi.member}`, expr, loc);
return lowerZlibStreamNew(
lowerer,
expr.arguments.nodes,
streamMode,
`zlib.${bi.member}`,
expr,
loc,
);
}
const syncFn = ZLIB_SYNC_FNS[bi.member];
if (syncFn !== undefined) {
if (expr.arguments.length === 0 || expr.arguments.length > 2) {
lowerer.noLowering(`${bi.member} with ${expr.arguments.length} arguments`, expr);
if (expr.arguments.nodes.length === 0 || expr.arguments.nodes.length > 2) {
lowerer.noLowering(`${bi.member} with ${expr.arguments.nodes.length} arguments`, expr);
}
const input = zlibInput(lowerer, expr.arguments[0]!, loc);
const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc);
if (input.dyn) {
return {
kind: "libCall",
@@ -366,8 +373,8 @@ export function lowerZlibModuleCall(
args: [
numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc),
input.value,
expr.arguments[1] !== undefined
? zlibOptionsValue(lowerer, expr.arguments[1], `zlib.${bi.member}`, loc)
expr.arguments.nodes[1] !== undefined
? zlibOptionsValue(lowerer, expr.arguments.nodes[1], `zlib.${bi.member}`, loc)
: dynUndefinedExpr(loc),
],
type: BYTES_U8,
@@ -375,15 +382,15 @@ export function lowerZlibModuleCall(
};
}
if (
expr.arguments.length === 2 &&
expr.arguments.nodes.length === 2 &&
(bi.member === "deflateSync" || bi.member === "deflateRawSync" || bi.member === "gzipSync")
) {
const options = expr.arguments[1]!;
const options = expr.arguments.nodes[1]!;
// A lone literal level keeps the direct compression entry (build-time
// compression in the native emitters uses it); other options take the
// runtime option ladder below.
if (ts.isObjectLiteralExpression(options) && options.properties.length === 1) {
const prop = options.properties[0]!;
if (ts.isObjectLiteralExpression(options) && options.properties.nodes.length === 1) {
const prop = options.properties.nodes[0]!;
if (
ts.isPropertyAssignment(prop) &&
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
@@ -416,14 +423,14 @@ export function lowerZlibModuleCall(
}
}
}
if (expr.arguments.length === 2) {
if (expr.arguments.nodes.length === 2) {
return {
kind: "libCall",
fn: "zlib.syncOpts",
args: [
numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc),
input.value,
zlibOptionsValue(lowerer, expr.arguments[1]!, `zlib.${bi.member}`, loc),
zlibOptionsValue(lowerer, expr.arguments.nodes[1]!, `zlib.${bi.member}`, loc),
],
type: BYTES_U8,
loc,
@@ -439,19 +446,19 @@ export function lowerZlibModuleCall(
}
const callbackFn = ZLIB_CALLBACK_FNS[bi.member];
if (callbackFn !== undefined) {
if (expr.arguments.length !== 2 && expr.arguments.length !== 3) {
if (expr.arguments.nodes.length !== 2 && expr.arguments.nodes.length !== 3) {
lowerer.noLowering(
`${bi.member} with ${expr.arguments.length} arguments`,
`${bi.member} with ${expr.arguments.nodes.length} arguments`,
expr,
`${bi.member}(data, callback) and ${bi.member}(data, options, callback) are supported`,
);
}
const input = zlibInput(lowerer, expr.arguments[0]!, loc);
const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc);
const dynData = input.dyn;
if (expr.arguments.length === 3 || dynData) {
if (expr.arguments.nodes.length === 3 || dynData) {
// (data, options, callback): Node validates the options before the
// callback and the input, so the runtime ladder runs first.
const withOptions = expr.arguments.length === 3;
const withOptions = expr.arguments.nodes.length === 3;
return {
kind: "libCall",
fn: dynData ? "zlib.cbOptsDyn" : "zlib.cbOpts",
@@ -459,9 +466,9 @@ export function lowerZlibModuleCall(
numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc),
input.value,
withOptions
? zlibOptionsValue(lowerer, expr.arguments[1]!, `zlib.${bi.member}`, loc)
? zlibOptionsValue(lowerer, expr.arguments.nodes[1]!, `zlib.${bi.member}`, loc)
: dynUndefinedExpr(loc),
errorFirstBytesCallback(lowerer, expr.arguments[withOptions ? 2 : 1]!, "zlib"),
errorFirstBytesCallback(lowerer, expr.arguments.nodes[withOptions ? 2 : 1]!, "zlib"),
],
type: VOID,
loc,
@@ -470,15 +477,15 @@ export function lowerZlibModuleCall(
return {
kind: "libCall",
fn: callbackFn,
args: [input.value, errorFirstBytesCallback(lowerer, expr.arguments[1]!, "zlib")],
args: [input.value, errorFirstBytesCallback(lowerer, expr.arguments.nodes[1]!, "zlib")],
type: VOID,
loc,
};
}
if (bi.member === "crc32") {
if (expr.arguments.length < 1 || expr.arguments.length > 2) {
if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) {
lowerer.noLowering(
`zlib.crc32 with ${expr.arguments.length} arguments`,
`zlib.crc32 with ${expr.arguments.nodes.length} arguments`,
expr,
"crc32(data[, initialValue]) is supported",
);
@@ -487,9 +494,9 @@ export function lowerZlibModuleCall(
kind: "libCall",
fn: "zlib.crc32",
args: [
lowerBuiltinByteInput(lowerer, expr.arguments[0]!, loc, "zlib"),
expr.arguments[1]
? lowerer.lowerExprExpecting(expr.arguments[1]!, F64)
lowerBuiltinByteInput(lowerer, expr.arguments.nodes[0]!, loc, "zlib"),
expr.arguments.nodes[1]
? lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, F64)
: { kind: "numLit", value: 0, type: F64, loc },
],
type: F64,
@@ -88,7 +88,7 @@ export function builtinConstantsDestructureDecl(
const module = builtinConstantsModuleOf(lowerer, init);
if (module === null) return false;
const table = BUILTIN_CONSTANTS_TABLES[module]!.table;
return nameNode.elements.every(
return nameNode.elements.nodes.every(
(el) =>
!el.dotDotDotToken &&
el.initializer === undefined &&
@@ -47,17 +47,17 @@ export function lowerCryptoComposedCall(
// "hex"/"base64" encoding. Anything else falls through (null): bare
// randomBytes lowers to a real Buffer through the crypto table, and
// the .toString rides the ordinary Buffer method lowering.
if (recv.arguments.length !== 1) return null;
const encNode = call.arguments[0];
if (recv.arguments.nodes.length !== 1) return null;
const encNode = call.arguments.nodes[0];
const encT = encNode ? lowerer.typeOf(encNode) : undefined;
if (
call.arguments.length !== 1 ||
call.arguments.nodes.length !== 1 ||
!encT?.isStringLiteralType() ||
(encT.value !== "hex" && encT.value !== "base64")
) {
return null;
}
const size = lowerer.lowerExprExpecting(recv.arguments[0]!, F64);
const size = lowerer.lowerExprExpecting(recv.arguments.nodes[0]!, F64);
const enc = lowerer.lowerExprExpecting(encNode!, STRING);
return {
kind: "libCall",
@@ -96,28 +96,30 @@ function lowerHashDigestChain(
: null;
if (!bi || bi.module !== "crypto" || bi.member !== "createHash") return null;
const loc = locOf(call);
const algT = chCall.arguments.length === 1 ? lowerer.typeOf(chCall.arguments[0]!) : undefined;
const algT =
chCall.arguments.nodes.length === 1 ? lowerer.typeOf(chCall.arguments.nodes[0]!) : undefined;
if (!algT?.isStringLiteralType() || (algT.value !== "sha256" && algT.value !== "sha1"))
return null;
if (updateCall.arguments.length !== 1) {
if (updateCall.arguments.nodes.length !== 1) {
return null;
}
const encT = call.arguments.length === 1 ? lowerer.typeOf(call.arguments[0]!) : undefined;
const encT =
call.arguments.nodes.length === 1 ? lowerer.typeOf(call.arguments.nodes[0]!) : undefined;
if (!encT?.isStringLiteralType() || (encT.value !== "hex" && encT.value !== "base64")) {
return null;
}
// alg and enc are proven literals (fenced above), so lowering them
// out of source position observes nothing; the data lowers between
// them in its own source order.
const alg = lowerer.lowerExprExpecting(chCall.arguments[0]!, STRING);
const alg = lowerer.lowerExprExpecting(chCall.arguments.nodes[0]!, STRING);
// The data picks the runtime entry by its static type, the
// fileURLToPath convention: strings hash their UTF-8 bytes (Node's
// default input encoding), Buffers/typed arrays hash their bytes.
const dataNode = updateCall.arguments[0]!;
const dataNode = updateCall.arguments.nodes[0]!;
const dataIr = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
if (dataIr?.kind === "bytes") {
const data = cryptoByteView(lowerer, dataNode, lowerer.lowerExpr(dataNode));
const enc = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
const enc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING);
return {
kind: "libCall",
fn: "crypto.hashDigestBytes",
@@ -128,7 +130,7 @@ function lowerHashDigestChain(
}
if (dataIr?.kind === "string") {
const data = lowerer.lowerExprExpecting(dataNode, STRING);
const enc = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
const enc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING);
return {
kind: "libCall",
fn: "crypto.hashDigestStr",
@@ -257,7 +259,7 @@ export function lowerCryptoModuleCall(
loc: SrcLoc,
): IrExpr | null {
if (bi.module !== "crypto") return null;
const args = expr.arguments;
const args = expr.arguments.nodes;
if (args.some(ts.isSpreadElement)) {
lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr);
}
@@ -465,8 +467,8 @@ export function lowerCryptoModuleCall(
let options = args[2];
// An options literal holding only a literal string-digest
// outputEncoding is that encoding; evaluating it observes nothing.
if (options && ts.isObjectLiteralExpression(options) && options.properties.length === 1) {
const property = options.properties[0]!;
if (options && ts.isObjectLiteralExpression(options) && options.properties.nodes.length === 1) {
const property = options.properties.nodes[0]!;
if (
ts.isPropertyAssignment(property) &&
ts.isIdentifier(property.name) &&
@@ -820,8 +822,8 @@ export function lowerCryptoModuleCall(
};
const list = own(LISTS, bi.member);
if (bi.member !== "getFips" && list === undefined) return null;
if (expr.arguments.length !== 0) {
lowerer.noLowering(`crypto.${bi.member} with ${expr.arguments.length} arguments`, expr);
if (expr.arguments.nodes.length !== 0) {
lowerer.noLowering(`crypto.${bi.member} with ${expr.arguments.nodes.length} arguments`, expr);
}
if (bi.member === "getFips") {
return { kind: "numLit", value: 0, type: F64, loc };
@@ -916,7 +918,7 @@ export function lowerCipherMethodCall(
"update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not",
);
}
if (call.arguments.some(ts.isSpreadElement))
if (call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering(`Cipheriv/Decipheriv.${name} with spread arguments`, call);
const loc = locOf(call);
return {
@@ -924,7 +926,7 @@ export function lowerCipherMethodCall(
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: name,
calleeName: v8CalleeText(access),
args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
@@ -946,9 +948,9 @@ export function lowerCryptoHashMethodCall(
// Hmac has no copy(): Node reads the member as undefined and throws a
// TypeError once the arguments are evaluated.
if (receiverType.kind === "cryptoHmac" && name === "copy") {
if (call.arguments.some(ts.isSpreadElement))
if (call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering("Hmac.copy with spread arguments", call);
const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({
const evaluated = [access.expression, ...call.arguments.nodes].map((node): IrStmt => ({
kind: "exprStmt",
expr: lowerer.lowerExpr(node),
loc,
@@ -965,19 +967,19 @@ export function lowerCryptoHashMethodCall(
const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType);
const hash = receiverType.kind === "cryptoHash";
const label = hash ? "Hash" : "Hmac";
if (call.arguments.some(ts.isSpreadElement))
if (call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering(`${label}.${name} with spread arguments`, call);
if (name === "update") {
if (call.arguments.length < 1 || call.arguments.length > 2) {
if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) {
lowerer.noLowering(
`${label}.update with ${call.arguments.length} arguments`,
`${label}.update with ${call.arguments.nodes.length} arguments`,
call,
"update(data[, inputEncoding]) is supported",
);
}
const target = receiver();
const dataNode = call.arguments[0]!;
const encodingNode = call.arguments[1];
const dataNode = call.arguments.nodes[0]!;
const encodingNode = call.arguments.nodes[1];
const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
// Indexed reads can retain optional storage after a nullish fallback
// or narrowing. Use the proven argument type through a checked coercion.
@@ -1032,14 +1034,14 @@ export function lowerCryptoHashMethodCall(
};
}
if (name === "digest") {
if (call.arguments.length > 1) {
if (call.arguments.nodes.length > 1) {
lowerer.noLowering(
`${label}.digest with ${call.arguments.length} arguments`,
`${label}.digest with ${call.arguments.nodes.length} arguments`,
call,
"digest([outputEncoding]) is supported",
);
}
const encodingNode = call.arguments[0];
const encodingNode = call.arguments.nodes[0];
if (encodingNode === undefined) {
return {
kind: "libCall",
@@ -1078,13 +1080,13 @@ export function lowerCryptoHashMethodCall(
};
}
if (name === "copy" && hash) {
if (call.arguments.length > 1)
if (call.arguments.nodes.length > 1)
lowerer.noLowering(
`Hash.copy with ${call.arguments.length} arguments`,
`Hash.copy with ${call.arguments.nodes.length} arguments`,
call,
"copy([options]) is supported",
);
if (call.arguments.length === 0)
if (call.arguments.nodes.length === 0)
return {
kind: "libCall",
fn: "crypto.hashCopy",
@@ -1095,7 +1097,7 @@ export function lowerCryptoHashMethodCall(
return {
kind: "libCall",
fn: "crypto.hashCopyOpts",
args: [receiver(), lowerer.lowerExprExpecting(call.arguments[0]!, DYN)],
args: [receiver(), lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)],
type: CRYPTOHASH_T,
loc,
};
@@ -1138,7 +1140,7 @@ export function lowerPromisifiedCryptoCall(
member: string,
loc: SrcLoc,
): IrExpr {
const args = expr.arguments;
const args = expr.arguments.nodes;
if (args.some(ts.isSpreadElement)) {
lowerer.noLowering(`the promisified crypto.${member} with spread arguments`, expr);
}
@@ -33,8 +33,14 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
getTimezoneOffset: "date.getTimezoneOffset",
};
const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
["toString", "date.toString"],
["toDateString", "date.toDateString"],
["toTimeString", "date.toTimeString"],
]);
const DATE_METHOD_HINT =
"getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering";
/** The Date slice: statics plus read-only Date values backed by one
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
@@ -47,8 +53,8 @@ export function lowerDateCall(
if (call.questionDotToken || access.questionDotToken) return null;
const loc = locOf(call);
if (lowerer.stdlibGlobalMember(access, "Date") === "now") {
if (call.arguments.length !== 0) {
lowerer.noLowering(`Date.now with ${call.arguments.length} arguments`, call);
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call);
}
return { kind: "libCall", fn: "date.now", args: [], type: F64, loc };
}
@@ -56,10 +62,10 @@ export function lowerDateCall(
// new Date(dateString).getTime(); keep their NaN and TimeClip behavior
// identical for the ISO timestamps used by CLI applications.
if (lowerer.stdlibGlobalMember(access, "Date") === "parse") {
if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) {
lowerer.noLowering(`Date.parse with ${call.arguments.length} arguments`, call);
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) {
lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call);
}
const input = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
const input = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING);
return { kind: "libCall", fn: "date.parse", args: [input], type: F64, loc };
}
// Date.UTC(year[, month[, date[, hours[, minutes[, seconds[, ms]]]]]]):
@@ -69,16 +75,16 @@ export function lowerDateCall(
// argument to number, so the seven-f64 ABI is exact.
if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") {
if (
call.arguments.length < 1 ||
call.arguments.length > 7 ||
call.arguments.some((a) => ts.isSpreadElement(a))
call.arguments.nodes.length < 1 ||
call.arguments.nodes.length > 7 ||
call.arguments.nodes.some((a) => ts.isSpreadElement(a))
) {
lowerer.noLowering(`Date.UTC with ${call.arguments.length} arguments`, call);
lowerer.noLowering(`Date.UTC with ${call.arguments.nodes.length} arguments`, call);
}
const defaults = [0, 0, 1, 0, 0, 0, 0]; // year is always present (arity ≥ 1)
const args: IrExpr[] = [];
for (let i = 0; i < 7; i++) {
const a = call.arguments[i];
const a = call.arguments.nodes[i];
args.push(
a !== undefined
? lowerer.lowerExprExpecting(a, F64)
@@ -91,7 +97,7 @@ export function lowerDateCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (call.arguments.length !== 0) {
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
const raw = lowerer.lowerExpr(access.expression);
@@ -113,10 +119,17 @@ export function lowerDateCall(
if (name === "toISOString") {
return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc };
}
if (name === "toUTCString") {
return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc };
}
const fn = own(DATE_GETTER_FNS, name);
if (fn !== undefined) {
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
}
const textFn = DATE_TEXT_FNS.get(name);
if (textFn !== undefined) {
return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc };
}
lowerer.noLowering(
`Date.prototype.${name}`,
call,
@@ -134,8 +147,8 @@ function lowerDateToJson(
access: ts.PropertyAccessExpression,
): IrExpr {
const loc = locOf(call);
if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a)))
lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.length} arguments`, call);
if (call.arguments.nodes.length > 1 || call.arguments.nodes.some((a) => ts.isSpreadElement(a)))
lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.nodes.length} arguments`, call);
const raw = lowerer.lowerExpr(access.expression);
const receiver: IrExpr =
raw.type.kind === "dyn"
@@ -145,7 +158,7 @@ function lowerDateToJson(
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
const date = lowerer.declareHiddenLocal("%date", receiver.type);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: date.id, init: receiver, loc }];
const key = call.arguments[0];
const key = call.arguments.nodes[0];
if (key) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(key), loc });
const value: IrExpr = { kind: "varRef", localId: date.id, type: receiver.type, loc };
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc };
@@ -22,7 +22,7 @@ export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression
if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null;
const symbol = lowerer.resolveValueSymbol(expr.expression);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
if ((expr.arguments ?? []).length !== 0) return null;
if ((expr.arguments?.nodes ?? []).length !== 0) return null;
return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) };
}
@@ -49,8 +49,8 @@ export function lowerDisposableStackUseCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (access.name.text !== "use" || call.arguments.length !== 1) return null;
const argNode = call.arguments[0]!;
if (access.name.text !== "use" || call.arguments.nodes.length !== 1) return null;
const argNode = call.arguments.nodes[0]!;
if (ts.isSpreadElement(argNode)) return null;
if (!isStdlibDisposableStack(lowerer, access.expression)) return null;
const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
@@ -78,12 +78,12 @@ export function lowerProcessEnvGet(
}
export function lowerProcessLoadEnvFile(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) {
if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering("process.loadEnvFile with spread or extra arguments", call);
}
const loc = locOf(call);
const path: IrExpr = call.arguments[0]
? lowerer.lowerExprExpecting(call.arguments[0], DYN)
const path: IrExpr = call.arguments.nodes[0]
? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN)
: dynUndefinedExpr(loc);
const load: IrExpr = {
kind: "libCall",

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