Merge main

This commit is contained in:
Chris Tate
2026-10-20 18:25:49 -05:00
228 changed files with 6481 additions and 816 deletions
+24 -23
View File
@@ -4,33 +4,34 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| Workload | Category | Exercises |
| --- | --- | --- |
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
| `ast-interp` | cpu | tokenizer, recursive-descent parser, class dispatch |
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `async-pipeline` | cpu | `async`/`await`, `Promise.all` fan-out |
| `word-graph` | cpu | string-keyed Map/Set, BFS |
| `alloc-trees` | cpu | allocation-heavy trees, immutable object-spread updates |
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
| `ast-interp` | cpu | tokenizer, recursive-descent parser, class dispatch |
| `async-pipeline` | cpu | `async`/`await`, `Promise.all` fan-out |
| `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 |
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
| `published-graphs` | cpu | syntax trees with parent links and a symbol table, published with `@scriptc/threads` and then read |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `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 |
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields |
| `published-graphs` | cpu | syntax trees with parent links and a symbol table, published with `@scriptc/threads` and then read |
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
| `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 |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
| `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 |
| `word-graph` | cpu | string-keyed Map/Set, BFS |
| `cli-config` | startup | a `deploy-plan` CLI: read a ~110 KB JSON manifest, validate, interpolate env values, order 80 services into dependency waves, print a summary |
| `http-api` | server | a `node:http` JSON API: routing, query parsing, a seeded catalog, per-cart state, typed JSON bodies, `ETag`/`If-None-Match`, auth and request-id headers |
@@ -46,7 +47,7 @@ Node is timed in the same interleaved loop and every row reports candidate/Node;
## Workload kinds
Entries in `workloads.json` are process workloads unless they set `kind`.
Each workload is a JSON file in `workloads/`, named after the workload, that holds its `entry`, `args` and options; adding a workload adds its file and its row in the table above, kept in category and then name order. Workloads are process workloads unless they set `kind`.
Process workloads time whole launches (wall time, startup included). An optional `launches` count makes each sample the median of that many sequential launches; `cli-config` uses 20 so that a startup-dominated command (a few milliseconds compiled, tens of milliseconds under Node) is measured with low noise. Its table row is labeled `(per launch)`. On a heavily loaded macOS host, whole batches of launches can run several times slower (samples become bimodal), so judge startup changes on the Linux lane or a quiet machine.
-283
View File
@@ -1,283 +0,0 @@
{
"workloads": [
{
"name": "json-records",
"entry": "workloads/json-records.ts",
"args": [
"4"
]
},
{
"name": "regex-logs",
"entry": "workloads/regex-logs.ts",
"args": [
"1"
]
},
{
"name": "ast-interp",
"entry": "workloads/ast-interp.ts",
"args": [
"5"
]
},
{
"name": "records-sort",
"entry": "workloads/records-sort.ts",
"args": [
"1"
]
},
{
"name": "template-render",
"entry": "workloads/template-render.ts",
"args": [
"1"
]
},
{
"name": "functional-pipeline",
"entry": "workloads/functional-pipeline.ts",
"args": [
"1"
]
},
{
"name": "async-pipeline",
"entry": "workloads/async-pipeline.ts",
"args": [
"1"
]
},
{
"name": "word-graph",
"entry": "workloads/word-graph.ts",
"args": [
"2"
]
},
{
"name": "alloc-trees",
"entry": "workloads/alloc-trees.ts",
"args": [
"1"
]
},
{
"name": "cyclic-graphs",
"entry": "workloads/cyclic-graphs.ts",
"args": [
"3"
]
},
{
"name": "numeric-kernels",
"entry": "workloads/numeric-kernels.ts",
"args": [
"0.5"
]
},
{
"name": "typed-array-scan",
"entry": "workloads/typed-array-scan.ts",
"args": [
"3"
]
},
{
"name": "typed-array-scan-workers",
"entry": "workloads/typed-array-scan-workers.ts",
"args": [
"3"
]
},
{
"name": "intern-table",
"entry": "workloads/intern-table.ts",
"args": [
"table",
"8",
"100000"
]
},
{
"name": "intern-table-map",
"entry": "workloads/intern-table.ts",
"args": [
"map",
"8",
"100000"
]
},
{
"name": "intern-table-1",
"entry": "workloads/intern-table.ts",
"args": [
"table",
"1",
"100000"
]
},
{
"name": "intern-table-map-1",
"entry": "workloads/intern-table.ts",
"args": [
"map",
"1",
"100000"
]
},
{
"name": "thread-slots",
"entry": "workloads/thread-slots.ts",
"args": [
"slots",
"8",
"50000",
"20"
]
},
{
"name": "thread-slots-cache",
"entry": "workloads/thread-slots.ts",
"args": [
"cache",
"8",
"50000",
"20"
]
},
{
"name": "channel-messages",
"entry": "workloads/channel-messages.ts",
"args": [
"200000"
]
},
{
"name": "atomics-contended",
"entry": "workloads/atomics-contended.ts",
"args": [
"2"
]
},
{
"name": "value-objects",
"entry": "workloads/value-objects.ts",
"args": [
"2"
]
},
{
"name": "value-records",
"entry": "workloads/value-records.ts",
"args": [
"1"
]
},
{
"name": "region-builds",
"entry": "workloads/region-builds.ts",
"args": [
"1"
]
},
{
"name": "optional-fields",
"entry": "workloads/optional-fields.ts",
"args": [
"10"
]
},
{
"name": "record-boundaries",
"entry": "workloads/record-boundaries.ts",
"args": [
"8"
]
},
{
"name": "number-maps",
"entry": "workloads/number-maps.ts",
"args": [
"4"
]
},
{
"name": "published-graphs",
"entry": "workloads/published-graphs.ts",
"args": [
"1"
]
},
{
"name": "validate-errors",
"entry": "workloads/validate-errors.ts",
"args": [
"1"
]
},
{
"name": "csv-numbers",
"entry": "workloads/csv-numbers.ts",
"args": [
"1"
]
},
{
"name": "log-lines",
"entry": "workloads/log-lines.ts",
"args": [
"4"
]
},
{
"name": "log-summary",
"entry": "../builds/log-summary/main.ts",
"input": "log-summary",
"args": [
"{input}",
"--min-status",
"200",
"--limit",
"12"
]
},
{
"name": "inventory-report",
"entry": "../builds/inventory-report/main.ts",
"input": "inventory-report",
"args": [
"{input}"
]
},
{
"name": "cli-config",
"entry": "workloads/cli-config.ts",
"input": "cli-config",
"launches": 20,
"args": [
"{input}",
"--env",
"production",
"--max-wave",
"12"
]
},
{
"name": "http-api",
"kind": "server",
"entry": "workloads/http-api.ts",
"args": [],
"client": {
"script": "clients/http-load.mjs",
"args": [
"--connections=64",
"--threads=2",
"--warmup=4000",
"--requests=40000"
]
}
}
]
}
+33
View File
@@ -0,0 +1,33 @@
/* The runtime benchmark workloads: one JSON file per workload in
* workloads/, named after the workload, next to the entry programs. A file
* holds the workload's entry, args and options; changes that add
* different workloads add different files. */
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
const CATEGORIES = ["cpu", "startup", "server"];
/* Server workloads run a listening program; startup-bound ones time
* several sequential launches per sample. */
export const workloadCategory = (w) =>
w.kind === "server" ? "server" : (w.launches ?? 1) > 1 ? "startup" : "cpu";
/* The workloads of the suite at `suite` (a benchmarks/runtime directory),
* by category and then name. A checkout from before the per-workload files
* lists them in workloads.json, in its own order. */
export function readWorkloads(suite) {
const legacy = join(suite, "workloads.json");
if (existsSync(legacy)) return JSON.parse(readFileSync(legacy, "utf8")).workloads;
const dir = join(suite, "workloads");
return readdirSync(dir)
.filter((file) => file.endsWith(".json"))
.map((file) => ({
name: file.slice(0, -".json".length),
...JSON.parse(readFileSync(join(dir, file), "utf8")),
}))
.sort(
(a, b) =>
CATEGORIES.indexOf(workloadCategory(a)) - CATEGORIES.indexOf(workloadCategory(b)) ||
(a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
);
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/alloc-trees.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/ast-interp.ts",
"args": [
"5"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/async-pipeline.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/atomics-contended.ts",
"args": [
"2"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/channel-messages.ts",
"args": [
"200000"
]
}
@@ -0,0 +1,12 @@
{
"entry": "workloads/cli-config.ts",
"input": "cli-config",
"launches": 20,
"args": [
"{input}",
"--env",
"production",
"--max-wave",
"12"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/csv-numbers.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/cyclic-graphs.ts",
"args": [
"3"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/functional-pipeline.ts",
"args": [
"1"
]
}
@@ -0,0 +1,14 @@
{
"kind": "server",
"entry": "workloads/http-api.ts",
"args": [],
"client": {
"script": "clients/http-load.mjs",
"args": [
"--connections=64",
"--threads=2",
"--warmup=4000",
"--requests=40000"
]
}
}
@@ -0,0 +1,8 @@
{
"entry": "workloads/intern-table.ts",
"args": [
"table",
"1",
"100000"
]
}
@@ -0,0 +1,8 @@
{
"entry": "workloads/intern-table.ts",
"args": [
"map",
"1",
"100000"
]
}
@@ -0,0 +1,8 @@
{
"entry": "workloads/intern-table.ts",
"args": [
"map",
"8",
"100000"
]
}
@@ -0,0 +1,8 @@
{
"entry": "workloads/intern-table.ts",
"args": [
"table",
"8",
"100000"
]
}
@@ -0,0 +1,7 @@
{
"entry": "../builds/inventory-report/main.ts",
"input": "inventory-report",
"args": [
"{input}"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/json-records.ts",
"args": [
"4"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/log-lines.ts",
"args": [
"4"
]
}
@@ -0,0 +1,11 @@
{
"entry": "../builds/log-summary/main.ts",
"input": "log-summary",
"args": [
"{input}",
"--min-status",
"200",
"--limit",
"12"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/number-maps.ts",
"args": [
"4"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/numeric-kernels.ts",
"args": [
"0.5"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/optional-fields.ts",
"args": [
"10"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/published-graphs.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/record-boundaries.ts",
"args": [
"8"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/records-sort.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/regex-logs.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/region-builds.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/template-render.ts",
"args": [
"1"
]
}
@@ -0,0 +1,9 @@
{
"entry": "workloads/thread-slots.ts",
"args": [
"cache",
"8",
"50000",
"20"
]
}
@@ -0,0 +1,9 @@
{
"entry": "workloads/thread-slots.ts",
"args": [
"slots",
"8",
"50000",
"20"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/typed-array-scan-workers.ts",
"args": [
"3"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/typed-array-scan.ts",
"args": [
"3"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/unicode-lexer.ts",
"args": [
"1"
]
}
@@ -0,0 +1,116 @@
// A lexer over a large bundled module whose comments and strings are in
// several languages: line starts, tokens by kind and comment text, read
// with charCodeAt as hand-written scanners do. Each pass relexes the whole
// text, as a bundler or an editor does after a change.
const comments = [
"// Berechnet die Größe des Puffers für übergroße Einträge",
"// Calcule la taille maximale avant la prochaine itération",
"// バッファのサイズを計算します",
"/* 计算缓冲区大小,然后返回结果 */",
"// Returns the cached value (fast path) — see the note above",
"/** Résumé: the état of the café's ñandú registry */",
];
const strings = ['"Grüße"', '"naïve façade"', '"東京"', '"ok ✓"', '"plain ascii"', "'mañana'"];
const names = ["buffer", "größe", "index", "resultat", "count", "value", "entry", "table"];
let seed = 11;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function pick(list: readonly string[]): string {
return list[Math.floor(random() * list.length)]!;
}
function makeSource(lines: number): string {
const out: string[] = [];
for (let i = 0; i < lines; i++) {
const r = random();
if (r < 0.2) out.push(pick(comments));
else if (r < 0.6) out.push(` const ${pick(names)}${i} = ${pick(names)}(${Math.floor(random() * 1000)}, ${pick(strings)});`);
else if (r < 0.85) out.push(` if (${pick(names)} >= ${i % 97}) return ${pick(names)}.${pick(names)} + 1;`);
else out.push(`function ${pick(names)}${i}(a, b) { return a * b - ${i}; }`);
}
return out.join("\n");
}
interface Counts {
lines: number;
identifiers: number;
numbers: number;
strings: number;
comments: number;
commentChars: number;
punctuation: number;
}
function isIdentifierStart(ch: number): boolean {
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || ch === 95 || ch === 36 || ch > 127;
}
function isIdentifierPart(ch: number): boolean {
return isIdentifierStart(ch) || (ch >= 48 && ch <= 57);
}
function lex(text: string): Counts {
const counts: Counts = { lines: 1, identifiers: 0, numbers: 0, strings: 0, comments: 0, commentChars: 0, punctuation: 0 };
let pos = 0;
const end = text.length;
while (pos < end) {
const ch = text.charCodeAt(pos);
if (ch === 10) {
counts.lines++;
pos++;
} else if (ch === 32 || ch === 9 || ch === 13) {
pos++;
} else if (ch === 47 && text.charCodeAt(pos + 1) === 47) {
const start = pos;
while (pos < end && text.charCodeAt(pos) !== 10) pos++;
counts.comments++;
counts.commentChars += pos - start;
} else if (ch === 47 && text.charCodeAt(pos + 1) === 42) {
const start = pos;
pos += 2;
while (pos < end && !(text.charCodeAt(pos) === 42 && text.charCodeAt(pos + 1) === 47)) {
if (text.charCodeAt(pos) === 10) counts.lines++;
pos++;
}
pos += 2;
counts.comments++;
counts.commentChars += pos - start;
} else if (ch === 34 || ch === 39) {
pos++;
while (pos < end && text.charCodeAt(pos) !== ch) pos++;
pos++;
counts.strings++;
} else if (ch >= 48 && ch <= 57) {
while (pos < end && text.charCodeAt(pos) >= 48 && text.charCodeAt(pos) <= 57) pos++;
counts.numbers++;
} else if (isIdentifierStart(ch)) {
while (pos < end && isIdentifierPart(text.charCodeAt(pos))) pos++;
counts.identifiers++;
} else {
counts.punctuation++;
pos++;
}
}
return counts;
}
function lineStarts(text: string): number[] {
const starts = [0];
for (let pos = 0; pos < text.length; pos++) if (text.charCodeAt(pos) === 10) starts.push(pos + 1);
return starts;
}
const scale = Number(process.argv[2] ?? "1");
const source = makeSource(60000);
const passes = Math.round(6 * scale);
let last = lex(source);
let total = last.identifiers + lineStarts(source).length;
for (let i = 1; i < passes; i++) {
last = lex(source);
total += last.identifiers + lineStarts(source).length;
}
console.log("units", source.length, "passes", passes, "total", total);
console.log(JSON.stringify(last));
@@ -0,0 +1,6 @@
{
"entry": "workloads/validate-errors.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/value-objects.ts",
"args": [
"2"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/value-records.ts",
"args": [
"1"
]
}
@@ -0,0 +1,6 @@
{
"entry": "workloads/word-graph.ts",
"args": [
"2"
]
}
+2 -1
View File
@@ -7,6 +7,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { readWorkloads } from "../runtime/workloads.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const suite = resolve(here, "../runtime");
@@ -18,7 +19,7 @@ const { values } = parseArgs({
},
});
mkdirSync(values.out, { recursive: true });
const { workloads } = JSON.parse(readFileSync(join(suite, "workloads.json"), "utf8"));
const workloads = readWorkloads(suite);
const cli = join(values.scriptc, "packages/cli/dist/bootstrap.js");
// Same inputs as scripts/bench-runtime.mjs generates.
function input(kind) {
+1 -1
View File
@@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<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>
<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, and a checked cast whose value does not match its type throws instead of continuing with the value. 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>
+1 -1
View File
@@ -60,7 +60,7 @@ Code that operates on values whose types are known only at runtime, such as `any
`--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.
`--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. It also makes a checked cast whose value does not match its type throw, where `auto`, the default, continues with the value in the dynamic tier (see [runtime type checks](/docs/limitations#runtime-type-checks)). Otherwise `auto` behaves the same way today.
## Diagnostic codes
+7 -8
View File
@@ -68,25 +68,24 @@ Programs are checked against the `es2025` standard library, scriptc's own declar
## Runtime type checks
Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. For example, parsed JSON must match the type you cast it to:
Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. Parsed JSON that matches the type you cast it to uses the type's native layout. A value that does not match continues as the value it is, as under Node.js, which does not check the `as Config` assertion:
```ts:cast.ts
type Config = { port: number };
try {
const config = JSON.parse('{"port": "eighty"}') as Config;
console.log(config.port);
} catch (error) {
if (error instanceof Error) console.log(error.message);
for (const text of ['{"port": 8080}', '{"port": "eighty"}']) {
const config = JSON.parse(text) as Config;
console.log(config.port, typeof config.port);
}
```
```console
$ scriptc run cast.ts
expected number at $.port, got string
8080 number
eighty string
```
Under Node.js, the same `as Config` assertion performs no check. See [runtime type checks](/docs/limitations#runtime-type-checks) for details.
See [runtime type checks](/docs/limitations#runtime-type-checks) for where a mismatch still throws.
## Build-time evaluation
@@ -1,5 +1,12 @@
Casts from values whose type is only known at runtime validate the value.
For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`.
A value that matches the type uses the type's native layout.
When the cast initializes a `const` or `let` binding declared in a function or block, and the binding is only read by the code after the cast, a value that does not match continues as the value it is, as under Node.js: with `JSON.parse(s) as Config` and `{"port": "eighty"}`, `config.port` is the string `"eighty"`, missing properties read `undefined`, and extra properties stay visible to `Object.keys`, `JSON.stringify`, and `util.inspect`.
That code then runs on dynamic values, which is slower than native records.
A statement of it that has no dynamic lowering throws a `TypeError` naming the construct and its location when such a value reaches it.
In the other cases a mismatch throws a catchable `TypeError` identifying the offending path, such as `expected number at $.port, got string`: casts that initialize module-level bindings or appear in other positions, and a mismatched value that leaves the code after its cast through a typed parameter, field, return value, or outer binding, where it is checked against that type.
A value that matches there continues, copied if it is an object.
`--tier=static-only` makes every mismatch throw at the cast.
A TypeScript assertion alone does not perform this validation when run under Node.js.
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.
@@ -1,7 +0,0 @@
{
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "ee72820d45a79aa154f0",
"rowCount": 3880
}
+9 -1
View File
@@ -17,13 +17,17 @@
*/
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
const root = fileURLToPath(new URL("../../../", import.meta.url));
const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`;
const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`;
const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`;
// The docs build derives this file's values from the public artifact; a
// merge that keeps a branch's copy would bring back a file every
// compatibility change rewrites.
const retiredOutputPaths = [`${root}docs/src/generated/node-v24-compatibility-meta.json`];
const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`;
const compilerCompatPath = `${packageRoot}static-support.json`;
const pinPath = `${packageRoot}node-v24.json`;
@@ -1009,6 +1013,10 @@ function publicSnapshot(snapshot) {
}
function checkLocal() {
for (const path of retiredOutputPaths) {
if (existsSync(path))
throw new Error(`${path.slice(root.length)} is no longer generated; delete it`);
}
const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8"));
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
throw new Error(
@@ -28,6 +28,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.dynTier ? { dynTier: true } : {}),
...(features.streamDyn ? { streamDyn: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
...(features.dynBufferNum ? { dynBufferNum: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
events: features.processEvents,
emitter: features.emitter,
symbol: features.symbol,
@@ -75,17 +75,25 @@ test("releases split into an inlinable fast path and an out-of-line remainder",
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
const body = (name: string): string =>
new RegExp(`^define internal void @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
// A hierarchy rooted in the program: a surviving release decrements and
// takes the possible-root step without loading the vtable.
// A hierarchy rooted in the program: a surviving release decrements the
// count (the low half of the rc word, which keeps the class id in its high
// half) and takes the possible-root step without loading the vtable.
const fast = body(mangleClassRelease("Node"));
expect(fast).toContain("%shared = icmp ult i64 %rcm2, -3");
expect(fast).toContain("%shared = icmp ult i32 %rcm2, -3");
expect(fast).toContain("load i32, ptr %bufp");
expect(fast).toContain(`call void @${mangleClassRelease("Node")}_slow(ptr %o)`);
expect(fast).not.toContain("%ScrVt");
// Only the last reference dispatches, from a noinline remainder.
const slow = body(`${mangleClassRelease("Node")}_slow`);
expect(slow).toMatch(/noinline/);
// The class id indexes the hierarchy's vtable array.
expect(slow).toContain("%vt.id = load i32, ptr %vt.idp");
expect(slow).toContain("ptr @sc_vta_Node, i64 0, i64 %vt.ix64");
expect(slow).toContain("getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2");
// 32-bit targets keep the vtable word.
const wasm = emitLlvmModule(mod, { pointerBits: 32 });
expect(wasm).toContain("; class Node (vt at 1)");
expect(wasm).not.toContain("@sc_vta_Node");
});
test("ProgramLifetime hierarchies are never freed and take no reference counts", () => {
@@ -121,9 +129,11 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
expect(body(mangleClassRetain(cls)), cls).toMatch(/entry:\n {2}ret ptr %o\n}/);
expect(body(mangleClassRelease(cls)), cls).toMatch(/entry:\n {2}ret void\n}/);
// new: no collector header (no descriptor, no scr_cyc_alloc) and the
// program-lifetime count 2^63, not SIZE_MAX (which means published).
// program-lifetime count 2^31 under the class id (Node's preorder number
// 0 + 1, Leaf's 1 + 1), not SIZE_MAX (which means published).
const nw = body(mangleClassNew(cls));
expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o");
const id = cls === "Node" ? 1 : 2;
expect(nw, cls).toContain(`store i64 ${id * 2 ** 32 + 2 ** 31}, ptr %o.vt`);
expect(nw, cls).not.toContain("store i64 -1, ptr %o");
expect(nw, cls).not.toContain("scr_cyc_alloc");
expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`);
@@ -206,13 +216,15 @@ test("published ProgramLifetime instances use the published mark every other obj
// a refusal restores the program-lifetime count); untraced, so it never
// leaves a collector buffer.
expect(llvm).toContain("call void @scr_publish(ptr");
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 4\)/);
// The frozen-object guard of the store tests the count against SIZE_MAX:
// an unpublished instance (2^63) stays writable.
// Kind 4 (an object) | 0x100 (the rc word keeps a class id: the mark
// writes only the count).
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 260\)/);
// The frozen-object guard of the store tests the count against UINT32_MAX:
// an unpublished instance (2^31) stays writable.
const store =
llvm.split(/^define /m).find((fn) => fn.includes("call void @scr_throw_published_field(")) ??
"";
expect(store).toMatch(/icmp eq i64 %\S+, -1/);
expect(store).toMatch(/icmp eq i32 %\S+, -1/);
expect(store).toContain("call void @scr_publish(ptr");
// 32-bit targets use 2^31.
const llvm32 = emitLlvmModule(mod, { pointerBits: 32 });
+151 -27
View File
@@ -26,6 +26,7 @@ import {
mangleCtorThunk,
mangleFunction,
mangleGlobal,
mangleVtArray,
mangleVtInstance,
mangleVtStruct,
} from "../mangle.js";
@@ -81,6 +82,12 @@ export class LlClassMeta {
* member's struct carries the i32 slot word before its fields
* (classSlotWordIndex). */
threadSlot = false;
/** A member of a program-rooted hierarchy on a 64-bit target: instead of
* a vtable word, the rc word holds the class id (classIdRcWord) in its
* high 32 bits (CLASS_ID_OFFSET) and the reference count in its low 32
* bits, and the hierarchy's vtables are one array indexed by the id
* (mangleVtArray). */
idInRc = false;
constructor(readonly def: IrClassDef) {
this.root = this;
@@ -96,6 +103,7 @@ export class LlClassMeta {
export function buildClassGraph(
mod: IrModule,
fnByName: Map<string, IrFunction>,
sizeType: "i32" | "i64" = "i64",
): Map<string, LlClassMeta> {
const metaMap = new Map<string, LlClassMeta>();
for (const cls of mod.classes ?? []) {
@@ -122,6 +130,11 @@ export function buildClassGraph(
if (meta.base === null) number(meta, meta);
meta.hierarchy = meta.base !== null || meta.children.length > 0;
}
// The runtime reads the vtable word of runtime-rooted hierarchies (Error,
// EventEmitter, EventTarget, Event and the streams) and 32-bit targets
// have no spare half in the rc word: those keep the vtable word.
for (const meta of metaMap.values())
meta.idInRc = sizeType === "i64" && meta.hierarchy && meta.root.def.runtime !== true;
const slotRoots = new Set<LlClassMeta>();
for (const key of mod.threadSlotKeys ?? []) {
const meta = metaMap.get(key);
@@ -336,11 +349,12 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null {
}
/** The GEP index after a class's header words: rc at 0, the vtable word
* at 1 on hierarchy members, then a runtime prefix (state + display name)
* at 1 on hierarchy members that do not keep their class id in the rc
* word (LlClassMeta.idInRc), then a runtime prefix (state + display name)
* on classes rooted at a runtime base, then the stream-state slot on
* stream-rooted ones. */
function headerEnd(meta: LlClassMeta): number {
if (!meta.hierarchy) return 1;
if (!meta.hierarchy || meta.idInRc) return 1;
if (streamRooted(meta)) return 5;
return runtimePrefixOf(meta) ? 4 : 2;
}
@@ -445,6 +459,69 @@ export function programLifetimeRc(sizeType: "i32" | "i64"): string {
return sizeType === "i32" ? "-2147483648" : "-9223372036854775808";
}
/** Lines that load the preorder number of the dynamic class of `obj`, an
* instance of `meta`'s hierarchy whose struct is `struct`, into `pre` (a
* `sizeType`): from the high half of the rc word (LlClassMeta.idInRc), or
* from the vtable its vtable word points at. */
export function classPreLines(
meta: LlClassMeta,
struct: string,
obj: string,
pre: string,
sizeType: "i32" | "i64",
): string[] {
if (meta.idInRc)
return [
` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
` ${pre}.id = load i32, ptr ${pre}.idp`,
` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`,
` ${pre} = zext i32 ${pre}.n to ${sizeType}`,
];
return [
` ${pre}.vtp = getelementptr inbounds %${struct}, ptr ${obj}, i64 0, i32 1`,
` ${pre}.vt = load ptr, ptr ${pre}.vtp`,
` ${pre}.prep = getelementptr inbounds %ScrVt, ptr ${pre}.vt, i64 0, i32 0`,
` ${pre} = load ${sizeType}, ptr ${pre}.prep`,
];
}
/** Lines that load the class id of the LlClassMeta.idInRc instance `obj`
* (`group` adds !invariant.group to the load) and point `vt` at its class's
* element of the hierarchy's vtable array. */
export function classIdVtLines(
meta: LlClassMeta,
obj: string,
vt: string,
group: string,
): string[] {
const t = `${vt}.`;
const root = meta.root;
const vtt = `%${mangleVtStruct(root.def.name)}`;
const n = root.post - root.pre + 1;
return [
` ${t}idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
` ${t}id = load i32, ptr ${t}idp${group}`,
` ${t}ix = sub i32 ${t}id, ${root.pre + 1}`,
` ${t}ix64 = zext i32 ${t}ix to i64`,
` ${vt} = getelementptr inbounds [${n} x ${vtt}], ptr @${mangleVtArray(root.def.name)}, i64 0, i64 ${t}ix64`,
];
}
/** The byte offset of the class id in the rc word of an instance that
* keeps it there (LlClassMeta.idInRc): the high half of the little-endian
* 64-bit word. The count is the i32 at offset 0. */
export const CLASS_ID_OFFSET = 4;
/** The rc word an LlClassMeta.idInRc instance is created with: the class
* id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits
* a count of 1, or for a program-lifetime instance the midpoint of the 32-bit
* count (as programLifetimeRc is of the 32-bit word). The id is never 0,
* 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so
* the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */
export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
}
function undefFieldInits(
host: ClassHost,
meta: LlClassMeta,
@@ -524,17 +601,18 @@ export function emitClassShapes(
// ScrEmitter* / ScrStream* / ScrEventTargetObj* / ScrEventObj* are the
// usual pointer reinterprets.
const runtimePrefix = runtimePrefixOf(meta);
const prefix = meta.hierarchy
? streamRooted(meta)
? ["ptr", "ptr", "ptr", "ptr"]
: runtimePrefix
? ["ptr", "ptr", "ptr"]
: ["ptr"]
: [];
const prefix =
meta.hierarchy && !meta.idInRc
? streamRooted(meta)
? ["ptr", "ptr", "ptr", "ptr"]
: runtimePrefix
? ["ptr", "ptr", "ptr"]
: ["ptr"]
: [];
const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys];
typeDefs.push(
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
`; class ${cls.name}${meta.idInRc ? " (class id in rc)" : meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
);
}
@@ -559,18 +637,40 @@ export function emitClassShapes(
// One constant vtable instance per emitted hierarchy class: interval,
// direct release, and the class's dispatch entry for every slot — the
// method functions THEMSELVES (no adapters; see the header comment).
for (const cls of emitted) {
const meta = metaMap.get(cls.name)!;
if (!meta.hierarchy) continue;
const vtInit = (meta: LlClassMeta): string => {
const entries = vtEntriesFor(meta).map(({ slot, impl }) =>
impl === null
? `ptr null` // outside the declaring subtree / fully-abstract chain
: `ptr @${host.virtualEntry?.(`%${impl.def.name}.${slot.method}`) ?? mangleFunction(`%${impl.def.name}.${slot.method}`)}`,
);
const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(cls.name)} }`;
const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(meta.def.name)} }`;
return `{ ${[head, ...entries].join(", ")} }`;
};
for (const cls of emitted) {
const meta = metaMap.get(cls.name)!;
if (!meta.hierarchy || meta.idInRc) continue;
defs.push(
`@${mangleVtInstance(cls.name)} = internal constant %${mangleVtStruct(meta.root.def.name)} ` +
`{ ${[head, ...entries].join(", ")} } ; class ${cls.name}`,
`${vtInit(meta)} ; class ${cls.name}`,
``,
);
}
// A hierarchy whose instances keep their class id in the rc word has
// one array of its classes' vtables, in preorder: the vtable of an
// instance is the element at its number minus the root's.
for (const name of roots) {
const root = metaMap.get(name)!;
if (!root.idInRc) continue;
const members = [...metaMap.values()]
.filter((m) => m.root === root)
.sort((a, b) => a.pre - b.pre);
const vtt = `%${mangleVtStruct(root.def.name)}`;
defs.push(
`@${mangleVtArray(root.def.name)} = internal constant [${members.length} x ${vtt}] [`,
...members.map(
(m, i) => ` ${vtt} ${vtInit(m)}${i + 1 < members.length ? "," : ""} ; class ${m.def.name}`,
),
`]`,
``,
);
}
@@ -605,6 +705,7 @@ export function emitClassShapes(
release: `@${release}`,
destroy: `${teardown}_destroy`,
traced: host.tracedShapes.has(`object:${cls.name}`),
...(meta.idInRc ? { count: "i32" as const } : {}),
},
]);
}
@@ -658,7 +759,13 @@ export function emitClassShapes(
// retain: NULL-tolerant, immortal-skip — layout-generic (rc at 0), so
// hierarchy members need no dispatch.
defs.push(...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime), ``);
// An instance that keeps its class id in the rc word counts in the
// low half (CLASS_ID_OFFSET).
const count = meta.idInRc ? "i32" : host.sizeType;
defs.push(
...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime, count),
``,
);
// An out-of-line teardown tears typed children down directly
// (directChildReleaseLines). It takes the chain length when it has such
@@ -714,6 +821,7 @@ export function emitClassShapes(
traced,
`release ${cls.name}`,
lifetime,
count,
),
``,
);
@@ -723,12 +831,16 @@ export function emitClassShapes(
` %isnull = icmp eq ptr %o, null`,
` br i1 %isnull, label %done, label %check`,
`check:`,
` %rc = load ${host.sizeType}, ptr %o`,
` %imm = icmp eq ${host.sizeType} %rc, -1`,
` %rc = load ${count}, ptr %o`,
` %imm = icmp eq ${count} %rc, -1`,
` br i1 %imm, label %done, label %disp`,
`disp:`,
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
` %vt = load ptr, ptr %vtp`,
...(meta.idInRc
? classIdVtLines(meta, "%o", "%vt", "")
: [
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
` %vt = load ptr, ptr %vtp`,
]),
` %relp = getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2`,
` %rel = load ptr, ptr %relp`,
` call void %rel(ptr %o) ; the DYNAMIC class's teardown`,
@@ -743,10 +855,10 @@ export function emitClassShapes(
const reld: string[] = [
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
`entry:`,
` %rc = load ${host.sizeType}, ptr %o`,
` %n = sub ${host.sizeType} %rc, 1`,
` store ${host.sizeType} %n, ptr %o`,
` %dead = icmp eq ${host.sizeType} %n, 0`,
` %rc = load ${count}, ptr %o`,
` %n = sub ${count} %rc, 1`,
` store ${count} %n, ptr %o`,
` %dead = icmp eq ${count} %n, 0`,
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
`free:`,
];
@@ -815,10 +927,22 @@ export function emitClassShapes(
host,
sizeOf,
traced && !lifetime ? { type: cycType } : null,
meta.hierarchy ? 16 : 8,
meta.hierarchy && !meta.idInRc ? 16 : 8,
),
);
nw.push(` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`);
if (meta.idInRc) {
// The class id in the high half of the rc word, the count in the low
// half (classIdRcWord). It never changes while the object lives:
// retains, releases and the runtime write only the low half.
host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`);
nw.push(
` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`,
` store i64 ${classIdRcWord(meta, lifetime)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`,
);
} else
nw.push(
` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`,
);
if (lifetime && audit) {
// --sanitize builds: the RC audit skips its leak report when the
// program keeps program-lifetime objects (scr_console.c), and
@@ -826,7 +950,7 @@ export function emitClassShapes(
host.declare(`declare void @scr_obj_lifetime_note(ptr)`);
nw.push(` call void @scr_obj_lifetime_note(ptr %o)`);
}
if (meta.hierarchy) {
if (meta.hierarchy && !meta.idInRc) {
// The vtable word is written once, here, and never changes while the
// object lives: its store and the virtual-call loads carry
// !invariant.group. A recycled block may have held an object of
+55 -4
View File
@@ -39,6 +39,7 @@ import {
isDirentShape,
isDynTypedRefType,
isRefCounted,
isUnitType,
typeKey,
} from "../../ir/ir.js";
import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
@@ -697,6 +698,46 @@ export class LlDyn {
/** Emit the standard in-helper pending check: on a pending exception
* run `cleanup` and return `dummy`. */
/** The failure block of a union dynCheck whose value matched no arm. A
* value of the one container kind exactly one arm can take (an object
* for the union's only record arm, an array for its only array arm,
* beside unit and scalar arms) runs that arm's builder, which throws the
* deepest failing path and the reason there (`expected string at
* $.order.owner.id, got undefined`) instead of the union's own outer
* path. Other values, and an arm builder that unexpectedly succeeds,
* fall through to the union's message. Only the failure path changes. */
private unionArmFailure(B: BlockBuilder, def: IrUnionDef, kind: string): void {
const scalar = (arm: IrType): boolean =>
isUnitType(arm) ||
arm.kind === "f64" ||
arm.kind === "bool" ||
arm.kind === "string" ||
arm.kind === "bigint";
const containers = def.arms.filter((arm) => !scalar(arm));
const arm = containers.length === 1 ? containers[0]! : null;
const container =
arm?.kind === "array"
? DYN_KIND.ARR
: arm?.kind === "record" && !this.host.recordsById.get(arm.shapeId)?.tuple
? DYN_KIND.OBJ
: null;
if (!arm || container === null) return;
const host = this.host;
const deep = B.newLabel("dcu.deep");
const outer = B.newLabel("dcu.outer");
const same = B.tmp();
B.line(`${same} = icmp eq i32 ${kind}, ${container}`);
B.condBr(same, deep, outer);
B.startBlock(deep);
const built = B.tmp();
B.line(`${built} = call ptr @${this.dynCheckHelper(arm)}(ptr %d, ptr %path)`);
this.pendingBail(B, "dcu.deep", () => {}, "ptr null");
const rc = vAdapters(host, arm);
B.line(`call void ${rc.release}(ptr ${built})`);
B.br(outer);
B.startBlock(outer);
}
private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void {
const p = B.tmp();
for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart());
@@ -1407,22 +1448,29 @@ export class LlDyn {
* differently), runs the checked-dynamic route — scr_json_parse plus the
* dynCheck builder — which reports the exact error. Returns +1, or null
* with the pending flag set. */
jsonParseHelper(t: IrType): string | null {
jsonParseHelper(t: IrType, fallback = false): string | null {
if (t.kind !== "record" && t.kind !== "array") return null;
const key = typeKey(t);
const existing = this.jsonParsers.get(key);
const cacheKey = fallback ? `fallback:${key}` : key;
const existing = this.jsonParsers.get(cacheKey);
if (existing) return existing;
if (!this.jsonSchemaSupported(t, new Set())) return null;
const host = this.host;
const name = `sc_jp_${this.jsonParsers.size}`;
this.jsonParsers.set(key, name);
this.jsonParsers.set(cacheKey, name);
const schema = this.jsonSchema(t);
const check = this.dynCheckHelper(t);
host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`);
host.declare(`declare ptr @scr_json_parse(ptr)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
// A fallback site (dynCheck.fallback): a value that fails validation
// parks as the checked value (scr_dyn_check_recover) and the helper
// returns the dummy with no exception pending. Syntax errors still
// throw.
if (fallback) host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`);
const recover = fallback ? [` call zeroext i1 @scr_dyn_check_recover(ptr %d)`] : [];
this.defs.push(
`define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}`,
`define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}${fallback ? " (fallback)" : ""}`,
`entry:`,
` %fast = call ptr @scr_json_parse_schema(ptr %text, ptr @${schema})`,
` %hit = icmp ne ptr %fast, null`,
@@ -1437,6 +1485,7 @@ export class LlDyn {
` ret ptr null`,
`check:`,
` %v = call ptr @${check}(ptr %d, ptr null)`,
...recover,
` call void @scr_dyn_release(ptr %d)`,
` ret ptr %v`,
`}`,
@@ -2291,6 +2340,7 @@ export class LlDyn {
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
B.terminate(`ret ptr ${checked}`);
B.startBlock(lFail);
this.unionArmFailure(B, def, kind);
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
B.terminate(`ret ptr null`);
break;
@@ -2359,6 +2409,7 @@ export class LlDyn {
);
B.terminate(`ret ptr ${checked}`);
B.startBlock(lFail);
this.unionArmFailure(B, def, kind);
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
B.terminate(`ret ptr null`);
break;
@@ -1340,14 +1340,17 @@ export function helperDefs(emitter: LlEmitter): string[] {
// of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
// little-endian targets) makes UTF-16 units bytes. Unproven strings and
// anything but an in-range index take the runtime's UTF-16 mapping.
// A scan's receiver reads inline too where runtime state is ordinary
// global storage (ScrStrScan; SCR_TL makes it thread-local otherwise).
const scanInline = emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true;
// A scan's receiver reads inline too (ScrStrScan): from ordinary global
// storage, or, in worker executables, from the thread's static TLS block,
// one thread-pointer-relative load (see executableTls).
const scanInline =
emitter.executableTls ||
(emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true);
let scanDeclared = false;
for (const helper of STRING_READ_HELPERS) {
if (!emitter.decls.has(helper.decl)) continue;
if (scanInline && !scanDeclared) {
defs.push(stringScanDecl(emitter.sizeType), ``);
defs.push(stringScanDecl(emitter.sizeType, emitter.executableTls), ``);
scanDeclared = true;
}
defs.push(...helper.define(emitter.sizeType, FN_ATTRS, scanInline), ``);
+32 -27
View File
@@ -266,6 +266,7 @@ import {
buildClassGraph,
classMembershipIntervals,
classFieldIndex,
classPreLines,
classStructSym,
programLifetimeClass,
type LlClassMeta,
@@ -1464,7 +1465,7 @@ export class LlEmitter {
// The class graph: base/children links, hierarchy membership, the
// whole-program preorder numbering for instanceof over runtime and
// compiled error objects, and the per-hierarchy virtual slot lists.
this.classMeta = buildClassGraph(mod, this.fnByName);
this.classMeta = buildClassGraph(mod, this.fnByName, this.sizeType);
if (mod.lifetimeRegions === true) {
this.regions = new LifetimeRegions(
this.shapeHost,
@@ -2815,10 +2816,16 @@ export class LlEmitter {
} else if (t.kind === "string") {
this.declare(`declare void @scr_throw_str(ptr)`);
B.line(`call void @scr_throw_str(ptr ${v.name})`);
} else if (t.kind === "object" && this.classMeta.get(t.className)?.hierarchy === true) {
} else if (
t.kind === "object" &&
this.classMeta.get(t.className)?.hierarchy === true &&
this.classMeta.get(t.className)?.idInRc !== true
) {
// Hierarchy instances carry a vtable word: the OBJ kind keeps the
// dynamic class inspectable (catch-binding instanceof, the uncaught
// printer's "name: message" for Error instances).
// printer's "name: message" for Error instances). Instances that keep
// their class id in the rc word are never Errors and are thrown as
// REF payloads (narrow-guards.ts emitCaughtClassIdInstanceof).
const rc = vAdapters(this.shapeHost, t);
this.declare(`declare void @scr_throw_obj(ptr, ptr, ptr, ptr)`);
B.line(
@@ -3151,20 +3158,20 @@ export class LlEmitter {
return { ptr: p, type, inline: this.inlineUnions.classField(className, field) };
}
/** `o->vt->pre` — the dynamic class's preorder number (borrowed object;
* the static class names which struct spelling carries the vt word). */
/** The dynamic class's preorder number (borrowed object; the static class
* names the struct spelling and where the number is, classPreLines). */
loadVtPre(objName: string, staticClassName: string): string {
const B = this.B;
const vtp = B.tmp();
const vt = B.tmp();
const prep = B.tmp();
const pre = B.tmp();
B.line(
`${vtp} = getelementptr inbounds %${classStructSym(staticClassName)}, ptr ${objName}, i64 0, i32 1`,
);
B.line(`${vt} = load ptr, ptr ${vtp}`);
B.line(`${prep} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`);
B.line(`${pre} = load ${this.sizeType}, ptr ${prep}`);
const meta = this.classMetaOf(staticClassName);
for (const line of classPreLines(
meta,
classStructSym(staticClassName),
objName,
pre,
this.sizeType,
))
B.line(line.trimStart());
return pre;
}
@@ -5096,6 +5103,7 @@ export class LlEmitter {
pre: meta.pre,
post: meta.post,
runtimeRooted: meta.root.def.runtime === true,
idInRc: meta.idInRc,
// Inline scalar-union fields are plain bits: nothing to push.
fields: meta.def.fields.flatMap((f) =>
this.inlineUnions.classField(className, f.name)
@@ -5134,15 +5142,17 @@ export class LlEmitter {
return this.own({ name: value.name, type: e.type });
}
/** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder
* in the cold block and answers the continuation label. */
private emitPublishedTest(obj: string): string {
/** Branches on "`obj` is published" (rc == SIZE_MAX, or a count of
* UINT32_MAX in the low half for `count` i32: classes.ts
* LlClassMeta.idInRc); leaves the builder in the cold block and answers
* the continuation label. */
private emitPublishedTest(obj: string, count: "i32" | "i64" = this.sizeType): string {
const B = this.B;
const rc = B.tmp(),
frozen = B.tmp(),
expected = B.tmp();
B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`);
B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`);
B.line(`${rc} = load ${count}, ptr ${obj}`);
B.line(`${frozen} = icmp eq ${count} ${rc}, -1`);
this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`);
const cold = B.newLabel("pub.frozen"),
@@ -5182,22 +5192,17 @@ export class LlEmitter {
if (published === null) return;
if (className !== null && !published.classGuarded(className)) return;
const B = this.B;
const ok = this.emitPublishedTest(obj);
let owner: string;
const meta = className === null ? undefined : this.classMeta.get(className);
const ok = this.emitPublishedTest(obj, meta?.idInRc === true ? "i32" : this.sizeType);
let owner: string;
if (meta === undefined) owner = this.cstr("Object");
else if (!meta.hierarchy) owner = this.cstr(meta.def.jsName ?? meta.def.name);
else {
// The DYNAMIC class names the object, as in Node.
this.publishedNamesSym ??= "@sc_pub_class_names";
const vtp = B.tmp(),
vt = B.tmp(),
pre = B.tmp(),
const pre = this.loadVtPre(obj, meta.def.name),
slot = B.tmp(),
name = B.tmp();
B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`);
B.line(`${vt} = load ptr, ptr ${vtp}`);
B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`);
B.line(
`${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`,
);
@@ -374,7 +374,7 @@ export function emitSerializationExpr(
// falls back to exactly this route whenever the fast path declines.
const parsed = e.value;
if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") {
const fused = host.dyn.jsonParseHelper(e.type);
const fused = host.dyn.jsonParseHelper(e.type, e.fallback === true);
if (fused) {
const text = emitBorrowedInput(host, parsed.args[0]!);
const r = B.tmp();
@@ -400,6 +400,12 @@ export function emitSerializationExpr(
B.line(
`${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`,
);
// A fallback site: a value that fails validation parks as the
// checked value instead of throwing (dynCheck.fallback).
if (e.fallback) {
host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`);
B.line(`call zeroext i1 @scr_dyn_check_recover(ptr ${dynV.name})`);
}
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return out;
@@ -34,6 +34,7 @@ import {
} from "../mangle.js";
import {
classEnvironmentIndex,
classIdVtLines,
classStructSym,
vtEntriesFor,
type LlClassMeta,
@@ -1193,15 +1194,20 @@ export function emitCallExpr(
if (!borrowed.has(index)) host.toFrame(a);
});
const recv = args[0]!.name;
const vtp = B.tmp();
const vt = B.tmp();
const fnp = B.tmp();
const fn = B.tmp();
B.line(
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
);
// Set once at allocation (classes.ts launders each new object).
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
if (meta.idInRc)
for (const line of classIdVtLines(meta, recv, vt, ", !invariant.group !{}"))
B.line(line.trimStart());
else {
const vtp = B.tmp();
B.line(
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
);
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
}
B.line(
`${fnp} = getelementptr inbounds %${mangleVtStruct(meta.root.def.name)}, ptr ${vt}, i64 0, i32 ${slotIdx + 1}`,
);
@@ -28,6 +28,7 @@ import { elemAccess, vAdapters } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { emitUnionWiden } from "./expr-records.js";
import { DYN_BUFFER_NUM_METHODS } from "../../ir/runtime-features.js";
export function emitDynamicExpr(
host: LlvmEmitterContext,
@@ -226,9 +227,12 @@ export function emitDynamicExpr(
});
argsPtr = arr;
}
host.declare(
`declare ptr @scr_dyn_invoke_prepared(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`,
);
// Fixed-width Buffer method names take scr_bytes_dyn.c's entry, which
// runs them on Buffer receivers and dispatches any other receiver.
const invoke = DYN_BUFFER_NUM_METHODS.has(e.method)
? "scr_dyn_invoke_buffer_num"
: "scr_dyn_invoke_prepared";
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
let name = host.cstr(e.calleeName);
if (e.calleeNameValue !== undefined) {
const value = emitBorrowedInput(host, e.calleeNameValue);
@@ -237,7 +241,7 @@ export function emitDynamicExpr(
}
const t = B.tmp();
B.line(
`${t} = call ptr @scr_dyn_invoke_prepared(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
@@ -1058,6 +1062,42 @@ export function emitDynamicExpr(
host.declare(
`declare ptr @scr_caught_check_obj(ptr, ${host.sizeType}, ${host.sizeType}, ptr)`,
);
if (target.idInRc) {
// A REF payload (emitCaughtInstanceof): a match retains it through
// its adapter; anything else gets the runtime's TypeError from the
// check against an empty interval.
const ok = emitCaughtInstanceof(host, c.name, e.className);
const yes = B.newLabel("caught.cast"),
no = B.newLabel("caught.cast.fail"),
join = B.newLabel("caught.cast.done");
B.condBr(ok, yes, no);
B.startBlock(yes);
const payloadSlot = B.tmp(),
payload = B.tmp(),
retainSlot = B.tmp(),
retain = B.tmp(),
retained = B.tmp();
B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`);
B.line(`${payload} = load ptr, ptr ${payloadSlot}`);
B.line(`${retainSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`);
B.line(`${retain} = load ptr, ptr ${retainSlot}`);
B.line(`${retained} = call ptr ${retain}(ptr ${payload})`);
const fromYes = B.currentLabel();
B.br(join);
B.startBlock(no);
const failed = B.tmp();
B.line(
`${failed} = call ptr @scr_caught_check_obj(ptr ${c.name}, ${host.sizeType} 1, ${host.sizeType} 0, ptr ${host.cstr(display)})`,
);
const fromNo = B.currentLabel();
B.br(join);
B.startBlock(join);
const t = B.tmp();
B.line(`${t} = phi ptr [ ${retained}, %${fromYes} ], [ ${failed}, %${fromNo} ]`);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return out;
}
let pre = String(target.pre);
let post = String(target.post);
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
@@ -331,3 +331,44 @@ test.each([32, 64] as const)(
expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]");
},
);
test("string reads of a scan receiver load it inline where emitted code can address it", () => {
const receiver: IrExpr = { kind: "varRef", localId: "text", type: STRING, loc };
const mod = fixture();
mod.functions = [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
{
name: "units",
params: [{ name: "text", localId: "text", type: STRING }],
returnType: F64,
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
loc,
body: [
{
kind: "return",
loc,
value: { kind: "strIntrinsic", method: "length", receiver, args: [], type: F64, loc },
},
],
},
];
const helper = (llvm: string): string =>
/^define internal double @sc_str_utf16_len\([^]*?^}/m.exec(llvm)![0];
const plain = emitLlvmModule(mod);
expect(plain).toContain("@scr_str_scan = external global");
expect(helper(plain)).toContain("%scan.same");
// ELF worker executables keep the receiver in the thread's static TLS block.
const elf = emitLlvmModule(
{ ...mod, workers: true },
{ targetTriple: "x86_64-unknown-linux-gnu" },
);
expect(elf).toContain("@scr_str_scan = external thread_local(localexec) global");
expect(helper(elf)).toContain("%scan.same");
// Darwin worker executables leave every non-ASCII read to the runtime.
const darwin = emitLlvmModule(
{ ...mod, workers: true },
{ targetTriple: "arm64-apple-macosx14.0.0" },
);
expect(darwin).not.toContain("@scr_str_scan");
expect(helper(darwin)).not.toContain("%scan.same");
});
@@ -20,7 +20,7 @@ import {
} from "../../ir/ir.js";
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { type LlClassMeta, classFieldIndex, classStructSym } from "./classes.js";
import { type LlClassMeta, classFieldIndex, classPreLines, classStructSym } from "./classes.js";
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
import {
FN_ATTRS,
@@ -232,10 +232,13 @@ export function streamTypedRefCommitAdapter(
];
if (meta.hierarchy && meta.children.length) {
lines.push(
` %derived_vt_slot = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 1`,
` %derived_vt = load ptr, ptr %derived_vt_slot`,
` %derived_pre_slot = getelementptr inbounds %ScrVt, ptr %derived_vt, i64 0, i32 0`,
` %derived_pre = load ${host.sizeType}, ptr %derived_pre_slot`,
...classPreLines(
meta,
classStructSym(t.className),
"%target",
"%derived_pre",
host.sizeType,
),
);
const descendants = (current: typeof meta): (typeof meta)[] =>
current.children.flatMap((child) => [child, ...descendants(child)]);
@@ -728,16 +731,9 @@ export function streamTypedRefMaterializeAdapter(
}
if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta);
if (meta.hierarchy && meta.children.length) {
const vtSlot = B.tmp(),
vt = B.tmp(),
preSlot = B.tmp(),
pre = B.tmp();
B.line(
`${vtSlot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 1`,
);
B.line(`${vt} = load ptr, ptr ${vtSlot}`);
B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`);
B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`);
const pre = B.tmp();
for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType))
B.line(line.trimStart());
const descendants = (current: typeof meta): (typeof meta)[] =>
current.children.flatMap((child) => [child, ...descendants(child)]);
for (const child of descendants(meta).reverse()) {
@@ -1364,16 +1360,9 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
if (subclasses.length > 0) {
exact = `@${snapshot}_cexact`;
const B = new BlockBuilder();
const vtSlot = B.tmp(),
vt = B.tmp(),
preSlot = B.tmp(),
pre = B.tmp();
B.line(
`${vtSlot} = getelementptr inbounds %${classStructSym(meta.def.name)}, ptr %p, i64 0, i32 1`,
);
B.line(`${vt} = load ptr, ptr ${vtSlot}`);
B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`);
B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`);
const pre = B.tmp();
for (const line of classPreLines(meta, classStructSym(meta.def.name), "%p", pre, host.sizeType))
B.line(line.trimStart());
for (const child of subclasses.reverse()) {
const childAdapter = host.liveDynRefAdapter({ kind: "object", className: child.def.name });
const match = B.tmp(),
@@ -865,6 +865,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "blob":
case "wasm":
case "record":
case "checkedCast":
case "disposable":
case "punycode":
case "constants":
@@ -374,6 +374,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fsp.globStrings": "scr_fsp_glob_strings",
"fsp.globError": "scr_fsp_glob_error",
"fs.realpathSync": "scr_fs_realpath",
"fs.realpathValue": "scr_fs_realpath_value",
"fs.realpathNativeSync": "scr_fs_realpath_promise",
"fs.unlinkSync": "scr_fs_unlink",
"fs.chmodSync": "scr_fs_chmod",
@@ -437,6 +438,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.concatDyn": "scr_buffer_concat_dyn",
"buffer.concatLenDyn": "scr_buffer_concat_len_dyn",
"buffer.alloc": "scr_buffer_alloc",
"buffer.allocValue": "scr_buffer_alloc_value",
"buffer.allocUnsafe": "scr_buffer_alloc_unsafe",
"buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow",
"buffer.poolFromStr": "scr_buffer_from_str",
@@ -444,9 +446,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.poolFromBytes": "scr_buffer_from_bytes",
"buffer.poolFromArray": "scr_buffer_from_arr",
"buffer.poolFromDyn": "scr_buffer_from_dyn_pooled",
"buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled",
"buffer.poolSize": "scr_buffer_pool_size",
"buffer.setPoolSize": "scr_buffer_set_pool_size",
"buffer.byteLenDyn": "scr_buffer_byte_length_dyn",
"buffer.byteLenValue": "scr_buffer_byte_length_value",
"buffer.byteLenStr": "scr_bytes_byte_length_str",
"buffer.isEncoding": "scr_bytes_is_encoding",
// The checked-dynamic compare/equals validators (scr_bytes_io.c):
@@ -643,6 +647,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"insp.circular": "scr_insp_circular",
// Undeclared source properties kept beside a checked record (scr_json.c).
"record.sourceHas": "scr_record_source_has",
// Checked-cast fallback reads (scr_json.c); take answers +1.
"checkedCast.fallbackTaken": "scr_dyn_check_fallback_taken",
"checkedCast.fallbackTake": "scr_dyn_check_fallback_take",
// WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c):
// constructions +1; url.new, the fileURLToPath pair, the win32
// pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw).
@@ -14,7 +14,7 @@
* 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 { classMembershipIntervals, type LlClassMeta } from "./classes.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { vAdapters } from "./shapes.js";
@@ -124,6 +124,7 @@ export function emitCaughtInstanceof(
const B = host.B;
const target = host.classMetaOf(className);
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
if (target.idInRc) return emitCaughtClassIdInstanceof(host, caught, target, intervals);
if (!target.hierarchy) {
const adapterSlot = B.tmp();
const adapter = B.tmp();
@@ -169,6 +170,80 @@ export function emitCaughtInstanceof(
return result;
}
/** `e instanceof C` for a class whose instances keep their class id in
* the rc word (classes.ts LlClassMeta.idInRc). They are thrown as REF
* payloads, like standalone class instances: the retain adapter names the
* hierarchy (the adapter of whichever static class threw), and the payload's
* class id is tested against the intervals. */
function emitCaughtClassIdInstanceof(
host: LlvmEmitterContext,
caught: string,
target: LlClassMeta,
intervals: { pre: number; post: number }[],
): string {
const B = host.B;
const kp = B.tmp(),
kind = B.tmp(),
isRef = B.tmp(),
adapterSlot = B.tmp(),
adapter = B.tmp();
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`);
B.line(`${kind} = load i32, ptr ${kp}`);
B.line(`${isRef} = icmp eq i32 ${kind}, ${CAUGHT_REF_KIND}`);
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`);
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
let anyAdapter: string | undefined;
for (const m of host.classMeta.values()) {
if (m.root !== target.root) continue;
const rc = vAdapters(host.shapeHost, { kind: "object", className: m.def.name });
const matches = B.tmp();
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
if (anyAdapter === undefined) anyAdapter = matches;
else {
const joined = B.tmp();
B.line(`${joined} = or i1 ${anyAdapter}, ${matches}`);
anyAdapter = joined;
}
}
const inHierarchy = B.tmp();
B.line(`${inHierarchy} = and i1 ${isRef}, ${anyAdapter!}`);
const test = B.newLabel("caught.class"),
join = B.newLabel("caught.class.done"),
from = B.currentLabel();
B.condBr(inHierarchy, test, join);
B.startBlock(test);
const payloadSlot = B.tmp(),
payload = B.tmp();
B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 4`);
B.line(`${payload} = load ptr, ptr ${payloadSlot}`);
const pre = host.loadVtPre(payload, target.def.name);
let result: string | undefined;
for (const interval of intervals) {
const ge = B.tmp(),
le = B.tmp(),
t = B.tmp();
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
B.line(`${t} = and i1 ${ge}, ${le}`);
if (result === undefined) result = t;
else {
const combined = B.tmp();
B.line(`${combined} = or i1 ${result}, ${t}`);
result = combined;
}
}
if (result === undefined) throw new InternalCompilerError("empty class membership");
const tested = B.currentLabel();
B.br(join);
B.startBlock(join);
const out = B.tmp();
B.line(`${out} = phi i1 [ false, %${from} ], [ ${result}, %${tested} ]`);
return out;
}
/** SCR_EXC_REF (scr_exception.h). */
const CAUGHT_REF_KIND = 4;
/** 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;
+41 -16
View File
@@ -63,6 +63,9 @@ export interface PublishClassInfo {
post: number;
/** A runtime-provided root class (errors, emitters, streams). */
runtimeRooted: boolean;
/** Instances keep their class id in the rc word (classes.ts
* LlClassMeta.idInRc). */
idInRc: boolean;
/** The class's own and inherited fields: GEP index and storage type. */
fields: { name: string; index: number; type: IrType }[];
}
@@ -70,6 +73,12 @@ export interface PublishClassInfo {
/** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */
const PUB_KIND_OBJECT = 4;
const PUB_KIND_UNION = 5;
/** SCR_PUB_CLASS_ID, or'ed into a kind: the object keeps its class id
* in the high half of its rc word (classes.ts LlClassMeta.idInRc). */
const PUB_CLASS_ID = 0x100;
/** SCR_PUB_LOG_COUNT (scr_publish.c), in a log entry's `buf`: `rc` is the
* count of the low half. */
const PUB_LOG_COUNT = "-2147483648";
const RUNTIME_WALKERS = {
string: "@scr_pub_str_fn",
@@ -307,9 +316,18 @@ export class PublishWalkers {
const lines = [
head,
`entry:`,
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
` %vt = load ptr, ptr %vtp`,
` %pre = load ${this.host.sizeType}, ptr %vt`,
...(info.idInRc
? [
` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`,
` %id = load i32, ptr %idp`,
` %idpre = add i32 %id, -1`,
` %pre = zext i32 %idpre to ${this.host.sizeType}`,
]
: [
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
` %vt = load ptr, ptr %vtp`,
` %pre = load ${this.host.sizeType}, ptr %vt`,
]),
` switch ${this.host.sizeType} %pre, label %other [ ${cases.map((c) => `${this.host.sizeType} ${c.pre}, label %c${c.pre}`).join(" ")} ]`,
];
for (const c of cases)
@@ -344,6 +362,7 @@ export class PublishWalkers {
info.fields,
path,
loc,
info.idInRc,
);
}
@@ -355,20 +374,23 @@ export class PublishWalkers {
* targets), and an object with a collector header leaves its candidate
* buffer through scr_cyc_pub_take. Anything else calls scr_pub_mark, as
* does every mark of an RC-audit build. */
private markLines(cyc: boolean, kind: number): string[] {
private markLines(cyc: boolean, kind: number, classId = false): string[] {
const call = (name: string): string[] => [
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${kind})`,
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${classId ? kind | PUB_CLASS_ID : kind})`,
` br i1 %${name}, label %walk, label %done`,
];
if (this.host.rcAudit === true) return call("m");
const S = this.host.sizeType;
// An object that keeps its class id in the rc word is marked and
// logged through its count, the low half.
const C = classId ? "i32" : S;
const field = (name: string, slot: number): string[] => [
` %${name}p = getelementptr inbounds ptr, ptr %ctx, i32 ${slot}`,
` %${name} = load ${S === "i64" ? "i64" : "i32"}, ptr %${name}p`,
];
const lines = [
` %mrc = load ${S}, ptr %o`,
` %mimm = icmp eq ${S} %mrc, -1`,
` %mrc = load ${C}, ptr %o`,
` %mimm = icmp eq ${C} %mrc, -1`,
` br i1 %mimm, label %done, label %mtest`,
`mtest:`,
` %mrefp = getelementptr inbounds ptr, ptr %ctx, i32 6`,
@@ -380,13 +402,13 @@ export class PublishWalkers {
` %mok1 = and i1 %mok0, %mroom`,
];
let ok = "%mok1";
if (S === "i64") {
if (C === "i64") {
// 0xfffffffe and up are the log's escapes (SCR_PUB_RC_LIFETIME, _BIG).
lines.push(` %msmall = icmp ult i64 %mrc, 4294967294`, ` %mok2 = and i1 %mok1, %msmall`);
ok = "%mok2";
}
lines.push(` br i1 ${ok}, label %mfast, label %mslow`, `mslow:`, ...call("ms"), `mfast:`);
let buf = "0";
let buf = classId ? String(PUB_LOG_COUNT) : "0";
if (cyc) {
// ScrCycHdr.buf, the word before the object: its top bit while the
// object is a buffered candidate.
@@ -402,9 +424,10 @@ export class PublishWalkers {
...field("mtaken", 7),
` %mtaken1 = add ${S} %mtaken, 1`,
` store ${S} %mtaken1, ptr %mtakenp`,
...(classId ? [` %mix2 = or i32 %mix1, ${PUB_LOG_COUNT}`] : []),
` br label %mput`,
`mput:`,
` %mbuf = phi i32 [ 0, %mfast ], [ %mix1, %mtake ]`,
` %mbuf = phi i32 [ ${buf}, %mfast ], [ %mix${classId ? 2 : 1}, %mtake ]`,
);
buf = "%mbuf";
}
@@ -414,13 +437,13 @@ export class PublishWalkers {
` %me = getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, ${S} %mlogged`,
` store ptr %o, ptr %me`,
` %merc = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 1`,
S === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`,
C === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`,
` store i32 %mrc32, ptr %merc`,
` %mebuf = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 2`,
` store i32 ${buf}, ptr %mebuf`,
` %mlogged1 = add ${S} %mlogged, 1`,
` store ${S} %mlogged1, ptr %mloggedp`,
` store ${S} -1, ptr %o`,
` store ${C} -1, ptr %o`,
` br label %walk`,
);
return lines;
@@ -431,13 +454,14 @@ export class PublishWalkers {
* before) are the ones scr_pub_push skips: they are tested here, without
* the call. The lines end in a fresh block labelled `pushed${id}`. */
private push(v: string, fn: string, id: string): string[] {
const size = this.host.sizeType;
return [
` %null${id} = icmp eq ptr ${v}, null`,
` br i1 %null${id}, label %pushed${id}, label %live${id}`,
`live${id}:`,
` %rc${id} = load ${size}, ptr ${v}`,
` %immortal${id} = icmp eq ${size} %rc${id}, -1`,
// The count (the low half of a 64-bit rc word) is UINT32_MAX for an
// immortal object of every kind (scr_rc.h SCR_RC_COUNT).
` %rc${id} = load i32, ptr ${v}`,
` %immortal${id} = icmp eq i32 %rc${id}, -1`,
` br i1 %immortal${id}, label %pushed${id}, label %push${id}`,
`push${id}:`,
...this.pushLines(v, fn, id),
@@ -480,8 +504,9 @@ export class PublishWalkers {
members: { name: string; index: number; type: IrType }[],
path: string,
loc: SrcLoc,
classId = false,
): string[] {
const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT), `walk:`];
const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT, classId), `walk:`];
let n = 0;
for (const member of members) {
const fn = this.walker(member.type, `${path}.${member.name}`, loc);
@@ -117,7 +117,13 @@ test("ProgramLifetime instances live in the current region and count in their ch
);
// The first reference is implied: allocation leaves the chunk count alone.
expect(nw, cls).not.toContain("2097151");
expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o");
// The rc word: the class id in the high half (the hierarchy keeps it
// there instead of a vtable word) over the program-lifetime count, the
// midpoint of the 32-bit count in the low half.
const word = /store i64 (\d+), ptr %o\.vt/.exec(nw)?.[1];
expect(word, cls).toBeDefined();
expect(BigInt(word!) % 2n ** 32n, cls).toBe(2n ** 31n);
expect(BigInt(word!) / 2n ** 32n > 0n, cls).toBe(true);
expect(nw, cls).not.toContain("scr_rt_calloc");
// retain/release: the chunk count, NULL-tolerant, once a region
// exists, and only in chunks that have an owner (freeable regions).
@@ -36,7 +36,7 @@ import {
mangleRecordStruct,
} from "../mangle.js";
import { threadHotAddressLines } from "./alloc.js";
import { classFieldBase, type LlClassMeta } from "./classes.js";
import { classFieldBase, classPreLines, type LlClassMeta } from "./classes.js";
import { llvmCommentText } from "./common.js";
import type { NullableRefFields } from "./nullable-fields.js";
import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js";
@@ -389,9 +389,7 @@ export class LifetimeRegions {
out.push(
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk ${llvmCommentText(cls.name)} and its subclasses`,
`entry:`,
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
` %vt = load ptr, ptr %vtp`,
` %pre = load ${S}, ptr %vt`,
...classPreLines(meta, mangleClassStruct(cls.name), "%o", "%pre", S),
` switch ${S} %pre, label %done [ ${members.map((m) => `${S} ${m.pre}, label %c${m.pre}`).join(" ")} ]`,
...members.flatMap((m) => [
`c${m.pre}: ; ${llvmCommentText(m.def.name)}`,
+15 -5
View File
@@ -1249,6 +1249,11 @@ export function rcMembers(
];
}
/** The integer type a release or retain loads and stores the count as:
* the rc word, or its low half (an i32 at offset 0) for class instances that
* keep their class number in the high half (classes.ts LlClassMeta.idInRc). */
export type RcCount = "i32" | "i64";
/** The NULL- and immortal-skipping retain body shared by every shape. A
* retain never touches the cycle header: a buffered candidate that is
* retained again stays a candidate (scr_cycle.c, markRoots). */
@@ -1257,8 +1262,9 @@ export function retainBody(
fnName: string,
comment = "",
immortal = false,
count: RcCount = host.sizeType,
): string[] {
const S = host.sizeType;
const S = count;
if (immortal) return immortalRcBody(host, fnName, "ptr", comment);
return [
`define internal ptr @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`,
@@ -1325,6 +1331,8 @@ export interface DirectDestroy {
readonly destroy: string;
/** The child carries a collector header. */
readonly traced: boolean;
/** The child's count type, when it is not the rc word (RcCount). */
readonly count?: RcCount;
}
/** The release of child `value` (a loaded field, possibly NULL) inside a
@@ -1339,19 +1347,20 @@ export function directChildReleaseLines(
comment: string,
): string[] {
const S = host.sizeType;
const C = target.count ?? S;
host.declare(`declare void @scr_rc_destroy(ptr, ptr)`);
const lines = [
` %${tag}.nn = icmp ne ptr ${value}, null`,
` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`,
`${tag}.chk:`,
` %${tag}.rc = load ${S}, ptr ${value}`,
` %${tag}.last = icmp eq ${S} %${tag}.rc, 1`,
` %${tag}.rc = load ${C}, ptr ${value}`,
` %${tag}.last = icmp eq ${C} %${tag}.rc, 1`,
` br i1 %${tag}.last, label %${tag}.dead, label %${tag}.shared`,
`${tag}.shared:`,
` call void ${target.release}(ptr ${value}) ; ${comment}`,
` br label %${tag}.next`,
`${tag}.dead: ; ${comment}: the last reference`,
` store ${S} 0, ptr ${value}`,
` store ${C} 0, ptr ${value}`,
];
if (target.traced) {
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
@@ -1514,8 +1523,9 @@ export function releaseFastPath(
traced: boolean,
comment = "",
immortal = false,
count: RcCount = host.sizeType,
): string[] {
const S = host.sizeType;
const S = count;
if (immortal) return immortalRcBody(host, fnName, "void", comment);
return [
`define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`,
@@ -13,8 +13,8 @@
* With `scan`, a string that is not proven ASCII is first compared with
* the runtime's published scan receiver (ScrStrScan, scr_runtime.h): the
* string an indexed scan converted to UTF-16 units, whose length and units
* then read inline. Only targets whose runtime state is not thread-local
* publish it to emitted code.
* then read inline. Where the runtime keeps it per thread, the declaration
* is thread-local.
*/
import { exactIndexLines, sizeToDouble } from "./common.js";
@@ -53,8 +53,8 @@ export function stringScanType(sz: string): string {
return `{ ptr, ptr, ${sz} }`;
}
export function stringScanDecl(sz: string): string {
return `@scr_str_scan = external global ${stringScanType(sz)}`;
export function stringScanDecl(sz: string, threadLocal: boolean): string {
return `@scr_str_scan = external ${threadLocal ? "thread_local " : ""}global ${stringScanType(sz)}`;
}
/** The scan receiver test of `%s`: branches to `hit` with `%scan.n` (its
@@ -85,12 +85,11 @@ test("slots.get reads the slot word, the page and the entry inline", () => {
];
expect(validateModule(module(calls))).toEqual([]);
const llvm = emitLlvmModule(module(calls), { pointerBits: 64 });
// The slot word sits after the header words of every hierarchy member.
// The slot word sits after the rc word of every hierarchy member, which
// keeps its class id in the rc word instead of a vtable word.
expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, i32, ptr \\}`));
expect(llvm).toMatch(
new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`),
);
expect(llvm).toMatch(
new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`),
new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, i32, ptr, (i32|double) \\}`),
);
expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`));
// One block per family name.
@@ -102,7 +101,7 @@ test("slots.get reads the slot word, the page and the entry inline", () => {
);
const main = body(llvm, "sc_f_main");
expect(main).toMatch(
/getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/,
/getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 1\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/,
);
expect(main).not.toMatch(/call ptr @scr_slots_get/);
expect(main).toMatch(
+7
View File
@@ -109,6 +109,13 @@ export function mangleVtStruct(rootClassName: string): string {
export function mangleVtInstance(className: string): string {
return `sc_vtable_${sanitize(className)}`;
}
/** The per-HIERARCHY array of the vtables of its classes in preorder, for
* hierarchies whose instances keep their class id in the rc word
* (classes.ts LlClassMeta.idInRc). `sc_vta_` differs from `sc_vtable_` and
* `sc_vtt_` before any user-controlled text. */
export function mangleVtArray(rootClassName: string): string {
return `sc_vta_${sanitize(rootClassName)}`;
}
/** A virtual slot's member name inside the vtable struct. Sibling
* branches of one hierarchy can each declare a slot for the SAME method
* name (root-most in their own subtree, overridden below — mixin layers
@@ -49,6 +49,12 @@ export interface NativeLinkFeatures {
/** events.once with options: scr_events_once_signal.c. Absent (not
* false) for programs without it. */
eventsOnceSignal?: boolean;
/** Fixed-width Buffer methods called on checked-dynamic values link
* scr_bytes_dyn.c; absent (false) for programs without them. */
dynBufferNum?: boolean;
/** Builtin functions read as values (Buffer.byteLength, Buffer.alloc,
* fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */
builtinValues?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -119,6 +119,11 @@ export interface CcOptions {
/** events.once with options (events.onceSignal): compiles
* scr_events_once_signal.c. */
eventsOnceSignal?: boolean;
/** Fixed-width Buffer methods called on checked-dynamic values:
* compiles scr_bytes_dyn.c. */
dynBufferNum?: boolean;
/** Builtin functions read as values: compiles scr_builtin_values.c. */
builtinValues?: boolean;
/** The program constructs EventTarget/Event/CustomEvent values or
* calls events.addAbortListener (the event.* libCalls), or uses the
* native fetch AbortSignal built on the same engine: compiles
@@ -179,6 +179,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
// Fixed-width Buffer methods called on checked-dynamic values.
unit((u) => u.opts.dynBufferNum === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
// Builtin functions read as values in JavaScript dependencies.
unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"),
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
@@ -86,6 +86,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
dynTier: boolean;
streamDyn: boolean;
eventsOnceSignal: boolean;
dynBufferNum: boolean;
builtinValues: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
@@ -279,6 +281,8 @@ export function effectiveRuntimeFeatures(
dynTier: features.dynTier === true,
streamDyn: features.streamDyn === true,
eventsOnceSignal: features.eventsOnceSignal === true,
dynBufferNum: features.dynBufferNum === true,
builtinValues: features.builtinValues === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
nativeFetch,
+3 -2
View File
@@ -73,8 +73,9 @@ Options:
--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
the program has no dynamic-tier demotions, and makes a
checked cast whose value does not fit its type throw
instead of continuing with the value
--explain-dynamic[=<name>]
coverage only: list the functions that operate on dynamic
values and the bindings, fields, functions, classes and
@@ -68,6 +68,8 @@ export interface EarlyExecutableNativeFeatures {
dynTier?: boolean;
streamDyn?: boolean;
eventsOnceSignal?: boolean;
dynBufferNum?: boolean;
builtinValues?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -650,5 +650,57 @@ export function lowerJsonProperty(
loc,
};
}
if (member === "parse") {
// JSON.parse as a value: ToString of the text, then the reviver form
// (a reviver that is not a function is ignored, as in Node).
const loc = locOf(expr);
const name = "%builtin.JSON.parse";
if (!lowerer.builtinCallableValueFns.has(name)) {
lowerer.builtinCallableValueFns.set(name, name);
const params = ["text", "reviver"].map((name) => ({ localId: name, name, type: DYN }));
lowerer.liftedFns.push({
name,
params,
returnType: DYN,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
loc,
body: [
{
kind: "return",
value: {
kind: "libCall",
fn: "json.parseReviver",
args: [
{
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [varRef("text", DYN, loc)],
type: STRING,
loc,
},
varRef("reviver", DYN, loc),
],
type: DYN,
loc,
},
loc,
},
],
});
}
return {
kind: "dynFrom",
value: {
kind: "closure",
fnName: name,
captures: [],
type: { kind: "func", params: [DYN, DYN], ret: DYN },
loc,
},
fnName: "parse",
type: DYN,
loc,
};
}
lowerer.unsupported("SC1090", expr, `JSON methods as values (call '${member}' directly)`);
}
@@ -266,6 +266,59 @@ export function isOptionalProcessStreamProperty(
return lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind === "procStream";
}
/** The process.versions entries @types/node declares as plain strings
* that the runtime's versions object (node and openssl only) lacks. */
const ABSENT_DECLARED_VERSIONS: ReadonlySet<string> = new Set([
"http_parser",
"v8",
"ares",
"uv",
"zlib",
"modules",
]);
/** A process.versions member read whose declaration promises a string the
* runtime does not have. @types/node declares the entries above as plain
* strings, so a checked string read would throw on the absent entry and
* equality with undefined would fold to false. These reads keep the
* undefined arm, like the optional stream properties. The shipped fallback
* types declare other entries `string | undefined` and never reach this.
* An element read with a computed key qualifies when the checker types it
* as a plain string. */
export function isOptionalProcessVersionsRead(lowerer: Lowerer, node: ts.Node): boolean {
let key: string | null;
if (ts.isPropertyAccessExpression(node)) key = node.name.text;
else if (ts.isElementAccessExpression(node))
key = ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
else return false;
if (key !== null && !ABSENT_DECLARED_VERSIONS.has(key)) return false;
if (!programMentionsVersions(lowerer)) return false;
const symbol = lowerer.typeOf(node.expression).getSymbol();
return (
symbol !== undefined &&
ts.symbolName(symbol) === "ProcessVersions" &&
symbol.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
) &&
lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "string"
);
}
const mentionsVersions = new WeakMap<Lowerer, boolean>();
/** Whether any implementation source spells `versions`. Programs that
* never do skip the per-read checker queries above entirely. */
function programMentionsVersions(lowerer: Lowerer): boolean {
let known = mentionsVersions.get(lowerer);
if (known === undefined) {
known = lowerer.program
.getImplementationSourceFiles()
.some((sf) => !sf.isDeclarationFile && sf.text.includes("versions"));
mentionsVersions.set(lowerer, known);
}
return known;
}
export function lowerProcessStreamProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -0,0 +1,861 @@
/* Checked-cast fallback: a value that fails a checked cast keeps running.
*
* `JSON.parse(text) as T` (and `const x: T = anyValue`, `await res.json()
* as T`) is a checked conversion: the static code after it reads `T`'s
* native layout, so a value that does not fit `T` used to throw a
* TypeError at the cast. Node never checks: the program continues with the
* value it has. This module makes the failing case continue too, without
* changing the code that runs when the value fits.
*
* The unit is a FALLBACK SITE: a function-local binding whose only write
* is the checked cast (the ROOT), declared with `const`/`let` in a function
* body. The code that can observe the root is the statements after the
* cast in each enclosing statement list up to the root's declaration (the
* TAILS; every other reference disqualifies the site). Each tail is lowered
* twice:
*
* - the static lowering, exactly as before, used while the cast held;
* - the dynamic lowering, used after a failure: the root reads its checked
* dynamic value (a shadow local), and every expression derived from it is
* typed `any` (fallbackTainted), so the region lowers through scriptc's
* ordinary `any` path, which has Node's semantics: missing members read
* undefined, values keep their real types, extra members stay visible,
* and objects keep their identity.
*
* `if (flag) dynamic else static` selects the lowering after each tail's
* first statement; the cast sets the flag. A derived value that flows into
* a typed slot outside the region (a typed parameter, a field, an outer
* binding) passes the same checked conversion as any `any` value does
* there: it continues when it fits and throws that conversion's TypeError
* at that use when it does not. A statement of the dynamic lowering that
* has no `any` lowering compiles to a runtime fence that throws at that
* statement only when a failed value reaches it.
*
* Code without a fallback site is unchanged; code with one runs its static
* tail unchanged behind one flag test per tail, and the cast reads one
* thread-local word after the check (checkedCast.fallbackTaken).
* `--tier=static-only` (and library mode) opens no sites: casts throw. */
import * as ts from "../ts7/adapter.js";
import {
BOOL,
DYN,
type IrExpr,
type IrLocal,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../ir/ir.js";
import { isJsSourceFile, locOf } from "../program.js";
import { PoisonError, type Lowerer } from "./lowerer.js";
export interface FallbackSite {
root: ts.Symbol;
/** The root's declared name (a module-level root is a module binding). */
name: ts.Identifier;
/** The statement holding the checked cast (the declaration itself, or
* the `root = value as T` expression statement). */
boundaryStmt: ts.Statement;
/** Statements after which a tail starts, innermost first; removed as
* their lists split. */
anchors: Set<ts.Statement>;
flag: IrLocal;
shadow: IrLocal;
armed: boolean;
/** An initializer-less root's flag and shadow declarations, and the
* statement list they were emitted into. */
decls: { stmts: IrStmt[]; out: IrStmt[] } | null;
}
export interface FallbackTaint {
symbols: Set<ts.Symbol>;
memo: Map<ts.Node, boolean>;
}
/** Tails larger than this keep the throwing cast: every node is lowered
* twice. */
const MAX_TAIL_NODES = 20000;
/** A site arms only while fewer than this many splits enclose its tail
* or share its split point: each one lowers the tail again, so a tail is
* lowered at most 2^n times. */
const MAX_NESTED_SPLITS = 2;
type StatementList = { statements: { nodes: readonly ts.Statement[] } };
function isStatementList(node: ts.Node | undefined): boolean {
return (
node !== undefined && (ts.isBlock(node) || ts.isModuleBlock(node) || ts.isSourceFile(node))
);
}
function listOf(node: ts.Node): readonly ts.Statement[] {
return (node as unknown as StatementList).statements.nodes;
}
function strip(e: ts.Expression): ts.Expression {
let v = e;
while (ts.isParenthesizedExpression(v)) v = v.expression;
return v;
}
function enclosingFunction(node: ts.Node): ts.Node | undefined {
let n: ts.Node | undefined = node.parent;
while (n !== undefined && !ts.isFunctionLike(n) && !ts.isSourceFile(n)) n = n.parent;
return n;
}
/** `value as T` / `<T>value` over an `any` or `unknown` value, or a
* dynamic value flowing into an annotated declaration. */
function isCheckedCast(lowerer: Lowerer, e: ts.Expression, declared: ts.Type | null): boolean {
const dynamicFlags = ts.TypeFlags.Any | ts.TypeFlags.Unknown;
const dynamic = (x: ts.Expression): boolean =>
(lowerer.checker.getTypeAtLocation(x).flags & dynamicFlags) !== 0;
const v = strip(e);
if (ts.isAsExpression(v) || ts.isTypeAssertion(v)) {
const target = lowerer.checker.getTypeFromTypeNode(v.type);
if (target.flags & dynamicFlags) return false;
return dynamic(strip(v.expression));
}
return declared !== null && (declared.flags & dynamicFlags) === 0 && dynamic(v);
}
function writesSymbol(lowerer: Lowerer, n: ts.Node, sym: ts.Symbol): ts.Node | null {
const names = (e: ts.Node): boolean =>
ts.isIdentifier(e) && lowerer.resolveValueSymbol(e) === sym;
const targetNames = (t: ts.Expression): boolean => {
const e = strip(t);
if (names(e)) return true;
if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) {
let found = false;
const walk = (x: ts.Node): void => {
if (names(x)) found = true;
else ts.forEachChild(x, walk);
};
walk(e);
return found;
}
return false;
};
if (ts.isBinaryExpression(n)) {
const k = n.operatorToken.kind;
if (
k >= ts.SyntaxKind.FirstAssignment &&
k <= ts.SyntaxKind.LastAssignment &&
targetNames(n.left)
)
return n;
} else if (
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
(n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken) &&
targetNames(n.operand as ts.Expression)
) {
return n;
} else if (
(ts.isForOfStatement(n) || ts.isForInStatement(n)) &&
!ts.isVariableDeclarationList(n.initializer) &&
targetNames(n.initializer as ts.Expression)
) {
return n;
}
return null;
}
/** Hoisted or registered-once declarations, and `super()` calls (which
* initialize the instance once), that a second lowering would duplicate. */
function tailIsRelowerable(stmts: readonly ts.Statement[]): boolean {
let ok = true;
let count = 0;
const visit = (n: ts.Node): void => {
if (!ok) return;
if (++count > MAX_TAIL_NODES) {
ok = false;
return;
}
if (
ts.isFunctionDeclaration(n) ||
ts.isClassDeclaration(n) ||
ts.isClassExpression(n) ||
ts.isModuleDeclaration(n) ||
ts.isEnumDeclaration(n) ||
(ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.SuperKeyword) ||
(ts.isVariableDeclarationList(n) &&
(n.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using)) === 0)
) {
ok = false;
return;
}
ts.forEachChild(n, visit);
};
for (const s of stmts) visit(s);
return ok;
}
interface Candidate {
root: ts.Symbol;
name: ts.Identifier;
boundaryStmt: ts.Statement;
anchors: ts.Statement[];
}
/** The fallback site a declaration statement starts, or null. */
function fallbackCandidate(lowerer: Lowerer, stmt: ts.VariableStatement): Candidate | null {
const list = stmt.declarationList;
if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) return null;
if ((list.flags & ts.NodeFlags.Using) !== 0) return null;
if (list.declarations.nodes.length !== 1) return null;
if (ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt)?.length ?? 0) > 0) return null;
const decl = list.declarations.nodes[0]!;
if (!ts.isIdentifier(decl.name)) return null;
if (isJsSourceFile(ts.getSourceFileOfNode(stmt))) return null;
// Cheap gates before any scan: a checked-cast initializer, or a typed
// declaration without one.
const declared = decl.type !== undefined ? lowerer.checker.getTypeFromTypeNode(decl.type) : null;
if (
decl.initializer !== undefined
? !isCheckedCast(lowerer, decl.initializer, declared)
: declared === null
)
return null;
const rootList = stmt.parent;
// Module-level bindings are module state (and module statements lower
// one at a time): roots live in blocks.
if (rootList === undefined || !ts.isBlock(rootList)) return null;
const fn = enclosingFunction(stmt);
if (fn === undefined) return null;
const root = lowerer.checker.getSymbolAtLocation(decl.name);
if (root === undefined) return null;
// A root in a module-level block is scanned across its file.
const body = ts.isSourceFile(fn) ? fn : (fn as ts.FunctionLikeDeclaration).body;
if (body === undefined) return null;
// The only write: the initializer, or one `root = value as T` statement.
let boundaryStmt: ts.Statement | null = null;
let boundaryWrite: ts.Node | null = null;
const writes: ts.Node[] = [];
const findWrites = (n: ts.Node): void => {
const w = writesSymbol(lowerer, n, root);
if (w) writes.push(w);
ts.forEachChild(n, findWrites);
};
findWrites(body);
if (decl.initializer !== undefined) {
if (writes.length > 0) return null;
boundaryStmt = stmt;
} else {
if (writes.length !== 1) return null;
const w = writes[0]!;
if (
!ts.isBinaryExpression(w) ||
w.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
!ts.isIdentifier(strip(w.left)) ||
!ts.isExpressionStatement(w.parent) ||
!isCheckedCast(lowerer, w.right, declared)
)
return null;
if (enclosingFunction(w) !== fn) return null;
boundaryStmt = w.parent;
boundaryWrite = w;
}
// The split points: the statement holding the cast in each enclosing
// statement list, up to the root's list.
const anchors: ts.Statement[] = [];
let cur: ts.Node = boundaryStmt;
for (;;) {
const parent: ts.Node | undefined = cur.parent;
if (parent === undefined || ts.isFunctionLike(parent)) return null;
if (ts.isCaseClause(parent) || ts.isDefaultClause(parent)) return null;
if (isStatementList(parent)) {
anchors.push(cur as ts.Statement);
if (parent === rootList) break;
if (ts.isSourceFile(parent)) return null;
}
cur = parent;
}
const anchorIndex = new Map<ts.Node, { list: ts.Node; index: number }>();
for (const a of anchors)
anchorIndex.set(a, { list: a.parent!, index: listOf(a.parent!).indexOf(a) });
const tails: ts.Statement[] = [];
for (const a of anchors) {
const { list, index } = anchorIndex.get(a)!;
tails.push(...listOf(list).slice(index + 1));
}
if (!tailIsRelowerable(tails)) return null;
// Every other reference sits in a tail.
const inTail = (ref: ts.Node): boolean => {
let n: ts.Node = ref;
while (n.parent !== undefined && n !== body) {
const parent = n.parent;
if (isStatementList(parent)) {
for (const a of anchors) {
if (a.parent !== parent) continue;
const at = anchorIndex.get(a)!.index;
const mine = listOf(parent).indexOf(n as ts.Statement);
if (mine > at) return true;
if (mine < at) return false;
}
}
n = parent;
}
return false;
};
let referencesOk = true;
const scanRefs = (n: ts.Node): void => {
if (!referencesOk) return;
if (ts.isIdentifier(n) && n !== decl.name && lowerer.resolveValueSymbol(n) === root) {
const isBoundaryTarget =
boundaryWrite !== null &&
ts.isBinaryExpression(boundaryWrite) &&
strip(boundaryWrite.left) === n;
if (!isBoundaryTarget && !inTail(n)) referencesOk = false;
return;
}
ts.forEachChild(n, scanRefs);
};
scanRefs(body);
if (!referencesOk) return null;
return { root, name: decl.name, boundaryStmt, anchors };
}
function declareSiteLocals(
lowerer: Lowerer,
candidate: Candidate,
loc: SrcLoc,
): { site: FallbackSite; decls: IrStmt[] } {
const name = ts.symbolName(candidate.root);
const flag = lowerer.declareHiddenLocal(`%fb.${name}`, BOOL);
flag.mutable = true;
const shadow = lowerer.declareHiddenLocal(`%fbv.${name}`, DYN);
shadow.mutable = true;
const decls: IrStmt[] = [
{
kind: "varDecl",
localId: flag.id,
init: { kind: "boolLit", value: false, type: BOOL, loc },
loc,
},
{ kind: "varDecl", localId: shadow.id, init: null, loc },
];
const site: FallbackSite = {
root: candidate.root,
name: candidate.name,
boundaryStmt: candidate.boundaryStmt,
anchors: new Set(candidate.anchors),
flag,
shadow,
armed: false,
decls: null,
};
return { site, decls };
}
/** Before an initializer-less typed declaration lowers: open the fallback
* site its later `root = value as T` statement starts, declaring the flag
* (false) and the shadow that holds a failed value where the root is
* declared. Declarations with an initializer open when they arm. */
export function openFallbackSite(lowerer: Lowerer, stmt: ts.Statement, out: IrStmt[]): void {
if (!ts.isVariableStatement(stmt) || lowerer.fnStack.length === 0) return;
if (lowerer.tierMode === "static-only") return;
const decls = stmt.declarationList.declarations.nodes;
if (decls.length !== 1 || decls[0]!.initializer !== undefined || decls[0]!.type === undefined)
return;
const candidate = fallbackCandidate(lowerer, stmt);
if (candidate === null) return;
const opened = declareSiteLocals(lowerer, candidate, locOf(stmt));
out.push(...opened.decls);
opened.site.decls = { stmts: opened.decls, out };
lowerer.fallbackSites.set(candidate.root, opened.site);
}
/** A site that will not arm: drop its declarations again, so the program
* lowers exactly as it did without the site. */
function closeUnarmed(lowerer: Lowerer, site: FallbackSite): void {
const decls = site.decls;
if (decls !== null) {
for (const d of decls.stmts) {
const at = decls.out.indexOf(d);
if (at >= 0) decls.out.splice(at, 1);
}
}
const locals = lowerer.ctx.locals;
for (const local of [site.flag, site.shadow]) {
const at = locals.indexOf(local);
if (at >= 0) locals.splice(at, 1);
}
}
const FALLBACK_TARGETS = new Set(["record", "array", "f64", "string", "bool"]);
/** A plain check of a record, array or scalar target that holds no class
* instance: casts to program classes keep their checked semantics (a
* documented scriptc boundary: a class cast checks the instance's class). */
function fallbackCheck(lowerer: Lowerer, check: IrExpr & { kind: "dynCheck" }): boolean {
if (check.preserveRefs || check.exact || !FALLBACK_TARGETS.has(check.type.kind)) return false;
return !holdsClassInstance(lowerer, check.type, new Set());
}
function holdsClassInstance(lowerer: Lowerer, t: IrType, seen: Set<string>): boolean {
switch (t.kind) {
case "object":
case "classval":
return true;
case "array":
return holdsClassInstance(lowerer, t.elem, seen);
case "record": {
if (seen.has(t.shapeId)) return false;
seen.add(t.shapeId);
const shape = lowerer.shapes.get(t.shapeId);
if (shape === undefined) return true;
if (shape.indexValue !== undefined && holdsClassInstance(lowerer, shape.indexValue, seen))
return true;
return shape.fields.some((f) => holdsClassInstance(lowerer, f.type, seen));
}
case "union": {
const def = lowerer.unions.get(t.unionId);
return def === undefined || def.arms.some((arm) => holdsClassInstance(lowerer, arm, seen));
}
default:
return false;
}
}
/** After a statement lowers: when it holds a fallback site's cast, mark the
* check as a fallback check and read its outcome into the flag and the
* shadow. A cast whose lowering is not a plain check keeps throwing. */
export function armFallbackSite(lowerer: Lowerer, stmt: ts.Statement, lowered: IrStmt[]): void {
// `const root = value as T`: the check is the declaration's initializer.
if (
ts.isVariableStatement(stmt) &&
lowerer.fnStack.length > 0 &&
lowerer.tierMode !== "static-only" &&
lowered.some((s) => s.kind === "varDecl" && s.init?.kind === "dynCheck")
) {
const decls = stmt.declarationList.declarations.nodes;
const decl = decls.length === 1 ? decls[0]! : undefined;
if (decl?.initializer !== undefined && ts.isIdentifier(decl.name)) {
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
const local = symbol ? lowerer.bindingIn(lowerer.ctx, symbol) : null;
const at =
local === null
? -1
: lowered.findIndex(
(s) => s.kind === "varDecl" && s.localId === local.id && s.init?.kind === "dynCheck",
);
const write = at >= 0 ? lowered[at]! : null;
const init = write?.kind === "varDecl" ? write.init : null;
if (init?.kind === "dynCheck" && fallbackCheck(lowerer, init)) {
const candidate = fallbackCandidate(lowerer, stmt);
if (candidate !== null) {
const opened = declareSiteLocals(lowerer, candidate, locOf(stmt));
if (armSite(lowerer, opened.site, lowered, local!.id))
lowered.splice(at, 0, ...opened.decls);
else closeUnarmed(lowerer, opened.site);
}
}
}
}
if (lowerer.fallbackSites.size === 0) return;
for (const site of [...lowerer.fallbackSites.values()]) {
if (site.boundaryStmt !== stmt || site.armed) continue;
lowerer.fallbackSites.delete(site.root);
const id = lowerer.bindingIn(lowerer.ctx, site.root)?.id;
if (id === undefined || !armSite(lowerer, site, lowered, id)) closeUnarmed(lowerer, site);
}
}
function armSite(lowerer: Lowerer, site: FallbackSite, lowered: IrStmt[], id: string): boolean {
const at = lowered.findIndex(
(s) =>
(s.kind === "varDecl" && s.localId === id && s.init?.kind === "dynCheck") ||
(s.kind === "assign" && s.localId === id && s.value.kind === "dynCheck"),
);
if (at < 0) return false;
const write = lowered[at]!;
let check: IrExpr | null = null;
if (write.kind === "varDecl") check = write.init;
else if (write.kind === "assign") check = write.value;
if (check === null || check.kind !== "dynCheck" || !fallbackCheck(lowerer, check)) return false;
// Every tail must split: each split point's list is lowering whole.
const whole = [...site.anchors].every((anchor) => {
const real = listOf(anchor.parent!);
const index = real.indexOf(anchor);
return lowerer.activeStmtLists.some(
(entry) =>
entry.stmts[entry.index] === anchor &&
entry.stmts.length - entry.index === real.length - index,
);
});
if (!whole) return false;
// Splits around this site (sequential casts each split the code after
// them) and sites sharing a split point multiply the lowerings.
let nested = lowerer.fallbackSplitDepth;
for (const other of lowerer.fallbackArmed) {
for (const a of other.anchors) if (site.anchors.has(a)) nested++;
}
if (nested >= MAX_NESTED_SPLITS) return false;
check.fallback = true;
const loc = check.loc;
lowered.splice(
at + 1,
0,
{
kind: "assign",
localId: site.flag.id,
value: { kind: "libCall", fn: "checkedCast.fallbackTaken", args: [], type: BOOL, loc },
loc,
},
{
kind: "if",
cond: { kind: "varRef", localId: site.flag.id, type: BOOL, loc },
then: [
{
kind: "assign",
localId: site.shadow.id,
value: { kind: "libCall", fn: "checkedCast.fallbackTake", args: [], type: DYN, loc },
loc,
},
],
else_: null,
loc,
},
);
site.armed = true;
lowerer.fallbackArmed.push(site);
return true;
}
/** Armed sites whose tail starts after `stmt`, consumed. */
export function fallbackSplitsAt(lowerer: Lowerer, stmt: ts.Statement): FallbackSite[] {
if (lowerer.fallbackArmed.length === 0) return [];
const here: FallbackSite[] = [];
for (const site of lowerer.fallbackArmed) {
if (site.anchors.delete(stmt)) here.push(site);
}
if (here.length > 0)
lowerer.fallbackArmed = lowerer.fallbackArmed.filter((s) => s.anchors.size > 0);
return here;
}
/** The tail after a split point: `if (flag) <dynamic> else <static>` per
* site. The static lowering runs first and is exactly the lowering the
* tail had without a site. */
export function lowerFallbackTail(
lowerer: Lowerer,
rest: readonly ts.Statement[],
sites: readonly FallbackSite[],
): IrStmt[] {
if (sites.length === 0) return lowerer.lowerStmts(rest);
// An empty tail (the cast ends its block) needs no split.
if (rest.length === 0) return [];
const [site, ...others] = sites as [FallbackSite, ...FallbackSite[]];
const at = locOf(rest.length > 0 ? rest[0]! : site.boundaryStmt);
let statics: IrStmt[];
let dynamic: IrStmt[];
lowerer.fallbackSplitDepth++;
try {
statics = scoped(lowerer, () => lowerFallbackTail(lowerer, rest, others));
dynamic = lowerDynamicTail(lowerer, rest, site, others);
} finally {
lowerer.fallbackSplitDepth--;
}
return [
{
kind: "if",
cond: { kind: "varRef", localId: site.flag.id, type: BOOL, loc: at },
then: dynamic,
else_: statics,
loc: at,
},
];
}
function scoped<T>(lowerer: Lowerer, lower: () => T, frame = new Map<ts.Symbol, IrLocal>()): T {
lowerer.scopes.push(frame);
try {
return lower();
} finally {
lowerer.scopes.pop();
}
}
function lowerDynamicTail(
lowerer: Lowerer,
rest: readonly ts.Statement[],
site: FallbackSite,
others: readonly FallbackSite[],
): IrStmt[] {
const saved = {
taint: lowerer.fallbackTaint,
sink: lowerer.diagSink,
stats: lowerer.suppressStats,
sites: new Map(lowerer.fallbackSites),
armed: lowerer.fallbackArmed,
scopes: lowerer.scopes.length,
};
lowerer.fallbackTaint = fallbackTaintOf(lowerer, saved.taint, site.root, rest);
lowerer.diagSink = [];
lowerer.suppressStats = true;
lowerer.fallbackArmed = [];
try {
return scoped(
lowerer,
() => lowerFallbackTail(lowerer, rest, others),
new Map([[site.root, site.shadow]]),
);
} catch (error) {
if (!(error instanceof PoisonError) && process.env.SCRIPTC_FALLBACK_DEBUG) throw error;
while (lowerer.scopes.length > saved.scopes) lowerer.scopes.pop();
const loc = rest.length > 0 ? locOf(rest[0]!) : locOf(site.boundaryStmt);
return [
{
kind: "runtimeFence",
code: "SC1090",
message: `a value that failed its checked cast reached code with no dynamic lowering`,
loc,
},
];
} finally {
lowerer.fallbackTaint = saved.taint;
lowerer.diagSink = saved.sink;
lowerer.suppressStats = saved.stats;
lowerer.fallbackSites = saved.sites;
lowerer.fallbackArmed = saved.armed;
}
}
/* ── taint: which expressions of a dynamic tail read a failed value ── */
/** True when `node`'s value derives from a failed checked value in the
* dynamic lowering being built: it is typed `any` there. */
export function fallbackTainted(lowerer: Lowerer, node: ts.Node): boolean {
const taint = lowerer.fallbackTaint;
if (taint === null) return false;
const known = taint.memo.get(node);
if (known !== undefined) return known;
taint.memo.set(node, false);
const result = computeTainted(lowerer, taint, node);
taint.memo.set(node, result);
return result;
}
function computeTainted(lowerer: Lowerer, taint: FallbackTaint, node: ts.Node): boolean {
const t = (n: ts.Node): boolean => fallbackTainted(lowerer, n);
if (ts.isIdentifier(node)) {
const sym = lowerer.resolveValueSymbol(node);
return sym !== null && taint.symbols.has(sym);
}
if (
ts.isParenthesizedExpression(node) ||
ts.isNonNullExpression(node) ||
ts.isAsExpression(node) ||
ts.isTypeAssertion(node) ||
ts.isSatisfiesExpression(node) ||
ts.isAwaitExpression(node)
)
return t(node.expression);
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))
return t(node.expression);
if (ts.isCallExpression(node)) return t(node.expression);
if (ts.isConditionalExpression(node)) return t(node.whenTrue) || t(node.whenFalse);
if (ts.isBinaryExpression(node)) {
const k = node.operatorToken.kind;
switch (k) {
case ts.SyntaxKind.QuestionQuestionToken:
case ts.SyntaxKind.BarBarToken:
case ts.SyntaxKind.AmpersandAmpersandToken:
case ts.SyntaxKind.PlusToken:
return t(node.left) || t(node.right);
case ts.SyntaxKind.CommaToken:
case ts.SyntaxKind.EqualsToken:
return t(node.right);
case ts.SyntaxKind.PlusEqualsToken:
case ts.SyntaxKind.QuestionQuestionEqualsToken:
case ts.SyntaxKind.BarBarEqualsToken:
case ts.SyntaxKind.AmpersandAmpersandEqualsToken:
return t(node.left) || t(node.right);
default:
return false;
}
}
// A literal beside a failed value (`value ?? {}`, `ok ? value : []`)
// is built dynamically too: its contextual type is the static one.
if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)) {
// A literal spreading a failed value is built dynamically, and so is
// one holding a failed value that initializes a binding (the binding
// turns dynamic) or sits in such a literal. A literal flowing straight
// into a typed slot keeps its layout: each failed member is checked
// into its field, so members that fit continue.
let holds = false;
if (ts.isObjectLiteralExpression(node)) {
for (const p of node.properties.nodes) {
if (ts.isSpreadAssignment(p) && t(p.expression)) return true;
if (
(ts.isPropertyAssignment(p) && t(p.initializer)) ||
(ts.isShorthandPropertyAssignment(p) && t(p.name))
)
holds = true;
}
} else {
for (const e of node.elements.nodes)
if (t(ts.isSpreadElement(e) ? e.expression : e)) holds = true;
}
if (holds) {
// The outermost literal of a nest decides for all of it.
let top: ts.Node = node;
for (;;) {
const host: ts.Node | undefined = top.parent;
if (host !== undefined && ts.isPropertyAssignment(host) && host.parent !== undefined) {
top = host.parent;
continue;
}
if (host !== undefined && ts.isArrayLiteralExpression(host)) {
top = host;
continue;
}
break;
}
// An untyped destination (`console.log`, `JSON.stringify`) takes it
// dynamically.
const contextual = lowerer.checker.getContextualType(top);
if (
contextual !== undefined &&
(contextual.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0
)
return true;
const host = top.parent;
if (host !== undefined && ts.isVariableDeclaration(host) && host.initializer === top)
return true;
}
let child: ts.Node = node;
let parent = node.parent;
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
child = parent;
parent = parent.parent;
}
if (parent === undefined) return false;
if (ts.isBinaryExpression(parent)) {
const k = parent.operatorToken.kind;
if (
k === ts.SyntaxKind.QuestionQuestionToken ||
k === ts.SyntaxKind.BarBarToken ||
k === ts.SyntaxKind.AmpersandAmpersandToken
)
return t(parent.left === child ? parent.right : parent.left);
}
if (ts.isConditionalExpression(parent) && parent.condition !== child)
return t(parent.whenTrue === child ? parent.whenFalse : parent.whenTrue);
return false;
}
if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
if (
node.operator === ts.SyntaxKind.PlusPlusToken ||
node.operator === ts.SyntaxKind.MinusMinusToken
)
return t(node.operand);
return false;
}
return false;
}
/** The taint of a dynamic tail: the root, and every binding declared in the
* tail that a tainted value is written to (a fixpoint over the tail,
* nested functions included). Parameters of callbacks passed to a tainted
* callee are tainted too: the dynamic call passes them dynamic values. */
function fallbackTaintOf(
lowerer: Lowerer,
outer: FallbackTaint | null,
root: ts.Symbol,
region: readonly ts.Statement[],
): FallbackTaint {
const symbols = new Set(outer?.symbols ?? []);
symbols.add(root);
const taint: FallbackTaint = { symbols, memo: new Map() };
const saved = lowerer.fallbackTaint;
lowerer.fallbackTaint = taint;
try {
growTaint(lowerer, taint, region);
} finally {
lowerer.fallbackTaint = saved;
}
return taint;
}
function growTaint(lowerer: Lowerer, taint: FallbackTaint, region: readonly ts.Statement[]): void {
const symbols = taint.symbols;
const declared = new Set<ts.Symbol>();
const collectDeclared = (n: ts.Node): void => {
if (
(ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) &&
ts.isIdentifier(n.name)
) {
const s = lowerer.checker.getSymbolAtLocation(n.name);
if (s) declared.add(s);
}
ts.forEachChild(n, collectDeclared);
};
for (const s of region) collectDeclared(s);
const bindNames = (name: ts.BindingName): boolean => {
let changed = false;
const visit = (n: ts.Node): void => {
if (ts.isIdentifier(n)) {
const s = lowerer.checker.getSymbolAtLocation(n);
if (s && !symbols.has(s)) {
symbols.add(s);
changed = true;
}
return;
}
if (ts.isBindingElement(n)) {
if (n.name !== undefined) visit(n.name);
return;
}
ts.forEachChild(n, visit);
};
visit(name);
return changed;
};
for (let round = 0; round < 32; round++) {
let changed = false;
taint.memo.clear();
const visit = (n: ts.Node): void => {
if (
ts.isVariableDeclaration(n) &&
n.initializer !== undefined &&
fallbackTainted(lowerer, n.initializer)
) {
if (bindNames(n.name)) changed = true;
} else if (ts.isBinaryExpression(n)) {
const k = n.operatorToken.kind;
if (
k >= ts.SyntaxKind.FirstAssignment &&
k <= ts.SyntaxKind.LastAssignment &&
ts.isIdentifier(strip(n.left))
) {
const s = lowerer.resolveValueSymbol(strip(n.left) as ts.Identifier);
if (s && declared.has(s) && !symbols.has(s) && fallbackTainted(lowerer, n.right)) {
symbols.add(s);
changed = true;
}
}
} else if (
ts.isForOfStatement(n) &&
ts.isVariableDeclarationList(n.initializer) &&
fallbackTainted(lowerer, n.expression)
) {
for (const d of n.initializer.declarations.nodes) if (bindNames(d.name)) changed = true;
} else if (ts.isCallExpression(n) && fallbackTainted(lowerer, n.expression)) {
for (const arg of n.arguments.nodes) {
const a = strip(arg);
if (ts.isArrowFunction(a) || ts.isFunctionExpression(a)) {
for (const p of a.parameters.nodes) if (bindNames(p.name)) changed = true;
}
}
}
ts.forEachChild(n, visit);
};
for (const s of region) visit(s);
if (!changed) break;
}
taint.memo.clear();
}
@@ -5245,7 +5245,15 @@ export class ClassDynamicDispatch {
if (methodInfo) {
owner = methodInfo.declarer;
params = methodInfo.sig.params;
result = methodInfo.sig.ret;
// An untyped async method's `dyn` result is its promise's payload; the
// call answers the promise. An async generator method answers its
// generator object, which its signature already returns.
result =
methodInfo.sig.async === true &&
!methodInfo.sig.gen &&
methodInfo.sig.ret.kind !== "promise"
? { kind: "promise", inner: methodInfo.sig.ret }
: methodInfo.sig.ret;
callee = `%${owner.def.name}.${method}`;
virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true;
} else if (
@@ -6,6 +6,8 @@ export class ClassProtocols {
private implemented: Set<ts.Symbol> | null = null;
private readonly methods = new Map<string, ts.ClassLikeDeclaration[]>();
private readonly answers = new Map<ts.Type, boolean>();
/** The program classes each Pick or Omit view accepts (classesAcceptedBy). */
private readonly accepted = new Map<ts.Type, ts.Symbol[]>();
/** What markIdentityFlows learned about the program, while it runs. */
private facts: FlowFacts | null = null;
@@ -414,6 +416,14 @@ export class ClassProtocols {
for (const part of ts.constituentTypes(type)) add(part);
return;
}
// A Pick or Omit view's values are instances of the class it views,
// or of any other program class that has the view's members.
if (this.pickOfProgramClass(type)) {
add(type.getAliasTypeArguments()[0]!);
for (const other of this.classesAcceptedBy(type))
for (const related of this.classFamily(other, facts)) out.add(related);
return;
}
const symbol = type.getSymbol();
if (symbol === undefined) return;
if (this.isProgramClassInstance(type)) {
@@ -429,6 +439,30 @@ export class ClassProtocols {
return out;
}
/** The program classes with methods whose instances are assignable to
* `type`. */
private classesAcceptedBy(type: ts.Type): ts.Symbol[] {
const known = this.accepted.get(type);
if (known !== undefined) return known;
this.index();
const found = new Set<ts.Symbol>();
for (const entries of this.methods.values())
for (const declaration of entries) {
const symbol = declaration.name
? this.checker.getSymbolAtLocation(declaration.name)
: undefined;
if (
symbol !== undefined &&
!found.has(symbol) &&
this.checker.isTypeAssignableTo(this.checker.getDeclaredTypeOfSymbol(symbol), type)
)
found.add(symbol);
}
const result = [...found];
this.accepted.set(type, result);
return result;
}
/** A class symbol with its base classes and the program classes that
* extend it. */
private classFamily(symbol: ts.Symbol, facts: FlowFacts): ts.Symbol[] {
@@ -570,6 +604,20 @@ export class ClassProtocols {
}
}
/** `Pick<C, K>` or `Omit<C, K>` over a program class instance type: a
* view of the class's own members, spelled inline instead of as an
* interface. Like an interface the class matches, a view that exposes a
* method needs a live receiver (see protocol). */
private pickOfProgramClass(type: ts.Type): boolean {
const alias = type.getAliasSymbol();
if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit"))
return false;
if (!alias.declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile))
return false;
const source = type.getAliasTypeArguments()[0];
return source !== undefined && this.isProgramClassInstance(source);
}
private isProgramClassInstance(type: ts.Type): boolean {
const symbol = type.getSymbol();
return (
@@ -592,6 +640,8 @@ export class ClassProtocols {
return element !== undefined && this.carriesIdentity(element, marked);
}
if (this.isProgramClassInstance(type)) return true;
// A live Pick or Omit view of a program class holds the instance itself.
if (this.pickOfProgramClass(type) && this.usesCheckedIdentity(type)) return true;
const symbol = type.getSymbol();
if (symbol === undefined) return false;
if (marked.has(symbol)) return true;
@@ -704,12 +754,13 @@ export class ClassProtocols {
const properties = this.checker.getPropertiesOfType(type);
if (symbol && protocols.has(symbol)) return true;
if (
!symbol ||
!symbol.declarations.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration) &&
!ts.getSourceFileOfNode(declaration).isDeclarationFile,
)
!this.pickOfProgramClass(type) &&
(!symbol ||
!symbol.declarations.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration) &&
!ts.getSourceFileOfNode(declaration).isDeclarationFile,
))
)
return false;
for (const property of properties) {
@@ -189,12 +189,16 @@ export function classPrototypeData(
continue;
const compiled = classMethodValue(lowerer, access, info, method, locOf(access));
if (!compiled) continue;
// A method value without a checked-dynamic form (an async method's
// promise-returning signature) gets no data slot.
const boxedValue = lowerer.coerceToExpected(compiled, DYN);
if (boxedValue.type.kind !== "dyn") continue;
const descriptor: IrExpr = {
kind: "dynObjLit",
fields: [
{
key: { kind: "strLit", value: "value", type: STRING, loc },
value: lowerer.coerceToExpected(compiled, DYN),
value: boxedValue,
},
{
key: { kind: "strLit", value: "writable", type: STRING, loc },
@@ -24,6 +24,7 @@ import {
typeEquals,
} from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import type { Lowerer } from "../lowerer.js";
import { newFnCtx, own } from "../lowerer.js";
import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js";
@@ -1306,6 +1307,185 @@ function knownBufferProducer(
return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac";
}
/** Buffer.from over a checked-dynamic source and encoding, both already in
* repeatable locals: Node's fromString takes utf8 for an absent, non-string
* or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name;
* other sources ignore the encoding. */
function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr {
const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc };
const isString = (x: IrExpr): IrExpr => ({
kind: "dynTest",
test: "string",
value: x,
type: BOOL,
loc,
});
const name: IrExpr = {
kind: "ternary",
cond: {
kind: "logical",
op: "&&",
left: isString(encoding),
right: {
kind: "strEq",
negated: true,
left: { kind: "dynCheck", value: encoding, type: STRING, loc },
right: { kind: "strLit", value: "", type: STRING, loc },
type: BOOL,
loc,
},
type: BOOL,
loc,
},
then: { kind: "dynCheck", value: encoding, type: STRING, loc },
else_: utf8,
type: STRING,
loc,
};
return {
kind: "ternary",
cond: isString(source),
then: {
kind: "libCall",
fn: "buffer.poolFromStrChecked",
args: [{ kind: "dynCheck", value: source, type: STRING, loc }, name],
type: BYTES_U8,
loc,
},
else_: {
kind: "libCall",
fn: "buffer.poolFromDynRef",
args: [source, utf8],
type: BYTES_U8,
loc,
},
type: BYTES_U8,
loc,
};
}
/** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in
* source order, and builds the result from their references. */
function withDynBufferArguments(
lowerer: Lowerer,
source: IrExpr,
encNode: ts.Expression | undefined,
loc: SrcLoc,
build: (source: IrExpr, encoding: IrExpr) => IrExpr,
): IrExpr {
const v = lowerer.declareHiddenLocal("%bufferValue", DYN);
const e = lowerer.declareHiddenLocal("%bufferEncoding", DYN);
const result = build(varRef(v.id, DYN, loc), varRef(e.id, DYN, loc));
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: v.id, init: lowerer.coerceToExpected(source, DYN), loc },
{
kind: "varDecl",
localId: e.id,
init:
encNode === undefined
? dynUndefinedExpr(loc)
: lowerer.coerceToExpected(lowerer.lowerExpr(encNode), DYN),
loc,
},
],
result,
type: result.type,
loc,
};
}
/** Buffer.from(value, encoding) over a checked-dynamic value whose encoding
* is not a literal: yazl forwards its `encoding` parameter, often undefined.
* Null when the encoding is absent or a literal. */
function dynBufferFromRuntimeEncoding(
lowerer: Lowerer,
value: IrExpr,
encNode: ts.Expression | undefined,
loc: SrcLoc,
): IrExpr | null {
if (encNode === undefined || lowerer.typeOf(encNode).isStringLiteralType()) return null;
return withDynBufferArguments(lowerer, value, encNode, loc, (source, encoding) =>
dynBufferFromParts(source, encoding, loc),
);
}
/** The deprecated `new Buffer(arg[, encoding])` in a dependency's
* JavaScript, through the global or an alias of it (buffer-crc32 and yazl
* keep it as the fallback for runtimes without Buffer.alloc/Buffer.from).
* Node never emits DEP0005 for calls from node_modules, so the construction
* is exactly Node's: a number allocates zero-filled bytes (and throws
* ERR_INVALID_ARG_TYPE beside a string second argument), anything else
* converts as Buffer.from does. Null elsewhere: other callers owe the
* deprecation warning. */
export function lowerNodeModulesBufferConstruct(
lowerer: Lowerer,
expr: ts.NewExpression,
): IrExpr | null {
const sf = expr.getSourceFile();
if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null;
const args = expr.arguments?.nodes ?? [];
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null;
const loc = locOf(expr);
const value = withDynBufferArguments(
lowerer,
lowerer.lowerExpr(args[0]!),
args[1],
loc,
(source, encoding) => ({
kind: "ternary",
cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc },
then: {
kind: "ternary",
cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc },
then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc },
else_: {
kind: "libCall",
fn: "buffer.alloc",
args: [{ kind: "dynCheck", value: source, type: F64, loc }],
type: BYTES_U8,
loc,
},
type: BYTES_U8,
loc,
},
else_: dynBufferFromParts(source, encoding, loc),
type: BYTES_U8,
loc,
}),
);
return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc };
}
/** Buffer.from(array or bytes, extra): Node copies an array-like source and
* ignores the encoding/offset argument (yazl passes its `encoding` through
* unchanged), but JavaScript still evaluates it after the source. */
function withIgnoredBufferArgument(
lowerer: Lowerer,
extraNode: ts.Expression | undefined,
source: IrExpr,
build: (source: IrExpr) => IrExpr,
): IrExpr {
if (extraNode === undefined) return build(source);
const extra = lowerer.lowerExpr(extraNode);
if (isSafeToDiscard(extra)) return build(source);
const loc = source.loc;
const tmp = lowerer.declareHiddenLocal("%bufferSource", source.type);
const result = build(varRef(tmp.id, source.type, loc));
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: tmp.id, init: source, loc },
{ kind: "exprStmt", expr: extra, loc },
],
result,
type: result.type,
loc,
};
}
export function lowerBufferStaticCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -1378,17 +1558,23 @@ function lowerBufferStaticValue(
) {
// A number[] literal (contextually typed by the lib's readonly
// number[] slot — build it element-wise, the Set-seed pattern).
if (args.length === 1) {
const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64));
const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc };
}
const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64));
const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
return withIgnoredBufferArgument(lowerer, args[1], seed, (array) => ({
kind: "libCall",
fn: "buffer.poolFromArray",
args: [array],
type: BYTES_U8,
loc,
}));
} else {
const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
const value = lowerer.lowerExpr(argNode);
// JS callback storage can be native DYN even when the checker
// reports any (or a narrowed Buffer). Dispatch on its actual ABI.
if (value.type.kind === "dyn") {
const runtimeEncoding = dynBufferFromRuntimeEncoding(lowerer, value, args[1], loc);
if (runtimeEncoding) return runtimeEncoding;
const encName = args[1] ? bufEncoding(lowerer, "Buffer.from", args[1]) : "utf8";
const enc: IrExpr = { kind: "strLit", value: encName, type: STRING, loc };
return {
@@ -1432,26 +1618,25 @@ function lowerBufferStaticValue(
// { toText: true } ? string : Uint8Array` result) whose lowered
// value is a u8 view copies like the typed form.
if (
args.length === 1 &&
((srcIr?.kind === "bytes" && srcIr.elem === "u8") ||
(srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8"))
(srcIr?.kind === "bytes" && srcIr.elem === "u8") ||
(srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8")
) {
return {
return withIgnoredBufferArgument(lowerer, args[1], value, (bytes) => ({
kind: "libCall",
fn: "buffer.poolFromBytes",
args: [value],
args: [bytes],
type: BYTES_U8,
loc,
};
}));
}
if (args.length === 1 && srcIr?.kind === "array" && srcIr.elem.kind === "f64") {
return {
if (srcIr?.kind === "array" && srcIr.elem.kind === "f64") {
return withIgnoredBufferArgument(lowerer, args[1], value, (array) => ({
kind: "libCall",
fn: "buffer.poolFromArray",
args: [value],
args: [array],
type: BYTES_U8,
loc,
};
}));
}
// A union of strings, u8 views and undefined/null (a JS-inferred
// npm result such as pako's deflate): the checked form dispatches
@@ -1981,8 +1981,60 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
case ts.SyntaxKind.LessThanLessThanToken:
case ts.SyntaxKind.GreaterThanGreaterThanToken:
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: {
if (!bothNum)
// Booleans take part as ToNumber converts them, true to 1 and false
// to 0: JavaScript code writes `isFn(a) ^ isFn(b)` and `flags | ok`.
// A hoisted JavaScript `var` can also hold undefined, and ToInt32
// maps ToNumber(undefined) (NaN) and ToNumber(null) to 0.
const optionalBoolTag = (e: IrExpr): number => {
if (e.type.kind !== "union") return -1;
const arms = lowerer.unions.get(e.type.unionId)?.arms ?? [];
const ok = arms.every(
(a) => a.kind === "bool" || a.kind === "undefinedT" || a.kind === "nullT",
);
return ok ? arms.findIndex((a) => a.kind === "bool") : -1;
};
const numeric = (e: IrExpr): boolean =>
e.type.kind === "f64" || e.type.kind === "bool" || optionalBoolTag(e) >= 0;
const boolBits = !bothNum && numeric(arithmeticLeft) && numeric(arithmeticRight);
if (!bothNum && !boolBits)
operatorUnsupported(lowerer, expr, ts.tokenToString(op) ?? "?", [left.type, right.type]);
const bitPrefix: IrStmt[] = [];
const oneOrZero = (cond: IrExpr): IrExpr => ({
kind: "ternary",
cond,
then: numLit(1, loc),
else_: numLit(0, loc),
type: F64,
loc,
});
// The tag test reads an operand twice: when either side needs it,
// both sides evaluate once into temporaries, in source order. A left
// operand that is safe to repeat still takes a temporary when the
// right one does: the right operand runs first in the prefix, and it
// can assign what the left operand reads.
const stash = optionalBoolTag(arithmeticLeft) >= 0 || optionalBoolTag(arithmeticRight) >= 0;
const rightTemp = stash && !isSafeToRepeat(arithmeticRight);
const stable = (e: IrExpr, beforeTemp: boolean): IrExpr => {
if (!stash || (isSafeToRepeat(e) && !beforeTemp)) return e;
const tmp = lowerer.declareHiddenLocal("%bits", e.type);
bitPrefix.push({ kind: "varDecl", localId: tmp.id, init: e, loc });
return varRef(tmp.id, e.type, loc);
};
const bitOperand = (operand: IrExpr, beforeTemp: boolean): IrExpr => {
const e = stable(operand, beforeTemp);
if (e.type.kind === "bool") return oneOrZero(e);
const tag = optionalBoolTag(e);
if (tag < 0 || e.type.kind !== "union") return e;
const unionId = e.type.unionId;
return {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: e, type: BOOL, loc },
then: oneOrZero({ kind: "unionNarrow", unionId, tag, value: e, type: BOOL, loc }),
else_: numLit(0, loc),
type: F64,
loc,
};
};
const bitOp =
op === ts.SyntaxKind.AmpersandToken
? "&"
@@ -1995,14 +2047,17 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
: op === ts.SyntaxKind.GreaterThanGreaterThanToken
? ">>"
: ">>>";
return {
const bits: IrExpr = {
kind: "bin",
op: bitOp,
left: arithmeticLeft,
right: arithmeticRight,
left: bitOperand(arithmeticLeft, rightTemp),
right: bitOperand(arithmeticRight, false),
type: F64,
loc,
};
return bitPrefix.length === 0
? bits
: { kind: "seqExpr", stmts: bitPrefix, result: bits, type: F64, loc };
}
case ts.SyntaxKind.EqualsEqualsEqualsToken:
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
@@ -235,7 +235,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
const receiver = tryLowerExpression(lowerer, expr.expression);
if (receiver?.type.kind === "dyn") {
const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
const optional = hasOptionalChainGuard(expr.expression);
const optional = hasOptionalChainGuard(expr.expression, lowerer);
return lowerer.maybeNarrow(
{
kind: "dynKeyGet",
@@ -378,7 +378,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
lowerer.lowerExpr(expr.argumentExpression),
expr.argumentExpression,
);
const optional = hasOptionalChainGuard(expr.expression);
const optional = hasOptionalChainGuard(expr.expression, lowerer);
return lowerer.maybeNarrow(
{
kind: "dynKeyGet",
@@ -488,7 +488,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
? lowerer.coerceToExpected(rawKey, DYN)
: lowerRecordPropertyKey(lowerer, rawKey, expr.argumentExpression);
if (key.type.kind === "string" || key.type.kind === "dyn") {
const opt = hasOptionalChainGuard(expr.expression);
const opt = hasOptionalChainGuard(expr.expression, lowerer);
return lowerer.maybeNarrow(
{
kind: "dynKeyGet",
@@ -211,6 +211,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
if (arithmetic && !runtimeOptionalUseOf(expr)) {
return { kind: "varRef", localId: local.id, type: local.type, loc };
}
// A failed checked value in a fallback tail reads its dynamic value
// plainly: `undefined` is a value it can hold (checked-fallback.ts).
if ((active || captured) && lowerer.fallbackTaint !== null && local.type.kind === "dyn") {
return { kind: "varRef", localId: local.id, type: local.type, loc };
}
if (active || captured) {
const use = runtimeOptionalUseOf(expr);
if (use?.complex) {
@@ -10,6 +10,7 @@ import {
type IrLocal,
type IrType,
NULL_T,
STRING,
type SrcLoc,
UNDEFINED_T,
arrayOf,
@@ -25,7 +26,10 @@ import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { jsBindingHasOpenWrites } from "../lower-stmts.js";
import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js";
import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js";
import { isOptionalProcessStreamProperty } from "../builtins/process.js";
import {
isOptionalProcessStreamProperty,
isOptionalProcessVersionsRead,
} from "../builtins/process.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { literalValues } from "../../literal-values.js";
import { literalUnionArm } from "../../union-discriminants.js";
@@ -413,6 +417,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
// instead of misreading the payload (the dyn boundary's usual stance).
// Object/array narrowings stay dyn-typed and keep their fences.
if (expr.type.kind === "dyn") {
// A process.versions entry declared as a string can be absent here.
if (isOptionalProcessVersionsRead(lowerer, node))
return lowerer.coerceToExpected(expr, lowerer.withUndefinedArm(STRING));
// An inferred JS member's storage may be deliberately wider than its
// declaration (prototype-selected buffers, null-initialized scratch
// objects). Extract at the consuming operation, not at the read.
@@ -591,6 +598,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
if (expr.type.kind !== "union") return expr;
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
return expr;
if (isOptionalProcessVersionsRead(lowerer, node)) return expr;
// Represented fields and index values can include undefined even when
// checker inference or lib.d.ts declares strings (RegExp captures).
@@ -3193,6 +3193,22 @@ export function lowerShorthandValue(
`generic functions as values (call '${propName.text}' directly)`,
);
}
// A program class name (`{ parse, TomlError }`, an imported class
// included): the class object, exactly the long form `{ C: C }`.
// Generic classes keep the identifier path's contextual pinning.
const classInfo = lowerer.classBySymbol.get(resolved);
if (classInfo && !classInfo.generic) {
const decoratedGlobal = classInfo.classDecorators?.valueGlobalId;
if (decoratedGlobal !== undefined) {
return {
kind: "varRef",
localId: decoratedGlobal,
type: { kind: "classval", className: classInfo.def.name },
loc,
};
}
return lowerer.classValueRef(classInfo, propName);
}
}
lowerer.rejectUnresolvedSymbol(
symbol
@@ -98,7 +98,11 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo
export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean {
const tail = chainTailDot(lowerer, expr);
if (!tail) return false;
const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
const recvTs = lowerer.typeOf(tail.expression);
// An `any` receiver short-circuits the whole tail too: `a?.b.c` reads
// `.c` normally once `a` is present.
if (recvTs.flags & ts.TypeFlags.Any) return true;
const recvT = lowerer.mapTypeOf(recvTs);
if (recvT?.kind === "dyn") return true;
if (recvT?.kind === "union") {
const def = lowerer.unions.get(recvT.unionId);
@@ -119,7 +123,7 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool
* throwing. Walks the receiver spine only; the argument of the access
* itself is not part of the guard. Parentheses end the chain
* (`(a?.b).c` reads `.c` from undefined); `!` does not. */
export function hasOptionalChainGuard(expr: ts.Expression): boolean {
export function hasOptionalChainGuard(expr: ts.Expression, lowerer?: Lowerer): boolean {
let cur: ts.Expression = expr;
for (;;) {
if (ts.isNonNullExpression(cur)) {
@@ -131,7 +135,11 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean {
ts.isElementAccessExpression(cur) ||
ts.isCallExpression(cur)
) {
if (cur.questionDotToken) return true;
// Inside an optional chain's guarded body (dynamic reads pass the
// lowerer), the enclosing optChain already short-circuits on the
// consumed token, so the steps above it read normally: `a?.b.c`
// throws when `a.b` is undefined.
if (cur.questionDotToken) return lowerer === undefined || !lowerer.chainHandled.has(cur);
cur = cur.expression;
continue;
}
@@ -27,6 +27,7 @@ import {
import {
BOOL,
BYTES_ELEMENT_NAME,
BYTES_U8,
DYN,
F64,
type IrExpr,
@@ -156,7 +157,7 @@ import { narrowStoredClassValue, structuralReadType } from "../class-unions.js";
import { InternalCompilerError } from "../../../errors.js";
import { lowerUnionFieldRead } from "./union-fields.js";
import { errorPropertyRead } from "../error-methods.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js";
import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js";
import { keyedFieldRead } from "./keyed-field-targets.js";
@@ -869,7 +870,7 @@ export function lowerPropertyAccessExpr(
type: STRING,
loc: locOf(expr.name),
};
const opt = hasOptionalChainGuard(expr.expression);
const opt = hasOptionalChainGuard(expr.expression, lowerer);
return lowerer.maybeNarrow(
{
kind: "dynKeyGet",
@@ -978,7 +979,7 @@ export function lowerPropertyAccessExpr(
type: STRING,
loc: locOf(expr.name),
};
const opt = hasOptionalChainGuard(expr.expression);
const opt = hasOptionalChainGuard(expr.expression, lowerer);
return lowerer.maybeNarrow(
{
kind: "dynKeyGet",
@@ -1110,7 +1111,7 @@ export function lowerPropertyAccessExpr(
type: STRING,
loc: locOf(expr.name),
};
const opt = hasOptionalChainGuard(expr.expression);
const opt = hasOptionalChainGuard(expr.expression, lowerer);
const read: IrExpr = {
kind: "dynKeyGet",
key,
@@ -1205,7 +1206,7 @@ export function lowerPropertyAccessExpr(
type: STRING,
loc: locOf(expr.name),
};
const opt = hasOptionalChainGuard(expr.expression);
const opt = hasOptionalChainGuard(expr.expression, lowerer);
return lowerer.maybeNarrow(
{
kind: "dynKeyGet",
@@ -1624,6 +1625,38 @@ export function lowerIntrinsicProperty(
if (netAddressStatic) return netAddressStatic;
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer")
return lowerCheckedPredicateValue(lowerer, "Buffer.isBuffer", "buffer", locOf(expr));
// Buffer statics as function values (`Buffer.byteLength.bind(Buffer)` in
// truncate-utf8-bytes): the checked forms take any argument as Node does.
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "byteLength")
return lowerer.lowerNativeCallableValue(
{
fn: "buffer.byteLenValue",
params: [DYN, DYN],
result: F64,
valueParams: [
{ mode: "required", type: DYN },
{ mode: "optional", type: DYN, defaultValue: undefined },
],
},
"Buffer.byteLength",
locOf(expr),
);
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "alloc")
return bufferAllocValue(lowerer, locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "compare")
return lowerer.lowerNativeCallableValue(
{
fn: "buffer.compareChk",
params: [DYN, DYN],
result: F64,
valueParams: [
{ mode: "required", type: DYN },
{ mode: "required", type: DYN },
],
},
"Buffer.compare",
locOf(expr),
);
if (lowerer.stdlibGlobalMember(expr, "Date") === "now")
return lowerer.lowerNativeCallableValue(
{
@@ -2205,11 +2238,80 @@ export function lowerIntrinsicProperty(
own(STDLIB_SURFACE.string, name) !== undefined
: ARRAY_METHODS.has(name)
) {
// A JavaScript string method read as a value (smol-toml's
// `!!(''.isWellFormed)` feature probe): String.prototype's method, the
// runtime's interned function that runs it on its receiver.
if (kind === "string" && isJsSourceFile(expr.getSourceFile()) && !expr.questionDotToken) {
const receiver = lowerer.lowerExpr(expr.expression);
const method: IrExpr = {
kind: "libCall",
fn: "dyn.builtinMethod",
args: [
{ kind: "strLit", value: "String", type: STRING, loc: locOf(expr) },
{ kind: "strLit", value: name, type: STRING, loc: locOf(expr.name) },
],
type: DYN,
loc: locOf(expr),
};
return isSafeToDiscard(receiver)
? method
: {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: receiver, loc: locOf(expr.expression) }],
result: method,
type: DYN,
loc: locOf(expr),
};
}
lowerer.unsupported("SC1090", expr, `${kind} methods as values (call '${name}' directly)`);
}
return null;
}
/** Buffer.alloc as a function value (`var alloc = Buffer.alloc` in
* tar-stream): the runtime checks the size, allocates and fills with
* `fill` and `encoding` as Node's alloc does. */
function bufferAllocValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const name = "%builtin.Buffer.alloc";
if (!lowerer.builtinCallableValueFns.has(name)) {
lowerer.builtinCallableValueFns.set(name, name);
const params = ["size", "fill", "encoding"].map((id) => ({ localId: id, name: id, type: DYN }));
lowerer.liftedFns.push({
name,
params,
returnType: BYTES_U8,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
loc,
body: [
{
kind: "return",
value: {
kind: "libCall",
fn: "buffer.allocValue",
args: params.map((p) => varRef(p.localId, DYN, loc)),
type: BYTES_U8,
loc,
},
loc,
},
],
});
}
return {
kind: "dynFrom",
value: {
kind: "closure",
fnName: name,
captures: [],
type: { kind: "func", params: [DYN, DYN, DYN], ret: BYTES_U8 },
loc,
},
fnName: "alloc",
type: DYN,
loc,
};
}
/** A `T | undefined` class field read that the checker narrowed to `T`
* where the field was assigned a value that may be missing (an unchecked
* indexed read). TypeScript's assignment narrowing is not a runtime fact
@@ -2723,7 +2825,7 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
kind: "dynKeyGet",
value,
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
...(expr.questionDotToken || hasOptionalChainGuard(expr.expression)
...(expr.questionDotToken || hasOptionalChainGuard(expr.expression, lowerer)
? { optional: true as const }
: {}),
type: DYN,
@@ -3079,7 +3181,7 @@ export function lowerUnionProperty(
kind: "dynKeyGet",
value: lowerer.coerceToExpected(value, DYN),
key,
...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}),
...(hasOptionalChainGuard(expr.expression, lowerer) ? { optional: true as const } : {}),
type: DYN,
loc: locOf(expr),
};
@@ -275,10 +275,13 @@ export function genericCallInstance(
{
const rdecl = rsig ? rsig.declaration : undefined;
if (
rsig &&
rdecl &&
(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) &&
!rdecl.body
) {
const overload = overloadCallInstance(lowerer, expr, info, rsig, rdecl);
if (overload) return overload;
lowerer.unsupported(
"SC1090",
expr,
@@ -313,14 +316,7 @@ export function genericCallInstance(
fenceGenericSignatureResult(lowerer, expr, retTs);
lowerer.badType(expr, retTs);
}
// A body that can return an indexed read that misses returns undefined
// there, as in JavaScript, whatever the declared result says. An async
// body's promise is always present and settles with undefined there.
const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl)
? mappedReturn.kind === "promise"
? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) }
: lowerer.runtimeOptionalType(mappedReturn)
: mappedReturn;
const returnType = runtimeReturnType(lowerer, info, mappedReturn);
// keyof-constrained type parameters (`K extends keyof T`): the bound
// LITERAL is semantic — the instance body reads the named field — so
@@ -331,23 +327,11 @@ export function genericCallInstance(
// share one runtime-keyed instance per IR signature, exactly the
// widened discipline.
const named = staticNameBindings(lowerer, expr, info);
const keyofTps = keyofConstrainedTypeParams(info);
if (keyofTps.size > 0) {
if (keyofConstrainedTypeParams(info).size > 0) {
const tsBindings = new Map<ts.Symbol, ts.Type>();
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
const litKey = info.typeParams
.filter((tp) => keyofTps.has(tp))
.map((tp) => {
const bound = tsBindings.get(tp);
return bound?.isStringLiteralType()
? JSON.stringify(bound.value)
: bound?.isNumberLiteralType()
? String(bound.value)
: "*";
})
.join(",");
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
extraKey: `@${litKey}${named?.key ?? ""}`,
extraKey: `${keyofLiteralKey(info, tsBindings)}${named?.key ?? ""}`,
tsBindings,
...(named ? { staticFnNames: named.names } : {}),
});
@@ -363,6 +347,110 @@ export function genericCallInstance(
);
}
/** A call that resolved to an OVERLOAD signature of a generic function
* or method with an implementation (`request<T>(p: WithHandler<T>):
* Promise<T>; request(p: Plain): Promise<Res>; request<T>(p: Params<T>)
* {...}`): the implementation monomorphizes under the bindings the
* overload's resolved signature proves for its type parameters (the
* structural unification every generic call uses, run against the
* overload; unbound parameters take their declared defaults). The
* instance's parameters and result are the IMPLEMENTATION's under those
* bindings — the body was checked against them — and the call's
* arguments and result coerce across, exactly as a non-generic overload
* call reconciles with its implementation (parameters the overload omits
* must be optional in the implementation). Null when the overload does
* not belong to this implementation or the arities do not line up. */
function overloadCallInstance(
lowerer: Lowerer,
expr: ts.CallExpression,
info: GenericFnInfo,
rsig: ts.Signature,
rdecl: ts.FunctionDeclaration | ts.MethodDeclaration,
): GenericInstance | null {
const impl = info.decl;
if (
rdecl.parent !== impl.parent ||
rdecl.name === undefined ||
impl.name === undefined ||
rdecl.name.getText() !== impl.name.getText() ||
rdecl.parameters.nodes.length > impl.parameters.nodes.length ||
impl.parameters.nodes
.slice(rdecl.parameters.nodes.length)
.some((p) => p.questionToken === undefined && p.initializer === undefined) ||
rdecl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined)
)
return null;
const declSig = lowerer.checker.getSignatureFromDeclaration(impl);
if (!declSig) return null;
const tsBindings = new Map<ts.Symbol, ts.Type>();
const bindings = new Map<ts.Symbol, IrType>();
unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings);
// Explicit type arguments name the OVERLOAD's type parameters, which
// need not match the implementation's in order: the resolved overload
// already carries them, so unification binds first, and an explicit
// argument binds only a still-unbound implementation parameter of the
// same name.
const overloadTps = rdecl.typeParameters?.nodes ?? [];
expr.typeArguments?.nodes.forEach((ta, i) => {
const name = overloadTps[i]?.name.text;
const tp = info.typeParams.find((p) => ts.symbolName(p) === name);
if (!tp || bindings.has(tp)) return;
const taT = lowerer.checker.getTypeFromTypeNode(ta);
const mapped = lowerer.mapTypeOf(taT);
if (mapped) {
bindings.set(tp, mapped);
tsBindings.set(tp, taT);
}
});
bindDefaultTypeParams(lowerer, info.typeParams, impl.typeParameters?.nodes, bindings, tsBindings);
// A type parameter the overload never mentions (a handler the overload
// omits) is `unknown`, the checker's own answer for an uninferred one.
for (const tp of info.typeParams) {
if (bindings.has(tp)) continue;
bindings.set(tp, DYN);
tsBindings.set(tp, lowerer.checker.getUnknownType());
}
const previous = lowerer.typeParamBindings;
const previousTs = lowerer.typeParamTsBindings;
lowerer.typeParamBindings = bindings;
lowerer.typeParamTsBindings = tsBindings;
let params: ParamShape[];
let returnType: IrType | null;
try {
params = paramShapes(
lowerer,
impl.parameters.nodes,
declSig,
(_param, i) => expr.arguments.nodes[i] ?? expr,
);
returnType = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(declSig));
} finally {
lowerer.typeParamBindings = previous;
lowerer.typeParamTsBindings = previousTs;
}
if (!returnType) return null;
// keyof-constrained parameters key their instance on the bound literal,
// as on the implementation-signature path.
const runtimeReturn = runtimeReturnType(lowerer, info, returnType);
return internGenericInstance(lowerer, expr, info, params, runtimeReturn, () => bindings, {
...(keyofConstrainedTypeParams(info).size > 0
? { extraKey: keyofLiteralKey(info, tsBindings) }
: {}),
tsBindings,
});
}
/** An instance's result type: a body that can return an indexed read
* that misses returns undefined there, as in JavaScript, whatever the
* declared result says. An async body's promise is always present and
* settles with undefined there. */
function runtimeReturnType(lowerer: Lowerer, info: GenericFnInfo, mapped: IrType): IrType {
if (!lowerer.runtimeOptionalGenericReturns.has(info.decl)) return mapped;
return mapped.kind === "promise"
? { kind: "promise", inner: lowerer.runtimeOptionalType(mapped.inner) }
: lowerer.runtimeOptionalType(mapped);
}
/** A generic factory whose result is one of its own local classes
* (`function base<T>(v: T) { return class { value: T = v }; }`). Each
* instance has its own class, so the instance is keyed on its bindings and
@@ -596,6 +684,25 @@ function keyofConstrainedTypeParams(info: GenericFnInfo): Set<ts.Symbol> {
return out;
}
/** The instance-key suffix of the literals bound to `info`'s
* keyof-constrained type parameters (`@"a"`; `*` for a non-literal
* binding): two literals can map to one IR signature while their bodies
* read different fields. */
function keyofLiteralKey(info: GenericFnInfo, tsBindings: Map<ts.Symbol, ts.Type>): string {
const keyofTps = keyofConstrainedTypeParams(info);
const literals = info.typeParams
.filter((tp) => keyofTps.has(tp))
.map((tp) => {
const bound = tsBindings.get(tp);
return bound?.isStringLiteralType()
? JSON.stringify(bound.value)
: bound?.isNumberLiteralType()
? String(bound.value)
: "*";
});
return `@${literals.join(",")}`;
}
/** Type-parameter symbol → concrete IR type for one instantiation.
* Explicit type arguments bind directly; the rest come from structurally
* matching each DECLARED param/return type (which mentions the type
@@ -736,6 +843,27 @@ function unifySignatureBindings(
// so match it against the whole remaining union. Removing null
// too is safe only when no undeclared null arm remains.
unify(dParts[0]!, lowerer.checker.getNonNullableType(inst), depth + 1);
} else if (dParts.length > 1 && iParts.length === dParts.length) {
// `T | Promise<T>` against `Res | Promise<Res>`: each declared
// part that names a generic target (Promise, an interface, an
// alias) pairs with the one instantiated part of the same target;
// a single remaining bare type parameter takes the single
// remaining instantiated part.
const remaining = [...iParts];
const bare: ts.Type[] = [];
for (const d of dParts) {
if (d.flags & ts.TypeFlags.TypeParameter) {
bare.push(d);
continue;
}
const target = d.getSymbol() ?? d.getAliasSymbol();
if (target === undefined) return;
const matches = remaining.filter((i) => (i.getSymbol() ?? i.getAliasSymbol()) === target);
if (matches.length !== 1) return;
remaining.splice(remaining.indexOf(matches[0]!), 1);
unify(d, matches[0]!, depth + 1);
}
if (bare.length === 1 && remaining.length === 1) unify(bare[0]!, remaining[0]!, depth + 1);
}
return;
}
@@ -3367,6 +3367,8 @@ function lowerDynReceiverMethodCall(
? lowerer.checker.getNonNullableType(rawRecvTs)
: rawRecvTs;
if (lowerer.classImplementedProtocol(recvTs)) {
const viewed = pickViewGenericMethodCall(lowerer, call, access, recvTs);
if (viewed) return viewed;
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (
property &&
@@ -9338,15 +9340,18 @@ function lowerObjLitGenericMethodCall(
lowerer.checker.getCallSignatures(recvT).length > 0
) {
const value = tryLowerExpression(lowerer, access.expression);
// Builtin function values held as checked-dynamic functions
// (String.fromCharCode, JSON.stringify) bind the same way.
if (
value?.type.kind === "func" &&
lowerer.dynConvertible(value.type) &&
value &&
(value.type.kind === "dyn" ||
(value.type.kind === "func" && lowerer.dynConvertible(value.type))) &&
!call.arguments.nodes.some(ts.isSpreadElement)
) {
const loc = locOf(call);
return {
kind: "dynInvoke",
recv: { kind: "dynFrom", value, type: DYN, loc },
recv: value.type.kind === "dyn" ? value : { kind: "dynFrom", value, type: DYN, loc },
method: name,
calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
@@ -10411,6 +10416,53 @@ function maybePromiseHandlerResult(
return stmt;
}
/** `view.m(args)` where `view` is a `Pick<C, K>` / `Omit<C, K>` view of a
* program class C and `m` is one of C's GENERIC methods (its overloads
* included): generic methods have no dispatchable method value, so the
* view's live receiver is extracted as the C instance it holds (a checked
* extraction — a value of another class throws Node-style TypeError
* text naming C) and the call monomorphizes statically against C, the
* same lowering a C-typed receiver gets. Null for other receivers. */
function pickViewGenericMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
recvTs: ts.Type,
): IrExpr | null {
const alias = recvTs.getAliasSymbol();
if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit"))
return null;
if (!alias.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return null;
const source = recvTs.getAliasTypeArguments()[0];
const symbol = source?.getSymbol();
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
if (!info || call.questionDotToken || access.questionDotToken) return null;
const found = findGenericMethodOn(lowerer, info, access.name.text);
if (!found) return null;
const loc = locOf(access.expression);
const lowered = lowerer.lowerExpr(access.expression);
const instance: IrType = { kind: "object", className: info.def.name };
// A view that statically holds an instance of another class (a
// structurally compatible one) calls that class's method in Node, not
// C's: the call is refused rather than extracted as a C instance.
if (
lowered.type.kind === "object" &&
lowered.type.className !== info.def.name &&
!lowerer.isSubclassOf(lowered.type.className, info.def.name)
) {
lowerer.unsupported(
"SC1090",
access,
`calling the generic method '${access.name.text}' through a '${ts.symbolName(alias)}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`,
);
}
const receiver: IrExpr =
lowered.type.kind === "object"
? lowered
: { kind: "dynCheck", value: lowerer.coerceToExpected(lowered, DYN), type: instance, loc };
return lowerClassGenericMethodCall(lowerer, call, access, info, found, receiver);
}
/** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy
* target (`Object.assign(new Error(message), details)`): the sources'
* own members land on the error — the runtime copy over its
@@ -100,7 +100,7 @@ import { isAssertClassNew } from "./assert-instances.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
import { knownBufEncoding } from "./containers/bytes.js";
import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { lowerSearchParamsNew } from "./builtins/url.js";
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
@@ -6770,14 +6770,22 @@ function thunkAsyncSlot(
): void {
if (overridden.sig.async !== true) return;
const declarer = overridden.declarer;
// The slot answers the body's promise: an untyped async method's `dyn`
// result is the promise's payload, so the slot's ABI is that promise. An
// async generator method's slot keeps its generator object.
const ret: IrType =
overridden.sig.gen || overridden.sig.ret.kind === "promise"
? overridden.sig.ret
: { kind: "promise", inner: overridden.sig.ret };
declarer.returnAdapters ??= new Map();
if (!declarer.returnAdapters.has(mName)) {
declarer.returnAdapters.set(mName, {
params: [...overridden.sig.params],
ret: overridden.sig.ret,
ret,
async: true,
});
}
overridden.sig.ret = ret;
delete overridden.sig.async;
}
@@ -9204,6 +9212,11 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
// A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts).
{
const buffer = lowerNodeModulesBufferConstruct(lowerer, expr);
if (buffer) return buffer;
}
// new Function(...): Node's EvalError under
// --disallow-code-generation-from-strings (code-generation.ts).
if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function")
@@ -1,3 +1,5 @@
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { fallbackTainted } from "./checked-fallback.js";
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
@@ -269,6 +271,25 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const chainRecv = lowerer.chainRecvByNode.get(expr);
if (chainRecv) return { ...chainRecv, loc };
// A failed checked value in a fallback tail: assertions check nothing
// (checked-fallback.ts).
if (
lowerer.fallbackTaint !== null &&
(ts.isAsExpression(expr) ||
ts.isTypeAssertion(expr) ||
ts.isNonNullExpression(expr) ||
ts.isSatisfiesExpression(expr)) &&
fallbackTainted(lowerer, expr.expression)
)
return lowerer.lowerExpr(expr.expression);
if (lowerer.fallbackTaint !== null && fallbackTainted(lowerer, expr)) {
if (ts.isObjectLiteralExpression(expr)) return lowerDynObjectLiteral(lowerer, expr);
if (ts.isArrayLiteralExpression(expr) && !expr.elements.nodes.some(ts.isSpreadElement)) {
const elems = expr.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, DYN));
return { kind: "dynArrLit", elems, type: DYN, loc };
}
}
// --external-types supplies CHECKER truth only. A value rooted in one
// of those declarations has no runtime implementation in scriptc, so
// every use fences at its expression instead of accidentally lowering
@@ -2,6 +2,7 @@ import { declareTypedArguments } from "./typed-arguments.js";
import { varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { isRebindableFunctionSymbol } from "./rebindable-functions.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
import {
@@ -59,6 +60,8 @@ function declareCallReceiver(
* same collection lists top-level declarations ride. */
export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean {
const symbol = lowerer.resolveValueSymbol(ident);
// A reassigned JavaScript declaration is read through its module global.
if (isRebindableFunctionSymbol(lowerer, symbol ?? undefined)) return false;
if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true;
const decl = symbol ? symbol.declarations[0] : undefined;
return (
@@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js";
* seeds), npm/JSON import collection, per-file %init functions, %main, and
* the module artifacts (globals, embedded npm tables) the IR module carries. */
import * as ts from "../ts7/adapter.js";
import { collectRebindableFunctions, isRebindableFunction } from "./rebindable-functions.js";
import { bindingSource } from "../binding-source.js";
import type { Lowerer } from "./lowerer.js";
import { dirname as dirnamePath, resolve as resolvePath } from "node:path";
@@ -246,8 +247,12 @@ export function splitFiles(lowerer: Lowerer): FileParts[] {
const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry];
return files.map((sf) => {
const fp: FileParts = { sf, fnDecls: [], classDecls: [], topStmts: [] };
const rebindable = collectRebindableFunctions(lowerer, sf);
for (const stmt of sf.statements.nodes) {
if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt);
// A reassigned JavaScript declaration lives in a module global that
// the init body assigns first (rebindable-functions.ts).
if (ts.isFunctionDeclaration(stmt) && rebindable.has(stmt)) fp.topStmts.push(stmt);
else if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt);
else if (ts.isClassDeclaration(stmt)) fp.classDecls.push(stmt);
// Namespaces: ambient/type-only ones are zero-runtime and skip;
// instantiated bodies FLATTEN into this file's parts (functions/
@@ -1229,6 +1234,36 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
return false;
}
/** A JavaScript default-export object literal holding a class the program
* could not lower (`export default { parse, TomlDate }` where TomlDate
* extends an unsupported base): the literal narrows that member away, its
* diagnostics deferred to the reads that reach it, so the binding's type
* drops it too. Otherwise the module's own initialization would fail the
* width check against members no build can supply. */
function withoutRejectedClassMembers(
lowerer: Lowerer,
expression: ts.Expression,
type: IrType,
): IrType {
let literal = expression;
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
if (!ts.isObjectLiteralExpression(literal) || type.kind !== "record") return type;
const shape = lowerer.shapes.get(type.shapeId);
if (!shape || shape.tuple) return type;
const rejected = (t: IrType): boolean =>
t.kind === "classval" &&
!lowerer.classes.has(t.className) &&
lowerer.deferredClassByName.has(t.className);
if (!shape.fields.some((f) => rejected(f.type))) return type;
const kept = shape.fields.filter((f) => !rejected(f.type));
const shapeId = lowerer.shapes.intern(
kept.map((f) => ({ name: f.name, type: f.type })),
false,
shape.indexValue,
);
return { kind: "record", shapeId };
}
export function collectGlobals(
lowerer: Lowerer,
sf: ts.SourceFile,
@@ -1244,6 +1279,21 @@ export function collectGlobals(
// collected earlier in this pass must resolve them (lower-expando.ts).
collectExpandoMembers(lowerer, sf);
for (const stmt of topStmts) {
if (ts.isFunctionDeclaration(stmt) && stmt.name && isRebindableFunction(lowerer, stmt)) {
const symbol = lowerer.checker.getSymbolAtLocation(stmt.name);
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
const g: IrGlobal = {
id: `%g.${tag}${stmt.name.text}`,
name: stmt.name.text,
type: DYN,
mutable: true,
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsByDeclNode.set(stmt, g);
lowerer.globalsList.push(g);
}
continue;
}
// `export default <expr>`: the module's `default` binding is a const
// module global (registered under the checker's default-export
// symbol — the symbol a default IMPORT's alias chain terminates at,
@@ -1331,6 +1381,7 @@ export function collectGlobals(
type = unitOnlyUnion(lowerer.unions);
}
if (type.kind === "void") lowerer.badType(stmt.expression, tsType);
if (isJsSourceFile(sf)) type = withoutRejectedClassMembers(lowerer, stmt.expression, type);
const g: IrGlobal = { id: `%g.${tag}default`, name: "default", type, mutable: false };
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
@@ -1610,43 +1661,9 @@ export function collectGlobals(
)
)
) {
if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) {
for (const decl of stmt.declarationList.declarations.nodes) {
stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer);
}
}
// `const { codes: { ERR_X } } = require("./errors")`: the outer
// keys stay alias plumbing, but a nested pattern destructures the
// export's value at the require, so its bindings own storage like
// any destructured declaration (lowerVarDeclarator assigns them).
if (ts.isVariableStatement(stmt)) {
const isLetList =
(stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 ||
(stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0;
for (const decl of stmt.declarationList.declarations.nodes) {
for (const element of requireNestedPatternsOf(decl)) {
for (const nameNode of boundIdentifiersOf(element.name!)) {
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue;
const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode));
const type = mapped && mapped.kind !== "void" ? mapped : DYN;
const g: IrGlobal = {
id: `%g.${tag}${nameNode.text}`,
name: nameNode.text,
type,
mutable: isLetList,
source: bindingSource(nameNode),
...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl)
? { tdz: true as const }
: {}),
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
}
}
}
}
if (ts.isVariableStatement(stmt))
for (const decl of stmt.declarationList.declarations.nodes)
registerRequireDeclBindings(lowerer, sf, stmt.declarationList, decl, tag);
continue;
}
// `declare const __VERSION__: string` — AMBIENT: no storage exists
@@ -1680,6 +1697,16 @@ export function collectGlobals(
// probes below would otherwise claim the call-initializer shape
// and put its fences on the build.
if (provenanceElidedConstDecl(lowerer, decl)) continue;
// A require declarator beside other declarators (`var EventEmitter =
// require("events"), R = class extends EventEmitter {}`): the
// statement lowering treats it as the alias plumbing of a whole
// require statement (lowerVarDeclarator), so it owns no storage
// either. A global here would never be assigned, and a read through
// it would see null.
if (requireAliasDecl(lowerer, decl)) {
registerRequireDeclBindings(lowerer, sf, list, decl, tag);
continue;
}
// A stored built-in numeric value iterator has no first-class
// global representation. Its same-%init for-of uses are backed by
// hidden source/cursor/done locals registered when the declaration
@@ -2598,6 +2625,61 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal
lowerer.varGlobalEntryInits.set(sf, inits);
}
/** A JavaScript require declaration the statement lowering treats as alias
* plumbing (lowerVarDeclarator): no storage, the reads resolve through to
* the module. JSON documents and native FFI requires keep their storage. */
function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
return (
decl.initializer !== undefined &&
requireSpecOf(decl.initializer) !== null &&
!isNativeFfiRequire(lowerer, decl.initializer) &&
!isJsonRequireDecl(lowerer, decl) &&
isJsSourceFile(decl.getSourceFile())
);
}
/** The registrations a top-level require declaration does make. `const
* process = require('node:process')` aliases the global process
* (stdlibGlobalAliasDecl's require form) so reads through the binding
* lower via the process surface. In `const { codes: { ERR_X } } =
* require("./errors")` the outer keys stay alias plumbing, but a nested
* pattern destructures the export's value at the require, so its bindings
* own storage like any destructured declaration (lowerVarDeclarator
* assigns them). */
function registerRequireDeclBindings(
lowerer: Lowerer,
sf: ts.SourceFile,
list: ts.VariableDeclarationList,
decl: ts.VariableDeclaration,
tag: string,
): void {
if ((list.flags & ts.NodeFlags.Const) !== 0)
stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer);
const isLetList =
(list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0;
for (const element of requireNestedPatternsOf(decl)) {
for (const nameNode of boundIdentifiersOf(element.name!)) {
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue;
const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode));
const type = mapped && mapped.kind !== "void" ? mapped : DYN;
const g: IrGlobal = {
id: `%g.${tag}${nameNode.text}`,
name: nameNode.text,
type,
mutable: isLetList,
source: bindingSource(nameNode),
...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl)
? { tdz: true as const }
: {}),
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
}
}
}
/** The nested half of module-scope `var` hoisting: a `var` inside a
* top-level BLOCK (an if arm, a loop body, a try) is module-scoped in
* JS — other functions in the file read the same binding — so it must
@@ -2642,6 +2724,14 @@ function collectNestedVarGlobals(
});
for (const list of lists) {
for (const decl of list.declarations.nodes) {
// A nested builtin require (`if (c) { var fs = require("fs"); }`)
// lowers to nothing: its bindings are alias plumbing wherever the
// statement sits (lowerVarDeclarator), so they own no storage.
if (
requireAliasDecl(lowerer, decl) &&
canonicalBuiltinModule(requireSpecOf(decl.initializer!)!) !== null
)
continue;
for (const nameNode of boundIdentifiersOf(decl.name)) {
const diagsBefore = lowerer.diags.length;
try {
@@ -2805,6 +2895,23 @@ export function lowerFileInit(
loc: loc0,
});
}
// Reassigned function declarations are initialized when the module
// starts, as declarations are hoisted: before its dependencies run, so
// a module of an import cycle can call one while it initializes. Their
// statements then lower to nothing (rebindable-functions.ts).
for (const stmt of stmts) {
if (!ts.isFunctionDeclaration(stmt) || !stmt.name || !isRebindableFunction(lowerer, stmt))
continue;
const g = lowerer.globalsByDeclNode.get(stmt);
if (!g) continue;
header.push({
kind: "assign",
localId: g.id,
value: lowerer.coerceToExpected(lowerer.lowerLambda(stmt), DYN),
loc: locOf(stmt),
});
ctx.hoistedFnDecls.add(stmt);
}
for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) {
const npm = lowerer.npmInitActions.get(sf)?.get(stmt);
if (npm !== undefined && npm.length > 0) {
@@ -1,3 +1,10 @@
import {
armFallbackSite,
fallbackSplitsAt,
fallbackTainted,
lowerFallbackTail,
openFallbackSite,
} from "./checked-fallback.js";
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
import { declaredMemberOf } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
@@ -686,7 +693,10 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
// position — instead of failing the whole build). TypeScript keeps
// compile fences. ICEs (SC9001) always stay compile errors.
const sf = stmts.length === 0 ? undefined : ts.getSourceFileOfNode(stmts[0]!);
const deferFences = sf !== undefined && isJsSourceFile(sf);
// The dynamic lowering of a checked-cast fallback tail fences like
// JavaScript: a statement with no `any` lowering throws when a failed
// value reaches it (checked-fallback.ts).
const deferFences = sf !== undefined && (isJsSourceFile(sf) || lowerer.fallbackTaint !== null);
// Register this list as open for the forward-capture machinery: a
// nested function lowering mid-list may pre-declare a LATER const of
// this list as a TDZ box (predeclareForwardCapture), pushing the
@@ -733,6 +743,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
lowered = accounting;
appended = true;
} else {
openFallbackSite(lowerer, stmt, out);
lowered = lowerer.lowerStmt(stmt);
}
// The lib-boundary chokepoint (lib-boundary.ts): checked-dynamic
@@ -747,8 +758,17 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
else body.push(lowered);
const rewritten = lowerSurplusCalls(lowerer, body);
enforceLibBoundary(lowerer, rewritten);
armFallbackSite(lowerer, stmt, rewritten);
if (!appended) out.push(...rewritten);
}
// A checked-cast fallback site's tail starts after this
// statement: the rest of the list lowers statically and
// dynamically, selected by the site's flag.
const splits = fallbackSplitsAt(lowerer, stmt);
if (splits.length > 0) {
out.push(...lowerFallbackTail(lowerer, stmts.slice(i + 1), splits));
break;
}
// A JS statement that lowered CLEAN can still have flushed
// DEFERRED collection diagnostics (a symbol lookup on an alias
// resolves through flushDeferred — informational here, not a
@@ -5917,11 +5937,44 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
writes = indexed;
lowerer.jsBindingWritesByOwner.set(owner, writes);
}
return (writes.get(decl.name.text) ?? []).some(
(node) =>
lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol &&
(lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0,
const candidates = (writes.get(decl.name.text) ?? []).filter(
(node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol,
);
if (
candidates.some(
(node) =>
(lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0,
)
)
return true;
// A plain JavaScript write of a value the declared type cannot hold (the
// checker does not report it): buffer-crc32 replaces its number[] table
// with `new Int32Array(table)`. Only a different representation counts;
// writes that coerce into the declared type keep it.
// A write whose checker type is the declaration's own maps to the same
// representation; the declared type is mapped only for the others.
const declaredType = lowerer.typeOf(decl.name);
const differing = candidates.filter(
(node) =>
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
lowerer.typeOf(node.right) !== declaredType,
);
if (differing.length === 0) return false;
const declared = lowerer.mapTypeOf(declaredType);
if (declared === null || declared.kind === "dyn" || declared.kind === "union") return false;
return differing.some((node) => {
const written = lowerer.mapTypeOf(lowerer.typeOf(node.right));
return (
written !== null &&
written.kind !== "dyn" &&
written.kind !== "union" &&
!isUnitType(written) &&
lowerer.dynConvertible(written) &&
lowerer.dynConvertible(declared) &&
!typeEquals(written, declared) &&
(written.kind !== declared.kind || written.kind === "bytes")
);
});
}
function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
@@ -6891,6 +6944,36 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
);
return { kind: "if", cond, then, else_, loc: locOf(expr) };
}
// A property write to a failed checked value in a fallback tail writes
// the dynamic object (checked-fallback.ts).
if (
lowerer.fallbackTaint !== null &&
ts.isBinaryExpression(expr) &&
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(expr.left) &&
!expr.left.questionDotToken &&
fallbackTainted(lowerer, expr.left.expression)
) {
const loc = locOf(expr);
const recv = lowerer.lowerExpr(expr.left.expression);
if (recv.type.kind === "dyn") {
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.keySet",
args: [
recv,
{ kind: "strLit", value: expr.left.name.text, type: STRING, loc },
lowerer.lowerExprExpecting(expr.right, DYN),
],
type: VOID,
loc,
},
loc,
};
}
}
// Assignment statements over the no-storage binding families:
// - `f1 = f2` where the RHS roots at an ambient-undefined name:
// Node evaluates the RHS first and dies on the root's
@@ -32,8 +32,12 @@ import { scanDivergences } from "./divergences.js";
import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js";
import { checkedClassAssertion } from "./class-assertions.js";
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
import {
isOptionalProcessStreamProperty,
isOptionalProcessVersionsRead,
} from "./builtins/process.js";
import { isSettledIteratorValueRead } from "./lower-generators.js";
import { isRebindableFunctionSymbol } from "./rebindable-functions.js";
import { concreteCollectionNarrow } from "./collection-narrowing.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
@@ -135,6 +139,7 @@ import { earlyReadVarSymbols } from "./var-hoisting.js";
import { recordTextCodecClass } from "../../ir/ir.js";
import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js";
import { promoteJsvals } from "../../ir/jsval.js";
import { fallbackTainted, type FallbackSite, type FallbackTaint } from "./checked-fallback.js";
import {
analyzeTiers,
dynamicStatementCount,
@@ -1848,6 +1853,9 @@ export class Lowerer {
/** Syntactic writes indexed once per JS scope. Symbol and RHS type
* checks remain contextual to each binding probe. */
readonly jsBindingWritesByOwner = new Map<ts.Node, Map<string, ts.BinaryExpression[]>>();
/** JavaScript function declarations their own module reassigns
* (rebindable-functions.ts). */
readonly rebindableFunctions = new Set<ts.FunctionDeclaration>();
/** TRAP bindings: declarations whose initializer provably throws before
* producing a value (its chain roots at an ambient-undefined name —
* ambientUndefVarRootOf). Module init unwinds at the declaration, so no
@@ -2209,6 +2217,10 @@ export class Lowerer {
runtimeOptionalBindingType(node: ts.Node): IrType | null;
runtimeOptionalBindingType(node: ts.Node, fallback: IrType): IrType;
runtimeOptionalBindingType(node: ts.Node, fallback?: IrType): IrType | null {
// A binding holding a failed checked value is dynamic: undefined is one
// of its values already (checked-fallback.ts).
if (this.fallbackTaint !== null && ts.isIdentifier(node) && fallbackTainted(this, node))
return fallback ?? null;
const patternType = this.runtimeOptionalPatternTypes.get(node);
if (patternType) return patternType;
const symbol = ts.isIdentifier(node)
@@ -2608,6 +2620,14 @@ export class Lowerer {
/** True while re-lowering a base function's 2nd+ instance: the same source
* statements were already counted for the first instance. */
suppressStats = false;
/** Checked-cast fallback (checked-fallback.ts): sites opened by their
* root's declaration, by root; armed sites whose tails have not split
* yet; the taint of the dynamic tail being lowered; and the number of
* splits whose tails are being lowered (each lowers its tail twice). */
fallbackSites = new Map<ts.Symbol, FallbackSite>();
fallbackArmed: FallbackSite[] = [];
fallbackTaint: FallbackTaint | null = null;
fallbackSplitDepth = 0;
/** Comparisons interned by operand representations and equality semantics. */
readonly equalityHelpers = new Map<string, string>();
/** Synthetic array-HOF loop functions (map/filter/forEach desugar),
@@ -5085,6 +5105,7 @@ export class Lowerer {
if (isIndexedRead(e)) return true;
if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true;
if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true;
if (isOptionalProcessVersionsRead(this, e)) return true;
if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true;
if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this))
return true;
@@ -7918,10 +7939,14 @@ export class Lowerer {
): IrStmt & { kind: "runtimeFence" } {
const loc = locOf(node);
const code = first?.code ?? fallback?.code ?? "SC1090";
const baseMessage =
const plainMessage =
first?.message ??
fallback?.message ??
"this statement uses a construct with no static lowering";
const baseMessage =
this.fallbackTaint !== null
? `a value that failed its checked cast reached code with no dynamic lowering: ${plainMessage}`
: plainMessage;
const messageLoc = first?.loc ?? loc;
const source = first
? (this.program.getSourceFile(messageLoc.file) ?? ts.getSourceFileOfNode(node))
@@ -8229,6 +8254,9 @@ export class Lowerer {
}
stdlibMemberFence(access: ts.PropertyAccessExpression): void {
// A member of a failed checked value reads dynamically, whatever the
// static member resolves to (checked-fallback.ts).
if (this.fallbackTaint !== null && fallbackTainted(this, access.expression)) return;
return stdlibMemberFence(this, access);
}
@@ -8278,6 +8306,10 @@ export class Lowerer {
}
typeOf(node: ts.Node): ts.Type {
// The dynamic lowering of a checked-cast fallback tail reads values
// derived from the failed value as `any`.
if (this.fallbackTaint !== null && fallbackTainted(this, node))
return this.checker.getAnyType();
if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
const receiver =
node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0
@@ -11683,6 +11715,9 @@ export class Lowerer {
isSelfReference(ident: ts.Identifier): boolean {
const ctx = this.ctx;
if (!ctx.selfSymbol) return false;
// A reassigned JavaScript declaration names its module global, which
// may hold another function by the time the body runs.
if (isRebindableFunctionSymbol(this, ctx.selfSymbol)) return false;
// The self binding can be shadowed by a scoped local of the same symbol?
// No — a shadow is a different symbol; symbol identity is exact.
return this.checker.getSymbolAtLocation(ident) === ctx.selfSymbol;
@@ -11956,6 +11991,7 @@ export class Lowerer {
}
npmMemberFence(access: ts.PropertyAccessExpression): void {
if (this.fallbackTaint !== null && fallbackTainted(this, access.expression)) return;
return npmMemberFence(this, access);
}
@@ -12945,6 +12981,8 @@ export class Lowerer {
loc,
};
}
if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native"))
return fsRealpathValue(this, bi.member === "realpathSync.native", loc);
const fn = builtinModuleFnOf(this, bi.module, bi.member);
return fn ? this.lowerNativeCallableValue(fn, `${bi.module}.${bi.member}`, loc) : null;
}
@@ -13657,3 +13695,51 @@ function droppedMembers(lowerer: Lowerer, from: IrType, to: IrType, seen: Set<st
}
return out;
}
/** fs.realpathSync and its .native as function values (fs.realpath stores
* `var origRealpathSync = fs.realpathSync`): path and options pass through
* unchecked to the runtime, which applies Node's argument handling. */
function fsRealpathValue(lowerer: Lowerer, native: boolean, loc: SrcLoc): IrExpr {
const name = native ? "%builtin.fs.realpathSync.native" : "%builtin.fs.realpathSync";
if (!lowerer.builtinCallableValueFns.has(name)) {
lowerer.builtinCallableValueFns.set(name, name);
const params = ["path", "options"].map((id) => ({ localId: id, name: id, type: DYN }));
lowerer.liftedFns.push({
name,
params,
returnType: DYN,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
loc,
body: [
{
kind: "return",
value: {
kind: "libCall",
fn: "fs.realpathValue",
args: [
{ kind: "varRef", localId: "path", type: DYN, loc },
{ kind: "varRef", localId: "options", type: DYN, loc },
{ kind: "boolLit", value: native, type: BOOL, loc },
],
type: DYN,
loc,
},
loc,
},
],
});
}
return {
kind: "dynFrom",
value: {
kind: "closure",
fnName: name,
captures: [],
type: { kind: "func", params: [DYN, DYN], ret: DYN },
loc,
},
fnName: native ? "native" : "realpathSync",
type: DYN,
loc,
};
}
@@ -0,0 +1,64 @@
/* JavaScript function declarations that their own module reassigns.
*
* Capability probes in published packages replace a function declaration
* with the implementation that works on the first call (yazl's
* `function bufferFrom(...) { bufferFrom = modern; ... }`). A top-level
* declaration normally lowers to a static function, which cannot change.
* A reassigned one instead becomes a mutable module global holding the
* function as a checked-dynamic value: the module initializer assigns it
* before its first statement (the declaration's hoisting), and reads,
* calls and writes of the name all go through the global. TypeScript
* rejects such writes, so only JavaScript sources take this path. */
import * as ts from "../ts7/adapter.js";
import { isJsSourceFile } from "../program.js";
import type { Lowerer } from "./lowerer.js";
/** Top-level function declarations of `sf` that an assignment anywhere in
* the file writes, recorded for the queries below. */
export function collectRebindableFunctions(
lowerer: Lowerer,
sf: ts.SourceFile,
): Set<ts.FunctionDeclaration> {
const found = new Set<ts.FunctionDeclaration>();
if (!isJsSourceFile(sf)) return found;
const byName = new Map<string, ts.FunctionDeclaration>();
for (const stmt of sf.statements.nodes) {
if (
ts.isFunctionDeclaration(stmt) &&
stmt.name &&
stmt.body &&
stmt.typeParameters === undefined
)
byName.set(stmt.name.text, stmt);
}
if (byName.size === 0) return found;
ts.walkPreorder(sf, (node) => {
if (
!ts.isBinaryExpression(node) ||
node.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
!ts.isIdentifier(node.left)
)
return undefined;
const decl = byName.get(node.left.text);
if (!decl || found.has(decl)) return undefined;
const symbol = lowerer.checker.getSymbolAtLocation(node.left);
if (symbol && symbol.declarations.includes(decl)) found.add(decl);
return undefined;
});
for (const decl of found) lowerer.rebindableFunctions.add(decl);
return found;
}
export function isRebindableFunction(lowerer: Lowerer, decl: ts.Node | undefined): boolean {
return (
decl !== undefined && ts.isFunctionDeclaration(decl) && lowerer.rebindableFunctions.has(decl)
);
}
/** Whether the symbol names a reassigned function declaration. */
export function isRebindableFunctionSymbol(
lowerer: Lowerer,
symbol: ts.Symbol | undefined,
): boolean {
return symbol !== undefined && symbol.declarations.some((d) => isRebindableFunction(lowerer, d));
}
@@ -2068,6 +2068,9 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
readdirSync: ["fs.readdirTypesSync"],
// The { throwIfNoEntry } probe form.
statSync: ["fs.statProbeSync"],
// realpathSync and realpathSync.native read as function values.
realpathSync: ["fs.realpathValue"],
"realpathSync.native": ["fs.realpathValue"],
lstatSync: ["fs.lstatProbeSync"],
globSync: ["fs.globTypesSync"],
// The JS-source validation-ladder variant (lowerFsLadderCall): the
+181 -13
View File
@@ -2138,7 +2138,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(ts.isInterfaceDeclaration(d) ||
ts.isTypeAliasDeclaration(d) ||
ts.isTypeLiteralNode(d) ||
ts.isMappedTypeNode(d)),
ts.isMappedTypeNode(d) ||
isDeclaredDataClass(d) ||
(decls.some(isDeclaredDataClass) && isTypeOnlyNamespace(d))),
);
const aliasDecls = packageOnly(widened.getAliasSymbol());
const ownDecls = packageOnly(widened.getSymbol());
@@ -6520,6 +6522,48 @@ function isMappedShape(t: ts.Type): boolean {
* of a package the program does not compile. Its pure type shapes —
* interfaces, aliases, type literals — describe data a program can build
* itself (`import type` from a types-only package such as type-fest). */
/** A class an SDK declares only to describe data (aws-sdk's
* `declare class PresignedPost { url: string; fields: Fields }`, which
* arrives as JSON and is never constructed): public instance fields and
* nothing else — no constructor, methods, accessors, index signatures,
* static, private or protected members, base classes, implemented
* interfaces or type parameters. Its instances are exactly the records
* of those fields, so the type maps structurally like an interface. */
function isDeclaredDataClass(d: ts.Node): boolean {
if (!ts.isClassDeclaration(d) || !d.getSourceFile().isDeclarationFile) return false;
if (d.heritageClauses !== undefined && d.heritageClauses.nodes.length > 0) return false;
if (d.typeParameters !== undefined && d.typeParameters.nodes.length > 0) return false;
return d.members.nodes.every(
(member) =>
ts.isPropertyDeclaration(member) &&
!ts.isPrivateIdentifier(member.name) &&
!(member.modifiers?.nodes ?? []).some(
(modifier) =>
modifier.kind === ts.SyntaxKind.StaticKeyword ||
modifier.kind === ts.SyntaxKind.PrivateKeyword ||
modifier.kind === ts.SyntaxKind.ProtectedKeyword ||
modifier.kind === ts.SyntaxKind.AbstractKeyword,
),
);
}
/** A namespace merged into a declared data class that holds only types
* (`namespace PresignedPost { interface Fields { … } }`): it adds no value
* the program could reach. */
function isTypeOnlyNamespace(d: ts.Node): boolean {
if (!ts.isModuleDeclaration(d)) return false;
const body = d.body;
if (body === undefined) return true;
if (ts.isModuleDeclaration(body)) return isTypeOnlyNamespace(body);
if (!ts.isModuleBlock(body)) return false;
return body.statements.nodes.every(
(statement) =>
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
isTypeOnlyNamespace(statement),
);
}
function packageTypeFile(sf: ts.SourceFile, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
return (
sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf)
@@ -7287,10 +7331,11 @@ function mapRecordTypeInner(
let v: IrType | null = null;
for (const info of indexInfos) {
if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number)) return null;
const iv = mapType(info.type, ctx);
// JSONValue>` (a package's own JSON alias) and `Record<string, any>`
// An `any`-valued signature (`{ [key: string]: any }`, an SDK's
// `Dictionary<any>`) holds whatever the program stores: its
// overflow values are checked-dynamic, exactly `Record<string,
// unknown>`'s.
const iv = info.type.flags & ts.TypeFlags.Any ? DYN : mapType(info.type, ctx);
if (!iv || !isSupportedIndexValue(iv)) return null;
if (v !== null && !typeEquals(v, iv)) return null;
v = iv;
@@ -7517,6 +7562,22 @@ function mapRecordTypeInner(
pt = arrayOf(unitOnlyUnion(ctx.unions));
}
if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN;
// An `any[]` field (`list: any[]` in an SDK's message type, also
// optional or nullable) is the checked-dynamic array, exactly as
// `unknown[]` maps; the checked-dynamic value holds the units too.
// Declared members only: an object literal's inferred `any[]` member
// keeps its existing story.
if (
pt === null &&
declaredMember(p) &&
(fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every(
(part) =>
isAnyArrayType(part, checker) ||
(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0,
) &&
!isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)
)
pt = DYN;
// Unit-only FIELDS (`{ msg?: undefined }` — the discriminated-union
// absent-field idiom — and `{ p: undefined }` spellings): the
// unit-only union. The runtime value is the interned unit, JSON
@@ -7613,6 +7674,10 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
return true;
case "record":
case "array":
// `T | Promise<T>` with T bound to `unknown`: the checked-dynamic value
// holds a promise by reference, so `unknown` absorbs the promise arm
// exactly as the checker's own union reduction does.
case "promise":
return canConvertToDyn(
arm,
(id) => ctx.shapes.get(id),
@@ -7907,15 +7972,118 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx
if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue;
return `the record shape is supported, but its member '${ts.symbolName(p)}' ${describeOverloadSetBlocker(fieldTs, ctx)}`;
}
let pt = isGenericCallableMemberType(fieldTs, checker)
? erasedGenericMemberType(fieldTs, ctx)
: mapType(fieldTs, ctx);
if (pt === undefined) continue;
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam))
pt = unitOnlyUnion(ctx.unions);
if (!pt || pt.kind === "void") {
return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`;
if (memberMaps(fieldTs, ctx, p)) continue;
// Name the deepest member the failure comes from (`order.deployment.
// builds[].config`): a large message type otherwise points only at
// its outermost member.
const inner = innerBlockedMember(fieldTs, ctx, new Set());
if (inner !== null && inner.path !== "") {
return `the record shape is supported, but its member '${ts.symbolName(p)}${inner.path}' has type '${checker.typeToString(inner.type)}', which does not compile`;
}
return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`;
}
return null;
}
/** A member an interface, type literal or class declares (not one an
* object literal's inferred type carries). */
function declaredMember(member: ts.Symbol): boolean {
return (
member.declarations.length > 0 &&
member.declarations.every((d) => ts.isPropertySignature(d) || ts.isPropertyDeclaration(d))
);
}
/** Whether a record member of type `fieldTs` maps, by the member rules of
* mapRecordTypeInner (generic members erase; `any` and `any[]` members
* are checked-dynamic; unit-only members are the unit union). Overloaded
* members answer true: describeOverloadSetBlocker tells their story. */
function memberMaps(fieldTs: ts.Type, ctx: TypeMapperCtx, member?: ts.Symbol): boolean {
const { checker } = ctx;
if (isOverloadedCallableMemberType(fieldTs, checker)) return true;
const pt = isGenericCallableMemberType(fieldTs, checker)
? erasedGenericMemberType(fieldTs, ctx)
: mapType(fieldTs, ctx);
if (pt === undefined) return true;
if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) return true;
if (
pt === null &&
(member === undefined || declaredMember(member)) &&
(fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every(
(part) =>
isAnyArrayType(part, checker) ||
(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0,
) &&
!isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)
)
return true;
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) return true;
return pt !== null && pt.kind !== "void";
}
/** The member path inside the unmapped type `t` that its failure comes
* from, and that member's type: `.name` steps into record members, `[]`
* into array elements, `()` into a function's return; union arms and
* promise payloads are transparent. The empty path means `t` itself is
* the blocker; null means `t` maps (or is `any`). */
function innerBlockedMember(
t: ts.Type,
ctx: TypeMapperCtx,
seen: Set<ts.Type>,
): { path: string; type: ts.Type } | null {
const { checker } = ctx;
// `any` is no component to name: the member holding it is the blocker.
if ((t.flags & ts.TypeFlags.Any) !== 0 || mapType(t, ctx) !== null) return null;
const here = { path: "", type: t };
if (seen.has(t) || seen.size >= 16) return here;
seen.add(t);
const step = (prefix: string, inner: ts.Type): { path: string; type: ts.Type } => {
const found = innerBlockedMember(inner, ctx, seen);
return found === null ? here : { path: prefix + found.path, type: found.type };
};
if (t.isUnionType()) {
const UNIT = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null;
for (const part of ts.constituentTypes(t)) {
if ((part.flags & UNIT) !== 0 || mapType(part, ctx) !== null) continue;
return step("", part);
}
return here;
}
if ((t.flags & ts.TypeFlags.Object) === 0) return here;
if (checker.isArrayType(t)) {
const elem = checker.getTypeArguments(t as ts.TypeReference)[0];
return elem === undefined ? here : step("[]", elem);
}
const symbol = t.getSymbol();
if (
symbol !== undefined &&
ts.symbolName(symbol) === "Promise" &&
symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
) {
const payload = checker.getTypeArguments(t as ts.TypeReference)[0];
return payload === undefined ? here : step("", payload);
}
const calls = checker.getCallSignatures(t);
if (calls.length === 1) {
const sig = calls[0]!;
if (sig.getTypeParameters().length > 0) return here;
if (sig.getParameters().some((p) => mapType(checker.getTypeOfSymbol(p), ctx) === null))
return here;
return step("()", checker.getReturnTypeOfSignature(sig));
}
if (
calls.length > 0 ||
checker.getConstructSignatures(t).length > 0 ||
checker.isTupleType(t) ||
checker.getIndexInfosOfType(t).length > 0
)
return here;
for (const p of checker.getPropertiesOfType(t)) {
if ((p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0) continue;
if (ts.symbolName(p).startsWith("__@")) continue;
const fieldTs = checker.getTypeOfSymbol(p);
if (memberMaps(fieldTs, ctx, p)) continue;
return step(`.${ts.symbolName(p)}`, fieldTs);
}
return here;
}
+2
View File
@@ -547,6 +547,8 @@ async function compileExecutableNative(
...(features.dynTier ? { dynTier: true } : {}),
...(features.streamDyn ? { streamDyn: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
...(features.dynBufferNum ? { dynBufferNum: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
events: features.events,
emitter: features.emitter,
symbol: features.symbol,

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