diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md
index f8552b003..92eb099db 100644
--- a/benchmarks/runtime/README.md
+++ b/benchmarks/runtime/README.md
@@ -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.
diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json
deleted file mode 100644
index 9a3f1bc2f..000000000
--- a/benchmarks/runtime/workloads.json
+++ /dev/null
@@ -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"
- ]
- }
- }
- ]
-}
diff --git a/benchmarks/runtime/workloads.mjs b/benchmarks/runtime/workloads.mjs
new file mode 100644
index 000000000..e63ee98eb
--- /dev/null
+++ b/benchmarks/runtime/workloads.mjs
@@ -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),
+ );
+}
diff --git a/benchmarks/runtime/workloads/alloc-trees.json b/benchmarks/runtime/workloads/alloc-trees.json
new file mode 100644
index 000000000..d4ae2ae72
--- /dev/null
+++ b/benchmarks/runtime/workloads/alloc-trees.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/alloc-trees.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/ast-interp.json b/benchmarks/runtime/workloads/ast-interp.json
new file mode 100644
index 000000000..1817541ac
--- /dev/null
+++ b/benchmarks/runtime/workloads/ast-interp.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/ast-interp.ts",
+ "args": [
+ "5"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/async-pipeline.json b/benchmarks/runtime/workloads/async-pipeline.json
new file mode 100644
index 000000000..610f62793
--- /dev/null
+++ b/benchmarks/runtime/workloads/async-pipeline.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/async-pipeline.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/atomics-contended.json b/benchmarks/runtime/workloads/atomics-contended.json
new file mode 100644
index 000000000..7f78d265b
--- /dev/null
+++ b/benchmarks/runtime/workloads/atomics-contended.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/atomics-contended.ts",
+ "args": [
+ "2"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/channel-messages.json b/benchmarks/runtime/workloads/channel-messages.json
new file mode 100644
index 000000000..56b84dda4
--- /dev/null
+++ b/benchmarks/runtime/workloads/channel-messages.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/channel-messages.ts",
+ "args": [
+ "200000"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/cli-config.json b/benchmarks/runtime/workloads/cli-config.json
new file mode 100644
index 000000000..db7a4c645
--- /dev/null
+++ b/benchmarks/runtime/workloads/cli-config.json
@@ -0,0 +1,12 @@
+{
+ "entry": "workloads/cli-config.ts",
+ "input": "cli-config",
+ "launches": 20,
+ "args": [
+ "{input}",
+ "--env",
+ "production",
+ "--max-wave",
+ "12"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/csv-numbers.json b/benchmarks/runtime/workloads/csv-numbers.json
new file mode 100644
index 000000000..acb3c21d7
--- /dev/null
+++ b/benchmarks/runtime/workloads/csv-numbers.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/csv-numbers.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/cyclic-graphs.json b/benchmarks/runtime/workloads/cyclic-graphs.json
new file mode 100644
index 000000000..2e62f7fdf
--- /dev/null
+++ b/benchmarks/runtime/workloads/cyclic-graphs.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/cyclic-graphs.ts",
+ "args": [
+ "3"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/functional-pipeline.json b/benchmarks/runtime/workloads/functional-pipeline.json
new file mode 100644
index 000000000..5ee3af902
--- /dev/null
+++ b/benchmarks/runtime/workloads/functional-pipeline.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/functional-pipeline.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/http-api.json b/benchmarks/runtime/workloads/http-api.json
new file mode 100644
index 000000000..145166945
--- /dev/null
+++ b/benchmarks/runtime/workloads/http-api.json
@@ -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"
+ ]
+ }
+}
diff --git a/benchmarks/runtime/workloads/intern-table-1.json b/benchmarks/runtime/workloads/intern-table-1.json
new file mode 100644
index 000000000..8a2b253f6
--- /dev/null
+++ b/benchmarks/runtime/workloads/intern-table-1.json
@@ -0,0 +1,8 @@
+{
+ "entry": "workloads/intern-table.ts",
+ "args": [
+ "table",
+ "1",
+ "100000"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/intern-table-map-1.json b/benchmarks/runtime/workloads/intern-table-map-1.json
new file mode 100644
index 000000000..fefafac67
--- /dev/null
+++ b/benchmarks/runtime/workloads/intern-table-map-1.json
@@ -0,0 +1,8 @@
+{
+ "entry": "workloads/intern-table.ts",
+ "args": [
+ "map",
+ "1",
+ "100000"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/intern-table-map.json b/benchmarks/runtime/workloads/intern-table-map.json
new file mode 100644
index 000000000..136c6c31e
--- /dev/null
+++ b/benchmarks/runtime/workloads/intern-table-map.json
@@ -0,0 +1,8 @@
+{
+ "entry": "workloads/intern-table.ts",
+ "args": [
+ "map",
+ "8",
+ "100000"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/intern-table.json b/benchmarks/runtime/workloads/intern-table.json
new file mode 100644
index 000000000..86fa11413
--- /dev/null
+++ b/benchmarks/runtime/workloads/intern-table.json
@@ -0,0 +1,8 @@
+{
+ "entry": "workloads/intern-table.ts",
+ "args": [
+ "table",
+ "8",
+ "100000"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/inventory-report.json b/benchmarks/runtime/workloads/inventory-report.json
new file mode 100644
index 000000000..e75f5e3e5
--- /dev/null
+++ b/benchmarks/runtime/workloads/inventory-report.json
@@ -0,0 +1,7 @@
+{
+ "entry": "../builds/inventory-report/main.ts",
+ "input": "inventory-report",
+ "args": [
+ "{input}"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/json-records.json b/benchmarks/runtime/workloads/json-records.json
new file mode 100644
index 000000000..3263ac068
--- /dev/null
+++ b/benchmarks/runtime/workloads/json-records.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/json-records.ts",
+ "args": [
+ "4"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/log-lines.json b/benchmarks/runtime/workloads/log-lines.json
new file mode 100644
index 000000000..635bcc49f
--- /dev/null
+++ b/benchmarks/runtime/workloads/log-lines.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/log-lines.ts",
+ "args": [
+ "4"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/log-summary.json b/benchmarks/runtime/workloads/log-summary.json
new file mode 100644
index 000000000..274a1cff6
--- /dev/null
+++ b/benchmarks/runtime/workloads/log-summary.json
@@ -0,0 +1,11 @@
+{
+ "entry": "../builds/log-summary/main.ts",
+ "input": "log-summary",
+ "args": [
+ "{input}",
+ "--min-status",
+ "200",
+ "--limit",
+ "12"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/number-maps.json b/benchmarks/runtime/workloads/number-maps.json
new file mode 100644
index 000000000..2adfbcc47
--- /dev/null
+++ b/benchmarks/runtime/workloads/number-maps.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/number-maps.ts",
+ "args": [
+ "4"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/numeric-kernels.json b/benchmarks/runtime/workloads/numeric-kernels.json
new file mode 100644
index 000000000..04718346b
--- /dev/null
+++ b/benchmarks/runtime/workloads/numeric-kernels.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/numeric-kernels.ts",
+ "args": [
+ "0.5"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/optional-fields.json b/benchmarks/runtime/workloads/optional-fields.json
new file mode 100644
index 000000000..c54d87437
--- /dev/null
+++ b/benchmarks/runtime/workloads/optional-fields.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/optional-fields.ts",
+ "args": [
+ "10"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/published-graphs.json b/benchmarks/runtime/workloads/published-graphs.json
new file mode 100644
index 000000000..b22f5a0db
--- /dev/null
+++ b/benchmarks/runtime/workloads/published-graphs.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/published-graphs.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/record-boundaries.json b/benchmarks/runtime/workloads/record-boundaries.json
new file mode 100644
index 000000000..a35c346eb
--- /dev/null
+++ b/benchmarks/runtime/workloads/record-boundaries.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/record-boundaries.ts",
+ "args": [
+ "8"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/records-sort.json b/benchmarks/runtime/workloads/records-sort.json
new file mode 100644
index 000000000..fd0097fe2
--- /dev/null
+++ b/benchmarks/runtime/workloads/records-sort.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/records-sort.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/regex-logs.json b/benchmarks/runtime/workloads/regex-logs.json
new file mode 100644
index 000000000..636db124e
--- /dev/null
+++ b/benchmarks/runtime/workloads/regex-logs.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/regex-logs.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/region-builds.json b/benchmarks/runtime/workloads/region-builds.json
new file mode 100644
index 000000000..dc53dbc36
--- /dev/null
+++ b/benchmarks/runtime/workloads/region-builds.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/region-builds.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/template-render.json b/benchmarks/runtime/workloads/template-render.json
new file mode 100644
index 000000000..9897494f6
--- /dev/null
+++ b/benchmarks/runtime/workloads/template-render.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/template-render.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/thread-slots-cache.json b/benchmarks/runtime/workloads/thread-slots-cache.json
new file mode 100644
index 000000000..0eb6e11ce
--- /dev/null
+++ b/benchmarks/runtime/workloads/thread-slots-cache.json
@@ -0,0 +1,9 @@
+{
+ "entry": "workloads/thread-slots.ts",
+ "args": [
+ "cache",
+ "8",
+ "50000",
+ "20"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/thread-slots.json b/benchmarks/runtime/workloads/thread-slots.json
new file mode 100644
index 000000000..b8ed5b140
--- /dev/null
+++ b/benchmarks/runtime/workloads/thread-slots.json
@@ -0,0 +1,9 @@
+{
+ "entry": "workloads/thread-slots.ts",
+ "args": [
+ "slots",
+ "8",
+ "50000",
+ "20"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/typed-array-scan-workers.json b/benchmarks/runtime/workloads/typed-array-scan-workers.json
new file mode 100644
index 000000000..c01893a2c
--- /dev/null
+++ b/benchmarks/runtime/workloads/typed-array-scan-workers.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/typed-array-scan-workers.ts",
+ "args": [
+ "3"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/typed-array-scan.json b/benchmarks/runtime/workloads/typed-array-scan.json
new file mode 100644
index 000000000..0b2ba0592
--- /dev/null
+++ b/benchmarks/runtime/workloads/typed-array-scan.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/typed-array-scan.ts",
+ "args": [
+ "3"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/unicode-lexer.json b/benchmarks/runtime/workloads/unicode-lexer.json
new file mode 100644
index 000000000..91ccab087
--- /dev/null
+++ b/benchmarks/runtime/workloads/unicode-lexer.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/unicode-lexer.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/unicode-lexer.ts b/benchmarks/runtime/workloads/unicode-lexer.ts
new file mode 100644
index 000000000..21638bb86
--- /dev/null
+++ b/benchmarks/runtime/workloads/unicode-lexer.ts
@@ -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));
diff --git a/benchmarks/runtime/workloads/validate-errors.json b/benchmarks/runtime/workloads/validate-errors.json
new file mode 100644
index 000000000..b8d457897
--- /dev/null
+++ b/benchmarks/runtime/workloads/validate-errors.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/validate-errors.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/value-objects.json b/benchmarks/runtime/workloads/value-objects.json
new file mode 100644
index 000000000..276fa4ab8
--- /dev/null
+++ b/benchmarks/runtime/workloads/value-objects.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/value-objects.ts",
+ "args": [
+ "2"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/value-records.json b/benchmarks/runtime/workloads/value-records.json
new file mode 100644
index 000000000..84c0e12dd
--- /dev/null
+++ b/benchmarks/runtime/workloads/value-records.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/value-records.ts",
+ "args": [
+ "1"
+ ]
+}
diff --git a/benchmarks/runtime/workloads/word-graph.json b/benchmarks/runtime/workloads/word-graph.json
new file mode 100644
index 000000000..95ffb27e5
--- /dev/null
+++ b/benchmarks/runtime/workloads/word-graph.json
@@ -0,0 +1,6 @@
+{
+ "entry": "workloads/word-graph.ts",
+ "args": [
+ "2"
+ ]
+}
diff --git a/benchmarks/scaling/strace-workloads.mjs b/benchmarks/scaling/strace-workloads.mjs
index b4db5af19..f0644cf13 100644
--- a/benchmarks/scaling/strace-workloads.mjs
+++ b/benchmarks/scaling/strace-workloads.mjs
@@ -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) {
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index 786cf2c63..df8274258 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
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 SC1010 blockers. Relative paths resolve from the current working directory. Accepted files end in .d.ts, .d.mts, or .d.cts.
--tier <auto|static-only>
- The tier policy for code that has no static lowering. auto is the default. static-only 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 Dynamic tier.
+ The tier policy for code that has no static lowering. auto is the default. static-only 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 Dynamic tier.
--explain-dynamic[=<name>]
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 x, Point, or Point.x, show only the matching units. See Dynamic tier.
diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx
index 1bb645bd9..b1f6e2c64 100644
--- a/docs/content/docs/coverage.mdx
+++ b/docs/content/docs/coverage.mdx
@@ -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
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
index de6c5bd24..f771ec78c 100644
--- a/docs/content/docs/index.mdx
+++ b/docs/content/docs/index.mdx
@@ -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
diff --git a/docs/content/limitations/runtime-type-checks.mdx b/docs/content/limitations/runtime-type-checks.mdx
index f6ead482b..bf325b713 100644
--- a/docs/content/limitations/runtime-type-checks.mdx
+++ b/docs/content/limitations/runtime-type-checks.mdx
@@ -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.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
deleted file mode 100644
index 167256bca..000000000
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "schemaVersion": 3,
- "nodeVersion": "24.22.0",
- "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "ee72820d45a79aa154f0",
- "rowCount": 3880
-}
diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs
index 8c7f136cf..d2b333bb2 100644
--- a/internal/compatibility/src/generate.mjs
+++ b/internal/compatibility/src/generate.mjs
@@ -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(
diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts
index 0fcf86ada..a8b30d646 100644
--- a/packages/compiler/src/backend/executable-features.ts
+++ b/packages/compiler/src/backend/executable-features.ts
@@ -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,
diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts
index be1c58941..8a2a4659a 100644
--- a/packages/compiler/src/backend/llvm/classes.test.ts
+++ b/packages/compiler/src/backend/llvm/classes.test.ts
@@ -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 });
diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts
index f5a882bbf..5ad29430d 100644
--- a/packages/compiler/src/backend/llvm/classes.ts
+++ b/packages/compiler/src/backend/llvm/classes.ts
@@ -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,
+ sizeType: "i32" | "i64" = "i64",
): Map {
const metaMap = new Map();
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();
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
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index 88bad51a0..19c1a17ac 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -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;
diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts
index 3ea0f4c84..6a3df30cd 100644
--- a/packages/compiler/src/backend/llvm/emit-module.ts
+++ b/packages/compiler/src/backend/llvm/emit-module.ts
@@ -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), ``);
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index f376dff5e..8a4b16fc5 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -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}`,
);
diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts
index 6da63977d..901a8749e 100644
--- a/packages/compiler/src/backend/llvm/expr-async.ts
+++ b/packages/compiler/src/backend/llvm/expr-async.ts
@@ -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;
diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts
index b49c8895e..43dfce459 100644
--- a/packages/compiler/src/backend/llvm/expr-calls.ts
+++ b/packages/compiler/src/backend/llvm/expr-calls.ts
@@ -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}`,
);
diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts
index bc4f1128a..da3e1bfa4 100644
--- a/packages/compiler/src/backend/llvm/expr-dynamic.ts
+++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts
@@ -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);
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
index d6235306d..41ecd189e 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
@@ -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");
+});
diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
index 02fd53fc1..6b51f9afa 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
@@ -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(),
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index 924027ee2..283c5b38d 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -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":
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 8cd40240f..072682edb 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -374,6 +374,7 @@ export const LIB_FN_SYMS: Record = {
"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 = {
"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 = {
"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 = {
"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).
diff --git a/packages/compiler/src/backend/llvm/narrow-guards.ts b/packages/compiler/src/backend/llvm/narrow-guards.ts
index 910af6f79..4744bed7c 100644
--- a/packages/compiler/src/backend/llvm/narrow-guards.ts
+++ b/packages/compiler/src/backend/llvm/narrow-guards.ts
@@ -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;
diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts
index 4f807424b..15221bbc3 100644
--- a/packages/compiler/src/backend/llvm/publish.ts
+++ b/packages/compiler/src/backend/llvm/publish.ts
@@ -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);
diff --git a/packages/compiler/src/backend/llvm/regions.test.ts b/packages/compiler/src/backend/llvm/regions.test.ts
index 51ceb21c8..f2fed6b8e 100644
--- a/packages/compiler/src/backend/llvm/regions.test.ts
+++ b/packages/compiler/src/backend/llvm/regions.test.ts
@@ -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).
diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts
index 1ec46a5b1..e7ad39f7c 100644
--- a/packages/compiler/src/backend/llvm/regions.ts
+++ b/packages/compiler/src/backend/llvm/regions.ts
@@ -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)}`,
diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts
index 9cceaa443..40803a5ff 100644
--- a/packages/compiler/src/backend/llvm/shapes.ts
+++ b/packages/compiler/src/backend/llvm/shapes.ts
@@ -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)` : ""}`,
diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts
index ef0702361..d7bbed272 100644
--- a/packages/compiler/src/backend/llvm/string-reads.ts
+++ b/packages/compiler/src/backend/llvm/string-reads.ts
@@ -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
diff --git a/packages/compiler/src/backend/llvm/thread-slots.test.ts b/packages/compiler/src/backend/llvm/thread-slots.test.ts
index dba4453b3..b8d8dccf9 100644
--- a/packages/compiler/src/backend/llvm/thread-slots.test.ts
+++ b/packages/compiler/src/backend/llvm/thread-slots.test.ts
@@ -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(
diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts
index 43c9cea71..da6cbe0f5 100644
--- a/packages/compiler/src/backend/mangle.ts
+++ b/packages/compiler/src/backend/mangle.ts
@@ -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
diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts
index 84f448e93..c139b4a65 100644
--- a/packages/compiler/src/backend/native-link-info-core.ts
+++ b/packages/compiler/src/backend/native-link-info-core.ts
@@ -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;
diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts
index a9c93d3df..7f8456484 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -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
diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts
index 949f03839..a1b018aef 100644
--- a/packages/compiler/src/backend/native/executable.ts
+++ b/packages/compiler/src/backend/native/executable.ts
@@ -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"),
diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts
index 5c9d417c5..69aade6ce 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -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,
diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts
index 26b439167..69d30b76e 100644
--- a/packages/compiler/src/cli/usage.ts
+++ b/packages/compiler/src/cli/usage.ts
@@ -73,8 +73,9 @@ Options:
--tier
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[=]
coverage only: list the functions that operate on dynamic
values and the bindings, fields, functions, classes and
diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts
index 064b1f867..185ba3ff4 100644
--- a/packages/compiler/src/executable/executable-cache.ts
+++ b/packages/compiler/src/executable/executable-cache.ts
@@ -68,6 +68,8 @@ export interface EarlyExecutableNativeFeatures {
dynTier?: boolean;
streamDyn?: boolean;
eventsOnceSignal?: boolean;
+ dynBufferNum?: boolean;
+ builtinValues?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts
index 54bcae4b5..7eb8aa9b2 100644
--- a/packages/compiler/src/frontend/lowering/builtins/json.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/json.ts
@@ -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)`);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts
index 78dc6ef14..2db62d675 100644
--- a/packages/compiler/src/frontend/lowering/builtins/process.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/process.ts
@@ -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 = 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();
+
+/** 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,
diff --git a/packages/compiler/src/frontend/lowering/checked-fallback.ts b/packages/compiler/src/frontend/lowering/checked-fallback.ts
new file mode 100644
index 000000000..b1d746c1d
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/checked-fallback.ts
@@ -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;
+ 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;
+ memo: Map;
+}
+
+/** 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` / `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();
+ 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): 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) else ` 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(lowerer: Lowerer, lower: () => T, frame = new Map()): 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();
+ 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();
+}
diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
index de84e3fed..cb5c28444 100644
--- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
@@ -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 (
diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts
index 4de7f03ad..172224053 100644
--- a/packages/compiler/src/frontend/lowering/class-protocols.ts
+++ b/packages/compiler/src/frontend/lowering/class-protocols.ts
@@ -6,6 +6,8 @@ export class ClassProtocols {
private implemented: Set | null = null;
private readonly methods = new Map();
private readonly answers = new Map();
+ /** The program classes each Pick or Omit view accepts (classesAcceptedBy). */
+ private readonly accepted = new Map();
/** 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();
+ 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` or `Omit` 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) {
diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts
index 6c6c7811d..7b4b58ae3 100644
--- a/packages/compiler/src/frontend/lowering/class-prototypes.ts
+++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts
@@ -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 },
diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts
index f8e49a516..49292c260 100644
--- a/packages/compiler/src/frontend/lowering/containers/bytes.ts
+++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts
@@ -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
diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts
index 07d983cc0..ccf9dc8c0 100644
--- a/packages/compiler/src/frontend/lowering/expressions/binary.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts
@@ -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:
diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
index 7ef4b9f25..bede93072 100644
--- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
@@ -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",
diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
index b5f99d682..a7596cc6d 100644
--- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
@@ -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) {
diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
index dec06fdd9..a70f0dfa9 100644
--- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
@@ -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).
diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
index 303b6ed55..e276326c9 100644
--- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
@@ -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
diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
index 5dc512a6c..fbcecdc32 100644
--- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
@@ -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;
}
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index 36ea0e8c2..e0c9b5879 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -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),
};
diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts
index 880cccc8b..ac5949656 100644
--- a/packages/compiler/src/frontend/lowering/generic-functions.ts
+++ b/packages/compiler/src/frontend/lowering/generic-functions.ts
@@ -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();
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(p: WithHandler):
+ * Promise; request(p: Plain): Promise; request(p: Params)
+ * {...}`): 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();
+ const bindings = new Map();
+ 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(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 {
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): 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` against `Res | Promise`: 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;
}
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index b1b4086cd..adc83fcf2 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -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` / `Omit` 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
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 49e7ad9fc..70cb732cc 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -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")
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 7789f8a86..3e52f0cdc 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -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
diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts
index bec8528c4..0ddfa0ac4 100644
--- a/packages/compiler/src/frontend/lowering/lower-functions.ts
+++ b/packages/compiler/src/frontend/lowering/lower-functions.ts
@@ -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 (
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index f0199d6a4..68dbce996 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -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 `: 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) {
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 69f6cc99b..2d814b457 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -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
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index e2236f592..216b12635 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -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>();
+ /** JavaScript function declarations their own module reassigns
+ * (rebindable-functions.ts). */
+ readonly rebindableFunctions = new Set();
/** 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();
+ fallbackArmed: FallbackSite[] = [];
+ fallbackTaint: FallbackTaint | null = null;
+ fallbackSplitDepth = 0;
/** Comparisons interned by operand representations and equality semantics. */
readonly equalityHelpers = new Map();
/** 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 ({ 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,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/rebindable-functions.ts b/packages/compiler/src/frontend/lowering/rebindable-functions.ts
new file mode 100644
index 000000000..da41833a6
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/rebindable-functions.ts
@@ -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 {
+ const found = new Set();
+ if (!isJsSourceFile(sf)) return found;
+ const byName = new Map();
+ 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));
+}
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index a884b39e1..1084acede 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -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
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index c9a1cc9df..02f2f4cfb 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -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): 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`
-
+ // An `any`-valued signature (`{ [key: string]: any }`, an SDK's
+ // `Dictionary`) holds whatever the program stores: its
+ // overflow values are checked-dynamic, exactly `Record`'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` 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,
+): { 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;
+}
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 0f144ca2e..9c8ce5920 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -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,
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index d4e96a35a..cd4c99a75 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -85,6 +85,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"bytes.constructor",
"bytes.instanceOf",
"buffer.byteLenDyn",
+ "buffer.byteLenValue",
+ "buffer.allocValue",
+ "fs.realpathValue",
"bytes.construct",
"arrayBuffer.new",
"sharedArrayBuffer.new",
@@ -769,6 +772,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"buffer.poolFromBytes",
"buffer.poolFromArray",
"buffer.poolFromDyn",
+ "buffer.poolFromDynRef",
// The checked-dynamic compare/equals validators: Node's argument
// ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably.
"buffer.compareChk",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index cd3db784d..0888519e2 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -687,6 +687,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** realpath(3) with Node's error shape (syscall "lstat" in the message,
* Node's own spelling for realpathSync failures). +1 fresh string. */
"fs.realpathSync": { argTypes: [STRING], result: STRING },
+ /** fs.realpathSync (or its .native when the flag is true) as a function
+ * value: path and options arrive unchecked; the result is a string or a
+ * Buffer by the options' encoding. */
+ "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN },
/** Native realpath with syscall "realpath" on failure. +1 fresh string. */
"fs.realpathNativeSync": { argTypes: [STRING], result: STRING },
/** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name,
@@ -2872,6 +2876,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* allocUnsafeSlow never pool. The size forms validate like Node
* (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */
"buffer.alloc": { argTypes: [F64], result: BYTES_U8 },
+ /** Buffer.alloc as a function value: the size, fill and encoding arrive
+ * unchecked, and the runtime fills as Node's alloc does. */
+ "buffer.allocValue": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 },
"buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 },
"buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 },
"buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 },
@@ -2879,12 +2886,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 },
"buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 },
"buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 },
+ /** poolFromDyn that also copies an array crossing by reference. */
+ "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 },
"buffer.poolSize": { argTypes: [], result: F64 },
"buffer.setPoolSize": { argTypes: [F64], result: VOID },
/** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like
* fromStr's — and Buffer.isEncoding(name) over a runtime string
* (case-insensitive against Node's alias set). Pure; never throw. */
"buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 },
+ /** Buffer.byteLength as a function value: the encoding arrives at run time. */
+ "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 },
"buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 },
"buffer.isEncoding": { argTypes: [STRING], result: BOOL },
/** Buffer.concat(list, totalLength): the concatenation truncated or
@@ -4176,6 +4187,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* (inspect, Object.keys/values/entries) then answer through its live
* checked view. The receiver is any such record type. Never throws. */
"record.sourceHas": { argTypes: [null], result: BOOL },
+ /** A checked-cast fallback site's reads right after the check
+ * (dynCheck.fallback): whether its value failed validation, and that
+ * value (+1), taken once. */
+ "checkedCast.fallbackTaken": { argTypes: [], result: BOOL },
+ "checkedCast.fallbackTake": { argTypes: [], result: DYN },
"insp.key": { argTypes: [STRING], result: STRING },
"insp.moreItems": { argTypes: [F64], result: STRING },
"insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING },
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 4c5bbd183..499a279b5 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -3824,11 +3824,20 @@ export type IrExpr =
/** `exact`: the value is known to be a capsule this program boxed from
* exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the
* expression), so the check is one load of the capsule's instance. */
+ /** `fallback`: a checked-cast fallback site (frontend
+ * checked-fallback.ts). A value that fails validation does not throw: the
+ * result is the dummy, and the checked dyn value parks for the
+ * `checkedCast.fallbackTaken`/`checkedCast.fallbackTake` reads the
+ * frontend emits right after the site, whose code then continues in the
+ * dynamic lowering of the region the value flows through. Other
+ * exceptions (a JSON syntax error) still throw. Only record, array,
+ * number, string and boolean targets carry it. */
| {
kind: "dynCheck";
value: IrExpr;
preserveRefs?: true;
exact?: true;
+ fallback?: true;
type: IrType;
loc: SrcLoc;
};
@@ -4214,6 +4223,10 @@ export function canConvertToDyn(
(a) =>
a.kind === "undefinedT" ||
isJsonSafeType(a, getRecord, getUnion) ||
+ // Composite arms (records holding dyn, Map or Set members) box
+ // through the walker beside the promise arm: a method answering
+ // either its result or a promise of it.
+ canBoxDynComposite(a, getRecord, getUnion, visiting) ||
isDynTypedRefType(a) ||
a.kind === "classval" ||
DYN_HANDLE_KINDS.has(a.kind) ||
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index fd7c98cf6..4914e0f28 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -9,97 +9,118 @@ import {
} from "./ir.js";
import { everyModuleNode } from "./traverse.js";
+/** The fixed-width Buffer read/write method names (both "UInt" and "Uint"
+ * spellings) a checked-dynamic method call routes to scr_bytes_dyn.c. */
+export const DYN_BUFFER_NUM_METHODS: ReadonlySet = new Set(
+ ["read", "write"].flatMap((rw) => [
+ ...["UInt", "Uint"].flatMap((u) =>
+ ["8", "16LE", "16BE", "32LE", "32BE"].map((w) => `${rw}${u}${w}`),
+ ),
+ ...[
+ "Int8",
+ "Int16LE",
+ "Int16BE",
+ "Int32LE",
+ "Int32BE",
+ "FloatLE",
+ "FloatBE",
+ "DoubleLE",
+ "DoubleBE",
+ ].map((n) => `${rw}${n}`),
+ ]),
+);
+
const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([
// DEP0040 rides the dyn-async unit's warning machinery.
- "punycode.warn",
- "fs.callbackValue",
- "fs.callbackCall",
- "fs.callbackGlob",
"async.awaitDyn",
- "timers.immediatePromise",
"crypto.native",
"dyn.promiseAll",
"dyn.promiseAllSettled",
"dyn.promiseAny",
"dyn.promiseResolve",
- "process.onUncaughtException",
- "process.offUncaughtException",
- "process.onUnhandledRejection",
- "process.offUnhandledRejection",
- "process.onRejectionHandled",
- "process.offRejectionHandled",
- "process.onWarning",
- "process.offWarning",
+ "fs.callbackCall",
+ "fs.callbackGlob",
+ "fs.callbackValue",
+ "process.deprecationFlag",
"process.emitWarning",
"process.emitWarningText",
- "process.deprecationFlag",
+ "process.offRejectionHandled",
+ "process.offUncaughtException",
+ "process.offUnhandledRejection",
+ "process.offWarning",
+ "process.onRejectionHandled",
+ "process.onUncaughtException",
+ "process.onUnhandledRejection",
+ "process.onWarning",
"process.setDeprecationFlag",
"process.setEmitWarning",
+ "punycode.warn",
+ "timers.immediatePromise",
// The performance timeline (scr_perf.c, linked with this unit for its
// MaxPerformanceEntryBufferExceededWarning).
- "perf.mark",
- "perf.measure",
- "perf.entries",
"perf.clearMarks",
"perf.clearMeasures",
- "perf.entryName",
- "perf.entryType",
- "perf.entryStart",
- "perf.entryDuration",
+ "perf.entries",
"perf.entryDetail",
+ "perf.entryDuration",
"perf.entryKeep",
+ "perf.entryName",
"perf.entryRef",
+ "perf.entryStart",
+ "perf.entryType",
+ "perf.mark",
+ "perf.measure",
// Blob and File (scr_blob.c, linked with this unit, which boxes their
// reads as dyn promises).
- "blob.new",
+ "als.disable",
+ "als.enterWith",
+ "als.exitRun",
+ "als.get",
+ "als.new",
+ "als.run",
+ "blob.arrayBuffer",
+ "blob.bytes",
"blob.fileNew",
+ "blob.is",
+ "blob.new",
"blob.slice",
"blob.text",
- "blob.bytes",
- "blob.arrayBuffer",
- "blob.is",
- "util.styleText",
- "util.inherits",
- "util.deprecate",
+ "dc.chanBindStore",
+ "dc.chanRunStores",
+ "dc.chanUnbindStore",
"util.callbackify",
"util.debuglog",
- "als.new",
- "als.get",
- "als.run",
- "als.exitRun",
- "als.enterWith",
- "als.disable",
- "dc.chanBindStore",
- "dc.chanUnbindStore",
- "dc.chanRunStores",
+ "util.deprecate",
+ "util.inherits",
+ "util.styleText",
]);
/** The libCalls that enter AsyncLocalStorage stores (RuntimeFeatures
* asyncContext). */
const ASYNC_CONTEXT_LIB_FNS: ReadonlySet = new Set([
- "als.run",
- "als.exitRun",
"als.enterWith",
+ "als.exitRun",
+ "als.run",
"dc.chanBindStore",
"dc.chanRunStores",
// getTestContext() reads the test runner's entry in the same snapshot.
- "test.hasContext",
"test.context",
+ "test.hasContext",
]);
const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([
- "url.legacyParse",
- "url.legacyFormat",
- "url.legacyResolve",
"url.domainToASCII",
"url.domainToUnicode",
+ "url.legacyFormat",
+ "url.legacyParse",
+ "url.legacyResolve",
]);
const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([
- "util.inherits",
- "util.deprecate",
"util.callbackify",
"util.debuglog",
+ "util.deprecate",
+ "util.inherits",
]);
/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */
@@ -167,14 +188,14 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([
"stream.addAbortSignalFn",
"stream.destroy",
"stream.destroyErr",
- "stream.errored",
- "stream.prop",
"stream.dynNative",
"stream.dynNativePipe",
+ "stream.errored",
"stream.pipeDyn",
- "stream.unpipeDyn",
+ "stream.prop",
"stream.stdioWritable",
"stream.unboxDyn",
+ "stream.unpipeDyn",
"stream.workerStdio",
]);
@@ -209,10 +230,6 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea
/** Entry points that link the dyn-invoke unit. */
const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([
- "stream.dynNative",
- "stream.dynNativePipe",
- "stream.pipeDyn",
- "stream.unpipeDyn",
"dyn.arrayProtoCall",
"dyn.arrayPrototype",
"dyn.builtinFunction",
@@ -224,6 +241,10 @@ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([
"dyn.globalConstructor",
"dyn.objCreateWithProperties",
"dyn.objectTag",
+ "stream.dynNative",
+ "stream.dynNativePipe",
+ "stream.pipeDyn",
+ "stream.unpipeDyn",
]);
/** Member names whose constant-key dynamic reads answer the inherited Object.prototype member through the dyn-invoke unit. Mirrors expr-dynamic.ts INHERITED_OBJECT_MEMBERS (the IR layer does not import the backend). */
@@ -258,8 +279,8 @@ const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRe
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
"dyn.builtinClass",
// the stream classes, through scr_dyn_builtin_stream.c
- "dyn.builtinStreamClass",
"dyn.builtinInstanceOf",
+ "dyn.builtinStreamClass",
"dyn.functionInstanceOf",
];
@@ -387,64 +408,64 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
* process signal/exit listeners and the piped-stdin surface. */
const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([
- "process.stdio",
- "process.onSignal",
+ "process.offExit",
"process.offSignal",
"process.onExit",
- "process.offExit",
+ "process.onSignal",
+ "process.stdio",
+ "stdin.nextChunk",
"stdin.onData",
"stdin.onEnd",
"stdin.onError",
- "stdin.nextChunk",
// node:readline rides the stdin unit (scr_readline.c links beside
// scr_events.c under the same gate).
+ "rl.checkCompleter",
+ "rl.checkOptions",
+ "rl.close",
"rl.create",
"rl.createSignal",
- "rl.question",
- "rl.questionAsync",
- "rl.questionAsyncSignal",
- "rl.questionDyn",
- "rl.questionAsyncDyn",
- "rl.questionAsyncSignalDyn",
- "rl.close",
- "rl.pause",
- "rl.resume",
- "rl.prompt",
- "rl.promptWith",
- "rl.setPrompt",
- "rl.setPromptDyn",
- "rl.getPrompt",
- "rl.write",
- "rl.writeKey",
- "rl.on",
- "rl.iterOpen",
- "rl.iterNext",
- "rl.iterReturn",
- "rl.checkOptions",
- "rl.checkCompleter",
- "rl.setOptions",
- "rl.setHistory",
- "rl.history",
- "rl.numProp",
- "rl.terminal",
- "rl.line",
- "rl.isTerminal",
- "rl.isPromises",
"rl.cursor",
- "rl.seq",
- "rl.moves",
"rl.cursorPos",
+ "rl.getPrompt",
+ "rl.history",
+ "rl.isPromises",
+ "rl.isTerminal",
+ "rl.iterNext",
+ "rl.iterOpen",
+ "rl.iterReturn",
"rl.keypressStdin",
"rl.keypressStream",
- "stdin.onKeypress",
- "rl.uiOpen",
- "rl.uiOpenStream",
- "rl.uiCursorTo",
- "rl.uiMoveCursor",
+ "rl.line",
+ "rl.moves",
+ "rl.numProp",
+ "rl.on",
+ "rl.pause",
+ "rl.prompt",
+ "rl.promptWith",
+ "rl.question",
+ "rl.questionAsync",
+ "rl.questionAsyncDyn",
+ "rl.questionAsyncSignal",
+ "rl.questionAsyncSignalDyn",
+ "rl.questionDyn",
+ "rl.resume",
+ "rl.seq",
+ "rl.setHistory",
+ "rl.setOptions",
+ "rl.setPrompt",
+ "rl.setPromptDyn",
+ "rl.terminal",
"rl.uiClearLine",
"rl.uiClearScreenDown",
- "rl.uiRollback",
"rl.uiCommit",
+ "rl.uiCursorTo",
+ "rl.uiMoveCursor",
+ "rl.uiOpen",
+ "rl.uiOpenStream",
+ "rl.uiRollback",
+ "rl.write",
+ "rl.writeKey",
+ "stdin.onKeypress",
]);
/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the
@@ -485,7 +506,9 @@ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "htt
/** One rule per runtime feature, sorted by name. A new feature is an
* entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan
- * and its early exit derive from this table. */
+ * and its early exit derive from this table. The lists above keep one
+ * entry per line, sorted within each commented group, so changes that add
+ * different entries touch different lines. */
const RUNTIME_FEATURE_RULES = {
assert: rule({ prefixes: ["assert."] }),
/** The program enters AsyncLocalStorage stores (store.run, enterWith,
@@ -573,6 +596,13 @@ const RUNTIME_FEATURE_RULES = {
streamDyn: rule({
fns: ["stream.dynNative", "stream.dynNativePipe", "stream.pipeDyn", "stream.unpipeDyn"],
}),
+ /** Fixed-width Buffer read/write methods called on checked-dynamic
+ * values (scr_bytes_dyn.c). */
+ dynBufferNum: rule({
+ expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_NUM_METHODS.has(node.method),
+ }),
+ /** Builtin functions read as values (scr_builtin_values.c). */
+ builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }),
/** The stream unit emits through the emitter unit, also when the
* program constructs no stream (a pipeline of functions). */
emitter: rule({
@@ -739,13 +769,13 @@ function noRuntimeFeatures(): RuntimeFeatures {
fetch: false,
fileHandle: false,
fsWatch: false,
- netLookup: false,
- http: false,
http2: false,
+ http: false,
inspect: false,
legacyTextDecoder: false,
mockTimers: false,
net: false,
+ netLookup: false,
nodeTest: false,
parseArgs: false,
perfLoop: false,
@@ -756,6 +786,8 @@ function noRuntimeFeatures(): RuntimeFeatures {
stdioExtended: false,
stream: false,
streamDyn: false,
+ dynBufferNum: false,
+ builtinValues: false,
symbol: false,
tls: false,
tlsCa: false,
diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts
index 7607034b2..70aa08d52 100644
--- a/packages/compiler/src/native/sanitizer.test.ts
+++ b/packages/compiler/src/native/sanitizer.test.ts
@@ -79,6 +79,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
dynTier: false,
streamDyn: false,
eventsOnceSignal: false,
+ dynBufferNum: false,
+ builtinValues: false,
warningReplace: false,
stdioExtended: false,
regex: true,
diff --git a/packages/compiler/test/checked-cast-fallback.test.ts b/packages/compiler/test/checked-cast-fallback.test.ts
new file mode 100644
index 000000000..1eda2a7af
--- /dev/null
+++ b/packages/compiler/test/checked-cast-fallback.test.ts
@@ -0,0 +1,172 @@
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { expect, test } from "vitest";
+import { compile, deserializeModule } from "../src/index.js";
+import type { IrExpr, IrFunction, IrModule, IrStmt } from "../src/ir/ir.js";
+import { everyStmtList } from "../src/ir/traverse.js";
+import type { TierMode } from "../src/frontend/tier-analysis.js";
+
+async function lowerSource(source: string, tier?: TierMode): Promise {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cast-fallback-"));
+ try {
+ const entry = join(dir, "main.ts");
+ await writeFile(entry, source);
+ const outPath = join(dir, "main.ir.json");
+ const result = await compile(entry, {
+ outPath,
+ outDir: dir,
+ outputKind: "ir",
+ ...(tier ? { tier } : {}),
+ });
+ if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
+ return deserializeModule(await readFile(outPath, "utf8"));
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+}
+
+function fnNamed(mod: IrModule, name: string): IrFunction {
+ const fn = mod.functions.find((f) => f.name === name);
+ if (!fn) throw new Error(`no function ${name}`);
+ return fn;
+}
+
+function checks(fn: IrFunction): (IrExpr & { kind: "dynCheck" })[] {
+ const found: (IrExpr & { kind: "dynCheck" })[] = [];
+ everyStmtList(fn.body, {
+ expr: (e) => {
+ if (e.kind === "dynCheck") found.push(e);
+ return true;
+ },
+ stmt: () => true,
+ });
+ return found;
+}
+
+/** The `if (flag) dynamic else static` split a fallback tail lowers to. */
+function splits(fn: IrFunction): (IrStmt & { kind: "if" })[] {
+ const found: (IrStmt & { kind: "if" })[] = [];
+ everyStmtList(fn.body, {
+ expr: () => true,
+ stmt: (s) => {
+ if (
+ s.kind === "if" &&
+ s.cond.kind === "varRef" &&
+ s.cond.localId.startsWith("%fb.") &&
+ s.else_ !== null
+ )
+ found.push(s);
+ return true;
+ },
+ });
+ return found;
+}
+
+const listener = `
+interface Order { deployment: { id: string; plan: string } }
+interface Req { buildOrder: Order; traceCarrier?: Record }
+function accept(body: string): string {
+ let request: Partial;
+ try {
+ request = JSON.parse(body) as Partial;
+ } catch {
+ return "400";
+ }
+ const id = request.buildOrder?.deployment?.id;
+ return id ? "200 " + id : "400 missing";
+}
+console.log(accept('{"buildOrder":{"deployment":{"id":"d"}}}'));
+`;
+
+test("a block-local checked cast continues in a dynamic tail behind one flag", async () => {
+ const fn = fnNamed(await lowerSource(listener), "accept");
+ const [check] = checks(fn);
+ expect(check?.fallback).toBe(true);
+ const split = splits(fn);
+ expect(split).toHaveLength(1);
+ // The static tail reads the record natively; the dynamic one reads the
+ // failed value's shadow.
+ const shadowReads = (body: IrStmt[]): number => {
+ let n = 0;
+ everyStmtList(body, {
+ expr: (e) => {
+ if (e.kind === "varRef" && e.localId.startsWith("%fbv.")) n++;
+ return true;
+ },
+ stmt: () => true,
+ });
+ return n;
+ };
+ expect(shadowReads(split[0]!.then)).toBeGreaterThan(0);
+ expect(shadowReads(split[0]!.else_!)).toBe(0);
+});
+
+test("--tier=static-only keeps the throwing cast", async () => {
+ const fn = fnNamed(await lowerSource(listener, "static-only"), "accept");
+ expect(checks(fn).some((c) => c.fallback)).toBe(false);
+ expect(splits(fn)).toHaveLength(0);
+ expect(fn.locals.some((l) => l.id.startsWith("%fb"))).toBe(false);
+});
+
+test("casts whose binding is read outside the code after them, or written again, keep throwing", async () => {
+ const mod = await lowerSource(`
+interface P { x: number }
+const top = JSON.parse('{"x":1}') as P;
+console.log(top.x);
+function before(text: string): number {
+ let p: P | undefined;
+ for (let i = 0; i < 2; i++) {
+ if (p) console.log(p.x);
+ p = JSON.parse(text) as P;
+ }
+ return p?.x ?? 0;
+}
+function rewritten(text: string): number {
+ let p = JSON.parse(text) as P;
+ p = { x: 2 };
+ return p.x;
+}
+function hoisted(text: string): number {
+ const p = JSON.parse(text) as P;
+ function read(): number { return p.x; }
+ return read();
+}
+console.log(before('{"x":1}'), rewritten('{"x":1}'), hoisted('{"x":1}'));
+`);
+ for (const name of ["before", "rewritten", "hoisted"]) {
+ const fn = fnNamed(mod, name);
+ expect(
+ checks(fn).some((c) => c.fallback),
+ name,
+ ).toBe(false);
+ expect(
+ fn.locals.some((l) => l.id.startsWith("%fb")),
+ name,
+ ).toBe(false);
+ }
+ expect(mod.functions.flatMap(checks).some((c) => c.fallback)).toBe(false);
+});
+
+test("sequential casts split the code after them at most twice", async () => {
+ const source = `
+interface P { x: number }
+function sum(a: string, b: string, c: string, d: string): number {
+ const p = JSON.parse(a) as P;
+ const q = JSON.parse(b) as P;
+ const r = JSON.parse(c) as P;
+ const s = JSON.parse(d) as P;
+ return p.x + q.x + r.x + s.x;
+}
+console.log(sum('{"x":1}', '{"x":2}', '{"x":3}', '{"x":4}'));
+`;
+ const fn = fnNamed(await lowerSource(source), "sum");
+ // p splits the code after it, and q splits each copy of that code; r and
+ // s keep the throwing cast instead of doubling the copies again.
+ expect(splits(fn)).toHaveLength(3);
+ const binding = (c: { loc: { start: number } }) =>
+ /const (\w) = [^\n]*$/.exec(source.slice(0, c.loc.start))?.[1];
+ const casts = checks(fn);
+ expect(new Set(casts.filter((c) => c.fallback).map(binding))).toEqual(new Set(["p", "q"]));
+ expect(new Set(casts.filter((c) => !c.fallback).map(binding))).toEqual(new Set(["r", "s"]));
+});
diff --git a/packages/compiler/test/json-callbacks.test.ts b/packages/compiler/test/json-callbacks.test.ts
index 308d626e3..d10721d0b 100644
--- a/packages/compiler/test/json-callbacks.test.ts
+++ b/packages/compiler/test/json-callbacks.test.ts
@@ -53,7 +53,8 @@ for (const backend of ["llvm"] as const) {
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
- // A checked cast still validates the result AFTER revival.
+ // A checked cast validates the result AFTER revival; a revived value
+ // that does not fit continues as that value (checked-cast fallback).
try {
const value = JSON.parse('{"n":1}', (key: string, value: unknown): unknown => key === "n" ? "wrong" : value) as { n: number };
console.log(value.n);
@@ -75,6 +76,8 @@ for (const backend of ["llvm"] as const) {
console.log(result === undefined, typeof result);
}
local();
+ // Values that do not fit continue as their parsed values, every
+ // iteration (and release cleanly under the sanitizer lane).
for (let i = 0; i < 20; i++) {
try {
const record = JSON.parse('{"opaque":{"deep":[1,2]},"typed":"wrong"}') as { opaque: unknown; typed: number };
@@ -105,13 +108,12 @@ for (const backend of ["llvm"] as const) {
expect(result.stdout.toString()).toBe(
[
"JSON.parse reviver deleting array elements is not supported yet",
- "true",
+ "wrong",
"true",
"true",
"true",
"true undefined",
- "true",
- "true",
+ ...Array.from({ length: 20 }, () => ["wrong", "2"]).flat(),
"1",
"",
].join("\n"),
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json
new file mode 100644
index 000000000..70a5823f4
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6412-any-valued-dictionaries.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json
new file mode 100644
index 000000000..83fe76101
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6413-sdk-declared-data-class/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json
new file mode 100644
index 000000000..c5b440992
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6414-pick-class-method-views.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json
new file mode 100644
index 000000000..ab4ee5819
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6419-untyped-async-package/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json
new file mode 100644
index 000000000..24634cff1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json
@@ -0,0 +1,9 @@
+{
+ "order": [
+ "/tests/corpus/6494-default-export-class-members/lib/date.js",
+ "/tests/corpus/6494-default-export-class-members/lib/error.js",
+ "/tests/corpus/6494-default-export-class-members/lib/index.js",
+ "/tests/corpus/6494-default-export-class-members/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json
new file mode 100644
index 000000000..98806a53c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6495-generic-method-overloads.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json
new file mode 100644
index 000000000..8bc601bd6
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6496-overload-signatures.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json
new file mode 100644
index 000000000..06e7bb17f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6497-generic-overload-bindings.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json
new file mode 100644
index 000000000..a34eeedc0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6550-npm-boolean-bitwise/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json
new file mode 100644
index 000000000..62b900d3c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6560-npm-js-capability-probes/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json
new file mode 100644
index 000000000..f0d46c0bf
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6565-rebindable-function-import-cycle/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json
new file mode 100644
index 000000000..3db56c8ab
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6570-npm-builtin-function-bind/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json
new file mode 100644
index 000000000..fd070103c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6570-require-mixed-declarations.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json
new file mode 100644
index 000000000..1fb3f21bc
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6592-promise-armed-method-values/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json
new file mode 100644
index 000000000..01d871cb3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/class-id-instances-as-streams.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json
new file mode 100644
index 000000000..8f5694905
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/json-parse-fallback-extra-keys.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json
new file mode 100644
index 000000000..2084d8a65
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/json-parse-fallback-flows.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json
new file mode 100644
index 000000000..17562fe47
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/json-parse-fallback-missing.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json
new file mode 100644
index 000000000..3585687e2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/json-parse-fallback-nested.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json
new file mode 100644
index 000000000..26bae0445
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/json-parse-fallback-partial.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json
new file mode 100644
index 000000000..fd445ea1c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/json-parse-fallback-wrong-types.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json
new file mode 100644
index 000000000..b52b29f92
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/nested-member-blocker-path.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json
deleted file mode 100644
index 1515ceeb9..000000000
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "order": [
- "/tests/diagnostics/overload-fences.ts"
- ],
- "diags": []
-}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json
new file mode 100644
index 000000000..1dafe9a3a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/pick-view-generic-methods.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json
new file mode 100644
index 000000000..1d842a3fe
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/process-versions-absent.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs
index 258df8dec..aa71d5fb8 100644
--- a/packages/runtime-pack-common/runtime-pack-matrix.mjs
+++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs
@@ -61,6 +61,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_inspect.c", "inspect"],
["scr_console_native.c", "inspect"],
["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")],
+ // Fixed-width Buffer methods called on checked-dynamic values.
+ ["scr_bytes_dyn.c", all("dynBufferNum", "dynInvoke")],
+ // Builtin functions read as values in JavaScript dependencies.
+ ["scr_builtin_values.c", "builtinValues"],
["scr_dc.c", "dc"],
// The dynamic tier's jsval values (two-tier design I3): linked only into
// programs whose IR has dynamic-tier operations.
diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c
new file mode 100644
index 000000000..ea240359a
--- /dev/null
+++ b/packages/runtime/src/scr_builtin_values.c
@@ -0,0 +1,261 @@
+/* Builtin functions called as function values in JavaScript dependencies
+ * (`Buffer.byteLength.bind(Buffer)`, `var alloc = Buffer.alloc`, a stored
+ * `fs.realpathSync`): the arguments arrive unchecked, so these entries apply
+ * Node's argument handling before the ordinary operation. A separate unit,
+ * linked only by programs that read these values. */
+#include "scr_runtime.h"
+
+#include
+#include
+#include
+
+/* Buffer.byteLength called as a function value (`Buffer.byteLength.bind(
+ * Buffer)` in truncate-utf8-bytes), with the encoding known only at run
+ * time. Node's ladder: a non-string source answers its byte length or
+ * throws before the encoding is read; an empty string answers 0; a falsy
+ * encoding, or one getEncodingOps does not know after ToString (any ASCII
+ * case), counts UTF-8. */
+double scr_buffer_byte_length_value(ScrDyn *value, ScrDyn *encoding) {
+ static const struct { const char *from; const char *to; } map[] = {
+ {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"},
+ {"base64", "base64"}, {"base64url", "base64url"},
+ {"latin1", "latin1"}, {"binary", "latin1"}, {"ascii", "ascii"},
+ {"utf16le", "utf16le"}, {"utf-16le", "utf16le"},
+ {"ucs2", "utf16le"}, {"ucs-2", "utf16le"},
+ };
+ const char *name = "utf8";
+ if (value->kind == SCR_DYN_STR && value->v.str->len > 0 && scr_dyn_truthy(encoding)) {
+ ScrStr *raw = scr_dyn_string_coerce_js(encoding);
+ if (!raw) return 0;
+ for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) {
+ size_t n = strlen(map[i].from);
+ bool same = raw->len == n;
+ for (size_t k = 0; same && k < n; k++) {
+ char c = raw->data[k];
+ if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A'));
+ same = c == map[i].from[k];
+ }
+ if (same) {
+ name = map[i].to;
+ break;
+ }
+ }
+ scr_str_release(raw);
+ }
+ ScrStr *canonical = scr_str_new(name, strlen(name));
+ double length = scr_buffer_byte_length_dyn(value, canonical);
+ scr_str_release(canonical);
+ return length;
+}
+
+/* Node's normalizeEncoding: the canonical name for a string encoding
+ * (any ASCII case; null, undefined and "" read as utf8), or NULL. */
+static const char *scr_bv_encoding(const ScrDyn *encoding) {
+ static const struct { const char *from; const char *to; } map[] = {
+ {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"},
+ {"base64", "base64"}, {"base64url", "base64url"},
+ {"latin1", "latin1"}, {"binary", "latin1"}, {"ascii", "ascii"},
+ {"utf16le", "utf16le"}, {"utf-16le", "utf16le"},
+ {"ucs2", "utf16le"}, {"ucs-2", "utf16le"},
+ };
+ if (encoding->kind == SCR_DYN_UNDEF || encoding->kind == SCR_DYN_NULL) return "utf8";
+ if (encoding->kind != SCR_DYN_STR) return NULL;
+ const ScrStr *name = encoding->v.str;
+ if (name->len == 0) return "utf8";
+ for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) {
+ size_t n = strlen(map[i].from);
+ bool same = name->len == n;
+ for (size_t k = 0; same && k < n; k++) {
+ char c = name->data[k];
+ if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A'));
+ same = c == map[i].from[k];
+ }
+ if (same) return map[i].to;
+ }
+ return NULL;
+}
+
+static void scr_bv_invalid_value(const ScrDyn *value) {
+ char rendered[160];
+ /* The rejected views are empty, which util.inspect prints whole. */
+ const char *text = value->kind == SCR_DYN_BYTES && value->v.bytes->len == 0 && value->buffer ? ""
+ : scr_dyn_inspect_lite(value, rendered, sizeof rendered);
+ char message[224];
+ int n = snprintf(message, sizeof message, "The argument 'value' is invalid. Received %s", text);
+ scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_INVALID_ARG_VALUE");
+}
+
+/* Buffer.alloc called as a function value (`var alloc = Buffer.alloc` in
+ * tar-stream), with Node's alloc and _fill: the size must be a number,
+ * ERR_INVALID_ARG_TYPE otherwise, then the allocation and range checks.
+ * A fill that is undefined or 0, or a size of 0, leaves the buffer zeroed.
+ * A string fill encodes with the encoding (ERR_INVALID_ARG_TYPE for a
+ * non-string encoding Node cannot normalize, ERR_UNKNOWN_ENCODING for an
+ * unknown name), an empty string zero-fills, and a non-empty string that
+ * encodes to no bytes throws ERR_INVALID_ARG_VALUE. A view fills with
+ * its bytes (ERR_INVALID_ARG_VALUE when it is empty); any other value
+ * fills with ToUint32(ToNumber(value)) & 255. +1, or NULL with the
+ * exception pending. */
+ScrBytes *scr_buffer_alloc_value(const ScrDyn *size, const ScrDyn *fill, const ScrDyn *encoding) {
+ if (size->kind != SCR_DYN_NUM) {
+ scr_dyn_arg_type_fail("size", "of type number", size);
+ return NULL;
+ }
+ ScrBytes *buf = scr_buffer_alloc(size->v.num);
+ if (!buf) return NULL;
+ if (fill->kind == SCR_DYN_UNDEF || (fill->kind == SCR_DYN_NUM && fill->v.num == 0) || buf->len == 0)
+ return buf;
+ if (fill->kind == SCR_DYN_STR) {
+ const char *name = scr_bv_encoding(encoding);
+ if (!name) {
+ if (encoding->kind != SCR_DYN_STR) {
+ scr_dyn_arg_type_fail("encoding", "of type string", encoding);
+ } else {
+ ScrJsonBuf b;
+ scr_jb_init(&b);
+ scr_jb_puts(&b, "Unknown encoding: ");
+ scr_jb_put_str(&b, encoding->v.str);
+ ScrStr *message = scr_jb_finish(&b);
+ scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_UNKNOWN_ENCODING");
+ scr_str_release(message);
+ }
+ scr_bytes_release(buf);
+ return NULL;
+ }
+ if (fill->v.str->len == 0) return buf;
+ ScrStr *canonical = scr_str_new(name, strlen(name));
+ ScrBytes *pattern = scr_bytes_from_str(fill->v.str, canonical);
+ scr_str_release(canonical);
+ if (!pattern || pattern->len == 0) {
+ scr_bytes_release(pattern);
+ scr_bytes_release(buf);
+ scr_bv_invalid_value(fill);
+ return NULL;
+ }
+ ScrBytes *filled = scr_bytes_fill(buf, pattern, 0, 0, 0);
+ scr_bytes_release(pattern);
+ scr_bytes_release(filled);
+ return buf;
+ }
+ if (fill->kind == SCR_DYN_BYTES) {
+ ScrBytes *pattern = scr_bytes_buffer_source(fill);
+ if (!pattern || pattern->len == 0) {
+ scr_bytes_release(pattern);
+ scr_bytes_release(buf);
+ scr_bv_invalid_value(fill);
+ return NULL;
+ }
+ ScrBytes *filled = scr_bytes_fill(buf, pattern, 0, 0, 0);
+ scr_bytes_release(pattern);
+ scr_bytes_release(filled);
+ return buf;
+ }
+ double value;
+ if (!scr_dyn_number_coerce_js(fill, &value)) {
+ scr_bytes_release(buf);
+ return NULL;
+ }
+ double byte = isfinite(value) ? fmod(trunc(value), 256.0) : 0;
+ if (byte < 0) byte += 256;
+ memset(buf->data, (int)byte, buf->len);
+ return buf;
+}
+
+/* fs.realpathSync and fs.realpathSync.native called as function values
+ * (fs.realpath keeps `var origRealpathSync = fs.realpathSync`). Node's
+ * argument handling: options that are null, undefined or a function mean
+ * none, a string is the encoding, an object supplies `encoding`, anything
+ * else throws ERR_INVALID_ARG_TYPE; a file URL is its path (toPathIfFileURL,
+ * with fileURLToPath's errors for other URLs). The JavaScript form converts
+ * any other path that is not a string with ToString (a Buffer decodes as
+ * UTF-8); the native form takes a Uint8Array's bytes and throws
+ * ERR_INVALID_ARG_TYPE for other values. The result is a string for
+ * utf8, a Buffer for "buffer", and the UTF-8 bytes decoded in any other
+ * encoding, whose name must be one Node knows. +1 or NULL. */
+ScrDyn *scr_fs_realpath_value(const ScrDyn *path, const ScrDyn *options, bool native) {
+ const ScrDyn *encoding = NULL;
+ if (options->kind == SCR_DYN_STR) {
+ encoding = options;
+ } else if (options->kind == SCR_DYN_OBJ) {
+ encoding = scr_dyn_obj_get(options, "encoding", 8);
+ } else if (options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL &&
+ options->kind != SCR_DYN_FUNC) {
+ scr_dyn_arg_type_fail("options", "of type string or an instance of Object", options);
+ return NULL;
+ }
+ static const struct { const char *from; const char *to; } names[] = {
+ {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, {"base64", "base64"},
+ {"base64url", "base64url"}, {"latin1", "latin1"}, {"binary", "latin1"},
+ {"ascii", "ascii"}, {"utf16le", "utf16le"}, {"utf-16le", "utf16le"},
+ {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, {"buffer", "buffer"},
+ };
+ const char *canonical = "utf8";
+ if (encoding && encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL &&
+ !(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 0)) {
+ const char *found = NULL;
+ if (encoding->kind == SCR_DYN_STR) {
+ for (size_t i = 0; i < sizeof names / sizeof names[0] && !found; i++) {
+ size_t n = strlen(names[i].from);
+ if (encoding->v.str->len != n) continue;
+ bool same = true;
+ for (size_t k = 0; k < n && same; k++) {
+ char c = encoding->v.str->data[k];
+ if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A'));
+ same = c == names[i].from[k];
+ }
+ if (same) found = names[i].to;
+ }
+ }
+ if (!found) {
+ ScrStr *shown = scr_dyn_string_coerce_js(encoding);
+ if (!shown) return NULL;
+ char msg[256];
+ int n = snprintf(msg, sizeof msg, "The argument 'encoding' is invalid encoding. Received '%.*s'",
+ (int)(shown->len > 160 ? 160 : shown->len), shown->data);
+ scr_str_release(shown);
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_ARG_VALUE");
+ return NULL;
+ }
+ canonical = found;
+ }
+ ScrStr *p;
+ if (scr_dyn_native_url_is(path)) {
+ p = scr_url_to_path(path->v.handle.ptr);
+ } else if (path->kind == SCR_DYN_STR) {
+ p = scr_str_retain(path->v.str);
+ } else if (native && path->kind == SCR_DYN_BYTES && path->v.bytes->elem == SCR_BYTES_U8) {
+ /* getValidatedPath takes a Uint8Array's bytes as the path. */
+ ScrStr *utf8 = scr_str_new("utf8", 4);
+ p = scr_bytes_to_str(path->v.bytes, utf8);
+ scr_str_release(utf8);
+ } else if (native) {
+ scr_dyn_arg_type_fail("path", "of type string or an instance of Buffer or URL", path);
+ return NULL;
+ } else {
+ p = scr_dyn_string_coerce_js(path);
+ }
+ if (!p) return NULL;
+ ScrStr *resolved = native ? scr_fs_realpath_promise(p) : scr_fs_realpath(p);
+ scr_str_release(p);
+ if (!resolved) return NULL;
+ ScrDyn *out;
+ if (!strcmp(canonical, "utf8")) {
+ out = scr_dyn_new_str(resolved);
+ } else {
+ ScrStr *utf8 = scr_str_new("utf8", 4);
+ ScrBytes *bytes = scr_buffer_from_str(resolved, utf8);
+ scr_str_release(utf8);
+ if (!strcmp(canonical, "buffer")) {
+ out = scr_dyn_new_buffer(bytes);
+ } else {
+ ScrStr *enc = scr_str_new(canonical, strlen(canonical));
+ ScrStr *text = scr_bytes_to_str(bytes, enc);
+ scr_str_release(enc);
+ out = scr_dyn_new_str(text);
+ scr_str_release(text);
+ }
+ scr_bytes_release(bytes);
+ }
+ scr_str_release(resolved);
+ return out;
+}
diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h
index b7b913d7f..e0fa7b857 100644
--- a/packages/runtime/src/scr_bytes.h
+++ b/packages/runtime/src/scr_bytes.h
@@ -389,11 +389,15 @@ ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc);
ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */
ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */
ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding);
+ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding);
ScrBytes *scr_buffer_concat(const ScrArr *list);
ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total);
/* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list
* and each element, then the typed concat. NULL with the exception pending. */
ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list);
+ScrBytes *scr_buffer_alloc_value(const struct ScrDyn *size, const struct ScrDyn *fill,
+ const struct ScrDyn *encoding);
+struct ScrDyn *scr_fs_realpath_value(const struct ScrDyn *path, const struct ScrDyn *options, bool native);
ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total);
/* Drops the pool's slab reference and restores Buffer.poolSize (process
* exit audits and library session resets). */
@@ -408,6 +412,7 @@ ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc);
* Borrow; never throw. */
double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc);
double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *enc);
+double scr_buffer_byte_length_value(ScrDyn *value, ScrDyn *encoding);
bool scr_bytes_is_encoding(const ScrStr *s);
/* equals / compare / indexOf-lastIndexOf / fill / copy / swap / write on
diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c
new file mode 100644
index 000000000..d4b29574b
--- /dev/null
+++ b/packages/runtime/src/scr_bytes_dyn.c
@@ -0,0 +1,101 @@
+/* The fixed-width read/write methods of a Buffer held as a checked-dynamic
+ * value: a JavaScript dependency's `buf.writeUInt32LE(v, 0)` on a buffer it
+ * got back untyped (yazl). The compiler routes only the dyn method calls
+ * that name one of these methods here, so programs without them link
+ * nothing new; any other receiver takes the ordinary dispatch. */
+#include "scr_runtime.h"
+
+#include
+#include
+#include
+
+static const struct {
+ const char *name;
+ ScrBytesNumKind kind;
+ bool le;
+} scr_bytes_dyn_names[] = {
+ { "UInt8", SCR_BN_U8, false }, { "Uint8", SCR_BN_U8, false },
+ { "Int8", SCR_BN_I8, false }, { "UInt16LE", SCR_BN_U16, true },
+ { "Uint16LE", SCR_BN_U16, true }, { "UInt16BE", SCR_BN_U16, false },
+ { "Uint16BE", SCR_BN_U16, false }, { "Int16LE", SCR_BN_I16, true },
+ { "Int16BE", SCR_BN_I16, false }, { "UInt32LE", SCR_BN_U32, true },
+ { "Uint32LE", SCR_BN_U32, true }, { "UInt32BE", SCR_BN_U32, false },
+ { "Uint32BE", SCR_BN_U32, false }, { "Int32LE", SCR_BN_I32, true },
+ { "Int32BE", SCR_BN_I32, false }, { "FloatLE", SCR_BN_F32, true },
+ { "FloatBE", SCR_BN_F32, false }, { "DoubleLE", SCR_BN_F64, true },
+ { "DoubleBE", SCR_BN_F64, false },
+};
+
+static size_t scr_bytes_dyn_width(ScrBytesNumKind kind) {
+ switch (kind) {
+ case SCR_BN_U8:
+ case SCR_BN_I8:
+ return 1;
+ case SCR_BN_U16:
+ case SCR_BN_I16:
+ return 2;
+ case SCR_BN_F64:
+ return 8;
+ default:
+ return 4;
+ }
+}
+
+/* Node's checkInt range test and message, before the offset is looked at. */
+static bool scr_bytes_dyn_value_in_range(double value, size_t width, bool sign) {
+ double max = sign ? exp2((double)(8 * width - 1)) - 1 : exp2((double)(8 * width)) - 1;
+ double min = sign ? -exp2((double)(8 * width - 1)) : 0;
+ if (!(value > max || value < min)) return true;
+ char recv[48];
+ scr_num_received(value, recv);
+ char minb[32], maxb[32];
+ scr_f64_to_str(min, minb);
+ scr_f64_to_str(max, maxb);
+ char msg[160];
+ int mlen = snprintf(msg, sizeof msg,
+ "The value of \"value\" is out of range. It must be >= %s and <= %s. Received %s",
+ minb, maxb, recv);
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
+ return false;
+}
+
+/* Node's order: a write converts the value with ToNumber and checks its
+ * range, then the offset must be a number (absent means 0), then the
+ * bounds. +1, or NULL with the exception pending. */
+static ScrDyn *scr_bytes_dyn_num_method(ScrBytes *b, bool read, size_t at, ScrDyn *const *args,
+ size_t argc) {
+ ScrBytesNumKind kind = scr_bytes_dyn_names[at].kind;
+ double value = 0;
+ if (!read) {
+ if (!scr_dyn_number_coerce_js(argc > 0 ? args[0] : scr_dyn_undefined(), &value)) return NULL;
+ if (kind != SCR_BN_F32 && kind != SCR_BN_F64) {
+ bool sign = kind == SCR_BN_I8 || kind == SCR_BN_I16 || kind == SCR_BN_I32;
+ if (!scr_bytes_dyn_value_in_range(value, scr_bytes_dyn_width(kind), sign)) return NULL;
+ }
+ }
+ size_t offset_at = read ? 0 : 1;
+ double offset = 0;
+ if (argc > offset_at && args[offset_at]->kind != SCR_DYN_UNDEF) {
+ if (args[offset_at]->kind != SCR_DYN_NUM) {
+ scr_dyn_arg_type_fail("offset", "of type number", args[offset_at]);
+ return NULL;
+ }
+ offset = args[offset_at]->v.num;
+ }
+ bool le = scr_bytes_dyn_names[at].le;
+ double out = read ? scr_bytes_read_num(b, offset, kind, le) : scr_bytes_write_num(b, value, offset, kind, le);
+ return scr_exc_pending() ? NULL : scr_dyn_new_num(out);
+}
+
+ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args,
+ size_t argc, const char *what) {
+ if (recv->kind == SCR_DYN_BYTES && recv->buffer && !recv->v.bytes->is_data_view) {
+ bool read = !strncmp(method, "read", 4);
+ const char *rest = method + (read ? 4 : 5);
+ for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) {
+ if (!strcmp(rest, scr_bytes_dyn_names[at].name))
+ return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc);
+ }
+ }
+ return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what);
+}
diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c
index 4d7ef5f67..c852388e9 100644
--- a/packages/runtime/src/scr_bytes_io.c
+++ b/packages/runtime/src/scr_bytes_io.c
@@ -1350,6 +1350,24 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding
return scr_buffer_pool_adopt(out);
}
+/* scr_buffer_from_dyn_pooled for values that can be a typed array crossing
+ * an untyped slot by reference (a number[] passed to an untyped parameter,
+ * as yazl's Buffer.from(something, encoding) receives it): the array copies
+ * like the array it is. Used by the runtime-encoding Buffer.from forms. */
+ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding) {
+ if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(value) != NULL) {
+ ScrDyn *materialized = scr_dyn_typed_ref_materialize(value);
+ if (scr_exc_pending()) {
+ scr_dyn_release(materialized);
+ return NULL;
+ }
+ ScrBytes *out = scr_buffer_from_dyn_pooled(materialized, encoding);
+ scr_dyn_release(materialized);
+ return out;
+ }
+ return scr_buffer_from_dyn_pooled(value, encoding);
+}
+
/* ── the checked-dynamic Buffer compare/equals validators ──────────────
* Node's argument ladders for buf.equals / buf.compare / Buffer.compare
* over dyn-boxed arguments (the invalid-input probes: string needles,
diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c
index d92699573..9085bf9c9 100644
--- a/packages/runtime/src/scr_cycle.c
+++ b/packages/runtime/src/scr_cycle.c
@@ -165,8 +165,9 @@ void scr_weak_dispose(void *object) {
#include
#include
-/* Every heap object's first member is `size_t rc`. */
-#define SCR_RC(obj) (*(size_t *)(obj))
+/* Every heap object's first member is `size_t rc`; the collector works on
+ * its count (SCR_RC_COUNT). */
+#define SCR_RC(obj) SCR_RC_COUNT(obj)
static void scr_cyc_oom(void) {
scr_trap("scriptc: out of memory\n");
@@ -1171,7 +1172,7 @@ void scr_cyc_on_release(void *obj) {
#define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h)
#endif
#define SCR_CYC_SKIP(child) \
- ((child) == NULL || SCR_RC(child) == SIZE_MAX \
+ ((child) == NULL || SCR_RC(child) == SCR_RC_IMMORTAL \
|| SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit)
/* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where
@@ -1215,7 +1216,7 @@ SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit)
static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth);
static void scr_mg_visit(void *child, void *ctx) {
- if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
+ if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) {
scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder);
@@ -1374,7 +1375,7 @@ static SCR_TL size_t scr_xg_freed = 0;
static void scr_xg_release(void *obj);
static void scr_xg_child_visit(void *child, void *ctx) {
(void)ctx;
- if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
+ if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
scr_xg_release(child);
}
@@ -1599,7 +1600,7 @@ static void scr_yin_leave(ScrCycHdr *h, void *obj) {
static void scr_yin_count_visit(void *child, void *ctx) {
(void)ctx;
- if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
+ if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++;
}
@@ -1607,7 +1608,7 @@ static void scr_yin_count_visit(void *child, void *ctx) {
/* Marked young objects are GRAY (young objects are otherwise BLACK). */
static void scr_yin_mark_visit(void *child, void *ctx) {
(void)ctx;
- if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
+ if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return;
scr_cyc_set_color(h, SCR_CYC_GRAY);
@@ -1619,7 +1620,7 @@ static void scr_yin_mark_visit(void *child, void *ctx) {
* the count until every teardown has run. */
static void scr_yin_dead_visit(void *child, void *ctx) {
(void)ctx;
- if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
+ if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return;
scr_yin_push(&scr_yin_xrel, child);
diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h
index 401bf2b39..0ac280436 100644
--- a/packages/runtime/src/scr_dyn.h
+++ b/packages/runtime/src/scr_dyn.h
@@ -1134,6 +1134,10 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b);
* result owned (+1). MAY THROW (NULL with the exception pending). */
ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method);
ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what);
+/* scr_dyn_invoke_prepared for the fixed-width Buffer read/write method names
+ * (scr_bytes_dyn.c): a Buffer receiver runs the method, any other takes the
+ * ordinary dispatch. */
+ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what);
ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what);
/* Keyed read on a FUNC node: the own-property table first (defineProperties
* writes land there), then "name" (the box's best-effort static name; ""
@@ -1241,6 +1245,16 @@ ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, cons
* scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */
void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got);
+/* Checked-cast fallback (a `JSON.parse(text) as T` whose value fails
+ * validation keeps its dynamic value instead of throwing). Right after a
+ * fallback site's check, scr_dyn_check_recover takes back the exception the
+ * check raised (Node runs nothing at a cast, so nothing it raises is
+ * observable) and parks the checked value, which the lowered code reads
+ * with scr_dyn_check_fallback_taken and takes (+1) with _take. */
+bool scr_dyn_check_recover(ScrDyn *d);
+bool scr_dyn_check_fallback_taken(void);
+ScrDyn *scr_dyn_check_fallback_take(void);
+
/* Matchers and builders of recursive shapes (records and arrays whose type
* reaches itself) track the dyn values they visit, so cyclic values match
* and convert without unbounded recursion. Every matcher and builder of a
diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c
index b7295f59a..209c8c39a 100644
--- a/packages/runtime/src/scr_dyn_builtin_stream.c
+++ b/packages/runtime/src/scr_dyn_builtin_stream.c
@@ -140,9 +140,11 @@ static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); }
/* Also the capsule's tag. */
static void bcs_capsule_release(void *p) { scr_stream_release(p); }
-static bool bcs_stream_vt(const ScrVt *vt) {
- return (vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
- (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post);
+/* Whether `stream`, boxed as an "object:" typed ref, is a runtime stream. */
+static bool bcs_stream_vt(const ScrStream *stream) {
+ const ScrVt *vt = scr_stream_vt_of(stream);
+ return vt != NULL && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
+ (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post));
}
static void bcs_rec_release(void *p);
@@ -154,7 +156,7 @@ static ScrStream *bcs_of(const ScrDyn *value) {
if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) {
ScrStream *stream = value->v.typed_ref.ptr;
- return bcs_stream_vt(stream->vt) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL;
+ return bcs_stream_vt(stream) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL;
}
if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL;
ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN);
@@ -184,7 +186,7 @@ static ScrStream *bcs_native_of(const ScrDyn *value) {
memcmp(value->v.typed_ref.type_key, "object:", 7) != 0)
return NULL;
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
- if (bcs_stream_vt(stream->vt)) return stream;
+ if (bcs_stream_vt(stream)) return stream;
value->v.typed_ref.release(stream);
return NULL;
}
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index 9746e8eb1..4b0ef7d1c 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -8567,6 +8567,34 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) {
return "unknown";
}
+static SCR_TL ScrDyn *scr_dyn_fallback_value;
+static SCR_TL bool scr_dyn_fallback_registered;
+
+static void scr_dyn_fallback_cleanup(void) {
+ scr_dyn_release(scr_dyn_fallback_value);
+ scr_dyn_fallback_value = NULL;
+}
+
+bool scr_dyn_check_recover(ScrDyn *d) {
+ if (!scr_exc_pending() || scr_exc_active->kind == SCR_EXC_TERMINATE) return false;
+ scr_exc_clear();
+ if (!scr_dyn_fallback_registered) {
+ scr_dyn_fallback_registered = true;
+ scr_atexit(scr_dyn_fallback_cleanup);
+ }
+ scr_dyn_release(scr_dyn_fallback_value);
+ scr_dyn_fallback_value = scr_dyn_retain(d);
+ return true;
+}
+
+bool scr_dyn_check_fallback_taken(void) { return scr_dyn_fallback_value != NULL; }
+
+ScrDyn *scr_dyn_check_fallback_take(void) {
+ ScrDyn *value = scr_dyn_fallback_value;
+ scr_dyn_fallback_value = NULL;
+ return value;
+}
+
void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got) {
ScrJsonBuf b;
scr_jb_init(&b);
diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c
index 374b5469a..cb9bd8a23 100644
--- a/packages/runtime/src/scr_message.c
+++ b/packages/runtime/src/scr_message.c
@@ -282,7 +282,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
/* Published values (immortal) travel by pointer: a typed value as a
* capsule over the same object, a dyn graph as itself. */
if (input->kind == SCR_DYN_TYPED_REF && input->v.typed_ref.ptr &&
- *(const size_t *)input->v.typed_ref.ptr == SIZE_MAX) {
+ SCR_RC_COUNT(input->v.typed_ref.ptr) == SCR_RC_IMMORTAL) {
scr_sa_thread_shares();
encoder->visits[id].view = (ScrDyn *)input;
ScrMessageNode *node = &encoder->message->nodes[id];
diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c
index e95f8767d..5bd4b198a 100644
--- a/packages/runtime/src/scr_publish.c
+++ b/packages/runtime/src/scr_publish.c
@@ -37,9 +37,14 @@
typedef struct {
uintptr_t obj;
uint32_t rc;
- uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0 */
+ uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0;
+ * SCR_PUB_LOG_COUNT: `rc` is the count of the low half */
} ScrPubLog;
+/* In ScrPubLog.buf: the object keeps its class id in the high half of
+ * its rc word (SCR_PUB_CLASS_ID), and `rc` is the count of the low half. */
+#define SCR_PUB_LOG_COUNT UINT32_C(0x80000000)
+
#define SCR_PUB_DYN ((uintptr_t)1)
#define SCR_PUB_RC_LIFETIME UINT32_C(0xfffffffe)
#define SCR_PUB_RC_BIG UINT32_C(0xffffffff)
@@ -103,7 +108,7 @@ static void *scr_pub_grow(void *p, size_t *cap, size_t size, void *inline_buf) {
void scr_pub_str_fn(void *v, ScrPub *ctx);
void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) {
- if (!v || *(size_t *)v == SIZE_MAX || ctx->refusal) return;
+ if (!v || SCR_RC_COUNT(v) == SCR_RC_IMMORTAL || ctx->refusal) return;
if (fn == scr_pub_str_fn) { /* a leaf: mark it now */
scr_pub_str_fn(v, ctx);
return;
@@ -120,7 +125,9 @@ void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) {
* touch the objects again. */
bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
size_t *rc = obj;
- if (*rc == SIZE_MAX || ctx->refusal) return false;
+ if (SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL || ctx->refusal) return false;
+ bool class_id = (kind & SCR_PUB_CLASS_ID) != 0;
+ kind &= ~SCR_PUB_CLASS_ID;
if (ctx->logged == ctx->log_cap)
ctx->log = scr_pub_grow(ctx->log, &ctx->log_cap, sizeof *ctx->log, ctx->log_inline);
#ifdef SCR_RC_AUDIT
@@ -135,10 +142,11 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
buf = scr_cyc_pub_take(obj) + 1;
ctx->taken++;
}
- size_t count = *rc;
+ size_t count = class_id ? SCR_RC_COUNT(obj) : *rc;
uint32_t logged = (uint32_t)count;
+ if (class_id) buf |= SCR_PUB_LOG_COUNT;
#if SIZE_MAX > UINT32_MAX
- if (count >= SCR_PUB_RC_LIFETIME) {
+ if (!class_id && count >= SCR_PUB_RC_LIFETIME) {
if (count == (size_t)1 << 63) logged = SCR_PUB_RC_LIFETIME;
else {
if (ctx->nbig == ctx->big_cap) ctx->big = scr_pub_grow(ctx->big, &ctx->big_cap, sizeof *ctx->big, NULL);
@@ -148,7 +156,8 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
}
#endif
*e = (ScrPubLog){(uintptr_t)obj, logged, buf};
- *rc = SIZE_MAX;
+ if (class_id) SCR_RC_COUNT(obj) = SCR_RC_IMMORTAL;
+ else *rc = SIZE_MAX;
return true;
}
@@ -373,7 +382,7 @@ static bool scr_pub_keep_ok(size_t bytes) {
}
void scr_publish(void *root, ScrPubFn fn) {
- if (!root || *(size_t *)root == SIZE_MAX) return;
+ if (!root || SCR_RC_COUNT(root) == SCR_RC_IMMORTAL) return;
ScrPub ctx;
if (scr_pub_keep.stack) {
ctx.stack = scr_pub_keep.stack;
@@ -418,13 +427,18 @@ void scr_publish(void *root, ScrPubFn fn) {
for (size_t i = ctx.logged; i-- > 0;) {
const ScrPubLog *e = &ctx.log[i];
void *obj = scr_pub_log_obj(e);
- size_t rc = e->rc;
+ uint32_t buf = e->buf & ~SCR_PUB_LOG_COUNT;
+ if (e->buf & SCR_PUB_LOG_COUNT) {
+ SCR_RC_COUNT(obj) = (ScrRcCount)e->rc;
+ } else {
+ size_t rc = e->rc;
#if SIZE_MAX > UINT32_MAX
- if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63;
- else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig];
+ if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63;
+ else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig];
#endif
- *(size_t *)obj = rc;
- if (e->buf) scr_cyc_pub_restore(obj, e->buf - 1);
+ *(size_t *)obj = rc;
+ }
+ if (buf) scr_cyc_pub_restore(obj, buf - 1);
}
if (ctx.taken) scr_cyc_pub_fill();
scr_throw_error_msg(SCR_ERR_TYPE, ctx.refusal, strlen(ctx.refusal));
diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h
index 8611741da..10eb96d18 100644
--- a/packages/runtime/src/scr_rc.h
+++ b/packages/runtime/src/scr_rc.h
@@ -6,6 +6,41 @@
#include "scr_core.h"
+/* ── reference counts ─────────────────────────────────────────────────
+ * Every heap object's first member is its `size_t rc` word. On 64-bit
+ * targets the reference count is the word's low 32 bits: an emitted
+ * instance of a class in a program-rooted hierarchy keeps its class id (its
+ * class's preorder number + 1) in the high 32 bits (llvm/classes.ts
+ * classIdRcWord), and no other object's count reaches 2^32. A count of SCR_RC_IMMORTAL marks
+ * an immortal object; for every object but those class instances the whole
+ * word is then SIZE_MAX. Code that reads or writes the count of an object of
+ * any kind goes through SCR_RC_COUNT. */
+#if SIZE_MAX > UINT32_MAX
+typedef uint32_t ScrRcCount;
+#ifdef __BYTE_ORDER__
+_Static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__,
+ "the reference count is the low half of the rc word, at offset 0");
+#endif
+#else
+typedef size_t ScrRcCount;
+#endif
+#define SCR_RC_IMMORTAL ((ScrRcCount)-1)
+#define SCR_RC_COUNT(obj) (*(ScrRcCount *)(obj))
+
+/* The class id in the high half of `obj`'s rc word, or 0 for an object
+ * without one: the high half of any other object's word is 0, or 0xffffffff
+ * (immortal) or 0x80000000 (program-lifetime), and a class id is never one of
+ * these. */
+static inline uint32_t scr_rc_class_id(const void *obj) {
+#if SIZE_MAX > UINT32_MAX
+ uint32_t high = (uint32_t)(*(const size_t *)obj >> 32);
+ return high == UINT32_MAX || high == UINT32_C(0x80000000) ? 0 : high;
+#else
+ (void)obj;
+ return 0;
+#endif
+}
+
/* ── cycle collection (scr_cycle.c) ───────────────────────────────────
* Reference counting alone cannot free cycles, so every object that can
* participate in one — capture boxes, heap closures, unions, promises, and
@@ -591,6 +626,10 @@ enum {
SCR_PUB_KIND_OBJECT = 4,
SCR_PUB_KIND_UNION = 5,
SCR_PUB_KIND_DYN = 6,
+ /* Or'ed into a kind: an object whose rc word keeps its class id in
+ * the high half (SCR_RC_COUNT). The mark, and a refusal's restore, write
+ * only the count. */
+ SCR_PUB_CLASS_ID = 0x100,
};
void scr_publish(void *root, ScrPubFn fn);
void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn);
diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c
index 75d4bb74b..455b23dbb 100644
--- a/packages/runtime/src/scr_region.c
+++ b/packages/runtime/src/scr_region.c
@@ -666,11 +666,11 @@ static void scr_rgn_queue(ScrRgnWalk *w, size_t entry, void *p, int kind, void (
* region counted as explained. RC-audit builds still enter it, and stop if
* the walk reaches it twice. */
static void scr_rgn_enter(ScrRgnWalk *w, void *p, int kind, void (*walk)(void *, ScrRgnWalk *)) {
- if (!p || *(size_t *)p == SIZE_MAX) return; /* immortal: never freed, not walked */
+ if (!p || SCR_RC_COUNT(p) == SCR_RC_IMMORTAL) return; /* immortal: never freed, not walked */
#ifdef SCR_RGN_DEBUG
- bool single = w->src == 0 && *(size_t *)p == 1 && !w->debug;
+ bool single = w->src == 0 && SCR_RC_COUNT(p) == 1 && !w->debug;
#else
- bool single = w->src == 0 && *(size_t *)p == 1;
+ bool single = w->src == 0 && SCR_RC_COUNT(p) == 1;
#endif
#ifndef SCR_RC_AUDIT
if (single) {
@@ -923,7 +923,7 @@ static int64_t scr_rgn_settle(ScrRgnWalk *w, size_t *black_out) {
size_t top = 0;
for (size_t i = 0; i < n; i++) {
ScrRgnEntry *x = &w->ents[i];
- if (*(size_t *)x->obj != (size_t)x->found) {
+ if (SCR_RC_COUNT(x->obj) != (ScrRcCount)x->found) {
x->black = true;
stack[top++] = i;
}
@@ -1042,7 +1042,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex
}
for (size_t i = 0; i < w->nents; i++) {
const ScrRgnEntry *x = &w->ents[i];
- if (*(size_t *)x->obj == (size_t)x->found) continue; /* not referenced from outside itself */
+ if (SCR_RC_COUNT(x->obj) == (ScrRcCount)x->found) continue; /* not referenced from outside itself */
if (printed == lines) {
more++;
continue;
@@ -1056,7 +1056,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex
}
const ScrRgnFrom *f = &w->from[at];
fprintf(stderr, " held by %s.%s, with %zu references, %lld of them from what the region owns\n",
- f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", *(size_t *)x->obj,
+ f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", (size_t)SCR_RC_COUNT(x->obj),
(long long)x->found);
}
if (more) fprintf(stderr, " %zu more\n", more);
@@ -1112,7 +1112,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r, bool tentative) {
for (ScrRgnChunk *c = r->chunks; c; c = c->next) c->found = 0;
size_t objects = 0;
SCR_RGN_EACH(r, s, o) {
- if (*(size_t *)o == SIZE_MAX) {
+ if (SCR_RC_COUNT(o) == SCR_RC_IMMORTAL) {
/* Published: other threads may hold it, uncounted. */
r->state = SCR_RGN_PINNED;
t->stats.pinned++;
diff --git a/packages/runtime/src/scr_slots.c b/packages/runtime/src/scr_slots.c
index 7c5bc7f88..84236c0d5 100644
--- a/packages/runtime/src/scr_slots.c
+++ b/packages/runtime/src/scr_slots.c
@@ -29,7 +29,7 @@ static uint32_t scr_slots_index(void *obj, uint32_t *word) {
if (index != 0) return index;
index = atomic_fetch_add_explicit(&scr_slots_next, 1, memory_order_relaxed);
if (index == 0) scr_trap("scriptc: ThreadSlots ran out of slot indices\n");
- if (*(size_t *)obj != SIZE_MAX) {
+ if (SCR_RC_COUNT(obj) != SCR_RC_IMMORTAL) {
__atomic_store_n(word, index, __ATOMIC_RELAXED);
return index;
}
diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c
index 7c501f20c..7d7ec04ed 100644
--- a/packages/runtime/src/scr_stream.c
+++ b/packages/runtime/src/scr_stream.c
@@ -4740,12 +4740,14 @@ ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
- const ScrVt *vt = stream->vt;
- bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt ||
- scr_vt_within(vt, &scr_duplex_vt);
- bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt)
- : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt));
- if (ok) return stream;
+ const ScrVt *vt = scr_stream_vt_of(stream);
+ if (vt) {
+ bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt ||
+ scr_vt_within(vt, &scr_duplex_vt);
+ bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt)
+ : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt));
+ if (ok) return stream;
+ }
value->v.typed_ref.release(stream);
}
static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering";
@@ -6411,9 +6413,9 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
- const ScrVt *vt = stream->vt;
- if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
- (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream;
+ const ScrVt *vt = scr_stream_vt_of(stream);
+ if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
+ (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream;
value->v.typed_ref.release(stream);
}
scr_dyn_arg_type_fail("destination", "an instance of Stream", value);
@@ -6716,9 +6718,9 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
- const ScrVt *vt = stream->vt;
- if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
- (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream;
+ const ScrVt *vt = scr_stream_vt_of(stream);
+ if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
+ (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream;
value->v.typed_ref.release(stream);
}
scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020");
diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h
index 78290d8c4..99f58331c 100644
--- a/packages/runtime/src/scr_stream.h
+++ b/packages/runtime/src/scr_stream.h
@@ -48,6 +48,13 @@ typedef struct ScrStream {
ScrStreamState *st;
} ScrStream;
+/* The vtable of an object boxed as an "object:" typed ref, which may be any
+ * program class instance: NULL for one with a class id in its rc word
+ * (scr_rc.h), which has no vtable word and is no stream. */
+static inline const ScrVt *scr_stream_vt_of(const ScrStream *s) {
+ return scr_rc_class_id(s) == 0 ? s->vt : NULL;
+}
+
extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt,
scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */
diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c
index 0859c4e07..11dcf6ee2 100644
--- a/packages/runtime/src/scr_stream_dyn.c
+++ b/packages/runtime/src/scr_stream_dyn.c
@@ -23,7 +23,11 @@ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) {
memcmp(value->v.typed_ref.type_key, "object:", 7) != 0)
return NULL;
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
- const ScrVt *vt = stream->vt;
+ const ScrVt *vt = scr_stream_vt_of(stream);
+ if (vt == NULL) {
+ value->v.typed_ref.release(stream);
+ return NULL;
+ }
bool duplex = scr_stream_dyn_within(vt, &scr_duplex_vt) ||
scr_stream_dyn_within(vt, &scr_transform_vt) ||
scr_stream_dyn_within(vt, &scr_passthrough_vt);
diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c
index 77c020ee0..2b4752b34 100644
--- a/packages/runtime/src/scr_string.c
+++ b/packages/runtime/src/scr_string.c
@@ -1846,9 +1846,10 @@ static uint16_t *scr_str_units(const ScrStr *s, size_t *n) {
* UTF-16 units to bytes through the cursor and checkpoints. Once its
* reads would have paid for converting it (one read per 16 bytes), a
* receiver up to SCR_SIDX_UNITS_MAX units keeps its code units in its
- * entry and reads become loads. The array is freed with the entry, and an
- * append drops it (scr_sidx_concat_append). */
-#define SCR_SIDX_UNITS_MAX ((size_t)1 << 20)
+ * entry and reads become loads. The bound admits source texts of several
+ * megabytes, which scanners read in full. The array is freed with the
+ * entry, and an append drops it (scr_sidx_concat_append). */
+#define SCR_SIDX_UNITS_MAX ((size_t)1 << 24)
static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) {
if (!e->units) {
if (++e->reads < 32 || e->reads * 16 < s->len || s->len > SCR_SIDX_UNITS_MAX) return false;
diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h
index 6ed2e09df..1f25b8766 100644
--- a/packages/runtime/src/scr_string_methods.h
+++ b/packages/runtime/src/scr_string_methods.h
@@ -27,7 +27,8 @@ double scr_str_utf16_len(ScrStr *s);
* holds (scr_string.c), published so emitted length and charCodeAt reads of
* that string are a compare and a load. Cleared whenever its entry drops
* the units (release, append, eviction); all fields zero otherwise. Emitted
- * code reads it only where SCR_TL is empty. */
+ * code reads it where SCR_TL is empty, and in worker executables on ELF
+ * targets, whose thread-locals live in the executable's static TLS block. */
typedef struct ScrStrScan {
const ScrStr *s;
const uint16_t *units;
diff --git a/scripts/bench-runtime.mjs b/scripts/bench-runtime.mjs
index 4e4f2c245..61134fa4c 100644
--- a/scripts/bench-runtime.mjs
+++ b/scripts/bench-runtime.mjs
@@ -1,6 +1,6 @@
/* Runtime performance A/B harness for compiled programs.
*
- * Builds every workload in benchmarks/runtime/workloads.json with one or two
+ * Builds every workload in benchmarks/runtime/workloads/ with one or two
* scriptc checkouts, verifies that each executable's output is identical to
* Node's (stdout, stderr, exit status), then times interleaved launches and reports
* per-workload medians with bootstrap confidence intervals for the
@@ -23,6 +23,7 @@ import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
+import { readWorkloads, workloadCategory } from "../benchmarks/runtime/workloads.mjs";
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
const suiteRoot = join(repoRoot, "benchmarks/runtime");
@@ -62,7 +63,7 @@ evenly over those layouts, so verdicts cover code-placement luck instead of one
placement.
Use it for small codegen changes (e.g. --layouts=4 --runs=16).
-Workload names are listed in benchmarks/runtime/workloads.json; for example
+Workload names are the file names in benchmarks/runtime/workloads/*.json; for example
--workloads=cli-config,http-api runs only the startup and server workloads.
--baseline-optimization/--candidate-optimization pick each contender's
@@ -88,13 +89,13 @@ assert.ok(
"--layouts needs macOS or Linux",
);
-const manifest = JSON.parse(readFileSync(join(suiteRoot, "workloads.json"), "utf8"));
+const allWorkloads = readWorkloads(suiteRoot);
const selected = values.workloads ? new Set(values.workloads.split(",")) : null;
-const workloads = manifest.workloads.filter((w) => selected === null || selected.has(w.name));
+const workloads = allWorkloads.filter((w) => selected === null || selected.has(w.name));
if (selected) {
for (const name of selected)
assert.ok(
- manifest.workloads.some((w) => w.name === name),
+ allWorkloads.some((w) => w.name === name),
`unknown workload ${name}`,
);
}
@@ -488,8 +489,6 @@ const report = {
/* Workload kinds: "process" (default) times whole launches; `launches` > 1
* makes each sample the median of that many sequential launches (startup-bound
* CLIs). "server" times the measured phase of a load-client session. */
-const categoryOf = (w) =>
- w.kind === "server" ? "server" : (w.launches ?? 1) > 1 ? "startup" : "cpu";
for (const w of workloads) {
assert.ok((w.kind ?? "process") === "process" || w.kind === "server", `${w.name}: unknown kind`);
if (w.kind === "server") assert.ok(w.client?.script, `${w.name}: server workloads need a client`);
@@ -569,7 +568,7 @@ try {
const entry = { name: w.name, args, results: {} };
if (w.kind === "server") entry.kind = "server";
if ((w.launches ?? 1) > 1) entry.launches = w.launches;
- entry.category = categoryOf(w);
+ entry.category = workloadCategory(w);
const oracleFailure = server ? serverFailure(oracle) : oracle.timedOut ? "timed out" : "";
if (oracle.status !== 0 || oracleFailure) {
entry.error = `node oracle failed (status ${oracle.status}${oracleFailure ? `, ${oracleFailure}` : ""}): ${oracle.stderr.slice(0, 2000)}`;
diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs
index de3c0d0fc..1575fc5ee 100644
--- a/scripts/zero-cost-gate.mjs
+++ b/scripts/zero-cost-gate.mjs
@@ -27,6 +27,7 @@ import { availableParallelism } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
+import { readWorkloads } from "../benchmarks/runtime/workloads.mjs";
import {
cachegrindCounts,
compareArtifact,
@@ -173,9 +174,8 @@ function goodCodeSet() {
const programs = [];
if (sets.has("bench")) {
const suite = join(programsRoot, "benchmarks/runtime");
- const manifest = JSON.parse(readFileSync(join(suite, "workloads.json"), "utf8"));
const seen = new Set();
- for (const w of manifest.workloads) {
+ for (const w of readWorkloads(suite)) {
const entry = join(suite, w.entry);
if (seen.has(entry)) continue;
seen.add(entry);
diff --git a/tests/corpus/6412-any-valued-dictionaries.ts b/tests/corpus/6412-any-valued-dictionaries.ts
new file mode 100644
index 000000000..0cbdc6aad
--- /dev/null
+++ b/tests/corpus/6412-any-valued-dictionaries.ts
@@ -0,0 +1,50 @@
+// Message types whose members are typed with `any`: string-keyed
+// dictionaries of `any` values (`{ [key: string]: any }`, a `Dictionary` alias used bare), and `any[]` lists, also optional, nested in
+// arrays of records. Values keep whatever the program or JSON stored.
+type Dictionary = { [key: string]: T };
+
+interface Build {
+ use: string;
+ src?: string;
+ config?: Dictionary;
+}
+interface Order {
+ id: string;
+ builds?: Build[];
+ upload: { url: string; fields: { [key: string]: any } };
+ tags: any[];
+ extra?: any[];
+}
+
+function tags(): Dictionary {
+ return { a: 1, b: "x", c: [1, 2] };
+}
+async function meta(): Promise {
+ return { n: 3 };
+}
+
+async function main(): Promise {
+ const order: Order = JSON.parse(
+ '{"id":"o1","builds":[{"use":"node","config":{"zero":true,"n":2}},{"use":"static","src":"a/**"}],' +
+ '"upload":{"url":"u","fields":{"k":"v","n":2}},"tags":[1,"two",null]}',
+ );
+ console.log(order.id, order.upload.url, String(order.upload.fields.k), order.upload.fields["n"] + 1);
+ for (const build of order.builds ?? []) {
+ console.log(build.use, build.src ?? "-", JSON.stringify(build.config ?? null));
+ }
+ console.log(order.tags.length, order.extra === undefined, JSON.stringify(order.tags));
+
+ const t = tags();
+ for (const k of Object.keys(t)) console.log(k, JSON.stringify(t[k]));
+ t.d = { deep: true };
+ console.log(JSON.stringify(t));
+ const m = await meta();
+ console.log(m.n * 2, JSON.stringify(m));
+
+ const made: Order = { id: "o2", upload: { url: "w", fields: {} }, tags: [], extra: [0] };
+ made.upload.fields.signature = "s";
+ made.tags.push({ k: 1 });
+ console.log(JSON.stringify(made));
+}
+void main();
diff --git a/tests/corpus/6413-sdk-declared-data-class/main.ts b/tests/corpus/6413-sdk-declared-data-class/main.ts
new file mode 100644
index 000000000..32509da57
--- /dev/null
+++ b/tests/corpus/6413-sdk-declared-data-class/main.ts
@@ -0,0 +1,27 @@
+// A types-only SDK package declares the shape of JSON data as a class with
+// fields only (`declare class UploadForm { url: string; fields: ... }`,
+// with a merged namespace holding the fields interface). The program never
+// constructs it: values arrive through JSON.parse or object literals, so
+// they compile as records of the declared fields.
+import type { UploadForm } from "upload-sdk";
+
+interface Order {
+ id: string;
+ upload?: UploadForm;
+ forms: UploadForm[];
+}
+
+function describe(form: UploadForm | undefined): string {
+ if (!form) return "none";
+ return `${form.url} ${form.fields.Policy} ${form.fields["X-Signature"]} ${form.fields["key"] ?? "-"} ${Object.keys(form.fields).join(",")}`;
+}
+
+const order = JSON.parse(
+ '{"id":"o1","upload":{"url":"u","fields":{"Policy":"p","X-Signature":"s","key":"k"}},"forms":[]}',
+) as Order;
+console.log(order.id, describe(order.upload));
+const made: UploadForm = { url: "v", fields: { Policy: "q", "X-Signature": "t" } };
+order.forms.push(made);
+made.fields.extra = "e";
+console.log(describe(made), JSON.stringify(order.forms));
+console.log(describe(undefined));
diff --git a/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts
new file mode 100644
index 000000000..5c72897ab
--- /dev/null
+++ b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts
@@ -0,0 +1,13 @@
+export declare class UploadForm {
+ /** The URL the form posts to. */
+ url: string;
+ /** The hidden form fields. */
+ fields: UploadForm.Fields;
+}
+export declare namespace UploadForm {
+ interface Fields {
+ Policy: string;
+ "X-Signature": string;
+ [key: string]: string;
+ }
+}
diff --git a/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json
new file mode 100644
index 000000000..093d34b91
--- /dev/null
+++ b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json
@@ -0,0 +1 @@
+{ "name": "upload-sdk", "version": "1.0.0", "types": "index.d.ts" }
diff --git a/tests/corpus/6414-pick-class-method-views.ts b/tests/corpus/6414-pick-class-method-views.ts
new file mode 100644
index 000000000..6bbbbf780
--- /dev/null
+++ b/tests/corpus/6414-pick-class-method-views.ts
@@ -0,0 +1,83 @@
+// `Pick` and `Omit` over a class, used as structural parameter
+// and field types: a view of the instance's own members. Calls through the
+// view run the instance's methods on the instance itself, so its state is
+// shared with every other reference.
+class ApiClient {
+ private readonly base: string;
+ calls = 0;
+ constructor(base: string) {
+ this.base = base;
+ }
+ request(path: string): Promise {
+ this.calls++;
+ return Promise.resolve(this.base + path);
+ }
+ close(): void {
+ console.log("closed after", this.calls);
+ }
+}
+
+interface Context {
+ client: Pick;
+ name: string;
+}
+
+async function run(ctx: Context): Promise {
+ console.log(ctx.name, await ctx.client.request("/x"), await ctx.client.request("/y"));
+}
+function finish(client: Pick): void {
+ client.close();
+}
+function count(client: Omit): number {
+ return client.calls;
+}
+
+const client = new ApiClient("https://h");
+void run({ client, name: "ctx" }).then(() => {
+ finish(client);
+ console.log(count(client), client.calls);
+});
+
+// A live view that flows into another structural type keeps the instance:
+// reads through the second view see the instance's later writes.
+interface Counter {
+ calls: number;
+}
+function counted(view: Pick): Counter {
+ void view.request("/z");
+ return view;
+}
+const second = new ApiClient("https://k");
+const counter = counted(second);
+second.calls += 10;
+const holder: { counter: Counter } = { counter: counted(second) };
+second.calls = 100;
+console.log("counter", counter.calls, holder.counter.calls, second.calls);
+
+// A view also accepts other classes that have its members. Their instances
+// flow through it the same way.
+class Meter {
+ label = "meter";
+ reads = 0;
+ read(): number {
+ return this.reads;
+ }
+}
+class Gauge {
+ reads = 0;
+ read(): number {
+ return this.reads;
+ }
+}
+interface Reading {
+ reads: number;
+}
+function reading(view: Pick): Reading {
+ console.log("read", view.read());
+ return view;
+}
+const gauge = new Gauge();
+const result = reading(gauge);
+gauge.reads = 50;
+const meter = new Meter();
+console.log("reading", result.reads, reading(meter).reads, meter.label);
diff --git a/tests/corpus/6419-untyped-async-package/main.ts b/tests/corpus/6419-untyped-async-package/main.ts
new file mode 100644
index 000000000..9ec10f7c5
--- /dev/null
+++ b/tests/corpus/6419-untyped-async-package/main.ts
@@ -0,0 +1,38 @@
+// Untyped async methods of a package, reached through the package's own
+// untyped callers and prototype reflection, and overridden by a typed
+// subclass. An async generator method called from the package answers its
+// generator object.
+import { Pool, Schema, limiter, listIssues, prototypeNames, validateAll } from "token-pool";
+
+class CountingPool extends Pool {
+ acquired = 0;
+ async acquire(): Promise {
+ await super.acquire();
+ this.acquired++;
+ }
+}
+
+async function main(): Promise {
+ const pool = new CountingPool(1);
+ await pool.acquire();
+ const waiting = pool.acquire();
+ console.log("waiting", pool.nrWaiting());
+ pool.release("t");
+ await waiting;
+ console.log("acquired", pool.acquired, pool.nrWaiting());
+ const base = new Pool(1);
+ console.log("token", await base.acquire());
+ const run = limiter(1);
+ console.log(await Promise.all([run(async () => 1), run(async () => 2)]));
+ const schema = new Schema((value) => typeof value === "number");
+ console.log(await validateAll(schema, [1, "x", 3]));
+ console.log(await listIssues(schema, ["p", 5, "q"]));
+ console.log(await schema.parseAsync(4));
+ try {
+ await schema.parseAsync("y");
+ } catch (e) {
+ console.log("rejected", e instanceof Error ? e.message : String(e));
+ }
+ console.log(prototypeNames());
+}
+main();
diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts
new file mode 100644
index 000000000..ed0c82579
--- /dev/null
+++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts
@@ -0,0 +1,17 @@
+export declare class Pool {
+ constructor(size: number);
+ tryAcquire(): any;
+ acquire(): Promise;
+ release(token?: any): void;
+ nrWaiting(): number;
+}
+export declare function limiter(size: number): (work: () => Promise) => Promise;
+export declare class Schema {
+ constructor(check: (value: unknown) => boolean);
+ safeParseAsync(data: unknown): Promise;
+ parseAsync(data: unknown): Promise;
+ issues(values: unknown[]): AsyncGenerator;
+}
+export declare function validateAll(schema: Schema, values: unknown[]): Promise;
+export declare function listIssues(schema: Schema, values: unknown[]): Promise;
+export declare function prototypeNames(): string;
diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js
new file mode 100644
index 000000000..04935ff29
--- /dev/null
+++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js
@@ -0,0 +1,123 @@
+"use strict";
+// An untyped token pool and schema in the shape of a semaphore and a
+// validation library: async methods whose results come from storage that
+// passes through untyped helpers.
+Object.defineProperty(exports, "__esModule", { value: true });
+function moveItems(src, srcIndex, dst, dstIndex, len) {
+ for (let j = 0; j < len; ++j) {
+ dst[j + dstIndex] = src[j + srcIndex];
+ src[j + srcIndex] = void 0;
+ }
+}
+class Queue {
+ constructor() {
+ this._length = 0;
+ this.arr = [];
+ }
+ push(item) {
+ if (this._length > 64) moveItems(this.arr, 1, this.arr, 0, this._length - 1);
+ this.arr[this._length] = item;
+ this._length = this._length + 1;
+ }
+ pop() {
+ const length = this._length;
+ if (length === 0) return void 0;
+ const ret = this.arr[length - 1];
+ this.arr[length - 1] = void 0;
+ this._length = length - 1;
+ return ret;
+ }
+ shift() {
+ const length = this._length;
+ if (length === 0) return void 0;
+ const ret = this.arr[0];
+ for (let i = 1; i < length; i++) this.arr[i - 1] = this.arr[i];
+ this.arr[length - 1] = void 0;
+ this._length = length - 1;
+ return ret;
+ }
+ get length() {
+ return this._length;
+ }
+}
+class Pool {
+ constructor(size) {
+ this.free = new Queue();
+ this.waiting = new Queue();
+ for (let i = 0; i < size; i++) this.free.push("token" + i);
+ }
+ tryAcquire() {
+ return this.free.pop();
+ }
+ async acquire() {
+ const token = this.tryAcquire();
+ if (token !== void 0) return token;
+ return new Promise((resolve) => {
+ this.waiting.push(resolve);
+ });
+ }
+ release(token) {
+ const next = this.waiting.shift();
+ if (next) next(token);
+ else this.free.push(token);
+ }
+ nrWaiting() {
+ return this.waiting.length;
+ }
+}
+exports.Pool = Pool;
+function limiter(size) {
+ const pool = new Pool(size);
+ return async function limited(work) {
+ const token = await pool.acquire();
+ try {
+ return await work();
+ } finally {
+ pool.release(token);
+ }
+ };
+}
+exports.limiter = limiter;
+class Schema {
+ constructor(check) {
+ this.check = check;
+ this.results = new Queue();
+ }
+ async safeParseAsync(data) {
+ await null;
+ this.results.push(this.check(data) ? { success: true, data } : { success: false, issue: "invalid " + data });
+ return this.results.pop();
+ }
+ async *issues(values) {
+ for (const value of values) {
+ const result = await this.safeParseAsync(value);
+ if (!result.success) yield result.issue;
+ }
+ }
+ async parseAsync(data) {
+ const result = await this.safeParseAsync(data);
+ if (result.success) return result.data;
+ throw new Error(result.issue);
+ }
+}
+exports.Schema = Schema;
+async function validateAll(schema, values) {
+ const out = [];
+ for (const value of values) {
+ const result = await schema.safeParseAsync(value);
+ out.push(result.success ? "ok " + result.data : result.issue);
+ }
+ return out.join(", ");
+}
+exports.validateAll = validateAll;
+async function listIssues(schema, values) {
+ const out = [];
+ const it = schema.issues(values);
+ for (let step = await it.next(); !step.done; step = await it.next()) out.push(step.value);
+ return out.join(", ");
+}
+exports.listIssues = listIssues;
+function prototypeNames() {
+ return typeof Schema.prototype.parseAsync + " " + typeof Schema.prototype.safeParseAsync;
+}
+exports.prototypeNames = prototypeNames;
diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json
new file mode 100644
index 000000000..994de07b2
--- /dev/null
+++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json
@@ -0,0 +1 @@
+{ "name": "token-pool", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/6494-default-export-class-members/lib/date.js b/tests/corpus/6494-default-export-class-members/lib/date.js
new file mode 100644
index 000000000..0c4a426f7
--- /dev/null
+++ b/tests/corpus/6494-default-export-class-members/lib/date.js
@@ -0,0 +1,8 @@
+// A class whose base scriptc cannot extend yet: the default export object
+// below narrows this member away, and only reads of it would fence.
+export class StampDate extends Date {
+ constructor(text) {
+ super(text);
+ this.label = "stamp:" + text;
+ }
+}
diff --git a/tests/corpus/6494-default-export-class-members/lib/error.js b/tests/corpus/6494-default-export-class-members/lib/error.js
new file mode 100644
index 000000000..7f2ac5744
--- /dev/null
+++ b/tests/corpus/6494-default-export-class-members/lib/error.js
@@ -0,0 +1,7 @@
+export class ParseError extends Error {
+ constructor(message, line) {
+ super(message);
+ this.name = "ParseError";
+ this.line = line;
+ }
+}
diff --git a/tests/corpus/6494-default-export-class-members/lib/index.js b/tests/corpus/6494-default-export-class-members/lib/index.js
new file mode 100644
index 000000000..66935fac6
--- /dev/null
+++ b/tests/corpus/6494-default-export-class-members/lib/index.js
@@ -0,0 +1,14 @@
+import { StampDate } from "./date.js";
+import { ParseError } from "./error.js";
+function parse(text) {
+ if (text === "") throw new ParseError("empty document", 1);
+ const [key, value] = text.split("=").map((part) => part.trim());
+ return { key, value };
+}
+function stringify(value) {
+ return JSON.stringify(value);
+}
+// A default export object of functions and class values (the shorthand
+// spelling of `{ ParseError: ParseError }`).
+export default { parse, stringify, StampDate, ParseError };
+export { parse, stringify, StampDate, ParseError };
diff --git a/tests/corpus/6494-default-export-class-members/main.ts b/tests/corpus/6494-default-export-class-members/main.ts
new file mode 100644
index 000000000..1548e6415
--- /dev/null
+++ b/tests/corpus/6494-default-export-class-members/main.ts
@@ -0,0 +1,15 @@
+// A JavaScript module's default export object holding functions and class
+// values builds at module initialization, and its members work through
+// both the default object and the named exports.
+import lib, { parse, ParseError } from "./lib/index.js";
+
+console.log(JSON.stringify(parse("a = 1")));
+console.log(lib.stringify(lib.parse("b = 2")));
+try {
+ lib.parse("");
+} catch (e) {
+ if (e instanceof ParseError) console.log(e.name, e.message, e.line);
+}
+const Ctor = lib.ParseError;
+const made = new Ctor("built through the default object", 7);
+console.log(made instanceof ParseError, made.message, made.line, Ctor === ParseError);
diff --git a/tests/corpus/6494-default-export-class-members/package.json b/tests/corpus/6494-default-export-class-members/package.json
new file mode 100644
index 000000000..ca00e63c9
--- /dev/null
+++ b/tests/corpus/6494-default-export-class-members/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "default-export-class-members-corpus",
+ "private": true,
+ "type": "module"
+}
diff --git a/tests/corpus/6495-generic-method-overloads.ts b/tests/corpus/6495-generic-method-overloads.ts
new file mode 100644
index 000000000..34bcfa712
--- /dev/null
+++ b/tests/corpus/6495-generic-method-overloads.ts
@@ -0,0 +1,54 @@
+// Calls that resolve to OVERLOAD signatures of a generic method or
+// function: the implementation monomorphizes under what the overload
+// proves for its type parameters (a parameter the overload never mentions
+// is unknown), directly and through a `Pick` view of the class, whose
+// live receiver keeps the instance's state. `T | Promise` callbacks
+// bind T from either arm.
+interface Reply {
+ code: number;
+ text: string;
+}
+type Mapper = (reply: Reply) => T | Promise;
+
+class Store {
+ loads = 0;
+ async load(key: string, map: Mapper): Promise;
+ async load(key: string): Promise;
+ async load(key: string, map?: Mapper): Promise {
+ this.loads++;
+ const reply: Reply = { code: 200, text: key };
+ if (map === undefined) return reply;
+ return await map(reply);
+ }
+}
+
+interface Services {
+ store: Pick;
+}
+
+function pick(items: T[], index: number): T;
+function pick(items: T[]): T;
+function pick(items: T[], index = 0): T {
+ return items[index]!;
+}
+
+async function settle(reply: Reply, map?: Mapper): Promise {
+ return map ? await map(reply) : reply;
+}
+
+async function main(): Promise {
+ const store = new Store();
+ const plain = await store.load("alpha");
+ console.log(plain.code, plain.text);
+ const length = await store.load("beta", (reply) => reply.text.length);
+ console.log(length * 2);
+ const services: Services = { store };
+ const viaView = await services.store.load("gamma");
+ const upper = await services.store.load("delta", async (reply) => reply.text.toUpperCase());
+ console.log(viaView.text, upper, store.loads);
+ console.log(pick(["x", "y"]), pick([1, 2, 3], 2));
+ console.log(JSON.stringify(await settle({ code: 1, text: "s" })));
+ console.log(await settle({ code: 2, text: "tt" }, (reply) => reply.code + reply.text.length));
+}
+
+main();
diff --git a/tests/diagnostics/overload-fences.ts b/tests/corpus/6496-overload-signatures.ts
similarity index 66%
rename from tests/diagnostics/overload-fences.ts
rename to tests/corpus/6496-overload-signatures.ts
index 0cf1f7e15..5902d8c19 100644
--- a/tests/diagnostics/overload-fences.ts
+++ b/tests/corpus/6496-overload-signatures.ts
@@ -1,7 +1,7 @@
-// The overload fences that REMAIN now that overload signatures lower:
-// generic overload calls (the per-instantiation body would be typed
-// against a signature it was never checked under), and the multi-
-// signature type surfacing where a binding needs it spelled.
+// Overload signatures: a generic overload call monomorphizes the
+// implementation under the bindings the overload proves (a parameter the
+// overload omits is optional in the implementation), and an immutable
+// alias of an overloaded declaration projects onto the implementation.
function twice(x: T): T[];
function twice(x: T, n: number): T[];
function twice(x: T, n?: number): T[] {
diff --git a/tests/corpus/6497-generic-overload-bindings.ts b/tests/corpus/6497-generic-overload-bindings.ts
new file mode 100644
index 000000000..0d33acaae
--- /dev/null
+++ b/tests/corpus/6497-generic-overload-bindings.ts
@@ -0,0 +1,47 @@
+// Bindings of generic overload calls: explicit type arguments name the
+// overload's type parameters, which can be declared in another order than
+// the implementation's, and keyof-constrained parameters bound to
+// different literals through an overload read their own fields.
+function tag(value: A, label: B): string;
+function tag(value: A, label?: B): string {
+ return `${typeof value}:${typeof label}:${String(value)}${String(label)}`;
+}
+console.log(tag(5, "x"));
+console.log(tag("y", true));
+
+class Labels {
+ prefix = "#";
+ render(value: A, label: B): string;
+ render(value: A, label?: B): string {
+ return `${this.prefix}${typeof value}/${typeof label}`;
+ }
+}
+console.log(new Labels().render("z", 7));
+
+interface Row {
+ a: number;
+ b: number;
+ name: string;
+}
+function read(row: T, key: K): T[K];
+function read(row: T, key: K, fallback: T[K]): T[K];
+function read(row: T, key: K, fallback?: T[K]): T[K] {
+ const value = row[key];
+ return value === undefined ? fallback! : value;
+}
+const row: Row = { a: 1, b: 2, name: "r" };
+console.log(read(row, "a"), read(row, "b"), read(row, "name"));
+console.log(read(row, "b", 0) + read(row, "a", 0));
+
+// An overload call whose implementation returns an indexed read that
+// misses returns undefined, as the implementation's own calls do.
+function at(items: T[], index: number): T;
+function at(items: T[], index: number, label: string): T;
+function at(items: T[], index: number, label?: string): T {
+ if (label !== undefined) console.log(label);
+ return items[index];
+}
+console.log(at([1, 2, 3], 1), at([1, 2, 3], 5));
+console.log(at(["a", "b"], 0, "strings"), at(["a", "b"], 9, "strings"));
+const missing = at([{ id: 1 }], 3);
+console.log(missing === undefined, typeof missing);
diff --git a/tests/corpus/6550-npm-boolean-bitwise/main.ts b/tests/corpus/6550-npm-boolean-bitwise/main.ts
new file mode 100644
index 000000000..a08502343
--- /dev/null
+++ b/tests/corpus/6550-npm-boolean-bitwise/main.ts
@@ -0,0 +1,15 @@
+// Bitwise operators over booleans in a JavaScript dependency convert them
+// with ToNumber, true to 1 and false to 0, as Node does (async-sema's
+// `isFn(pauseFn) ^ isFn(resumeFn)` option check).
+import { Sema, bits } from 'token-sema';
+
+const plain = new Sema(3);
+const paired = new Sema(2, { pauseFn: () => {}, resumeFn: () => {} });
+console.log(plain.capacity, plain.pausable, paired.capacity, paired.pausable);
+try {
+ new Sema(1, { pauseFn: () => {} });
+} catch (error) {
+ console.log((error as Error).message);
+}
+console.log(bits([1, 0, 3]));
+console.log(bits([]));
diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts
new file mode 100644
index 000000000..de028a027
--- /dev/null
+++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts
@@ -0,0 +1,6 @@
+export declare class Sema {
+ constructor(capacity: number, options?: { pauseFn?: () => void; resumeFn?: () => void });
+ readonly capacity: number;
+ readonly pausable: number;
+}
+export declare function bits(values: number[]): string;
diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js
new file mode 100644
index 000000000..1efaf449c
--- /dev/null
+++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js
@@ -0,0 +1,35 @@
+// The option check from async-sema (MIT): pauseFn and resumeFn must be given
+// together, tested with `^` over two booleans. Bitwise operators convert
+// booleans with ToNumber.
+'use strict';
+function isFn(x) {
+ return typeof x == 'function';
+}
+class Sema {
+ constructor(capacity, { pauseFn, resumeFn } = {}) {
+ if (isFn(pauseFn) ^ isFn(resumeFn)) throw new Error('pauseFn and resumeFn must be both set for pausing');
+ this.capacity = capacity;
+ this.pausable = isFn(pauseFn) & isFn(resumeFn);
+ }
+}
+function bits(values) {
+ var yes = values.length > 0;
+ var no = values.length < 0;
+ var flags = 0;
+ var count = 0;
+ for (var v of values) {
+ flags = flags | (v > 0);
+ count = count + ((v > 1) & 1);
+ }
+ // The right operand assigns the variable the left one reads; the left
+ // operand reads it first.
+ var seen = values.length > 0;
+ var toggled = seen ^ (seen = !seen);
+ var shifted = seen << (seen = true);
+ return [
+ yes ^ no, yes ^ yes, yes & no, yes | no, no | no, yes & 3, 2 | yes,
+ yes << 3, yes >> 0, no >>> 0, -1 >>> yes, 5 ^ yes, no ^ 7,
+ typeof (yes ^ no), flags, count, toggled, shifted, seen,
+ ].join(' ');
+}
+module.exports = { Sema: Sema, bits: bits };
diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json
new file mode 100644
index 000000000..7d74609a7
--- /dev/null
+++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json
@@ -0,0 +1 @@
+{ "name": "token-sema", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/6560-npm-js-capability-probes/main.ts b/tests/corpus/6560-npm-js-capability-probes/main.ts
new file mode 100644
index 000000000..e9eacee8e
--- /dev/null
+++ b/tests/corpus/6560-npm-js-capability-probes/main.ts
@@ -0,0 +1,14 @@
+// JavaScript dependency forms from bundled zip packages, which run as Node
+// runs them:
+// - a var table replaced by an Int32Array view of itself (buffer-crc32);
+// - function declarations that redefine themselves on the first call, and
+// Buffer.from over an array with an undefined encoding (yazl);
+// - the deprecated `new Buffer(...)` fallback both keep, and the Buffer
+// methods called on the untyped buffers they return.
+import { crc32, localHeader, signature, legacy } from 'zip-helpers';
+
+console.log(crc32('hello'), crc32(Buffer.from([1, 2, 3])), crc32('world', 17), crc32(4));
+console.log(Array.from(localHeader('a.txt', 9)).join(','));
+console.log(Array.from(localHeader('b', 1)).join(','));
+console.log(Array.from(signature()).join(','));
+console.log(legacy());
diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts
new file mode 100644
index 000000000..0b51fe366
--- /dev/null
+++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts
@@ -0,0 +1,4 @@
+export declare function crc32(input: Uint8Array | string | number, previous?: number): number;
+export declare function localHeader(name: string, size: number): Uint8Array;
+export declare function signature(): Uint8Array;
+export declare function legacy(): string;
diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js
new file mode 100644
index 000000000..15fc0a0c4
--- /dev/null
+++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js
@@ -0,0 +1,125 @@
+// Small parts of buffer-crc32 (MIT) and yazl (MIT), in their published
+// shapes: a var table replaced by an Int32Array view of itself, and
+// function declarations that replace themselves with the implementation
+// that works on the first call.
+'use strict';
+var Buffer2 = require('buffer').Buffer;
+var CRC_TABLE = [
+ 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035,
+ 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049,
+ 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639,
+ 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317,
+ 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443,
+ 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665,
+ 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303,
+ 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565,
+ 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059,
+ 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297,
+ 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223,
+ 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405,
+ 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995,
+ 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649,
+ 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015,
+ 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989,
+ 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523,
+ 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377,
+ 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879,
+ 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637,
+ 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859,
+ 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161,
+ 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815,
+ 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221,
+ 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371,
+ 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881,
+ 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567,
+ 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701,
+ 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035,
+ 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897,
+ 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431,
+ 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117
+];
+typeof Int32Array < 'u' && (CRC_TABLE = new Int32Array(CRC_TABLE));
+
+function ensureBuffer(input) {
+ if (Buffer2.isBuffer(input)) return input;
+ var hasNewBufferAPI = typeof Buffer2.alloc == 'function' && typeof Buffer2.from == 'function';
+ if (typeof input == 'number') return hasNewBufferAPI ? Buffer2.alloc(input) : new Buffer2(input);
+ if (typeof input == 'string') return hasNewBufferAPI ? Buffer2.from(input) : new Buffer2(input);
+ throw new Error('input must be buffer, number, or string, received ' + typeof input);
+}
+
+function crc32(buf, previous) {
+ buf = ensureBuffer(buf);
+ for (var crc = ~~previous ^ -1, n = 0; n < buf.length; n++) crc = CRC_TABLE[(crc ^ buf[n]) & 255] ^ (crc >>> 8);
+ return (crc ^ -1) >>> 0;
+}
+
+function bufferFrom(something, encoding) {
+ bufferFrom = modern;
+ try {
+ return bufferFrom(something, encoding);
+ } catch {
+ return (bufferFrom = legacy), bufferFrom(something, encoding);
+ }
+ function modern(something2, encoding2) {
+ return Buffer.from(something2, encoding2);
+ }
+ function legacy(something2, encoding2) {
+ return new Buffer(something2, encoding2);
+ }
+}
+
+function bufferAlloc(size) {
+ bufferAlloc = modern;
+ try {
+ return bufferAlloc(size);
+ } catch {
+ return (bufferAlloc = legacy), bufferAlloc(size);
+ }
+ function modern(size2) {
+ return Buffer.alloc(size2);
+ }
+ function legacy(size2) {
+ return new Buffer(size2);
+ }
+}
+
+function localHeader(name, size) {
+ var nameBuffer = bufferFrom(name, 'utf-8');
+ var fixed = bufferAlloc(8);
+ fixed.writeUInt32LE(0x04034b50, 0);
+ fixed.writeUInt16LE(nameBuffer.length, 4);
+ fixed.writeUInt16LE(size, 6);
+ return Buffer.concat([fixed, nameBuffer]);
+}
+
+var SIGNATURE = bufferFrom([0x50, 0x4b, 0x05, 0x06], undefined);
+
+function legacy() {
+ var sized = new Buffer2(3);
+ var text = new Buffer2('hi', 'utf8');
+ var array = new Buffer2([1, 2, 255]);
+ var out = [sized.length, sized[0], text.toString(), array.readUInt16BE(1), array.readInt8(2)];
+ try {
+ new Buffer2(2, 'utf8');
+ } catch (error) {
+ out.push(error.code);
+ }
+ try {
+ array.writeUInt8(300, 0);
+ } catch (error) {
+ out.push(error.code);
+ }
+ try {
+ array.readUInt8('0');
+ } catch (error) {
+ out.push(error.code);
+ }
+ return out.join(' ');
+}
+
+function signature() {
+ return SIGNATURE;
+}
+
+module.exports = { crc32: crc32, localHeader: localHeader, signature: signature, legacy: legacy };
diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json
new file mode 100644
index 000000000..2192addcf
--- /dev/null
+++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json
@@ -0,0 +1 @@
+{ "name": "zip-helpers", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/6565-rebindable-function-import-cycle/main.ts b/tests/corpus/6565-rebindable-function-import-cycle/main.ts
new file mode 100644
index 000000000..ff505d978
--- /dev/null
+++ b/tests/corpus/6565-rebindable-function-import-cycle/main.ts
@@ -0,0 +1,5 @@
+// A package whose import cycle calls a reassigned function declaration
+// while the cycle initializes.
+import { report } from "cycle-format";
+
+for (const line of report()) console.log(line);
diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js
new file mode 100644
index 000000000..730e7b34e
--- /dev/null
+++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js
@@ -0,0 +1,8 @@
+import { format } from "./index.js";
+
+// Runs before index.js's body: format is the hoisted declaration.
+export const early = format("a");
+
+export function describe(value) {
+ return `${typeof format} ${format(value)}`;
+}
diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts
new file mode 100644
index 000000000..55a683bb7
--- /dev/null
+++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts
@@ -0,0 +1,2 @@
+export declare function format(value: string): string;
+export declare const report: () => string[];
diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js
new file mode 100644
index 000000000..c96fadcad
--- /dev/null
+++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js
@@ -0,0 +1,18 @@
+// A reassigned function declaration of a module in an import cycle: the
+// dependency calls it while the cycle initializes, before this module's
+// body runs, and gets the hoisted declaration; later calls see the
+// replacement.
+import { early, describe } from "./dep.js";
+
+export function format(value) {
+ format = function formatUpper(text) {
+ return `[${String(text).toUpperCase()}]`;
+ };
+ return `<${value}>`;
+}
+
+export const report = () => [
+ "early: " + early,
+ "now: " + format("b") + " " + format("c"),
+ "dep: " + describe("d"),
+];
diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json
new file mode 100644
index 000000000..becff6bc6
--- /dev/null
+++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json
@@ -0,0 +1 @@
+{ "name": "cycle-format", "version": "1.0.0", "type": "module", "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } } }
diff --git a/tests/corpus/6570-npm-builtin-function-bind/main.ts b/tests/corpus/6570-npm-builtin-function-bind/main.ts
new file mode 100644
index 000000000..f04dc2053
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/main.ts
@@ -0,0 +1,26 @@
+// Function.prototype.bind, call and apply on builtin static functions in
+// JavaScript dependencies. The vendored truncate-utf8-bytes 1.0.2 (WTFPL)
+// binds Buffer.byteLength at load time and calls the bound function per
+// character; bound-builtins binds Buffer, Math, String and JSON statics.
+// fs.realpath 1.0.0 (ISC) and value-forms read builtin functions as values:
+// fs.realpathSync stored and compared, Buffer.alloc aliased, and a String
+// method read for a feature probe.
+import truncate from 'truncate-utf8-bytes';
+import { report, badLength } from 'bound-builtins';
+import realpath from 'fs.realpath';
+import { report as valueForms, more as moreValueForms } from 'value-forms';
+
+console.log(truncate('hello world', 5), truncate('héllo', 2), truncate('héllo', 3));
+console.log(JSON.stringify(truncate('a😀b', 3)), JSON.stringify(truncate('a😀b', 5)), truncate('short', 99));
+try {
+ (truncate as unknown as (value: unknown, n: number) => string)(42, 1);
+} catch (error) {
+ console.log((error as Error).message);
+}
+for (const value of report()) console.log(value);
+console.log(badLength());
+
+console.log(realpath.realpathSync('/'), realpath.sync('/'));
+realpath('/', (error, resolved) => console.log('async', error, resolved));
+for (const value of valueForms()) console.log(value);
+for (const value of moreValueForms()) console.log(value);
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts
new file mode 100644
index 000000000..9810c8bfc
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts
@@ -0,0 +1,2 @@
+export declare function report(): unknown[];
+export declare function badLength(): string;
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js
new file mode 100644
index 000000000..7cc668957
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js
@@ -0,0 +1,42 @@
+// Function.prototype.bind, call and apply on builtin static functions in
+// JavaScript: Buffer, Math, String and JSON.
+'use strict';
+var byteLength = Buffer.byteLength.bind(Buffer);
+var max = Math.max.bind(null, 10);
+var fromCodes = String.fromCharCode.bind(String, 104);
+var stringify = JSON.stringify.bind(JSON);
+var parse = JSON.parse.bind(JSON);
+var compare = Buffer.compare.bind(Buffer);
+
+exports.report = function report() {
+ return [
+ byteLength('héllo'),
+ byteLength('héllo', 'latin1'),
+ byteLength('abcd', 'hex'),
+ byteLength('héllo', 'not-an-encoding'),
+ byteLength(''),
+ byteLength(Buffer.from([1, 2, 3])),
+ max(3, 42),
+ max(),
+ Math.min.call(null, 4, 2),
+ Math.abs.apply(null, [-7]),
+ fromCodes(105, 33),
+ String.fromCharCode.apply(null, [111, 107]),
+ stringify({ a: [1, 2] }),
+ stringify({ b: 1 }, null, 2),
+ compare(Buffer.from('a'), Buffer.from('b')),
+ parse('{"n":1}').n,
+ JSON.stringify(parse('[1,2]', function (key, value) {
+ return typeof value === 'number' ? value * 10 : value;
+ })),
+ ];
+};
+
+exports.badLength = function badLength() {
+ try {
+ byteLength(42);
+ return 'no error';
+ } catch (error) {
+ return error.code + ' ' + error.name;
+ }
+};
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json
new file mode 100644
index 000000000..dd7b59369
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json
@@ -0,0 +1 @@
+{ "name": "bound-builtins", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE
new file mode 100644
index 000000000..5bd884c25
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE
@@ -0,0 +1,43 @@
+The ISC License
+
+Copyright (c) Isaac Z. Schlueter and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+----
+
+This library bundles a version of the `fs.realpath` and `fs.realpathSync`
+methods from Node.js v0.10 under the terms of the Node.js MIT license.
+
+Node's license follows, also included at the header of `old.js` which contains
+the licensed code:
+
+ Copyright Joyent, Inc. and other Node contributors.
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts
new file mode 100644
index 000000000..308c19e6c
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts
@@ -0,0 +1,6 @@
+declare function realpath(p: string, cb: (error: Error | null, resolved?: string) => void): void;
+declare namespace realpath {
+ function realpathSync(p: string, cache?: object): string;
+ function sync(p: string, cache?: object): string;
+}
+export = realpath;
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js
new file mode 100644
index 000000000..b09c7c7e6
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js
@@ -0,0 +1,66 @@
+module.exports = realpath
+realpath.realpath = realpath
+realpath.sync = realpathSync
+realpath.realpathSync = realpathSync
+realpath.monkeypatch = monkeypatch
+realpath.unmonkeypatch = unmonkeypatch
+
+var fs = require('fs')
+var origRealpath = fs.realpath
+var origRealpathSync = fs.realpathSync
+
+var version = process.version
+var ok = /^v[0-5]\./.test(version)
+var old = require('./old.js')
+
+function newError (er) {
+ return er && er.syscall === 'realpath' && (
+ er.code === 'ELOOP' ||
+ er.code === 'ENOMEM' ||
+ er.code === 'ENAMETOOLONG'
+ )
+}
+
+function realpath (p, cache, cb) {
+ if (ok) {
+ return origRealpath(p, cache, cb)
+ }
+
+ if (typeof cache === 'function') {
+ cb = cache
+ cache = null
+ }
+ origRealpath(p, cache, function (er, result) {
+ if (newError(er)) {
+ old.realpath(p, cache, cb)
+ } else {
+ cb(er, result)
+ }
+ })
+}
+
+function realpathSync (p, cache) {
+ if (ok) {
+ return origRealpathSync(p, cache)
+ }
+
+ try {
+ return origRealpathSync(p, cache)
+ } catch (er) {
+ if (newError(er)) {
+ return old.realpathSync(p, cache)
+ } else {
+ throw er
+ }
+ }
+}
+
+function monkeypatch () {
+ fs.realpath = realpath
+ fs.realpathSync = realpathSync
+}
+
+function unmonkeypatch () {
+ fs.realpath = origRealpath
+ fs.realpathSync = origRealpathSync
+}
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js
new file mode 100644
index 000000000..b40305e73
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js
@@ -0,0 +1,303 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var pathModule = require('path');
+var isWindows = process.platform === 'win32';
+var fs = require('fs');
+
+// JavaScript implementation of realpath, ported from node pre-v6
+
+var DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG);
+
+function rethrow() {
+ // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and
+ // is fairly slow to generate.
+ var callback;
+ if (DEBUG) {
+ var backtrace = new Error;
+ callback = debugCallback;
+ } else
+ callback = missingCallback;
+
+ return callback;
+
+ function debugCallback(err) {
+ if (err) {
+ backtrace.message = err.message;
+ err = backtrace;
+ missingCallback(err);
+ }
+ }
+
+ function missingCallback(err) {
+ if (err) {
+ if (process.throwDeprecation)
+ throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs
+ else if (!process.noDeprecation) {
+ var msg = 'fs: missing callback ' + (err.stack || err.message);
+ if (process.traceDeprecation)
+ console.trace(msg);
+ else
+ console.error(msg);
+ }
+ }
+ }
+}
+
+function maybeCallback(cb) {
+ return typeof cb === 'function' ? cb : rethrow();
+}
+
+var normalize = pathModule.normalize;
+
+// Regexp that finds the next partion of a (partial) path
+// result is [base_with_slash, base], e.g. ['somedir/', 'somedir']
+if (isWindows) {
+ var nextPartRe = /(.*?)(?:[\/\\]+|$)/g;
+} else {
+ var nextPartRe = /(.*?)(?:[\/]+|$)/g;
+}
+
+// Regex to find the device root, including trailing slash. E.g. 'c:\\'.
+if (isWindows) {
+ var splitRootRe = /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/;
+} else {
+ var splitRootRe = /^[\/]*/;
+}
+
+exports.realpathSync = function realpathSync(p, cache) {
+ // make p is absolute
+ p = pathModule.resolve(p);
+
+ if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
+ return cache[p];
+ }
+
+ var original = p,
+ seenLinks = {},
+ knownHard = {};
+
+ // current character position in p
+ var pos;
+ // the partial path so far, including a trailing slash if any
+ var current;
+ // the partial path without a trailing slash (except when pointing at a root)
+ var base;
+ // the partial path scanned in the previous round, with slash
+ var previous;
+
+ start();
+
+ function start() {
+ // Skip over roots
+ var m = splitRootRe.exec(p);
+ pos = m[0].length;
+ current = m[0];
+ base = m[0];
+ previous = '';
+
+ // On windows, check that the root exists. On unix there is no need.
+ if (isWindows && !knownHard[base]) {
+ fs.lstatSync(base);
+ knownHard[base] = true;
+ }
+ }
+
+ // walk down the path, swapping out linked pathparts for their real
+ // values
+ // NB: p.length changes.
+ while (pos < p.length) {
+ // find the next part
+ nextPartRe.lastIndex = pos;
+ var result = nextPartRe.exec(p);
+ previous = current;
+ current += result[0];
+ base = previous + result[1];
+ pos = nextPartRe.lastIndex;
+
+ // continue if not a symlink
+ if (knownHard[base] || (cache && cache[base] === base)) {
+ continue;
+ }
+
+ var resolvedLink;
+ if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
+ // some known symbolic link. no need to stat again.
+ resolvedLink = cache[base];
+ } else {
+ var stat = fs.lstatSync(base);
+ if (!stat.isSymbolicLink()) {
+ knownHard[base] = true;
+ if (cache) cache[base] = base;
+ continue;
+ }
+
+ // read the link if it wasn't read before
+ // dev/ino always return 0 on windows, so skip the check.
+ var linkTarget = null;
+ if (!isWindows) {
+ var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
+ if (seenLinks.hasOwnProperty(id)) {
+ linkTarget = seenLinks[id];
+ }
+ }
+ if (linkTarget === null) {
+ fs.statSync(base);
+ linkTarget = fs.readlinkSync(base);
+ }
+ resolvedLink = pathModule.resolve(previous, linkTarget);
+ // track this, if given a cache.
+ if (cache) cache[base] = resolvedLink;
+ if (!isWindows) seenLinks[id] = linkTarget;
+ }
+
+ // resolve the link, then start over
+ p = pathModule.resolve(resolvedLink, p.slice(pos));
+ start();
+ }
+
+ if (cache) cache[original] = p;
+
+ return p;
+};
+
+
+exports.realpath = function realpath(p, cache, cb) {
+ if (typeof cb !== 'function') {
+ cb = maybeCallback(cache);
+ cache = null;
+ }
+
+ // make p is absolute
+ p = pathModule.resolve(p);
+
+ if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
+ return process.nextTick(cb.bind(null, null, cache[p]));
+ }
+
+ var original = p,
+ seenLinks = {},
+ knownHard = {};
+
+ // current character position in p
+ var pos;
+ // the partial path so far, including a trailing slash if any
+ var current;
+ // the partial path without a trailing slash (except when pointing at a root)
+ var base;
+ // the partial path scanned in the previous round, with slash
+ var previous;
+
+ start();
+
+ function start() {
+ // Skip over roots
+ var m = splitRootRe.exec(p);
+ pos = m[0].length;
+ current = m[0];
+ base = m[0];
+ previous = '';
+
+ // On windows, check that the root exists. On unix there is no need.
+ if (isWindows && !knownHard[base]) {
+ fs.lstat(base, function(err) {
+ if (err) return cb(err);
+ knownHard[base] = true;
+ LOOP();
+ });
+ } else {
+ process.nextTick(LOOP);
+ }
+ }
+
+ // walk down the path, swapping out linked pathparts for their real
+ // values
+ function LOOP() {
+ // stop if scanned past end of path
+ if (pos >= p.length) {
+ if (cache) cache[original] = p;
+ return cb(null, p);
+ }
+
+ // find the next part
+ nextPartRe.lastIndex = pos;
+ var result = nextPartRe.exec(p);
+ previous = current;
+ current += result[0];
+ base = previous + result[1];
+ pos = nextPartRe.lastIndex;
+
+ // continue if not a symlink
+ if (knownHard[base] || (cache && cache[base] === base)) {
+ return process.nextTick(LOOP);
+ }
+
+ if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
+ // known symbolic link. no need to stat again.
+ return gotResolvedLink(cache[base]);
+ }
+
+ return fs.lstat(base, gotStat);
+ }
+
+ function gotStat(err, stat) {
+ if (err) return cb(err);
+
+ // if not a symlink, skip to the next path part
+ if (!stat.isSymbolicLink()) {
+ knownHard[base] = true;
+ if (cache) cache[base] = base;
+ return process.nextTick(LOOP);
+ }
+
+ // stat & read the link if not read before
+ // call gotTarget as soon as the link target is known
+ // dev/ino always return 0 on windows, so skip the check.
+ if (!isWindows) {
+ var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
+ if (seenLinks.hasOwnProperty(id)) {
+ return gotTarget(null, seenLinks[id], base);
+ }
+ }
+ fs.stat(base, function(err) {
+ if (err) return cb(err);
+
+ fs.readlink(base, function(err, target) {
+ if (!isWindows) seenLinks[id] = target;
+ gotTarget(err, target);
+ });
+ });
+ }
+
+ function gotTarget(err, target, base) {
+ if (err) return cb(err);
+
+ var resolvedLink = pathModule.resolve(previous, target);
+ if (cache) cache[base] = resolvedLink;
+ gotResolvedLink(resolvedLink);
+ }
+
+ function gotResolvedLink(resolvedLink) {
+ // resolve the link, then start over
+ p = pathModule.resolve(resolvedLink, p.slice(pos));
+ start();
+ }
+};
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json
new file mode 100644
index 000000000..9adf9cdf3
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json
@@ -0,0 +1 @@
+{ "name": "fs.realpath", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "license": "ISC" }
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts
new file mode 100644
index 000000000..2d051a0b8
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts
@@ -0,0 +1,2 @@
+declare function truncate(string: string, byteLength: number): string;
+export = truncate;
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js
new file mode 100644
index 000000000..39e899c37
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js
@@ -0,0 +1,5 @@
+'use strict';
+
+var truncate = require("./lib/truncate");
+var getLength = Buffer.byteLength.bind(Buffer);
+module.exports = truncate.bind(null, getLength);
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js
new file mode 100644
index 000000000..7837a4e11
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js
@@ -0,0 +1,42 @@
+'use strict';
+
+function isHighSurrogate(codePoint) {
+ return codePoint >= 0xd800 && codePoint <= 0xdbff;
+}
+
+function isLowSurrogate(codePoint) {
+ return codePoint >= 0xdc00 && codePoint <= 0xdfff;
+}
+
+// Truncate string by size in bytes
+module.exports = function truncate(getLength, string, byteLength) {
+ if (typeof string !== "string") {
+ throw new Error("Input must be string");
+ }
+
+ var charLength = string.length;
+ var curByteLength = 0;
+ var codePoint;
+ var segment;
+
+ for (var i = 0; i < charLength; i += 1) {
+ codePoint = string.charCodeAt(i);
+ segment = string[i];
+
+ if (isHighSurrogate(codePoint) && isLowSurrogate(string.charCodeAt(i + 1))) {
+ i += 1;
+ segment += string[i];
+ }
+
+ curByteLength += getLength(segment);
+
+ if (curByteLength === byteLength) {
+ return string.slice(0, i + 1);
+ }
+ else if (curByteLength > byteLength) {
+ return string.slice(0, i - segment.length + 1);
+ }
+ }
+
+ return string;
+};
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json
new file mode 100644
index 000000000..05cd0181a
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "truncate-utf8-bytes",
+ "version": "1.0.2",
+ "description": "Truncate string to given length in bytes",
+ "main": "index.js",
+ "types": "index.d.ts",
+ "license": "WTFPL"
+}
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts
new file mode 100644
index 000000000..d2d676c10
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts
@@ -0,0 +1,2 @@
+export declare function report(): unknown[];
+export declare function more(): string[];
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js
new file mode 100644
index 000000000..8144f66b9
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js
@@ -0,0 +1,74 @@
+// Builtin functions read as values: tar-stream's `var alloc = Buffer.alloc`,
+// fs.realpathSync stored and compared, and smol-toml's `''.isWellFormed`
+// feature probe.
+'use strict';
+var fs = require('fs');
+var alloc = Buffer.alloc;
+var origRealpathSync = fs.realpathSync;
+var HAS_WELLFORMED = !!''.isWellFormed;
+
+exports.report = function report() {
+ var header = alloc(8);
+ var filled = alloc(5, 'ab');
+ var numbered = alloc(3, 7);
+ var out = [];
+ out.push(header.length, header[0], filled.toString(), numbered[2], typeof alloc, alloc.length);
+ try {
+ alloc('8');
+ } catch (error) {
+ out.push(error.code);
+ }
+ out.push(origRealpathSync('/'), origRealpathSync === fs.realpathSync, typeof fs.realpathSync.native);
+ out.push(Buffer.isBuffer(origRealpathSync('/', 'buffer')), origRealpathSync('/', { encoding: 'hex' }));
+ out.push(fs.realpathSync.native('/'));
+ try {
+ origRealpathSync('/definitely/missing/path');
+ } catch (error) {
+ out.push(error.code);
+ }
+ out.push(HAS_WELLFORMED, 'ok'.isWellFormed(), typeof ''.toUpperCase);
+ return out;
+};
+
+// The stored alloc with each fill form, and the stored realpathSync and
+// realpathSync.native with file URLs.
+var realpathNative = fs.realpathSync.native;
+var url = require('url');
+exports.more = function more() {
+ var out = [];
+ function attempt(label, fn) {
+ try {
+ var value = fn();
+ out.push(label + ' ' + (Buffer.isBuffer(value) ? value.toString('hex') : value));
+ } catch (error) {
+ out.push(label + ' ' + error.name + ' ' + error.code + ' ' + error.message);
+ }
+ }
+ attempt('hex', function () { return alloc(4, 'ab', 'hex'); });
+ attempt('base64', function () { return alloc(4, 'YQ==', 'base64'); });
+ attempt('latin1', function () { return alloc(3, '\u00e9', 'latin1'); });
+ attempt('upper case', function () { return alloc(4, 'ab', 'UTF8'); });
+ attempt('null encoding', function () { return alloc(4, 'ab', null); });
+ attempt('buffer pattern', function () { return alloc(5, Buffer.from([1, 2, 3])); });
+ attempt('uint16 pattern', function () { return alloc(4, new Uint16Array([0x0102])); });
+ attempt('null fill', function () { return alloc(3, null); });
+ attempt('true fill', function () { return alloc(3, true); });
+ attempt('object fill', function () { return alloc(3, { valueOf: function () { return 258; } }); });
+ attempt('negative fill', function () { return alloc(2, -1); });
+ attempt('empty string', function () { return alloc(2, ''); });
+ attempt('empty buffer', function () { return alloc(2, Buffer.alloc(0)); });
+ attempt('no bytes', function () { return alloc(2, 'zz', 'hex'); });
+ attempt('unknown encoding', function () { return alloc(2, 'ab', 'nope'); });
+ attempt('number encoding', function () { return alloc(2, 'ab', 5); });
+ attempt('empty size', function () { return alloc(0, 'ab', 'nope').length; });
+ // Through call, as a wrapper forwarding its arguments passes them.
+ var root = url.pathToFileURL('/');
+ attempt('url', function () { return origRealpathSync.call(null, root); });
+ attempt('native url', function () { return realpathNative.call(null, root); });
+ attempt('http url', function () { return origRealpathSync.call(null, new URL('http://example.com/')); });
+ attempt('native http url', function () { return realpathNative.call(null, new URL('http://example.com/')); });
+ attempt('encoded slash', function () { return origRealpathSync.call(null, new URL('file:///a%2Fb')); });
+ attempt('native number', function () { return realpathNative.call(null, 5); });
+ attempt('native bytes', function () { return realpathNative.call(null, new Uint8Array(Buffer.from('/'))); });
+ return out;
+};
diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json
new file mode 100644
index 000000000..6f96b3124
--- /dev/null
+++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json
@@ -0,0 +1 @@
+{ "name": "value-forms", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/6570-require-mixed-declarations.cjs b/tests/corpus/6570-require-mixed-declarations.cjs
new file mode 100644
index 000000000..3d3e2c2fc
--- /dev/null
+++ b/tests/corpus/6570-require-mixed-declarations.cjs
@@ -0,0 +1,46 @@
+// A require declarator beside other declarators in one var statement, the
+// shape bundlers emit for a module's top level. The require binding has no
+// storage of its own, so every read resolves to the module: construction in
+// a class constructor, a class expression extending it, and calls through
+// the other builtin bindings of the same statement.
+var EventEmitter = require("events"), util = require("util"), path = require("path"), Base = class extends EventEmitter {};
+
+class Pool {
+ constructor(size) {
+ this.size = size;
+ this.events = new EventEmitter;
+ this.events.on("grow", (by) => {
+ this.size += by;
+ console.log("grow", by, util.format("%d -> %d", this.size - by, this.size));
+ });
+ }
+}
+const pool = new Pool(2);
+pool.events.emit("grow", 3);
+console.log("pool", pool.size, pool.events instanceof EventEmitter, pool.events.listenerCount("grow"));
+
+var Worker = class {
+ constructor(name) {
+ this.name = name;
+ this.bus = new Base;
+ this.bus.on("done", (file) => console.log("done", this.name, path.basename(file)));
+ }
+};
+const worker = new Worker("w1");
+worker.bus.emit("done", "/tmp/out/report.txt");
+console.log("worker", worker.bus instanceof Base, worker.bus instanceof EventEmitter, typeof Base);
+
+// A builtin require in a nested block is alias plumbing too.
+if (pool.size > 0) {
+ var Emitter = require("node:events"), label = "nested";
+}
+class Relay {
+ constructor() {
+ this.out = new Emitter();
+ this.out.once("ping", (n) => console.log(label, "ping", n));
+ }
+}
+const relay = new Relay();
+relay.out.emit("ping", 1);
+relay.out.emit("ping", 2);
+console.log("relay", relay.out.listenerCount("ping"), Emitter === EventEmitter);
diff --git a/tests/corpus/6592-promise-armed-method-values/main.ts b/tests/corpus/6592-promise-armed-method-values/main.ts
new file mode 100644
index 000000000..379c56ef9
--- /dev/null
+++ b/tests/corpus/6592-promise-armed-method-values/main.ts
@@ -0,0 +1,27 @@
+// Parse steps that answer either a status record or a promise of one keep
+// their method values: reads through untyped code, detached calls, and the
+// per-instance rebinding of the base constructor all see the function.
+import { map, number, object, record, set, stepDetached, stepKinds } from "status-schema";
+async function main(): Promise {
+ const shaped = object({ count: number(), size: number() });
+ const counts = record(number());
+ const byName = map(number());
+ const seen = set(number());
+ console.log(stepKinds(shaped), "|", stepKinds(counts), "|", stepKinds(byName), "|", stepKinds(seen));
+ console.log(shaped.safeParse({ count: 1, size: 2 }).success, shaped.safeParse({ count: "x", size: 2 }).success);
+ console.log(JSON.stringify(shaped.parse({ count: 3, size: 4 })));
+ console.log(JSON.stringify(await shaped.parseAsync({ count: 5, size: 6 })), (await shaped.spa(7)).success);
+ console.log(counts.safeParse({ a: 1, b: 2 }).success, (await counts.safeParseAsync({ a: "x" })).success);
+ console.log(byName.safeParse(new Map([["a", 1]])).success, (await byName.safeParseAsync(new Map([["b", "x"]]))).success);
+ console.log(seen.safeParse(new Set([1, 2])).success, (await seen.spa(new Set([3]))).success);
+ console.log(await stepDetached(shaped, { count: 1, size: 1 }), "|", await stepDetached(counts, { a: "x" }));
+ console.log(await stepDetached(byName, new Map([["c", 2]])), "|", await stepDetached(seen, [1]));
+ try {
+ await counts.parseAsync(null);
+ } catch (e) {
+ console.log("rejected", e instanceof Error ? e.message : String(e));
+ }
+ const nested = object({ inner: record(number()), tags: set(number()) });
+ console.log(JSON.stringify(await nested.parseAsync({ inner: { q: 1 }, tags: new Set([1]) })));
+}
+main();
diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts
new file mode 100644
index 000000000..6607bf0c7
--- /dev/null
+++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts
@@ -0,0 +1,18 @@
+export interface SafeResult {
+ success: boolean;
+ data?: unknown;
+}
+export declare class Schema {
+ safeParse(data: unknown): SafeResult;
+ parse(data: unknown): unknown;
+ safeParseAsync(data: unknown): Promise;
+ spa(data: unknown): Promise;
+ parseAsync(data: unknown): Promise;
+}
+export declare function number(): Schema;
+export declare function object(shape: { [key: string]: Schema }): Schema;
+export declare function record(valueType: Schema): Schema;
+export declare function map(valueType: Schema): Schema;
+export declare function set(valueType: Schema): Schema;
+export declare function stepKinds(schema: Schema): string;
+export declare function stepDetached(schema: Schema, data: unknown): Promise;
diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js
new file mode 100644
index 000000000..4fb5ae460
--- /dev/null
+++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js
@@ -0,0 +1,118 @@
+// A schema hierarchy in the shape of a validation library's: each parse
+// step answers a status record or, in async mode, a promise of one built
+// by a static async merge helper. The base constructor rebinds its
+// methods per instance, async ones included.
+export const INVALID = Object.freeze({ status: "aborted" });
+export class Status {
+ static async mergeAsync(pairs) {
+ const settled = [];
+ for (const pair of pairs) {
+ settled.push({ key: pair.key, value: await pair.value });
+ }
+ return Status.mergeSync(settled);
+ }
+ static mergeSync(pairs) {
+ const value = {};
+ for (const pair of pairs) {
+ if (pair.value.status === "aborted") return INVALID;
+ value[pair.key] = pair.value.value;
+ }
+ return { status: "valid", value };
+ }
+}
+export class Schema {
+ constructor(def) {
+ this.spa = this.safeParseAsync;
+ this._def = def;
+ this.parse = this.parse.bind(this);
+ this.safeParse = this.safeParse.bind(this);
+ this.parseAsync = this.parseAsync.bind(this);
+ this.safeParseAsync = this.safeParseAsync.bind(this);
+ this.spa = this.spa.bind(this);
+ }
+ safeParse(data) {
+ const result = this._parse({ data, async: false });
+ if (result instanceof Promise) throw new Error("async parse step in safeParse");
+ return result.status === "valid" ? { success: true, data: result.value } : { success: false };
+ }
+ parse(data) {
+ const result = this.safeParse(data);
+ if (result.success) return result.data;
+ throw new Error(this._def.name + ": invalid input");
+ }
+ async safeParseAsync(data) {
+ const result = await this._parse({ data, async: true });
+ return result.status === "valid" ? { success: true, data: result.value } : { success: false };
+ }
+ async parseAsync(data) {
+ const result = await this.safeParseAsync(data);
+ if (result.success) return result.data;
+ throw new Error(this._def.name + ": invalid input");
+ }
+}
+export class NumberSchema extends Schema {
+ _parse(input) {
+ return typeof input.data === "number" ? { status: "valid", value: input.data } : INVALID;
+ }
+}
+export class ObjectSchema extends Schema {
+ _parse(input) {
+ if (input.data === null || typeof input.data !== "object") return INVALID;
+ const pairs = [];
+ for (const key of Object.keys(this._def.shape)) {
+ pairs.push({ key, value: this._def.shape[key]._parse({ data: input.data[key], async: input.async }) });
+ }
+ return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs);
+ }
+}
+export class RecordSchema extends Schema {
+ _parse(input) {
+ if (input.data === null || typeof input.data !== "object") return INVALID;
+ const pairs = [];
+ for (const key in input.data) {
+ pairs.push({ key, value: this._def.valueType._parse({ data: input.data[key], async: input.async }) });
+ }
+ return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs);
+ }
+}
+export class MapSchema extends Schema {
+ _parse(input) {
+ if (!(input.data instanceof Map)) return INVALID;
+ const finish = () => {
+ const out = new Map();
+ for (const [k, v] of input.data) {
+ if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID;
+ out.set(k, v);
+ }
+ return { status: "valid", value: out };
+ };
+ return input.async ? Promise.resolve().then(finish) : finish();
+ }
+}
+export class SetSchema extends Schema {
+ _parse(input) {
+ if (!(input.data instanceof Set)) return INVALID;
+ const finish = () => {
+ const out = new Set();
+ for (const v of input.data) {
+ if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID;
+ out.add(v);
+ }
+ return { status: "valid", value: out };
+ };
+ return input.async ? Promise.resolve().then(finish) : finish();
+ }
+}
+export const number = () => new NumberSchema({ name: "number" });
+export const object = (shape) => new ObjectSchema({ name: "object", shape });
+export const record = (valueType) => new RecordSchema({ name: "record", valueType });
+export const map = (valueType) => new MapSchema({ name: "map", valueType });
+export const set = (valueType) => new SetSchema({ name: "set", valueType });
+// Untyped readers of the parse step's method value.
+export const stepKinds = (schema) => typeof schema._parse + " " + typeof Object.getPrototypeOf(schema)._parse;
+export const stepDetached = async (schema, data) => {
+ const step = schema._parse;
+ const sync = step.call(schema, { data, async: false });
+ const later = await step.call(schema, { data, async: true });
+ return sync.status + " " + later.status;
+};
diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json
new file mode 100644
index 000000000..1160994b1
--- /dev/null
+++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json
@@ -0,0 +1 @@
+{ "name": "status-schema", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/class-id-instances-as-streams.ts b/tests/corpus/class-id-instances-as-streams.ts
new file mode 100644
index 000000000..813e54ed2
--- /dev/null
+++ b/tests/corpus/class-id-instances-as-streams.ts
@@ -0,0 +1,39 @@
+// Instances of a class hierarchy, held as untyped values, where the stream
+// adapters probe for a native stream: pipe, pipeline, readline, and
+// instanceof checks against the stream classes. Their first field is a
+// number, which sits where a stream's vtable word would be.
+import { Duplex, PassThrough, Readable, Transform, Writable, pipeline } from "node:stream";
+import { createInterface } from "node:readline";
+
+class Shape {
+ size = 3.5;
+ area(): number {
+ return 0;
+ }
+}
+class Square extends Shape {
+ override area(): number {
+ return this.size * this.size;
+ }
+}
+
+function attempt(label: string, run: () => unknown): void {
+ try {
+ run();
+ console.log(label, "ok");
+ } catch (error) {
+ console.log(label, "throws", error instanceof Error);
+ }
+}
+
+const shape: any = new Square();
+try {
+ Readable.from(["a"]).pipe(shape);
+} catch (error) {
+ console.log("pipe", (error as Error).name);
+}
+attempt("pipeline", () => pipeline(shape, new PassThrough(), () => {}));
+attempt("readline-input", () => createInterface({ input: shape }));
+console.log("instanceof", shape instanceof Readable, shape instanceof Writable, shape instanceof Duplex);
+console.log("instanceof-transform", shape instanceof Transform, shape instanceof PassThrough);
+console.log("area", (shape as Square).area());
diff --git a/tests/corpus/json-parse-fallback-extra-keys.ts b/tests/corpus/json-parse-fallback-extra-keys.ts
new file mode 100644
index 000000000..7eba7b2a8
--- /dev/null
+++ b/tests/corpus/json-parse-fallback-extra-keys.ts
@@ -0,0 +1,37 @@
+// A failed checked cast keeps every member of the parsed object: extra
+// members stay visible to Object.keys, JSON.stringify, for...in and
+// util.inspect, in the source object's key order.
+interface Point {
+ x: number;
+ y: number;
+}
+
+function show(text: string): void {
+ const p = JSON.parse(text) as Point;
+ console.log(Object.keys(p).join(","));
+ console.log(Object.values(p).length, Object.entries(p).length);
+ console.log(JSON.stringify(p));
+ console.log(JSON.stringify(p, null, 2));
+ const seen: string[] = [];
+ for (const k in p) seen.push(k);
+ console.log(seen.join("|"));
+ console.log(p);
+ console.log("has z:", "z" in p, Object.hasOwn(p, "z"));
+}
+
+show('{"x":1,"y":2}');
+show('{"z":3,"x":"1","y":2,"label":{"text":"hi","size":[1,2]}}');
+show('{"x":1}');
+show('{"b":1,"a":2,"x":null,"y":[]}');
+
+// Spread copies every member of the failed value, in source order.
+function copyOf(text: string): void {
+ const p = JSON.parse(text) as Point;
+ const copy = { ...p, y: 10 };
+ console.log(copy, Object.keys(copy).join(","));
+ const pair = [p.x, p.y];
+ console.log(pair, JSON.stringify({ point: p, at: [p.x] }));
+}
+
+copyOf('{"x":1,"y":2}');
+copyOf('{"w":0,"x":"1","extra":true}');
diff --git a/tests/corpus/json-parse-fallback-flows.ts b/tests/corpus/json-parse-fallback-flows.ts
new file mode 100644
index 000000000..82232959f
--- /dev/null
+++ b/tests/corpus/json-parse-fallback-flows.ts
@@ -0,0 +1,57 @@
+// Checked-cast fallback across declaration forms and control flow: an
+// annotated declaration from an untyped value, `unknown as T`, a site in a
+// loop whose iterations alternate between fitting and failing values,
+// closures and callbacks over the failed value, and a value that fits a
+// typed parameter once it reaches it.
+interface Job {
+ name: string;
+ retries: number;
+ steps: string[];
+}
+
+function retriesOf(n: number): string {
+ return n === 1 ? "1 retry" : `${n} retries`;
+}
+
+function annotated(text: string): void {
+ const job: Job = JSON.parse(text);
+ console.log("annotated", job.name, job.retries, job.steps?.length);
+}
+
+function fromUnknown(value: unknown): void {
+ const job = value as Job;
+ console.log("unknown", job.name ?? "(unnamed)", typeof job.retries);
+}
+
+function loop(texts: string[]): void {
+ for (const text of texts) {
+ const job = JSON.parse(text) as Job;
+ const label = job.name + ":" + job.retries;
+ console.log("loop", label, typeof job.retries === "number" ? retriesOf(job.retries) : "n/a");
+ }
+}
+
+async function later(text: string): Promise {
+ const job = JSON.parse(text) as Job;
+ const steps = job.steps ?? [];
+ await new Promise((resolve) => setTimeout(resolve, 1));
+ steps.forEach((step, i) => console.log("step", i, step));
+ const upper = steps.map((s) => String(s).toUpperCase());
+ console.log("upper", upper.join(" "));
+ const describeJob = (): string => `${job.name} (${job.retries})`;
+ console.log("closure", describeJob());
+}
+
+annotated('{"name":"a","retries":1,"steps":["x"]}');
+annotated('{"name":"b","retries":"2"}');
+fromUnknown(JSON.parse('{"name":"c","retries":3,"steps":[]}'));
+fromUnknown(JSON.parse('{"retries":"3"}'));
+loop([
+ '{"name":"d","retries":1,"steps":[]}',
+ '{"name":"e","retries":"x","steps":[]}',
+ '{"name":"f","retries":2,"steps":[]}',
+ '{"name":"g"}',
+]);
+later('{"name":"h","retries":0,"steps":["s1","s2"]}').then(() =>
+ later('{"name":"i","retries":"0","steps":["s1",2]}'),
+);
diff --git a/tests/corpus/json-parse-fallback-missing.ts b/tests/corpus/json-parse-fallback-missing.ts
new file mode 100644
index 000000000..b84f8af2f
--- /dev/null
+++ b/tests/corpus/json-parse-fallback-missing.ts
@@ -0,0 +1,36 @@
+// A `JSON.parse(text) as T` whose value lacks required members keeps the
+// parsed value: missing members read undefined, as in Node.
+interface Address {
+ city: string;
+ zip: string;
+}
+interface User {
+ id: number;
+ name: string;
+ admin: boolean;
+ address: Address;
+ tags: string[];
+}
+
+function describe(text: string): void {
+ const user = JSON.parse(text) as User;
+ console.log("id", user.id, typeof user.id);
+ console.log("name", user.name, user.name === undefined);
+ console.log("admin", user.admin ? "yes" : "no", !user.admin);
+ console.log("city", user.address?.city ?? "(no city)");
+ console.log("zip?", "zip" in (user.address ?? {}));
+ console.log("tags", user.tags?.length ?? 0);
+ if (!user.address) console.log("no address");
+ else console.log("address", user.address.city, user.address.zip);
+ try {
+ console.log("street", user.address.city.length);
+ } catch (e) {
+ console.log("read threw", e instanceof TypeError, (e as Error).message);
+ }
+ console.log(`label: ${user.name}#${user.id}`);
+}
+
+describe('{"id":1,"name":"ada","admin":true,"address":{"city":"London","zip":"N1"},"tags":["a"]}');
+describe('{"id":2}');
+describe('{"name":"bob","address":{"city":"Paris"}}');
+describe("{}");
diff --git a/tests/corpus/json-parse-fallback-nested.ts b/tests/corpus/json-parse-fallback-nested.ts
new file mode 100644
index 000000000..96abddaf9
--- /dev/null
+++ b/tests/corpus/json-parse-fallback-nested.ts
@@ -0,0 +1,42 @@
+// Nested records and arrays of a value that failed its checked cast keep
+// their identity: reads, writes and comparisons reach the same objects.
+interface Line {
+ sku: string;
+ qty: number;
+}
+interface Cart {
+ id: string;
+ lines: Line[];
+ owner: { name: string; email: string };
+}
+
+function lineTotal(lines: Line[]): number {
+ let n = 0;
+ for (const line of lines) n += line.qty;
+ return n;
+}
+
+function inspectCart(text: string): void {
+ const cart = JSON.parse(text) as Cart;
+ const lines = cart.lines;
+ console.log("same lines:", lines === cart.lines, cart.owner === cart.owner);
+ lines.push({ sku: "added", qty: 1 });
+ console.log("count:", cart.lines.length, lines.length);
+ const first = lines[0];
+ first.qty = first.qty + 10;
+ console.log("first:", cart.lines[0].sku, cart.lines[0].qty);
+ console.log("skus:", lines.map((l) => l.sku).join(","));
+ console.log("big:", lines.filter((l) => l.qty > 5).length);
+ console.log("found:", lines.find((l) => l.sku === "b")?.qty ?? "none");
+ console.log("some string qty:", lines.some((l) => typeof l.qty === "string"));
+ console.log("owner:", cart.owner?.name ?? "(anonymous)", cart.owner?.email);
+ console.log(JSON.stringify(cart));
+ // A typed function receives the lines when they fit its parameter.
+ if (lines.every((l) => typeof l.qty === "number" && typeof l.sku === "string"))
+ console.log("total:", lineTotal(lines));
+ else console.log("total: skipped");
+}
+
+inspectCart('{"id":"c1","lines":[{"sku":"a","qty":1},{"sku":"b","qty":2}],"owner":{"name":"n","email":"e"}}');
+inspectCart('{"id":"c2","lines":[{"sku":"a","qty":1},{"sku":"b","qty":2}]}');
+inspectCart('{"id":3,"lines":[{"sku":"a","qty":"1"}],"owner":{"name":"n"}}');
diff --git a/tests/corpus/json-parse-fallback-partial.ts b/tests/corpus/json-parse-fallback-partial.ts
new file mode 100644
index 000000000..ec3e2c769
--- /dev/null
+++ b/tests/corpus/json-parse-fallback-partial.ts
@@ -0,0 +1,59 @@
+// `Partial` and optional members: a request whose nested required
+// members are missing is accepted the way Node accepts it (the listener
+// pattern: the binding is assigned inside try, read after it).
+interface Deployment {
+ id: string;
+ name: string;
+ billing: { plan: string };
+}
+interface Order {
+ deployment: Deployment;
+ owner: { id: string };
+}
+interface ListenerRequest {
+ buildOrder: Order;
+ traceCarrier?: Record;
+}
+
+const delivered: string[] = [];
+
+async function readBody(body: string): Promise {
+ return body;
+}
+
+async function handle(body: string): Promise {
+ const text = await readBody(body);
+ let request: Partial;
+ try {
+ request = JSON.parse(text) as Partial;
+ } catch {
+ console.log("400 invalid JSON body");
+ return 400;
+ }
+ const buildOrder = request.buildOrder;
+ const deploymentId = buildOrder?.deployment?.id;
+ if (!deploymentId) {
+ console.log("400 missing buildOrder.deployment.id");
+ return 400;
+ }
+ delivered.push(deploymentId);
+ console.log("200 accepted", deploymentId, Object.keys(request).join(","));
+ console.log("carrier:", request.traceCarrier?.traceparent ?? "(none)");
+ try {
+ console.log("plan:", buildOrder?.deployment.billing.plan);
+ } catch (e) {
+ console.log("plan threw:", (e as Error).message);
+ }
+ return 200;
+}
+
+async function main(): Promise {
+ await handle('{"buildOrder":{"deployment":{"id":"dpl_1","name":"n","billing":{"plan":"pro"}},"owner":{"id":"o"}}}');
+ await handle('{"buildOrder":{"deployment":{"id":"dpl_2"}},"traceCarrier":{"traceparent":"00-ab"}}');
+ await handle('{"buildOrder":{"deployment":{}}}');
+ await handle('{"traceCarrier":{}}');
+ await handle("not json");
+ console.log(delivered.join(","));
+}
+
+main();
diff --git a/tests/corpus/json-parse-fallback-wrong-types.ts b/tests/corpus/json-parse-fallback-wrong-types.ts
new file mode 100644
index 000000000..06ed79255
--- /dev/null
+++ b/tests/corpus/json-parse-fallback-wrong-types.ts
@@ -0,0 +1,33 @@
+// Members whose JSON type differs from the declared one keep their real
+// values and JavaScript semantics after a failed checked cast.
+interface Reading {
+ sensor: string;
+ value: number;
+ ok: boolean | number;
+ samples: number[];
+}
+
+function report(text: string): string {
+ const r = JSON.parse(text) as Reading;
+ const doubled = r.value * 2;
+ const sum = r.value + 1;
+ console.log(r.sensor, typeof r.sensor, r.value, typeof r.value, doubled, sum);
+ console.log("ok:", r.ok, typeof r.ok, r.ok === true, r.ok == 1);
+ console.log("cmp:", r.value > 10, r.value < 10, r.value === 5);
+ console.log("samples:", Array.isArray(r.samples), r.samples.length);
+ let total = 0;
+ for (const s of r.samples) total = total + s;
+ console.log("total:", total, typeof total);
+ try {
+ const parts = r.samples.map((s) => typeof s);
+ console.log("kinds:", parts.join(","));
+ } catch (e) {
+ console.log("map threw:", (e as Error).message);
+ }
+ return `${r.sensor}=${r.value}`;
+}
+
+console.log(report('{"sensor":"t1","value":5,"ok":true,"samples":[1,2,3]}'));
+console.log(report('{"sensor":7,"value":"5","ok":1,"samples":[1,"2",3]}'));
+console.log(report('{"sensor":"t3","value":null,"ok":"yes","samples":"abc"}'));
+console.log(report('{"sensor":["x"],"value":{"n":1},"ok":false,"samples":[]}'));
diff --git a/tests/diagnostics/nested-member-blocker-path.ts b/tests/diagnostics/nested-member-blocker-path.ts
new file mode 100644
index 000000000..60edd053c
--- /dev/null
+++ b/tests/diagnostics/nested-member-blocker-path.ts
@@ -0,0 +1,19 @@
+// A record blocked by a member deep inside it names the member's path:
+// through a function's result (`()`), the promise and union around it,
+// record members (`.name`) and array elements (`[]`).
+interface Job {
+ id: string;
+ handle: Promise & { cancel(): void };
+}
+interface Order {
+ id: string;
+ jobs: Job[];
+}
+interface Provider {
+ name: string;
+ poll: () => Promise;
+}
+function makeProvider(): Provider {
+ return { name: "p", poll: async () => undefined };
+}
+console.log(makeProvider().name);
diff --git a/tests/diagnostics/pick-view-generic-methods.ts b/tests/diagnostics/pick-view-generic-methods.ts
new file mode 100644
index 000000000..912867dc6
--- /dev/null
+++ b/tests/diagnostics/pick-view-generic-methods.ts
@@ -0,0 +1,16 @@
+// A generic method called through a `Pick` view of a class compiles
+// against that class. A view that holds an instance of a structurally
+// compatible, unrelated class is refused: Node calls that class's own
+// method.
+class Store {
+ load(key: string, map: (text: string) => T): T {
+ return map(key);
+ }
+}
+class Mirror {
+ load(key: string, map: (text: string) => T): T {
+ return map(`${key}!`);
+ }
+}
+const view: Pick = new Mirror();
+console.log(view.load("k", (text) => text.length));
diff --git a/tests/fixtures/node-types/process-versions-absent.ts b/tests/fixtures/node-types/process-versions-absent.ts
new file mode 100644
index 000000000..8a59f7a58
--- /dev/null
+++ b/tests/fixtures/node-types/process-versions-absent.ts
@@ -0,0 +1,35 @@
+/* @types/node declares http_parser, v8, uv, zlib, ares and modules as
+ * strings. A compiled binary provides only node and openssl, and Node 24
+ * has no http_parser either, so those entries read undefined and compare
+ * like it. Checks on the others print agreement between equality, typeof
+ * and `in`, which holds whichever entries the running process has. */
+const versions = process.versions;
+
+console.log(process.versions.http_parser === undefined, process.versions['http_parser'] === undefined);
+console.log(versions.http_parser == null, typeof versions.http_parser, versions.http_parser ?? 'none');
+const parser = versions.http_parser;
+console.log(parser === undefined ? 'no http_parser' : parser);
+if (versions.http_parser) console.log('http_parser truthy');
+else console.log('http_parser falsy');
+
+console.log(
+ (process.versions.v8 === undefined) === !('v8' in process.versions),
+ (process.versions['uv'] !== undefined) === 'uv' in versions,
+ (versions.zlib == null) === (typeof versions.zlib === 'undefined'),
+ (versions.ares === undefined) === (typeof versions.ares !== 'string'),
+);
+const names = ['v8', 'uv', 'zlib', 'ares', 'modules', 'node', 'openssl'] as const;
+for (const name of names) {
+ const value = versions[name];
+ const absent = value === undefined;
+ console.log(
+ name,
+ absent === !(name in versions),
+ absent === (typeof value === 'undefined'),
+ ((value ?? '') === '') === absent,
+ );
+}
+const v8 = versions.v8;
+console.log(v8 === undefined || v8.length > 0);
+console.log(typeof (versions.modules || 'fallback'));
+console.log(typeof versions.node, versions.openssl !== undefined, versions.node === process.version.slice(1));
diff --git a/tests/harness/__snapshots__/json-dyn.ts.txt b/tests/harness/__snapshots__/json-dyn.ts.txt
index 4a09378fb..c0a9c7fe9 100644
--- a/tests/harness/__snapshots__/json-dyn.ts.txt
+++ b/tests/harness/__snapshots__/json-dyn.ts.txt
@@ -12,11 +12,4 @@ json-dyn.ts:46:7 - error SC2007: values of type '{ (text: string, reviver?: ((th
| ^~~~~~~~
47 | // The exact `string | undefined` root now lowers: strings serialize while
- hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow
-
-json-dyn.ts:46:18 - error SC1090: JSON methods as values (call 'parse' directly) are not supported yet
-
- 45 | const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585)
- 46 | const parseRef = JSON.parse;
- | ^~~~~~~~~~
- 47 | // The exact `string | undefined` root now lowers: strings serialize while
\ No newline at end of file
+ hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/nested-member-blocker-path.ts.txt b/tests/harness/__snapshots__/nested-member-blocker-path.ts.txt
new file mode 100644
index 000000000..e2d83b47c
--- /dev/null
+++ b/tests/harness/__snapshots__/nested-member-blocker-path.ts.txt
@@ -0,0 +1,17 @@
+nested-member-blocker-path.ts:16:10 - error SC2009: values of type 'Provider' cannot be compiled: the record shape is supported, but its member 'poll().jobs[].handle' has type 'Promise & { cancel(): void; }', which does not compile
+
+ 15 | }
+ 16 | function makeProvider(): Provider {
+ | ^~~~~~~~~~~~
+ 17 | return { name: "p", poll: async () => undefined };
+
+ hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
+
+nested-member-blocker-path.ts:19:13 - error SC2004: uses of 'makeProvider' inherit the blocker on its declaration
+
+ 18 | }
+ 19 | console.log(makeProvider().name);
+ | ^~~~~~~~~~~~
+ 20 |
+
+ hint: the declaration of 'makeProvider' did not compile — fix the diagnostic reported there and these sites clear with it
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/overload-fences.ts.txt b/tests/harness/__snapshots__/overload-fences.ts.txt
deleted file mode 100644
index 6c4b2f405..000000000
--- a/tests/harness/__snapshots__/overload-fences.ts.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-overload-fences.ts:12:13 - error SC1090: calls selecting an overload signature of a generic function (only the implementation signature monomorphizes) is not supported yet
-
- 11 | }
- 12 | console.log(twice("q").length);
- | ^~~~~~~~~~
- 13 | function pick(x: "a"): string;
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/pick-view-generic-methods.ts.txt b/tests/harness/__snapshots__/pick-view-generic-methods.ts.txt
new file mode 100644
index 000000000..59e7ed642
--- /dev/null
+++ b/tests/harness/__snapshots__/pick-view-generic-methods.ts.txt
@@ -0,0 +1,6 @@
+pick-view-generic-methods.ts:16:13 - error SC1090: calling the generic method 'load' through a 'Pick' view of 'Store' that holds a 'Mirror' is not supported yet
+
+ 15 | const view: Pick = new Mirror();
+ 16 | console.log(view.load("k", (text) => text.length));
+ | ^~~~~~~~~
+ 17 |
\ No newline at end of file
diff --git a/tests/harness/bench-dynamic-tier.test.ts b/tests/harness/bench-dynamic-tier.test.ts
index de19f2b21..9276221ab 100644
--- a/tests/harness/bench-dynamic-tier.test.ts
+++ b/tests/harness/bench-dynamic-tier.test.ts
@@ -7,7 +7,7 @@
* release binary the gate measures, so other hosts and the sanitized lane
* skip the builds. */
import { execFile } from "node:child_process";
-import { mkdirSync, readFileSync } from "node:fs";
+import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
@@ -17,6 +17,7 @@ import {
definedFunctionNames,
dynamicTierSymbols,
} from "../../scripts/zero-cost-compare.mjs";
+import { readWorkloads } from "../../benchmarks/runtime/workloads.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = join(import.meta.dirname, "../..");
@@ -31,11 +32,8 @@ interface Workload {
// One program per entry file, named after its first workload, as the gate
// names them.
-const manifest = JSON.parse(readFileSync(join(suite, "workloads.json"), "utf8")) as {
- workloads: Workload[];
-};
const programs = new Map();
-for (const w of manifest.workloads)
+for (const w of readWorkloads(suite) as Workload[])
if (![...programs.values()].includes(w.entry)) programs.set(`bench/${w.name}`, w.entry);
test("every workload the gate lists as dynamic is a bench program", () => {
diff --git a/tests/harness/corpus-costs/6495-generic-method-overloads.ts.json b/tests/harness/corpus-costs/6495-generic-method-overloads.ts.json
new file mode 100644
index 000000000..89eeb5b0c
--- /dev/null
+++ b/tests/harness/corpus-costs/6495-generic-method-overloads.ts.json
@@ -0,0 +1 @@
+{ "plain": 2783, "san": 2211 }
diff --git a/tests/harness/corpus-costs/6496-overload-signatures.ts.json b/tests/harness/corpus-costs/6496-overload-signatures.ts.json
new file mode 100644
index 000000000..5b1f193e3
--- /dev/null
+++ b/tests/harness/corpus-costs/6496-overload-signatures.ts.json
@@ -0,0 +1 @@
+{ "plain": 2032, "san": 1924 }
diff --git a/tests/harness/corpus-costs/6497-generic-overload-bindings.ts.json b/tests/harness/corpus-costs/6497-generic-overload-bindings.ts.json
new file mode 100644
index 000000000..a94adc8be
--- /dev/null
+++ b/tests/harness/corpus-costs/6497-generic-overload-bindings.ts.json
@@ -0,0 +1 @@
+{ "plain": 2061, "san": 1900 }
diff --git a/tests/harness/corpus-costs/6570-npm-builtin-function-bind/main.ts.json b/tests/harness/corpus-costs/6570-npm-builtin-function-bind/main.ts.json
new file mode 100644
index 000000000..96853d8bb
--- /dev/null
+++ b/tests/harness/corpus-costs/6570-npm-builtin-function-bind/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 9100, "san": 13000 }
diff --git a/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json
new file mode 100644
index 000000000..07189e390
--- /dev/null
+++ b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json
@@ -0,0 +1 @@
+{ "plain": 4000, "san": 4500 }
diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts
index 2ab323f37..2633cc815 100644
--- a/tests/harness/dyncheck.test.ts
+++ b/tests/harness/dyncheck.test.ts
@@ -159,7 +159,8 @@ console.log("unreachable", c.host);
test("schema-directed JSON.parse declines to the checked route with identical errors", async () => {
// `JSON.parse(text) as T` for plain record/array targets parses in one
// pass and reruns the checked-dynamic route whenever it declines, so
- // syntax, depth, and checked-cast failures keep their exact messages.
+ // syntax and depth failures keep their exact messages, and a value the
+ // check rejects continues as its parsed value (checked-cast fallback).
const r = await compileAndRun(
"schema-parse-errors",
`interface Item { sku: string; qty: number }
@@ -197,9 +198,9 @@ for (const text of inputs) {
'SyntaxError: Unexpected token \']\', ...","x":[1,],"qty":"... is not valid JSON (same)',
"SyntaxError: Bad Unicode escape in JSON at position 21 (same)",
"RangeError: Maximum call stack size exceeded (same)",
- "TypeError: expected number at $[0].qty, got undefined (plain ok)",
- "TypeError: expected number at $[1].qty, got boolean (plain ok)",
- "TypeError: expected number at $[0].qty, got string (plain ok)",
+ "ok 1 undefined (plain ok)",
+ "ok 2 1 (plain ok)",
+ "ok 1 x (plain ok)",
"ok 1 2 (plain ok)",
"",
]);
@@ -211,7 +212,9 @@ for (const text of inputs) {
// string arrays; null for object and number; missing fields; object vs
// array at a field and at the root; duplicate keys; truncated JSON; 1e400
// and a lone surrogate; extra nested members; reordered keys; and an Item[]
- // root. Every line equals the perf/harness (checked-route-only) output.
+ // root. A value the check rejects continues as its parsed value, so every
+ // line but the truncated input's (whose SyntaxError text is approximate)
+ // equals Node's.
const r = await compileAndRun(
"schema-parse-wrong-types",
`interface Item { sku: string; qty: number; ok: boolean }
@@ -255,23 +258,23 @@ try {
expect(r.stderr).toBe("");
expect(r.stdout.split("\n")).toEqual([
"ok 1 a 1 x:2:true t",
- "err TypeError: expected number at $.id, got string",
- "err TypeError: expected string at $.name, got number",
- "err TypeError: expected number at $.items[0].qty, got string",
- "err TypeError: expected boolean at $.items[0].ok, got undefined",
- "err TypeError: expected boolean at $.items[0].ok, got number",
- "err TypeError: expected array at $.items, got object",
- "err TypeError: expected object at $.items[0], got null",
- "err TypeError: expected string at $.tags[0], got number",
+ "ok 1 a 0 ",
+ "ok 1 2 0 ",
+ "ok 1 a 1 x:2:true ",
+ "ok 1 a 1 x:2:undefined ",
+ "ok 1 a 1 x:2:1 ",
+ "err TypeError: o.items.map is not a function",
+ "err TypeError: Cannot read properties of null (reading 'sku')",
+ "ok 1 a 0 1",
"ok 1 a 0 ",
"ok 2 a 0 ",
- "err TypeError: expected number at $.id, got undefined",
- "err TypeError: expected number at $.id, got null",
- "err TypeError: expected object at $, got array",
+ "ok undefined a 0 ",
+ "ok null a 0 ",
+ "err TypeError: Cannot read properties of undefined (reading 'length')",
"err SyntaxError: Unexpected end of JSON input",
"ok Infinity \uFFFD 0 ",
"ok 7 reordered 0 ",
- "err expected number at $[1].qty, got string",
+ "ok 2",
"",
]);
});
@@ -305,6 +308,46 @@ console.log("unreachable", c.host);
);
});
+ test("optional record member: the failure names the deepest path inside it", async () => {
+ // `Partial` makes every member an `X | undefined` union. A present
+ // object that fails X used to report only the union's own path
+ // ("expected object | undefined at $.order, got object"); the record
+ // arm's builder now reports where inside it the value fails.
+ const r = await compileAndRun(
+ "optional-record-deep",
+ `interface Order {
+ owner: { id: string; plan: { tier: string } };
+ total: number;
+}
+interface Envelope { order: Order; tags?: string[] }
+const texts = [
+ '{"order":{"owner":{"id":"u1"},"total":3}}',
+ '{"order":{"owner":{"id":7,"plan":{"tier":"pro"}},"total":3}}',
+ '{"order":{"owner":{"id":"u1","plan":{"tier":"pro"}},"total":3},"tags":["a",2]}',
+ '{"order":5}',
+];
+for (const text of texts) {
+ try {
+ const e = JSON.parse(text) as Partial;
+ console.log("ok", e.order?.owner.id);
+ } catch (err) {
+ if (err instanceof TypeError) console.log(err.message);
+ }
+}
+`,
+ );
+ expect(r.exitCode).toBe(0);
+ expect(r.stdout).toBe(
+ [
+ "expected object at $.order.owner.plan, got undefined",
+ "expected string at $.order.owner.id, got number",
+ "expected string at $.tags[1], got number",
+ "expected object | undefined at $.order, got number",
+ "",
+ ].join("\n"),
+ );
+ });
+
test("JSON null matches nothing", async () => {
const r = await compileAndRun(
"null-value",
@@ -468,7 +511,60 @@ console.log("unreachable", t[1]);
expect(r.stderr).toContain("Uncaught TypeError: expected array at $, got object");
});
+ test("checked-cast fallback: a failed value leaving its region is checked where it leaves", async () => {
+ // A block-local `JSON.parse(text) as T` whose value fails validation
+ // continues as its parsed value (checked-cast fallback): reads in the
+ // binding's region behave as in Node. Where the value flows into a typed
+ // slot outside that region (a typed parameter, a typed outer binding, a
+ // typed return), it is checked there: a value that fits continues, one
+ // that does not throws that conversion's TypeError at that use, where
+ // Node continues. Module-level bindings keep the check at the cast.
+ const r = await compileAndRun(
+ "fallback-escapes",
+ `interface Order { id: string; deployment: { id: string; plan: string } }
+function deliver(o: Order): string { return o.deployment.id + "/" + o.deployment.plan; }
+function plan(text: string): string {
+ const order = JSON.parse(text) as Order;
+ console.log("read", order.deployment?.id, order.deployment?.plan);
+ const id: string = order.id;
+ return id;
+}
+function handle(text: string): void {
+ const order = JSON.parse(text) as Order;
+ console.log("id", order.id);
+ try { console.log(deliver(order)); } catch (e) { console.log((e as Error).message); }
+}
+handle('{"id":"a","deployment":{"id":"d","plan":"pro"},"extra":1}');
+handle('{"id":"b","deployment":{"id":"d"}}');
+console.log(plan('{"id":"c","deployment":{}}'));
+try { console.log(plan('{"id":7,"deployment":{"id":"d","plan":"p"}}')); }
+catch (e) { console.log((e as Error).message); }
+let top: Order;
+try { top = JSON.parse('{"id":"t"}') as Order; console.log(top.id); }
+catch (e) { console.log((e as Error).message); }
+`,
+ );
+ expect(r.exitCode).toBe(0);
+ expect(r.stderr).toBe("");
+ expect(r.stdout).toBe(
+ [
+ "id a",
+ "d/pro",
+ "id b",
+ "expected string at $.deployment.plan, got undefined",
+ "read undefined undefined",
+ "c",
+ "read d p",
+ "expected string at $, got number",
+ "expected object at $.deployment, got undefined",
+ "",
+ ].join("\n"),
+ );
+ });
+
test("tuple failure-path RC stress: partial tuples release on unwind", async () => {
+ // The rejected value continues as its parsed value (checked-cast
+ // fallback); the partial tuple the check built still releases.
const r = await compileAndRun(
"tuple-rc-failure",
`let recovered = 0;
@@ -482,14 +578,18 @@ for (let i = 0; i < 100; i = i + 1) {
}
console.log(recovered);
`,
+ "ts",
+ true,
);
expect(r.exitCode).toBe(0);
- expect(r.stdout).toBe("100\n");
+ expect(r.stdout).toBe("-200\n");
});
test("failure-path RC stress: repeated mismatches through catch stay clean", async () => {
// Meaningful mostly under SCRIPTC_SAN=1 (ASan + RC audit at exit): every
- // iteration builds a partial record/array that must release on unwind.
+ // iteration builds a partial record/array that must release when the
+ // check rejects it, and the parsed value continues (checked-cast
+ // fallback), so `e2.v` reads "no" and the count becomes NaN as in Node.
const r = await compileAndRun(
"rc-failure-stress",
`type Entry = { tags: string[]; v: number };
@@ -510,9 +610,11 @@ for (let i = 0; i < 100; i = i + 1) {
}
console.log(recovered);
`,
+ "ts",
+ true,
);
expect(r.exitCode).toBe(0);
- expect(r.stdout).toBe("200\n");
+ expect(r.stdout).toBe("NaN\n");
});
test("index-signature shapes CAPTURE extras — and a wrong-typed extra throws with its key path", async () => {
@@ -685,13 +787,19 @@ console.log(total);
// The static-any lane's exit stance (the checked cast's rule applied
// to implicit any→typed flows): Node assigns the mismatched value and
// carries on; scriptc throws the catchable TypeError instead of
- // returning a wrong result — the divergence is deliberate and documented.
+ // returning a wrong result where the value leaves the binding it was
+ // checked into. A block-local binding read only after the check keeps
+ // the value instead (checked-cast fallback), as Node does.
const r = await compileAndRun(
"any-lying-exit",
`const a: any = "not a number";
+function width(n: number): number {
+ return n * 2;
+}
try {
const n: number = a;
- console.log("unreachable", n);
+ console.log("kept", n, typeof n);
+ console.log(width(n));
} catch (e) {
console.log((e as Error).name);
}
@@ -699,7 +807,7 @@ console.log("recovered");
`,
);
expect(r.exitCode).toBe(0);
- expect(r.stdout).toBe("TypeError\nrecovered\n");
+ expect(r.stdout).toBe("kept not a number string\nTypeError\nrecovered\n");
});
test("a lying cast into a union that cannot represent the value throws the stranded trap catchably", async () => {
diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts
index 27a176764..df79a6235 100644
--- a/tests/harness/inheritance.test.ts
+++ b/tests/harness/inheritance.test.ts
@@ -100,7 +100,7 @@ console.log(a.id(), a.speak(), b.speak());
// base-typed call site dispatches through it: both classes are
// constructed, so the loaded slot selects a direct call of each.
expect(llvm).toMatch(/%sc_vtt_Animal = type \{ %ScrVt, ptr \}/);
- expect(llvm).toMatch(/@sc_vtable_Dog = [^\n]*ptr @sc_b?f__x25_Dog_speak\b/);
+ expect(llvm).toMatch(/%sc_vtt_Animal \{[^\n]*ptr @sc_b?f__x25_Dog_speak\b[^\n]*; class Dog\n/);
expect(llvm).toMatch(
/getelementptr inbounds %sc_vtt_Animal, ptr %\w+, i64 0, i32 1[\s\S]*icmp eq ptr %\w+, @sc_b?f__x25_(?:Animal|Dog)_speak\n/,
);
@@ -168,7 +168,9 @@ console.log(c.v, c.label, plain.label);
);
// The overridden getter dispatches through its slot...
expect(llvm).toMatch(/%sc_vtt_Cell = type \{ %ScrVt, ptr \}/);
- expect(llvm).toMatch(/@sc_vtable_UpperCell = [^\n]*ptr @sc_b?f__x25_UpperCell_get_x3a_label\b/);
+ expect(llvm).toMatch(
+ /%sc_vtt_Cell \{[^\n]*ptr @sc_b?f__x25_UpperCell_get_x3a_label\b[^\n]*; class UpperCell\n/,
+ );
expect(llvm).toMatch(
/getelementptr inbounds %sc_vtt_Cell, ptr %\w+, i64 0, i32 1[\s\S]*icmp eq ptr %\w+, @sc_b?f__x25_(?:Cell|UpperCell)_get_x3a_label\n/,
);
@@ -265,7 +267,7 @@ console.log(b.v, plain.v);
// dispatch dynamically here.
expect(llvm).toMatch(/%sc_vtt_Box = type \{ %ScrVt, ptr, ptr \}/);
expect(llvm).toMatch(
- /@sc_vtable_SealedBox = [^\n]*ptr @sc_b?f__x25_SealedBox_get_x3a_v(\.virtual)?, ptr @sc_b?f__x25_SealedBox_set_x3a_v\b/,
+ /%sc_vtt_Box \{[^\n]*ptr @sc_b?f__x25_SealedBox_get_x3a_v(\.virtual)?, ptr @sc_b?f__x25_SealedBox_set_x3a_v\b[^\n]*; class SealedBox\n/,
);
expect(llvm).toMatch(/getelementptr inbounds %sc_vtt_Box, ptr %\w+, i64 0, i32 2/);
expect(llvm).toMatch(/sc_f__x25_SealedBox_set_x3a_v/);
diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts
index b7c30a910..2b705edae 100644
--- a/tests/harness/project-config.test.ts
+++ b/tests/harness/project-config.test.ts
@@ -1193,3 +1193,21 @@ test("node-types: terminal properties and filesystem error metadata match Node",
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
+
+test("node-types: process.versions entries @types/node declares as strings read undefined when absent", async () => {
+ const outDir = outDirFor("process-versions-absent");
+ const entry = join(nodeTypesDir, "process-versions-absent.ts");
+ const result = await compile(entry, {
+ outPath: join(outDir, "process-versions-absent"),
+ outDir,
+ sanitize,
+ });
+ expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
+ if (!result.ok) return;
+ const [native, node] = await Promise.all([
+ execFileAsync(result.binaryPath),
+ execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
+ ]);
+ expect(native.stdout).toBe(node.stdout);
+ expect(comparableStderr(native.stderr)).toBe(node.stderr);
+});
diff --git a/tests/harness/self-hosting-emission-cases.ts b/tests/harness/self-hosting-emission-cases.ts
index cf3babf6f..efba5e019 100644
--- a/tests/harness/self-hosting-emission-cases.ts
+++ b/tests/harness/self-hosting-emission-cases.ts
@@ -243,7 +243,11 @@ export function emissionCases(): EmissionCase[] {
const forest = structuredClone(hierarchy);
forest.classes!.push(cls("Unrelated"), cls("Leaf", "Child"));
forest.classes![3]!.fields = [...child.fields];
- add("class forest numbering", forest, ["class Leaf (vt at 1)", "class Unrelated { value }"]);
+ add("class forest numbering", forest, [
+ "class Leaf (class id in rc)",
+ "class Unrelated { value }",
+ "@sc_vta_Base",
+ ]);
const abstract = emissionModule();
const abstractRoot = cls("Abstract");