Merge origin/main into opt/gc-nursery

main now has #1286 and later collector changes. Resolved in scr_cycle.c:
the scheduled collection point collects the nursery, then notes the
live-count fall (death rule). Semantic fixes for the new main code:
a full pass's markGray no longer overwrites a young object's `buf` (its
nursery index) with the component number, and a nursery collection
refreshes the death rule's live reading at its end so the young garbage
it frees does not count as deaths outside passes. Formats the runtime
pack matrix test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 13:14:30 -07:00
co-authored by Claude Opus 5.5
689 changed files with 38229 additions and 2675 deletions
+6
View File
@@ -48,6 +48,12 @@ Corpus programs are differential tests against Node: every program runs under No
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=<regex>` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared:
```bash
node scripts/zero-cost-gate.mjs --baseline=/vercel/sandbox/prev --candidate=. --allow='\barguments\b'
```
Test location follows scope:
- Co-locate white-box unit tests with implementation files under
+1
View File
@@ -7,6 +7,7 @@ Programs that build generations of `ProgramLifetime` objects and free each gener
| `generations.ts` | Per-file syntax trees and binder symbols in one region per file, checker state (types, signatures, node and symbol links in paged tables, literal and union caches) in one region per generation. `edit` re-parses one file per generation, `all` re-parses every file. |
| `compiler.ts` | A tokenizer, recursive-descent parser, binder and checker over generated sources, rebuilt from scratch in one region per build, like a watch mode that does not reuse syntax trees. |
| `emit.ts` | A large live graph of ordinary objects with parent links, standing in for a type checker, and a per-file emit that builds its output in a region released right after. |
| `requests.ts` | Batches of concurrent async request handlers, each in its own region across its awaits: tokenize a JSON body, parse it into a syntax tree that points at its tokens, validate it into issues and an index, render a response, release the region. |
```console
$ node benchmarks/regions/run.mjs --baseline=../scriptc-main --runs=7
+1 -1
View File
@@ -54,7 +54,7 @@ let level: Type[] = [new Type(id++, undefined)];
while (id < typeCount) {
const next: Type[] = [];
for (const t of level) for (let k = 0; k < 4 && id < typeCount; k++) next.push(new Type(id++, t));
roots.push(...next);
for (const t of next) roots.push(t);
level = next;
}
+221
View File
@@ -0,0 +1,221 @@
// An async request handler: each request tokenizes a JSON body, awaits (as
// if reading more of it), parses the tokens into a syntax tree whose nodes
// point back at their tokens, awaits again (as if loading reference data),
// validates the tree into issues held in an index, and renders a response.
// Requests run in batches of concurrent handlers, so the handlers of one
// batch interleave at every await.
//
// requests <requests> <concurrency> <items-per-body> [plain]
//
// Each request runs in its own LifetimeRegion, released when its response
// is rendered; `plain` never constructs a region.
import { readFileSync } from "node:fs";
import type { ProgramLifetime } from "@scriptc/types";
import { LifetimeRegion } from "@scriptc/experimental";
const T_PUNCT = 0;
const T_STR = 1;
const T_NUM = 2;
const T_WORD = 3;
class Token implements ProgramLifetime {
readonly kind: number;
readonly text: string;
readonly pos: number;
constructor(kind: number, text: string, pos: number) {
this.kind = kind;
this.text = text;
this.pos = pos;
}
}
const N_OBJECT = 0;
const N_ARRAY = 1;
const N_STRING = 2;
const N_NUMBER = 3;
const N_LITERAL = 4;
class JsonNode implements ProgramLifetime {
readonly kind: number;
readonly token: Token;
parent: JsonNode | undefined = undefined;
readonly keys: string[] = [];
readonly children: JsonNode[] = [];
constructor(kind: number, token: Token) {
this.kind = kind;
this.token = token;
}
}
class Issue implements ProgramLifetime {
readonly path: string;
readonly node: JsonNode;
readonly message: string;
constructor(path: string, node: JsonNode, message: string) {
this.path = path;
this.node = node;
this.message = message;
}
}
// ---- request bodies (deterministic) ----
function body(id: number, items: number): string {
let seed = 7 + id * 7919;
const rand = (n: number): number => {
seed = (seed * 1103515245 + 12345) % 2147483648;
return seed % n;
};
const parts: string[] = [];
for (let i = 0; i < items; i++) {
const qty = rand(12) - 1;
const sku = rand(9) === 0 ? "" : `sku-${rand(5000)}`;
const tags = Array.from({ length: rand(4) }, (_, k) => `"t${k}${rand(10)}"`).join(",");
parts.push(`{"sku":"${sku}","qty":${qty},"price":${rand(10000) / 100},"gift":${rand(2) === 0},"tags":[${tags}]}`);
}
return `{"order":${id},"customer":"c${id % 97}","items":[${parts.join(",")}]}`;
}
// ---- handler ----
function tokenize(text: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < text.length) {
const c = text.charCodeAt(i);
const start = i;
if (c === 34) {
i++;
while (text.charCodeAt(i) !== 34) i++;
i++;
tokens.push(new Token(T_STR, text.slice(start + 1, i - 1), start));
} else if (c === 45 || (c >= 48 && c <= 57)) {
i++;
while (i < text.length && (text.charCodeAt(i) === 46 || (text.charCodeAt(i) >= 48 && text.charCodeAt(i) <= 57))) i++;
tokens.push(new Token(T_NUM, text.slice(start, i), start));
} else if (c >= 97 && c <= 122) {
while (i < text.length && text.charCodeAt(i) >= 97 && text.charCodeAt(i) <= 122) i++;
tokens.push(new Token(T_WORD, text.slice(start, i), start));
} else {
i++;
tokens.push(new Token(T_PUNCT, text[start]!, start));
}
}
return tokens;
}
class Parser {
private at = 0;
private readonly tokens: Token[];
constructor(tokens: Token[]) {
this.tokens = tokens;
}
value(): JsonNode {
const token = this.tokens[this.at++]!;
if (token.kind === T_STR) return new JsonNode(N_STRING, token);
if (token.kind === T_NUM) return new JsonNode(N_NUMBER, token);
if (token.kind === T_WORD) return new JsonNode(N_LITERAL, token);
const node = new JsonNode(token.text === "{" ? N_OBJECT : N_ARRAY, token);
const close = token.text === "{" ? "}" : "]";
while (this.tokens[this.at]!.text !== close) {
if (node.kind === N_OBJECT) {
node.keys.push(this.tokens[this.at]!.text);
this.at += 2; // the key and the colon
}
const child = this.value();
child.parent = node;
node.children.push(child);
if (this.tokens[this.at]!.text === ",") this.at++;
}
this.at++;
return node;
}
}
function member(node: JsonNode, key: string): JsonNode | undefined {
const i = node.keys.indexOf(key);
return i < 0 ? undefined : node.children[i];
}
function validate(root: JsonNode, issues: Issue[], index: Map<string, JsonNode>): number {
let total = 0;
const items = member(root, "items")!;
for (let i = 0; i < items.children.length; i++) {
const item = items.children[i]!;
const path = `items[${i}]`;
const sku = member(item, "sku")!;
if (sku.token.text === "") issues.push(new Issue(`${path}.sku`, sku, "empty sku"));
else if (index.has(sku.token.text)) issues.push(new Issue(`${path}.sku`, sku, `duplicate of ${index.get(sku.token.text)!.token.pos}`));
else index.set(sku.token.text, item);
const qty = Number(member(item, "qty")!.token.text);
if (qty <= 0) issues.push(new Issue(`${path}.qty`, member(item, "qty")!, "quantity must be positive"));
else total += qty * Number(member(item, "price")!.token.text);
const tags = member(item, "tags")!;
for (const tag of tags.children) if (tag.token.text.endsWith("0")) issues.push(new Issue(`${path}.tags`, tag, "reserved tag"));
}
return total;
}
const hop = (): Promise<void> => new Promise((resolve) => setImmediate(resolve));
async function handle(text: string): Promise<string> {
const tokens = tokenize(text);
await hop();
const root = new Parser(tokens).value();
await hop();
const issues: Issue[] = [];
const index = new Map<string, JsonNode>();
const total = validate(root, issues, index);
await hop();
const first = issues[0];
return `${member(root, "order")!.token.text} ${tokens.length} ${index.size} ${issues.length} ${total.toFixed(2)}` +
(first === undefined ? "" : ` ${first.path}@${first.node.token.pos}`);
}
// ---- driver ----
function rssMb(key: string): number {
const m = new RegExp(`${key}:\\s*(\\d+) kB`).exec(readFileSync("/proc/self/status", "utf8"));
return m === null ? 0 : Number(m[1]) / 1024;
}
const args = process.argv.slice(2);
const requests = Number(args[0] ?? "4000");
const concurrency = Number(args[1] ?? "16");
const items = Number(args[2] ?? "100");
const useRegions = args[3] !== "plain";
async function serve(id: number): Promise<string> {
const text = body(id, items);
if (!useRegions) return await handle(text);
const region = new LifetimeRegion();
const response = await region.run(() => handle(text));
region.release();
return response;
}
async function main(): Promise<void> {
let hash = 0;
let lines = 0;
let sample = "";
let rssFirst = 0;
const t0 = performance.now();
for (let next = 0; next < requests; next += concurrency) {
const batch: Promise<string>[] = [];
for (let k = next; k < next + concurrency && k < requests; k++) batch.push(serve(k));
for (const response of await Promise.all(batch)) {
for (let i = 0; i < response.length; i++) hash = (hash * 31 + response.charCodeAt(i)) % 1000000007;
lines++;
if (lines === 1) sample = response;
}
if (next === 0) rssFirst = rssMb("VmRSS");
}
const ms = performance.now() - t0;
console.log(`${lines} responses, hash ${hash}, first ${sample}`);
console.log(
`total ${ms.toFixed(1)} ms per-request ${((ms * 1000) / requests).toFixed(1)} us` +
` rss-first ${rssFirst.toFixed(1)} MB rss-last ${rssMb("VmRSS").toFixed(1)} MB peak ${rssMb("VmHWM").toFixed(1)} MB`,
);
}
main();
+3 -1
View File
@@ -39,6 +39,8 @@ const CONFIGS = [
{ name: "compiler-one-shot", program: "compiler.ts", args: ["400", "60", "1"] },
// Per-file emit regions next to a large live heap of ordinary objects.
{ name: "emit-files", program: "emit.ts", args: ["400000", "200", "2000"] },
// Concurrent async request handlers, one region per request across its awaits.
{ name: "requests-async", program: "requests.ts", args: ["4000", "16", "100"] },
];
const work = join(tmpdir(), `scriptc-region-bench-${process.pid}`);
@@ -93,7 +95,7 @@ for (const config of CONFIGS) {
const lines = r.stdout.trim().split("\n");
outputs.set(name, lines[0].replace(/ gen0 .*/, ""));
const metrics = {};
for (const [, key, value] of lines.at(-1).matchAll(/([a-z0-9-]+) ([0-9.]+) (?:ms|MB)/g))
for (const [, key, value] of lines.at(-1).matchAll(/([a-z0-9-]+) ([0-9.]+) (?:ms|us|MB)/g))
metrics[key] = Number(value);
samples[name].push(metrics);
}
+1
View File
@@ -19,6 +19,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `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 |
| `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 |
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields |
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
+7
View File
@@ -141,6 +141,13 @@
"2"
]
},
{
"name": "value-records",
"entry": "workloads/value-records.ts",
"args": [
"1"
]
},
{
"name": "region-builds",
"entry": "workloads/region-builds.ts",
@@ -0,0 +1,93 @@
// Records that hold references: a tokenizer that builds token records with
// string fields and passes them to helpers, an immutable reducer that
// updates its state with object spread, and tuple-returning helpers whose
// results are destructured at once. None of these temporaries outlives
// the statement or loop iteration that builds it.
interface Token {
kind: string;
text: string;
pos: number;
}
const KEYWORDS = ["let", "const", "if", "else", "for", "while", "return", "function"];
function classify(text: string): string {
return KEYWORDS.includes(text) ? "keyword" : "ident";
}
function token(kind: string, text: string, pos: number): Token {
return { kind, text, pos };
}
function weight(t: Token): number {
return t.kind === "keyword" ? t.text.length * 3 : t.text.length + (t.pos & 7);
}
function tokenize(source: string): number {
let sum = 0;
let start = -1;
for (let i = 0; i <= source.length; i++) {
const c = i < source.length ? source.charCodeAt(i) : 32;
const word = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95;
if (word) {
if (start < 0) start = i;
continue;
}
if (start >= 0) {
const text = source.slice(start, i);
const t = token(classify(text), text, start);
sum += weight(t);
start = -1;
}
if (c !== 32 && c !== 10) {
const punct = { kind: "punct", text: source[i]!, pos: i };
sum += weight(punct) + (punct.text === ";" ? 1 : 0);
}
}
return sum;
}
interface State {
mode: string;
depth: number;
count: number;
last: string;
}
function step(s: State, text: string): State {
if (text === "{") return { ...s, depth: s.depth + 1, last: text };
if (text === "}") return { ...s, depth: s.depth - 1, last: text };
return s.mode === "code" ? { ...s, count: s.count + 1 } : { ...s, mode: "code", last: text };
}
function reduce(texts: string[], rounds: number): number {
let total = 0;
for (let r = 0; r < rounds; r++) {
let state: State = { mode: "start", depth: 0, count: 0, last: "" };
for (const text of texts) {
state = step(state, text);
if (state.depth > 3) state = { ...state, depth: 3 };
}
total += state.count + state.depth + state.last.length + state.mode.length;
}
return total;
}
function divmod(a: number, b: number): [number, number] {
return [Math.floor(a / b), a % b];
}
function split(text: string, at: number): [string, string] {
return [text.slice(0, at), text.slice(at)];
}
function tuples(n: number, texts: string[]): number {
let sum = 0;
for (let i = 0; i < n; i++) {
const [q, r] = divmod(i * 31, 17);
const [head, tail] = split(texts[i % texts.length]!, r % 3);
sum += q + r + head.length * 2 + tail.length;
}
return sum;
}
const scaleArg = Number(process.argv[2] ?? "1");
const line = "function step ( s , text ) { if ( text ) { return s ; } let x = s ; while ( x ) { x = x ; } }\n";
const source = line.repeat(Math.floor(400 * scaleArg));
const texts = source.split(" ").filter((t) => t.length > 0);
let lexed = 0;
for (let i = 0; i < 240; i++) lexed += tokenize(source);
const reduced = reduce(texts, Math.floor(600 * scaleArg));
const pairs = tuples(Math.floor(2_000_000 * scaleArg), texts);
console.log("tokens", lexed, "reduce", reduced, "tuples", pairs);
+6
View File
@@ -127,6 +127,12 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<dt><code>--external-types &lt;specifier=file.d.ts&gt;</code></dt>
<dd>Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit <code>SC1010</code> blockers. Relative paths resolve from the current working directory. Accepted files end in <code>.d.ts</code>, <code>.d.mts</code>, or <code>.d.cts</code>.</dd>
<dt><code>--tier &lt;auto|static-only&gt;</code></dt>
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dt><code>--explain-dynamic[=&lt;name&gt;]</code></dt>
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dt><code>--sanitize</code></dt>
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
+1 -1
View File
@@ -331,7 +331,7 @@ $ scriptc build graph.ts
## ProgramLifetime
`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed.
`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. On Linux, the instances a worker thread creates are returned with the rest of its memory after it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)).
```ts:tree.ts
import type { ProgramLifetime } from "@scriptc/types";
+12 -1
View File
@@ -36,6 +36,7 @@ When a program has unsupported operations, the report adds a group for each root
<tbody>
<tr><td><code>statements analyzed</code></td><td>Executable statements in application code, excluding dependency implementation code.</td></tr>
<tr><td><code>compile statically</code></td><td>Statements that compile to native instructions.</td></tr>
<tr><td><code>in dynamic tier</code></td><td>Of those, the statements in functions that operate on values typed only at runtime, such as <code>any</code> values and untyped JavaScript. Shown when there are any. See <a href="#dynamic-tier">Dynamic tier</a>.</td></tr>
<tr><td><code>blockers</code></td><td>Unsupported operations with counts, diagnostic codes, reasons, locations, and hints.</td></tr>
<tr><td><code>in unreached code</code></td><td>Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.</td></tr>
<tr><td><code>differs from Node</code></td><td>Sites that compile but can behave differently from Node.js. See <a href="#node-divergences">Node divergences</a>.</td></tr>
@@ -51,6 +52,16 @@ If a function's signature can't compile, the statements in its body count as not
<span id="analyze-with-dynamic" />
<span id="embedded-nodejs-builtins" />
<span id="dynamic-tier" />
## Dynamic tier
Code that operates on values whose types are known only at runtime, such as `any` values, the result of an untyped `JSON.parse`, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's `in dynamic tier` line counts their statements.
`--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information.
`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.
## Diagnostic codes
Coverage and `scriptc build` report the same diagnostic codes. Common ones include:
@@ -160,7 +171,7 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost
</tr>
<tr>
<td><code>stats</code></td>
<td>Coverage only, absent after a preflight failure: the report's statement counts. <code>statementsDynamic</code> is always 0 and remains for schema compatibility.</td>
<td>Coverage only, absent after a preflight failure: the report's statement counts. <code>statementsStatic</code> counts every statement that compiles to native code; <code>statementsDynamic</code> is the part of those in dynamic-tier functions.</td>
</tr>
<tr>
<td><code>category</code></td>
+4
View File
@@ -69,6 +69,10 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null
Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
## CommonJS packages
CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
## Unsupported code inside a package
A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses.
+59 -4
View File
@@ -73,9 +73,9 @@ scriptc region heap passes: 0
<dt><code>new LifetimeRegion()</code></dt>
<dd>Creates a region. Creating a region also retries the released regions that could not be freed yet.</dd>
<dt><code>region.run(fn)</code></dt>
<dd>Calls <code>fn</code> with the region as the current region and returns its result. <code>ProgramLifetime</code> instances created during the call belong to the region. Calls nest: a <code>run</code> inside the callback makes another region current until it returns, and the previous region is current again however the callback finishes, including when it throws.</dd>
<dd>Calls <code>fn</code> with the region as the current region and returns its result. <code>ProgramLifetime</code> instances created during the call belong to the region, and so do those that async functions called during it create after an <code>await</code> (see <a href="#async-work">Async work</a>). Calls nest: a <code>run</code> inside the callback makes another region current until it returns, and the previous region is current again however the callback finishes, including when it throws.</dd>
<dt><code>region.release()</code></dt>
<dd>Requests that the region be freed. scriptc frees it as soon as nothing outside the region refers to any of its instances, which may be at once, at a later release or region creation, or never. A region is never freed while a <code>run</code> call on it is active. After <code>release()</code>, <code>run</code> still calls its callback, and the instances it creates belong to the default region.</dd>
<dd>Requests that the region be freed. scriptc frees it as soon as nothing outside the region refers to any of its instances, which may be at once, at a later release or region creation, or never. A region is never freed while a <code>run</code> call on it is active, or while async work started inside it has not finished. After <code>release()</code>, <code>run</code> still calls its callback, and the instances it creates belong to the default region.</dd>
</dl>
`ProgramLifetime` instances created outside every `run` call belong to the default region, which is never freed. Ordinary objects (instances of classes without the contract, arrays, maps, closures) are reference counted as always; a region frees the ones that only its instances refer to.
@@ -92,7 +92,62 @@ A released region stays allocated, and its instances stay usable, while anything
- an instance of the default region, which is never freed;
- a promise, generator or `Error` that holds an instance, until it is no longer referenced.
Work that resumes after an `await` inside the callback runs outside the region, so the instances it creates belong to the default region and keep the instances they refer to allocated.
### Async work
An async function called inside `run` keeps the region for its whole run: the instances it creates after each `await` belong to the region too, as do those of the async functions it calls. A server can give every request its own region, even while requests interleave at their awaits:
```ts:requests.ts
import type { ProgramLifetime } from "@scriptc/types";
import { LifetimeRegion } from "@scriptc/experimental";
class Part implements ProgramLifetime {
readonly text: string;
previous: Part | undefined = undefined;
constructor(text: string) {
this.text = text;
}
}
const read = (): Promise<void> => new Promise((resolve) => setImmediate(resolve));
async function handle(id: number): Promise<string> {
let last: Part | undefined;
for (let i = 0; i < 1000; i++) {
if (i % 100 === 0) await read(); // the next chunk of the request body
const part = new Part(`${id}.${i}`);
part.previous = last;
last = part;
}
return `request ${id}: last part ${last!.text}`;
}
async function serve(id: number): Promise<string> {
const region = new LifetimeRegion();
try {
return await region.run(() => handle(id));
} finally {
region.release();
}
}
Promise.all([1, 2, 3].map(serve)).then((responses) => {
for (const response of responses) console.log(response);
});
```
Each request's 1000 parts are freed when the request releases its region:
```console
$ scriptc build requests.ts -o requests >/dev/null
$ SCRIPTC_REGION_STATS=1 ./requests >/dev/null
scriptc region 1: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms
scriptc region 2: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms
scriptc region 3: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms
scriptc regions: 3 created, 3 released, 3 freed, 0 pinned, 0 kept
scriptc region heap passes: 0
```
A region released while async work started inside it is still pending stays allocated until that work has finished, and the work keeps creating its instances in the region. Callbacks that the event loop calls (timers, `queueMicrotask`, event listeners) and the bodies of async generators run outside the region; synchronous generators run in the region of the code that resumes them.
### Threads
@@ -102,7 +157,7 @@ Each thread has its own regions and its own default region. A region belongs to
Programs that never construct a `LifetimeRegion` compile exactly as they would without the package. In programs that do, every retain and release of a `ProgramLifetime` instance that belongs to a region updates a count of the region chunk that holds it. Instances of the default region are not counted, and until the program creates its first region, a retain or release only tests whether counting has started.
Freeing a region visits each of its instances and the ordinary objects they own. A region that the check would keep may be kept only by unreachable cycles of ordinary objects that refer into it, so before keeping it scriptc runs the cycle collector over the thread's heap and checks again.
Freeing a region visits each of its instances and the ordinary objects they own. Each thread keeps up to 8 MiB of the memory of freed regions for its next regions, so a program that frees a region per request or per batch does not map and fault in new pages every time. A region that the check would keep may be kept only by unreachable cycles of ordinary objects that refer into it, so before keeping it scriptc runs the cycle collector over the thread's heap and checks again.
`SCRIPTC_REGION_STATS=1` prints one line per release attempt, freed, `kept` (with the number of references the check could not explain) or `pinned` (published), and at exit a summary per thread and the number of those cycle-collector passes over the thread's heap.
+2
View File
@@ -32,6 +32,8 @@ title: "Limitations"
<include>../limitations/language-features/dynamic-imports.mdx</include>
<include>../limitations/language-features/commonjs-require.mdx</include>
### Types and class definitions
<include>../limitations/types-and-class-definitions/bigint.mdx</include>
+2
View File
@@ -234,6 +234,8 @@ Without `@scriptc/threads`, a scriptc executable also shares a message of untype
Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit.
On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as `ProgramLifetime` instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an `InternTable`. The memory of a worker that shared stays allocated, as on other platforms. Set `SCR_SA_DISCARD=0` in the environment to keep the memory of every worker.
`publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.
Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`.
+3 -1
View File
@@ -96,11 +96,13 @@ After initialization, <code>counter_step(2)</code> returns <code>2</code>, and a
<tr><td><code>i64</code>, <code>u64</code></td><td>JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range.</td></tr>
<tr><td><code>string</code>, <code>bytes</code> parameters</td><td>Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values.</td></tr>
<tr><td><code>string</code>, <code>bytes</code> results</td><td>Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order.</td></tr>
<tr><td><code>cstring</code> parameters</td><td>One i32 argument: the byte offset of NUL-terminated UTF-8 text. The text is copied into a runtime-owned string; malformed UTF-8 becomes U+FFFD. A zero pointer traps with <code>SC4012</code>.</td></tr>
<tr><td><code>cstring</code> results</td><td>An i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with <code>SC4015</code> instead of being truncated.</td></tr>
<tr><td><code>void</code></td><td>No result.</td></tr>
</tbody>
</table>
The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host.
The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host. A <code>cstring</code> callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with <code>SC4015</code>. The <code>cstring</code> class requires <code>"profile_format": 2</code>. In native libraries it is a <code>const char *</code> with the same rules.
Allocate host input buffers and output slots with <code>scriptc_alloc(size)</code>; a zero result indicates allocation failure. Release each allocation exactly once with <code>scriptc_free(pointer)</code>. Only free pointers returned by <code>scriptc_alloc</code>. These allocations remain live across application calls, initialization, and collection until freed.
@@ -2,6 +2,8 @@ Typed-array reads at an index that is not an integer in the array's range return
Ordinary arrays track holes separately from present `undefined` values. Missing reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes.
Keyed reads of a `Record` or an index signature return `undefined` for a missing key, as in Node.js, with or without `noUncheckedIndexedAccess`. A missing array element or key that reaches storage that cannot hold `undefined`, such as a class field typed `number`, throws a `TypeError`.
Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process.
Deep recursion in Linux and macOS executables throws Node's catchable `RangeError: Maximum call stack size exceeded`, on the main thread, in worker threads, and inside async functions and generators. The depth at which it is thrown differs from Node: the main thread uses the process stack limit (usually 8 MiB), worker threads get an 8 MiB stack, and async functions and generators run on 512 KiB stacks, so native code usually recurses deeper than Node on threads and somewhat less deeply inside async bodies. Recursion inside runtime library operations, such as serializing deeply nested data, is not checked. Windows executables, WASI builds and library builds do not check the stack, and exhausting it there terminates the process.
@@ -1 +1 @@
- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, Float16 arrays, and BigInt arrays are unsupported. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool.
- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, and BigInt arrays are unsupported. `Float16Array` stores IEEE 754 half-precision values, rounding each stored number to nearest-even as Node does; `Math.f16round`, `DataView.prototype.getFloat16` and `setFloat16`, and `util.types.isFloat16Array` compile. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool.
@@ -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`, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. 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/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.
@@ -1 +1 @@
- **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
- **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported. Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
@@ -1 +1 @@
- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. Reads, resets, and assignments used as values are unsupported.
- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. `process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does. Reads, resets, and assignments used as values are unsupported.
@@ -1 +1 @@
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `d` (match indices) and `v` (unicode sets) flags are unsupported: regex literals and `new RegExp` calls with a literal flag string containing them fail to compile with SC1120, and `new RegExp` with a computed flag string containing them throws an error naming the flag.
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Named groups (`groups`) read on matches of a statically known regex: from `exec` and `match`, through optional chains (`m?.groups?.name`), in a `for...of` over `matchAll`, and on the element parameter of an array-method callback over `matchAll` rows. The groups object has a null prototype, as in Node. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
@@ -1 +1,2 @@
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module`, `mock.timers` and mock options throw `SC2020`.
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers. As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`. While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error. A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`. When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker.
@@ -1 +1,5 @@
- **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node.
- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `<entry file>.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
- **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.
@@ -5,7 +5,7 @@ Some builtins can be stored and passed as functions:
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
@@ -1 +1 @@
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises with an optional mapping function; async generator sources, a `thisArg`, and arrays that mix values and promises are unsupported.
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported.
@@ -0,0 +1 @@
- **CommonJS `require`:** Literal `require()` calls of compiled modules, supported builtins, and JSON documents compile. Reading a top-level `const` or `let` require binding before its `require` runs throws the same `ReferenceError` as Node.js. A `var` binding that code above its `require` can reach is unsupported, and so is a read that compiles to no checkable expression, such as exporting the binding before the `require`. JSON documents are parsed at build time, and every `require()` of a document returns the same object. Destructuring the exports of a module supports plain and renamed properties, plus nested patterns for builtin sub-modules such as `util.types`. Any pattern works on a JSON document. Defaults, rest elements, and nested patterns over other modules are unsupported. A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call. Other require cycles are rejected with `SC1016`.
@@ -1 +1 @@
- **Error causes:** Accessor constructor options and explicit subclass redeclarations of `cause` are unsupported. `Error`, `TypeError`, `RangeError`, and `SyntaxError` otherwise accept `{ cause }`, including through inherited constructors. Reads, writes, deletion, and `"cause" in error` distinguish an absent cause from an explicitly undefined cause. Constructor causes are non-enumerable; assignment to an absent cause creates an enumerable property. Property descriptors and checked-value aliases share the cause, including getters and setters installed after construction.
- **Error causes:** Accessor constructor options are unsupported. Subclasses may redeclare `cause` as `unknown` or `object` (for example `cause?: unknown`), which defines the own property after `super()` returns as Node does; redeclarations with other types, parameter properties, and accessors named `cause` are unsupported. `Error`, `TypeError`, `RangeError`, and `SyntaxError` otherwise accept `{ cause }`, including through inherited constructors. Reads, writes, deletion, and `"cause" in error` distinguish an absent cause from an explicitly undefined cause. Constructor causes are non-enumerable; assignment to an absent cause creates an enumerable property. Property descriptors and checked-value aliases share the cause, including getters and setters installed after construction.
@@ -1 +1 @@
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`).
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `AsyncDisposableStack`, are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws.
@@ -1,6 +1,6 @@
`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, and a `transferList` of MessagePorts; runtime paths, `eval`, loaders, custom stdio, shared environments, and resource limits are unsupported. Worker argument strings containing null bytes are also unsupported.
`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB). The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node. With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events. Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused. With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node. Runtime paths, `eval`, loaders, and shared environments are unsupported. `setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned. Worker argument strings containing null bytes are also unsupported.
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. A port's `onmessage` property, `addEventListener`, `receiveMessageOnPort(parentPort)`, and `BroadcastChannel` are unsupported. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `worker[Symbol.asyncDispose]()` terminates the worker and resolves with `undefined` after its `exit` event, so `await using` disposes a Worker at the end of its block. A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do. An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason. A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. `receiveMessageOnPort(parentPort)` in a worker takes the parent's next message. A port's `onmessage` property and `addEventListener` are unsupported. `BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`. A `messageerror` event is never dispatched. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. `markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits. `markAsUncloneable` marks a plain object, an error, or a class instance, and `structuredClone` and `postMessage` then throw `DataCloneError` for it, also where it is nested; as in Node, the mark has no effect on primitives, arrays, typed arrays, and ArrayBuffers. A plain object that the compiler copies into a container with a fixed element type, such as an array literal whose elements all have one shape, is a separate object in the copy, which is not marked. Marked objects stay alive until their thread exits. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. Atomics operations run as sequentially consistent atomic instructions and take no lock. Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint.
@@ -1,4 +1,4 @@
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
`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`.
@@ -3,3 +3,5 @@ Casts from values whose type is only known at runtime validate the value. For ex
Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it.
A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions.
A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union. If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "a1ff929d49885bd8c92e",
"artifactVersion": "a610bdd1ae56e1c4cd38",
"rowCount": 3880
}
+206 -206
View File
@@ -164,9 +164,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
"partial": 17,
"partial": 18,
"refused": 0,
"not-implemented": 22,
"not-implemented": 21,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -509,9 +509,9 @@
"apiEntries": 24,
"static": {
"supported": 0,
"partial": 23,
"partial": 24,
"refused": 0,
"not-implemented": 1,
"not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -569,9 +569,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
"partial": 50,
"partial": 51,
"refused": 0,
"not-implemented": 70,
"not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -689,9 +689,9 @@
"apiEntries": 117,
"static": {
"supported": 0,
"partial": 45,
"partial": 64,
"refused": 0,
"not-implemented": 72,
"not-implemented": 53,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -779,9 +779,9 @@
"apiEntries": 62,
"static": {
"supported": 0,
"partial": 39,
"partial": 42,
"refused": 0,
"not-implemented": 23,
"not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -794,9 +794,9 @@
"apiEntries": 109,
"static": {
"supported": 0,
"partial": 53,
"partial": 72,
"refused": 0,
"not-implemented": 56,
"not-implemented": 37,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -884,9 +884,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 26,
"partial": 46,
"refused": 0,
"not-implemented": 40,
"not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -9312,9 +9312,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"
}
},
{
@@ -52503,9 +52503,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -57593,9 +57593,9 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -65550,9 +65550,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"
}
},
{
@@ -65573,9 +65573,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"
}
},
{
@@ -65596,9 +65596,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"
}
},
{
@@ -65619,9 +65619,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"
}
},
{
@@ -65642,9 +65642,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"
}
},
{
@@ -65665,9 +65665,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"
}
},
{
@@ -66401,9 +66401,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"
}
},
{
@@ -66424,9 +66424,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"
}
},
{
@@ -66447,9 +66447,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"
}
},
{
@@ -66516,9 +66516,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"
}
},
{
@@ -66562,9 +66562,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"
}
},
{
@@ -66585,9 +66585,9 @@
"inherited": false
},
"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"
}
},
{
@@ -66608,9 +66608,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"
}
},
{
@@ -66677,9 +66677,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"
}
},
{
@@ -66723,9 +66723,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"
}
},
{
@@ -66746,9 +66746,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"
}
},
{
@@ -66769,9 +66769,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"
}
},
{
@@ -66815,9 +66815,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"
}
},
{
@@ -66838,9 +66838,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"
}
},
{
@@ -71599,9 +71599,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"
}
},
{
@@ -71714,9 +71714,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"
}
},
{
@@ -71806,9 +71806,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"
}
},
{
@@ -73324,9 +73324,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"
}
},
{
@@ -73347,9 +73347,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"
}
},
{
@@ -73508,9 +73508,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"
}
},
{
@@ -74060,9 +74060,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74083,9 +74083,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74106,9 +74106,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74129,9 +74129,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74152,9 +74152,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74175,9 +74175,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74221,9 +74221,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74244,9 +74244,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74267,9 +74267,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74451,9 +74451,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74497,9 +74497,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74566,9 +74566,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74681,9 +74681,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74704,9 +74704,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74727,9 +74727,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -74750,9 +74750,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
"verification": "test-backed"
}
},
{
@@ -82920,9 +82920,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"
}
},
{
@@ -82943,9 +82943,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"
}
},
{
@@ -82966,9 +82966,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"
}
},
{
@@ -82989,9 +82989,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"
}
},
{
@@ -83012,9 +83012,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"
}
},
{
@@ -83035,9 +83035,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"
}
},
{
@@ -83564,9 +83564,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"
}
},
{
@@ -83633,9 +83633,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"
}
},
{
@@ -83656,9 +83656,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"
}
},
{
@@ -83679,9 +83679,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"
}
},
{
@@ -83702,9 +83702,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"
}
},
{
@@ -83748,9 +83748,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"
}
},
{
@@ -83886,9 +83886,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"
}
},
{
@@ -83909,9 +83909,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"
}
},
{
@@ -83932,9 +83932,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"
}
},
{
@@ -83955,9 +83955,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"
}
},
{
@@ -84047,9 +84047,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"
}
},
{
@@ -84070,9 +84070,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"
}
},
{
@@ -84139,9 +84139,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"
}
},
{
@@ -84208,9 +84208,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"
}
},
{
+4
View File
@@ -273,6 +273,10 @@
"text": "When a region is freed",
"id": "when-a-region-is-freed"
},
{
"text": "Async work",
"id": "async-work"
},
{
"text": "Threads",
"id": "threads"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+26 -8
View File
@@ -104,6 +104,7 @@
"node-builtin.os.endianness": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.freemem": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.getPriority": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.setPriority": ["tests/corpus/5883-os-set-priority.ts"],
"node-builtin.os.hostname": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.loadavg": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.machine": ["tests/corpus/5140-os-host-facts.ts"],
@@ -145,7 +146,7 @@
"node-builtin.process.versions": ["tests/corpus/process-versions-storage.ts"],
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.exitCode": ["tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
"node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
@@ -204,10 +205,20 @@
{"chapter": "worker_threads", "symbols": ["worker.terminate"], "status": "partial", "evidence": ["tests/corpus/native-worker-terminate-loop.ts", "tests/corpus/native-worker-terminate-wait.ts", "tests/corpus/native-worker-terminate-suspended.ts", "tests/corpus/native-worker-loop-cancellation.ts"]},
{"chapter": "worker_threads", "symbols": ["worker.ref", "worker.unref"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts"]},
{"chapter": "worker_threads", "symbols": ["port.close", "port.start", "port.ref", "port.unref", "port.hasRef"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts"]},
{"chapter": "worker_threads", "parent": "Class: `Worker`", "signatures": ["Event: `'online'`", "Event: `'message'`", "Event: `'error'`", "Event: `'exit'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", "tests/corpus/native-worker-exit-listeners.ts"]},
{"chapter": "worker_threads", "parent": "Class: `Worker`", "signatures": ["Event: `'online'`", "Event: `'message'`", "Event: `'error'`", "Event: `'exit'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", "tests/corpus/native-worker-exit-listeners.ts", "tests/corpus/5807-worker-error-values.ts", "tests/corpus/5808-worker-unhandled-rejections.ts"]},
{"chapter": "worker_threads", "parent": "Class: `MessagePort`", "signatures": ["Event: `'message'`", "Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-messages.ts"]},
{"chapter": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts"]},
{"chapter": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts", "tests/corpus/5805-worker-receive-message-parent-port.ts"]},
{"chapter": "worker_threads", "symbols": ["worker_threads.getEnvironmentData", "worker_threads.setEnvironmentData"], "status": "partial", "evidence": ["tests/corpus/5803-worker-environment-data.ts", "tests/corpus/5804-worker-environment-data.cjs"]},
{"chapter": "worker_threads", "symbols": ["stdout", "stderr", "stdin"], "status": "partial", "evidence": ["tests/corpus/5874-worker-stdio-streams.ts", "tests/corpus/5875-worker-stdout-pipe.cjs", "tests/corpus/5879-worker-stdin.ts"]},
{"chapter": "worker_threads", "symbols": ["threadName", "worker_threads.markAsUntransferable", "worker_threads.isMarkedAsUntransferable"], "status": "partial", "evidence": ["tests/corpus/5870-worker-thread-names.ts", "tests/corpus/5871-worker-untransferable.cjs"]},
{"chapter": "worker_threads", "symbols": ["worker_threads.markAsUncloneable", "isInternalThread"], "status": "partial", "evidence": ["tests/corpus/5990-worker-uncloneable.mjs"]},
{"chapter": "worker_threads", "signatures": ["`worker[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/5991-worker-async-dispose.mjs"]},
{"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]},
{"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] },
{ "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] },
{ "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] },
{ "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" },
@@ -300,7 +311,7 @@
{ "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] },
{ "chapter": "modules", "symbols": ["extensions"], "status": "not-implemented" },
{ "chapter": "module", "symbols": ["module.createRequire"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2675-create-require-nested-esm.js", "tests/corpus/2932-create-require-program/main.ts", "tests/harness/npm-static.test.ts"] },
{ "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts"] },
{ "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts", "tests/corpus/5846-events-esm-default-max.ts"] },
{ "chapter": "module", "symbols": ["builtinModules"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2932-create-require-program/main.ts"] },
{ "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] },
{ "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts", "tests/corpus/2933-static-dynamic-import/main.ts", "tests/corpus/2934-static-import-stored.ts", "tests/corpus/2935-module-namespace-values/main.ts"] },
@@ -311,6 +322,7 @@
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
{ "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] },
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
@@ -337,18 +349,24 @@
{ "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] },
{ "chapter": "url", "symbols": ["url.fileURLToPath", "url.fileURLToPathBuffer", "url.pathToFileURL"], "status": "partial", "evidence": ["tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js", "tests/fixtures/node-types/file-url-conversion.ts"] },
{ "chapter": "url", "symbols": ["URL.canParse", "URL.parse", "url.toString", "url.toJSON"], "status": "partial", "evidence": ["tests/corpus/node24-url-factories.ts", "tests/corpus/node24-url-factory-errors.ts", "tests/fixtures/node-types/url-factories.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "url", "symbols": ["urlSearchParams.entries", "urlSearchParams.keys", "urlSearchParams.values"], "status": "partial", "evidence": ["tests/corpus/5833-url-search-params-iterators.ts", "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"] },
{ "chapter": "url", "signatures": ["Class: `URL`"], "status": "partial", "evidence": ["tests/corpus/url-runtime-base.ts", "tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`protocol` Type: {string}", "`username` Type: {string}", "`password` Type: {string}", "`host` Type: {string}", "`hostname` Type: {string}", "`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-authority.ts", "tests/corpus/node24-url-authority-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`href` Type: {string}", "`pathname` Type: {string}", "`search` Type: {string}", "`hash` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] },
{ "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] },
{ "chapter": "test", "parent": "`context.assert`", "symbols": ["context.assert.snapshot", "context.assert.fileSnapshot"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/snapshot.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.filePath", "passed", "attempt", "tags", "workerId"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-members.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `SuiteContext`", "symbols": ["context.name", "context.fullName", "context.filePath", "context.diagnostic", "passed", "attempt"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-context.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
{ "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": ["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/harness/project-config.test.ts"] },
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }
@@ -359,7 +377,7 @@
{ "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2631-create-require/main.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["console.count", "console.countReset", "console.group", "console.groupCollapsed", "console.groupEnd"], "status": "partial", "evidence": ["tests/corpus/5095-console-count-group.ts", "tests/corpus/5098-console-time-warnings.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["console.time", "console.timeEnd", "console.timeLog"], "status": "partial", "evidence": ["tests/corpus/5098-console-time-warnings.ts"] },
{ "symbols": ["console.assert", "console.dir", "console.dirxml"], "status": "partial", "evidence": ["tests/corpus/5096-console-assert-dir.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["console.assert", "console.dir", "console.dirxml"], "status": "partial", "evidence": ["tests/corpus/5096-console-assert-dir.ts", "tests/corpus/5099-console-methods-dynamic.cjs", "tests/corpus/5884-inspect-layout-options.ts"] },
{ "symbols": ["console.table"], "status": "partial", "evidence": ["tests/corpus/5097-console-table.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["console.clear", "console.profile", "console.profileEnd", "console.timeStamp"], "status": "partial", "evidence": ["tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["assert.AssertionError"], "status": "partial", "evidence": ["tests/corpus/5510-assert-error-properties.js", "tests/corpus/5511-assert-error-class.ts", "tests/corpus/5512-assert-error-options.js"] },
@@ -375,9 +393,9 @@
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "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"] },
{ "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": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.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"] },
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs"] },
+2
View File
@@ -35,6 +35,8 @@ requested output.
Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host.
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
Unsanitized `speed` program builds, and unsanitized `release` builds of programs small enough for one partition, pass `--import-bitcode <unit.bc>` once per runtime-pack unit the program links that ships bitcode. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
+181 -2
View File
@@ -10,9 +10,15 @@
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Analysis/RuntimeLibcallInfo.h"
#include "llvm/Analysis/TargetLibraryInfo.h"
#include "llvm/Bitcode/BitcodeReader.h"
#include "llvm/Bitcode/BitcodeWriter.h"
#include "llvm/Bitcode/BitcodeWriterPass.h"
#include "llvm/CodeGen/MachineDominators.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineModuleInfo.h"
#include "llvm/IR/DiagnosticInfo.h"
#include "llvm/IR/DiagnosticPrinter.h"
#include "llvm/IR/LegacyPassManager.h"
@@ -20,6 +26,9 @@
#include "llvm/IR/Verifier.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/LTO/LTO.h"
#include "llvm/Pass.h"
#include "llvm/PassInfo.h"
#include "llvm/PassRegistry.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/Caching.h"
#include "llvm/Support/FileSystem.h"
@@ -140,6 +149,138 @@ std::optional<EmitFailure> verify(Module &M, StringRef Code) {
return std::nullopt;
}
// Runs WebAssembly's pass that gives multiple-entry loops a single entry only
// on functions that have such a loop. The pass first records, for every
// block, every block it can reach, so its time and memory grow with the
// square of the block count even when it changes nothing: a module
// initializer of 40,000 blocks took four minutes and 22 GB in it. It changes
// a function only when two loop entries can reach each other, which a
// reducible control flow graph never has, so skipping reducible functions
// leaves the output unchanged.
class IrreducibleControlFlowOnly final : public MachineFunctionPass {
public:
static char ID;
explicit IrreducibleControlFlowOnly(FunctionPass *Fix)
: MachineFunctionPass(ID), Fix(Fix) {}
StringRef getPassName() const override { return Fix->getPassName(); }
bool doInitialization(Module &M) override {
MachineFunctionPass::doInitialization(M);
return Fix->doInitialization(M);
}
bool doFinalization(Module &M) override { return Fix->doFinalization(M); }
void getAnalysisUsage(AnalysisUsage &Usage) const override {
Fix->getAnalysisUsage(Usage);
}
bool runOnMachineFunction(MachineFunction &MF) override {
if (isReducible(MF))
return false;
// The wrapped pass reads the machine function through the module's
// machine code analysis, which this pass requires on its behalf.
if (Fix->getResolver() == nullptr) {
auto *Resolver = new AnalysisResolver(getResolver()->getPMDataManager());
Resolver->addAnalysisImplsPair(
&MachineModuleInfoWrapperPass::ID,
&getAnalysis<MachineModuleInfoWrapperPass>());
Fix->setResolver(Resolver);
}
return Fix->runOnFunction(MF.getFunction());
}
private:
// A graph is reducible when every edge that a depth-first search finds
// going back to a block on its stack targets a block that dominates the
// edge's source. Blocks the entry cannot reach count as irreducible, since
// the wrapped pass also examines them.
static bool isReducible(MachineFunction &MF) {
MachineDominatorTree Dominators(MF);
enum class State : unsigned char { New, Active, Done };
std::vector<State> States(MF.getNumBlockIDs(), State::New);
SmallVector<std::pair<MachineBasicBlock *,
MachineBasicBlock::succ_iterator>,
32>
Stack;
size_t Visited = 1;
MachineBasicBlock *Entry = &MF.front();
States[Entry->getNumber()] = State::Active;
Stack.push_back({Entry, Entry->succ_begin()});
while (!Stack.empty()) {
auto &[Block, Next] = Stack.back();
if (Next == Block->succ_end()) {
States[Block->getNumber()] = State::Done;
Stack.pop_back();
continue;
}
MachineBasicBlock *Successor = *Next++;
State &Seen = States[Successor->getNumber()];
if (Seen == State::Active) {
if (!Dominators.dominates(Successor, Block))
return false;
} else if (Seen == State::New) {
Seen = State::Active;
++Visited;
Stack.push_back({Successor, Successor->succ_begin()});
}
}
return Visited == MF.size();
}
std::unique_ptr<FunctionPass> Fix;
};
char IrreducibleControlFlowOnly::ID = 0;
// The code generation pipeline for one module, with two changes for
// WebAssembly. The irreducible control flow pass runs only where it can
// change the function (see IrreducibleControlFlowOnly). Object emission also
// leaves out the last machine pass, a module pass that records the external
// symbols every function uses so that assembly output can declare them
// before the first function. A module pass in the middle of the pipeline
// keeps the machine code of every function alive until all of them are
// compiled. Object files do not contain those declarations, and the printer
// creates each symbol when it lowers the first use, so without the pass each
// function's machine code is freed once it is printed.
class CodeGenPasses final : public legacy::PassManagerBase {
public:
CodeGenPasses(const TargetMachine &Machine, CodeGenFileType Type)
: Wasm(Machine.getTargetTriple().isWasm()),
Object(Type == CodeGenFileType::ObjectFile) {}
void add(Pass *P) override {
if (Wasm) {
StringRef Argument = passArgument(*P);
if (Argument == "wasm-fix-irreducible-control-flow") {
Passes.add(
new IrreducibleControlFlowOnly(static_cast<FunctionPass *>(P)));
return;
}
if (Object && Argument == "wasm-mclower-prepass") {
delete P;
return;
}
}
Passes.add(P);
}
void run(Module &M) { Passes.run(M); }
private:
static StringRef passArgument(const Pass &P) {
const PassInfo *Info =
PassRegistry::getPassRegistry()->getPassInfo(P.getPassID());
return Info == nullptr ? StringRef() : Info->getPassArgument();
}
bool Wasm;
bool Object;
legacy::PassManager Passes;
};
// Writes code for an optimized module to a unique sibling of OutputPath,
// which is appended to Temporaries once it exists.
std::optional<EmitFailure>
@@ -153,7 +294,7 @@ generateCode(Module &M, TargetMachine &Machine, CodeGenFileType Type,
return EmitFailure{"output_open_failed", EC.message()};
Temporaries.push_back(TemporaryPath);
raw_fd_ostream Output(TemporaryFd, true);
legacy::PassManager CodeGeneration;
CodeGenPasses CodeGeneration(Machine, Type);
if (Machine.addPassesToEmitFile(CodeGeneration, Output, nullptr, Type))
return EmitFailure{"emission_not_supported",
"target does not support the requested file type"};
@@ -325,7 +466,7 @@ compileUnoptimizedPartitions(const EmitOptions &Options,
P.PB.buildPerModuleDefaultPipeline(OptimizationLevel::O0)
.run(**Mod, P.MAM);
raw_svector_ostream Output(Objects[I]);
legacy::PassManager CodeGeneration;
CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
CodeGenFileType::ObjectFile))
return Failures.set({"emission_not_supported",
@@ -437,6 +578,44 @@ compileOptimizedPartitions(const EmitOptions &Options,
return true;
};
// The backend's own code generation cannot use CodeGenPasses, so
// WebAssembly partitions are compiled here with the passes it would add,
// and returning false skips it.
if (TargetTriple.isWasm())
Config.PreCodeGenModuleHook = [&](unsigned Task, const Module &Optimized) {
size_t I = Task - 1;
if (I >= Count)
return true;
std::string LookupError;
std::unique_ptr<TargetMachine> Machine =
createTargetMachine(Options.Target, Options.OptLevel, LookupError);
if (!Machine) {
Failures.set({"target_machine_failed", LookupError});
return false;
}
// The backend passes the module it owns; only the hook's signature is
// const.
Module &Mod = const_cast<Module &>(Optimized);
Objects[I].clear();
raw_svector_ostream Output(Objects[I]);
CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
TargetLibraryInfoImpl LibraryInfo(Mod.getTargetTriple(),
Machine->Options.VecLib);
CodeGeneration.add(new TargetLibraryInfoWrapperPass(LibraryInfo));
CodeGeneration.add(new RuntimeLibraryInfoWrapper(
Mod.getTargetTriple(), Machine->Options.ExceptionModel,
Machine->Options.FloatABIType, Machine->Options.EABIVersion,
Machine->Options.MCOptions.ABIName, Machine->Options.VecLib));
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
CodeGenFileType::ObjectFile)) {
Failures.set({"emission_not_supported",
"target does not support object emission"});
return false;
}
CodeGeneration.run(Mod);
return false;
};
// The link refers to module identifiers for its whole lifetime.
std::vector<std::string> Names(Count);
for (size_t I = 0; I < Count; ++I)
+171 -8
View File
@@ -708,6 +708,7 @@ interface MessageEvent<T = unknown> extends Event {
readonly data: T;
readonly origin: string;
readonly lastEventId: string;
readonly source: unknown;
readonly ports: readonly MessagePort[];
}
declare var MessageEvent: {
@@ -2109,6 +2110,8 @@ declare module "os" {
export function version(): string;
export function endianness(): "BE" | "LE";
export function getPriority(pid?: number): number;
export function setPriority(priority: number): void;
export function setPriority(pid: number, priority: number): void;
/* The TARGET platform's null device ("\\\\.\\nul" under a win32 triple). */
export const devNull: string;
/* The platform's constants: null-prototype objects with read-only
@@ -3030,10 +3033,10 @@ declare module "child_process" {
args?: string[] | null,
options?: ExecSyncOptions,
): unknown;
/* The async shell form — declared surface without full lowering (the
* type-level tolerance story): harness code on paths tests don't reach
* must typecheck; a reached call fences at its site. util.promisify
* recognizes it statically, as it does execFile. */
/* The async shell form. The callback forms lower like execFile's, with
* the command line run by /bin/sh -c, and util.promisify recognizes it
* statically, as it does execFile; reached calls without a callback
* fence at their site. */
export function exec(
command: string,
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
@@ -3171,6 +3174,69 @@ declare module "util" {
export function promisify(
fn: typeof import("node:stream").finished,
): typeof import("node:stream/promises").finished;
/* The crypto callback functions: the promise settles with the callback's
* value. */
export function promisify(
fn: typeof import("node:crypto").randomBytes,
): (size: number) => Promise<Buffer>;
export function promisify(
fn: typeof import("node:crypto").pbkdf2,
): (
password: string | Uint8Array,
salt: string | Uint8Array,
iterations: number,
keylen: number,
digest: string,
) => Promise<Buffer>;
export function promisify(
fn: typeof import("node:crypto").scrypt,
): (
password: import("node:crypto").BinaryLike,
salt: import("node:crypto").BinaryLike,
keylen: number,
options?: import("node:crypto").ScryptOptions,
) => Promise<Buffer>;
export function promisify(
fn: typeof import("node:crypto").hkdf,
): (
digest: string,
ikm: import("node:crypto").BinaryLike | import("node:crypto").KeyObject,
salt: import("node:crypto").BinaryLike,
info: import("node:crypto").BinaryLike,
keylen: number,
) => Promise<ArrayBuffer>;
export function promisify(
fn: typeof import("node:crypto").randomFill,
): <T extends ArrayBufferView | ArrayBufferLike>(
buffer: T,
offset?: number,
size?: number,
) => Promise<T>;
export function promisify(fn: typeof import("node:crypto").randomInt): {
(max: number): Promise<number>;
(min: number, max: number): Promise<number>;
};
export function promisify(
fn: typeof import("node:crypto").generateKey,
): (
type: "hmac" | "aes",
options: { length: number },
) => Promise<import("node:crypto").KeyObject>;
/* dns.lookup's promise settles with { address, family } (the address
* array with options.all), and lookupService's with { hostname, service }. */
export function promisify(fn: typeof import("node:dns").lookup): {
(
hostname: string,
options: import("node:dns").LookupAllOptions,
): Promise<import("node:dns").LookupAddress[]>;
(
hostname: string,
options?: number | import("node:dns").LookupOneOptions,
): Promise<import("node:dns").LookupAddress>;
};
export function promisify(
fn: typeof import("node:dns").lookupService,
): (address: string, port: number) => Promise<{ hostname: string; service: string }>;
/* Declared surface without full lowering (the type-level tolerance
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
* without a lowering fences at its site). inspect's options carry the
@@ -3250,6 +3316,7 @@ declare module "util/types" {
export function isInt8Array(value: unknown): value is Int8Array;
export function isInt16Array(value: unknown): value is Int16Array;
export function isInt32Array(value: unknown): value is Int32Array;
export function isFloat16Array(value: unknown): value is Float16Array;
export function isFloat32Array(value: unknown): value is Float32Array;
export function isFloat64Array(value: unknown): value is Float64Array;
export function isMap(value: unknown): value is Map<unknown, unknown>;
@@ -3264,6 +3331,21 @@ declare module "util/types" {
export function isGeneratorFunction(value: unknown): boolean;
export function isGeneratorObject(value: unknown): boolean;
export function isModuleNamespaceObject(value: unknown): boolean;
export function isDate(value: unknown): value is Date;
export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer;
export function isMapIterator(value: unknown): boolean;
export function isSetIterator(value: unknown): boolean;
export function isKeyObject(value: unknown): boolean;
export function isNumberObject(value: unknown): boolean;
export function isStringObject(value: unknown): boolean;
export function isBooleanObject(value: unknown): boolean;
export function isBigIntObject(value: unknown): boolean;
export function isSymbolObject(value: unknown): boolean;
export function isBoxedPrimitive(value: unknown): boolean;
export function isBigInt64Array(value: unknown): value is BigInt64Array;
export function isBigUint64Array(value: unknown): value is BigUint64Array;
export function isCryptoKey(value: unknown): boolean;
export function isExternal(value: unknown): boolean;
}
declare module "node:util/types" {
export * from "util/types";
@@ -3293,8 +3375,18 @@ declare module "node:util/types" {
* suites typecheck but fence at their use sites (no lowering). */
declare module "node:test" {
type TestFn = (t: TestContext) => void | Promise<void>;
type SuiteFn = () => void;
type HookFn = () => void | Promise<void>;
type SuiteFn = (s: SuiteContext) => void;
type HookFn = (c: TestContext | SuiteContext) => void | Promise<void>;
/* The describe/suite callback's context, and the before/after hook
* context of a suite. */
interface SuiteContext {
readonly name: string;
readonly fullName: string;
readonly filePath: string | undefined;
readonly attempt: number;
readonly passed: boolean;
diagnostic(message: string): void;
}
interface TestOptions {
skip?: boolean | string;
todo?: boolean | string;
@@ -3306,6 +3398,11 @@ declare module "node:test" {
interface TestContext {
readonly name: string;
readonly fullName: string;
readonly filePath: string | undefined;
readonly attempt: number;
readonly passed: boolean;
readonly workerId: number | undefined;
readonly tags: readonly string[];
readonly assert: TestContextAssert;
readonly mock: MockTracker;
test(name: string, fn?: TestFn): Promise<void>;
@@ -3319,7 +3416,12 @@ declare module "node:test" {
beforeEach(fn: HookFn): void;
afterEach(fn: HookFn): void;
}
interface AssertSnapshotOptions {
readonly serializers?: ReadonlyArray<(value: any) => any> | undefined;
}
interface TestContextAssert {
snapshot(value: any, options?: AssertSnapshotOptions): void;
fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void;
ok(value: unknown, message?: string): void;
strictEqual(actual: unknown, expected: unknown, message?: string): void;
notStrictEqual(actual: unknown, expected: unknown, message?: string): void;
@@ -3410,6 +3512,19 @@ declare module "node:test" {
: never;
reset(): void;
restoreAll(): void;
readonly timers: MockTimers;
}
interface MockTimersOptions {
apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">;
now?: number | Date | undefined;
}
interface MockTimers {
enable(options?: MockTimersOptions): void;
setTime(milliseconds?: number): void;
reset(): void;
tick(milliseconds?: number): void;
runAll(): void;
[Symbol.dispose](): void;
}
const mock: MockTracker;
/* Declared for typechecking only — every use fences (no lowering).
@@ -3432,11 +3547,14 @@ declare module "node:test" {
run,
snapshot,
TestContext,
SuiteContext,
TestOptions,
Mock,
MockFunctionContext,
MockFunctionCall,
MockTracker,
MockTimers,
MockTimersOptions,
};
export default test;
}
@@ -3618,7 +3736,8 @@ declare module "querystring" {
| string
| number
| boolean
| ReadonlyArray<string | number | boolean>
| bigint
| ReadonlyArray<string | number | boolean | bigint>
| null
| undefined;
}
@@ -5806,19 +5925,42 @@ declare module "worker_threads" {
export const isMainThread: boolean;
export const isInternalThread: boolean;
export const threadId: number;
export interface ResourceLimits {
maxYoungGenerationSizeMb?: number;
maxOldGenerationSizeMb?: number;
codeRangeSizeMb?: number;
stackSizeMb?: number;
}
/* {} in the main thread */
export const resourceLimits: ResourceLimits;
/* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */
export const threadName: string | null;
export interface WorkerOptions {
workerData?: unknown;
transferList?: readonly unknown[];
argv?: readonly unknown[];
name?: string;
resourceLimits?: ResourceLimits;
stdout?: boolean;
stderr?: boolean;
stdin?: boolean;
[option: string]: unknown;
}
export class Worker {
constructor(filename: string | URL, options?: WorkerOptions);
readonly threadId: number;
readonly resourceLimits?: ResourceLimits;
readonly threadName: string | null;
/* with the stdout/stderr options: the worker's writes to those streams */
readonly stdout: import("stream").Readable;
readonly stderr: import("stream").Readable;
/* with the stdin option: what the worker reads from process.stdin */
readonly stdin: import("stream").Writable | null;
/* terminate(), resolving with undefined once the worker exited */
[Symbol.asyncDispose](): Promise<void>;
on(event: "online", listener: () => void): this;
on(event: "message", listener: (value: any) => void): this;
on(event: "error", listener: (error: Error) => void): this;
on(event: "error", listener: (error: any) => void): this;
on(event: "exit", listener: (code: number) => void): this;
on(event: string, listener: (...args: any[]) => void): this;
once(event: string, listener: (...args: any[]) => void): this;
@@ -5860,6 +6002,27 @@ declare module "worker_threads" {
readonly port2: MessagePort;
}
export function receiveMessageOnPort(port: MessagePort): { message: any } | undefined;
/* This thread's environment data; each new Worker starts from a clone. */
export function setEnvironmentData(key: any, value?: any): void;
export function getEnvironmentData(key: any): any;
/* A marked object cannot be listed in a transfer list. */
export function markAsUntransferable(object: any): void;
/* structuredClone and postMessage throw DataCloneError for a marked
* object, error or class instance */
export function markAsUncloneable(object: any): void;
export function isMarkedAsUntransferable(object: any): boolean;
/* Delivers each message to every other open channel with the name, in
* any thread, as a 'message' MessageEvent. */
export class BroadcastChannel extends EventTarget {
constructor(name: string);
readonly name: string;
onmessage: ((event: MessageEvent) => void) | null;
onmessageerror: ((event: MessageEvent) => void) | null;
postMessage(message: unknown): void;
close(): void;
ref(): this;
unref(): this;
}
export const parentPort: MessagePort | null;
export const workerData: any;
}
@@ -513,6 +513,19 @@ test("a subclass constructor runs after its base constructor's escape", () => {
checkExact(mod, ["object:Base", "object:Sub"], ["link"]);
});
test("a base without a constructor function lets nothing escape", () => {
// Instantiations of a generic class name the generic class as their base,
// which has no constructor function of its own.
const mod = module();
mod.classes = [
{ name: "Chain", fields: [{ name: "next", type: link }], loc },
{ name: "Chain%0", base: "Chain", fields: [{ name: "next", type: link }], loc },
];
mod.unions = [{ id: "link", arms: [obj("Chain%0"), { kind: "nullT" }] }];
mod.functions.push(ctor("Chain%0", [setThis("Chain%0", "next", link)]));
checkExact(mod, []);
});
test("field reads of this inside the constructor do not escape", () => {
const read: IrStmt = {
kind: "fieldSet",
@@ -216,8 +216,13 @@ export function computeCycleMutability(
escapePoints.set(name, point);
return point;
};
const escapes = (name: string): boolean =>
escapePoint(name) < (fnByName.get(`%${name}.constructor`)?.body.length ?? 1);
const escapes = (name: string): boolean => {
const fn = fnByName.get(`%${name}.constructor`);
// A program class without a constructor function runs no code at construction. Generic
// classes are such bases: their instantiations name them as their base.
if (fn === undefined && classByName.get(name)?.runtime !== true) return false;
return escapePoint(name) < (fn?.body.length ?? 1);
};
const ancestorEscaped = (name: string): boolean => {
for (let c = classByName.get(name)?.base; c !== undefined; c = classByName.get(c)?.base)
if (escapes(c)) return true;
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { BlockBuilder } from "./blocks.js";
import { BlockBuilder, volatileSlotAccess } from "./blocks.js";
import { f64Lit } from "./common.js";
test("entry allocations precede instructions and temporaries stay unique", () => {
@@ -106,3 +106,34 @@ test("inclusive loops and nested loops keep labels separate", () => {
expect(text).toContain("fcmp ole double");
expect(text.match(/alloca double/g)).toHaveLength(2);
});
test("volatile slots render their loads and stores volatile and nothing else", () => {
const slots = new Set(["%sc_l_a_0"]);
expect(volatileSlotAccess(" %t1 = load ptr, ptr %sc_l_a_0 ; a", slots)).toBe(
" %t1 = load volatile ptr, ptr %sc_l_a_0 ; a",
);
expect(volatileSlotAccess(" store ptr %t1, ptr %sc_l_a_0, !dbg !7", slots)).toBe(
" store volatile ptr %t1, ptr %sc_l_a_0, !dbg !7",
);
expect(volatileSlotAccess(" store ptr null, ptr %sc_l_a_0", slots)).toBe(
" store volatile ptr null, ptr %sc_l_a_0",
);
// Other addresses, a slot stored as a value, prefixes and other opcodes.
for (const line of [
" %t2 = load ptr, ptr %sc_l_a_01",
" store ptr %sc_l_a_0, ptr %sc_l_b_0",
" %t3 = getelementptr inbounds ptr, ptr %sc_l_a_0, i64 1",
" call void @scr_box_release(ptr %sc_l_a_0)",
" %t4 = load volatile ptr, ptr %sc_l_a_0",
])
expect(volatileSlotAccess(line, slots)).toBe(line);
const blocks = new BlockBuilder();
blocks.volatileSlots = slots;
blocks.entryAllocas.push("%sc_l_a_0 = alloca ptr");
blocks.line("store ptr null, ptr %sc_l_a_0");
blocks.terminate("ret void");
expect(blocks.render()).toBe(
"entry:\n %sc_l_a_0 = alloca ptr\n store volatile ptr null, ptr %sc_l_a_0\n ret void",
);
});
+42 -1
View File
@@ -31,6 +31,9 @@ export class BlockBuilder {
/** Temps known to hold immortal values (temp names are only unique
* within one builder). */
readonly immortalTemps = new Set<string>();
/** Entry slots whose loads and stores render volatile, so no LLVM pass
* promotes them to SSA values (see volatileSlotAccess). */
volatileSlots: ReadonlySet<string> | null = null;
constructor() {
this.cur = { label: "entry", lines: [], term: null };
@@ -130,9 +133,14 @@ export class BlockBuilder {
}
render(): string {
const slots = this.volatileSlots;
return this.blocks
.map((b, i) => {
const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...b.lines] : b.lines;
const body =
slots === null || slots.size === 0
? b.lines
: b.lines.map((line) => volatileSlotAccess(line, slots));
const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...body] : body;
// A block left unterminated is a structurally unreachable join
// (every predecessor jumped elsewhere); the verifier still wants a
// terminator.
@@ -142,3 +150,36 @@ export class BlockBuilder {
.join("\n");
}
}
/** `line` with its load or store made volatile when the address operand is
* one of `slots`. Lines are this builder's own instruction text, where the
* address is the first `ptr %name` operand after the opcode (a stored value
* precedes it without a comma). */
export function volatileSlotAccess(line: string, slots: ReadonlySet<string>): string {
let op = 0;
while (line.charCodeAt(op) === 32) op++;
if (line.charCodeAt(op) === 37) {
const assign = line.indexOf(" = ", op);
if (assign < 0) return line;
op = assign + 3;
}
const kind = line.startsWith("load ", op) ? 5 : line.startsWith("store ", op) ? 6 : 0;
if (kind === 0 || line.startsWith("volatile ", op + kind)) return line;
const address = line.indexOf(", ptr %", op + kind);
if (address < 0) return line;
const start = address + 6;
let end = start + 1;
while (end < line.length && isNameCode(line.charCodeAt(end))) end++;
if (!slots.has(line.slice(start, end))) return line;
return `${line.slice(0, op + kind)}volatile ${line.slice(op + kind)}`;
}
function isNameCode(code: number): boolean {
return (
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code === 95 ||
code === 46
);
}
@@ -182,7 +182,11 @@ export function emitByteNumber(
const integerType = bits <= 32 ? "i32" : "i64";
if (spec.write) {
let raw: string;
if (spec.floating) {
if (spec.floating && bits === 16) {
host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
raw = B.tmp();
B.line(`${raw} = call zeroext i16 @scr_f16_from_f64(double ${value!.name})`);
} else if (spec.floating) {
let stored = value!.name;
if (bits === 32) {
stored = B.tmp();
@@ -228,7 +232,10 @@ export function emitByteNumber(
const raw = endianBits(host, loaded, spec.width, little);
fastResult = B.tmp();
if (spec.floating) {
if (bits === 32) {
if (bits === 16) {
host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`);
B.line(`${fastResult} = call double @scr_f16_to_f64(i16 zeroext ${raw})`);
} else if (bits === 32) {
const float = B.tmp();
B.line(`${float} = bitcast i32 ${raw} to float`);
B.line(`${fastResult} = fpext float ${float} to double`);
@@ -178,6 +178,12 @@ function collectUses(
)
return true;
break;
case "recordClone":
// A same-shape spread copies the source's fields (retaining the
// references) and keeps no reference to the source itself.
if (node.source.kind !== "varRef") break;
for (const field of node.overrides) expr(field.value);
return true;
case "recordLit":
if (node.type.kind !== "record") break;
for (const field of node.fields)
@@ -159,6 +159,7 @@ export const BYTES_ELEM_NUM: Record<IrBytesElem, number> = {
u16: 6,
i16: 7,
u8c: 8,
f16: 9,
};
/** The payload marker of an ABSENT record field slot (IR fieldAbsent): the
@@ -3,6 +3,7 @@ import {
BOOL,
F64,
STRING,
UNDEFINED_T,
VOID,
arrayOf,
type IrExpr,
@@ -23,11 +24,17 @@ const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
});
const index = ref("i", F64);
function module(name: string, returnType: IrExpr["type"], body: IrStmt[]): IrModule {
function module(
name: string,
returnType: IrExpr["type"],
body: IrStmt[],
unions: IrModule["unions"] = [],
): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "__main",
unions,
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
{
@@ -174,6 +181,76 @@ test.each([32, 64] as const)(
},
);
test.each([32, 64] as const)(
"pop removes the last element of a packed array inline and keeps a cold runtime pop (%i-bit)",
(bits) => {
const maybe = { kind: "union", unionId: "maybe" } as const;
const pop: IrExpr = {
kind: "arrIntrinsic",
method: "pop",
receiver: ref("a", nums),
args: [],
type: maybe,
loc,
};
const llvm = emitLlvmModule(
module(
"take",
maybe,
[{ kind: "return", value: pop, loc }],
[{ id: "maybe", arms: [F64, UNDEFINED_T] }],
),
{ pointerBits: bits },
);
const fn = body(llvm, "take");
const size = bits === 32 ? "i32" : "i64";
// An empty array answers undefined without a call.
expect(fn).toMatch(new RegExp(`icmp eq ${size} %t\\d+, 0\\n br i1 %t\\d+, label %remove\\.a`));
expect(fn).toMatch(new RegExp(`sub nuw ${size} %t\\d+, 1`));
expect(fn).toMatch(/store i64 0, ptr /);
expect(fn).toMatch(/store i8 0, ptr /);
expect(fn).toMatch(/phi i64 /);
expect(fn).toMatch(/call zeroext i8 @scr_arr_pop_state\(.*\) cold$/m);
},
);
test("a discarded pop releases the removed element inline", () => {
const words = ref("s", arrayOf(STRING));
const length: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: words,
args: [],
type: F64,
loc,
};
const llvm = emitLlvmModule(
module("drop", VOID, [
{
kind: "arraySetLength",
arr: words,
length: {
kind: "bin",
op: "-",
left: length,
right: { kind: "numLit", value: 1, type: F64, loc },
type: F64,
loc,
},
loc,
pop: true,
},
]),
);
const fn = body(llvm, "drop");
expect(fn).toMatch(/fsub double %t\d+, 1\.0/);
expect(fn).toMatch(/load i64, ptr .*\n(?:.*\n)*? store i64 0, ptr /);
expect(fn).toMatch(/inttoptr i64 %t\d+ to ptr/);
expect(fn).toContain("call void @scr_str_release(");
// Holes, sparse storage, and every other length keep the runtime call.
expect(fn).toContain("call void @scr_arr_set_len(");
});
test.each([32, 64] as const)(
"indices below the hole-free prefix bound skip the state bytes (%i-bit)",
(bits) => {
@@ -47,7 +47,8 @@ interface DenseGuard {
* then extends the length over hole states exactly the same way). An index
* below the hole-free prefix bound branches to `filledLabel` first; both
* that block and the returned (state-checked) dense block may use the
* guard's values. */
* guard's values. A `proven` index is known to lie below the bound (a dense
* array loop's fast copy, dense-array-loops.ts) and always takes it. */
function emitDenseGuard(
host: LlvmEmitterContext,
array: string,
@@ -56,6 +57,7 @@ function emitDenseGuard(
slow: string,
requireBelowLen: boolean,
filledLabel: string,
proven = false,
): DenseGuard {
const B = host.B;
const size = host.sizeType;
@@ -137,8 +139,10 @@ function emitDenseGuard(
B.line(`${offset} = trunc i64 ${wide} to i32`);
}
// The bound never exceeds the length or the dense capacity.
let inFilled = B.tmp();
B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
// The proof covers the counter's integer value only.
const below = proven && integral === null;
let inFilled = below ? "true" : B.tmp();
if (!below) B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
if (integral) {
const t = B.tmp();
B.line(`${t} = and i1 ${integral}, ${inFilled}`);
@@ -272,6 +276,7 @@ export function emitDenseArrayGet(
element: IrType,
getter: string,
retain: boolean,
proven = false,
): string {
const B = host.B;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
@@ -285,7 +290,7 @@ export function emitDenseArrayGet(
const slow = B.newLabel("arr.get.slow"),
join = B.newLabel("arr.get.join"),
valueLabel = B.newLabel("arr.get.value");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel);
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel, proven);
const state = loadState(host, guard);
const present = B.tmp();
B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
@@ -331,6 +336,7 @@ export function emitDenseArrayNumber(
array: string,
index: LlValue,
indexExpr: IrExpr | undefined,
proven = false,
): string {
return emitDenseArrayGet(
host,
@@ -341,6 +347,7 @@ export function emitDenseArrayNumber(
{ kind: "f64" },
"scr_arr_get_number",
false,
proven,
);
}
@@ -399,6 +406,7 @@ export function emitDenseArraySet(
acc: ArrayAccess,
element: IrType,
value: string,
proven = false,
): void {
const B = host.B;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
@@ -413,7 +421,7 @@ export function emitDenseArraySet(
const slow = B.newLabel("arr.set.slow"),
join = B.newLabel("arr.set.join"),
filledLabel = B.newLabel("arr.set.filled");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel);
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel, proven);
const { offset, len } = guard;
const storeValue = (valuePtr: string): void => {
if (acc === "f64") {
@@ -508,6 +516,75 @@ export function emitDenseArraySet(
B.startBlock(join);
}
/** The dense half of `pop`. An empty array branches to `empty` (the
* runtime's pop returns a hole state there and changes nothing). When the
* hole-free prefix bound equals the length, the last element is a present
* value in dense storage, and the runtime's pop is exactly: clear its slot
* and state, lower the bound and the length to its index, and move the
* slot out. That block is left open and its slot bits are returned as an
* i64 (a boolean's 0 or 1, zero-extended); every other array branches to
* `slow`, which keeps the runtime call. */
export function emitDenseArrayPop(
host: LlvmEmitterContext,
array: string,
acc: ArrayAccess,
empty: string,
slow: string,
): string {
const B = host.B;
const size = host.sizeType;
const header = (field: number): string => {
const ptr = B.tmp();
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
const lenPtr = header(SCR_ARR_FIELD.len),
len = B.tmp(),
none = B.tmp();
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(`${none} = icmp eq ${size} ${len}, 0`);
const check = B.newLabel("arr.pop.check"),
dense = B.newLabel("arr.pop.dense");
B.condBr(none, empty, check);
B.startBlock(check);
const filledPtr = header(SCR_ARR_FIELD.filled),
filled = B.tmp(),
packed = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${packed} = icmp eq ${size} ${sizedFilled(host, filled)}, ${len}`);
B.condBr(packed, dense, slow);
B.startBlock(dense);
const last = B.tmp();
B.line(`${last} = sub nuw ${size} ${len}, 1`);
const dataPtr = header(SCR_ARR_FIELD.data),
data = B.tmp(),
statesPtr = header(SCR_ARR_FIELD.present),
states = B.tmp();
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
const valuePtr = valuePointer(host, { offset: last, data }, acc);
const statePtr = statePointer(host, { offset: last, states });
const slotTy = slotType(acc);
let raw = B.tmp();
B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
if (slotTy === "i8") {
const wide = B.tmp();
B.line(`${wide} = zext i8 ${raw} to i64`);
raw = wide;
}
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
let last32 = last;
if (size === "i64") {
last32 = B.tmp();
B.line(`${last32} = trunc i64 ${last} to i32`);
}
B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
return raw;
}
/** `scr_arr_push_*` (the array takes the value's reference) returning the
* new length as a double. When `len < cap` the runtime's append is exactly
* a dense store at `len` plus `len + 1`: no sparse entry can exist past the
@@ -0,0 +1,147 @@
import { expect, test } from "vitest";
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
import {
countedDenseShape,
matchDenseArrayLoop,
type DenseLoopShape,
} from "./dense-array-loops.js";
const loc = { file: "dense-array-loops.ts", start: 0, end: 0 };
const ARR = arrayOf(F64);
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
const arr = (localId: string): IrExpr & { kind: "varRef" } => ({
kind: "varRef",
localId,
type: ARR,
loc,
});
const method = (
receiver: string,
name: Extract<IrExpr, { kind: "arrIntrinsic" }>["method"],
args: IrExpr[] = [],
): IrExpr => ({
kind: "arrIntrinsic",
method: name,
receiver: arr(receiver),
args,
type: F64,
loc,
});
const read = (receiver: string, index = ref("i")): IrExpr => method(receiver, "getNumber", [index]);
const write = (receiver: string, value: IrExpr, index = ref("i")): IrStmt => ({
kind: "arraySet",
arr: arr(receiver),
index,
value,
loc,
});
const locals = new Map<string, IrLocal>([
["i", { id: "i", name: "i", type: F64, mutable: true }],
["a", { id: "a", name: "a", type: ARR, mutable: false }],
["b", { id: "b", name: "b", type: ARR, mutable: true }],
["c", { id: "c", name: "c", type: ARR, mutable: false }],
]);
/** for (let i = 0; i < a.length; i++) body */
function loop(body: IrStmt[]): IrStmt & { kind: "for" } {
return {
kind: "for",
loc,
init: { kind: "varDecl", localId: "i", init: num(0), loc },
cond: { kind: "bin", op: "<", left: ref("i"), right: method("a", "length"), type: BOOL, loc },
update: {
kind: "assign",
localId: "i",
value: { kind: "bin", op: "+", left: ref("i"), right: num(1), type: F64, loc },
loc,
},
body,
};
}
const shape: DenseLoopShape = {
counter: "i",
length: arr("a"),
range: { min: 0, max: 4294967294 },
};
const match = (body: IrStmt[], initialized = new Set(["a", "b"])) =>
matchDenseArrayLoop(loop(body), shape, locals, new Set(), initialized);
test("arrays indexed at the counter of a length-preserving loop get an int32 fast copy", () => {
const scale = write("b", {
kind: "bin",
op: "*",
left: read("a"),
right: num(2),
type: F64,
loc,
});
const dense = match([scale]);
expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]);
expect(dense!.range).toEqual({ min: 0, max: 2147483646 });
// A binding not definitely assigned before the loop is not read by the guard.
expect([...match([scale], new Set())!.arrays.keys()]).toEqual(["a"]);
});
test("loops that may resize, rebind or move storage keep the ordinary copy only", () => {
expect(match([{ kind: "exprStmt", expr: method("a", "push", [num(1)]), loc }])).toBeNull();
expect(match([write("a", num(0), ref("unknown"))])).toBeNull();
expect(
match([
{ kind: "exprStmt", expr: { kind: "call", callee: "f", args: [], type: F64, loc }, loc },
]),
).toBeNull();
// A nested loop would be duplicated too.
expect(match([{ kind: "exprStmt", expr: read("a"), loc }, loop([])])).toBeNull();
// No access at the counter: nothing to prove.
expect(match([{ kind: "exprStmt", expr: read("a", num(0)), loc }])).toBeNull();
// Copying the bounding array into another binding lets it escape.
expect(
match([
{ kind: "exprStmt", expr: read("a"), loc },
{ kind: "assign", localId: "b", value: arr("a"), loc },
]),
).toBeNull();
// A rebound array is not covered, but the bounding array still is.
const rebound = match([
{ kind: "exprStmt", expr: read("b"), loc },
{ kind: "exprStmt", expr: read("a"), loc },
{ kind: "assign", localId: "b", value: arr("c"), loc },
]);
expect([...rebound!.arrays.keys()]).toEqual(["a"]);
});
test("counted shapes need a non-negative counter strictly below the length", () => {
const ascending = (limit: IrExpr, inclusive: boolean, min = 0) =>
countedDenseShape(loop([]), {
localId: "i",
start: num(min),
limit,
step: 1,
inclusive,
range: { min, max: 100 },
});
const minus = (offset: number): IrExpr => ({
kind: "bin",
op: "-",
left: method("a", "length"),
right: num(offset),
type: F64,
loc,
});
expect(ascending(method("a", "length"), false)?.length.localId).toBe("a");
expect(ascending(minus(1), true)?.length.localId).toBe("a");
expect(ascending(method("a", "length"), true)).toBeNull();
expect(ascending(minus(1), false, -1)).toBeNull();
const descending = (offset: number) =>
countedDenseShape(loop([]), {
localId: "i",
start: minus(offset),
limit: num(0),
step: -1,
inclusive: true,
range: { min: 0, max: 100 },
});
expect(descending(1)?.length.localId).toBe("a");
expect(descending(0)).toBeNull();
});
@@ -0,0 +1,215 @@
/* Dense array loops: a versioned copy of an index loop over an array whose
* length the loop never changes.
*
* `for (let i = 0; i < a.length; i++) b[i] = a[i] * 2` reads its bound from
* `a`, and every element access at the counter normally tests the index
* against the array's hole-free prefix bound (`filled`, see
* dense-array-access.ts) before it touches the slot. When the loop provably
* never changes `a.length` (ir/array-length-stability.ts) and never moves or
* cuts the storage of the arrays it indexes, one test before the loop covers
* every pass: if `a.length` is at most each indexed array's `filled` bound,
* every counter value the loop can reach (at least 0, below `a.length`)
* names a present value in dense storage. The fast copy then reads and
* replaces those slots directly, which leaves LLVM a loop without
* per-element branches (vectorizable where the body allows it). The same
* test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32.
* Arrays longer than that are sparse in practice (a dense one would need
* 16 GB of slots) and take the ordinary copy, as does every array with a
* hole, present undefined or sparse storage below `a.length`. */
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
import type { IntegerRange } from "../../ir/integer-ranges.js";
import { preservesArrayLength } from "../../ir/array-length-stability.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { SCR_ARR_FIELD } from "./common.js";
export const INT32_MAX = 2147483647;
export interface DenseArrayLoop {
/** The loop's counter binding. */
counter: string;
/** The array whose length bounds the counter. */
length: IrExpr & { kind: "varRef" };
/** Arrays whose accesses at the counter are proven to be inside the
* hole-free prefix in the fast copy, with a read of each binding. */
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
/** The counter's range on every pass of the fast copy. */
range: IntegerRange;
}
/** The fast copy duplicates the loop; keep the duplicated code small. */
const MAX_LOOP_NODES = 120;
/** How a matched integer loop bounds its counter below `length.length`. */
export interface DenseLoopShape {
counter: string;
length: IrExpr & { kind: "varRef" };
/** The counter's proven range on every pass of the ordinary loop; the
* fast copy keeps its minimum, which must be at least 0. */
range: IntegerRange;
}
/** `length.length - c` (c an integer literal, 0 when absent). */
function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offset: number } | null {
let offset = 0;
if (
e.kind === "bin" &&
e.op === "-" &&
e.right.kind === "numLit" &&
Number.isSafeInteger(e.right.value)
) {
offset = e.right.value;
e = e.left;
}
return e.kind === "arrIntrinsic" &&
e.method === "length" &&
e.args.length === 0 &&
e.receiver.kind === "varRef" &&
e.receiver.type.kind === "array"
? { receiver: e.receiver, offset }
: null;
}
/** The array bounding a counted loop's counter below its length: an
* ascending loop compared below `a.length - c`, or a descending one starting
* at `a.length - c` (c >= 1). */
export function countedDenseShape(
loop: IrStmt & { kind: "for" },
counted: {
localId: string;
start: IrExpr;
limit: IrExpr;
step: number;
inclusive: boolean;
range: IntegerRange;
},
): DenseLoopShape | null {
if (counted.range.min < 0) return null;
if (counted.step > 0) {
const bound = lengthOffset(counted.limit);
if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null;
return { counter: counted.localId, length: bound.receiver, range: counted.range };
}
// The start is evaluated once, before the loop; the counter only falls.
const start = lengthOffset(counted.start);
if (!start || start.offset < 1 || loop.init?.kind !== "varDecl") return null;
return { counter: counted.localId, length: start.receiver, range: counted.range };
}
/** Match the fast copy of a loop of the given shape. `initialized` holds the
* bindings definitely assigned when the loop starts; `unaliased` the local
* arrays no other binding holds. */
export function matchDenseArrayLoop(
loop: IrStmt & { kind: "for" },
shape: DenseLoopShape,
locals: ReadonlyMap<string, IrLocal>,
unaliased: ReadonlySet<string>,
initialized: ReadonlySet<string>,
): DenseArrayLoop | null {
const { counter, length } = shape;
if (shape.range.min < 0 || !loop.cond) return null;
let nodes = 0,
nested = false;
const candidates = new Map<string, IrExpr & { kind: "varRef" }>();
// Arrays written, resized, or indexed other than at the counter by an
// element write.
const disturbed = new Set<string>();
const access = (arr: IrExpr, index: IrExpr): void => {
if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter)
candidates.set(arr.localId, arr);
};
const expr = (e: IrExpr): boolean => {
if (++nodes > MAX_LOOP_NODES) return false;
if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId);
if (e.kind === "arrayGet") access(e.arr, e.index);
if (e.kind === "arrIntrinsic") {
if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]);
else if (e.method !== "length" && e.receiver.kind === "varRef")
disturbed.add(e.receiver.localId);
}
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => {
if (++nodes > MAX_LOOP_NODES) return false;
if (s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf")
nested = true;
if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId);
if (s.kind === "arraySet") {
access(s.arr, s.index);
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter))
disturbed.add(s.arr.localId);
}
return everyStmtChild(s, expr, stmt);
};
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
if (
!preservesArrayLength(
[loop.cond, ...loop.body],
{ receiver: length.localId, inRange: counter },
locals,
unaliased,
)
)
return null;
const arrays = new Map<string, IrExpr & { kind: "varRef" }>();
for (const [id, ref] of candidates) {
const local = locals.get(id);
if (
!local ||
local.type.kind !== "array" ||
local.boxed ||
local.tdz ||
disturbed.has(id) ||
(id !== length.localId && !initialized.has(id))
)
continue;
arrays.set(id, ref);
}
if (arrays.size === 0) return null;
return {
counter,
length,
arrays,
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
};
}
/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and
* at most every indexed array's hole-free prefix bound. */
export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string {
const B = host.B;
const size = host.sizeType;
const header = (array: string, field: number): string => {
const ptr = B.tmp();
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
const receiver = host.emitStableReceiver(loop.length, []);
const lenPtr = header(receiver.name, 1),
len = B.tmp(),
small = B.tmp();
B.line(
`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
);
B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`);
let valid = small;
for (const [id, ref] of loop.arrays) {
const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []);
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
filled = B.tmp(),
covered = B.tmp(),
both = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
let bound = filled;
if (size === "i64") {
bound = B.tmp();
B.line(`${bound} = zext i32 ${filled} to i64`);
}
B.line(`${covered} = icmp ule ${size} ${len}, ${bound}`);
B.line(`${both} = and i1 ${valid}, ${covered}`);
valid = both;
}
return valid;
}
+35 -85
View File
@@ -1217,12 +1217,13 @@ export class LlDyn {
`llvm emitter bug: JSON schema of unknown shape ${t.shapeId}`,
);
const struct = mangleRecordStruct(t.shapeId);
const index = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
// The runtime names kept-source keys by this index (record-sources.ts).
const order = recordSourceFieldOrder(shape);
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
const entries = order.map((fieldName) => {
const field = shape.fields[index.get(fieldName)! - 1]!;
const field = shape.fields[index.get(fieldName)! - base]!;
const sub = this.jsonSchema(field.type);
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
@@ -1240,7 +1241,7 @@ export class LlDyn {
const overflow = shape.indexValue
? {
elem: `@${this.jsonSchema(shape.indexValue)}`,
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + 1}) to ${S})`,
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
}
: { elem: "null", offset: "0" };
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
@@ -1547,18 +1548,19 @@ export class LlDyn {
B.line(`call void @scr_arr_outline(ptr ${cached})`);
B.line(`call void @scr_arr_outline(ptr ${checked})`);
}
const base = t.kind === "record" ? host.nullableUnions.fieldBase(t.shapeId) : 1;
const members =
t.kind === "record"
? [
...shape!.fields.map((field, index) => ({
index: index + 1,
index: index + base,
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
name: field.name,
})),
...(shape!.indexValue
? [
{
index: shape!.fields.length + 1,
index: shape!.fields.length + base,
type: "ptr" as const,
name: "[key: string] overflow",
},
@@ -1819,7 +1821,8 @@ export class LlDyn {
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
);
const struct = mangleRecordStruct(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
const releaseR = (): void => {
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
};
@@ -1996,7 +1999,7 @@ export class LlDyn {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + 1}`,
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + base}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (shape.fields.length === 0) {
@@ -2418,6 +2421,15 @@ export class LlDyn {
const B = new BlockBuilder();
let sourceAccessor: { name: string; release: string } | null = null;
switch (t.kind) {
case "date": {
// A Date handle holding the same time value; native Dates are
// immutable values, and so is the handle.
host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`);
B.terminate(`ret ptr ${r}`);
break;
}
case "url":
case "searchParams": {
const native = t.kind === "url" ? "url" : "search_params";
@@ -2569,7 +2581,8 @@ export class LlDyn {
if (!shape)
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
const struct = mangleRecordStruct(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
const loadFieldOf = (fname: string, ft: IrType): string => {
const p = B.tmp();
B.line(
@@ -2693,7 +2706,7 @@ export class LlDyn {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + base}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
const ks = B.tmp();
@@ -3209,82 +3222,19 @@ export class LlDyn {
this.puts(B, "%b", "[object Object]");
B.br(done);
B.startBlock(lErr);
const en = this.objGetLit(B, "%d", "name");
const em = this.objGetLit(B, "%d", "message");
// ens/ems: the member's ScrStr when it is a string, else null.
const strOf = (m: string, hint: string): string => {
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ptr`);
B.line(`store ptr null, ptr ${slot}`);
const nn = B.tmp();
B.line(`${nn} = icmp ne ptr ${m}, null`);
const lt = B.newLabel(`${hint}.t`);
const lj = B.newLabel(`${hint}.j`);
B.condBr(nn, lt, lj);
B.startBlock(lt);
const k = this.kindOf(B, m);
const isStr = B.tmp();
B.line(`${isStr} = icmp eq i32 ${k}, ${DYN_KIND.STR}`);
const ls = B.newLabel(`${hint}.s`);
B.condBr(isStr, ls, lj);
B.startBlock(ls);
const sv = this.payloadOf(B, m, "ptr");
B.line(`store ptr ${sv}, ptr ${slot}`);
B.br(lj);
B.startBlock(lj);
const out = B.tmp();
B.line(`${out} = load ptr, ptr ${slot}`);
return out;
};
const ens = strOf(en, "ds.en");
const ems = strOf(em, "ds.em");
const putIf = (s: string, hint: string): void => {
const nn = B.tmp();
B.line(`${nn} = icmp ne ptr ${s}, null`);
const lt = B.newLabel(`${hint}.t`);
const lj = B.newLabel(`${hint}.j`);
B.condBr(nn, lt, lj);
B.startBlock(lt);
this.putScrStr(B, "%b", s);
B.br(lj);
B.startBlock(lj);
};
putIf(ens, "ds.pn");
{
// if (ens && ens->len && ems && ems->len) ": "
const lenNz = (s: string, hint: string): string => {
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca i1`);
B.line(`store i1 false, ptr ${slot}`);
const nn = B.tmp();
B.line(`${nn} = icmp ne ptr ${s}, null`);
const lt = B.newLabel(`${hint}.t`);
const lj = B.newLabel(`${hint}.j`);
B.condBr(nn, lt, lj);
B.startBlock(lt);
const { len } = this.strParts(B, s);
const nz = B.tmp();
B.line(`${nz} = icmp ne ${this.S} ${len}, 0`);
B.line(`store i1 ${nz}, ptr ${slot}`);
B.br(lj);
B.startBlock(lj);
const out = B.tmp();
B.line(`${out} = load i1, ptr ${slot}`);
return out;
};
const a = lenNz(ens, "ds.ln");
const b2 = lenNz(ems, "ds.lm");
const both = B.tmp();
B.line(`${both} = and i1 ${a}, ${b2}`);
const lt = B.newLabel("ds.cl");
const lj = B.newLabel("ds.cj");
B.condBr(both, lt, lj);
B.startBlock(lt);
this.puts(B, "%b", ": ");
B.br(lj);
B.startBlock(lj);
}
putIf(ems, "ds.pm");
// Error.prototype.toString, reading name and message through the
// prototype. Null leaves the exception its conversion threw pending.
host.declare(`declare ptr @scr_dyn_error_string(ptr)`);
host.declare(`declare void @scr_str_release(ptr)`);
const text = B.tmp();
B.line(`${text} = call ptr @scr_dyn_error_string(ptr %d)`);
const written = B.tmp();
B.line(`${written} = icmp ne ptr ${text}, null`);
const lPut = B.newLabel("ds.ep");
B.condBr(written, lPut, done);
B.startBlock(lPut);
this.putScrStr(B, "%b", text);
B.line(`call void @scr_str_release(ptr ${text})`);
B.br(done);
}
B.startBlock(labels.get(DYN_KIND.BYTES)!);
@@ -126,6 +126,61 @@ test("the shared indexed-read helpers carry an inline hint", () => {
expect(define("plain")).not.toContain("inlinehint");
});
test("bodies holding thousands of capture boxes compile unoptimized with boxes in memory", () => {
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
// A declaring body with `count` boxed bindings, and a closure body that
// receives `count` of them through its environment.
const declaring = (name: string, count: number): IrFunction => {
const ids = Array.from({ length: count }, (_, i) => `${name}${i}`);
return {
name,
params: [],
returnType: F64,
locals: ids.map((id) => ({ id, name: id, type: F64, mutable: true, boxed: true as const })),
body: [
...ids.map((id, i): IrStmt => ({ kind: "varDecl", localId: id, init: number(i), loc })),
{ kind: "return", value: read(ids[count - 1]!), loc },
],
loc,
};
};
const receiving = (name: string, count: number): IrFunction => {
const ids = Array.from({ length: count }, (_, i) => `${name}${i}`);
return {
name,
params: [],
returnType: F64,
captures: ids.map((id) => ({ localId: id, name: id, type: F64 })),
locals: ids.map((id) => ({ id, name: id, type: F64, mutable: true, boxed: true as const })),
body: [{ kind: "return", value: read(ids[0]!), loc }],
loc,
};
};
const module = moduleFor([]);
const llvm = emitLlvmModule({
...module,
functions: [
...module.functions,
declaring("wide", 512),
declaring("narrow", 511),
receiving("env", 512),
],
});
const body = (name: string): string => {
const start = llvm.split("\n").findIndex((line) => line.endsWith(`{ ; ${name}`));
const lines = llvm.split("\n").slice(start);
return lines.slice(0, lines.indexOf("}")).join("\n");
};
expect(body("wide")).toMatch(/\) noinline optnone #0 \{/);
expect(body("wide")).toMatch(/store volatile ptr %[st]\d+, ptr %sc_l_wide0\b/);
expect(body("wide")).toMatch(/= load volatile ptr, ptr %sc_l_wide511\b/);
expect(body("env")).toMatch(/\) noinline optnone #0 \{/);
expect(body("env")).toMatch(/store volatile ptr %t\d+, ptr %sc_l_env0 ; captured env0/);
expect(body("narrow")).not.toContain("optnone");
expect(body("narrow")).not.toContain("volatile");
});
test("throwing-call count does not multiply identical scope cleanup", () => {
for (const boxed of [false, true]) {
const small = emitLlvmModule(moduleFor(Array.from({ length: 4 }, call), { boxed }));
+383 -28
View File
@@ -50,12 +50,19 @@ import {
STRING,
VOID,
} from "../../ir/ir.js";
import { moduleRuntimeFeatures } from "../../ir/runtime-features.js";
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
matchIntegerCountedForLoop,
} from "../../ir/integer-loops.js";
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
import {
countedDenseShape,
emitDenseLoopGuard,
matchDenseArrayLoop,
type DenseArrayLoop,
} from "./dense-array-loops.js";
import { emitCountedLoopLimit } from "./counted-loops.js";
import {
emitSafeIntegerCheck,
@@ -89,7 +96,7 @@ import {
type StringSplit,
} from "./split-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js";
import { RESULT_DST_ID, RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js";
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
@@ -167,7 +174,9 @@ import {
emitStackObject,
findLocalStackObjects,
findReassignedStackObjects,
stackObjectFields,
stackObjectStruct,
type StackObjectField,
type StackObjectSource,
} from "./stack-objects.js";
import {
@@ -200,7 +209,12 @@ import {
type LocalArrayRead,
} from "./local-array-reads.js";
import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
import { emitDenseArrayGet, emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
import {
emitDenseArrayGet,
emitDenseArrayPop,
emitDenseArraySet,
emitDenseSlotUnlink,
} from "./dense-array-access.js";
import {
emitStackMapRead,
findMapReadLifetimes,
@@ -223,7 +237,10 @@ import { findAppendedArrayLiterals } from "./appended-array-literals.js";
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
import {
findScalarStringSlices,
findScratchStringSlices,
findLentStringParameters,
emitStringSliceSnapshot,
type LentStringParameters,
type StringSliceSnapshot,
} from "./string-slices.js";
import { LazyCaptures } from "./lazy-captures.js";
@@ -377,6 +394,7 @@ import {
elemAccess,
emitInlineRcHelpers,
emitNullableRetainWrappers,
emitShapeUnionHelpers,
FN_ATTRS,
inlineRcDecls,
type InlineRcMode,
@@ -392,6 +410,7 @@ import {
} from "./shapes.js";
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
import { INLINE_LITERAL_BYTES, literalEqualityDefinition } from "./string-literal-equality.js";
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
export { LlvmUnsupportedError } from "./unsupported.js";
@@ -549,10 +568,35 @@ const AWAIT_SUSPENSIONS = /@(?:scr_await_(?:void|f64|bool|str|ref|hop)\b|sc_a[wd
* own fiber and promise. */
const COROUTINE_SUSPENDS =
/@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_(?!self\b|promise\b))|sc_a[wd]_)/;
/** Captured bindings (boxes a body declares or receives) from which a
* function compiles without LLVM optimization (see boxSlotCount). */
const UNOPTIMIZED_BOX_SLOTS = 512;
/** ScrExcKind tags of a generator slot holding a number or a boolean. */
const SCR_EXC_F64 = 1;
const SCR_EXC_BOOL = 2;
/** The capture boxes `fn` holds: its boxed bindings and the bindings its
* environment brings in. Each box pointer is a value from its binding (or
* function entry) to its last use, and every one of them crosses the calls
* in between. With thousands of them in one body, as in a factory whose
* returned methods share a large environment, LLVM's inliner, instruction
* selection and register allocation grow quadratically and take minutes.
* Past UNOPTIMIZED_BOX_SLOTS, which ordinary functions stay far below, the
* body is compiled `optnone` (nothing inlines into it and code generation
* takes its fast paths) and its box slots are accessed volatile, so no
* pass promotes them to values even where a pipeline runs IR passes over
* optnone bodies, as the native helper's does. Each use reloads its box
* pointer and code generation stays linear. Semantics, boxes and
* environments are unchanged; only that body's machine code is slower. */
function boxSlotCount(fn: IrFunction): number {
const captured = new Set<string>();
for (const capture of fn.captures ?? []) captured.add(capture.localId);
for (const capture of fn.classCaptures ?? []) captured.add(capture.localId);
let count = 0;
for (const local of fn.locals) if (local.boxed === true || captured.has(local.id)) count++;
return count;
}
/** Whether any node of `e` names the local (reads, writes, declarations,
* captures). */
function mentionsLocal(e: IrExpr, localId: string): boolean {
@@ -591,11 +635,18 @@ export class LlEmitter {
private loopArrayBorrows: ReadonlySet<IrStmt> = new Set();
private scalarStringSlices = new Map<string, IrExpr & { kind: "strIntrinsic" }>();
readonly stringSlices = new Map<string, StringSliceSnapshot>();
/** Slices written into stack buffers (findScratchStringSlices). */
scratchStringSlices: ReadonlySet<IrExpr> = new Set();
/** String parameters a caller may lend scratch slices to. */
private lentStringParameters: LentStringParameters = new Map();
private localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
private localStackObjects = new Map<string, StackObjectSource>();
private reassignedStackObjects = new Set<string>();
/** The binding slot of each reassigned stack-object local. */
private stackObjectSlots = new Map<string, string>();
/** Entry-block pointers to the reference fields of each frame object
* slot (stack-objects.ts stackObjectFields). */
stackObjectFieldPtrs = new Map<string, readonly StackObjectField[]>();
private localUnionStorageProofs = new Map<string, LocalUnionStorageProof>();
private localUnionStorage = new Map<string, LocalUnionStorage>();
integerArrayBindings = new Set<string>();
@@ -639,6 +690,21 @@ export class LlEmitter {
return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : "";
}
/** Whether an element access at `index` of `arr` lies in the hole-free
* prefix: the counter of a dense array loop's fast copy indexing one of
* the arrays its entry test covered. */
provenDenseIndex(arr: IrExpr, index: IrExpr): boolean {
const loop = this.denseLoop;
return (
loop !== null &&
arr.kind === "varRef" &&
loop.arrays.has(arr.localId) &&
index.kind === "varRef" &&
index.localId === loop.counter &&
this.integerLoopBindings.has(loop.counter)
);
}
fieldAliasAttachment(ptr: string): string {
const tag = this.fieldPointerTags.get(ptr);
return tag === undefined ? "" : `, !tbaa !${tag}`;
@@ -883,6 +949,9 @@ export class LlEmitter {
/** setTimeout and friends appeared somewhere: main must run the event
* loop even in programs with no async functions. */
usesTimers = false;
/** The program calls mock.timers members (moduleUsesMockTimers): timer
* calls emit scr_test.c's mock-aware entry points. */
readonly mockTimers: boolean;
/** Emitted ref-kind resolve thunks for new Promise, interned per inner
* typeKey → thunk symbol. */
readonly resolveThunks = new Map<string, string>();
@@ -992,6 +1061,14 @@ export class LlEmitter {
* immortal from construction (classes.ts programLifetimeClass). */
readonly programLifetime: ReadonlySet<string>;
private numericLocals = new Map<string, IrLocal>();
/** Local arrays of the current function that no other binding can hold
* (ir/array-length-stability.ts). */
private unaliasedArrays: ReadonlySet<string> = new Set();
/** Dense array loops (dense-array-loops.ts) run in this function. */
private denseLoopsEnabled = false;
private denseLoopEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
/** The fast copy of a dense array loop being emitted. */
private denseLoop: DenseArrayLoop | null = null;
private captureIds = new Set<string>();
/** Active induction bindings retain their width independently of size_t. */
integerLoopBindings = new Map<
@@ -1108,7 +1185,8 @@ export class LlEmitter {
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
}),
);
this.nullableUnions = new NullableUnions(mod);
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
this.mockTimers = moduleUsesMockTimers(mod);
this.debug =
options.debugSources === undefined
? null
@@ -1206,8 +1284,10 @@ export class LlEmitter {
// Optional array reads also cover nullable unions: emitLocalArrayRead
// then yields the (borrowed or owned) element pointer, not a stack box.
// Arrays of such a union are read the same way.
this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) =>
this.nullableUnions.has(id),
this.optionalArrayReads = new OptionalArrayReads(
this.fnByName,
this.unionsById,
(id) => this.nullableUnions.single(id) !== null,
);
this.unionSources = {
unions: this.boxedUnionsById,
@@ -1260,7 +1340,8 @@ export class LlEmitter {
(type) =>
this.plainReference(type) &&
type.kind !== "string" &&
(type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"),
(type.kind !== "union" ||
this.nullableUnions.single(type.unionId)?.arm.kind !== "string"),
this.walkBorrowsByFunction.get(fn.name) ?? new Set(),
);
if (rebound.size === 0) continue;
@@ -1286,6 +1367,8 @@ export class LlEmitter {
return shape !== undefined && !recordDynKeepsIdentity(shape);
},
);
this.lentStringParameters =
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
// Integer clones signal a stack overflow through their result, which
// needs the single-threaded guard; worker programs also poll for
@@ -1311,6 +1394,27 @@ export class LlEmitter {
const traced = computeTraced(mod);
this.tracedShapes = traced.shapes;
this.tracedUnions = traced.unions;
// A shape union's value is whichever record it holds, and the collector
// visits it as that record: when one arm carries a collector header,
// every arm must (nullable-unions.ts). Unions that share an arm pass a
// header on to each other's arms, so repeat until no union adds one.
const shapeUnions = this.nullableUnions.allShapeUnions();
for (let changed = true; changed;) {
changed = false;
for (const union of shapeUnions) {
if (
!this.tracedUnions.has(union.unionId) &&
!union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`))
)
continue;
for (const arm of union.arms) {
const key = `record:${arm.shapeId}`;
if (this.tracedShapes.has(key)) continue;
this.tracedShapes.add(key);
changed = true;
}
}
}
this.shapeHost = {
sizeType: this.sizeType,
cycleBufOffset: this.cycleBufOffset,
@@ -2030,12 +2134,28 @@ export class LlEmitter {
this.dyn.toDynHelper({ kind: "object", className: "%Writable" }),
]
: null;
if (childStreamBoxes) {
// worker.stdout and worker.stderr box the stream unit's Readable the
// same way, and worker.stdin its Writable when the program has one
// (scr_worker_stdio_install, scr_worker_events.c).
const workerStdioBox =
this.mod.workers === true && usesStream && this.classMeta.has("%Readable")
? (childStreamBoxes?.[0] ??
this.dyn.toDynHelper({ kind: "object", className: "%Readable" }))
: null;
const workerStdinBox =
this.mod.workers === true && usesStream && this.classMeta.has("%Writable")
? (childStreamBoxes?.[1] ??
this.dyn.toDynHelper({ kind: "object", className: "%Writable" }))
: null;
if (childStreamBoxes || workerStdioBox || workerStdinBox) {
this.declare(
`@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`,
);
this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`);
}
if (childStreamBoxes)
this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`);
if (workerStdioBox || workerStdinBox)
this.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`);
if (usesFsWatch) this.declare(`declare void @scr_watch_install()`);
if (this.ffiHasForeignCallback) this.declare(`declare void @scr_ffi_install()`);
if (usesStream) this.declare(`declare void @scr_stream_install()`);
@@ -2048,7 +2168,10 @@ export class LlEmitter {
if (usesHttp) this.declare(`declare void @scr_http_dyn_install()`);
if (usesFetch) this.declare(`declare void @scr_fetch_install()`);
if (usesNodeTest) this.declare(`declare i32 @scr_test_exit_code()`);
if (usesNodeTest) {
this.declare(`declare i32 @scr_test_exit_code()`);
this.declare(`declare void @scr_test_init()`);
}
if (snapshotsTlsCa) {
this.declare(`declare void @scr_tls_ca_install()`);
}
@@ -2192,6 +2315,12 @@ export class LlEmitter {
if (this.mod.lib.exports.some((e) => e.returns === "bytes")) {
this.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`);
}
if (this.mod.lib.exports.some((e) => e.params.includes("cstring"))) {
this.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`);
}
if (this.mod.lib.exports.some((e) => e.returns === "cstring")) {
this.declare(`declare ptr @scr_library_cstr_out(ptr)`);
}
}
// Helpers assemble BEFORE the declaration table flushes (they add
// write/abort declarations).
@@ -2625,6 +2754,11 @@ export class LlEmitter {
` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`,
]
: []),
...(workerStdioBox || workerStdinBox
? [
` call void @scr_worker_stdio_install(ptr @scr_stream_child_ops, ptr ${workerStdioBox ? `@${workerStdioBox}` : "null"}, ptr ${workerStdinBox ? `@${workerStdinBox}` : "null"})`,
]
: []),
// fs.watch programs fill the loop's watch hooks the same way —
// scr_watch.c links only when this line is emitted.
...(usesFsWatch ? [` call void @scr_watch_install()`] : []),
@@ -2637,6 +2771,8 @@ export class LlEmitter {
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
...(usesStream ? [` call void @scr_stream_install()`] : []),
` call void @scr_lib_init(i32 %argc, ptr %argv)`,
// node:test takes Node's --test-update-snapshots out of argv first.
...(usesNodeTest ? [` call void @scr_test_init()`] : []),
...(asyncEntry
? [` %top = call ptr @${mangleAsyncSpawn(this.mod.entry)}()`]
: [` call void @${mangleFunction(this.mod.entry)}()`]),
@@ -2968,6 +3104,15 @@ export class LlEmitter {
``,
);
}
if (e.inboundCstringTrap !== undefined) {
// The cstring-in helper's host-contract trap message (a NULL
// `const char *`): same assembled structured form and SC4012 code.
const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8");
out.push(
`@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`,
``,
);
}
e.params.forEach((cls, i) => {
switch (cls) {
case "f64":
@@ -3025,6 +3170,13 @@ export class LlEmitter {
);
args.push(`ptr %c${i}`);
break;
case "cstring":
params.push(`ptr %a${i}`);
body.push(
` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`,
);
args.push(`ptr %c${i}`);
break;
}
});
if (e.returns === "string" || e.returns === "bytes") {
@@ -3089,6 +3241,16 @@ export class LlEmitter {
` ret void`,
);
break;
case "cstring":
// The result's arena copy, returned as `const char *`.
retType = "ptr";
body.push(
` %r = call ptr ${target}(${callArgs})`,
` call void @scr_library_check_exc()`,
` %z = call ptr @scr_library_cstr_out(ptr %r)`,
` ret ptr %z`,
);
break;
}
out.push(
`define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`,
@@ -3594,6 +3756,7 @@ export class LlEmitter {
return [
...late,
...emitNullableRetainWrappers(this.shapeHost),
...emitShapeUnionHelpers(this.shapeHost),
...emitInlineRcHelpers(this.shapeHost),
];
}
@@ -4701,6 +4864,30 @@ export class LlEmitter {
if (isRefCounted(type)) this.currentFrame().push({ name: slot, type, slot: true });
}
/** The innermost lexical scope releases a frame object's reference
* fields when it exits, as it releases its bindings. */
ownScopeFields(fields: readonly StackObjectField[]): void {
const scope = this.scopes[this.scopes.length - 1]!;
for (const field of fields) scope.push({ slot: field.ptr, type: field.type });
}
/** Strike frame-owned slots (ownSlot) from their statement frames: their
* contents are moving to another owner. */
disownSlots(slots: readonly { ptr: string }[]): void {
for (const { ptr } of slots) {
let found = false;
for (let i = this.frames.length - 1; i >= 0 && !found; i--) {
const idx = this.frames[i]!.findIndex((e) => e.slot === true && e.name === ptr);
if (idx >= 0) {
this.frames[i]!.splice(idx, 1);
found = true;
}
}
if (!found)
throw new InternalCompilerError(`llvm emitter bug: disowned slot ${ptr} not in a frame`);
}
}
/** Strike a refcounted temp from its frame: ownership is being moved. */
moveTemp(v: LlValue): void {
if (!isRefCounted(v.type) || this.isImmortal(v.name)) return;
@@ -5737,7 +5924,11 @@ export class LlEmitter {
const nullable = this.nullableUnions.get(unionId);
if (nullable) {
// The owned payload IS the nullable union's value.
if (tag !== nullable.refTag)
if (
nullable.repr === "single"
? tag !== nullable.refTag
: !nullable.arms.some((arm) => arm.tag === tag)
)
throw new InternalCompilerError(
`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`,
);
@@ -5842,7 +6033,7 @@ export class LlEmitter {
const index = shape.fields.findIndex((entry) => entry.name === field);
if (index < 0)
throw new InternalCompilerError(`llvm emitter bug: missing union field ${field}`);
layout = "record:";
layout = this.nullableUnions.fieldBase(arm.shapeId) === 2 ? "record:tagged;" : "record:";
for (let i = 0; i <= index; i++) {
const type = shape.fields[i]!.type;
layout += `${llFieldType(type, this.inlineUnions.record(type))};`;
@@ -5878,7 +6069,7 @@ export class LlEmitter {
);
const p = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + 1}`,
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
);
const type = shape.fields[idx]!.type;
return { ptr: p, type, inline: this.inlineUnions.record(type) };
@@ -5892,7 +6083,7 @@ export class LlEmitter {
const p = this.B.tmp();
const v = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + 1}`,
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + this.nullableUnions.fieldBase(shapeId)}`,
);
this.B.line(`${v} = load ptr, ptr ${p}`);
return v;
@@ -6555,6 +6746,7 @@ export class LlEmitter {
const initializerBindings = this.initializerBindings.get(fn.name) ?? [];
const numericFn = withInitializerBindings(fn, initializerBindings);
this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
this.unaliasedArrays = findUnaliasedLocalArrays(fn);
this.borrowedParameters.clear();
this.referenceWrites?.enter(fn);
this.reboundOwnerSlots.clear();
@@ -6593,6 +6785,12 @@ export class LlEmitter {
this.finalFieldBorrows =
!fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
this.byteWindowsEnabled = this.countedLoopsEnabled;
this.denseLoopsEnabled = this.countedLoopsEnabled;
this.denseLoop = null;
this.denseLoopEntries =
this.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
? findInitializedByteLoopBindings(fn)
: new Map();
this.byteWindowEntries =
this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
? findInitializedByteLoopBindings(fn)
@@ -6630,6 +6828,15 @@ export class LlEmitter {
);
this.scalarStringSlices =
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
this.scratchStringSlices =
this.debug === null
? findScratchStringSlices(
fn,
this.callLifetimes,
this.scalarStringSlices,
this.lentStringParameters,
)
: new Set();
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
this.readNumberMapWrites = findReadNumberMapWrites(fn);
this.mapEntryFusion = this.debug === null ? findMapEntryFusions(fn) : null;
@@ -6641,6 +6848,7 @@ export class LlEmitter {
? findReassignedStackObjects(this, fn, this.callLifetimes)
: new Set();
this.stackObjectSlots = new Map();
this.stackObjectFieldPtrs = new Map();
this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources);
this.localUnionStorage = new Map();
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
@@ -6819,6 +7027,15 @@ export class LlEmitter {
B.line(`store ptr null, ptr %${mangleLocal(local.id)}`);
}
}
// A body holding thousands of capture boxes compiles unoptimized, its
// box pointers kept in their slots (see boxSlotCount).
const unoptimized = this.currentCoro === null && boxSlotCount(fn) >= UNOPTIMIZED_BOX_SLOTS;
if (unoptimized) {
const slots = new Set<string>();
for (const local of this.currentLocals.values())
if (local.boxed || this.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`);
B.volatileSlots = slots;
}
// Captured bindings come in through the environment — borrowed for the
// whole call (the closure owns them): bound here, never released here.
(fn.captures ?? []).forEach((c, i) => {
@@ -7022,6 +7239,7 @@ export class LlEmitter {
if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`;
else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`;
if (fn.name.startsWith(RESULT_ENTRY_PREFIX)) attrs = `inlinehint ${attrs}`;
if (unoptimized && attrs === FN_ATTRS) attrs = `noinline optnone ${attrs}`;
const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
this.privateGlobalSlots.clear();
@@ -7766,12 +7984,18 @@ export class LlEmitter {
}
const stackObject = this.localStackObjects.get(s.localId);
if (stackObject) {
B.line(`store ptr ${emitStackObject(this, stackObject).name}, ptr ${b.slot}`);
const value = emitStackObject(this, stackObject, (fields) => this.ownScopeFields(fields));
B.line(`store ptr ${value.name}, ptr ${b.slot}`);
break;
}
if (s.init && this.reassignedStackObjects.has(s.localId)) {
const slot = this.stackObjectSlot(s.localId, s.init.type);
emitStackObject(this, s.init as StackObjectSource, slot);
emitStackObject(
this,
s.init as StackObjectSource,
(fields) => this.ownScopeFields(fields),
slot,
);
B.line(`store ptr ${slot}, ptr ${b.slot}`);
break;
}
@@ -7944,12 +8168,28 @@ export class LlEmitter {
break;
}
if (this.reassignedStackObjects.has(s.localId)) {
// The new value may read the old one: build it apart, then copy.
// The new value may read the old one: build it apart (its
// reference fields owned by this statement until it completes),
// then copy it over the binding's slot. The binding takes over the
// new fields; the old ones are released after the slot no longer
// holds them.
const struct = stackObjectStruct(s.value.type);
const value = emitStackObject(this, s.value as StackObjectSource);
let owned: readonly StackObjectField[] = [];
const value = emitStackObject(this, s.value as StackObjectSource, (fields) => {
owned = fields;
for (const field of fields) this.ownSlot(field.ptr, field.type);
});
const target = this.stackObjectSlot(s.localId, s.value.type);
const old = stackObjectFields(this, s.value.type, target).map((field) => {
const t = B.tmp();
B.line(`${t} = load ptr, ptr ${field.ptr}`);
return { name: t, type: field.type };
});
const bits = B.tmp();
B.line(`${bits} = load ${struct}, ptr ${value.name}`);
B.line(`store ${struct} ${bits}, ptr ${this.stackObjectSlot(s.localId, s.value.type)}`);
B.line(`store ${struct} ${bits}, ptr ${target}`);
this.disownSlots(owned);
for (const field of old) this.releaseValue(field.name, field.type);
break;
}
const localStorage = this.localUnionStorage.get(s.localId);
@@ -8032,7 +8272,16 @@ export class LlEmitter {
const acc = elemAccess(s.arr.type.elem);
this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
if (acc === "ref") this.moveTemp(v);
emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
emitDenseArraySet(
this,
arr.name,
idx,
s.index,
acc,
s.arr.type.elem,
v.name,
this.provenDenseIndex(s.arr, s.index),
);
break;
}
case "arraySetLength": {
@@ -8070,6 +8319,28 @@ export class LlEmitter {
B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
B.condBr(same, done, resize);
B.startBlock(resize);
if (s.pop === true) {
// A discarded pop's `length - 1` on a packed array removes the
// last element inline, then releases it.
const call = B.newLabel("arr.len.call"),
pop = B.newLabel("arr.len.pop");
const lastNumber = B.tmp(),
dropsLast = B.tmp();
B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`);
B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`);
B.condBr(dropsLast, pop, call);
B.startBlock(pop);
const elem = s.arr.type.elem;
const acc = elemAccess(elem);
const raw = emitDenseArrayPop(this, arr.name, acc, call, call);
if (acc === "ref" && isRefCounted(elem)) {
const old = B.tmp();
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
this.releaseValue(old, elem);
}
B.br(done);
B.startBlock(call);
}
B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
this.emitPendingCheck();
B.br(done);
@@ -8159,7 +8430,15 @@ export class LlEmitter {
s.kind === "fieldSet"
? this.classFieldPtr(obj.name, s.className, s.field)
: this.recordFieldPtr(obj.name, s.shapeId, s.field);
this.replaceField(ptr, type, inline, v);
// A result-slot entry fills zeroed caller storage, each field once.
if (
s.kind === "recordSet" &&
s.obj.kind === "varRef" &&
s.obj.localId === RESULT_DST_ID &&
this.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX)
)
this.initField(ptr, type, inline, v);
else this.replaceField(ptr, type, inline, v);
if (
s.kind === "fieldSet" &&
s.field === "name" &&
@@ -8449,7 +8728,12 @@ export class LlEmitter {
(this.debug === null ? matchIntegerBytesForLoop(s, this.numericLocals) : null);
const countedLoop =
!integerLoop && this.countedLoopsEnabled
? matchIntegerCountedForLoop(s, this.numericLocals, this.integerRanges)
? matchIntegerCountedForLoop(
s,
this.numericLocals,
this.integerRanges,
this.unaliasedArrays,
)
: null;
const window =
countedLoop && this.byteWindowsEnabled
@@ -8462,6 +8746,49 @@ export class LlEmitter {
this.byteWindowEntries.get(s) ?? new Set(),
)
: null;
const denseShape = !this.denseLoopsEnabled
? null
: integerArrayLoop
? {
counter: integerArrayLoop.localId,
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
range: { min: 0, max: 4294967294 },
}
: countedLoop && !countedLoop.guarded
? countedDenseShape(s, countedLoop)
: null;
const dense =
denseShape && !window
? matchDenseArrayLoop(
s,
denseShape,
this.numericLocals,
this.unaliasedArrays,
this.denseLoopEntries.get(s) ?? new Set(),
)
: null;
if (dense) {
const valid = emitDenseLoopGuard(this, dense);
const fast = B.newLabel("dense.loop.fast"),
slow = B.newLabel("dense.loop.slow"),
done = B.newLabel("dense.loop.done");
B.condBr(valid, fast, slow);
this.denseLoopsEnabled = false;
try {
B.startBlock(slow);
this.emitStmt(s);
B.br(done);
B.startBlock(fast);
this.denseLoop = dense;
this.emitStmt(s);
B.br(done);
} finally {
this.denseLoop = null;
this.denseLoopsEnabled = true;
}
B.startBlock(done);
break;
}
if (window) {
const valid = emitByteWindowGuard(this, window);
const fast = B.newLabel("bytes.window.fast"),
@@ -8564,7 +8891,10 @@ export class LlEmitter {
this.integerLoopBindings.set(integerLoop.localId, {
slot: integerSlot,
type: this.sizeType,
range: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
range:
this.denseLoop?.counter === integerLoop.localId
? this.denseLoop.range
: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
});
} else if (countedLoop) {
integerSlot = B.slot();
@@ -8579,7 +8909,10 @@ export class LlEmitter {
slot: integerSlot,
type: "i64",
signed: true,
range: countedLoop.range,
range:
this.denseLoop?.counter === countedLoop.localId
? this.denseLoop.range
: countedLoop.range,
});
countedLimit = emitCountedLoopLimit(this, countedLoop);
} else if (s.init) {
@@ -8921,6 +9254,12 @@ export class LlEmitter {
// own turn is the await (nothing runs in between).
const genSettles =
this.currentAsyncGenerator && (s.awaited === true || this.finallyStack.length > 0);
// A completion turn that awaits x replaces a return() request's
// already-taken await.
if (this.currentAsyncGenerator && !genSettles) {
this.declare(`declare void @scr_async_gen_return_awaits()`);
B.line(`call void @scr_async_gen_return_awaits()`);
}
if (this.currentAsyncGenerator && s.awaited !== true && this.finallyStack.length > 0) {
if (this.wasi) this.emitWasiSuspend(null);
else {
@@ -9916,7 +10255,12 @@ export class LlEmitter {
unionsById: this.unionsById,
recordsById: this.recordsById,
inlineUnions: this.inlineUnions,
nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
nullableArm: (t) => {
const nullable = this.nullableUnions.of(t);
return nullable?.repr === "single" ? nullable.arm : null;
},
shapeUnion: (t) => this.nullableUnions.of(t)?.repr === "shapes",
fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId),
classInfo: (className) => {
const meta = this.classMetaOf(className);
return {
@@ -10168,11 +10512,16 @@ export class LlEmitter {
if (canStackUnion(e, this.boxedUnionsById)) return emitStackUnion(this, e).value;
if (e.kind === "unionNarrow") {
const union = this.emitUnionProjection(e.value);
emitUnionNarrowGuard(this, union.name, e.unionId, e.tag);
return { name: this.unionPeek(union.name, e.unionId), type: e.type };
}
if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
// Prefix layouts: a class cast reads the operand's own pointer.
return { name: this.emitReadReceiver(e.value).name, type: e.type };
// Prefix layouts: a class cast reads the operand's own pointer; a
// downcast verifies its target class (narrow-guards.ts).
const receiver = this.emitReadReceiver(e.value);
if (e.kind === "downcast" && e.type.kind === "object")
emitDowncastGuard(this, receiver.name, e.value.type.className, e.type.className);
return { name: receiver.name, type: e.type };
}
// A nullable-pointer field has no box to borrow. Projections use
// emitUnionProjection; other borrowers get an owned heap box.
@@ -10632,7 +10981,11 @@ export class LlEmitter {
const nullable = this.nullableUnions.get(e.unionId);
if (!nullable) return null;
if (isUnitType(e.value.type)) return e.value.kind === "unitLit" ? "unit" : null;
return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
const refArm =
nullable.repr === "single"
? e.tag === nullable.refTag
: nullable.arms.some((arm) => arm.tag === e.tag);
return refArm && isRefCounted(e.value.type) ? "ref" : null;
}
materializeSplitLocal(localId: string): void {
@@ -10917,6 +11270,7 @@ export class LlEmitter {
resultType: IrType,
overflowOnly: boolean,
loc?: SrcLoc,
missNaN = false,
): void {
return keyedRecordReadInto(
this,
@@ -10928,6 +11282,7 @@ export class LlEmitter {
resultType,
overflowOnly,
loc,
missNaN,
);
}
@@ -338,8 +338,17 @@ export function emitSerializationExpr(
// checked source can throw from the dyn walker: finish still runs
// (frees the buffer, the partial string joins the frame and
// releases on unwind), then the pending check unwinds — the
// runtime ABI’s contract exactly.
if (host.walkers.jsonWriteMayThrow(e.value.type)) host.emitPendingCheck();
// runtime ABI’s contract exactly. Under %j the runtime first turns
// a circular-structure TypeError into the text "[Circular]".
if (host.walkers.jsonWriteMayThrow(e.value.type)) {
if (e.circular) {
host.declare(`declare ptr @scr_util_format_j_text(ptr)`);
const shown = B.tmp();
B.line(`${shown} = call ptr @scr_util_format_j_text(ptr ${t})`);
compact = host.own({ name: shown, type: e.type });
}
host.emitPendingCheck();
}
}
// A pretty-print form (`stringify(v, null, 2)`): the frontend
// resolved the space to a compile-time indent string (Node's
@@ -44,6 +44,7 @@ const DV_GET_KIND: Record<string, number> = {
dvGetFloat64: 7,
dvGetBigUint64Number: 8,
dvGetBigInt64Number: 9,
dvGetFloat16: 10,
};
const DV_SET_KIND: Record<string, number> = {
@@ -55,6 +56,7 @@ const DV_SET_KIND: Record<string, number> = {
dvSetInt32: 5,
dvSetFloat32: 6,
dvSetFloat64: 7,
dvSetFloat16: 10,
};
export function emitIntegerLoopIndex(host: LlvmEmitterContext, expr: IrExpr): string | null {
@@ -273,7 +275,7 @@ function emitSharedOrLocalLoad(
idx: string,
): LlValue {
const B = host.B;
const floating = elem === "f32" || elem === "f64";
const floating = elem === "f16" || elem === "f32" || elem === "f64";
const fn = floating ? "scr_bytes_shared_get_f64" : "scr_bytes_shared_get_int";
const ret = floating ? "double" : "i32";
host.declare(`declare ${ret} @${fn}(ptr, ${host.sizeType})`);
@@ -314,6 +316,16 @@ function emitBytesLoadAt(
const B = host.B;
const data = host.emitBytesData(receiver);
const p = B.tmp();
if (elem === "f16") {
// Half-precision bits widen exactly; the runtime converts them.
host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`);
const raw = B.tmp();
const out = B.tmp();
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${raw} = load i16, ptr ${p}, align 1`);
B.line(`${out} = call double @scr_f16_to_f64(i16 zeroext ${raw})`);
return { name: out, type: F64 };
}
if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const signed = elem === "i8" || elem === "i16";
@@ -472,10 +484,19 @@ export function emitBytesSet(
B.line(`${stored} = fptoui double ${clamped} to i32`);
} else
stored =
elem === "f32" || elem === "f64" ? null : host.emitToUint32(value.name, expr, value.uint32);
elem === "f16" || elem === "f32" || elem === "f64"
? null
: host.emitToUint32(value.name, expr, value.uint32);
const data = host.emitBytesData(receiver);
const p = B.tmp();
if (BYTES_ELEMENT_SIZE[elem] < 4) {
if (elem === "f16") {
// One round-to-nearest-even step from the double, in the runtime.
host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
const narrowed = B.tmp();
B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`);
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i16 ${narrowed}, ptr ${p}, align 1`);
} else if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const byte = B.tmp();
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
@@ -882,6 +903,7 @@ export function emitBytesIntrinsic(
case "dvGetInt16":
case "dvGetUint32":
case "dvGetInt32":
case "dvGetFloat16":
case "dvGetFloat32":
case "dvGetFloat64":
case "dvGetBigUint64Number":
@@ -912,6 +934,7 @@ export function emitBytesIntrinsic(
case "dvSetInt16":
case "dvSetUint32":
case "dvSetInt32":
case "dvSetFloat16":
case "dvSetFloat32":
case "dvSetFloat64": {
// DataView setters: [offset, value, littleEndian?] — void; throw
@@ -182,6 +182,7 @@ export function genResultThunkFor(
sym = mangleGenResThunk(host.resolveThunks.size);
host.resolveThunks.set(key, sym);
const shape = host.recordsById.get(recT.shapeId);
const base = host.nullableUnions.fieldBase(recT.shapeId);
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
if (!shape || !valueT)
throw new InternalCompilerError("llvm emitter bug: genResume record lacks its value field");
@@ -242,7 +243,7 @@ export function genResultThunkFor(
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
` %dz = zext i1 %d to i8`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + 1}`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + base}`,
` store i8 %dz, ptr %dp`,
` %has = call zeroext i1 @scr_gen_out_has(ptr %g)`,
` br i1 %has, label %take, label %undefv`,
@@ -259,7 +260,7 @@ export function genResultThunkFor(
` br label %join`,
`join:`,
` %v = load ptr, ptr %vslot`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + 1}`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + base}`,
` store ptr %v, ptr %vp`,
` ret ptr %r`,
`}`,
@@ -293,7 +294,7 @@ export function genResultThunkFor(
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
` %dz = zext i1 %d to i8`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + 1}`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + base}`,
` store i8 %dz, ptr %dp`,
` br i1 %d, label %doneb, label %susp`,
];
@@ -410,7 +411,7 @@ export function genResultThunkFor(
` br label %join`,
`join:`,
` %v = load ptr, ptr %vslot`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + 1}`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + base}`,
// An inline value field copies the box's bits and drops the box.
...(host.inlineUnions.record(valueT)
? [
@@ -1092,7 +1093,8 @@ export function settledStoreFor(
return { tag, arm };
};
const fieldIndex = (arm: IrType & { kind: "record" }, field: string): number =>
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) + 1;
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) +
host.nullableUnions.fieldBase(arm.shapeId);
const fulfilled = armOf("value");
const rejected = armOf("reason");
const valueT = host.recordsById
@@ -32,7 +32,6 @@ import {
mangleVtStruct,
} from "../mangle.js";
import {
classMembershipIntervals,
classEnvironmentIndex,
classStructSym,
vtEntriesFor,
@@ -52,6 +51,7 @@ import {
inputSuffixWrites,
} from "./borrowed-inputs.js";
import type { Writes } from "./reference-writes.js";
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
export function emitCallExpr(
host: LlvmEmitterContext,
@@ -147,7 +147,10 @@ export function emitCallExpr(
return emitScalarOptional(host, a);
if (a.kind === "call" && host.scalarResultCall(a))
return emitCallExpr(host, a, false, true);
if (host.stackObjects && canStackObject(host, a)) return emitStackObject(host, a);
if (host.stackObjects && canStackObject(host, a))
return emitStackObject(host, a, (fields) => {
for (const field of fields) host.ownSlot(field.ptr, field.type);
});
if (host.nullableFieldGet(a))
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
// An inline union field read copies its tag and bits.
@@ -333,6 +336,16 @@ export function emitCallExpr(
natArgs.push(`ptr ${data}`, `${host.sizeType} ${len}`);
break;
}
case "cstring": {
// The string's own NUL-terminated data, after the check that
// it holds no U+0000 (a C string cannot carry one).
host.declare(`declare ptr @scr_library_cstr_arg(ptr)`);
const data = B.tmp();
B.line(`${data} = call ptr @scr_library_cstr_arg(ptr ${arg.name})`);
natTypes.push("ptr");
natArgs.push(`ptr ${data}`);
break;
}
case "bytes": {
const lenPtr = B.tmp();
const len = B.tmp();
@@ -1116,8 +1129,11 @@ export function emitCallExpr(
// Prefix layout: both directions are reinterprets of the SAME
// pointer — no RC traffic, ownership transfers from the operand
// temp to the result temp (struck so the one +1 releases exactly
// once, under the RESULT type's release).
// once, under the RESULT type's release). A downcast verifies the
// class tsc's narrowing proved (narrow-guards.ts).
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);
return host.own({ name: v.name, type: e.type });
}
@@ -1129,24 +1145,14 @@ export function emitCallExpr(
// The test only reads the vtable word: a borrowable operand needs no
// retain/release pair around it.
const v = emitBorrowedInputs(host, [e.value])[0]!;
const target = host.classMetaOf(e.className);
const pre = host.loadVtPre(v.name, e.value.type.className);
let result: string | undefined;
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
const ge = B.tmp(),
le = B.tmp(),
test = B.tmp();
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
B.line(`${test} = and i1 ${ge}, ${le} ; instanceof ${e.className}`);
if (result === undefined) result = test;
else {
const combined = B.tmp();
B.line(`${combined} = or i1 ${result}, ${test}`);
result = combined;
}
}
return { name: result!, type: e.type };
const result = emitClassMembership(
host,
v.name,
e.value.type.className,
e.className,
`instanceof ${e.className}`,
);
return { name: result, type: e.type };
}
case "virtualCall": {
// Dispatch through the receiver's vtable: the slot lives on the
@@ -42,14 +42,18 @@ import {
} from "./map-number-lookup.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
import { emitStringSliceRead } from "./string-slices.js";
import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js";
import {
STRING_CHAR_CODE_AT_DECL,
STRING_CHAR_CODE_AT_INT_MARKER,
STRING_UTF16_LEN_DECL,
} from "./string-reads.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { emitDenseArrayNumber, emitDenseArrayPush } from "./dense-array-access.js";
import {
emitDenseArrayNumber,
emitDenseArrayPop,
emitDenseArrayPush,
} from "./dense-array-access.js";
import { emitUnionWiden } from "./expr-records.js";
export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string {
@@ -391,6 +395,7 @@ export function emitStrIntrinsic(
false,
);
case "slice":
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
return call(
"scr_str_slice",
"ptr (ptr, double, double)",
@@ -399,6 +404,7 @@ export function emitStrIntrinsic(
true,
);
case "substring":
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
return call(
"scr_str_substring",
"ptr (ptr, double, double)",
@@ -571,7 +577,16 @@ export function emitArrIntrinsic(
? host.constantNumericTables.get(e.receiver.localId)
: undefined;
if (!table && elem.kind === "f64")
return { name: emitDenseArrayNumber(host, r.name, index, e.args[0]), type: e.type };
return {
name: emitDenseArrayNumber(
host,
r.name,
index,
e.args[0],
host.provenDenseIndex(e.receiver, e.args[0]!),
),
type: e.type,
};
host.declare(`declare double @scr_arr_get_number(ptr, double)`);
const t = B.tmp();
B.line(
@@ -667,19 +682,38 @@ export function emitArrIntrinsic(
const rawSlot = B.slot();
const resultSlot = B.slot();
B.entryAllocas.push(`${rawSlot} = alloca i64`, `${resultSlot} = alloca ptr`);
B.line(`store i64 0, ptr ${rawSlot}`);
const state = B.tmp();
const has = B.tmp();
B.line(`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})`);
B.line(`${has} = icmp eq i8 ${state}, 1`);
const lp = B.newLabel("remove.p");
const la = B.newLabel("remove.a");
const lj = B.newLabel("remove.j");
// pop answers an empty or packed array inline; shift and every other
// pop take the runtime call.
let denseRaw: string | null = null;
let denseLabel = "";
let slowLabel = "";
if (method === "pop") {
slowLabel = B.newLabel("remove.slow");
denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel);
denseLabel = B.currentLabel();
B.br(lp);
B.startBlock(slowLabel);
}
B.line(`store i64 0, ptr ${rawSlot}`);
const state = B.tmp();
const has = B.tmp();
const slowRaw = B.tmp();
B.line(
`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})${denseRaw === null ? "" : " cold"}`,
);
B.line(`${slowRaw} = load i64, ptr ${rawSlot}`);
B.line(`${has} = icmp eq i8 ${state}, 1`);
B.condBr(has, lp, la);
B.startBlock(lp);
const raw = B.tmp();
let raw = slowRaw;
const value = B.tmp();
B.line(`${raw} = load i64, ptr ${rawSlot}`);
if (denseRaw !== null) {
raw = B.tmp();
B.line(`${raw} = phi i64 [ ${denseRaw}, %${denseLabel} ], [ ${slowRaw}, %${slowLabel} ]`);
}
if (into) {
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
let bits = raw;
@@ -330,7 +330,7 @@ export function emitControlExpr(
);
B.line(`store ${ty} ${t}, ptr ${slot}`);
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
} else if (host.nullableUnions.has(e.left.type.unionId)) {
} else if (host.nullableUnions.single(e.left.type.unionId)) {
// The owned nullable pointer IS the owned arm value.
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
} else {
@@ -440,7 +440,7 @@ export function emitControlExpr(
host.emitBranchInto(slot, e.right);
B.br(lj);
B.startBlock(lv);
if (typeEquals(e.type, e.left.type) || host.nullableUnions.has(def.id)) {
if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
// Pass-through, or a nullable pointer that IS its owned arm value.
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
} else {
@@ -1,4 +1,9 @@
import { classMembershipIntervals } from "./classes.js";
import {
emitCaughtInstanceof,
emitCaughtNarrowGuard,
emitUnionNarrowGuard,
} from "./narrow-guards.js";
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
import { preservesDynTest } from "./checked-value-lifetimes.js";
import { typedRefConstructor } from "./shapes.js";
@@ -295,13 +300,14 @@ export function emitDynamicExpr(
return host.own({ name: host.unionNewOwned(e.unionId, e.tag, v), type: e.type });
}
case "unionNarrow": {
// Tag-UNCHECKED payload extraction: the frontend emits this only
// where tsc's control-flow narrowing proved the tag. Ref payloads
// come out +1. The receiver is consumed before any later expression.
// Payload extraction where tsc's control-flow narrowing proved the
// tag; the guard verifies it (narrow-guards.ts). Ref payloads come
// out +1. The receiver is consumed before any later expression.
const u = host.emitUnionProjection(e.value);
const arm = e.type;
if (isUnitType(arm))
throw new InternalCompilerError(`llvm emitter bug: unionNarrow to unit arm ${arm.kind}`);
emitUnionNarrowGuard(host, u.name, e.unionId, e.tag);
const v = host.unionExtract(u.name, e.unionId, arm);
return host.own({ name: v, type: arm });
}
@@ -974,56 +980,7 @@ export function emitDynamicExpr(
// borrowed. SCR_EXC_STR = 3, SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2.
const c = host.emitReadReceiver(e.value);
if (e.test === "instanceof") {
const target = host.classMetaOf(e.className!);
if (!target.hierarchy) {
// Standalone class payloads have no vtable word. Their retain
// adapter identifies the exact native layout in the snapshot.
const adapterSlot = B.tmp();
const adapter = B.tmp();
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`);
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
let result: string | undefined;
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
for (const candidate of host.classMeta.values()) {
if (
candidate.hierarchy ||
!intervals.some((range) => range.pre <= candidate.pre && candidate.pre <= range.post)
)
continue;
const rc = vAdapters(host.shapeHost, { kind: "object", className: candidate.def.name });
const matches = B.tmp();
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
if (result === undefined) result = matches;
else {
const joined = B.tmp();
B.line(`${joined} = or i1 ${result}, ${matches}`);
result = joined;
}
}
if (result === undefined)
throw new InternalCompilerError("empty standalone class membership");
if (e.negated !== true) return { name: result, type: e.type };
const negated = B.tmp();
B.line(`${negated} = xor i1 ${result}, true`);
return { name: negated, type: e.type };
}
host.declare(
`declare zeroext i1 @scr_caught_instanceof(ptr, ${host.sizeType}, ${host.sizeType})`,
);
let t: string | undefined;
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
const test = B.tmp();
B.line(
`${test} = call zeroext i1 @scr_caught_instanceof(ptr ${c.name}, ${host.sizeType} ${interval.pre}, ${host.sizeType} ${interval.post})`,
);
if (t === undefined) t = test;
else {
const combined = B.tmp();
B.line(`${combined} = or i1 ${t}, ${test}`);
t = combined;
}
}
if (t === undefined) throw new InternalCompilerError("empty class membership");
const t = emitCaughtInstanceof(host, c.name, e.className!);
if (e.negated !== true) return { name: t, type: e.type };
const n = B.tmp();
B.line(`${n} = xor i1 ${t}, true`);
@@ -1097,10 +1054,18 @@ export function emitDynamicExpr(
return out;
}
case "caughtNarrow": {
// Checker-trusted extraction (the matching caughtTest was proven
// by tsc's narrowing): scalars read the snapshot's slots,
// refcounted payloads come out retained (+1). Box borrowed.
// Extraction where tsc's narrowing proved the matching caughtTest;
// the guard verifies it (narrow-guards.ts). Scalars read the
// snapshot's slots, refcounted payloads come out retained (+1). Box
// borrowed.
const c = host.emitReadReceiver(e.value);
if (
e.type.kind === "f64" ||
e.type.kind === "bool" ||
e.type.kind === "string" ||
e.type.kind === "object"
)
emitCaughtNarrowGuard(host, c.name, e.type);
if (e.type.kind === "f64") {
const p = B.tmp();
const v = B.tmp();
@@ -64,16 +64,25 @@ export function emitLiteralExpr(
const integer = B.tmp();
const number = B.tmp();
B.line(`${integer} = load ${integerSlot.type}, ptr ${integerSlot.slot}`);
// A 64-bit induction value is a safe integer, so its sign bit is
// clear and the signed conversion is exact: one cvtsi2sd instead of
// x86-64's multi-instruction unsigned 64-bit conversion.
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
B.line(
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
);
const uint32 = integerSlot.type === "i32" ? integer : B.tmp();
if (integerSlot.type !== "i32")
B.line(`${uint32} = trunc ${integerSlot.type} ${integer} to i32`);
// An int32 counter (a dense array loop's) converts from its low half,
// so LLVM folds a later `fptosi` of the double (an inlined callee's
// index) back to the integer. Otherwise a 64-bit induction value is
// a safe integer, so its sign bit is clear and the signed conversion
// is exact: one cvtsi2sd instead of x86-64's multi-instruction
// unsigned 64-bit conversion. (A uint32 counter would fold through
// `uitofp i32` too, but the zero extension it adds on x86-64 cost
// more in loops that never convert the double than it saved.)
const range = integerSlot.range;
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
if (range && range.min >= -2147483648 && range.max <= 2147483647)
B.line(`${number} = sitofp i32 ${uint32} to double`);
else
B.line(
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
);
return {
name: number,
type: e.type,
@@ -851,6 +860,7 @@ export function emitContainerExpr(
elem,
`scr_arr_get_${acc}`,
true,
host.provenDenseIndex(e.arr, e.index),
);
return host.own({ name: t, type: e.type });
}
@@ -62,9 +62,17 @@ export function emitUnionRetype(
const from = host.nullableUnions.get(e.fromId);
const to = host.nullableUnions.get(e.unionId);
if (from && to) {
// Equal encodings make the pointer itself the target value.
if (from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag)
return host.own({ name: value.name, type: e.type });
// Equal encodings make the pointer itself the target value. A shape
// union derives its record arms from the record, so only the unit
// encodings must agree.
const same =
from.repr === "single" && to.repr === "single"
? from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag
: from.repr === "shapes" &&
to.repr === "shapes" &&
from.unitTag === to.unitTag &&
from.nullTag === to.nullTag;
if (same) return host.own({ name: value.name, type: e.type });
return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
}
if (from || to) return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
@@ -339,6 +347,7 @@ export function emitRecordKeyGet(
e.type,
e.overflowOnly === true,
e.loc,
e.missNaN === true,
);
B.startBlock(join);
const t = B.tmp();
@@ -356,6 +365,7 @@ export function keyedRecordReadInto(
resultType: IrType,
overflowOnly: boolean,
loc?: SrcLoc,
missNaN = false,
): void {
const B = host.B;
const shape = host.recordShape(shapeId);
@@ -563,6 +573,12 @@ export function keyedRecordReadInto(
B.br(join);
return;
}
if (missNaN && resultType.kind === "f64") {
// A NaN-coded read: every consumer treats undefined like NaN.
B.line(`store double ${f64Lit(NaN)}, ptr ${slot}`);
B.br(join);
return;
}
// A direct result cannot represent a miss. This is unreachable for
// programs whose behavior matches Node, but preserve the missing-key trap.
host.needsBadKey = true;
@@ -500,16 +500,17 @@ export function streamTypedRefCommitAdapter(
` br i1 %missing, label %done, label %swap`,
`swap:`,
];
const base = host.nullableUnions.fieldBase(t.shapeId);
const members = [
...shape.fields.map((field, index) => ({
index: index + 1,
index: index + base,
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
name: field.name,
})),
...(shape.indexValue
? [
{
index: shape.fields.length + 1,
index: shape.fields.length + base,
type: "ptr" as const,
name: "[key: string] overflow",
},
@@ -943,7 +944,7 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
const fields = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
for (const field of fields) {
const index = shape.fields.indexOf(field) + 1;
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
@@ -980,7 +981,7 @@ export function streamTypedRefMaterializeAdapter(
liveRecordOps(host, t, snapshot, fields);
}
for (const field of fields) {
const index = shape.fields.indexOf(field) + 1;
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
@@ -1202,7 +1203,7 @@ function liveRecordOps(
const names = `${snapshot}_names`;
const ops = `${snapshot}_rops`;
const loadField = (B: BlockBuilder, field: IrRecordShape["fields"][number]): string => {
const index = shape.fields.indexOf(field) + 1;
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(`${fieldPtr} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${index}`);
return loadStructField(
@@ -47,7 +47,7 @@ function lifted(name: string, body: IrStmt[], captured = "options"): IrFunction
loc,
};
}
function lazy(...functions: IrFunction[]): Set<string> {
function lazy(...functions: IrFunction[]): ReadonlySet<string> {
const map = new Map(functions.map((fn) => [fn.name, fn]));
return new LazyCaptures(map).parameters(functions[0]!);
}
@@ -26,9 +26,15 @@ function rebound(node: IrStmt | IrExpr): string | null {
* closure then allocate no box. The proof follows each capture into nested
* function bodies; class environments keep the eager box. */
export class LazyCaptures {
private readonly unchanged = new Map<string, boolean>();
private readonly stable = new Map<string, Set<string>>();
private readonly once = new Map<string, Set<string>>();
private readonly unchangedParameters = new Map<IrFunction, Set<string>>();
/** Per function, every local rebound there or through nested captures;
* the second map counts declarations separately (see rebindings). Each
* body is walked once per map, so the queries stay linear in the IR. */
private readonly rebound = new Map<IrFunction, Set<string>>();
private readonly reboundAfterDeclaration = new Map<IrFunction, Set<string>>();
private readonly pending = new Set<IrFunction>();
private sites: Map<string, { owner: string; captures: string[] }[]> | null = null;
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
@@ -112,8 +118,11 @@ export class LazyCaptures {
return sites;
}
parameters(fn: IrFunction): Set<string> {
parameters(fn: IrFunction): ReadonlySet<string> {
const known = this.unchangedParameters.get(fn);
if (known !== undefined) return known;
const result = new Set<string>();
this.unchangedParameters.set(fn, result);
if (fn.async || fn.generator) return result;
const locals = new Map(fn.locals.map((local) => [local.id, local]));
for (const param of fn.params) {
@@ -132,18 +141,33 @@ export class LazyCaptures {
ids: ReadonlySet<string>,
declarationsBind = false,
): Set<string> {
const all = this.reboundIn(fn, declarationsBind);
const changed = new Set<string>();
for (const id of ids) if (all.has(id)) changed.add(id);
return changed;
}
/** Every local rebound in `fn` or, through the closures it creates, in
* any function that captures it, directly or through further nesting.
* With `declarationsBind`, declarations are counted separately. */
private reboundIn(fn: IrFunction, declarationsBind: boolean): ReadonlySet<string> {
const memo = declarationsBind ? this.reboundAfterDeclaration : this.rebound;
const known = memo.get(fn);
if (known !== undefined) return known;
const changed = new Set<string>();
// Capture chains follow lexical nesting and cannot cycle; on malformed
// input a function reached again while its walk is pending reports
// every capture as rebound (see isUnchanged).
if (!declarationsBind) this.pending.add(fn);
const nested: { target: string; local: string; source: string }[] = [];
const visit = (node: IrStmt | IrExpr): boolean => {
// With `declarationsBind`, declarations are counted separately.
const target = declarationsBind && node.kind === "varDecl" ? null : rebound(node);
if (target !== null && ids.has(target)) changed.add(target);
if (target !== null) changed.add(target);
if (node.kind === "classRef") {
for (const id of node.captures ?? []) if (ids.has(id)) changed.add(id);
for (const id of node.captures ?? []) changed.add(id);
} else if (node.kind === "closure") {
const callee = this.functions.get(node.fnName);
node.captures.forEach((id, index) => {
if (!ids.has(id)) return;
const capture = callee?.captures?.[index];
if (!capture) changed.add(id);
else nested.push({ target: node.fnName, local: capture.localId, source: id });
@@ -156,19 +180,14 @@ export class LazyCaptures {
if (!changed.has(edge.source) && !this.isUnchanged(edge.target, edge.local))
changed.add(edge.source);
}
if (!declarationsBind) this.pending.delete(fn);
memo.set(fn, changed);
return changed;
}
private isUnchanged(fnName: string, localId: string): boolean {
const key = `${fnName}\0${localId}`;
const known = this.unchanged.get(key);
if (known !== undefined) return known;
// Capture chains follow lexical nesting and cannot cycle; the
// provisional entry only bounds work on malformed input.
this.unchanged.set(key, false);
const fn = this.functions.get(fnName);
const result = fn !== undefined && this.rebindings(fn, new Set([localId])).size === 0;
this.unchanged.set(key, result);
return result;
if (fn === undefined || this.pending.has(fn)) return false;
return !this.reboundIn(fn, false).has(localId);
}
}
@@ -29,7 +29,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
const isString = field === "address" || field === "family";
if (isString) lines.push(` %${field}.owned = call ptr @scr_str_retain_v(ptr %${field})`);
lines.push(
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + 1}`,
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
` store ${isString ? "ptr" : "double"} %${field}${isString ? ".owned" : ""}, ptr %${field}.slot`,
);
}
@@ -121,6 +121,8 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
"dgram.getSendQueueSize": "scr_dgram_get_send_queue_size",
"dgram.getSendQueueCount": "scr_dgram_get_send_queue_count",
"dgram.disconnect": "scr_dgram_disconnect",
"dns.lookupPromise": "scr_dns_lookup_promise",
"dns.lookupServicePromise": "scr_dns_lookup_service_promise",
};
const symbol = direct[e.fn];
if (symbol !== undefined) return finishRuntimeCall(host, e, symbol, a);
@@ -167,7 +169,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
);
const slot = host.B.tmp();
host.B.line(
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + 1}`,
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
);
host.B.line(`store ${ty} ${value}, ptr ${slot}`);
}
@@ -11,7 +11,12 @@ import { InternalCompilerError } from "../../errors.js";
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
import { ASSERT_VERDICT_LIB_FNS, LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js";
import {
ASSERT_VERDICT_LIB_FNS,
LIB_FN_SYMS,
mockTimerSym,
USES_TIMERS_LIB_FNS,
} from "./lib-shared.js";
import { isDynUndefined } from "../../ir/runtime-features.js";
import { DYN_KIND } from "./dyn.js";
import { rawBytes } from "./lib-abi.js";
@@ -322,6 +327,94 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
host.emitPendingCheck();
return { name: "", type: e.type };
}
if (e.fn.startsWith("crypto.") && e.fn.includes("Promise")) {
// util.promisify over the callback forms: the callback form runs with
// a runtime callback that owns the +1 promise and settles it in the
// promise's value representation; an argument error the callback form
// threw moves into the promise as its rejection.
host.usesTimers = true;
if (e.type.kind !== "promise")
throw new InternalCompilerError("llvm emitter bug: crypto promise result not a promise");
const inner = e.type.inner;
const dyn = inner.kind === "dyn";
const args = e.args.map((arg) => host.emitExpr(arg));
const settle =
e.fn === "crypto.randomIntPromise"
? dyn
? "scr_crypto_promise_settle_number_dyn"
: "scr_crypto_promise_settle_number"
: e.fn === "crypto.hkdfPromise"
? "scr_crypto_promise_settle_array_buffer"
: e.fn === "crypto.generateKeyPromise" || e.fn === "crypto.randomFillPromiseDyn"
? "scr_crypto_promise_settle_dyn"
: !dyn
? "scr_crypto_promise_settle_bytes"
: e.fn === "crypto.randomFillPromise"
? "scr_crypto_promise_settle_view_dyn"
: "scr_crypto_promise_settle_buffer_dyn";
host.declare(`declare ptr @scr_promise_new()`);
host.declare(`declare ptr @scr_crypto_promise_callback(ptr)`);
host.declare(`declare void @scr_crypto_promise_reject_thrown(ptr)`);
host.declare(
e.fn === "crypto.randomIntPromise"
? `declare void @${settle}(ptr, double)`
: `declare void @${settle}(ptr, ptr, ptr)`,
);
const promise = B.tmp();
const cb = B.tmp();
B.line(`${promise} = call ptr @scr_promise_new()`);
B.line(`${cb} = call ptr @scr_crypto_promise_callback(ptr ${promise})`);
const a = (i: number, type = "ptr") => `${type} ${args[i]!.name}`;
const call = (symbol: string, params: string[], values: string[]) => {
host.declare(`declare void @${symbol}(${[...params, "ptr", "ptr"].join(", ")})`);
B.line(`call void @${symbol}(${[...values, `ptr ${cb}`, `ptr @${settle}`].join(", ")})`);
};
if (e.fn === "crypto.randomBytesPromise") {
call("scr_crypto_random_bytes_async", ["double"], [a(0, "double")]);
} else if (e.fn === "crypto.pbkdf2Promise") {
call(
"scr_crypto_pbkdf2_async",
["ptr", "ptr", "double", "double", "ptr"],
[a(0), a(1), a(2, "double"), a(3, "double"), a(4)],
);
} else if (e.fn === "crypto.scryptPromise") {
call(
"scr_crypto_scrypt_async",
["ptr", "ptr", "double", "ptr"],
[a(0), a(1), a(2, "double"), a(3)],
);
} else if (e.fn === "crypto.hkdfPromise") {
call(
"scr_crypto_hkdf_async",
["ptr", "ptr", "ptr", "ptr", "double"],
[a(0), a(1), a(2), a(3), a(4, "double")],
);
} else if (e.fn === "crypto.randomFillPromise") {
call(
"scr_crypto_random_fill_async",
["ptr", "double", "double", "double"],
[a(0), a(1, "double"), a(2, "double"), a(3, "double")],
);
} else if (e.fn === "crypto.randomFillPromiseDyn") {
call(
"scr_crypto_random_fill_async_dyn",
["ptr", "ptr", "ptr", "double"],
[a(0), a(1), a(2), a(3, "double")],
);
} else if (e.fn === "crypto.randomIntPromise") {
call(
"scr_crypto_random_int_async",
["double", "double", "double"],
[a(0, "double"), a(1, "double"), a(2, "double")],
);
} else if (e.fn === "crypto.generateKeyPromise") {
call("scr_crypto_generate_key_async", ["ptr", "ptr"], [a(0), a(1)]);
} else {
throw new InternalCompilerError(`llvm emitter bug: unknown crypto promise ${e.fn}`);
}
B.line(`call void @scr_crypto_promise_reject_thrown(ptr ${promise})`);
return host.own({ name: promise, type: e.type });
}
if (
e.fn === "crypto.randomBytesCb" ||
e.fn === "crypto.pbkdf2Cb" ||
@@ -431,7 +524,7 @@ 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 = LIB_FN_SYMS[e.fn];
const sym = mockTimerSym(host, e) ?? 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)
@@ -680,6 +773,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "events":
case "blob":
case "record":
case "disposable":
return host.emitGenericLibCall(e);
case "test":
return emitTestLibCall(host, e);
@@ -746,6 +746,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
const dict = B.tmp();
B.line(`${dict} = call ptr @${mangleRecordNew(e.type.shapeId)}()`);
const out = host.own({ name: dict, type: e.type });
// Node builds the result with a null prototype.
B.line(`store i8 1, ptr ${host.recordMapFlagsPtr(dict, e.type.shapeId)}`);
const ovf = host.recordOvfPtr(dict, e.type.shapeId);
B.line(
`call void @scr_qs_parse_into(ptr ${ovf}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, double ${args[3]!.name}, i32 ${strTag}, i32 ${arrTag})`,
@@ -910,11 +912,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
scopeid.inline,
);
}
const rc = vAdapters(host.shapeHost, t);
const rowU = B.tmp();
B.line(
`${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
);
// The row (+1) moves into its union arm (a shape union is the row).
const rowU = host.unionNewOwned(infoT.unionId, tag, { name: r, type: t });
B.line(`store ptr ${rowU}, ptr ${rowSlot}`);
B.br(lRow);
};
@@ -1037,6 +1036,16 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(`${t} = sitofp i32 ${product} to double`);
return { name: t, type: e.type, uint32: product };
}
if (e.fn === "math.f16round") {
const v = host.emitExpr(e.args[0]!);
host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`);
const narrowed = B.tmp();
const t = B.tmp();
B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${v.name})`);
B.line(`${t} = call double @scr_f16_to_f64(i16 zeroext ${narrowed})`);
return { name: t, type: e.type };
}
if (e.fn === "math.fround") {
const v = host.emitExpr(e.args[0]!);
const narrowed = B.tmp();
@@ -65,7 +65,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
" br i1 %more, label %body, label %finish",
"body:",
" %record = call ptr @scr_arr_get_ref(ptr %addresses, double %index)",
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + 1}`,
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + host.nullableUnions.fieldBase(addresses.elem.shapeId)}`,
" %address = load ptr, ptr %addressp",
" %owned.address = call ptr @scr_str_retain_v(ptr %address)",
" call double @scr_arr_push_ref(ptr %ips, ptr %owned.address)",
@@ -168,7 +168,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
const i = shape.fields.findIndex((f) => f.name === name);
if (i < 0)
throw new InternalCompilerError(`llvm emitter bug: net.serverAddress record lacks ${name}`);
return i + 1;
return i + host.nullableUnions.fieldBase(recT.shapeId);
};
const store = (name: string, ty: string, v: string): void => {
const p = B.tmp();
@@ -4,6 +4,7 @@ import { undefinedArmTag } from "../../ir/analysis.js";
import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { mockTimerSym } from "./lib-shared.js";
export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
@@ -18,12 +19,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
const argv = host.emitExpr(e.args[1]!);
const cb = host.emitExpr(callbackArg);
host.moveTemp(cb);
const shell = host.emitExpr(e.args[3]!);
const adapter = host.execFileThunkFor(cbT);
host.usesTimers = true;
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr)`);
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
const out = B.tmp();
B.line(
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`,
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
);
return host.own({ name: out, type: e.type });
}
@@ -372,14 +374,16 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
host.usesTimers = true;
const cb = host.emitExpr(e.args[0]!);
host.moveTemp(cb);
const sym = {
"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];
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];
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(
", ",
@@ -405,21 +409,24 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
// The chaining forms: bookkeep, then yield the handle itself (the
// C comma expressions).
const h = host.emitExpr(e.args[0]!);
const sym = {
"timers.unref": "scr_timer_unref",
"timers.ref": "scr_timer_ref",
"timers.refresh": "scr_timer_refresh",
"timers.immediateUnref": "scr_immediate_unref",
"timers.immediateRef": "scr_immediate_ref",
}[e.fn];
const sym =
mockTimerSym(host, e) ??
{
"timers.unref": "scr_timer_unref",
"timers.ref": "scr_timer_ref",
"timers.refresh": "scr_timer_refresh",
"timers.immediateUnref": "scr_immediate_unref",
"timers.immediateRef": "scr_immediate_ref",
}[e.fn];
host.declare(`declare void @${sym}(double)`);
B.line(`call void @${sym}(double ${h.name})`);
return { name: h.name, type: e.type };
}
if (e.fn === "timers.clearTimeout" || e.fn === "timers.clearInterval") {
const h = host.emitExpr(e.args[0]!);
host.declare(`declare void @scr_clear_interval(double)`);
B.line(`call void @scr_clear_interval(double ${h.name})`);
const sym = mockTimerSym(host, e) ?? "scr_clear_interval";
host.declare(`declare void @${sym}(double)`);
B.line(`call void @${sym}(double ${h.name})`);
return { name: "", type: e.type };
}
if (e.fn === "timers.clearNoop") {
@@ -37,7 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap<string, number | null> = new Ma
export const LIB_FN_SYMS: Record<string, string> = {
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
"insp.jsonDyn": "scr_dyn_format_j",
"insp.jsonDyn": "scr_util_format_j",
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
"intl.segmenterNew": "scr_intl_segmenter_new",
"weakMap.is": "scr_weak_map_is",
@@ -65,6 +65,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"arrayBuffer.viewU8": "scr_array_buffer_view_u8",
"arrayBuffer.viewU32": "scr_array_buffer_view_u32",
"arrayBuffer.viewI32": "scr_array_buffer_view_i32",
"arrayBuffer.viewF16": "scr_array_buffer_view_f16",
"arrayBuffer.viewF32": "scr_array_buffer_view_f32",
"arrayBuffer.viewF64": "scr_array_buffer_view_f64",
"arrayBuffer.viewDV": "scr_array_buffer_view_dv",
@@ -188,6 +189,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"os.uptime": "scr_os_uptime",
"os.loadavg": "scr_os_loadavg",
"os.getPriority": "scr_os_get_priority",
"os.setPriority": "scr_os_set_priority",
"os.cpusTake": "scr_os_cpus_take",
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
@@ -198,6 +200,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"os.userHomedir": "scr_os_user_homedir",
"os.tmpdir": "scr_os_tmpdir",
"process.argv": "scr_process_argv",
"process.execArgv": "scr_process_exec_argv",
"process.platform": "scr_process_platform",
"process.cwd": "scr_process_cwd",
"process.pid": "scr_process_pid",
@@ -477,6 +480,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"strdec.write": "scr_strdec_write",
"strdec.next": "scr_strdec_next",
"strdec.end": "scr_strdec_end",
"strdec.encoding": "scr_strdec_encoding",
"stats.isFile": "scr_stats_is_file",
"stats.isDirectory": "scr_stats_is_dir",
"stats.isSymbolicLink": "scr_stats_is_symlink",
@@ -575,14 +579,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.stdoutWriteBytes": "scr_process_stdout_write_bytes",
"process.stderrWriteBytes": "scr_process_stderr_write_bytes",
"insp.buffer": "scr_insp_buffer",
"insp.typedArray": "scr_insp_typed_array",
// util.inspect (scr_inspect.c): all args borrowed; string results +1;
// the begin/entry/key/moreItems/end accumulator drives the emitted
// container walks. None of these throw.
"insp.f64": "scr_insp_f64",
"insp.str": "scr_insp_str",
"insp.regex": "scr_insp_regex",
"insp.date": "scr_insp_date",
"insp.error": "scr_insp_error",
"insp.begin": "scr_insp_begin",
"insp.layout": "scr_insp_layout",
"insp.entry": "scr_insp_entry",
"insp.key": "scr_insp_key",
"insp.moreItems": "scr_insp_more_items",
@@ -841,6 +848,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fetch.abortAny": "scr_fetch_abort_any",
"fetch.webIs": "scr_fetch_web_is",
"fetch.streamIs": "scr_fetch_stream_is",
"zlib.streamIs": "scr_zlib_stream_is",
"fetch.streamNew": "scr_fetch_stream_new",
"fetch.streamFrom": "scr_fetch_stream_from",
"fetch.readerRead": "scr_fetch_reader_read",
@@ -931,8 +939,18 @@ export const LIB_FN_SYMS: Record<string, string> = {
"worker.root": "scr_worker_root",
"worker.data": "scr_worker_data",
"worker.parentPort": "scr_worker_parent_port",
"worker.resourceLimits": "scr_worker_resource_limits",
"worker.threadName": "scr_worker_thread_name_current",
"worker.channel": "scr_worker_message_channel",
"worker.receiveMessageOnPort": "scr_worker_receive_message_on_port",
"worker.setEnvironmentData": "scr_worker_set_environment_data",
"worker.getEnvironmentData": "scr_worker_get_environment_data",
"worker.markUntransferable": "scr_worker_mark_untransferable",
"worker.isMarkedUntransferable": "scr_worker_is_marked_untransferable",
"dyn.markUncloneable": "scr_mark_uncloneable",
"stream.workerStdio": "scr_stream_worker_stdio",
"worker.broadcastChannel": "scr_worker_broadcast_channel",
"event.broadcastInstall": "scr_et_broadcast_install",
"process.stdinDestroy": "scr_process_stdin_destroy",
"process.stdinSetRawMode": "scr_process_stdin_set_raw_mode",
// node:events EventEmitter (scr_events_emitter.c): receivers borrowed,
@@ -961,6 +979,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"emitter.getMax": "scr_emitter_get_max",
"emitter.setDefaultMax": "scr_emitter_set_default_max",
"emitter.getDefaultMax": "scr_emitter_get_default_max",
"emitter.esmDefaultMax": "scr_emitter_esm_default_max_get",
"emitter.esmSync": "scr_emitter_esm_sync",
"emitter.errorMonitor": "scr_emitter_error_monitor",
// node:stream (scr_stream.c): receivers borrowed, chunks borrowed,
// chaining forms answer the receiver +1. The throwing members
@@ -1223,10 +1243,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
// loop gate already runs) and diagnostics_channel (scr_dc.c): all
// generic shapes; the throwing members ride the pending check.
"tp.setTimeout": "scr_tp_set_timeout",
"tp.schedulerWait": "scr_tp_set_timeout",
"tp.setImmediate": "scr_tp_set_immediate",
"tp.setTimeoutValue": "scr_tp_set_timeout_value",
"tp.setImmediateValue": "scr_tp_set_immediate_value",
"tp.setTimeoutSignal": "scr_tp_set_timeout_signal",
"tp.schedulerWaitValue": "scr_tp_set_timeout_value",
"tp.schedulerWaitSignal": "scr_tp_set_timeout_signal",
"tp.setImmediateSignal": "scr_tp_set_immediate_signal",
// stream/promises' finished form (+1 promise; may-throw pending check;
// the pipeline form needs the stream-array compound and emits below).
@@ -1324,6 +1347,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.reflectGet": "scr_dyn_reflect_get",
"dyn.usingMethod": "scr_dyn_using_method",
"dyn.usingDispose": "scr_dyn_using_dispose",
"disposable.stackNew": "scr_disposable_stack_new",
"disposable.stackIs": "scr_disposable_stack_is",
"disposable.stackUseCallback": "scr_disposable_stack_use_callback",
"dyn.reflectSet": "scr_dyn_reflect_set",
"dyn.reflectDefine": "scr_dyn_reflect_define",
"dyn.abstractEq": "scr_dyn_abstract_eq",
@@ -1376,6 +1402,40 @@ export const LIB_FN_SYMS: Record<string, string> = {
"timers.immediatePromise": "scr_immediate_promise",
};
/** The mock-aware entry points (scr_test.c) that the timer calls of a
* program using mock.timers emit in place of the loop's functions; each
* falls back to the loop's function while its API is not mocked. */
export const MOCK_TIMER_SYMS: Readonly<Record<string, string>> = {
"timers.setTimeout": "scr_test_mt_set_timeout",
"timers.setTimeoutHandle": "scr_test_mt_set_timeout_handle",
"timers.setInterval": "scr_test_mt_set_interval",
"timers.clearTimeout": "scr_test_mt_clear_timeout",
"timers.clearInterval": "scr_test_mt_clear_interval",
"timers.unref": "scr_test_mt_unref",
"timers.ref": "scr_test_mt_ref",
"timers.refresh": "scr_test_mt_refresh",
"timers.hasRef": "scr_test_mt_has_ref",
"timers.setImmediate": "scr_test_mt_set_immediate",
"timers.clearImmediate": "scr_test_mt_clear_immediate",
"timers.immediateUnref": "scr_test_mt_immediate_unref",
"timers.immediateRef": "scr_test_mt_immediate_ref",
"timers.immediateHasRef": "scr_test_mt_immediate_has_ref",
"date.now": "scr_test_mt_date_now",
"date.newNow": "scr_test_mt_date_now",
"tp.schedulerWait": "scr_test_mt_scheduler_wait",
"tp.schedulerWaitValue": "scr_test_mt_scheduler_wait_value",
"tp.schedulerWaitSignal": "scr_tp_scheduler_wait_signal",
};
/** The mock-aware symbol for a timer call of a program that uses
* mock.timers, undefined for other calls and for `realTimer` calls. */
export function mockTimerSym(
host: { readonly mockTimers: boolean },
e: { fn: string; realTimer?: true },
): string | undefined {
return host.mockTimers && !e.realTimer ? MOCK_TIMER_SYMS[e.fn] : undefined;
}
export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.emitWarning",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
@@ -1393,6 +1453,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"worker.parentPort",
"worker.channel",
"worker.receiveMessageOnPort",
"worker.broadcastChannel",
"process.stdio",
"fetch.start",
"fetch.input",
@@ -45,6 +45,10 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
return finishRuntimeCall(host, e, "scr_test_ctx_name", a);
case "test.ctxFullName":
return finishRuntimeCall(host, e, "scr_test_ctx_full_name", a);
case "test.ctxPassed":
return finishRuntimeCall(host, e, "scr_test_ctx_passed", a);
case "test.workerId":
return finishRuntimeCall(host, e, "scr_test_worker_id", a);
case "test.ctxHook":
host.moveTemp(args[2]!);
return finishRuntimeCall(host, e, "scr_test_ctx_hook", a);
@@ -66,6 +70,34 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a);
case "test.ctxMockReset":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_reset", a);
case "test.snapshotValue":
return finishRuntimeCall(host, e, "scr_test_snapshot_value", a);
case "test.ctxSnapshot":
return finishRuntimeCall(host, e, "scr_test_ctx_snapshot", a);
case "test.fileSnapshot":
return finishRuntimeCall(host, e, "scr_test_file_snapshot", a);
case "test.snapshotSerialize":
return finishRuntimeCall(host, e, "scr_test_snapshot_serialize", a);
case "test.mockTimersEnable":
return finishRuntimeCall(host, e, "scr_test_mock_timers_enable", a);
case "test.mockTimersTick":
return finishRuntimeCall(host, e, "scr_test_mock_timers_tick", a);
case "test.mockTimersRunAll":
return finishRuntimeCall(host, e, "scr_test_mock_timers_run_all", a);
case "test.mockTimersReset":
return finishRuntimeCall(host, e, "scr_test_mock_timers_reset", a);
case "test.mockTimersSetTime":
return finishRuntimeCall(host, e, "scr_test_mock_timers_set_time", a);
case "test.ctxMockTimersEnable":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_enable", a);
case "test.ctxMockTimersTick":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_tick", a);
case "test.ctxMockTimersRunAll":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_run_all", a);
case "test.ctxMockTimersReset":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_reset", a);
case "test.ctxMockTimersSetTime":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_set_time", a);
default:
throw new InternalCompilerError(`unhandled test call ${e.fn}`);
}
@@ -0,0 +1,195 @@
/* Runtime guards for checker-proven narrowings.
*
* `unionNarrow`, `downcast` and `caughtNarrow` extract a payload whose arm,
* class or kind TypeScript's control-flow narrowing proved. Emitted code
* verifies that proof once at the narrowing site: the tag, class interval or
* caught kind is compared and a mismatch stops the program through the
* runtime trap funnel instead of reading the payload as the wrong type. The
* frontend lowers every narrowing it knows it cannot prove through checked
* extraction, which throws the catchable TypeError, so a failing guard means
* either a compiler bug or a program whose hand-written type predicate or
* assertion function (`x is T`, `asserts x is T`) lied about a value's type:
* TypeScript trusts those claims without checking them. The guard aborts
* rather than unwinding through code that assumed the extraction could not
* throw. The guards are cheap: one load and compare per site, which LLVM
* folds where an identical test already dominates. */
import { InternalCompilerError } from "../../errors.js";
import { classMembershipIntervals } from "./classes.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { vAdapters } from "./shapes.js";
const NARROW_GUARD_HINT = " (a type predicate or assertion may be wrong)\n";
export const NARROW_GUARD_MESSAGES = {
union: `scriptc: a narrowed value does not have the type its narrowing proved${NARROW_GUARD_HINT}`,
downcast: `scriptc: a narrowed object is not an instance of the class its narrowing proved${NARROW_GUARD_HINT}`,
caught: `scriptc: a narrowed catch binding does not have the type its narrowing proved${NARROW_GUARD_HINT}`,
} as const;
/** Continues in a fresh block where `ok` holds; otherwise traps with the
* message for `kind`. */
export function emitNarrowGuard(
host: LlvmEmitterContext,
ok: string,
kind: keyof typeof NARROW_GUARD_MESSAGES,
): void {
const B = host.B;
host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
host.declare(`declare void @scr_trap(ptr)`);
const expected = B.tmp();
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${ok}, i1 true)`);
const pass = B.newLabel("narrow.ok");
const fail = B.newLabel("narrow.bad");
B.condBr(expected, pass, fail);
B.startBlock(fail);
B.line(`call void @scr_trap(ptr ${host.cstr(NARROW_GUARD_MESSAGES[kind])})`);
B.terminate("unreachable");
B.startBlock(pass);
}
/** Verifies that the union value `unionName` holds arm `tag`. A two-arm
* union holds it exactly when it does not hold the other arm; testing the
* other arm lets LLVM fold the guard under a dominating `!== other` test as
* well as under `=== tag` (a tag outside the arms is not a union value). */
export function emitUnionNarrowGuard(
host: LlvmEmitterContext,
unionName: string,
unionId: string,
tag: number,
): void {
const ok = host.B.tmp();
const actual = host.unionTag(unionName, unionId);
if (host.unionsById.get(unionId)?.arms.length === 2)
host.B.line(`${ok} = icmp ne i32 ${actual}, ${1 - tag}`);
else host.B.line(`${ok} = icmp eq i32 ${actual}, ${tag}`);
emitNarrowGuard(host, ok, "union");
}
/** The i1 `instanceof` test of the borrowed object `objName`, statically a
* `staticClassName`, against `targetClassName`'s preorder intervals. */
export function emitClassMembership(
host: LlvmEmitterContext,
objName: string,
staticClassName: string,
targetClassName: string,
comment: string,
): string {
const B = host.B;
const target = host.classMetaOf(targetClassName);
const pre = host.loadVtPre(objName, staticClassName);
let result: string | undefined;
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
const ge = B.tmp(),
le = B.tmp(),
test = B.tmp();
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
B.line(`${test} = and i1 ${ge}, ${le} ; ${comment}`);
if (result === undefined) result = test;
else {
const combined = B.tmp();
B.line(`${combined} = or i1 ${result}, ${test}`);
result = combined;
}
}
return result!;
}
/** Verifies that the borrowed object `objName` is a `targetClassName`. */
export function emitDowncastGuard(
host: LlvmEmitterContext,
objName: string,
staticClassName: string,
targetClassName: string,
): void {
const ok = emitClassMembership(
host,
objName,
staticClassName,
targetClassName,
`downcast ${targetClassName}`,
);
emitNarrowGuard(host, ok, "downcast");
}
/** The i1 `e instanceof C` test of the borrowed catch snapshot `caught`.
* Hierarchy classes compare the OBJ payload's vtable preorder; standalone
* class payloads have no vtable word, so their retain adapter identifies
* the exact native layout. */
export function emitCaughtInstanceof(
host: LlvmEmitterContext,
caught: string,
className: string,
): string {
const B = host.B;
const target = host.classMetaOf(className);
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
if (!target.hierarchy) {
const adapterSlot = B.tmp();
const adapter = B.tmp();
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`);
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
let result: string | undefined;
for (const candidate of host.classMeta.values()) {
if (
candidate.hierarchy ||
!intervals.some((range) => range.pre <= candidate.pre && candidate.pre <= range.post)
)
continue;
const rc = vAdapters(host.shapeHost, { kind: "object", className: candidate.def.name });
const matches = B.tmp();
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
if (result === undefined) result = matches;
else {
const joined = B.tmp();
B.line(`${joined} = or i1 ${result}, ${matches}`);
result = joined;
}
}
if (result === undefined) throw new InternalCompilerError("empty standalone class membership");
return result;
}
host.declare(
`declare zeroext i1 @scr_caught_instanceof(ptr, ${host.sizeType}, ${host.sizeType})`,
);
let result: string | undefined;
for (const interval of intervals) {
const test = B.tmp();
B.line(
`${test} = call zeroext i1 @scr_caught_instanceof(ptr ${caught}, ${host.sizeType} ${interval.pre}, ${host.sizeType} ${interval.post})`,
);
if (result === undefined) result = test;
else {
const combined = B.tmp();
B.line(`${combined} = or i1 ${result}, ${test}`);
result = combined;
}
}
if (result === undefined) throw new InternalCompilerError("empty class membership");
return result;
}
/** SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2, SCR_EXC_STR = 3 (scr_exception.h). */
export const CAUGHT_SCALAR_KINDS = { f64: 1, bool: 2, string: 3 } as const;
/** Verifies that the borrowed catch snapshot `caught` holds a value of the
* narrowed type: a scalar's kind tag, or an object's class. */
export function emitCaughtNarrowGuard(
host: LlvmEmitterContext,
caught: string,
type: { kind: "f64" | "bool" | "string" } | { kind: "object"; className: string },
): void {
const B = host.B;
let ok: string;
if (type.kind === "object") {
ok = emitCaughtInstanceof(host, caught, type.className);
} else {
const kp = B.tmp();
const k = B.tmp();
ok = B.tmp();
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`);
B.line(`${k} = load i32, ptr ${kp}`);
B.line(`${ok} = icmp eq i32 ${k}, ${CAUGHT_SCALAR_KINDS[type.kind]}`);
}
emitNarrowGuard(host, ok, "caught");
}
@@ -0,0 +1,83 @@
import { expect, test } from "vitest";
import {
F64,
NULL_T,
UNDEFINED_T,
type IrModule,
type IrType,
type IrUnionDef,
} from "../../ir/ir.js";
import { NullableUnions } from "./nullable-unions.js";
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
const a: IrType = { kind: "record", shapeId: "a" };
const b: IrType = { kind: "record", shapeId: "b" };
const c: IrType = { kind: "record", shapeId: "c" };
function module(unions: IrUnionDef[], extra: Partial<IrModule> = {}): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
records: ["a", "b", "c"].map((id) => ({ id, fields: [{ name: id, type: F64 }] })),
unions,
functions: [],
...extra,
};
}
test("unions of two or more records become shape unions", () => {
const unions = new NullableUnions(
module([
{ id: "ab", arms: [a, b] },
{ id: "abu", arms: [a, UNDEFINED_T, b] },
{ id: "abnu", arms: [a, b, NULL_T, UNDEFINED_T] },
{ id: "af", arms: [a, F64] },
{ id: "au", arms: [a, UNDEFINED_T] },
]),
);
expect(unions.shapes("ab")).toMatchObject({ unitTag: -1, nullTag: -1 });
expect(unions.shapes("abu")).toMatchObject({ unitTag: 1, nullTag: -1 });
expect(unions.shapes("abnu")).toMatchObject({ unitTag: 3, nullTag: 2 });
expect(unions.shapes("abnu")?.arms).toEqual([
{ tag: 0, shapeId: "a" },
{ tag: 1, shapeId: "b" },
]);
// A scalar arm keeps the box; one record arm is a single-arm nullable.
expect(unions.get("af")).toBeNull();
expect(unions.single("au")).not.toBeNull();
expect(unions.shapes("au")).toBeNull();
// Tagged shapes number from 2 and move their fields after the word.
expect([unions.shapeNumber("a"), unions.shapeNumber("b"), unions.shapeNumber("c")]).toEqual([
2, 3, 0,
]);
expect([unions.fieldBase("a"), unions.fieldBase("c")]).toEqual([2, 1]);
expect(unions.boxedUnions(new Map([["af", { id: "af", arms: [a, F64] }]])).size).toBe(1);
});
test("records of runtime-inspected unions keep every union over them boxed", () => {
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
const unions = new NullableUnions(
module(
[
{ id: "ab", arms: [a, b] },
{ id: "abu", arms: [a, b, UNDEFINED_T] },
{ id: "bc", arms: [b, c] },
{ id: "ac", arms: [a, c] },
],
{ globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] },
),
);
expect(unions.get("ab")).toBeNull();
expect(unions.get("abu")).toBeNull();
expect(unions.get("bc")).toBeNull();
expect(unions.get("ac")).toBeNull();
expect(unions.shapeNumber("c")).toBe(0);
});
test("library, lifetime-region and debug builds keep record unions boxed", () => {
const unions = [{ id: "ab", arms: [a, b] }];
expect(new NullableUnions(module(unions), false).get("ab")).toBeNull();
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
});
@@ -17,6 +17,7 @@ import { everyModuleNode } from "../../ir/traverse.js";
* every container, field, capture, parameter, and return slot holds the
* plain pointer. Every other union keeps the tagged `%ScrUnion` box. */
export interface NullableUnion {
repr: "single";
unionId: string;
refTag: number;
/** The unit arm NULL encodes. */
@@ -24,7 +25,57 @@ export interface NullableUnion {
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
* -1 when the union has one unit arm. */
nullTag: number;
arm: NullableRefArm;
/** One of NullableRefArm. Typed as the plain IrType so the self-hosted
* frontend keeps IrType's structurally identical arms one record shape
* (a PointerUnion arm holding the narrowed intersection would split them). */
arm: IrType;
}
/** A union of two or more record arms and at most two unit arms (`A | B`,
* `A | B | undefined`, discriminated unions of object literals) whose
* VALUES are the record pointer itself, like a NullableUnion: NULL is the
* unit arm (`undefined` when both exist, NULLABLE_NULL is then `null`).
* Every record shape that is an arm of such a union is a TAGGED shape: its
* struct carries the shape's module-wide number in a hidden `i32` word
* right after the reference count (struct index 1), stored by every
* allocation, so the arm of a pointer is that word mapped through the
* union's arm table (the per-union `@sc_stag.<id>` helper). Records share
* one retain; release and trace dispatch on the arm. */
export interface ShapeUnion {
repr: "shapes";
unionId: string;
/** The unit arm NULL encodes; -1 when the union has no unit arm. */
unitTag: number;
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
* -1 otherwise. */
nullTag: number;
/** The record arms in tag order. */
arms: readonly { tag: number; shapeId: string }[];
}
/** A union whose values are plain pointers (no `%ScrUnion` box). */
export type PointerUnion = NullableUnion | ShapeUnion;
/** The struct index of a tagged record shape's hidden shape-number word. */
export const SHAPE_WORD_INDEX = 1;
/** The LLVM type through which any tagged record's shape word is read
* (the reference count, then the word). */
export function shapeWordHeader(sizeType: "i32" | "i64"): string {
return `{ ${sizeType}, i32 }`;
}
/** The per-union helper mapping a shape-union value to its tag. */
export function shapeTagFn(unionId: string): string {
return `@sc_stag.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
/** The per-union release and trace dispatchers of a shape union. */
export function shapeReleaseFn(unionId: string): string {
return `@sc_srelease.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
export function shapeTraceFn(unionId: string): string {
return `@sc_strace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
/** The module-scope immortal sentinel standing for `null` in a
@@ -48,44 +99,98 @@ export type NullableRefArm =
| (IrType & { kind: "array" | "map" | "set" });
/** The arm is a JS object: always truthy, compared by identity. */
export function isObjectArm(nullable: NullableUnion): boolean {
return nullable.arm.kind !== "string";
export function isObjectArm(nullable: PointerUnion): boolean {
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
}
/** The representation decision for every union of one module. It is a
* pure function of the module, so serialized-IR and in-memory emission
* agree. */
export class NullableUnions {
private readonly byId = new Map<string, NullableUnion>();
private readonly byId = new Map<string, PointerUnion>();
/** Tagged record shapes (arms of a shape union) by their module-wide
* shape number (2 and up: 0 and 1 are the unit encodings' pseudo
* numbers, never stored). */
private readonly shapeNumbers = new Map<string, number>();
/** NULL-skipping retain wrappers requested so far: symbol → inner retain
* call target. The emitter defines each once. */
readonly retainWrappers = new Map<string, string>();
/** Some nullable union encodes `null` with NULLABLE_NULL. */
readonly usesNullSentinel: boolean = false;
constructor(mod: IrModule) {
/** `shapes`: allow shape unions (debug builds keep boxes, whose
* debug-info layouts describe every arm). */
constructor(mod: IrModule, shapes = true) {
const emitted = new Set<string>();
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
const records = new Set<string>();
for (const shape of mod.records ?? []) records.add(shape.id);
const boxed = boxedUnionIds(mod);
// Shape unions stay out of library builds (their host ABI marshals
// boxes) and of programs with lifetime regions (whose walks report a
// reference slot by its type's release entry point).
const allowShapes = shapes && mod.lib === undefined && mod.lifetimeRegions !== true;
// A record that is an arm of a boxed union never becomes a shape
// union's arm: conversions between unions over the same records then
// keep one representation.
const boxedRecords = new Set<string>();
for (const def of mod.unions ?? [])
if (boxed.has(def.id))
for (const arm of def.arms) if (arm.kind === "record") boxedRecords.add(arm.shapeId);
for (const def of mod.unions ?? []) {
if (boxed.has(def.id)) continue;
const nullable = nullableShape(def, emitted, records);
const nullable =
nullableShape(def, emitted, records) ??
(allowShapes ? shapeUnion(def, records, boxedRecords) : null);
if (!nullable) continue;
this.byId.set(def.id, nullable);
if (nullable.nullTag >= 0) this.usesNullSentinel = true;
if (nullable.repr === "shapes")
for (const arm of nullable.arms)
if (!this.shapeNumbers.has(arm.shapeId))
this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2);
}
}
get(unionId: string): NullableUnion | null {
get(unionId: string): PointerUnion | null {
return this.byId.get(unionId) ?? null;
}
of(type: IrType): NullableUnion | null {
of(type: IrType): PointerUnion | null {
return type.kind === "union" ? this.get(type.unionId) : null;
}
/** The single-reference-arm form only (shape unions answer null). */
single(unionId: string): NullableUnion | null {
const nullable = this.byId.get(unionId);
return nullable?.repr === "single" ? nullable : null;
}
/** The shape-union form only. */
shapes(unionId: string): ShapeUnion | null {
const nullable = this.byId.get(unionId);
return nullable?.repr === "shapes" ? nullable : null;
}
/** Every shape union, in module order. */
allShapeUnions(): ShapeUnion[] {
const out: ShapeUnion[] = [];
for (const nullable of this.byId.values()) if (nullable.repr === "shapes") out.push(nullable);
return out;
}
/** The module-wide number a tagged record shape stores in its shape
* word, or 0 for shapes that carry none. */
shapeNumber(shapeId: string): number {
return this.shapeNumbers.get(shapeId) ?? 0;
}
/** The struct index of a record shape's first declared field: 2 for
* tagged shapes (after the shape word), 1 otherwise. */
fieldBase(shapeId: string): number {
return this.shapeNumbers.has(shapeId) ? 2 : 1;
}
has(unionId: string): boolean {
return this.byId.has(unionId);
}
@@ -94,10 +199,11 @@ export class NullableUnions {
return this.byId.size;
}
/** The reference arm of every nullable union, by union id. */
/** The reference arm of every single-arm nullable union, by union id. */
armTypes(): Map<string, IrType> {
const result = new Map<string, IrType>();
for (const [id, nullable] of this.byId) result.set(id, nullable.arm);
for (const [id, nullable] of this.byId)
if (nullable.repr === "single") result.set(id, nullable.arm);
return result;
}
@@ -129,7 +235,7 @@ function nullableShape(
if (unitTag < 0 || nullTag < 0) return null;
}
const arm = def.arms[refTag]!;
const base = { unionId: def.id, refTag, unitTag, nullTag };
const base = { repr: "single" as const, unionId: def.id, refTag, unitTag, nullTag };
if (arm.kind === "object" && emittedClasses.has(arm.className))
return { ...base, arm: { kind: "object", className: arm.className } };
if (arm.kind === "record" && records.has(arm.shapeId))
@@ -140,6 +246,34 @@ function nullableShape(
return null;
}
/** Two or more emitted record arms and at most two unit arms (both:
* undefined and null). */
function shapeUnion(
def: IrUnionDef,
records: ReadonlySet<string>,
boxedRecords: ReadonlySet<string>,
): ShapeUnion | null {
const units = def.arms.filter(isUnitType).length;
if (units > 2 || def.arms.length - units < 2) return null;
const arms: { tag: number; shapeId: string }[] = [];
for (let tag = 0; tag < def.arms.length; tag++) {
const arm = def.arms[tag]!;
if (isUnitType(arm)) continue;
if (arm.kind !== "record" || !records.has(arm.shapeId) || boxedRecords.has(arm.shapeId))
return null;
arms.push({ tag, shapeId: arm.shapeId });
}
let unitTag = -1,
nullTag = -1;
if (units === 1) unitTag = def.arms.findIndex(isUnitType);
else if (units === 2) {
unitTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
nullTag = def.arms.findIndex((arm) => arm.kind === "nullT");
if (unitTag < 0 || nullTag < 0) return null;
}
return { repr: "shapes", unionId: def.id, unitTag, nullTag, arms };
}
/** Unions the runtime itself inspects as tagged boxes keep that
* representation: Map keys and Set elements hash and compare union keys by
* tag and payload inside the C map. */
+21 -2
View File
@@ -25,6 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
import { FN_ATTRS } from "./shapes.js";
import { shapeTagFn } from "./nullable-unions.js";
import type { InlineUnionFields } from "./inline-unions.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -44,6 +45,10 @@ export interface PublishHost {
readonly inlineUnions: InlineUnionFields;
/** The nullable-pointer representation of a union, when it has one. */
nullableArm(t: IrType): IrType | null;
/** The union's values are tagged record pointers (nullable-unions.ts). */
shapeUnion(t: IrType): boolean;
/** The struct index of a record shape's first field. */
fieldBase(shapeId: string): number;
/** Class layout: hierarchy membership, preorder interval, field slots
* (with their storage types), runtime roots. */
classInfo(className: string): PublishClassInfo;
@@ -210,6 +215,19 @@ export class PublishWalkers {
if (def.arms.some((arm) => scalarRefusal(arm)))
throw new LlvmUnsupportedError(`publish of ${typeKey(t)}`, loc);
const live = arms.flatMap((a) => (a.fn === null ? [] : [{ tag: a.tag, fn: a.fn }]));
if (this.host.shapeUnion(t)) {
// The value is the record itself (or a unit encoding): no box to
// mark; the arm's walker marks and walks the record.
const lines = [head, `entry:`];
lines.push(
` %tag = call i32 ${shapeTagFn(t.unionId)}(ptr %o)`,
` switch i32 %tag, label %done [ ${live.map((a) => `i32 ${a.tag}, label %arm${a.tag}`).join(" ")} ]`,
);
for (const a of live)
lines.push(`arm${a.tag}:`, ` call void ${a.fn}(ptr %o, ptr %ctx)`, ` br label %done`);
lines.push(`done:`, ` ret void`, `}`);
return lines;
}
const lines = [
head,
`entry:`,
@@ -240,17 +258,18 @@ export class PublishWalkers {
const shape = this.host.recordsById.get(t.shapeId);
if (!shape)
throw new LlvmUnsupportedError(`publish of an unknown record ${t.shapeId}`, loc);
const base = this.host.fieldBase(shape.id);
const members = [
...shape.fields.flatMap((f, i) =>
this.host.inlineUnions.record(f.type)
? []
: [{ name: f.name, index: i + 1, type: f.type }],
: [{ name: f.name, index: i + base, type: f.type }],
),
...(shape.indexValue
? [
{
name: "[key: string]",
index: shape.fields.length + 1,
index: shape.fields.length + base,
type: mapOf(STRING, shape.indexValue),
},
]
@@ -21,6 +21,16 @@ export function resultEntryName(name: string): string {
return `${RESULT_ENTRY_PREFIX}${name}`;
}
/** The local id of a result-slot entry's destination parameter. The
* caller passes zeroed frame storage, and on every path the entry stores
* each of its fields at most once, so a store into it is an
* initialization: there is no old value to release. */
export const RESULT_DST_ID = "%into.dst";
function distinctNames(names: readonly string[]): boolean {
return new Set(names).size === names.length;
}
/** A plain record shape whose fields are numbers, booleans and inline
* scalar unions (inline-unions.ts): its fields own nothing. */
export function scalarRecordShape(
@@ -38,6 +48,15 @@ export function scalarRecordShape(
);
}
/** A closed record or tuple shape: declared fields only. Its instance can
* live in caller storage; fields that own references are released by the
* frame that holds the storage (stack-objects.ts). Tuples are element
* records: destructuring and element reads project them, and every
* array-like use of one is a whole-value use that keeps it on the heap. */
export function frameRecordShape(shape: IrRecordShape | undefined): boolean {
return shape !== undefined && !shape.indexValue;
}
/** A literal that stores every field of its shape, none of them through
* the overflow map, and that sets no overflow-map flag. */
export function completeRecordLiteral(
@@ -110,7 +129,7 @@ export function constructorInitializesFields(cls: IrClassDef, ctor: IrFunction):
}
/** Add a result-slot entry for every function whose returns all build a
* fresh scalar record or standalone scalar object, return another such
* fresh closed record or standalone scalar object, return another such
* producer's result (forwarding the slot), or return a parameter of a
* record shape no store can change (copied into the slot). The original
* function stays as it is; the backend calls the entry only where the
@@ -155,8 +174,12 @@ export function addRecordResultEntries(mod: IrModule): IrModule {
},
});
const dynamicStores = dynamicFieldStores(mod);
// Copies are limited to scalar shapes: a copy of a reference-owning
// record would not carry the undeclared source keys a checked conversion
// may keep beside the original (record-sources.ts).
const copyable = (type: IrType): boolean =>
type.kind === "record" &&
scalarRecordShape(shapes.get(type.shapeId), inline) &&
mod.lib === undefined &&
!written.has(type.shapeId) &&
!dynamicStores.all &&
@@ -165,9 +188,11 @@ export function addRecordResultEntries(mod: IrModule): IrModule {
const fillable = (value: IrExpr, params: ReadonlySet<string>): boolean => {
switch (value.kind) {
case "recordLit":
return completeRecordLiteral(
value,
shapes.get((value.type as IrType & { kind: "record" }).shapeId),
return (
completeRecordLiteral(
value,
shapes.get((value.type as IrType & { kind: "record" }).shapeId),
) && distinctNames(value.fields.map((field) => field.name))
);
case "new":
return stackClass(value.className);
@@ -175,6 +200,15 @@ export function addRecordResultEntries(mod: IrModule): IrModule {
return fillable(value.then, params) && fillable(value.else_, params);
case "varRef":
return params.has(value.localId) && copyable(value.type);
case "recordClone":
// A spread of a binding: the copies read it before any override
// evaluates, as the heap clone does.
return (
value.source.kind === "varRef" &&
value.type.kind === "record" &&
frameRecordShape(shapes.get(value.type.shapeId)) &&
distinctNames(value.overrides.map((override) => override.name))
);
case "call":
return producers.has(value.callee);
default:
@@ -210,6 +244,39 @@ export function addRecordResultEntries(mod: IrModule): IrModule {
loc: value.loc,
}));
}
case "recordClone": {
const shapeId = (value.type as IrType & { kind: "record" }).shapeId;
const overridden = new Set(value.overrides.map((override) => override.name));
const copies: IrStmt[] = shapes
.get(shapeId)!
.fields.filter((field) => !overridden.has(field.name))
.map((field): IrStmt => ({
kind: "recordSet",
obj: dst,
shapeId,
field: field.name,
value: {
kind: "recordGet",
obj: value.source,
shapeId,
field: field.name,
type: field.type,
loc: value.loc,
},
loc: value.loc,
}));
return [
...copies,
...value.overrides.map((override): IrStmt => ({
kind: "recordSet",
obj: dst,
shapeId,
field: override.name,
value: override.value,
loc: override.value.loc,
})),
];
}
case "call":
return [
{
@@ -327,7 +394,7 @@ export function addRecordResultEntries(mod: IrModule): IrModule {
return false;
const ret = fn.returnType;
return (
(ret.kind === "record" && scalarRecordShape(shapes.get(ret.shapeId), inline)) ||
(ret.kind === "record" && frameRecordShape(shapes.get(ret.shapeId))) ||
(ret.kind === "object" && stackClass(ret.className))
);
});
@@ -360,7 +427,7 @@ export function addRecordResultEntries(mod: IrModule): IrModule {
for (const fn of candidates) {
if (!producers.has(fn.name)) continue;
const ret = fn.returnType;
const dstId = "%into.dst";
const dstId = RESULT_DST_ID;
const dst: IrExpr = { kind: "varRef", localId: dstId, type: ret, loc: fn.loc };
const body = transformStmtList(fn.body, {
expr: (e) => e,
@@ -106,6 +106,7 @@ const INERT_LIB_CALLS = new Set([
"regex.groupParseInt",
"process.cwd",
"process.argv",
"process.execArgv",
"process.platform",
"process.execPath",
"process.isTTY",
@@ -101,8 +101,8 @@ export class LifetimeRegions {
/** Can a value of type `t` hold (or own something that holds) a
* reference to a ProgramLifetime instance the walk can explain? */
private reaches(t: IrType): boolean {
const arm = this.host.nullableUnions.of(t)?.arm;
if (arm !== undefined) return this.reaches(arm);
const nullable = this.host.nullableUnions.of(t);
if (nullable?.repr === "single") return this.reaches(nullable.arm);
switch (t.kind) {
case "object":
return this.isLifetime(t.className) || this.reach.get(`object:${t.className}`) === true;
@@ -287,7 +287,8 @@ export class LifetimeRegions {
* ProgramLifetime instance). */
private reportLines(struct: string, f: Field, i: number): string[] {
if (!isRefCounted(f.type) || !this.reaches(f.type)) return [];
const arm = this.host.nullableUnions.of(f.type)?.arm ?? f.type;
const nullable = this.host.nullableUnions.of(f.type);
const arm = nullable?.repr === "single" ? nullable.arm : f.type;
const load = [
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
` %v${i} = load ptr, ptr %f${i}`,
@@ -411,7 +412,7 @@ export class LifetimeRegions {
...this.reportBody(
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk record ${shape.id}`,
mangleRecordStruct(shape.id),
rcMembers(shape, this.host.inlineUnions),
rcMembers(shape, this.host.inlineUnions, this.host.nullableUnions.fieldBase(shape.id)),
),
);
rows.push(`{ ptr, ptr } { ptr @${mangleRecordRelease(shape.id)}, ptr ${walk} }`);
+134 -9
View File
@@ -31,7 +31,16 @@ import {
import { CYC_HDR, cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
import type { BlockBuilder } from "./blocks.js";
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
import type { NullableUnions } from "./nullable-unions.js";
import {
NULLABLE_NULL,
SHAPE_WORD_INDEX,
shapeReleaseFn,
shapeTagFn,
shapeTraceFn,
shapeWordHeader,
type NullableUnions,
type ShapeUnion,
} from "./nullable-unions.js";
import {
INLINE_UNION_FIELD,
loadInlineUnion,
@@ -194,6 +203,7 @@ export { computeTraced } from "../cycle-analysis.js";
* `_v`-shaped. */
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
const nullable = host.nullableUnions.of(t);
if (nullable?.repr === "shapes") return shapeUnionRc(nullable);
if (nullable) {
const arm = vAdapters(host, nullable.arm);
return { retain: nullableRetainSym(host, nullable.arm), release: arm.release };
@@ -249,6 +259,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release
* Families with a mirrored fast path call the inline helper instead. */
export function retainSym(host: ShapeHost, t: IrType): string {
const nullable = host.nullableUnions.of(t);
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain;
if (nullable) return nullableRetainSym(host, nullable.arm);
const stem = runtimeRcStem(t);
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) {
@@ -260,6 +271,99 @@ export function retainSym(host: ShapeHost, t: IrType): string {
return vAdapters(host, t).retain;
}
/** A shape union's RC entry points: every record retain is the same
* NULL- and immortal-skipping increment (the null sentinel is immortal),
* so the first arm's serves; release dispatches on the arm. */
function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } {
return {
retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`,
release: shapeReleaseFn(union.unionId),
};
}
/** The tag, release and trace helpers of every shape union
* (nullable-unions.ts). The tag helper maps a value to its arm: NULL and
* the null sentinel are the unit arms, anything else a tagged record whose
* shape word names its arm (the switch folds to a lookup table). */
export function emitShapeUnionHelpers(host: ShapeHost): string[] {
const out: string[] = [];
const S = host.sizeType;
for (const union of host.nullableUnions.allShapeUnions()) {
const header = shapeWordHeader(S);
const tagFn = shapeTagFn(union.unionId);
const cases = union.arms
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`)
.join(" ");
out.push(
`define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`,
`entry:`,
` %isnull = icmp eq ptr %u, null`,
` br i1 %isnull, label %unit, label %ref`,
`unit:`,
` ret i32 ${union.unitTag}`,
`ref:`,
);
if (union.nullTag >= 0)
out.push(
` %isnullarm = icmp eq ptr %u, ${NULLABLE_NULL}`,
` br i1 %isnullarm, label %nullarm, label %rec`,
`nullarm:`,
` ret i32 ${union.nullTag}`,
`rec:`,
);
out.push(
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
` %w = load i32, ptr %wp`,
` switch i32 %w, label %bad [ ${cases} ]`,
...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
`bad:`,
` ret i32 -1`,
`}`,
``,
);
// release: unit encodings are NULL or immortal; each arm releases
// through its own (NULL- and immortal-tolerant) helper.
out.push(
`define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`,
`entry:`,
` %t = call i32 ${tagFn}(ptr %u)`,
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
...union.arms.flatMap((arm) => [
`a${arm.tag}:`,
` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`,
` br label %done`,
]),
`done:`,
` ret void`,
`}`,
``,
);
if (shapeUnionTraced(host, union))
out.push(
`define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
`entry:`,
` %t = call i32 ${tagFn}(ptr %u)`,
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
...union.arms.flatMap((arm) => [
`a${arm.tag}:`,
` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`,
` br label %done`,
]),
`done:`,
` ret void`,
`}`,
``,
);
}
return out;
}
/** A shape union's arms carry collector headers (all or none: the emitter
* traces every arm of a union with one traced arm). */
function shapeUnionTraced(host: ShapeHost, union: ShapeUnion): boolean {
return union.arms.some((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
}
/** The retain entry point of a nullable union's value: the arm's own when
* it tolerates NULL (emitted class and record helpers), otherwise a
* NULL-skipping wrapper around it (the null sentinel and literals are
@@ -298,6 +402,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] {
* Families with a mirrored fast path call the inline helper instead. */
export function releaseSym(host: ShapeHost, t: IrType): string {
const nullable = host.nullableUnions.of(t);
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release;
if (nullable) return releaseSym(host, nullable.arm);
const stem = runtimeRcStem(t);
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) {
@@ -708,6 +813,8 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null {
return "@scr_promise_trace_v";
case "union": {
const nullable = host.nullableUnions.get(t.unionId);
if (nullable?.repr === "shapes")
return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null;
if (nullable) return traceAdapter(host, nullable.arm);
if (!host.tracedUnions.has(t.unionId)) return null;
host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`);
@@ -1010,15 +1117,17 @@ export function mapValKindNum(value: IrType): number {
export function rcMembers(
shape: IrRecordShape,
inline: InlineUnionFields,
/** The struct index of the first field (2 on tagged shapes). */
base = 1,
): { index: number; type: IrType; name: string }[] {
return [
...shape.fields.flatMap((f, i) =>
inline.record(f.type) ? [] : [{ index: i + 1, type: f.type, name: f.name }],
inline.record(f.type) ? [] : [{ index: i + base, type: f.type, name: f.name }],
),
...(shape.indexValue
? [
{
index: shape.fields.length + 1,
index: shape.fields.length + base,
type: mapOf(STRING, shape.indexValue),
name: "[key: string] overflow",
},
@@ -1172,7 +1281,12 @@ export function directDestroyTarget(
}
function directDestroyKey(host: ShapeHost, type: IrType): string | null {
const t = host.nullableUnions.of(type)?.arm ?? type;
const nullable = host.nullableUnions.of(type);
let t = type;
if (nullable !== null) {
if (nullable.repr === "shapes") return null;
t = nullable.arm;
}
if (t.kind === "record") return `record:${t.shapeId}`;
return t.kind === "object" ? `object:${t.className}` : null;
}
@@ -1403,8 +1517,12 @@ export function emitRecordShapes(
for (const shape of records) {
const struct = mangleRecordStruct(shape.id);
const fieldTys = shape.fields.map((f) => llFieldType(f.type, host.inlineUnions.record(f.type)));
const fieldTys: string[] = shape.fields.map((f) =>
llFieldType(f.type, host.inlineUnions.record(f.type)),
);
if (shape.indexValue) fieldTys.push("ptr"); // the overflow ScrMap *
// A tagged shape (an arm of a shape union) carries its shape number.
if (host.nullableUnions.shapeNumber(shape.id) !== 0) fieldTys.unshift("i32");
typeDefs.push(
`%${struct} = type { ${host.sizeType}${fieldTys.length ? ", " + fieldTys.join(", ") : ""} } ` +
`; record ${shape.id} { ${shape.fields.map((f) => llvmCommentText(f.name)).join("; ")}${shape.indexValue ? "; [key: string]" : ""} }`,
@@ -1417,7 +1535,7 @@ export function emitRecordShapes(
const candidates: [string, DirectDestroy][] = [];
const memberTypes: IrType[] = [];
for (const shape of records) {
const members = rcMembers(shape, host.inlineUnions);
const members = rcMembers(shape, host.inlineUnions, host.nullableUnions.fieldBase(shape.id));
for (const m of members) memberTypes.push(m.type);
if (!members.some((m) => isRefCounted(m.type) && needsBoundedRelease(m.type))) continue;
const release = mangleRecordRelease(shape.id);
@@ -1435,7 +1553,9 @@ export function emitRecordShapes(
for (const shape of records) {
const struct = mangleRecordStruct(shape.id);
const traced = host.tracedShapes.has(`record:${shape.id}`);
const members = rcMembers(shape, host.inlineUnions);
const base = host.nullableUnions.fieldBase(shape.id);
const shapeNumber = host.nullableUnions.shapeNumber(shape.id);
const members = rcMembers(shape, host.inlineUnions, base);
const refMembers = members.filter((m) => isRefCounted(m.type));
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
@@ -1504,6 +1624,11 @@ export function emitRecordShapes(
);
nw.push(...emitObjectAlloc(host, sizeOf, traced ? { type: cycType } : null));
nw.push(` store ${host.sizeType} 1, ptr %o`);
if (shapeNumber !== 0)
nw.push(
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
);
if (shape.indexValue) {
// The overflow map (string-keyed): value handling is type-directed
// exactly like shapes.ts's overflowNewC.
@@ -1514,7 +1639,7 @@ export function emitRecordShapes(
const trace = valKind === 2 ? traceArg(host, v) : "null";
nw.push(
` %ovf = call ptr @scr_map_new(i32 1, i32 ${valKind}, ptr ${host.mapOps(["null", "null", "null", rc.retain, rc.release, trace, "null"])})`,
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + 1}`,
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + base}`,
` store ptr %ovf, ptr %ovfp`,
);
}
@@ -1531,7 +1656,7 @@ export function emitRecordShapes(
];
let i = 0;
for (const field of shape.fields) {
const index = i + 1;
const index = i + base;
const inline = host.inlineUnions.record(field.type);
const fieldTy = llFieldType(field.type, inline);
clone.push(
@@ -275,3 +275,64 @@ console.log(pick({ x: 1, y: 2 }, { x: 3, y: 4 }, true).y, forward({ x: 1, y: 1 }
.sort();
expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}forward`, `${RESULT_ENTRY_PREFIX}pick`]);
});
const TOKENS = `
interface Token { kind: string; text: string; pos: number }
function make(kind: string, text: string, pos: number): Token { return { kind, text, pos }; }
function width(t: Token): number { return t.text.length + t.kind.length; }
function divmod(a: number, b: number): [number, number] { return [Math.floor(a / b), a % b]; }
interface State { name: string; count: number }
function bump(s: State): State { return { ...s, count: s.count + 1 }; }
export function tokens(n: number, w: string): number {
const t = { kind: "id", text: w, pos: n };
const u = make("num", w, n);
const [q, r] = divmod(n, 7);
return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + q + r;
}
export function states(n: number, w: string): number {
let s: State = { name: w, count: 0 };
for (let i = 0; i < n; i++) {
s = { ...s, count: s.count + 1 };
s = bump(s);
}
return s.count + s.name.length;
}
console.log(tokens(3, "ab"), states(4, "cd"));
`;
test("records and tuples that own references live in frame storage", async () => {
const ir = emitLlvmModule(await moduleFor(TOKENS));
const tokens = body(ir, "tokens");
expect(tokens).not.toMatch(/@sc_rnew_/);
expect(tokens).toMatch(/call void @sc_\w*into_make\(/);
expect(tokens).toMatch(/call void @sc_\w*into_divmod\(/);
// Reference fields start empty in entry-addressed slots, and the frame
// releases them; an interned literal field (`kind: "id"`) needs neither.
const fields = [...tokens.matchAll(/(%s\d+) = getelementptr inbounds %sc_rs_\w+, ptr %s\d+/g)];
const owned = fields.filter(([, ptr]) => tokens.includes(`store ptr null, ptr ${ptr}\n`));
expect(owned.length).toBe(5);
expect(fields.length).toBe(6);
for (const [, ptr] of owned)
expect(tokens).toMatch(new RegExp(`load ptr, ptr ${ptr}\\n\\s+call void @\\w+\\(`));
});
test("spread updates copy into the binding's slot and release the replaced fields", async () => {
const mod = await moduleFor(TOKENS);
const entries = addRecordResultEntries(mod)
.functions.map((fn) => fn.name)
.filter((name) => name.startsWith(RESULT_ENTRY_PREFIX));
expect(entries).toEqual(
expect.arrayContaining([
`${RESULT_ENTRY_PREFIX}make`,
`${RESULT_ENTRY_PREFIX}divmod`,
`${RESULT_ENTRY_PREFIX}bump`,
]),
);
const states = body(emitLlvmModule(mod), "states");
expect(states).not.toMatch(/@sc_rnew_|@sc_rclone_/);
expect(states).toMatch(/call void @sc_\w*into_bump\(/);
// The old name is loaded before the copy and released after it.
expect(states).toMatch(
/(%t\d+) = load ptr, ptr %s\d+\n\s+(%t\d+) = load %sc_rs_\w+, ptr %s\d+\n\s+store %sc_rs_\w+ \2, ptr %s\d+\n(?:.*\n)*?\s+call void @\w+\(ptr \1\)/,
);
});
@@ -1,9 +1,16 @@
import { VOID, typeEquals, type IrExpr, type IrFunction, type IrType } from "../../ir/ir.js";
import {
VOID,
isRefCounted,
typeEquals,
type IrExpr,
type IrFunction,
type IrType,
} from "../../ir/ir.js";
import {
completeRecordLiteral,
constructorInitializesFields,
frameRecordShape,
resultEntryName,
scalarRecordShape,
scalarStandaloneClass,
} from "./record-results.js";
import { everyStmtList } from "../../ir/traverse.js";
@@ -14,23 +21,46 @@ import type { CallLifetimes } from "./call-lifetimes.js";
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { emitRecordLitFields } from "./expr-primitives.js";
import { SHAPE_WORD_INDEX } from "./nullable-unions.js";
import { llFieldType } from "./shapes.js";
export type StackObjectSource = IrExpr & { kind: "recordLit" | "new" | "call" };
export type StackObjectSource = IrExpr & { kind: "recordLit" | "recordClone" | "new" | "call" };
/** A record literal, `new` or result-slot producer call whose object can
* live in the frame's own storage: a plain scalar record shape, or a
/** A field of a frame object that owns a reference: an entry-block pointer
* to its slot, and the field's type for the release. */
export interface StackObjectField {
name: string;
ptr: string;
type: IrType;
}
/** Records the owners of a frame object's reference fields before anything
* stores them: a lexical scope for bindings, a statement frame for
* temporaries. */
export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => void;
/** A record literal, spread copy, `new` or result-slot producer call whose
* object can live in the frame's own storage: a closed record shape, or a
* standalone scalar class whose constructor never lets `this` escape
* (record-results.ts). The caller proves that every use of the value only
* projects it. */
* projects it. Record fields may own references; the frame releases them
* (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 {
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
// caller's storage.
if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee));
if (e.kind === "recordLit") {
if (e.type.kind !== "record") return false;
const shape = host.recordsById.get(e.type.shapeId);
return scalarRecordShape(shape, host.inlineUnions) && completeRecordLiteral(e, shape);
return frameRecordShape(shape) && completeRecordLiteral(e, shape);
}
// A same-shape spread (`{ ...source, field: value }`) copies the source's
// fields; the frontend builds it only for plain declared-field records.
if (e.kind === "recordClone")
return e.type.kind === "record" && frameRecordShape(host.recordsById.get(e.type.shapeId));
if (e.kind !== "new") return false;
const meta = host.classMeta.get(e.className);
return (
@@ -45,6 +75,33 @@ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackO
);
}
/** Whether a frame object of this type has fields that own references. */
function ownsReferences(host: LlvmEmitterContext, type: IrType): boolean {
return (
type.kind === "record" &&
host
.recordShape(type.shapeId)
.fields.some((field) => isRefCounted(field.type) && !host.inlineUnions.record(field.type))
);
}
/** Locals declared directly in a switch case. The cases share one scope
* whose exit runs whichever case was entered, so a scope-owned field would
* be released without its declaration having run. */
function caseScopedLocals(fn: IrFunction): Set<string> {
const ids = new Set<string>();
everyStmtList(fn.body, {
expr: () => true,
stmt: (stmt) => {
if (stmt.kind === "switch")
for (const c of stmt.cases)
for (const s of c.body) if (s.kind === "varDecl") ids.add(s.localId);
return true;
},
});
return ids;
}
/** Locals initialized once by a stack-eligible object and only projected
* afterwards (CallLifetimes.locals): field reads, and arguments to
* parameters that are themselves only projected. */
@@ -56,6 +113,7 @@ export function findLocalStackObjects(
const result = new Map<string, StackObjectSource>();
const safe = lifetimes.locals.get(fn.name);
if (!safe?.size || fn.async || fn.generator) return result;
const caseScoped = caseScopedLocals(fn);
everyStmtList(fn.body, {
expr: () => true,
stmt: (stmt) => {
@@ -63,7 +121,8 @@ export function findLocalStackObjects(
stmt.kind === "varDecl" &&
safe.has(stmt.localId) &&
stmt.init &&
canStackObject(host, stmt.init)
canStackObject(host, stmt.init) &&
!(caseScoped.has(stmt.localId) && ownsReferences(host, stmt.init.type))
)
result.set(stmt.localId, stmt.init);
return true;
@@ -112,7 +171,10 @@ export function findReassignedStackObjects(
return true;
},
});
for (const id of result) if (!initialized.has(id)) result.delete(id);
const caseScoped = caseScopedLocals(fn);
for (const id of result)
if (!initialized.has(id) || (caseScoped.has(id) && ownsReferences(host, types.get(id)!)))
result.delete(id);
return result;
}
@@ -122,13 +184,78 @@ export function stackObjectStruct(type: IrType): string {
: `%${mangleClassStruct((type as IrType & { kind: "object" }).className)}`;
}
/** The reference fields of the frame object in `slot`, addressed from the
* entry block so that every cleanup path can reach them. Inline union
* fields copy bits and own nothing; class objects in frame storage are
* scalar. */
export function stackObjectFields(
host: LlvmEmitterContext,
type: IrType,
slot: string,
): readonly StackObjectField[] {
if (type.kind !== "record") return [];
const cached = host.stackObjectFieldPtrs.get(slot);
if (cached) return cached;
const struct = stackObjectStruct(type);
const base = host.nullableUnions.fieldBase(type.shapeId);
const fields: StackObjectField[] = [];
host.recordShape(type.shapeId).fields.forEach((field, index) => {
if (!isRefCounted(field.type) || host.inlineUnions.record(field.type)) return;
const ptr = host.B.slot();
host.B.entryAllocas.push(
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + base}`,
);
fields.push({ name: field.name, ptr, type: field.type });
});
host.stackObjectFieldPtrs.set(slot, fields);
return fields;
}
/** Copy a same-shape spread's source fields into `slot`, retaining the
* references, then store the overrides in source order. The source is
* read once, before any override evaluates, as the heap clone helper
* reads it; an overridden field is never copied, so its slot stays empty
* until the override initializes it. */
function emitRecordCloneFields(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordClone" },
slot: string,
): void {
const B = host.B;
const shapeId = (e.type as IrType & { kind: "record" }).shapeId;
const overridden = new Set(e.overrides.map((override) => override.name));
const source = host.emitReadReceiver(e.source);
for (const field of host.recordShape(shapeId).fields) {
if (overridden.has(field.name)) continue;
const from = host.recordFieldPtr(source.name, shapeId, field.name);
const to = host.recordFieldPtr(slot, shapeId, field.name);
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;
B.line(`store ${fieldTy} ${stored}, ptr ${to.ptr}`);
}
const initialized = new Set<string>();
for (const override of e.overrides) {
const v = host.emitFieldValue(override.value, host.recordFieldInline(shapeId, override.name));
const { ptr, type, inline } = host.recordFieldPtr(slot, shapeId, override.name);
// The first store fills an empty slot; a repeated name replaces it.
if (initialized.has(override.name)) host.replaceField(ptr, type, inline, v);
else host.initField(ptr, type, inline, v);
initialized.add(override.name);
}
}
/** Build the object in an entry-block slot (or `into`, an existing one).
* The immortal reference count makes every retain and release a no-op;
* the proof that the value is only projected keeps the pointer from
* outliving the frame. */
* outliving the frame. Reference fields start empty and are handed to
* `owner` before anything is stored, so an exception while the object is
* being built releases exactly the fields stored so far. */
export function emitStackObject(
host: LlvmEmitterContext,
e: StackObjectSource,
owner: StackObjectFieldOwner,
into?: string,
): LlValue {
const B = host.B;
@@ -139,6 +266,18 @@ export function emitStackObject(
slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${struct}, align 8`);
}
// A literal's interned string fields are immortal and need no owner. A
// binding's own slot (`into`) keeps an owner for every field: later
// assignments copy arbitrary values into it.
const literals =
into === undefined && e.kind === "recordLit"
? new Set(e.fields.filter((f) => f.value.kind === "strLit").map((f) => f.name))
: new Set<string>();
const fields = stackObjectFields(host, type, slot).filter((f) => !literals.has(f.name));
if (fields.length > 0) {
for (const field of fields) B.line(`store ptr null, ptr ${field.ptr}`);
owner(fields);
}
// Literals store every field; constructors that store every field
// before reading one need no zeroed storage either.
if (type.kind === "object") {
@@ -159,6 +298,17 @@ export function emitStackObject(
}
}
B.line(`store ${host.sizeType} -1, ptr ${slot}`);
if (type.kind === "record") {
// A tagged shape's word names it to every shape union's tag read.
const shapeNumber = host.nullableUnions.shapeNumber(type.shapeId);
if (shapeNumber !== 0) {
const p = B.tmp();
B.line(
`${p} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${SHAPE_WORD_INDEX}`,
);
B.line(`store i32 ${shapeNumber}, ptr ${p}`);
}
}
if (e.kind === "call") {
// The entry writes the result into the slot it receives last.
emitCallExpr(
@@ -169,6 +319,7 @@ export function emitStackObject(
host.markImmortal({ name: slot, type }),
);
} else if (e.kind === "recordLit") emitRecordLitFields(host, e, slot);
else if (e.kind === "recordClone") emitRecordCloneFields(host, e, slot);
else emitConstruction(host, e, slot, host.fnByName.get(`%${e.className}.constructor`)!);
return { name: slot, type: e.type };
}
@@ -1,7 +1,11 @@
import { expect, test } from "vitest";
import { F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import { analyzeCallLifetimes } from "./call-lifetimes.js";
import { findScalarStringSlices } from "./string-slices.js";
import {
findLentStringParameters,
findScalarStringSlices,
findScratchStringSlices,
} from "./string-slices.js";
const loc = { file: "slices.ts", start: 0, end: 0 };
const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: STRING, loc });
function fixture(): IrFunction {
@@ -95,3 +99,146 @@ test.each(["escape", "capture", "write", "other-consumer", "suspend"])(
expect(facts(fn).size).toBe(0);
},
);
const slice = (receiver: string, start: number, end: number): IrExpr => ({
kind: "strIntrinsic",
method: "slice",
receiver: ref(receiver),
args: [
{ kind: "numLit", value: start, type: F64, loc },
{ kind: "numLit", value: end, type: F64, loc },
],
type: STRING,
loc,
});
const equals = (left: IrExpr): IrExpr => ({
kind: "strEq",
negated: false,
left,
right: { kind: "strLit", value: "GET", type: STRING, loc },
type: BOOL,
loc,
});
const parse = (value: IrExpr): IrExpr => ({
kind: "libCall",
fn: "num.fromString",
args: [value],
type: F64,
loc,
});
function lending(body: IrStmt[]): IrFunction {
return {
name: "scan",
params: [{ localId: "source", name: "source", type: STRING }],
locals: [
{ id: "source", name: "source", type: STRING, mutable: false },
{ id: "word", name: "word", type: STRING, mutable: false },
],
returnType: VOID,
loc,
body,
};
}
function scratch(fn: IrFunction, extra: IrFunction[] = []): Set<IrExpr> {
const functions = new Map([fn, ...extra].map((f) => [f.name, f]));
const lifetimes = analyzeCallLifetimes(functions);
return findScratchStringSlices(
fn,
lifetimes,
findScalarStringSlices(fn, lifetimes),
findLentStringParameters(functions, lifetimes),
);
}
test("slices read only by lending consumers use a stack buffer", () => {
const inline = slice("source", 0, 3);
const parsed = slice("source", 4, 6);
const init = slice("source", 1, 5);
const fn = lending([
{ kind: "exprStmt", expr: equals(inline), loc },
{ kind: "exprStmt", expr: parse(parsed), loc },
{ kind: "varDecl", localId: "word", init, loc },
{ kind: "exprStmt", expr: equals(ref("word")), loc },
{ kind: "exprStmt", expr: parse(ref("word")), loc },
]);
expect(scratch(fn)).toEqual(new Set([inline, parsed, init]));
});
test.each(["return", "concat", "slice-of-slice", "capture", "async"])(
"a slice local that may be kept (%s) stays owned",
(reason) => {
const init = slice("source", 1, 5);
const fn = lending([
{ kind: "varDecl", localId: "word", init, loc },
{ kind: "exprStmt", expr: equals(ref("word")), loc },
]);
if (reason === "return") fn.body.push({ kind: "return", value: ref("word"), loc });
if (reason === "concat")
fn.body.push({
kind: "exprStmt",
expr: {
kind: "strConcat",
left: { kind: "strLit", value: "", type: STRING, loc },
right: ref("word"),
type: STRING,
loc,
},
loc,
});
if (reason === "slice-of-slice")
fn.body.push({ kind: "exprStmt", expr: equals(slice("word", 0, 2)), loc });
if (reason === "capture") fn.locals[1]!.boxed = true;
if (reason === "async") fn.async = true;
expect(scratch(fn).has(init)).toBe(false);
},
);
function helper(name: string, body: IrStmt[]): IrFunction {
return {
name,
params: [{ localId: "text", name: "text", type: STRING }],
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
returnType: BOOL,
loc,
body,
};
}
const callOf = (callee: string, arg: IrExpr): IrExpr => ({
kind: "call",
callee,
args: [arg],
type: BOOL,
loc,
});
test("helper parameters read only by lending consumers accept stack slices", () => {
const recursive = helper("isGet", [
{
kind: "return",
value: {
kind: "logical",
op: "||",
left: equals(ref("text")),
right: callOf("isGet", ref("text")),
type: BOOL,
loc,
},
loc,
},
]);
const keeps = helper("keeps", [{ kind: "return", value: callOf("stores", ref("text")), loc }]);
const stores = helper("stores", [
{
kind: "exprStmt",
expr: { kind: "arrayLit", elems: [ref("text")], type: { kind: "array", elem: STRING }, loc },
loc,
},
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
]);
const lent = slice("source", 0, 3);
const kept = slice("source", 0, 4);
const fn = lending([
{ kind: "exprStmt", expr: callOf("isGet", lent), loc },
{ kind: "exprStmt", expr: callOf("keeps", kept), loc },
]);
const functions = [recursive, keeps, stores];
expect(scratch(fn, functions)).toEqual(new Set([lent]));
const all = new Map([fn, ...functions].map((f) => [f.name, f]));
expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]);
});
@@ -1,4 +1,9 @@
import { type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import {
type IrExpr,
type IrFunction,
type IrStmt,
type IrStrIntrinsicMethod,
} from "../../ir/ir.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
import type { CallLifetimes } from "./call-lifetimes.js";
@@ -6,6 +11,12 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
type Slice = IrExpr & { kind: "strIntrinsic" };
function isSlice(value: IrExpr): value is Slice {
return (
value.kind === "strIntrinsic" && (value.method === "slice" || value.method === "substring")
);
}
/** A local used exclusively for scalar indexing needs no copied bytes.
* Snapshot the receiver and normalized bounds at its initializer, including
* side effects in the bounds. Escapes, captures, writes, suspension, and all
@@ -111,3 +122,234 @@ export function emitStringSliceRead(host: LlvmEmitterContext, value: Slice): LlV
);
return { name: result, type: value.type };
}
/** Bytes of the stack buffer behind one scratch slice, header included. */
export const SCRATCH_SLICE_BYTES = 96;
/** String methods that read a lent receiver or argument only during their
* runtime call and never return it. Methods returning a string can return
* their unchanged receiver, so they stay on the owned path. */
const LENDING_METHODS: ReadonlySet<IrStrIntrinsicMethod> = new Set([
"length",
"charCodeAt",
"indexOf",
"includes",
"startsWith",
"endsWith",
]);
/** Numeric parsers read their text during the call. */
const LENDING_LIB_FNS: ReadonlySet<string> = new Set([
"num.fromString",
"num.parseInt",
"num.parseFloat",
]);
/** Typed string-keyed collection operations: reads probe with the key, and
* an insertion stores scr_str_scratch_keep's copy (scr_map_append). */
const LENDING_KEY_METHODS: ReadonlySet<string> = new Set(["get", "has", "set", "add", "delete"]);
function stringKeyed(value: IrExpr & { kind: "mapIntrinsic" | "setIntrinsic" }): boolean {
const receiver = value.receiver.type;
if (receiver.kind === "map")
return receiver.key.kind === "string" && receiver.value.kind !== "dyn";
if (receiver.kind === "set") return receiver.elem.kind === "string";
return false;
}
/** String parameters, by function and index, that the body reads only in
* lending positions (findLentStringParameters). */
export type LentStringParameters = ReadonlyMap<string, ReadonlySet<number>>;
/** The operands `value` reads only while it runs, keeping no reference. */
function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly IrExpr[] {
switch (value.kind) {
case "strEq":
case "strCmp":
return [value.left, value.right];
case "strIntrinsic":
return LENDING_METHODS.has(value.method) ? [value.receiver, ...value.args] : [];
case "libCall":
return LENDING_LIB_FNS.has(value.fn) ? value.args.slice(0, 1) : [];
case "mapIntrinsic":
case "setIntrinsic":
return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value)
? value.args.slice(0, 1)
: [];
case "call": {
const lent = parameters.get(value.callee);
return lent ? value.args.filter((_, index) => lent.has(index)) : [];
}
default:
return [];
}
}
interface LendingScan {
/** Slice operands of lending consumers, excluding any node also reached
* in an owning position. */
scratch: Set<IrExpr>;
/** Candidates read anywhere other than a lending position. */
invalid: Set<string>;
/** Direct callees, for the parameter fixpoint. */
callees: Set<string>;
}
/** One pass over `fn`: which candidate bindings only lending consumers read
* (a slice initializer of a candidate local is not itself a read), and
* which slice operands those consumers lend. A switch discriminant is only
* compared with the case tests. */
function scanLending(
fn: IrFunction,
candidates: ReadonlySet<string>,
initializers: ReadonlyMap<string, Slice>,
parameters: LentStringParameters,
): LendingScan {
const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() };
const owned = new Set<IrExpr>();
function lent(value: IrExpr): boolean {
if (value.kind === "varRef" && candidates.has(value.localId)) return true;
if (!isSlice(value)) return expr(value);
scan.scratch.add(value);
return everyExprChild(value, expr, stmt);
}
function expr(value: IrExpr): boolean {
if (isSlice(value)) owned.add(value);
if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId);
if (value.kind === "closure" || value.kind === "classRef")
for (const id of value.captures ?? []) scan.invalid.add(id);
if (value.kind === "call") scan.callees.add(value.callee);
const operands = lentOperands(value, parameters);
if (operands.length === 0) return everyExprChild(value, expr, stmt);
return everyExprChild(
value,
(child) => (operands.includes(child) ? lent(child) : expr(child)),
stmt,
);
}
function stmt(value: IrStmt): boolean {
// A candidate's single declaration initializes it with its slice.
if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId))
return everyExprChild(value.init, expr, stmt);
if (value.kind === "switch" && value.disc.type.kind === "string") {
lent(value.disc);
for (const entry of value.cases) {
if (entry.test) expr(entry.test);
entry.body.forEach(stmt);
}
return true;
}
return everyStmtChild(value, expr, stmt);
}
fn.body.forEach(stmt);
for (const node of owned) scan.scratch.delete(node);
return scan;
}
/** String parameters that every body reads only in lending positions,
* including lent arguments of other such parameters: a caller may pass a
* scratch slice to them. Start from every borrowed (unchanged) string
* parameter of a synchronous function and drop violations until nothing changes, so
* recursion through a lent parameter stays lent. A coroutine can keep its
* parameters past the caller's frame, so it never lends. */
export function findLentStringParameters(
functions: ReadonlyMap<string, IrFunction>,
lifetimes: CallLifetimes,
): Map<string, Set<number>> {
const lent = new Map<string, Set<number>>();
for (const fn of functions.values()) {
if (fn.async || fn.generator) continue;
// Borrowed parameters are the unchanged ones nothing redeclares.
const stable = lifetimes.borrowed.get(fn.name);
const indexes = new Set<number>();
fn.params.forEach((param, index) => {
if (param.type.kind === "string" && stable?.has(index)) indexes.add(index);
});
if (indexes.size > 0) lent.set(fn.name, indexes);
}
const callers = new Map<string, Set<string>>();
const pending = new Set(lent.keys());
while (pending.size > 0) {
const name = pending.values().next().value as string;
pending.delete(name);
const indexes = lent.get(name);
const fn = functions.get(name);
if (!indexes || !fn) continue;
const ids = new Set([...indexes].map((index) => fn.params[index]!.localId));
const scan = scanLending(fn, ids, new Map(), lent);
for (const callee of scan.callees) {
let set = callers.get(callee);
if (!set) callers.set(callee, (set = new Set()));
set.add(name);
}
let changed = false;
for (const index of [...indexes]) {
if (!scan.invalid.has(fn.params[index]!.localId)) continue;
indexes.delete(index);
changed = true;
}
if (!changed) continue;
if (indexes.size === 0) lent.delete(name);
const affected = callers.get(name);
if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller);
}
return lent;
}
/** Slices whose every reader borrows them are written into a stack buffer
* instead of a heap allocation (scr_str_slice_scratch): a slice operand of
* a lending consumer, and the initializer of a stable local read only by
* lending consumers. The copy is ASCII, bounded, and immortal for retain and
* release, so it neither pins its source nor needs to be freed; longer or
* non-ASCII results keep the ordinary owned string. Each site owns its
* buffer: a site runs again only after its previous value is dead, since
* nothing can keep it. Coroutine frames do not keep stack buffers across a
* suspension, so they are left alone. */
export function findScratchStringSlices(
fn: IrFunction,
lifetimes: CallLifetimes,
scalar: ReadonlyMap<string, Slice>,
parameters: LentStringParameters = new Map(),
): Set<IrExpr> {
if (fn.async || fn.generator) return new Set();
const bindings = lifetimes.bindings.get(fn.name);
const locals = new Map<string, Slice>();
everyStmtList(fn.body, {
expr: () => true,
stmt: (stmt) => {
if (
stmt.kind === "varDecl" &&
bindings?.has(stmt.localId) &&
!scalar.has(stmt.localId) &&
stmt.init !== null &&
isSlice(stmt.init)
)
locals.set(stmt.localId, stmt.init);
return true;
},
});
const scan = scanLending(fn, new Set(locals.keys()), locals, parameters);
for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init);
return scan.scratch;
}
/** Write a scratch slice into its own stack buffer. The result is owned:
* releasing the scratch copy does nothing, and a heap fallback is freed. */
export function emitScratchStringSlice(
host: LlvmEmitterContext,
slice: Slice,
receiver: LlValue,
args: readonly LlValue[],
): LlValue {
const B = host.B;
const buffer = B.slot();
B.entryAllocas.push(`${buffer} = alloca [${SCRATCH_SLICE_BYTES} x i8], align 8`);
host.declare(
`declare ptr @scr_str_slice_scratch(ptr, double, double, i1 zeroext, ptr, ${host.sizeType})`,
);
const result = B.tmp();
B.line(
`${result} = call ptr @scr_str_slice_scratch(ptr ${receiver.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${buffer}, ${host.sizeType} ${SCRATCH_SLICE_BYTES})`,
);
return host.own({ name: result, type: slice.type });
}
@@ -2,14 +2,19 @@
* helper generators (walkers, dyn). A tagged union is a `%ScrUnion` box; a
* nullable union (nullable-unions.ts) is the arm pointer itself: NULL for
* its NULL-encoded unit arm and, in `C | null | undefined`, the immortal
* NULLABLE_NULL sentinel for `null`. Every structural read of a union value
* NULLABLE_NULL sentinel for `null`. A shape union's value is likewise the
* record pointer; its tag comes from the record's shape word. Every structural read of a union value
* goes through here. */
import type { BlockBuilder } from "./blocks.js";
import { NULLABLE_NULL, type NullableUnion } from "./nullable-unions.js";
import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js";
/** The i32 tag of a union value. */
export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
const t = B.tmp();
if (nullable?.repr === "shapes") {
B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`);
return t;
}
if (nullable) {
const isNull = B.tmp();
B.line(`${isNull} = icmp eq ptr ${u}, null`);
@@ -32,8 +37,13 @@ export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u:
/** i1: a nullable union value holds its reference arm (neither NULL nor
* the `null` sentinel). */
export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u: string): string {
export function emitNullablePresent(B: BlockBuilder, nullable: PointerUnion, u: string): string {
const present = B.tmp();
if (nullable.repr === "shapes" && nullable.unitTag < 0) {
// No unit arm: every value is a record (NULL only as a pending dummy).
B.line(`${present} = icmp ne ptr ${u}, null`);
return present;
}
B.line(`${present} = icmp ne ptr ${u}, null`);
if (nullable.nullTag < 0) return present;
const notNullArm = B.tmp(),
@@ -46,12 +56,18 @@ export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u:
/** i1: a nullable union value holds the arm `tag` (`negated`: does not). */
export function emitNullableIsTag(
B: BlockBuilder,
nullable: NullableUnion,
nullable: PointerUnion,
u: string,
tag: number,
negated: boolean,
): string {
if (tag === nullable.refTag) {
if (nullable.repr === "shapes" && tag !== nullable.unitTag && tag !== nullable.nullTag) {
const actual = emitUnionTag(B, nullable, u),
t = B.tmp();
B.line(`${t} = icmp ${negated ? "ne" : "eq"} i32 ${actual}, ${tag}`);
return t;
}
if (nullable.repr === "single" && tag === nullable.refTag) {
const present = emitNullablePresent(B, nullable, u);
if (!negated) return present;
const t = B.tmp();
@@ -65,7 +81,7 @@ export function emitNullableIsTag(
}
/** The BORROWED payload pointer of a reference arm. */
export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
if (nullable) return u;
const p = B.tmp();
const t = B.tmp();
@@ -76,7 +92,9 @@ export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u
/** Text-template forms for helpers written as raw LLVM lines: `%out`
* receives the tag / payload of `u`. */
export function unionTagLines(nullable: NullableUnion | null, u: string, out: string): string[] {
export function unionTagLines(nullable: PointerUnion | null, u: string, out: string): string[] {
if (nullable?.repr === "shapes")
return [` ${out} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`];
if (nullable && nullable.nullTag < 0)
return [
` ${out}.isnull = icmp eq ptr ${u}, null`,
@@ -95,7 +113,7 @@ export function unionTagLines(nullable: NullableUnion | null, u: string, out: st
];
}
export function unionPeekLines(nullable: NullableUnion | null, u: string, out: string): string[] {
export function unionPeekLines(nullable: PointerUnion | null, u: string, out: string): string[] {
// A plain copy of the pointer (NULL included) under the template's name.
if (nullable) return [` ${out} = getelementptr i8, ptr ${u}, i64 0`];
return [
@@ -183,7 +183,9 @@ test("a nullable-pointer source rebuilds the target box", () => {
expect(retag).not.toContain("retype.share");
});
async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }> {
/** `boxed`: keep every union a tagged box (record unions otherwise become
* shape unions, which retype without any box). */
async function llvmFor(source: string, boxed = true): Promise<{ module: IrModule; ir: string }> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-union-retype-"));
try {
const entry = join(dir, "main.ts"),
@@ -194,7 +196,7 @@ async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
const module = deserializeModule(await readFile(output, "utf8"));
expect(validateModule(module)).toEqual([]);
return { module, ir: emitLlvmModule(module) };
return { module, ir: emitLlvmModule(boxed ? keepUnionsBoxed(module) : module) };
} finally {
await rm(dir, { recursive: true, force: true });
}
@@ -253,3 +255,16 @@ console.log(leafValue(parsed));
expect(stored).not.toBeNull();
expect(ir).toContain(`load i8, ptr ${stored![1]}`);
});
test("record unions retype as the record pointer itself", async () => {
const { module, ir } = await llvmFor(VISITOR, false);
const helpers = module.functions.filter((fn) => fn.name.startsWith("%union.retag."));
expect(helpers.length).toBeGreaterThan(0);
for (const helper of helpers) {
const symbol = `sc_bf__x25_union_retag_${helper.name.slice("%union.retag.".length)}`;
const retag = body(ir, symbol);
expect(retag).not.toMatch(/@scr_union_new|retype\.share|sc_retype_guard/);
}
// No record of the visitor is ever boxed.
expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
});
+24 -27
View File
@@ -64,9 +64,6 @@ export class LlWalkers {
private get S(): "i32" | "i64" {
return this.host.sizeType;
}
private abiOffset(native64: number, wasm32: number): number {
return this.S === "i32" ? wasm32 : native64;
}
/** The value-parameter LLVM type of a writer for `t`. */
private valTy(t: IrType): string {
@@ -220,10 +217,10 @@ export class LlWalkers {
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
break;
case "dyn":
// Overflow values under an `unknown` index signature: the checked-dynamic tree
// serializes itself (runtime walker). Bare stringify of dyn stays
// frontend-fenced; this writer is reachable only through overflow
// entries.
// A checked-dynamic value serializes itself (runtime walker).
// Bare stringify of dyn stays frontend-fenced, and overflow
// entries under an `unknown` index signature go through
// scr_jb_put_dyn_entry, which also runs the value's toJSON.
this.host.declare(`declare void @scr_jb_put_dyn(ptr, ptr)`);
B.line(`call void @scr_jb_put_dyn(ptr %b, ptr %v)`);
break;
@@ -287,7 +284,8 @@ export class LlWalkers {
throw new InternalCompilerError(
`llvm emitter bug: jsonStringify of unknown shape ${shapeId}`,
);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
if (recordMayKeepSource(shape)) this.emitRecordSourceWriter(B, shapeId);
// CYCLE-CAPABLE shapes bracket the walk with the circular-detection
// stack; edge labels stamp before members whose walk can re-enter —
@@ -440,7 +438,8 @@ export class LlWalkers {
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
const name = `sc_jwf_${this.recordFieldWriters.size}`;
this.recordFieldWriters.set(shapeId, name);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
const byName = new Map(shape.fields.map((f) => [f.name, f]));
const cyclic = traceAdapter(this.host, { kind: "record", shapeId }) !== null;
const order = recordSourceFieldOrder(shape);
@@ -494,10 +493,13 @@ export class LlWalkers {
}
/** Whether the walker for `t` can leave an exception pending: cyclic
* values, and records serialized through a kept checked source. */
* values, records serialized through a kept checked source, and
* checked-dynamic values (toJSON methods, getters, BigInts). */
jsonWriteMayThrow(t: IrType, seen = new Set<string>()): boolean {
if (traceAdapter(this.host, t) !== null) return true;
switch (t.kind) {
case "dyn":
return true;
case "array":
return this.jsonWriteMayThrow(t.elem, seen);
case "union":
@@ -534,9 +536,18 @@ export class LlWalkers {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + this.host.nullableUnions.fieldBase(shape.id)}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (iv.kind === "dyn") {
// The runtime writes every entry with JSON.stringify's member rules:
// each value's toJSON runs with its key before the entry is dropped
// or written, and containers below it join the circular-structure
// stack. It may leave an exception pending.
host.declare(`declare void @scr_jb_put_dyn_entries(ptr, ptr, ptr, ptr)`);
B.line(`call void @scr_jb_put_dyn_entries(ptr %b, ptr %v, ptr ${ovf}, ptr ${first})`);
return;
}
host.declare(`declare ptr @scr_map_keys_js_order(ptr)`);
host.declare(`declare double @scr_arr_len(ptr)`);
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
@@ -576,23 +587,9 @@ export class LlWalkers {
B.line(`${val} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k}) ; value (+1)`);
}
// Undefined-valued entries drop, exactly the optional-field rule: a
// dyn value whose dyn kind follows its size_t RC word (enum
// member 6 — scr_runtime.h's ScrDynKind), or a union holding its
// undefined arm.
// union holding its undefined arm.
let skipUndef: string | null = null;
if (iv.kind === "dyn") {
const kp = B.tmp();
const kd = B.tmp();
const isu = B.tmp();
B.line(
`${kp} = getelementptr inbounds i8, ptr ${val}, i64 ${this.abiOffset(8, 4)} ; ->kind`,
);
B.line(`${kd} = load i32, ptr ${kp}`);
B.line(
`${isu} = icmp eq i32 ${kd}, 6 ; SCR_DYN_UNDEF (NULL is 0 — null members DO serialize)`,
);
skipUndef = isu;
} else if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) {
if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) {
const tag = this.unionTag(B, val, iv.unionId);
const isu = B.tmp();
B.line(`${isu} = icmp eq i32 ${tag}, ${undefinedArmTag(iv, this.host.unionsById)}`);
@@ -129,6 +129,7 @@ const WASI_UNAVAILABLE_HOST_FACTS: ReadonlyMap<string, string> = new Map([
["os.loadavg", "system load averages (WASI Preview 1 has no load API)"],
["os.uptime", "system uptime (WASI Preview 1 has no boot-time API)"],
["os.getPriority", "process scheduling priority (WASI Preview 1 has no priority API)"],
["os.setPriority", "process scheduling priority (WASI Preview 1 has no priority API)"],
["process.memoryUsageTake", "process memory statistics (WASI Preview 1 has no memory API)"],
["process.rss", "process memory statistics (WASI Preview 1 has no memory API)"],
["process.ppid", "the parent process id (WASI Preview 1 has no process API)"],
+40 -3
View File
@@ -7,8 +7,10 @@ import {
coverageEnvelope,
coveragePasses,
renderCoverage,
renderExplainDynamic,
type CoverageFailOn,
} from "../coverage/report.js";
import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js";
import { buildEnvelope } from "../diagnostics/envelope.js";
import { renderWarnings } from "../diagnostics/render.js";
import { isNativeOptimization } from "../backend/optimization.js";
@@ -45,8 +47,15 @@ function parseCli(
): ReturnType<
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
> {
// `--explain-dynamic` takes an optional value: alone it lists every unit.
const normalized = args.map((arg) => (arg === "--explain-dynamic" ? "--explain-dynamic=" : arg));
try {
return parseArgs({ args, options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
return parseArgs({
args: normalized,
options: CLI_OPTIONS,
allowPositionals: true,
allowNegative: true,
});
} catch (err) {
// parseArgs appends a paragraph about `--` and positionals to the
// unknown-option message; the first sentence is the part that names
@@ -91,6 +100,8 @@ async function main(args: string[], host: CliHost): Promise<number> {
externalTypeArgs.length > 0 ||
values.out !== undefined ||
values["emit-ir"] ||
values.tier !== undefined ||
values["explain-dynamic"] !== undefined ||
!values["keep-llvm"]
) {
fail(
@@ -152,10 +163,12 @@ async function main(args: string[], host: CliHost): Promise<number> {
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
(values["npm-static"] ?? []).length > 0 ||
externalTypeArgs.length > 0
externalTypeArgs.length > 0 ||
values.tier !== undefined ||
values["explain-dynamic"] !== undefined
) {
fail(
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
);
}
const profilePath = resolve(profileArg);
@@ -223,6 +236,15 @@ async function main(args: string[], host: CliHost): Promise<number> {
fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`);
}
const failOn = failOnRaw as CoverageFailOn | undefined;
const tierRaw = values.tier;
if (tierRaw !== undefined && !(TIER_MODES as readonly string[]).includes(tierRaw)) {
fail(`unknown --tier value "${tierRaw}" (supported: ${TIER_MODES.join(", ")})\n\n${USAGE}`);
}
const tier = tierRaw as TierMode | undefined;
const explainDynamic = values["explain-dynamic"];
if (explainDynamic !== undefined && command !== "coverage") {
fail(`--explain-dynamic is a coverage option\n\n${USAGE}`);
}
if (printNativeLinkInfo && command !== "build") {
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
}
@@ -340,6 +362,8 @@ async function main(args: string[], host: CliHost): Promise<number> {
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
...(tier !== undefined ? { tier } : {}),
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
});
if (printDiagnostics) {
// Keep stdout pure JSON for tooling.
@@ -354,6 +378,18 @@ async function main(args: string[], host: CliHost): Promise<number> {
process.stdout.write(
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
);
if (explainDynamic !== undefined && coverage.tiers !== undefined) {
process.stdout.write(
"\n" +
renderExplainDynamic(coverage.tiers, {
color,
sourceTexts,
root: process.cwd(),
query: explainDynamic,
}) +
"\n",
);
}
}
return coveragePasses(coverage, failOn) ? 0 : 1;
}
@@ -386,6 +422,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
...(tier !== undefined ? { tier } : {}),
});
if (!result.ok) {
if (printDiagnostics) {
+12
View File
@@ -69,6 +69,16 @@ Options:
local declaration file. The declaration supplies types
for analysis; the host module remains an explicit
external-boundary blocker (repeatable)
--tier <auto|static-only>
tier policy (default: auto). static-only keeps every
construct without a static lowering a compile error, so
the program has no dynamic-tier demotions. Both modes
currently compile identically
--explain-dynamic[=<name>]
coverage only: list the functions that operate on dynamic
values and the bindings, fields, functions, classes and
modules the dynamic tier would take over, with the
refusal behind each; a name shows only that unit's chain
-h, --help show this help
-v, --version print the version
`;
@@ -90,6 +100,8 @@ export const CLI_OPTIONS = {
"npm-static": { type: "string", multiple: true },
"provenance-sources": { type: "boolean", default: false },
"external-types": { type: "string", multiple: true },
tier: { type: "string" },
"explain-dynamic": { type: "string" },
lib: { type: "boolean", default: false },
profile: { type: "string" },
help: { type: "boolean", short: "h", default: false },
+10
View File
@@ -3,6 +3,7 @@ import type { NativeLinkInfo } from "./backend/native-link-info.js";
import type { NativeOptimization } from "./backend/optimization.js";
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
import type { CoverageInput } from "./coverage/report.js";
import type { TierMode } from "./frontend/tier-analysis.js";
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
@@ -45,6 +46,10 @@ export interface CompileBaseOptions {
/** Attach the machine-readable external link recipe to an object result.
* Valid only with outputKind "obj"; it never invokes a linker. */
nativeLinkInfo?: boolean;
/** `--tier` (frontend/tier-analysis.ts). "static-only" keeps every demotion
* request a refusal (design invariant I2). Until demotion lands both modes
* compile exactly as before. */
tier?: TierMode;
}
/** Executable compile options. This remains the compatibility type for the
@@ -161,6 +166,11 @@ export interface AnalyzeOptions {
* to analyze project code, but imported runtime values remain explicit
* SC1010 blockers rather than being counted as executable. */
externalTypes?: Readonly<Record<string, string>>;
/** `--tier` (see CompileBaseOptions.tier). */
tier?: TierMode;
/** `--explain-dynamic`: attach the tier-assignment report
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
explainDynamic?: boolean;
}
export interface AnalyzeResult {
+147 -1
View File
@@ -17,10 +17,13 @@ import {
type DiagnosticsEnvelope,
type LocatedDiagnostic,
type SourcePosition,
type TierUnitPosition,
type TiersEnvelope,
} from "../diagnostics/envelope.js";
import { renderDiagnostics } from "../diagnostics/render.js";
import type { LowerStats } from "../frontend/lowering/lowerer.js";
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
export interface CoverageInput {
file: string;
@@ -51,6 +54,9 @@ export interface CoverageInput {
/** --provenance-sources: diagnostics of elided pure-annotated dead
* consts (off the build; the provenance section names them). */
provenanceElided?: ScrDiagnostic[];
/** --explain-dynamic: the report-only tier assignment (which bindings,
* fields, functions, classes and modules the dynamic tier would take). */
tiers?: TierReport;
/** True when tsc preflight failed — no coverage can be computed. */
preflightFailed: boolean;
}
@@ -180,6 +186,12 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
out.push(` statements analyzed ${total}`);
out.push(` compile statically ${ok} ${c(pctColor, c(BOLD, `(${pct}%)`))}`);
const dynamic = dynamicStatements(input);
if (dynamic > 0) {
out.push(
` in dynamic tier ${dynamic} ${c(DIM, "(operate on values typed only at runtime; --explain-dynamic lists them)")}`,
);
}
if (skipped > 0) {
out.push(
@@ -432,13 +444,147 @@ export function coverageEnvelope(
stats: {
statementsTotal: total,
statementsStatic: ok,
statementsDynamic: 0,
statementsDynamic: dynamicStatements(input),
statementsFailed: failed,
functionsSkipped: skipped,
percentStatic: total === 0 ? 100 : Math.floor((ok / total) * 100),
},
}),
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
groups,
diagnostics: located,
};
}
/** Reached statements in dynamic-tier functions, never more than the
* compiled statements they are part of. */
function dynamicStatements(input: CoverageInput): number {
const compiled = input.stats.statementsTotal - input.stats.statementsFailed;
return Math.max(0, Math.min(input.stats.statementsDynamic ?? 0, compiled));
}
function tiersEnvelope(tiers: TierReport, locator: SourceLocator): TiersEnvelope {
const at = (loc: { file: string; start: number }): SourcePosition => ({
file: loc.file,
...locator.position(loc.file, loc.start),
});
const unit = (u: DemotionUnit): TierUnitPosition => ({
kind: u.kind,
name: u.name,
...(u.owner !== undefined ? { owner: u.owner } : {}),
...at(u.loc),
});
return {
mode: tiers.mode,
statementsDynamic: tiers.statementsDynamic,
dynamicFunctions: tiers.dynamicFunctions.map((fn) => ({
name: fn.name,
...at(fn.loc),
operation: fn.operation,
operationAt: at(fn.operationLoc),
statements: fn.statements,
})),
requests: tiers.requests.map((r) => ({
unit: unit(r.unit),
enclosing: unit(r.enclosing),
seed: r.seed,
scope: r.scope,
code: r.code,
message: r.message,
...at(r.loc),
})),
units: tiers.units.map((u) => ({ ...unit(u), requests: u.requests, sites: u.sites })),
};
}
/** IR function names as a user reads them: `%init.3` is a module's top
* level, `%Point.norm` the method `Point.norm`; generated helpers keep
* their `%`-prefixed names. */
function functionDisplayName(name: string): string {
if (name.startsWith("%init.")) return "<top level>";
if (/^%[A-Za-z_$][\w$]*\.[A-Za-z_$#][\w$]*$/.test(name)) return name.slice(1);
return name;
}
function unitLabel(u: DemotionUnit, root: string | undefined): string {
if (u.kind === "module") return `module ${displayPath(u.name, root)}`;
if (u.kind === "binding" && u.owner !== undefined) return `binding ${u.name} in ${u.owner}`;
return `${u.kind} ${u.name}`;
}
function unitMatches(u: DemotionUnit, query: string): boolean {
return (
u.name === query ||
u.name.endsWith(`.${query}`) ||
u.name.split(" ")[0] === query ||
`${u.owner ?? ""}:${u.name}` === query
);
}
/** `scriptc coverage --explain-dynamic[=<unit>]`: the dynamic tier today
* and the demotion chains the report-only analysis computed. With a query,
* only units whose name matches it (`x`, `Point.x`, `parse:x`). */
export function renderExplainDynamic(
tiers: TierReport,
opts: CoverageRenderOptions & { query?: string } = {},
): string {
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
const locator = new SourceLocator(opts.sourceTexts);
const where = (loc: { file: string; start: number }) => {
const p = locator.position(loc.file, loc.start);
return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`;
};
const query = opts.query !== undefined && opts.query !== "" ? opts.query : undefined;
const out: string[] = [];
out.push(
` ${c(BOLD, "dynamic tier")} ${c(DIM, `(tier assignment, report-only; --tier=${tiers.mode})`)}`,
);
if (query === undefined) {
const fns = tiers.dynamicFunctions;
out.push(
` ${fns.length} function${fns.length === 1 ? "" : "s"} already operate${fns.length === 1 ? "s" : ""} on dynamic values ${c(DIM, `(${tiers.statementsDynamic} statement${tiers.statementsDynamic === 1 ? "" : "s"})`)}`,
);
for (const fn of fns) {
out.push(
` ${functionDisplayName(fn.name)} ${c(DIM, `${where(fn.loc)}: ${fn.operation} at ${where(fn.operationLoc)}`)}`,
);
}
}
const live = tiers.requests.filter((r) => r.scope !== "unreached" && r.seed !== "excluded");
const excluded = tiers.requests.filter((r) => r.seed === "excluded").length;
const unreached = tiers.requests.filter((r) => r.scope === "unreached").length;
if (query === undefined) {
out.push("");
out.push(
` ${live.length} demotion request${live.length === 1 ? "" : "s"} ${c(DIM, "(refusals and fences the dynamic tier would replace)")}` +
(unreached > 0 ? c(DIM, `, ${unreached} more in unreached code`) : "") +
(excluded > 0 ? c(DIM, `, ${excluded} excluded by policy`) : ""),
);
}
const units = tiers.units.filter((u) => query === undefined || unitMatches(u, query));
if (query !== undefined && units.length === 0) {
out.push(` no demoted unit matches '${query}'`);
return out.join("\n");
}
for (const u of units) {
const counts = [
u.requests > 0 ? `${u.requests} request${u.requests === 1 ? "" : "s"}` : "",
u.sites > 0 ? `${u.sites} site${u.sites === 1 ? "" : "s"} inside` : "",
]
.filter((s) => s !== "")
.join(", ");
out.push(` ${unitLabel(u, opts.root)} ${c(DIM, `${where(u.loc)} (${counts})`)}`);
if (query === undefined && u.requests === 0) continue;
for (const r of live) {
if (r.unit.kind !== u.kind || r.unit.name !== u.name || r.unit.loc.start !== u.loc.start)
continue;
if (r.unit.loc.file !== u.loc.file) continue;
out.push(
` ${c(DIM, "<-")} ${r.code} ${r.seed} at ${where(r.loc)}: ${capMessage(r.message)}`,
);
if (r.enclosing.name !== r.unit.name || r.enclosing.kind !== r.unit.kind)
out.push(` ${c(DIM, "->")} ${unitLabel(r.enclosing, opts.root)} turns dynamic`);
}
}
return out.join("\n");
}
@@ -89,6 +89,9 @@ export function verdictKey(
opts: AnalyzeOptions,
cache: VerdictCacheOptions,
): string | null {
// The tier report carries source locations the semantic replay does not
// move; explaining analyses always lower.
if (opts.explainDynamic === true) return null;
let ffi = "";
if (opts.ffiProfilePath !== undefined) {
try {
@@ -120,6 +123,8 @@ export function verdictKey(
opts.ffiProfilePath ?? "",
ffi,
external.join("\n"),
// Demotion (stage S6) makes the tier mode part of the verdict.
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
].join("\0"),
);
}
@@ -411,7 +411,7 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
SC1120: {
feature: "this regex feature",
milestone: "later",
hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates",
hint: "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates",
},
// SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse.
};
+34 -2
View File
@@ -81,6 +81,34 @@ export interface DiagnosticGroup {
sites: SourcePosition[];
}
export interface TierUnitPosition extends SourcePosition {
kind: "binding" | "field" | "function" | "class" | "module";
name: string;
owner?: string;
}
/** `scriptc coverage --explain-dynamic --print=diagnostics`: the tier
* assignment of frontend/tier-analysis.ts with source positions. */
export interface TiersEnvelope {
mode: "auto" | "static-only";
statementsDynamic: number;
dynamicFunctions: (SourcePosition & {
name: string;
operation: string;
operationAt: SourcePosition;
statements: number;
})[];
requests: (SourcePosition & {
unit: TierUnitPosition;
enclosing: TierUnitPosition;
seed: string;
scope: string;
code: string;
message: string;
})[];
units: (TierUnitPosition & { requests: number; sites: number })[];
}
export interface DiagnosticsEnvelope {
schema: typeof DIAGNOSTICS_SCHEMA;
schemaVersion: typeof DIAGNOSTICS_SCHEMA_VERSION;
@@ -97,8 +125,10 @@ export interface DiagnosticsEnvelope {
stats?: {
statementsTotal: number;
statementsStatic: number;
/** Always 0: every compiled statement is native. Kept so schema
* version 1 consumers keep reading the same fields. */
/** The compiled statements (counted in statementsStatic, which covers
* every statement that compiles to native code) that belong to
* dynamic-tier functions: code operating on values whose types are only
* known at runtime. */
statementsDynamic: number;
statementsFailed: number;
functionsSkipped: number;
@@ -106,6 +136,8 @@ export interface DiagnosticsEnvelope {
};
/** build only: the produced artifact path. */
artifact?: string;
/** coverage --explain-dynamic only: the report-only tier assignment. */
tiers?: TiersEnvelope;
groups: DiagnosticGroup[];
diagnostics: LocatedDiagnostic[];
}
@@ -0,0 +1,56 @@
import { expect, test } from "vitest";
import type { ScrDiagnostic } from "./diagnostic.js";
import { renderDiagnostic, renderDiagnostics } from "./render.js";
function diag(file: string, start: number, end: number): ScrDiagnostic {
return { code: "SC2020", message: "not supported", loc: { file, start, end } };
}
test("a short line renders whole", () => {
const text = "const a = 1;\nconst b = a.c;\nconst d = 2;";
const start = text.indexOf("a.c");
expect(renderDiagnostic(diag("m.ts", start, start + 3), { text })).toBe(
[
"m.ts:2:11 - error SC2020: not supported",
"",
" 1 | const a = 1;",
" 2 | const b = a.c;",
" | ^~~",
" 3 | const d = 2;",
].join("\n"),
);
});
test("a long line renders a window around the column", () => {
const line = "x".repeat(1000) + "bad" + "y".repeat(1000);
const text = "first\n" + line + "\nlast";
const start = text.indexOf("bad");
const out = renderDiagnostic(diag("min.js", start, start + 3), { text });
const lines = out.split("\n");
expect(lines[0]).toBe("min.js:2:1001 - error SC2020: not supported");
// the window starts 120 characters before the column
expect(lines[3]).toBe(` 2 | …${"x".repeat(120)}bad${"y".repeat(277)}…`);
expect(lines[4]).toBe(` | ${" ".repeat(121)}^~~`);
// context lines share the window: "first" and "last" start before it
expect(lines[2]).toBe(" 1 | …");
expect(lines[5]).toBe(" 3 | …");
});
test("a window at the line's end keeps the last characters", () => {
const line = "a".repeat(600) + "end";
const start = line.indexOf("end");
const out = renderDiagnostic(diag("e.js", start, start + 3), { text: line });
const lines = out.split("\n");
expect(lines[2]).toBe(` 1 | …${"a".repeat(397)}end`);
expect(lines[3]).toBe(` | ${" ".repeat(398)}^~~`);
});
test("thousands of diagnostics on a minified line render without exhausting string length", () => {
// one 2 MB line with 20,000 diagnostics: rendering every frame line whole
// would need tens of gigabytes
const text = "a;".repeat(1_000_000);
const diags = Array.from({ length: 20_000 }, (_, i) => diag("bundle.js", i * 100, i * 100 + 1));
const out = renderDiagnostics(diags, new Map([["bundle.js", text]]));
expect(out.split(" - error SC2020: ").length - 1).toBe(20_000);
expect(out.length).toBeLessThan(20_000 * 1_000);
});
+62 -12
View File
@@ -1,6 +1,8 @@
/* Terminal rendering: file:line:col, a hand-rolled code frame with ±1 line
* of context and a ^~~~ underline sized to the span, optional hint line,
* and the optional attributed profile-note line after it.
* and the optional attributed profile-note line after it. A source line
* longer than FRAME_MAX_WIDTH (minified sources) shows a window of that
* width around the diagnostic's column, with "…" where text is cut.
*/
import type { ScrDiagnostic } from "./diagnostic.js";
@@ -11,6 +13,27 @@ export interface RenderOptions {
interface SourceLookup {
/** Full text of the file the diagnostic points into. */
text: string;
/** The text's lines and their start offsets; computed from `text` when
* absent. renderDiagnostics computes them once per file. */
index?: LineIndex;
}
interface LineIndex {
lines: string[];
lineStarts: number[];
}
/** Frame lines longer than this many characters show a window of it. */
const FRAME_MAX_WIDTH = 400;
/** Characters a windowed frame keeps before the diagnostic's column. */
const FRAME_LEAD = 120;
function lineIndex(text: string): LineIndex {
const lineStarts = [0];
for (let i = 0; i < text.length; i++) {
if (text[i] === "\n") lineStarts.push(i + 1);
}
return { lines: text.split("\n"), lineStarts };
}
const RED = "\x1b[31m";
@@ -33,32 +56,52 @@ export function renderDiagnostic(
const frame: string[] = [];
if (source) {
const lineStarts = [0];
for (let i = 0; i < source.text.length; i++) {
if (source.text[i] === "\n") lineStarts.push(i + 1);
}
const { lines, lineStarts } = source.index ?? lineIndex(source.text);
// the last line starting at or before the diagnostic
let lo = 0;
while (lo + 1 < lineStarts.length && lineStarts[lo + 1]! <= diag.loc.start) lo++;
let hi = lineStarts.length - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (lineStarts[mid]! <= diag.loc.start) lo = mid;
else hi = mid - 1;
}
lineNum = lo + 1;
colNum = diag.loc.start - lineStarts[lo]! + 1;
const lines = source.text.split("\n");
const lineText = lines[lineNum - 1] ?? "";
// The window every frame line shows: the whole line, or for a long
// line FRAME_MAX_WIDTH characters starting FRAME_LEAD before the column.
const windowStart =
lineText.length > FRAME_MAX_WIDTH
? Math.max(0, Math.min(colNum - 1 - FRAME_LEAD, lineText.length - FRAME_MAX_WIDTH))
: 0;
const shown = (text: string): string => {
if (windowStart === 0 && text.length <= FRAME_MAX_WIDTH) return text;
const end = windowStart + FRAME_MAX_WIDTH;
return (
(windowStart > 0 ? "…" : "") + text.slice(windowStart, end) + (text.length > end ? "…" : "")
);
};
const gutterWidth = String(Math.min(lineNum + 1, lines.length)).length;
const emitLine = (n: number) => {
const text = lines[n - 1];
if (text === undefined) return;
frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + text);
frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + shown(text));
};
emitLine(lineNum - 1);
emitLine(lineNum);
const lineText = lines[lineNum - 1] ?? "";
const caretCol = colNum - 1 - windowStart + (windowStart > 0 ? 1 : 0);
const visibleEnd =
windowStart === 0 && lineText.length <= FRAME_MAX_WIDTH
? lineText.length
: Math.min(lineText.length, windowStart + FRAME_MAX_WIDTH);
const spanOnLine = Math.max(
1,
Math.min(diag.loc.end - diag.loc.start, lineText.length - (colNum - 1)),
Math.min(diag.loc.end - diag.loc.start, visibleEnd - (colNum - 1)),
);
frame.push(
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
" ".repeat(colNum - 1) +
" ".repeat(caretCol) +
c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
);
emitLine(lineNum + 1);
@@ -94,10 +137,17 @@ export function renderDiagnostics(
const sorted = [...diags].sort(
(a, b) => a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start,
);
const indexes = new Map<string, LineIndex>();
return sorted
.map((d) => {
const text = sourceTextByFile.get(d.loc.file);
return renderDiagnostic(d, text === undefined ? undefined : { text }, opts);
if (text === undefined) return renderDiagnostic(d, undefined, opts);
let index = indexes.get(d.loc.file);
if (index === undefined) {
index = lineIndex(text);
indexes.set(d.loc.file, index);
}
return renderDiagnostic(d, { text, index }, opts);
})
.join("\n\n");
}

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