diff --git a/.gitignore b/.gitignore
index 7b7bc57f2..fda9414aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ node_modules/
!tests/fixtures/npm-static/nested/inner/node_modules/
!tests/fixtures/npm-static/node_modules/*/node_modules/
!tests/fixtures/npm/store/*/node_modules/
+!tests/fixtures/npm/node_modules/*/node_modules/
+!tests/fixtures/npm/workspace/*/node_modules/
!tests/fixtures/npm/cases/*/node_modules/
!tests/library-mode/*/node_modules/
!tests/fixtures/provenance/node_modules/
@@ -49,3 +51,6 @@ tmp-*/
.env.local
.env*
.env*.local
+# the vendored fs-extra corpus package: its lib/output module and jsonfile's own universalify
+!tests/corpus/fs-extra-package/node_modules/fs-extra/lib/output/
+!tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/
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/dependencies.mdx b/docs/content/docs/dependencies.mdx
index b16c14e7d..85a232bb2 100644
--- a/docs/content/docs/dependencies.mdx
+++ b/docs/content/docs/dependencies.mdx
@@ -73,7 +73,58 @@ Some packages your program imports directly ship no declarations, so their types
$ scriptc build app.ts --npm-static=three -o app >/dev/null
```
-Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
+Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=semver...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
+
+## Built-in packages
+
+scriptc implements a few packages itself and compiles that implementation in place of the package's published JavaScript. Which implementation compiles depends on the version your program has installed, so an import resolves the same way it does under Node.js: a package that isn't installed is still a missing package, and a version scriptc doesn't implement compiles from its own files as usual.
+
+`undici` 5.x and 7.x are built in. The implementation covers `request()`, `Dispatcher` (including subclasses that override `dispatch`), `Client`, `Pool` with its `stats`, `Agent` with per-origin `factory` functions and `connect` options, `buildConnector`, the `errors` classes and their `UND_ERR_*` codes, `setGlobalDispatcher` and `getGlobalDispatcher`, `fetch` and `Headers`, and the request events undici publishes on `node:diagnostics_channel`. Requests use HTTP/1.1 with keep-alive connection reuse and undici's headers, body and idle timeouts, and each version keeps its own behavior where 5.x and 7.x differ, such as the error an aborted request rejects with. The package's other exports aren't declared, so using one is a type error at build time. Known differences:
+
+- A response body supports the readable-stream members undici's body mixin uses: `on`, `once`, `pause`, `resume`, `destroy`, `text()`, `json()`, `arrayBuffer()`, `bytes()`, `blob()` and `dump()`. It can't be piped or iterated with `for await`.
+- Diagnostics channel messages carry the request but not the socket or connector.
+- `fetch` with a dispatcher returns a response whose `url` is empty and whose `redirected` is `false`, offers only `gzip, deflate` encodings, and sends string and byte bodies. Without a dispatcher it is the built-in `fetch`.
+- Request bodies are strings and bytes; stream, iterable and form-data bodies, `pipelining` above 1, `CONNECT` requests and Unix socket paths aren't supported.
+
+`fs-extra` 8.x through 11.x is built in, and its declarations replace `@types/fs-extra`. The default import, a namespace import and named imports all read the same exports. fs's members are re-exported, and fs-extra's wrapped methods follow its rule: with a callback as the last argument they call back, otherwise they return a promise. The extra methods cover `copy`, `copySync`, `move`, `moveSync`, `remove`, `removeSync`, `emptyDir`, `ensureDir`, `mkdirs`, `mkdirp`, `ensureFile`, `pathExists`, `outputFile`, `readJson`, `readJsonSync`, `writeJson`, `writeJsonSync`, `outputJson` and their aliases, with fs-extra's error messages. Known differences:
+
+- `ensureLink`, `ensureSymlink`, `createLink`, `createSymlink`, `gracefulify`, the promise forms of the descriptor-based methods, and the callback forms of `stat`, `lstat`, `readdir`, `mkdtemp`, `realpath` and `readJson` aren't declared, so using one is a type error at build time. Paths are strings, text is UTF-8, and write flags, string modes and `copyFile` mode flags aren't accepted.
+- graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying.
+- An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there.
+
+Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled.
+
+
+
+ | Package | Versions | Compiles as |
+
+
+ isarray | 0.x, 1.x, 2.x | Array.isArray |
+ safe-buffer | 5.x | require("buffer") |
+ process-nextick-args | 2.x | process |
+ inherits | 2.x | require("util").inherits |
+ util-deprecate | 1.x | require("util").deprecate |
+ readable-stream | 2.x, 3.x, 4.x | require("stream") and its classes |
+
+
+
+`readable-stream` compiles as Node.js's built-in streams, which is its own `READABLE_STREAM=disable` mode for 2.x and 4.x. That can differ from its default behavior under Node.js; see [limitations](/docs/limitations#readable-stream).
+
+## Optional dependencies that never load
+
+Some packages load an optional dependency inside `try`/`catch` and work without it, such as `node-fetch` with `encoding`. To keep such a dependency out of the native build even when it is installed, mark it unloaded in your entry's `package.json`:
+
+```json:package.json
+{
+ "scriptc": {
+ "packages": {
+ "encoding": "unloaded"
+ }
+ }
+}
+```
+
+A `require()` of an unloaded package throws the same `MODULE_NOT_FOUND` error Node.js throws when the package isn't installed, and the package isn't compiled.
## CommonJS packages
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/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index 823b7f028..05f9d329a 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -178,6 +178,10 @@ title: "Limitations"
../limitations/buffer-construction.mdx
+### readable-stream
+
+../limitations/readable-stream.mdx
+
### HTTP headers
../limitations/http-headers.mdx
diff --git a/docs/content/limitations/readable-stream.mdx b/docs/content/limitations/readable-stream.mdx
new file mode 100644
index 000000000..80ab72f01
--- /dev/null
+++ b/docs/content/limitations/readable-stream.mdx
@@ -0,0 +1 @@
+`readable-stream` 2.x, 3.x and 4.x compile as Node.js's built-in `stream` module. For 2.x and 4.x, that is the package's own behavior when the `READABLE_STREAM` environment variable is `disable`. Without that variable, Node.js runs the package's JavaScript copy of the stream classes, so a native build can differ from Node.js wherever a program depends on that difference: `instanceof` checks against the built-in classes, class identity, error messages, and timing details. The package's `lib/_stream_*.js` files also compile as the matching built-in classes, although Node.js loads the package's own implementations through them in every environment. `readable-stream` 3.x is a copy of Node.js 10's streams. Its own `disable` mode throws on Node.js 24, so no setting makes Node.js run it against the built-in classes; a native build maps the members its entry exports (the stream classes, `Stream`, `finished` and `pipeline`) to the built-in ones.
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/README.md b/internal/compatibility/README.md
index 0b4fdb957..51c7d72c5 100644
--- a/internal/compatibility/README.md
+++ b/internal/compatibility/README.md
@@ -13,3 +13,7 @@ Statuses are evidence-aware: `supported` and `partial` require test evidence; `r
Run `pnpm node-compat:backlog` from the repository root for a summary. The filters `--tier`, `--action`, `--priority`, `--chapter`, and `--status` compose; use `--format=json` or `--format=tsv` for tooling.
Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
+
+## npm packages that compile as builtins
+
+The ledger counts Node APIs, so npm packages that compile as a Node builtin have no rows. `packages/compiler/src/frontend/builtin-package-aliases.ts` lists them, each with the checked major versions and the reason its module is that builtin on Node 24. One entry is a documented divergence rather than an identity: `readable-stream` 2.x, 3.x and 4.x compile as `node:stream`; for 2.x and 4.x that is the package's `READABLE_STREAM=disable` behavior. Without that variable Node runs the package's own JavaScript streams. The `tests/corpus/readable-stream-disable` program pins the disabled behavior. 3.x, a copy of Node 10's streams, cannot run disabled on Node 24 (its disable branch throws), so `tests/corpus/readable-stream-3` pins only behavior its own streams share with the builtin.
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/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts
index 39964898d..57c60b1bc 100644
--- a/packages/cli/test/native-link-info.test.ts
+++ b/packages/cli/test/native-link-info.test.ts
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
- runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" },
+ runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
- expect(error?.stderr).toContain("scr_runtime_abi_v22");
+ expect(error?.stderr).toContain("scr_runtime_abi_v23");
});
});
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 6dfcc90ef..7a1967572 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -4940,15 +4940,17 @@ declare module "net" {
host?: string,
connectListener?: () => void,
): Socket;
- /* The caller-resolver dial (@types/node's net.LookupFunction, narrowed
- * to the lowered shape): the runtime invokes it as Node does and dials
- * the answered addresses in order under autoSelectFamily: true. */
+ /* The caller-resolver dial (@types/node's net.LookupFunction): the
+ * runtime invokes it as Node's autoSelectFamily dial does (options
+ * { hints, all: true }, so the callback answers an address list) and
+ * dials the answered addresses in order. */
export type LookupFunction = (
hostname: string,
- options: unknown,
+ options: import("node:dns").LookupOptions,
callback: (
err: NodeJS.ErrnoException | null,
- addresses: { address: string; family: number }[],
+ address: string | import("node:dns").LookupAddress[],
+ family?: number,
) => void,
) => void;
export function connect(
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index 0c058c467..70bf3b27a 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -29,6 +29,7 @@
"files": [
"dist",
"ambient",
+ "shims",
"surface-manifest.json"
],
"publishConfig": {
diff --git a/packages/compiler/shims/fs-extra/core/api.ts b/packages/compiler/shims/fs-extra/core/api.ts
new file mode 100644
index 000000000..7a59f17e2
--- /dev/null
+++ b/packages/compiler/shims/fs-extra/core/api.ts
@@ -0,0 +1,1192 @@
+// fs-extra's exports over node:fs.
+//
+// fs-extra re-exports every fs member and wraps the asynchronous ones with
+// universalify: a call whose last argument is a function takes the callback
+// form, any other call returns a promise. The wrapped methods below follow
+// that rule over fs.promises (callback forms settle through the promise);
+// fs's other members are fs's own. The extra methods (copy, remove, mkdirs,
+// the JSON helpers, ...) restate fs-extra's behavior and error messages.
+// graceful-fs's EMFILE retry queue, which fs-extra loads under Node, has no
+// counterpart: on Node 24 it only changes behavior when the process runs
+// out of file descriptors.
+//
+// What is not declared here is a compile-time error: ensureLink,
+// ensureSymlink, createLink, createSymlink and their sync forms,
+// gracefulify, the descriptor-based methods' promise forms (fstat, read,
+// write, ...), the callback forms of the methods that answer stats,
+// listings or paths (stat, lstat, readdir, mkdtemp, realpath, readJson),
+// Buffer and URL paths, and option shapes fs.promises does not compile
+// natively (copyFile mode flags, append options, write flags, string modes,
+// numeric utimes times). A readFile callback that fails receives an empty
+// value beside the error, where Node passes undefined.
+import * as fs from "node:fs";
+import * as fsp from "node:fs/promises";
+import * as path from "node:path";
+import type { Stats } from "node:fs";
+
+// fs's other members, as fs-extra re-exports them (fs-extra's own versions
+// of the wrapped methods below take their place).
+export {
+ appendFileSync,
+ accessSync,
+ chownSync,
+ chmodSync,
+ close,
+ closeSync,
+ copyFileSync,
+ cp,
+ cpSync,
+ createReadStream,
+ createWriteStream,
+ exists,
+ existsSync,
+ fchown,
+ fchownSync,
+ fchmod,
+ fchmodSync,
+ fdatasync,
+ fdatasyncSync,
+ fstat,
+ fstatSync,
+ fsync,
+ fsyncSync,
+ ftruncate,
+ ftruncateSync,
+ futimes,
+ futimesSync,
+ lchownSync,
+ linkSync,
+ lstatSync,
+ lutimes,
+ lutimesSync,
+ mkdirSync,
+ mkdtempSync,
+ open,
+ openSync,
+ readdirSync,
+ read,
+ readSync,
+ readv,
+ readvSync,
+ readFileSync,
+ readlinkSync,
+ realpathSync,
+ renameSync,
+ rmSync,
+ rmdirSync,
+ statfs,
+ statSync,
+ statfsSync,
+ symlinkSync,
+ truncateSync,
+ unwatchFile,
+ unlinkSync,
+ utimesSync,
+ watch,
+ watchFile,
+ writeFileSync,
+ write,
+ writeSync,
+ writev,
+ writevSync,
+ Dirent,
+ Stats,
+ ReadStream,
+ WriteStream,
+ Dir,
+ opendir,
+ opendirSync,
+ constants,
+ promises,
+} from "node:fs";
+
+/** A Node-style callback with a result. */
+export type Callback = (err: NodeJS.ErrnoException | null, value: T) => void;
+/** A Node-style callback without a result. */
+export type VoidCallback = (err: NodeJS.ErrnoException | null) => void;
+
+export type SymlinkType = "dir" | "file" | "junction";
+export type Utf8 = "utf8" | "utf-8";
+
+export interface WriteFileOptions {
+ encoding?: Utf8 | undefined;
+ mode?: number | undefined;
+}
+
+export interface ReadOptions {
+ throws?: boolean | undefined;
+ encoding?: Utf8 | undefined;
+}
+
+export interface WriteOptions {
+ encoding?: Utf8 | undefined;
+ mode?: number | undefined;
+ spaces?: number | undefined;
+ EOL?: string | undefined;
+ finalEOL?: boolean | undefined;
+}
+
+/** A copy filter: fs-extra accepts a boolean or a promise of one. */
+export type CopyFilter = (src: string, dest: string) => boolean | Promise;
+
+export interface CopyOptions {
+ dereference?: boolean | undefined;
+ overwrite?: boolean | undefined;
+ preserveTimestamps?: boolean | undefined;
+ errorOnExist?: boolean | undefined;
+ filter?: CopyFilter | undefined;
+}
+
+export interface CopyOptionsSync {
+ dereference?: boolean | undefined;
+ overwrite?: boolean | undefined;
+ preserveTimestamps?: boolean | undefined;
+ errorOnExist?: boolean | undefined;
+ filter?: ((src: string, dest: string) => boolean) | undefined;
+}
+
+export interface MoveOptions {
+ overwrite?: boolean | undefined;
+}
+
+export interface EnsureOptions {
+ mode?: number | undefined;
+}
+
+export interface MakeDirectoryOptions {
+ recursive?: boolean | undefined;
+ mode?: number | undefined;
+}
+
+export interface RmOptions {
+ force?: boolean | undefined;
+ recursive?: boolean | undefined;
+}
+
+function settleVoid(promise: Promise, callback: VoidCallback | undefined): Promise {
+ if (callback === undefined) return promise;
+ // The callback form's promise settles only through the callback (it
+ // never rejects); fs-extra answers undefined there.
+ return promise.then(
+ () => callback(null),
+ (err: NodeJS.ErrnoException) => callback(err),
+ );
+}
+
+/** The result of a callback-form call that settles through fs's own
+ * callback: callers ignore it (fs-extra answers undefined). */
+function idle(): Promise {
+ return new Promise(() => undefined);
+}
+
+function codeOf(err: unknown): string | undefined {
+ return err instanceof Error ? (err as NodeJS.ErrnoException).code : undefined;
+}
+
+/* ── fs methods in universalify's wrapper ─────────────────────────────── */
+
+export function access(path: string, mode?: number): Promise;
+export function access(path: string, callback: VoidCallback): void;
+export function access(path: string, mode: number, callback: VoidCallback): void;
+export function access(
+ path: string,
+ mode?: number | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof mode === "function") return settleVoid(fsp.access(path), mode);
+ return settleVoid(fsp.access(path, mode ?? fs.constants.F_OK), callback);
+}
+
+export function appendFile(path: string, data: string | Uint8Array): Promise;
+export function appendFile(path: string, data: string | Uint8Array, callback: VoidCallback): void;
+export function appendFile(
+ path: string,
+ data: string | Uint8Array,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.appendFile(path, data), callback);
+}
+
+/** fs.writeFileSync(file, bytes, { mode }) as Node runs it: open with
+ * O_WRONLY | O_CREAT | O_TRUNC and the mode, write every byte, close. */
+function writeBytesSync(file: string, data: Uint8Array, mode: number): void {
+ const fd = fs.openSync(
+ file,
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC,
+ mode,
+ );
+ try {
+ let offset = 0;
+ while (offset < data.length) offset += fs.writeSync(fd, data, offset, data.length - offset);
+ } finally {
+ fs.closeSync(fd);
+ }
+}
+
+async function writeData(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8 | undefined,
+): Promise {
+ if (typeof data !== "string") {
+ // fs.promises.writeFile compiles string data; bytes take the
+ // synchronous write after the same microtask boundary.
+ await Promise.resolve();
+ if (options === undefined || typeof options === "string") fs.writeFileSync(file, data);
+ else writeBytesSync(file, data, options.mode ?? 0o666);
+ return;
+ }
+ if (options === undefined || typeof options === "string")
+ return fsp.writeFile(file, data, "utf8");
+ return fsp.writeFile(file, data, { encoding: "utf8", mode: options.mode ?? 0o666 });
+}
+
+export function writeFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8,
+): Promise;
+export function writeFile(file: string, data: string | Uint8Array, callback: VoidCallback): void;
+export function writeFile(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function writeFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(writeData(file, data, undefined), options);
+ return settleVoid(writeData(file, data, options), callback);
+}
+
+export function chmod(path: string, mode: number): Promise;
+export function chmod(path: string, mode: number, callback: VoidCallback): void;
+export function chmod(path: string, mode: number, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.chmod(path, mode), callback);
+}
+
+export function chown(path: string, uid: number, gid: number): Promise;
+export function chown(path: string, uid: number, gid: number, callback: VoidCallback): void;
+export function chown(
+ path: string,
+ uid: number,
+ gid: number,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.chown(path, uid, gid), callback);
+}
+
+export function lchown(path: string, uid: number, gid: number): Promise;
+export function lchown(path: string, uid: number, gid: number, callback: VoidCallback): void;
+export function lchown(
+ path: string,
+ uid: number,
+ gid: number,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.lchown(path, uid, gid), callback);
+}
+
+export function copyFile(src: string, dest: string): Promise;
+export function copyFile(src: string, dest: string, callback: VoidCallback): void;
+export function copyFile(src: string, dest: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.copyFile(src, dest), callback);
+}
+
+export function link(existingPath: string, newPath: string): Promise;
+export function link(existingPath: string, newPath: string, callback: VoidCallback): void;
+export function link(
+ existingPath: string,
+ newPath: string,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.link(existingPath, newPath), callback);
+}
+
+export function lstat(path: string): Promise {
+ return fsp.lstat(path);
+}
+
+export function stat(path: string): Promise {
+ return fsp.stat(path);
+}
+
+async function makeDirectory(
+ path: string,
+ options: number | MakeDirectoryOptions | undefined,
+): Promise {
+ if (options === undefined) return fsp.mkdir(path);
+ if (typeof options === "number") return fsp.mkdir(path, { mode: options });
+ const mode = options.mode ?? 0o777;
+ if (options.recursive === true) return fsp.mkdir(path, { recursive: true, mode });
+ return fsp.mkdir(path, { mode });
+}
+
+export function mkdir(path: string, options?: number | MakeDirectoryOptions): Promise;
+export function mkdir(path: string, callback: VoidCallback): void;
+export function mkdir(
+ path: string,
+ options: number | MakeDirectoryOptions,
+ callback: VoidCallback,
+): void;
+export function mkdir(
+ path: string,
+ options?: number | MakeDirectoryOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDirectory(path, undefined), options);
+ return settleVoid(makeDirectory(path, options), callback);
+}
+
+export function mkdtemp(prefix: string): Promise {
+ return fsp.mkdtemp(prefix);
+}
+
+export function readdir(path: string): Promise {
+ return fsp.readdir(path);
+}
+
+export type ReadFileOptions = Utf8 | { encoding: Utf8 };
+
+export function readFile(file: string): Promise;
+export function readFile(file: string, options: ReadFileOptions): Promise;
+export function readFile(file: string, callback: Callback): void;
+export function readFile(file: string, options: ReadFileOptions, callback: Callback): void;
+export function readFile(
+ file: string,
+ options?: ReadFileOptions | Callback,
+ callback?: Callback,
+): Promise {
+ if (typeof options === "function") {
+ fsp.readFile(file).then(
+ (data) => options(null, data),
+ (err: NodeJS.ErrnoException) => options(err, Buffer.alloc(0)),
+ );
+ return idle();
+ }
+ if (options === undefined) return fsp.readFile(file);
+ if (callback !== undefined) {
+ fsp.readFile(file, "utf8").then(
+ (text) => callback(null, text),
+ (err: NodeJS.ErrnoException) => callback(err, ""),
+ );
+ return idle();
+ }
+ return fsp.readFile(file, "utf8");
+}
+
+export function readlink(path: string): Promise;
+export function readlink(path: string, callback: Callback): void;
+export function readlink(path: string, callback?: Callback): Promise {
+ if (callback !== undefined) {
+ fs.readlink(path, (err, target) => callback(err, target));
+ return idle();
+ }
+ return fsp.readlink(path);
+}
+
+export function realpath(path: string): Promise {
+ return fsp.realpath(path);
+}
+
+export function rename(oldPath: string, newPath: string): Promise;
+export function rename(oldPath: string, newPath: string, callback: VoidCallback): void;
+export function rename(oldPath: string, newPath: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.rename(oldPath, newPath), callback);
+}
+
+/** Removes a file or directory tree; a missing path is not an error
+ * (fs-extra's rimraf and fs.rm's force). */
+async function removeTree(target: string): Promise {
+ let entry: Stats;
+ try {
+ entry = await fsp.lstat(target);
+ } catch (err) {
+ if (codeOf(err) === "ENOENT") return;
+ throw err;
+ }
+ if (entry.isDirectory()) {
+ for (const item of await fsp.readdir(target)) await removeTree(path.join(target, item));
+ await fsp.rmdir(target);
+ return;
+ }
+ await fsp.unlink(target);
+}
+
+async function rmPath(target: string, options: RmOptions | undefined): Promise {
+ const recursive = options === undefined ? false : (options.recursive ?? false);
+ const force = options === undefined ? false : (options.force ?? false);
+ if (recursive && force) return removeTree(target);
+ if (!recursive && !force) return fsp.rm(target);
+ // The mixed forms: fs.rm's own checks, then the walk.
+ if (force && !fs.existsSync(target)) return;
+ const entry = await fsp.lstat(target);
+ if (entry.isDirectory() && !recursive) return fsp.rm(target);
+ return removeTree(target);
+}
+
+export function rm(path: string, options?: RmOptions): Promise;
+export function rm(path: string, callback: VoidCallback): void;
+export function rm(path: string, options: RmOptions, callback: VoidCallback): void;
+export function rm(
+ path: string,
+ options?: RmOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(rmPath(path, undefined), options);
+ return settleVoid(rmPath(path, options), callback);
+}
+
+export function rmdir(path: string): Promise;
+export function rmdir(path: string, callback: VoidCallback): void;
+export function rmdir(path: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.rmdir(path), callback);
+}
+
+export function symlink(target: string, path: string, type?: SymlinkType): Promise;
+export function symlink(target: string, path: string, callback: VoidCallback): void;
+export function symlink(
+ target: string,
+ path: string,
+ type: SymlinkType | undefined,
+ callback: VoidCallback,
+): void;
+export function symlink(
+ target: string,
+ path: string,
+ type?: SymlinkType | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof type === "function") return settleVoid(fsp.symlink(target, path), type);
+ return settleVoid(fsp.symlink(target, path, type ?? null), callback);
+}
+
+export function truncate(path: string, len?: number): Promise;
+export function truncate(path: string, callback: VoidCallback): void;
+export function truncate(path: string, len: number, callback: VoidCallback): void;
+export function truncate(
+ path: string,
+ len?: number | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof len === "function") return settleVoid(fsp.truncate(path, 0), len);
+ return settleVoid(fsp.truncate(path, len ?? 0), callback);
+}
+
+export function unlink(path: string): Promise;
+export function unlink(path: string, callback: VoidCallback): void;
+export function unlink(path: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.unlink(path), callback);
+}
+
+export function utimes(path: string, atime: Date, mtime: Date): Promise;
+export function utimes(path: string, atime: Date, mtime: Date, callback: VoidCallback): void;
+export function utimes(
+ path: string,
+ atime: Date,
+ mtime: Date,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.utimes(path, atime, mtime), callback);
+}
+
+/* ── mkdirs, ensureDir, mkdirp ────────────────────────────────────────── */
+
+function dirMode(options: EnsureOptions | number | undefined): number {
+ if (options === undefined) return 0o777;
+ if (typeof options === "number") return options;
+ return options.mode ?? 0o777;
+}
+
+function makeDir(dir: string, mode: number): Promise {
+ return fsp.mkdir(path.resolve(dir), { recursive: true, mode });
+}
+
+function makeDirSync(dir: string, mode: number): void {
+ fs.mkdirSync(path.resolve(dir), { recursive: true, mode });
+}
+
+export function ensureDir(dir: string, options?: EnsureOptions | number): Promise;
+export function ensureDir(dir: string, callback: VoidCallback): void;
+export function ensureDir(
+ dir: string,
+ options: EnsureOptions | number,
+ callback: VoidCallback,
+): void;
+export function ensureDir(
+ dir: string,
+ options?: EnsureOptions | number | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options);
+ return settleVoid(makeDir(dir, dirMode(options)), callback);
+}
+
+export function mkdirs(dir: string, options?: EnsureOptions | number): Promise;
+export function mkdirs(dir: string, callback: VoidCallback): void;
+export function mkdirs(
+ dir: string,
+ options?: EnsureOptions | number | VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options);
+ return makeDir(dir, dirMode(options));
+}
+
+export function mkdirp(dir: string, options?: EnsureOptions | number): Promise;
+export function mkdirp(dir: string, callback: VoidCallback): void;
+export function mkdirp(
+ dir: string,
+ options?: EnsureOptions | number | VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options);
+ return makeDir(dir, dirMode(options));
+}
+
+export function ensureDirSync(dir: string, options?: EnsureOptions | number): void {
+ makeDirSync(dir, dirMode(options));
+}
+
+export function mkdirsSync(dir: string, options?: EnsureOptions | number): void {
+ makeDirSync(dir, dirMode(options));
+}
+
+export function mkdirpSync(dir: string, options?: EnsureOptions | number): void {
+ makeDirSync(dir, dirMode(options));
+}
+
+/* ── remove, emptyDir ─────────────────────────────────────────────────── */
+
+export function remove(dir: string): Promise;
+export function remove(dir: string, callback: VoidCallback): void;
+export function remove(dir: string, callback?: VoidCallback): Promise {
+ return settleVoid(removeTree(dir), callback);
+}
+
+export function removeSync(dir: string): void {
+ fs.rmSync(dir, { recursive: true, force: true });
+}
+
+async function emptyDirAsync(dir: string): Promise {
+ let items: string[];
+ try {
+ items = await fsp.readdir(dir);
+ } catch {
+ await makeDir(dir, 0o777);
+ return;
+ }
+ while (items.length > 0) {
+ const item = items.pop();
+ if (item !== undefined) await removeTree(path.join(dir, item));
+ }
+}
+
+export function emptyDir(dir: string): Promise;
+export function emptyDir(dir: string, callback: VoidCallback): void;
+export function emptyDir(dir: string, callback?: VoidCallback): Promise {
+ return settleVoid(emptyDirAsync(dir), callback);
+}
+
+export function emptyDirSync(dir: string): void {
+ let items: string[];
+ try {
+ items = fs.readdirSync(dir);
+ } catch {
+ makeDirSync(dir, 0o777);
+ return;
+ }
+ for (const item of items) fs.rmSync(path.join(dir, item), { recursive: true, force: true });
+}
+
+/* ── pathExists, ensureFile, outputFile ───────────────────────────────── */
+
+async function pathExistsAsync(file: string): Promise {
+ try {
+ await fsp.access(file);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function pathExists(file: string): Promise;
+export function pathExists(file: string, callback: Callback): void;
+export function pathExists(file: string, callback?: Callback): Promise {
+ const found = pathExistsAsync(file);
+ if (callback === undefined) return found;
+ return found.then((exists) => {
+ callback(null, exists);
+ return exists;
+ });
+}
+
+export function pathExistsSync(file: string): boolean {
+ return fs.existsSync(file);
+}
+
+async function createFileAsync(file: string): Promise {
+ try {
+ if ((await fsp.stat(file)).isFile()) return;
+ } catch {
+ // Missing: created below.
+ }
+ const dir = path.dirname(file);
+ let dirIsDirectory = true;
+ try {
+ dirIsDirectory = (await fsp.stat(dir)).isDirectory();
+ } catch (err) {
+ if (codeOf(err) !== "ENOENT") throw err;
+ await makeDir(dir, 0o777);
+ }
+ // A parent that is not a directory fails with readdir's ENOTDIR.
+ if (!dirIsDirectory) await fsp.readdir(dir);
+ await fsp.writeFile(file, "", "utf8");
+}
+
+export function ensureFile(file: string): Promise;
+export function ensureFile(file: string, callback: VoidCallback): void;
+export function ensureFile(file: string, callback?: VoidCallback): Promise {
+ return settleVoid(createFileAsync(file), callback);
+}
+
+export function ensureFileSync(file: string): void {
+ try {
+ if (fs.statSync(file).isFile()) return;
+ } catch {
+ // Missing: created below.
+ }
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) makeDirSync(dir, 0o777);
+ fs.writeFileSync(file, "");
+}
+
+async function outputFileAsync(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8 | undefined,
+): Promise {
+ const dir = path.dirname(file);
+ if (!(await pathExistsAsync(dir))) await makeDir(dir, 0o777);
+ await writeData(file, data, options);
+}
+
+export function outputFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8,
+): Promise;
+export function outputFile(file: string, data: string | Uint8Array, callback: VoidCallback): void;
+export function outputFile(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function outputFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function")
+ return settleVoid(outputFileAsync(file, data, undefined), options);
+ return settleVoid(outputFileAsync(file, data, options), callback);
+}
+
+export function outputFileSync(file: string, data: string | Uint8Array): void {
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) makeDirSync(dir, 0o777);
+ fs.writeFileSync(file, data);
+}
+
+/* ── JSON (jsonfile) ──────────────────────────────────────────────────── */
+
+function jsonText(value: unknown, options: WriteOptions | Utf8 | undefined): string {
+ const opts = options === undefined || typeof options === "string" ? undefined : options;
+ const eol = opts === undefined ? "\n" : (opts.EOL ?? "\n");
+ const finalEol = opts === undefined ? true : (opts.finalEOL ?? true);
+ const spaces = opts === undefined ? 0 : (opts.spaces ?? 0);
+ const text = JSON.stringify(value, null, spaces) ?? "undefined";
+ return text.replace(/\n/g, eol) + (finalEol ? eol : "");
+}
+
+function jsonWriteOptions(options: WriteOptions | Utf8 | undefined): WriteFileOptions | undefined {
+ if (options === undefined || typeof options === "string") return undefined;
+ return { mode: options.mode };
+}
+
+function jsonThrows(options: ReadOptions | Utf8 | undefined): boolean {
+ if (options === undefined || typeof options === "string") return true;
+ return options.throws ?? true;
+}
+
+function prefixed(file: string, err: unknown): Error {
+ const error = err instanceof Error ? err : new Error(String(err));
+ error.message = `${file}: ${error.message}`;
+ return error;
+}
+
+function parseJson(file: string, text: string, throws: boolean): any {
+ try {
+ return JSON.parse(text.replace(/^\uFEFF/, ""));
+ } catch (err) {
+ if (!throws) return null;
+ throw prefixed(file, err);
+ }
+}
+
+async function readJsonAsync(file: string, options: ReadOptions | Utf8 | undefined): Promise {
+ const text = await fsp.readFile(file, "utf8");
+ return parseJson(file, text, jsonThrows(options));
+}
+
+// readJson's callback form is not declared.
+export function readJson(file: string, options?: ReadOptions | Utf8): Promise {
+ return readJsonAsync(file, options);
+}
+
+export function readJsonSync(file: string, options?: ReadOptions | Utf8): any {
+ const throws = jsonThrows(options);
+ let text: string;
+ try {
+ text = fs.readFileSync(file, "utf8");
+ } catch (err) {
+ // jsonfile reads inside the same try as the parse.
+ if (!throws) return null;
+ throw prefixed(file, err);
+ }
+ return parseJson(file, text, throws);
+}
+
+async function writeJsonAsync(
+ file: string,
+ value: unknown,
+ options: WriteOptions | Utf8 | undefined,
+): Promise {
+ await writeData(file, jsonText(value, options), jsonWriteOptions(options));
+}
+
+export function writeJson(file: string, value: any, options?: WriteOptions | Utf8): Promise;
+export function writeJson(file: string, value: any, callback: VoidCallback): void;
+export function writeJson(
+ file: string,
+ value: any,
+ options: WriteOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function writeJson(
+ file: string,
+ value: any,
+ options?: WriteOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function")
+ return settleVoid(writeJsonAsync(file, value, undefined), options);
+ return settleVoid(writeJsonAsync(file, value, options), callback);
+}
+
+export function writeJsonSync(file: string, value: any, options?: WriteOptions | Utf8): void {
+ const text = jsonText(value, options);
+ if (options === undefined || typeof options === "string") fs.writeFileSync(file, text, "utf8");
+ else fs.writeFileSync(file, text, { encoding: "utf8", mode: options.mode ?? 0o666 });
+}
+
+async function outputJsonAsync(
+ file: string,
+ value: unknown,
+ options: WriteOptions | Utf8 | undefined,
+): Promise {
+ const dir = path.dirname(file);
+ if (!(await pathExistsAsync(dir))) await makeDir(dir, 0o777);
+ await writeJsonAsync(file, value, options);
+}
+
+export function outputJson(file: string, value: any, options?: WriteOptions | Utf8): Promise;
+export function outputJson(file: string, value: any, callback: VoidCallback): void;
+export function outputJson(
+ file: string,
+ value: any,
+ options: WriteOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function outputJson(
+ file: string,
+ value: any,
+ options?: WriteOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function")
+ return settleVoid(outputJsonAsync(file, value, undefined), options);
+ return settleVoid(outputJsonAsync(file, value, options), callback);
+}
+
+export function outputJsonSync(file: string, value: any, options?: WriteOptions | Utf8): void {
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) makeDirSync(dir, 0o777);
+ writeJsonSync(file, value, options);
+}
+
+/* ── copy, move ───────────────────────────────────────────────────────── */
+
+function isSrcSubdir(src: string, dest: string): boolean {
+ const srcParts = path
+ .resolve(src)
+ .split(path.sep)
+ .filter((part) => part !== "");
+ const destParts = path
+ .resolve(dest)
+ .split(path.sep)
+ .filter((part) => part !== "");
+ for (let i = 0; i < srcParts.length; i++) if (destParts[i] !== srcParts[i]) return false;
+ return true;
+}
+
+function subdirMessage(src: string, dest: string, verb: string): string {
+ return `Cannot ${verb} '${src}' to a subdirectory of itself, '${dest}'.`;
+}
+
+function sameNode(a: Stats, b: Stats): boolean {
+ return b.ino !== 0 && b.dev !== 0 && a.ino === b.ino && a.dev === b.dev;
+}
+
+async function statOrNull(file: string): Promise {
+ try {
+ return await fsp.stat(file);
+ } catch (err) {
+ if (codeOf(err) === "ENOENT") return null;
+ throw err;
+ }
+}
+
+/** fs-extra's checkPaths and checkParentPaths: the source exists, is not
+ * the destination, and a directory never copies or moves into itself.
+ * Answers whether the destination exists. */
+async function checkPaths(src: string, dest: string, verb: string): Promise {
+ const srcStat = await fsp.stat(src);
+ const destStat = await statOrNull(dest);
+ if (destStat !== null && sameNode(srcStat, destStat))
+ throw new Error("Source and destination must not be the same.");
+ if (srcStat.isDirectory() && isSrcSubdir(src, dest))
+ throw new Error(subdirMessage(src, dest, verb));
+ const srcParent = path.resolve(path.dirname(src));
+ let destParent = path.resolve(path.dirname(dest));
+ while (destParent !== srcParent && destParent !== path.parse(destParent).root) {
+ const parentStat = await statOrNull(destParent);
+ if (parentStat === null) break;
+ if (sameNode(srcStat, parentStat)) throw new Error(subdirMessage(src, dest, verb));
+ destParent = path.resolve(path.dirname(destParent));
+ }
+ return destStat !== null;
+}
+
+function statOrNullSync(file: string): Stats | null {
+ try {
+ return fs.statSync(file);
+ } catch (err) {
+ if (codeOf(err) === "ENOENT") return null;
+ throw err;
+ }
+}
+
+/** checkPaths for copySync and moveSync. Answers the destination's stats,
+ * or null when it does not exist. */
+function checkPathsSync(src: string, dest: string, verb: string): Stats | null {
+ const srcStat = fs.statSync(src);
+ const destStat = statOrNullSync(dest);
+ if (destStat !== null && sameNode(srcStat, destStat))
+ throw new Error("Source and destination must not be the same.");
+ if (srcStat.isDirectory() && isSrcSubdir(src, dest))
+ throw new Error(subdirMessage(src, dest, verb));
+ const srcParent = path.resolve(path.dirname(src));
+ let destParent = path.resolve(path.dirname(dest));
+ while (destParent !== srcParent && destParent !== path.parse(destParent).root) {
+ const parentStat = statOrNullSync(destParent);
+ if (parentStat === null) break;
+ if (sameNode(srcStat, parentStat)) throw new Error(subdirMessage(src, dest, verb));
+ destParent = path.resolve(path.dirname(destParent));
+ }
+ return destStat;
+}
+
+interface CopyState {
+ overwrite: boolean;
+ errorOnExist: boolean;
+ dereference: boolean;
+ preserveTimestamps: boolean;
+ filter: CopyFilter | undefined;
+}
+
+async function included(state: CopyState, src: string, dest: string): Promise {
+ const filter = state.filter;
+ if (filter === undefined) return true;
+ return await filter(src, dest);
+}
+
+/** fs-extra's onLink: a symlink source creates a symlink at `dest`. Only
+ * a destination that is itself a symlink is replaced; over a regular file
+ * or a directory, symlink fails with EEXIST, whatever `overwrite` says. */
+async function copyLink(
+ target: string,
+ dest: string,
+ destExists: boolean,
+ dereference: boolean,
+): Promise {
+ if (!destExists) return fsp.symlink(target, dest);
+ let destLinked: string;
+ try {
+ destLinked = await fsp.readlink(dest);
+ } catch (err) {
+ const code = codeOf(err);
+ if (code === "EINVAL" || code === "UNKNOWN") return fsp.symlink(target, dest);
+ throw err;
+ }
+ const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked;
+ checkLinkTargets(target, destTarget, (await fsp.stat(dest)).isDirectory());
+ await fsp.unlink(dest);
+ await fsp.symlink(target, dest);
+}
+
+function copyLinkSync(
+ target: string,
+ dest: string,
+ destExists: boolean,
+ dereference: boolean,
+): void {
+ if (!destExists) {
+ fs.symlinkSync(target, dest);
+ return;
+ }
+ let destLinked: string;
+ try {
+ destLinked = fs.readlinkSync(dest);
+ } catch (err) {
+ const code = codeOf(err);
+ if (code !== "EINVAL" && code !== "UNKNOWN") throw err;
+ fs.symlinkSync(target, dest);
+ return;
+ }
+ const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked;
+ checkLinkTargets(target, destTarget, fs.statSync(dest).isDirectory());
+ fs.unlinkSync(dest);
+ fs.symlinkSync(target, dest);
+}
+
+/** Replacing a destination symlink never points it into its own target,
+ * nor removes a directory link the source lives under. */
+function checkLinkTargets(target: string, destTarget: string, destIsDirectory: boolean): void {
+ if (isSrcSubdir(target, destTarget))
+ throw new Error(`Cannot copy '${target}' to a subdirectory of itself, '${destTarget}'.`);
+ if (destIsDirectory && isSrcSubdir(destTarget, target))
+ throw new Error(`Cannot overwrite '${destTarget}' with '${target}'.`);
+}
+
+async function copyEntry(
+ state: CopyState,
+ src: string,
+ dest: string,
+ destExists: boolean,
+ filtered: boolean,
+): Promise {
+ if (!filtered && !(await included(state, src, dest))) return;
+ const srcStat = state.dereference ? await fsp.stat(src) : await fsp.lstat(src);
+ if (srcStat.isDirectory()) {
+ if (destExists && !(await fsp.stat(dest)).isDirectory())
+ throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`);
+ if (!destExists) await fsp.mkdir(dest);
+ const items = await fsp.readdir(src);
+ while (items.length > 0) {
+ const item = items.pop();
+ if (item === undefined) break;
+ const srcItem = path.join(src, item);
+ const destItem = path.join(dest, item);
+ const destItemStat = await statOrNull(destItem);
+ if (destItemStat !== null && sameNode(await fsp.stat(srcItem), destItemStat))
+ throw new Error("Source and destination must not be the same.");
+ await copyEntry(state, srcItem, destItem, destItemStat !== null, false);
+ }
+ if (!destExists) await fsp.chmod(dest, srcStat.mode);
+ return;
+ }
+ if (srcStat.isSymbolicLink()) {
+ const linked = await fsp.readlink(src);
+ await copyLink(
+ state.dereference ? path.resolve(process.cwd(), linked) : linked,
+ dest,
+ destExists,
+ state.dereference,
+ );
+ return;
+ }
+ if (destExists) {
+ if (state.overwrite) await fsp.unlink(dest);
+ else if (state.errorOnExist) throw new Error(`'${dest}' already exists`);
+ else return;
+ }
+ await fsp.copyFile(src, dest);
+ if (state.preserveTimestamps) {
+ const updated = await fsp.stat(src);
+ if ((srcStat.mode & 0o200) === 0) await fsp.chmod(dest, srcStat.mode | 0o200);
+ await fsp.utimes(dest, updated.atime, updated.mtime);
+ }
+ await fsp.chmod(dest, srcStat.mode);
+}
+
+async function copyAsync(
+ src: string,
+ dest: string,
+ options: CopyOptions | undefined,
+): Promise {
+ const state: CopyState = {
+ overwrite: options === undefined ? true : (options.overwrite ?? true),
+ errorOnExist: options === undefined ? false : (options.errorOnExist ?? false),
+ dereference: options === undefined ? false : (options.dereference ?? false),
+ preserveTimestamps: options === undefined ? false : (options.preserveTimestamps ?? false),
+ filter: options === undefined ? undefined : options.filter,
+ };
+ const destExists = await checkPaths(src, dest, "copy");
+ if (!(await included(state, src, dest))) return;
+ const parent = path.dirname(dest);
+ if (!(await pathExistsAsync(parent))) await makeDir(parent, 0o777);
+ await copyEntry(state, src, dest, destExists, true);
+}
+
+export function copy(src: string, dest: string, options?: CopyOptions): Promise;
+export function copy(src: string, dest: string, callback: VoidCallback): void;
+export function copy(src: string, dest: string, options: CopyOptions, callback: VoidCallback): void;
+export function copy(
+ src: string,
+ dest: string,
+ options?: CopyOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(copyAsync(src, dest, undefined), options);
+ return settleVoid(copyAsync(src, dest, options), callback);
+}
+
+function copyEntrySync(
+ state: CopyOptionsSync,
+ src: string,
+ dest: string,
+ destExists: boolean,
+ filtered: boolean,
+): void {
+ const filter = state.filter;
+ if (!filtered && filter !== undefined && !filter(src, dest)) return;
+ const srcStat = state.dereference === true ? fs.statSync(src) : fs.lstatSync(src);
+ if (srcStat.isDirectory()) {
+ if (destExists && !fs.statSync(dest).isDirectory())
+ throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`);
+ if (!destExists) fs.mkdirSync(dest);
+ for (const item of fs.readdirSync(src)) {
+ const srcItem = path.join(src, item);
+ const destItem = path.join(dest, item);
+ // fs-extra's copyDirItem checks every entry against its destination:
+ // a destination symlink back into the source tree must not let the
+ // overwrite below unlink the source's own file.
+ const destItemStat = statOrNullSync(destItem);
+ if (destItemStat !== null && sameNode(fs.statSync(srcItem), destItemStat))
+ throw new Error("Source and destination must not be the same.");
+ copyEntrySync(state, srcItem, destItem, destItemStat !== null, false);
+ }
+ if (!destExists) fs.chmodSync(dest, srcStat.mode);
+ return;
+ }
+ if (srcStat.isSymbolicLink()) {
+ const dereference = state.dereference === true;
+ const linked = fs.readlinkSync(src);
+ copyLinkSync(
+ dereference ? path.resolve(process.cwd(), linked) : linked,
+ dest,
+ destExists,
+ dereference,
+ );
+ return;
+ }
+ if (destExists) {
+ if (state.overwrite ?? true) fs.unlinkSync(dest);
+ else if (state.errorOnExist === true) throw new Error(`'${dest}' already exists`);
+ else return;
+ }
+ fs.copyFileSync(src, dest);
+ if (state.preserveTimestamps === true) {
+ const updated = fs.statSync(src);
+ if ((srcStat.mode & 0o200) === 0) fs.chmodSync(dest, srcStat.mode | 0o200);
+ fs.utimesSync(dest, updated.atime, updated.mtime);
+ }
+ fs.chmodSync(dest, srcStat.mode);
+}
+
+export function copySync(src: string, dest: string, options?: CopyOptionsSync): void {
+ const opts: CopyOptionsSync = options ?? {};
+ const destStat = checkPathsSync(src, dest, "copy");
+ const filter = opts.filter;
+ if (filter !== undefined && !filter(src, dest)) return;
+ const parent = path.dirname(dest);
+ if (!fs.existsSync(parent)) makeDirSync(parent, 0o777);
+ copyEntrySync(opts, src, dest, destStat !== null, true);
+}
+
+async function moveAsync(
+ src: string,
+ dest: string,
+ options: MoveOptions | undefined,
+): Promise {
+ const overwrite = options === undefined ? false : (options.overwrite ?? false);
+ await checkPaths(src, dest, "move");
+ await makeDir(path.dirname(dest), 0o777);
+ if (overwrite) await removeTree(dest);
+ else if (await pathExistsAsync(dest)) throw new Error("dest already exists.");
+ try {
+ await fsp.rename(src, dest);
+ } catch (err) {
+ if (codeOf(err) !== "EXDEV") throw err;
+ await copyAsync(src, dest, { overwrite, errorOnExist: true });
+ await removeTree(src);
+ }
+}
+
+export function move(src: string, dest: string, options?: MoveOptions): Promise;
+export function move(src: string, dest: string, callback: VoidCallback): void;
+export function move(src: string, dest: string, options: MoveOptions, callback: VoidCallback): void;
+export function move(
+ src: string,
+ dest: string,
+ options?: MoveOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(moveAsync(src, dest, undefined), options);
+ return settleVoid(moveAsync(src, dest, options), callback);
+}
+
+export function moveSync(src: string, dest: string, options?: MoveOptions): void {
+ const overwrite = options === undefined ? false : (options.overwrite ?? false);
+ checkPathsSync(src, dest, "move");
+ makeDirSync(path.dirname(dest), 0o777);
+ if (overwrite) fs.rmSync(dest, { recursive: true, force: true });
+ else if (fs.existsSync(dest)) throw new Error("dest already exists.");
+ try {
+ fs.renameSync(src, dest);
+ } catch (err) {
+ if (codeOf(err) !== "EXDEV") throw err;
+ copySync(src, dest, { overwrite, errorOnExist: true });
+ fs.rmSync(src, { recursive: true, force: true });
+ }
+}
+
+/* ── fs-extra's aliases ───────────────────────────────────────────────── */
+
+export {
+ emptyDir as emptydir,
+ emptyDirSync as emptydirSync,
+ ensureFile as createFile,
+ ensureFileSync as createFileSync,
+ readJson as readJSON,
+ readJsonSync as readJSONSync,
+ writeJson as writeJSON,
+ writeJsonSync as writeJSONSync,
+ outputJson as outputJSON,
+ outputJsonSync as outputJSONSync,
+};
diff --git a/packages/compiler/shims/fs-extra/index.ts b/packages/compiler/shims/fs-extra/index.ts
new file mode 100644
index 000000000..43debf2d4
--- /dev/null
+++ b/packages/compiler/shims/fs-extra/index.ts
@@ -0,0 +1,9 @@
+// scriptc's built-in implementation of the `fs-extra` package, 8.x-11.x API.
+//
+// Programs whose installed fs-extra is 8.x through 11.x compile this module
+// in its place (see packages/compiler/src/frontend/package-shims.ts), and its
+// declarations stand in for @types/fs-extra. The default import, the
+// namespace import and named imports all read the same exports, as they read
+// fs-extra's single CommonJS export object under Node.
+export * from "./core/api.ts";
+export * as default from "./core/api.ts";
diff --git a/packages/compiler/shims/undici/core/agent.ts b/packages/compiler/shims/undici/core/agent.ts
new file mode 100644
index 000000000..c59f3cd1d
--- /dev/null
+++ b/packages/compiler/shims/undici/core/agent.ts
@@ -0,0 +1,163 @@
+// undici's Agent (lib/dispatcher/agent.js): one dispatcher per origin,
+// created by `factory` (default: a Pool, or a Client when connections is 1).
+import { Client } from "./client.ts";
+import { Dispatcher, DispatcherBase } from "./dispatcher.ts";
+import { InvalidArgumentError } from "./errors.ts";
+import { Pool } from "./pool.ts";
+import type {
+ AgentOptions,
+ DispatchHandlers,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./types.ts";
+
+interface AgentEntry {
+ dispatcher: Dispatcher;
+ count: number;
+}
+
+export class Agent extends DispatcherBase {
+ __options: PoolOptions;
+ __factory: ((origin: string | URL, opts: object) => Dispatcher) | null;
+ __clients: Map;
+ __onDrain: (origin: URL, targets: Dispatcher[]) => void;
+ __onConnect: (origin: URL, targets: Dispatcher[]) => void;
+ __onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void;
+ __onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void;
+
+ constructor(options: AgentOptions | undefined, profile: Profile) {
+ super(profile);
+ const opts: AgentOptions = options ?? {};
+ const interceptors = opts.interceptors;
+ if (profile.major < 7 && interceptors !== undefined && interceptors.Agent !== undefined) {
+ this.__interceptors = interceptors.Agent;
+ }
+ this.__options = {
+ keepAliveTimeout: opts.keepAliveTimeout,
+ keepAliveMaxTimeout: opts.keepAliveMaxTimeout,
+ keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold,
+ pipelining: opts.pipelining,
+ maxHeaderSize: opts.maxHeaderSize,
+ bodyTimeout: opts.bodyTimeout,
+ headersTimeout: opts.headersTimeout,
+ strictContentLength: opts.strictContentLength,
+ maxRequestsPerClient: opts.maxRequestsPerClient,
+ maxResponseSize: opts.maxResponseSize,
+ connectTimeout: opts.connectTimeout,
+ localAddress: opts.localAddress,
+ connections: opts.connections,
+ connect: opts.connect,
+ interceptors:
+ interceptors !== undefined
+ ? { Pool: interceptors.Pool, Client: interceptors.Client }
+ : undefined,
+ };
+ const factory = opts.factory;
+ this.__factory = factory !== undefined ? factory : null;
+ this.__clients = new Map();
+ this.__onDrain = (origin: URL, targets: Dispatcher[]): void => {
+ this.emit("drain", origin, [this as Dispatcher, ...targets]);
+ };
+ this.__onConnect = (origin: URL, targets: Dispatcher[]): void => {
+ this.emit("connect", origin, [this as Dispatcher, ...targets]);
+ };
+ this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("disconnect", origin, [this as Dispatcher, ...targets], err);
+ };
+ this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("connectionError", origin, [this as Dispatcher, ...targets], err);
+ };
+ }
+
+ __runningCount(): number {
+ let ret = 0;
+ for (const entry of this.__clients.values()) ret += entry.dispatcher.__runningCount();
+ return ret;
+ }
+
+ __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
+ const origin = opts.origin;
+ if (origin === undefined || origin === "") {
+ throw new InvalidArgumentError("opts.origin must be a non-empty string or URL.");
+ }
+ const key = typeof origin === "string" ? origin : origin.href;
+ const existing = this.__clients.get(key);
+ let dispatcher: Dispatcher;
+ if (existing !== undefined) {
+ dispatcher = existing.dispatcher;
+ } else {
+ const factory = this.__factory;
+ dispatcher =
+ factory !== null
+ ? factory(origin, this.__options)
+ : this.__defaultFactory(origin, this.__options);
+ const entry: AgentEntry = { dispatcher, count: 0 };
+ if (this.__profile.major >= 7) {
+ // 7.x closes and forgets an origin's dispatcher once all of its
+ // connections are gone.
+ const closeIfUnused = (connected: boolean): void => {
+ const current = this.__clients.get(key);
+ if (current !== undefined) {
+ if (connected) current.count -= 1;
+ if (current.count <= 0) {
+ this.__clients.delete(key);
+ if (!current.dispatcher.__isDestroyed()) current.dispatcher.close();
+ }
+ }
+ };
+ dispatcher.on("drain", this.__onDrain);
+ dispatcher.on("connect", (o: URL, targets: Dispatcher[]) => {
+ const current = this.__clients.get(key);
+ if (current !== undefined) current.count += 1;
+ this.__onConnect(o, targets);
+ });
+ dispatcher.on("disconnect", (o: URL, targets: Dispatcher[], err: Error) => {
+ closeIfUnused(true);
+ this.__onDisconnect(o, targets, err);
+ });
+ dispatcher.on("connectionError", (o: URL, targets: Dispatcher[], err: Error) => {
+ closeIfUnused(false);
+ this.__onConnectionError(o, targets, err);
+ });
+ } else {
+ dispatcher.on("drain", this.__onDrain);
+ dispatcher.on("connect", this.__onConnect);
+ dispatcher.on("disconnect", this.__onDisconnect);
+ dispatcher.on("connectionError", this.__onConnectionError);
+ }
+ this.__clients.set(key, entry);
+ }
+ return dispatcher.dispatch(opts, handler);
+ }
+
+ /** The default factory: a Client when `connections` is 1, else a Pool, of
+ * the same undici version. */
+ __defaultFactory(origin: string | URL, opts: PoolOptions): Dispatcher {
+ return opts.connections === 1
+ ? this.__createClient(origin, opts)
+ : this.__createPool(origin, opts);
+ }
+
+ __createClient(origin: string | URL, opts: PoolOptions): Dispatcher {
+ return new Client(origin, opts, this.__profile);
+ }
+
+ __createPool(origin: string | URL, opts: PoolOptions): Dispatcher {
+ return new Pool(origin, opts, this.__profile);
+ }
+
+ __close(): Promise {
+ const closing: Promise[] = [];
+ for (const entry of this.__clients.values()) closing.push(entry.dispatcher.close());
+ if (this.__profile.major >= 7) this.__clients.clear();
+ return Promise.all(closing).then(() => {});
+ }
+
+ __destroy(err: Error): Promise {
+ const destroying: Promise[] = [];
+ for (const entry of this.__clients.values()) destroying.push(entry.dispatcher.destroy(err));
+ if (this.__profile.major >= 7) this.__clients.clear();
+ return Promise.all(destroying).then(() => {});
+ }
+}
diff --git a/packages/compiler/shims/undici/core/api.ts b/packages/compiler/shims/undici/core/api.ts
new file mode 100644
index 000000000..b94d354ce
--- /dev/null
+++ b/packages/compiler/shims/undici/core/api.ts
@@ -0,0 +1,93 @@
+// undici's top-level request() (index.js makeDispatcher over
+// lib/api/api-request.js) and the global dispatcher
+// (lib/global.js: one process-wide slot shared by every undici copy).
+import { requestThrough } from "./dispatcher.ts";
+import type { Dispatcher, RequestOptions, ResponseData } from "./dispatcher.ts";
+import { parseOrigin, parseURL } from "./util.ts";
+import type { IncomingHttpHeaders, Profile, RequestBody, UndiciHeaders } from "./types.ts";
+
+/** request(url, options)'s options: Dispatcher.RequestOptions without
+ * origin/path, an optional method, and the dispatcher to use. */
+export interface TopLevelRequestOptions {
+ dispatcher?: Dispatcher;
+ method?: string;
+ path?: string;
+ body?: RequestBody;
+ headers?: UndiciHeaders;
+ idempotent?: boolean;
+ blocking?: boolean;
+ headersTimeout?: number | null;
+ bodyTimeout?: number | null;
+ reset?: boolean;
+ throwOnError?: boolean;
+ opaque?: unknown;
+ signal?: AbortSignal | null;
+ onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void;
+ highWaterMark?: number;
+}
+
+let globalDispatcher: Dispatcher | null = null;
+let globalDispatcherSet = false;
+
+export function setGlobalDispatcher(dispatcher: Dispatcher): void {
+ globalDispatcher = dispatcher;
+ globalDispatcherSet = true;
+}
+
+/** The global dispatcher, created on first use by the asking version's
+ * Agent (undici stores whichever copy asked first). */
+export function getGlobalDispatcher(create: () => Dispatcher): Dispatcher {
+ let dispatcher = globalDispatcher;
+ if (dispatcher === null) {
+ dispatcher = create();
+ globalDispatcher = dispatcher;
+ }
+ return dispatcher;
+}
+
+/** True once a program installed its own global dispatcher. */
+export function hasCustomGlobalDispatcher(): boolean {
+ return globalDispatcherSet;
+}
+
+export function request(
+ url: string | URL,
+ options: TopLevelRequestOptions | undefined,
+ createGlobal: () => Dispatcher,
+ profile: Profile,
+): Promise {
+ // makeDispatcher validates synchronously: an invalid URL throws at the
+ // call rather than rejecting.
+ {
+ const opts: TopLevelRequestOptions = options ?? {};
+ let target: URL;
+ const path = opts.path;
+ if (path !== undefined) {
+ const prefixed = path.startsWith("/") ? path : `/${path}`;
+ target = new URL(parseOrigin(url, profile).origin + prefixed);
+ } else {
+ target = parseURL(url, profile);
+ }
+ const dispatcher = opts.dispatcher ?? getGlobalDispatcher(createGlobal);
+ const method =
+ opts.method !== undefined && opts.method !== "" ? opts.method : opts.body ? "PUT" : "GET";
+ const dispatchOptions: RequestOptions = {
+ origin: target.origin,
+ path: target.search ? `${target.pathname}${target.search}` : target.pathname,
+ method,
+ body: opts.body,
+ headers: opts.headers,
+ idempotent: opts.idempotent,
+ blocking: opts.blocking,
+ headersTimeout: opts.headersTimeout,
+ bodyTimeout: opts.bodyTimeout,
+ reset: opts.reset,
+ throwOnError: opts.throwOnError,
+ opaque: opts.opaque,
+ signal: opts.signal,
+ onInfo: opts.onInfo,
+ highWaterMark: opts.highWaterMark,
+ };
+ return requestThrough(dispatcher, dispatchOptions, profile);
+ }
+}
diff --git a/packages/compiler/shims/undici/core/client.ts b/packages/compiler/shims/undici/core/client.ts
new file mode 100644
index 000000000..fb3404aab
--- /dev/null
+++ b/packages/compiler/shims/undici/core/client.ts
@@ -0,0 +1,1169 @@
+// undici's Client (lib/dispatcher/client.js + client-h1.js): HTTP/1.1 over
+// one TCP/TLS connection at a time, with keep-alive reuse, a request queue,
+// headers/body/idle timeouts and the same request lifecycle callbacks.
+import type { Socket } from "node:net";
+import { buildConnector } from "./connector.ts";
+import { channels } from "./diagnostics.ts";
+import { DispatcherBase } from "./dispatcher.ts";
+import type { Dispatcher } from "./dispatcher.ts";
+import {
+ BodyTimeoutError,
+ HeadersOverflowError,
+ HeadersTimeoutError,
+ HTTPParserError,
+ InformationalError,
+ InvalidArgumentError,
+ NotSupportedError,
+ RequestAbortedError,
+ RequestContentLengthMismatchError,
+ ResponseContentLengthMismatchError,
+ ResponseExceededMaxSizeError,
+ SocketError,
+ UndiciError,
+} from "./errors.ts";
+import type { SocketInfo } from "./errors.ts";
+import { Request } from "./request.ts";
+import { FastTimer } from "./timers.ts";
+import { parseKeepAliveTimeout, parseOrigin } from "./util.ts";
+import type {
+ ClientOptions,
+ Connector,
+ ConnectorOptions,
+ DispatchHandlers,
+ DispatchOptions,
+ Profile,
+} from "./types.ts";
+
+const TIMEOUT_NONE = 0;
+const TIMEOUT_HEADERS = 1;
+const TIMEOUT_BODY = 2;
+const TIMEOUT_IDLE = 3;
+
+export class Client extends DispatcherBase {
+ __url: URL;
+ __connector: Connector;
+ __socket: Socket | null;
+ __connecting: boolean;
+ __queue: (Request | null)[];
+ __runningIdx: number;
+ __pendingIdx: number;
+ __pipelining: number;
+ __hostHeader: string;
+ __maxHeaderSize: number;
+ __keepAliveDefaultTimeout: number;
+ __keepAliveMaxTimeout: number;
+ __keepAliveTimeoutThreshold: number;
+ __keepAliveTimeoutValue: number;
+ __headersTimeout: number;
+ __bodyTimeout: number;
+ __strictContentLength: boolean;
+ __maxRequests: number;
+ __maxResponseSize: number;
+ __resuming: number;
+ __needDrain: number;
+ __closedResolve: (() => void) | null;
+ __serverName: string | null;
+ __localAddress: string | null;
+ // Per-connection state (one socket at a time).
+ __sockNoRef: boolean;
+ __sockReset: boolean;
+ __sockBlocking: boolean;
+ __sockCounter: number;
+ __parser: ResponseParser | null;
+ __sockError: Error | null;
+ // The parser timer, held as closures over its handle (a nullable timer
+ // field does not compile under @types/node).
+ __timerCancel: (() => void) | null;
+ __timerRefresh: (() => void) | null;
+ __timerType: number;
+
+ constructor(url: string | URL, options: ClientOptions | undefined, profile: Profile) {
+ super(profile);
+ const opts: ClientOptions = options ?? {};
+ const pipelining = opts.pipelining ?? 1;
+ if (!Number.isInteger(pipelining) || pipelining < 0) {
+ throw new InvalidArgumentError("invalid pipelining");
+ }
+ if (pipelining > 1) {
+ throw new NotSupportedError(
+ "scriptc's undici client does not pipeline more than one request yet",
+ );
+ }
+ const maxHeaderSize = opts.maxHeaderSize ?? null;
+ if (maxHeaderSize !== null && !Number.isFinite(maxHeaderSize)) {
+ throw new InvalidArgumentError("invalid maxHeaderSize");
+ }
+ const connectTimeout = opts.connectTimeout ?? null;
+ if (connectTimeout !== null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) {
+ throw new InvalidArgumentError("invalid connectTimeout");
+ }
+ const keepAliveTimeout = opts.keepAliveTimeout ?? null;
+ if (
+ keepAliveTimeout !== null &&
+ (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)
+ ) {
+ throw new InvalidArgumentError("invalid keepAliveTimeout");
+ }
+ const keepAliveMaxTimeout = opts.keepAliveMaxTimeout ?? null;
+ if (
+ keepAliveMaxTimeout !== null &&
+ (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)
+ ) {
+ throw new InvalidArgumentError("invalid keepAliveMaxTimeout");
+ }
+ const keepAliveTimeoutThreshold = opts.keepAliveTimeoutThreshold ?? null;
+ if (keepAliveTimeoutThreshold !== null && !Number.isFinite(keepAliveTimeoutThreshold)) {
+ throw new InvalidArgumentError("invalid keepAliveTimeoutThreshold");
+ }
+ const headersTimeout = opts.headersTimeout ?? null;
+ if (headersTimeout !== null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError("headersTimeout must be a positive integer or zero");
+ }
+ const bodyTimeout = opts.bodyTimeout ?? null;
+ if (bodyTimeout !== null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError("bodyTimeout must be a positive integer or zero");
+ }
+ const maxRequests = opts.maxRequestsPerClient ?? 0;
+ if (!Number.isInteger(maxRequests) || maxRequests < 0) {
+ throw new InvalidArgumentError("maxRequestsPerClient must be a positive number");
+ }
+ const maxResponseSize = opts.maxResponseSize ?? -1;
+ if (!Number.isInteger(maxResponseSize) || maxResponseSize < -1) {
+ throw new InvalidArgumentError("maxResponseSize must be a positive number");
+ }
+
+ const connect = opts.connect ?? null;
+ if (typeof connect === "function") {
+ this.__connector = connect;
+ } else {
+ this.__connector = buildConnector(
+ connect !== null
+ ? {
+ ...connect,
+ timeout: connect.timeout !== undefined ? connect.timeout : connectTimeout,
+ }
+ : { timeout: connectTimeout },
+ );
+ }
+ // 5.x composes `interceptors.Client` around dispatch; 7.x dropped the
+ // option and ignores it.
+ const interceptors = opts.interceptors;
+ if (profile.major < 7 && interceptors !== undefined && interceptors.Client !== undefined) {
+ this.__interceptors = interceptors.Client;
+ }
+
+ this.__url = parseOrigin(url, profile);
+ this.__socket = null;
+ this.__connecting = false;
+ this.__queue = [];
+ this.__runningIdx = 0;
+ this.__pendingIdx = 0;
+ this.__pipelining = pipelining;
+ this.__hostHeader = `host: ${this.__url.hostname}${this.__url.port ? `:${this.__url.port}` : ""}\r\n`;
+ this.__maxHeaderSize = maxHeaderSize !== null && maxHeaderSize !== 0 ? maxHeaderSize : 16384;
+ this.__keepAliveDefaultTimeout = keepAliveTimeout ?? 4e3;
+ this.__keepAliveMaxTimeout = keepAliveMaxTimeout ?? 600e3;
+ this.__keepAliveTimeoutThreshold =
+ keepAliveTimeoutThreshold ?? (profile.major >= 7 ? 2e3 : 1e3);
+ this.__keepAliveTimeoutValue = this.__keepAliveDefaultTimeout;
+ const defaultTimeout = profile.major >= 7 ? 300e3 : 30e3;
+ this.__headersTimeout = headersTimeout ?? defaultTimeout;
+ this.__bodyTimeout = bodyTimeout ?? defaultTimeout;
+ this.__strictContentLength = opts.strictContentLength ?? true;
+ this.__maxRequests = maxRequests;
+ this.__maxResponseSize = maxResponseSize;
+ this.__resuming = 0;
+ this.__needDrain = 0;
+ this.__closedResolve = null;
+ this.__serverName = null;
+ this.__localAddress = opts.localAddress ?? null;
+ this.__sockNoRef = false;
+ this.__sockReset = false;
+ this.__sockBlocking = false;
+ this.__sockCounter = 0;
+ this.__parser = null;
+ this.__sockError = null;
+ this.__timerCancel = null;
+ this.__timerRefresh = null;
+ this.__timerType = TIMEOUT_NONE;
+ }
+
+ get pipelining(): number {
+ return this.__pipelining;
+ }
+
+ __pendingCount(): number {
+ return this.__queue.length - this.__pendingIdx;
+ }
+
+ __runningCount(): number {
+ return this.__pendingIdx - this.__runningIdx;
+ }
+
+ __sizeCount(): number {
+ return this.__queue.length - this.__runningIdx;
+ }
+
+ __connectedFlag(): boolean {
+ const socket = this.__socket;
+ return socket !== null && !this.__connecting && !socket.destroyed;
+ }
+
+ __busy(): boolean {
+ return (
+ (this.__socket !== null && (this.__sockReset || this.__sockBlocking)) ||
+ this.__sizeCount() >= (this.__pipelining || 1) ||
+ this.__pendingCount() > 0
+ );
+ }
+
+ __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
+ const originValue = opts.origin;
+ const origin =
+ originValue === undefined || originValue === ""
+ ? this.__url.origin
+ : typeof originValue === "string"
+ ? originValue
+ : originValue.origin;
+ const request = new Request(origin, opts, handler, this.__profile);
+ this.__queue.push(request);
+ if (this.__resuming === 0) this.__resume(true);
+ if (this.__resuming !== 0 && this.__needDrain !== 2 && this.__busy()) this.__needDrain = 2;
+ return this.__needDrain < 2;
+ }
+
+ __close(): Promise {
+ return new Promise((resolve) => {
+ if (this.__sizeCount() === 0) {
+ this.destroy().then(() => resolve());
+ } else {
+ this.__closedResolve = resolve;
+ }
+ });
+ }
+
+ __destroy(err: Error): Promise {
+ return new Promise((resolve) => {
+ const requests = this.__queue.splice(this.__pendingIdx);
+ for (const request of requests) if (request !== null) this.__errorRequest(request, err);
+ const done = (): void => {
+ const closed = this.__closedResolve;
+ if (closed !== null) {
+ this.__closedResolve = null;
+ closed();
+ }
+ resolve();
+ };
+ const socket = this.__socket;
+ if (socket === null) {
+ queueMicrotask(done);
+ } else {
+ socket.on("close", () => done());
+ this.__destroySocket(socket, err);
+ }
+ this.__resume(false);
+ });
+ }
+
+ /** util.destroy(socket, err): remember the first error for 'close'. */
+ __destroySocket(socket: Socket, err: Error): void {
+ if (socket === this.__socket && this.__sockError === null) this.__sockError = err;
+ socket.destroy();
+ }
+
+ __errorRequest(request: Request, err: Error): void {
+ try {
+ request.onError(err);
+ } catch (e) {
+ this.emit("error", e as Error);
+ }
+ }
+
+ __socketInfo(): SocketInfo {
+ const socket = this.__socket;
+ if (socket === null) return {};
+ return {
+ localAddress: socket.localAddress,
+ localPort: socket.localPort,
+ remoteAddress: socket.remoteAddress,
+ remotePort: socket.remotePort,
+ remoteFamily: socket.remoteFamily,
+ timeout: undefined,
+ bytesWritten: socket.bytesWritten,
+ bytesRead: socket.bytesRead,
+ };
+ }
+
+ /** The parser timer: 5.x uses native timers throughout; 7.x drives the
+ * headers and body timeouts from its fast timers. */
+ __setTimeout(delay: number, type: number): void {
+ this.__clearTimer();
+ this.__timerType = type;
+ if (delay > 0) {
+ if (this.__profile.major >= 7 && (type === TIMEOUT_HEADERS || type === TIMEOUT_BODY)) {
+ const fast = new FastTimer(() => this.__onTimeout(), delay);
+ this.__timerCancel = () => fast.clear();
+ this.__timerRefresh = () => fast.refresh();
+ } else {
+ const timer = setTimeout(() => this.__onTimeout(), delay);
+ timer.unref();
+ this.__timerCancel = () => clearTimeout(timer);
+ this.__timerRefresh = () => {
+ timer.refresh();
+ };
+ }
+ }
+ }
+
+ __clearTimer(): void {
+ const cancel = this.__timerCancel;
+ this.__timerCancel = null;
+ this.__timerRefresh = null;
+ if (cancel !== null) cancel();
+ }
+
+ __refreshTimer(): void {
+ const refresh = this.__timerRefresh;
+ if (refresh !== null) refresh();
+ }
+
+ __onTimeout(): void {
+ this.__timerCancel = null;
+ this.__timerRefresh = null;
+ const socket = this.__socket;
+ const parser = this.__parser;
+ if (socket === null || parser === null) return;
+ if (this.__timerType === TIMEOUT_HEADERS) {
+ this.__destroySocket(socket, new HeadersTimeoutError());
+ } else if (this.__timerType === TIMEOUT_BODY) {
+ if (!parser.paused) this.__destroySocket(socket, new BodyTimeoutError());
+ } else if (this.__timerType === TIMEOUT_IDLE) {
+ this.__destroySocket(socket, new InformationalError("socket idle timeout"));
+ }
+ }
+
+ __connect(): void {
+ const url = this.__url;
+ let hostname = url.hostname;
+ if (hostname.startsWith("[")) {
+ const idx = hostname.indexOf("]");
+ hostname = hostname.slice(1, idx);
+ }
+ this.__connecting = true;
+ const params: ConnectorOptions = {
+ host: url.host,
+ hostname,
+ protocol: url.protocol,
+ port: url.port,
+ servername: this.__serverName ?? undefined,
+ localAddress: this.__localAddress,
+ };
+ const connectParams = {
+ host: url.host,
+ hostname,
+ protocol: url.protocol,
+ port: url.port,
+ servername: this.__serverName,
+ localAddress: this.__localAddress,
+ };
+ if (channels.beforeConnect.hasSubscribers) {
+ channels.beforeConnect.publish({ connectParams });
+ }
+ let settled = false;
+ const onConnected = (err: Error | null, socket: Socket | null): void => {
+ if (settled) return;
+ settled = true;
+ // undici awaits the connector's promise: continue on a later tick.
+ queueMicrotask(() => {
+ if (err !== null || socket === null)
+ this.__onConnectError(err ?? new SocketError("connect failed"), connectParams);
+ else this.__onConnected(socket, connectParams);
+ });
+ };
+ try {
+ this.__connector(params, onConnected);
+ } catch (err) {
+ onConnected(err as Error, null);
+ }
+ }
+
+ __onConnected(
+ socket: Socket,
+ connectParams: {
+ host: string;
+ hostname: string;
+ protocol: string;
+ port: string;
+ servername: string | null;
+ localAddress: string | null;
+ },
+ ): void {
+ this.__connecting = false;
+ if (this.destroyed) {
+ socket.destroy();
+ this.__resume(false);
+ return;
+ }
+ this.__socket = socket;
+ this.__sockNoRef = false;
+ this.__sockReset = false;
+ this.__sockBlocking = false;
+ this.__sockCounter = 0;
+ this.__sockError = null;
+ const parser = new ResponseParser(this, socket);
+ this.__parser = parser;
+ socket.on("data", (chunk: Buffer) => {
+ if (this.__socket === socket) parser.execute(chunk);
+ });
+ socket.on("error", (err: Error) => {
+ if (this.__socket === socket) this.__onSocketError(socket, err);
+ });
+ socket.on("end", () => {
+ if (this.__socket === socket) this.__onSocketEnd(socket);
+ });
+ socket.on("close", () => {
+ if (this.__socket === socket) this.__onSocketClose(socket);
+ });
+ if (channels.connected.hasSubscribers) {
+ channels.connected.publish({ connectParams });
+ }
+ this.emit("connect", this.__url, [this as Dispatcher]);
+ this.__resume(false);
+ }
+
+ __onConnectError(
+ err: Error,
+ connectParams: {
+ host: string;
+ hostname: string;
+ protocol: string;
+ port: string;
+ servername: string | null;
+ localAddress: string | null;
+ },
+ ): void {
+ this.__connecting = false;
+ if (channels.connectError.hasSubscribers) {
+ channels.connectError.publish({ connectParams, error: err });
+ }
+ this.__onError(err);
+ this.emit("connectionError", this.__url, [this as Dispatcher], err);
+ this.__resume(false);
+ }
+
+ /** client.js onError: with nothing running, a connection-level failure
+ * fails every queued request. */
+ __onError(err: Error): void {
+ const code = errorCode(err);
+ if (this.__runningCount() === 0 && code !== "UND_ERR_INFO" && code !== "UND_ERR_SOCKET") {
+ const requests = this.__queue.splice(this.__runningIdx);
+ for (const request of requests) if (request !== null) this.__errorRequest(request, err);
+ }
+ }
+
+ __onSocketError(socket: Socket, err: Error): void {
+ const parser = this.__parser;
+ if (
+ errorCode(err) === "ECONNRESET" &&
+ parser !== null &&
+ parser.statusCode !== 0 &&
+ !parser.shouldKeepAlive
+ ) {
+ parser.finishOnEof();
+ return;
+ }
+ this.__destroySocket(socket, err);
+ this.__onError(err);
+ }
+
+ __onSocketEnd(socket: Socket): void {
+ const parser = this.__parser;
+ if (parser !== null && parser.statusCode !== 0 && !parser.shouldKeepAlive) {
+ parser.finishOnEof();
+ return;
+ }
+ this.__destroySocket(socket, new SocketError("other side closed", this.__socketInfo()));
+ }
+
+ __onSocketClose(_socket: Socket): void {
+ const parser = this.__parser;
+ const recorded = this.__sockError;
+ this.__sockError = null;
+ if (
+ recorded === null &&
+ parser !== null &&
+ parser.statusCode !== 0 &&
+ !parser.shouldKeepAlive
+ ) {
+ parser.finishOnEof();
+ }
+ const err = recorded ?? new SocketError("closed", this.__socketInfo());
+ this.__parser = null;
+ this.__socket = null;
+ this.__clearTimer();
+ this.__timerType = TIMEOUT_NONE;
+ if (this.destroyed) {
+ const requests = this.__queue.splice(this.__runningIdx);
+ for (const request of requests) if (request !== null) this.__errorRequest(request, err);
+ } else if (this.__runningCount() > 0 && errorCode(err) !== "UND_ERR_INFO") {
+ const request = this.__queue[this.__runningIdx];
+ this.__queue[this.__runningIdx++] = null;
+ if (request !== null && request !== undefined) this.__errorRequest(request, err);
+ }
+ this.__pendingIdx = this.__runningIdx;
+ this.emit("disconnect", this.__url, [this as Dispatcher], err);
+ this.__resume(false);
+ }
+
+ __emitDrain(): void {
+ this.__needDrain = 0;
+ this.emit("drain", this.__url, [this as Dispatcher]);
+ }
+
+ __resume(sync: boolean): void {
+ if (this.__resuming === 2) return;
+ this.__resuming = 2;
+ this.__resumeLoop(sync);
+ this.__resuming = 0;
+ if (this.__runningIdx > 256) {
+ this.__queue.splice(0, this.__runningIdx);
+ this.__pendingIdx -= this.__runningIdx;
+ this.__runningIdx = 0;
+ }
+ }
+
+ __resumeLoop(sync: boolean): void {
+ for (;;) {
+ if (this.destroyed) return;
+ if (this.closed && this.__sizeCount() === 0) {
+ this.destroy();
+ return;
+ }
+ const socket = this.__socket;
+ const parser = this.__parser;
+ if (socket !== null && parser !== null) {
+ if (this.__sizeCount() === 0) {
+ if (!this.__sockNoRef) {
+ socket.unref();
+ this.__sockNoRef = true;
+ }
+ } else if (this.__sockNoRef) {
+ socket.ref();
+ this.__sockNoRef = false;
+ }
+ if (this.__sizeCount() === 0) {
+ if (this.__timerType !== TIMEOUT_IDLE)
+ this.__setTimeout(this.__keepAliveTimeoutValue, TIMEOUT_IDLE);
+ } else if (this.__runningCount() > 0 && parser.statusCode < 200) {
+ if (this.__timerType !== TIMEOUT_HEADERS) {
+ const request = this.__queue[this.__runningIdx];
+ const headersTimeout =
+ request !== null && request !== undefined && request.headersTimeout !== null
+ ? request.headersTimeout
+ : this.__headersTimeout;
+ this.__setTimeout(headersTimeout, TIMEOUT_HEADERS);
+ }
+ }
+ }
+
+ if (this.__busy()) {
+ this.__needDrain = 2;
+ } else if (this.__needDrain === 2) {
+ if (sync) {
+ this.__needDrain = 1;
+ process.nextTick(() => this.__emitDrain());
+ } else {
+ this.__emitDrain();
+ }
+ continue;
+ }
+
+ if (this.__pendingCount() === 0) return;
+ if (this.__runningCount() >= (this.__pipelining || 1)) return;
+
+ const request = this.__queue[this.__pendingIdx]!;
+ if (this.__url.protocol === "https:" && this.__serverName !== request.servername) {
+ if (this.__runningCount() > 0) return;
+ this.__serverName = request.servername;
+ if (socket !== null) {
+ this.__destroySocket(socket, new InformationalError("servername changed"));
+ return;
+ }
+ }
+ if (this.__connecting) return;
+ if (socket === null) {
+ this.__connect();
+ continue;
+ }
+ if (socket.destroyed || this.__sockReset || this.__sockBlocking) return;
+ if (this.__runningCount() > 0 && !request.idempotent) return;
+
+ if (!request.aborted && this.__write(request, socket)) {
+ this.__pendingIdx++;
+ } else {
+ this.__queue.splice(this.__pendingIdx, 1);
+ }
+ }
+ }
+
+ __write(request: Request, socket: Socket): boolean {
+ const method = request.method;
+ const body = request.body;
+ const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH";
+ // util.bodyLength: a missing body has length 0.
+ let contentLength: number | null = body !== null ? body.length : 0;
+ if (contentLength === 0 && !expectsPayload) contentLength = null;
+ const declared = request.contentLength;
+ const sendsLength =
+ method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE";
+ const mismatch =
+ declared !== null &&
+ declared !== contentLength &&
+ (this.__profile.major < 7 || (sendsLength && contentLength !== null && contentLength > 0));
+ if (mismatch) {
+ if (this.__strictContentLength) {
+ this.__errorRequest(request, new RequestContentLengthMismatchError());
+ return false;
+ }
+ process.emitWarning(new RequestContentLengthMismatchError());
+ }
+
+ const abort = (err?: Error): void => {
+ if (request.aborted || request.completed) return;
+ this.__errorRequest(request, err ?? new RequestAbortedError());
+ this.__destroySocket(socket, new InformationalError("aborted"));
+ };
+ try {
+ request.onConnect(abort);
+ } catch (err) {
+ this.__errorRequest(request, err as Error);
+ }
+ if (request.aborted) return false;
+
+ if (method === "HEAD") this.__sockReset = true;
+ if (request.reset !== null) this.__sockReset = request.reset;
+ if (this.__maxRequests > 0 && this.__sockCounter++ >= this.__maxRequests)
+ this.__sockReset = true;
+ if (request.blocking) this.__sockBlocking = true;
+
+ let header = `${method} ${request.path} HTTP/1.1\r\n`;
+ header += request.host !== null ? `host: ${request.host}\r\n` : this.__hostHeader;
+ if (this.__pipelining !== 0 && !(this.__profile.major >= 7 && this.__sockReset)) {
+ header += "connection: keep-alive\r\n";
+ } else {
+ header += "connection: close\r\n";
+ }
+ header += request.headers;
+ if (channels.sendHeaders.hasSubscribers) {
+ channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header });
+ }
+ if (body === null) {
+ if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`);
+ else socket.write(`${header}\r\n`);
+ } else {
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`);
+ socket.write(body);
+ request.onBodySent(body);
+ if (!expectsPayload && (this.__profile.major < 7 || request.reset !== false))
+ this.__sockReset = true;
+ }
+ request.onRequestSent();
+ return true;
+ }
+
+ // ResponseParser callbacks.
+
+ __onHeadersComplete(parser: ResponseParser, socket: Socket): number {
+ const request = this.__queue[this.__runningIdx];
+ if (request === null || request === undefined || socket.destroyed) return -1;
+ const statusCode = parser.statusCode;
+ if (statusCode === 100) {
+ this.__destroySocket(socket, new SocketError("bad response", this.__socketInfo()));
+ return -1;
+ }
+ if (statusCode >= 200) {
+ const bodyTimeout = request.bodyTimeout !== null ? request.bodyTimeout : this.__bodyTimeout;
+ this.__setTimeout(bodyTimeout, TIMEOUT_BODY);
+ }
+ if (parser.shouldKeepAlive && this.__pipelining !== 0) {
+ const hint =
+ parser.keepAliveHeader !== "" ? parseKeepAliveTimeout(parser.keepAliveHeader) : null;
+ if (hint !== null) {
+ const timeout = Math.min(
+ hint - this.__keepAliveTimeoutThreshold,
+ this.__keepAliveMaxTimeout,
+ );
+ if (timeout <= 0) this.__sockReset = true;
+ else this.__keepAliveTimeoutValue = timeout;
+ } else {
+ this.__keepAliveTimeoutValue = this.__keepAliveDefaultTimeout;
+ }
+ } else {
+ this.__sockReset = true;
+ }
+ let pause = false;
+ try {
+ pause =
+ request.onHeaders(statusCode, parser.headers, () => parser.resume(), parser.statusText) ===
+ false;
+ } catch (err) {
+ this.__destroySocket(socket, err as Error);
+ return -1;
+ }
+ if (request.method === "HEAD") return 1;
+ if (statusCode < 200) return 1;
+ if (this.__sockBlocking) {
+ this.__sockBlocking = false;
+ this.__resume(false);
+ }
+ return pause ? 2 : 0;
+ }
+
+ __onBody(parser: ResponseParser, socket: Socket, chunk: Buffer): boolean {
+ const request = this.__queue[this.__runningIdx];
+ if (request === null || request === undefined || socket.destroyed) return false;
+ if (this.__timerType === TIMEOUT_BODY) this.__refreshTimer();
+ if (this.__maxResponseSize > -1 && parser.bytesRead + chunk.length > this.__maxResponseSize) {
+ this.__destroySocket(socket, new ResponseExceededMaxSizeError());
+ return false;
+ }
+ parser.bytesRead += chunk.length;
+ try {
+ return request.onData(chunk) !== false;
+ } catch (err) {
+ this.__destroySocket(socket, err as Error);
+ return false;
+ }
+ }
+
+ /** Returns false when the socket was torn down (stop parsing). */
+ __onMessageComplete(parser: ResponseParser, socket: Socket, trailers: Buffer[]): boolean {
+ if (socket.destroyed && (parser.statusCode === 0 || parser.shouldKeepAlive)) return false;
+ const request = this.__queue[this.__runningIdx];
+ if (request === null || request === undefined) return false;
+ const statusCode = parser.statusCode;
+ const shouldKeepAlive = parser.shouldKeepAlive;
+ const contentLength = parser.contentLength;
+ const bytesRead = parser.bytesRead;
+ parser.reset();
+ if (statusCode < 200) return true;
+ if (request.method !== "HEAD" && contentLength >= 0 && bytesRead !== contentLength) {
+ this.__destroySocket(socket, new ResponseContentLengthMismatchError());
+ return false;
+ }
+ try {
+ request.onComplete(trailers);
+ } catch (err) {
+ this.__errorRequest(request, err as Error);
+ }
+ this.__queue[this.__runningIdx++] = null;
+ if (!shouldKeepAlive) {
+ this.__destroySocket(socket, new InformationalError("reset"));
+ return false;
+ }
+ if (this.__sockReset && this.__runningCount() === 0) {
+ this.__destroySocket(socket, new InformationalError("reset"));
+ return false;
+ }
+ if (this.__pipelining === 1) setImmediate(() => this.__resume(false));
+ else this.__resume(false);
+ return true;
+ }
+
+ __onParseError(socket: Socket, err: Error): void {
+ this.__destroySocket(socket, err);
+ }
+}
+
+function errorCode(err: Error): string {
+ if (err instanceof UndiciError) return err.code;
+ return (err as NodeJS.ErrnoException).code ?? "";
+}
+
+const CRLF = Buffer.from("\r\n");
+
+/** The value of an ASCII hex digit, or -1. */
+function hexValue(c: number): number {
+ if (c >= 0x30 && c <= 0x39) return c - 0x30;
+ if (c >= 0x41 && c <= 0x46) return c - 0x37;
+ if (c >= 0x61 && c <= 0x66) return c - 0x57;
+ return -1;
+}
+
+/** An incremental HTTP/1.1 response parser (the subset of llhttp undici
+ * relies on): status line, raw header pairs, Content-Length, chunked and
+ * read-until-close bodies, trailers, and pause/resume back-pressure. */
+class ResponseParser {
+ client: Client;
+ socket: Socket;
+ /** Unconsumed bytes of the current arrival, then the later arrivals in
+ * order. */
+ buf: Buffer;
+ queue: Buffer[];
+ state: number;
+ paused: boolean;
+ statusCode: number;
+ statusText: string;
+ headers: Buffer[];
+ headersSize: number;
+ versionMinor: number;
+ contentLength: number;
+ chunked: boolean;
+ connectionHeader: string;
+ keepAliveHeader: string;
+ shouldKeepAlive: boolean;
+ remaining: number;
+ bytesRead: number;
+ trailers: Buffer[];
+ skipBody: boolean;
+ /** The chunk-size line read so far: its value, its digit count, whether
+ * an extension or whitespace follows the digits, and whether its CR was
+ * read. Extensions are skipped as they arrive, so the line keeps no
+ * bytes. */
+ chunkSize: number;
+ chunkDigits: number;
+ chunkExtension: boolean;
+ chunkCR: boolean;
+
+ constructor(client: Client, socket: Socket) {
+ this.client = client;
+ this.socket = socket;
+ this.buf = Buffer.alloc(0);
+ this.queue = [];
+ this.state = 0;
+ this.paused = false;
+ this.statusCode = 0;
+ this.statusText = "";
+ this.headers = [];
+ this.headersSize = 0;
+ this.versionMinor = 1;
+ this.contentLength = -1;
+ this.chunked = false;
+ this.connectionHeader = "";
+ this.keepAliveHeader = "";
+ this.shouldKeepAlive = true;
+ this.remaining = 0;
+ this.bytesRead = 0;
+ this.trailers = [];
+ this.skipBody = false;
+ this.chunkSize = 0;
+ this.chunkDigits = 0;
+ this.chunkExtension = false;
+ this.chunkCR = false;
+ }
+
+ reset(): void {
+ this.state = 0;
+ this.statusCode = 0;
+ this.statusText = "";
+ this.headers = [];
+ this.headersSize = 0;
+ this.contentLength = -1;
+ this.chunked = false;
+ this.connectionHeader = "";
+ this.keepAliveHeader = "";
+ this.remaining = 0;
+ this.bytesRead = 0;
+ this.trailers = [];
+ this.skipBody = false;
+ }
+
+ execute(data: Buffer): void {
+ if (this.buf.length === 0 && this.queue.length === 0) this.buf = data;
+ else this.queue.push(data);
+ if (!this.paused) this.run();
+ }
+
+ /** Moves the next queued arrival into `buf`, after the bytes left in it;
+ * false when nothing is queued. Only a partial status, header or trailer
+ * line (bounded by maxHeaderSize) or a partial chunk-data CRLF is copied. */
+ more(): boolean {
+ const next = this.queue.shift();
+ if (next === undefined) return false;
+ this.buf = this.buf.length === 0 ? next : Buffer.concat([this.buf, next]);
+ return true;
+ }
+
+ resume(): void {
+ if (!this.paused) return;
+ this.paused = false;
+ this.socket.resume();
+ this.run();
+ }
+
+ pause(): void {
+ this.paused = true;
+ this.socket.pause();
+ }
+
+ /** EOF while reading a close-delimited body completes the message. */
+ finishOnEof(): void {
+ if (this.state === 6) {
+ this.client.__onMessageComplete(this, this.socket, []);
+ }
+ }
+
+ fail(message: string, code: string): void {
+ // undici 7.x wraps llhttp's reason and leaves the code unset (its
+ // createError indexes the error-name table with the error number);
+ // 5.x reports the reason and its HPE_ code.
+ const error =
+ this.client.__profile.major >= 7
+ ? new HTTPParserError(`Response does not match the HTTP/1.1 protocol (${message})`)
+ : new HTTPParserError(message, code, "");
+ this.client.__onParseError(this.socket, error);
+ this.state = 99;
+ }
+
+ line(): Buffer | null {
+ const idx = this.buf.indexOf(CRLF);
+ if (idx === -1) return null;
+ const out = this.buf.subarray(0, idx);
+ this.buf = this.buf.subarray(idx + 2);
+ return out;
+ }
+
+ run(): void {
+ while (!this.paused && this.state !== 99) {
+ if (this.state === 0) {
+ // Status line.
+ const line = this.line();
+ if (line === null) {
+ if (this.buf.length > this.client.__maxHeaderSize) {
+ this.client.__onParseError(this.socket, new HeadersOverflowError());
+ this.state = 99;
+ return;
+ }
+ if (this.more()) continue;
+ return;
+ }
+ if (line.length === 0) continue;
+ const text = line.toString("latin1");
+ const m = /^HTTP\/1\.([01]) (\d{3})(?: (.*))?$/.exec(text);
+ if (m === null) {
+ this.fail("Response does not match the HTTP/1.1 protocol", "INVALID_CONSTANT");
+ return;
+ }
+ this.versionMinor = Number(m[1]);
+ this.statusCode = Number(m[2]);
+ this.statusText = m[3] ?? "";
+ this.headersSize = line.length;
+ this.state = 1;
+ } else if (this.state === 1 || this.state === 5) {
+ // Header (1) or trailer (5) lines.
+ const line = this.line();
+ if (line === null) {
+ if (this.headersSize + this.buf.length > this.client.__maxHeaderSize) {
+ this.client.__onParseError(this.socket, new HeadersOverflowError());
+ this.state = 99;
+ return;
+ }
+ if (this.more()) continue;
+ return;
+ }
+ this.headersSize += line.length;
+ if (this.headersSize > this.client.__maxHeaderSize) {
+ this.client.__onParseError(this.socket, new HeadersOverflowError());
+ this.state = 99;
+ return;
+ }
+ if (line.length > 0) {
+ const colon = line.indexOf(0x3a);
+ if (colon <= 0) {
+ this.fail("Invalid header token", "INVALID_HEADER_TOKEN");
+ return;
+ }
+ const name = line.subarray(0, colon);
+ let start = colon + 1;
+ let end = line.length;
+ while (start < end && (line[start] === 0x20 || line[start] === 0x09)) start++;
+ while (end > start && (line[end - 1] === 0x20 || line[end - 1] === 0x09)) end--;
+ const value = line.subarray(start, end);
+ if (this.state === 5) {
+ this.trailers.push(Buffer.from(name), Buffer.from(value));
+ continue;
+ }
+ this.headers.push(Buffer.from(name), Buffer.from(value));
+ const lower = name.toString("latin1").toLowerCase();
+ if (lower === "content-length") {
+ const n = Number(value.toString("latin1"));
+ if (!Number.isInteger(n) || n < 0) {
+ this.fail("Invalid character in Content-Length", "INVALID_CONTENT_LENGTH");
+ return;
+ }
+ this.contentLength = n;
+ } else if (lower === "transfer-encoding") {
+ this.chunked = /(?:^|,)\s*chunked\s*$/i.test(value.toString("latin1"));
+ } else if (lower === "connection") {
+ this.connectionHeader = value.toString("latin1").toLowerCase();
+ } else if (lower === "keep-alive") {
+ this.keepAliveHeader = value.toString("latin1");
+ }
+ continue;
+ }
+ if (this.state === 5) {
+ if (!this.complete()) return;
+ continue;
+ }
+ // End of headers.
+ const noBody = this.statusCode < 200 || this.statusCode === 204 || this.statusCode === 304;
+ const closeRequested = /(?:^|,)\s*close\s*(?:,|$)/.test(this.connectionHeader);
+ const keepAliveRequested = /(?:^|,)\s*keep-alive\s*(?:,|$)/.test(this.connectionHeader);
+ this.shouldKeepAlive = this.versionMinor === 1 ? !closeRequested : keepAliveRequested;
+ if (!noBody && !this.chunked && this.contentLength < 0) this.shouldKeepAlive = false;
+ const ret = this.client.__onHeadersComplete(this, this.socket);
+ if (ret === -1) {
+ this.state = 99;
+ return;
+ }
+ this.skipBody = ret === 1 || noBody;
+ if (this.skipBody) {
+ if (!this.complete()) return;
+ continue;
+ }
+ if (this.chunked) {
+ this.state = 3;
+ } else if (this.contentLength >= 0) {
+ this.remaining = this.contentLength;
+ this.state = 2;
+ if (this.remaining === 0) {
+ if (ret === 2) this.pause();
+ if (!this.complete()) return;
+ continue;
+ }
+ } else {
+ this.state = 6;
+ }
+ if (ret === 2) this.pause();
+ } else if (this.state === 2) {
+ // Content-Length body.
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ const n = Math.min(this.remaining, this.buf.length);
+ const chunk = this.buf.subarray(0, n);
+ this.buf = this.buf.subarray(n);
+ this.remaining -= n;
+ const more = this.client.__onBody(this, this.socket, Buffer.from(chunk));
+ if (this.socket.destroyed) {
+ this.state = 99;
+ return;
+ }
+ if (!more) this.pause();
+ if (this.remaining === 0) {
+ if (!this.complete()) return;
+ }
+ } else if (this.state === 3) {
+ // Chunk size line: hex digits, then optional whitespace and
+ // extensions, then CRLF. It is read byte by byte as it arrives.
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ let i = 0;
+ let done = false;
+ for (; i < this.buf.length && !done; i++) {
+ const c = this.buf[i]!;
+ if (this.chunkCR) {
+ if (c !== 0x0a) {
+ this.fail("Expected LF after chunk size", "STRICT");
+ return;
+ }
+ done = true;
+ } else if (c === 0x0d) {
+ if (this.chunkDigits === 0) {
+ this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
+ return;
+ }
+ this.chunkCR = true;
+ } else if (this.chunkExtension) {
+ continue;
+ } else if (hexValue(c) >= 0) {
+ this.chunkSize = this.chunkSize * 16 + hexValue(c);
+ this.chunkDigits++;
+ if (this.chunkSize > Number.MAX_SAFE_INTEGER) {
+ this.fail("Chunk size overflow", "INVALID_CHUNK_SIZE");
+ return;
+ }
+ } else if (this.chunkDigits > 0 && (c === 0x3b || c === 0x20 || c === 0x09)) {
+ this.chunkExtension = true;
+ } else {
+ this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
+ return;
+ }
+ }
+ this.buf = this.buf.subarray(i);
+ if (!done) continue;
+ const size = this.chunkSize;
+ this.chunkSize = 0;
+ this.chunkDigits = 0;
+ this.chunkExtension = false;
+ this.chunkCR = false;
+ if (size === 0) {
+ this.state = 5;
+ } else {
+ this.remaining = size;
+ this.state = 4;
+ }
+ } else if (this.state === 4) {
+ // Chunk data, then its CRLF.
+ if (this.remaining > 0) {
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ const n = Math.min(this.remaining, this.buf.length);
+ const chunk = this.buf.subarray(0, n);
+ this.buf = this.buf.subarray(n);
+ this.remaining -= n;
+ const more = this.client.__onBody(this, this.socket, Buffer.from(chunk));
+ if (this.socket.destroyed) {
+ this.state = 99;
+ return;
+ }
+ if (!more) this.pause();
+ continue;
+ }
+ if (this.buf.length < 2) {
+ if (this.more()) continue;
+ return;
+ }
+ if (this.buf[0] !== 0x0d || this.buf[1] !== 0x0a) {
+ this.fail("Expected LF after chunk data", "STRICT");
+ return;
+ }
+ this.buf = this.buf.subarray(2);
+ this.state = 3;
+ } else if (this.state === 6) {
+ // Read until the connection closes.
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ const chunk = this.buf;
+ this.buf = Buffer.alloc(0);
+ const more = this.client.__onBody(this, this.socket, Buffer.from(chunk));
+ if (this.socket.destroyed) {
+ this.state = 99;
+ return;
+ }
+ if (!more) this.pause();
+ } else {
+ return;
+ }
+ }
+ }
+
+ /** Message complete: hand the trailers over and get ready for the next
+ * response on this connection. Returns false when parsing must stop. */
+ complete(): boolean {
+ const trailers = this.trailers;
+ const ok = this.client.__onMessageComplete(this, this.socket, trailers);
+ if (!ok) {
+ this.state = 99;
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/packages/compiler/shims/undici/core/connector.ts b/packages/compiler/shims/undici/core/connector.ts
new file mode 100644
index 000000000..f264912ea
--- /dev/null
+++ b/packages/compiler/shims/undici/core/connector.ts
@@ -0,0 +1,157 @@
+// undici's buildConnector (lib/core/connect.js) over node:net and node:tls.
+import * as net from "node:net";
+import * as tls from "node:tls";
+import { ConnectTimeoutError, InvalidArgumentError, NotSupportedError } from "./errors.ts";
+import type {
+ Connector,
+ ConnectorBuildOptions,
+ ConnectorCallback,
+ ConnectorOptions,
+ LookupAddress,
+} from "./types.ts";
+
+export function buildConnector(options: ConnectorBuildOptions): Connector {
+ const maxCachedSessions = options.maxCachedSessions;
+ if (
+ maxCachedSessions !== undefined &&
+ maxCachedSessions !== null &&
+ (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)
+ ) {
+ throw new InvalidArgumentError("maxCachedSessions must be a positive integer or zero");
+ }
+ if (options.socketPath !== undefined && options.socketPath !== null) {
+ throw new NotSupportedError("scriptc's undici connector does not dial socketPath yet");
+ }
+ const timeout =
+ options.timeout === undefined || options.timeout === null ? 10e3 : options.timeout;
+ const lookup = options.lookup;
+ // The plain-TCP dial, with the caller's resolver when one is configured.
+ let dialTcp: (port: number, host: string) => net.Socket;
+ if (lookup !== undefined) {
+ // net.connect's lowered resolver answers every address at once; adapt a
+ // resolver that answers a single address string.
+ const userLookup: (
+ hostname: string,
+ options: unknown,
+ callback: (
+ err: NodeJS.ErrnoException | null,
+ address: string | LookupAddress[],
+ family?: number,
+ ) => void,
+ ) => void = lookup;
+ const resolver = (
+ hostname: string,
+ lookupOptions: unknown,
+ callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void,
+ ): void => {
+ userLookup(hostname, lookupOptions, (err, address, family) => {
+ if (err !== null) callback(err, []);
+ else if (typeof address === "string") {
+ callback(null, [{ address, family: family ?? (net.isIPv6(address) ? 6 : 4) }]);
+ } else callback(null, address);
+ });
+ };
+ dialTcp = (port: number, host: string): net.Socket =>
+ net.connect({ port, host, autoSelectFamily: true, lookup: resolver });
+ } else {
+ dialTcp = (port: number, host: string): net.Socket => net.connect({ port, host });
+ }
+ const rejectUnauthorized = options.rejectUnauthorized;
+ const ca = options.ca;
+ const defaultServername = options.servername;
+
+ return (params: ConnectorOptions, callback: ConnectorCallback): net.Socket => {
+ let pending: ConnectorCallback | null = callback;
+ const https = params.protocol === "https:";
+ const port = params.port !== "" ? Number(params.port) : https ? 443 : 80;
+ let socket: net.Socket;
+ if (https) {
+ if (lookup !== undefined) {
+ throw new NotSupportedError(
+ "scriptc's undici connector does not apply a custom lookup to TLS connections yet",
+ );
+ }
+ const servername =
+ params.servername !== undefined && params.servername !== ""
+ ? params.servername
+ : defaultServername !== undefined
+ ? defaultServername
+ : serverNameOf(params.host ?? params.hostname);
+ socket = tls.connect({
+ port,
+ host: params.hostname,
+ servername,
+ rejectUnauthorized: rejectUnauthorized ?? true,
+ ...(ca !== undefined ? { ca } : {}),
+ });
+ } else {
+ socket = dialTcp(port, params.hostname);
+ }
+
+ let cancelTimer: (() => void) | null = null;
+ if (timeout !== 0) {
+ const timer = setTimeout(() => {
+ cancelTimer = null;
+ setImmediate(() => {
+ const err = new ConnectTimeoutError();
+ socket.destroy();
+ if (pending !== null) {
+ const cb = pending;
+ pending = null;
+ cb(err, null);
+ }
+ });
+ }, timeout);
+ cancelTimer = () => clearTimeout(timer);
+ }
+ const cancelTimeout = (): void => {
+ const cancel = cancelTimer;
+ cancelTimer = null;
+ if (cancel !== null) cancel();
+ };
+
+ socket.setNoDelay(true);
+ const onConnected = (): void => {
+ cancelTimeout();
+ if (pending !== null) {
+ const cb = pending;
+ pending = null;
+ cb(null, socket);
+ }
+ };
+ if (https) socket.once("secureConnect", onConnected);
+ else socket.once("connect", onConnected);
+ socket.on("error", (err: Error) => {
+ cancelTimeout();
+ if (pending !== null) {
+ const cb = pending;
+ pending = null;
+ cb(err, null);
+ }
+ });
+ return socket;
+ };
+}
+
+/** undici's util.getServerName: the host without its port, or null for an
+ * IP literal (no SNI). */
+function serverNameOf(host: string): string | undefined {
+ if (host === "") return undefined;
+ let servername = host;
+ if (servername.startsWith("[")) {
+ const idx = servername.indexOf("]");
+ servername = idx === -1 ? servername : servername.slice(1, idx);
+ } else {
+ const idx = servername.indexOf(":");
+ if (idx !== -1) servername = servername.slice(0, idx);
+ }
+ return net.isIP(servername) !== 0 ? undefined : servername;
+}
+
+/** The `buildConnector.*` type namespace of undici's types/connector.d.ts. */
+export declare namespace buildConnector {
+ type BuildOptions = ConnectorBuildOptions;
+ type Options = ConnectorOptions;
+ type Callback = ConnectorCallback;
+ type connector = Connector;
+}
diff --git a/packages/compiler/shims/undici/core/diagnostics-types.ts b/packages/compiler/shims/undici/core/diagnostics-types.ts
new file mode 100644
index 000000000..3b84915ec
--- /dev/null
+++ b/packages/compiler/shims/undici/core/diagnostics-types.ts
@@ -0,0 +1,68 @@
+// undici's DiagnosticsChannel message types (types/diagnostics-channel.d.ts).
+import type { Socket } from "node:net";
+import type { Connector } from "./types.ts";
+
+export declare namespace DiagnosticsChannel {
+ interface Request {
+ origin?: string | URL;
+ completed: boolean;
+ method?: string;
+ path: string;
+ headers: string;
+ addHeader(key: string, value: string): Request;
+ }
+ interface Response {
+ statusCode: number;
+ statusText: string;
+ headers: Array;
+ }
+ type Error = unknown;
+ interface ConnectParams {
+ host: string;
+ hostname: string;
+ protocol: string;
+ port: string;
+ servername: string | null;
+ }
+ type Connector = import("./types.ts").Connector;
+ interface RequestCreateMessage {
+ request: Request;
+ }
+ interface RequestBodySentMessage {
+ request: Request;
+ }
+ interface RequestHeadersMessage {
+ request: Request;
+ response: Response;
+ }
+ interface RequestTrailersMessage {
+ request: Request;
+ trailers: Array;
+ }
+ interface RequestErrorMessage {
+ request: Request;
+ error: Error;
+ }
+ interface ClientSendHeadersMessage {
+ request: Request;
+ headers: string;
+ socket: Socket;
+ }
+ interface ClientBeforeConnectMessage {
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+ interface ClientConnectedMessage {
+ socket: Socket;
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+ interface ClientConnectErrorMessage {
+ error: Error;
+ socket: Socket;
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+}
+
+export type { Connector };
diff --git a/packages/compiler/shims/undici/core/diagnostics.ts b/packages/compiler/shims/undici/core/diagnostics.ts
new file mode 100644
index 000000000..4efd897f9
--- /dev/null
+++ b/packages/compiler/shims/undici/core/diagnostics.ts
@@ -0,0 +1,16 @@
+// undici's diagnostics_channel publications (lib/core/diagnostics.js). The
+// channel names and message shapes are undici's; subscribers see the same
+// request objects across one request's lifetime.
+import { channel } from "node:diagnostics_channel";
+
+export const channels = {
+ create: channel("undici:request:create"),
+ bodySent: channel("undici:request:bodySent"),
+ headers: channel("undici:request:headers"),
+ trailers: channel("undici:request:trailers"),
+ error: channel("undici:request:error"),
+ beforeConnect: channel("undici:client:beforeConnect"),
+ connected: channel("undici:client:connected"),
+ connectError: channel("undici:client:connectError"),
+ sendHeaders: channel("undici:client:sendHeaders"),
+};
diff --git a/packages/compiler/shims/undici/core/dispatcher.ts b/packages/compiler/shims/undici/core/dispatcher.ts
new file mode 100644
index 000000000..cd195b5da
--- /dev/null
+++ b/packages/compiler/shims/undici/core/dispatcher.ts
@@ -0,0 +1,568 @@
+// undici's Dispatcher (lib/dispatcher/dispatcher.js), DispatcherBase
+// (dispatcher-base.js) and the request() API's handler (lib/api/api-request.js).
+import { EventEmitter } from "node:events";
+import { BodyReadable } from "./readable.ts";
+import {
+ ClientClosedError,
+ ClientDestroyedError,
+ InvalidArgumentError,
+ RequestAbortedError,
+ ResponseStatusCodeError,
+} from "./errors.ts";
+import { parseHeaders, rawHeaderStrings } from "./util.ts";
+import type {
+ DispatchController,
+ DispatchFn,
+ DispatchHandlers,
+ DispatchInterceptor,
+ DispatchOptions,
+ IncomingHttpHeaders,
+ Profile,
+} from "./types.ts";
+
+/** Dispatcher.RequestOptions: the dispatch options (which carry the
+ * request members, see DispatchOptions). */
+export type RequestOptions = DispatchOptions;
+
+export interface ResponseData {
+ statusCode: number;
+ /** 7.x only. */
+ statusText?: string;
+ headers: IncomingHttpHeaders;
+ body: BodyReadable;
+ trailers: Record;
+ opaque: unknown;
+ context: object | undefined;
+}
+
+const defaultProfile: Profile = { major: 7 };
+
+export class Dispatcher extends EventEmitter {
+ dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
+ throw new Error("not implemented");
+ }
+
+ close(): Promise {
+ return Promise.reject(new Error("not implemented"));
+ }
+
+ destroy(_err?: Error | null): Promise {
+ return Promise.reject(new Error("not implemented"));
+ }
+
+ /** Dispatcher.prototype.request: dispatches through this dispatcher and
+ * resolves with the response once its headers arrive. */
+ request(options: RequestOptions): Promise {
+ return requestThrough(this, options, this.__undiciProfile());
+ }
+
+ __undiciProfile(): Profile {
+ return defaultProfile;
+ }
+
+ // Pool statistics hooks (undici's kConnected/kPending/kRunning/kSize/
+ // kBusy); dispatchers that are not clients or pools count nothing.
+ __connectedFlag(): boolean {
+ return false;
+ }
+
+ __isDestroyed(): boolean {
+ return false;
+ }
+
+ __connectedCount(): number {
+ return 0;
+ }
+
+ __freeCount(): number {
+ return 0;
+ }
+
+ __pendingCount(): number {
+ return 0;
+ }
+
+ __queuedCount(): number {
+ return 0;
+ }
+
+ __runningCount(): number {
+ return 0;
+ }
+
+ __sizeCount(): number {
+ return 0;
+ }
+}
+
+export class DispatcherBase extends Dispatcher {
+ __profile: Profile;
+ __destroyed: boolean;
+ __closed: boolean;
+ __onDestroyed: (() => void)[] | null;
+ __onClosed: (() => void)[] | null;
+ __interceptors: readonly DispatchInterceptor[];
+ __interceptedDispatch: DispatchFn | null;
+
+ constructor(profile: Profile) {
+ super();
+ this.__profile = profile;
+ this.__destroyed = false;
+ this.__closed = false;
+ this.__onDestroyed = [];
+ this.__onClosed = [];
+ this.__interceptors = [];
+ this.__interceptedDispatch = null;
+ }
+
+ __undiciProfile(): Profile {
+ return this.__profile;
+ }
+
+ get destroyed(): boolean {
+ return this.__destroyed;
+ }
+
+ __isDestroyed(): boolean {
+ return this.__destroyed;
+ }
+
+ get closed(): boolean {
+ return this.__closed;
+ }
+
+ close(): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.__destroyed) {
+ queueMicrotask(() => reject(new ClientDestroyedError()));
+ return;
+ }
+ if (this.__closed) {
+ const waiting = this.__onClosed;
+ if (waiting !== null) waiting.push(() => resolve());
+ else queueMicrotask(() => resolve());
+ return;
+ }
+ this.__closed = true;
+ this.__onClosed!.push(() => resolve());
+ this.__close()
+ .then(() => this.destroy())
+ .then(() => {
+ queueMicrotask(() => {
+ const callbacks = this.__onClosed;
+ this.__onClosed = null;
+ if (callbacks !== null) for (const cb of callbacks) cb();
+ });
+ });
+ });
+ }
+
+ destroy(err?: Error | null): Promise {
+ return new Promise((resolve) => {
+ if (this.__destroyed) {
+ const waiting = this.__onDestroyed;
+ if (waiting !== null) waiting.push(() => resolve());
+ else queueMicrotask(() => resolve());
+ return;
+ }
+ const error = err ?? new ClientDestroyedError();
+ this.__destroyed = true;
+ this.__onDestroyed!.push(() => resolve());
+ this.__destroy(error).then(() => {
+ queueMicrotask(() => {
+ const callbacks = this.__onDestroyed;
+ this.__onDestroyed = null;
+ if (callbacks !== null) for (const cb of callbacks) cb();
+ });
+ });
+ });
+ }
+
+ dispatch(options: DispatchOptions, handler: DispatchHandlers): boolean {
+ const target = this.__profile.major >= 7 ? unwrapHandler(handler, this.__profile) : handler;
+ try {
+ if (this.__destroyed) throw new ClientDestroyedError();
+ if (this.__closed) throw new ClientClosedError();
+ return this.__dispatchIntercepted(options, target);
+ } catch (err) {
+ if (target.onError === undefined) {
+ if (this.__profile.major >= 7) throw err;
+ throw new InvalidArgumentError("invalid onError method");
+ }
+ target.onError!(err as Error);
+ return false;
+ }
+ }
+
+ __dispatchIntercepted(options: DispatchOptions, handler: DispatchHandlers): boolean {
+ let dispatch = this.__interceptedDispatch;
+ if (dispatch === null) {
+ dispatch = (o: DispatchOptions, h: DispatchHandlers): boolean => this.__dispatch(o, h);
+ for (let i = this.__interceptors.length - 1; i >= 0; i--) {
+ dispatch = this.__interceptors[i]!(dispatch);
+ }
+ this.__interceptedDispatch = dispatch;
+ }
+ return dispatch(options, handler);
+ }
+
+ __dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
+ throw new Error("not implemented");
+ }
+
+ __close(): Promise {
+ return Promise.resolve();
+ }
+
+ __destroy(_err: Error): Promise {
+ return Promise.resolve();
+ }
+}
+
+/** The request() API over a dispatcher (api-request.js's request with the
+ * promise callback). */
+export function requestThrough(
+ dispatcher: Dispatcher,
+ options: RequestOptions,
+ profile: Profile,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const callback = (err: Error | null, data: ResponseData | null): void => {
+ if (err !== null) reject(err);
+ else resolve(data!);
+ };
+ try {
+ const handler = createRequestHandler(options, profile, callback);
+ dispatcher.dispatch(options, handler);
+ } catch (err) {
+ queueMicrotask(() => callback(err as Error, null));
+ }
+ });
+}
+
+/** api-request.js's RequestHandler, as a handler record over closure state. */
+function createRequestHandler(
+ opts: RequestOptions,
+ profile: Profile,
+ done: (err: Error | null, data: ResponseData | null) => void,
+): DispatchHandlers {
+ if (opts.method === "CONNECT") throw new InvalidArgumentError("invalid method");
+ if (
+ opts.highWaterMark !== undefined &&
+ (!Number.isFinite(opts.highWaterMark) || opts.highWaterMark < 0)
+ ) {
+ throw new InvalidArgumentError("invalid highWaterMark");
+ }
+ const v7 = profile.major >= 7;
+ const opaque: unknown = opts.opaque ?? null;
+ const onInfo = opts.onInfo ?? null;
+ const throwOnError = opts.throwOnError === true;
+ const trailers: Record = {};
+ let callback: ((err: Error | null, data: ResponseData | null) => void) | null = done;
+ let res: BodyReadable | null = null;
+ let abortFn: ((err?: Error) => void) | null = null;
+ let reason: Error | null = null;
+ let removeAbortListener: (() => void) | null = null;
+
+ const removeSignal = (): void => {
+ const remove = removeAbortListener;
+ removeAbortListener = null;
+ if (remove !== null) remove();
+ };
+
+ const onError = (err: Error): void => {
+ removeSignal();
+ const cb = callback;
+ if (cb !== null) {
+ callback = null;
+ queueMicrotask(() => cb(err, null));
+ }
+ const body = res;
+ if (body !== null) {
+ res = null;
+ queueMicrotask(() => {
+ body.on("error", () => {});
+ body.destroy(err);
+ });
+ }
+ };
+
+ // 5.x: abort() before the request reached a socket fails it at once.
+ const abortV5 = (): void => {
+ const abort = abortFn;
+ if (abort !== null) abort();
+ else onError(new RequestAbortedError());
+ };
+
+ const signal = opts.signal ?? null;
+ if (signal !== null) {
+ if (v7) {
+ if (signal.aborted) {
+ reason = abortReason(signal);
+ } else {
+ const listener = (): void => {
+ const why = abortReason(signal);
+ reason = why;
+ const body = res;
+ const abort = abortFn;
+ if (body !== null) {
+ body.on("error", () => {});
+ body.destroy(why);
+ } else if (abort !== null) {
+ abort(why);
+ }
+ };
+ signal.addEventListener("abort", listener);
+ removeAbortListener = () => signal.removeEventListener("abort", listener);
+ }
+ } else if (signal.aborted) {
+ abortV5();
+ } else {
+ const listener = (): void => abortV5();
+ signal.addEventListener("abort", listener);
+ removeAbortListener = () => signal.removeEventListener("abort", listener);
+ }
+ }
+
+ return {
+ onConnect: (abort: (err?: Error) => void): void => {
+ if (v7) {
+ if (reason !== null) {
+ abort(reason);
+ return;
+ }
+ } else if (callback === null) {
+ throw new RequestAbortedError();
+ }
+ abortFn = abort;
+ },
+ onHeaders: (
+ statusCode: number,
+ rawHeaders: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean => {
+ const headers = parseHeaders(rawHeaderStrings(rawHeaders, profile), profile);
+ if (statusCode < 200) {
+ if (onInfo !== null) onInfo({ statusCode, headers });
+ return true;
+ }
+ const contentTypeValue = headers["content-type"];
+ const contentType = typeof contentTypeValue === "string" ? contentTypeValue : "";
+ const abort = abortFn;
+ const body = new BodyReadable(
+ resume,
+ (err: Error) => {
+ if (abort !== null) {
+ if (v7) abort(err);
+ else abort();
+ }
+ },
+ contentType,
+ profile,
+ );
+ if (v7 && removeAbortListener !== null) body.on("close", () => removeSignal());
+ const cb = callback;
+ callback = null;
+ res = body;
+ if (cb !== null) {
+ if (!v7 && throwOnError && statusCode >= 400) {
+ resolveErrorBody(cb, body, contentType, statusCode, statusText, headers);
+ return true;
+ }
+ const data: ResponseData = v7
+ ? { statusCode, statusText, headers, trailers, opaque, body, context: undefined }
+ : { statusCode, headers, trailers, opaque, body, context: undefined };
+ cb(null, data);
+ }
+ return true;
+ },
+ onData: (chunk: Buffer): boolean => {
+ const body = res;
+ return body !== null ? body.push(chunk) : true;
+ },
+ onComplete: (rawTrailers: string[] | null): void => {
+ if (!v7) removeSignal();
+ if (rawTrailers !== null) {
+ const parsed = parseHeaders(rawTrailers, profile);
+ for (const key of Object.keys(parsed)) {
+ const value = parsed[key];
+ if (typeof value === "string") trailers[key] = value;
+ else if (value !== undefined) trailers[key] = value.join(", ");
+ }
+ }
+ const body = res;
+ if (body !== null) body.push(null);
+ },
+ onError,
+ };
+}
+
+function abortReason(signal: AbortSignal): Error {
+ const reason: unknown = signal.reason;
+ return reason instanceof Error ? reason : new RequestAbortedError();
+}
+
+/** 5.x throwOnError: reject with ResponseStatusCodeError carrying the parsed
+ * JSON or text body. */
+function resolveErrorBody(
+ callback: (err: Error | null, data: ResponseData | null) => void,
+ body: BodyReadable,
+ contentType: string,
+ statusCode: number,
+ statusMessage: string,
+ headers: IncomingHttpHeaders,
+): void {
+ const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ""}`;
+ const record: Record = {};
+ for (const key of Object.keys(headers)) {
+ const value = headers[key];
+ if (value !== undefined) record[key] = value;
+ }
+ if (statusCode === 204 || contentType === "") {
+ body.dump();
+ process.nextTick(() =>
+ callback(new ResponseStatusCodeError(message, statusCode, record), null),
+ );
+ return;
+ }
+ const finish = (payload: unknown): void => {
+ process.nextTick(() =>
+ callback(new ResponseStatusCodeError(message, statusCode, record, payload), null),
+ );
+ };
+ const fallback = (): void => {
+ body.dump();
+ process.nextTick(() =>
+ callback(new ResponseStatusCodeError(message, statusCode, record), null),
+ );
+ };
+ if (contentType.startsWith("application/json")) {
+ body.json().then((payload: unknown) => finish(payload), fallback);
+ } else if (contentType.startsWith("text/")) {
+ body.text().then((payload: string) => finish(payload), fallback);
+ } else {
+ fallback();
+ }
+}
+
+/** 7.x's UnwrapHandler: a controller-API handler (onRequestStart, ...)
+ * driven through the legacy callbacks the clients invoke. */
+function unwrapHandler(handler: DispatchHandlers, profile: Profile): DispatchHandlers {
+ if (handler.onRequestStart === undefined) return handler;
+ let controller: UnwrapController | null = null;
+ const controllerOf = (): UnwrapController => {
+ let c = controller;
+ if (c === null) {
+ c = new UnwrapController(() => {});
+ controller = c;
+ }
+ return c;
+ };
+ return {
+ onConnect: (abort: (err?: Error) => void): void => {
+ const c = new UnwrapController(abort);
+ controller = c;
+ if (handler.onRequestStart !== undefined) handler.onRequestStart!(c, undefined);
+ },
+ onHeaders: (
+ statusCode: number,
+ rawHeaders: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean => {
+ const c = controllerOf();
+ c.__resume = resume;
+ if (handler.onResponseStart !== undefined) {
+ handler.onResponseStart!(
+ c,
+ statusCode,
+ parseHeaders(rawHeaderStrings(rawHeaders, profile), profile),
+ statusText,
+ );
+ }
+ return !c.paused;
+ },
+ onData: (chunk: Buffer): boolean => {
+ const c = controllerOf();
+ if (handler.onResponseData !== undefined) handler.onResponseData!(c, chunk);
+ return !c.paused;
+ },
+ onComplete: (trailers: string[] | null): void => {
+ if (handler.onResponseEnd !== undefined) {
+ handler.onResponseEnd!(controllerOf(), parseHeaders(trailers ?? [], profile));
+ }
+ },
+ onError: (err: Error): void => {
+ if (handler.onResponseError === undefined)
+ throw new InvalidArgumentError("invalid onError method");
+ handler.onResponseError!(controllerOf(), err);
+ },
+ };
+}
+
+/** 7.x's UnwrapController. */
+class UnwrapController implements DispatchController {
+ __paused: boolean;
+ __reason: Error | null;
+ __aborted: boolean;
+ __abort: (err?: Error) => void;
+ __resume: (() => void) | null;
+
+ constructor(abort: (err?: Error) => void) {
+ this.__paused = false;
+ this.__reason = null;
+ this.__aborted = false;
+ this.__abort = abort;
+ this.__resume = null;
+ }
+
+ pause(): void {
+ this.__paused = true;
+ }
+
+ resume(): void {
+ if (this.__paused) {
+ this.__paused = false;
+ const resume = this.__resume;
+ if (resume !== null) resume();
+ }
+ }
+
+ abort(reason: Error): void {
+ if (!this.__aborted) {
+ this.__aborted = true;
+ this.__reason = reason;
+ this.__abort(reason);
+ }
+ }
+
+ get aborted(): boolean {
+ return this.__aborted;
+ }
+
+ get reason(): Error | null {
+ return this.__reason;
+ }
+
+ get paused(): boolean {
+ return this.__paused;
+ }
+}
+
+/** The `Dispatcher.*` type namespace of undici's types/dispatcher.d.ts
+ * (5.x and 7.x spellings). */
+export declare namespace Dispatcher {
+ type DispatchOptions = import("./types.ts").DispatchOptions;
+ type DispatchHandlers = import("./types.ts").DispatchHandlers;
+ type DispatchHandler = import("./types.ts").DispatchHandlers;
+ type DispatchController = import("./types.ts").DispatchController;
+ type DispatchInterceptor = import("./types.ts").DispatchInterceptor;
+ type Dispatch = import("./types.ts").DispatchFn;
+ type HttpMethod = string;
+ type UndiciHeaders = import("./types.ts").UndiciHeaders;
+ // The 7.x spellings take an opaque type parameter; the opaque value is
+ // `unknown` here.
+ type RequestOptions<_TOpaque = null> = import("./dispatcher.ts").RequestOptions;
+ type ResponseData<_TOpaque = null> = import("./dispatcher.ts").ResponseData;
+}
diff --git a/packages/compiler/shims/undici/core/errors-v5.ts b/packages/compiler/shims/undici/core/errors-v5.ts
new file mode 100644
index 000000000..781dc7ce1
--- /dev/null
+++ b/packages/compiler/shims/undici/core/errors-v5.ts
@@ -0,0 +1,22 @@
+// undici 5.x's `errors` export (lib/core/errors.js module.exports).
+export {
+ HTTPParserError,
+ UndiciError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ BodyTimeoutError,
+ RequestContentLengthMismatchError,
+ ConnectTimeoutError,
+ ResponseStatusCodeError,
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError,
+ ClientDestroyedError,
+ ClientClosedError,
+ InformationalError,
+ SocketError,
+ NotSupportedError,
+ ResponseContentLengthMismatchError,
+ BalancedPoolMissingUpstreamError,
+ ResponseExceededMaxSizeError,
+} from "./errors.ts";
diff --git a/packages/compiler/shims/undici/core/errors-v7.ts b/packages/compiler/shims/undici/core/errors-v7.ts
new file mode 100644
index 000000000..644a00490
--- /dev/null
+++ b/packages/compiler/shims/undici/core/errors-v7.ts
@@ -0,0 +1,25 @@
+// undici 7.x's `errors` export (the implemented classes of
+// lib/core/errors.js module.exports).
+export {
+ AbortError,
+ HTTPParserError,
+ UndiciError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ BodyTimeoutError,
+ RequestContentLengthMismatchError,
+ ConnectTimeoutError,
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError,
+ ClientDestroyedError,
+ ClientClosedError,
+ InformationalError,
+ SocketError,
+ NotSupportedError,
+ ResponseContentLengthMismatchError,
+ BalancedPoolMissingUpstreamError,
+ ResponseExceededMaxSizeError,
+ RequestRetryError,
+ ResponseError,
+} from "./errors.ts";
diff --git a/packages/compiler/shims/undici/core/errors.ts b/packages/compiler/shims/undici/core/errors.ts
new file mode 100644
index 000000000..0af5835da
--- /dev/null
+++ b/packages/compiler/shims/undici/core/errors.ts
@@ -0,0 +1,255 @@
+// undici's error classes (lib/core/errors.js), shared by the 5.x and 7.x
+// entries. Names, messages and codes are undici's; the 7.x-only classes are
+// exported by the 7.x entry alone.
+
+export interface SocketInfo {
+ localAddress?: string;
+ localPort?: number;
+ remoteAddress?: string;
+ remotePort?: number;
+ remoteFamily?: string;
+ timeout?: number;
+ bytesWritten?: number;
+ bytesRead?: number;
+}
+
+export class UndiciError extends Error {
+ code: string;
+ constructor(message?: string) {
+ super(message);
+ this.name = "UndiciError";
+ this.code = "UND_ERR";
+ }
+}
+
+export class ConnectTimeoutError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ConnectTimeoutError";
+ this.message = message || "Connect Timeout Error";
+ this.code = "UND_ERR_CONNECT_TIMEOUT";
+ }
+}
+
+export class HeadersTimeoutError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "HeadersTimeoutError";
+ this.message = message || "Headers Timeout Error";
+ this.code = "UND_ERR_HEADERS_TIMEOUT";
+ }
+}
+
+export class HeadersOverflowError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "HeadersOverflowError";
+ this.message = message || "Headers Overflow Error";
+ this.code = "UND_ERR_HEADERS_OVERFLOW";
+ }
+}
+
+export class BodyTimeoutError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "BodyTimeoutError";
+ this.message = message || "Body Timeout Error";
+ this.code = "UND_ERR_BODY_TIMEOUT";
+ }
+}
+
+export class InvalidArgumentError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "InvalidArgumentError";
+ this.message = message || "Invalid Argument Error";
+ this.code = "UND_ERR_INVALID_ARG";
+ }
+}
+
+export class InvalidReturnValueError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "InvalidReturnValueError";
+ this.message = message || "Invalid Return Value Error";
+ this.code = "UND_ERR_INVALID_RETURN_VALUE";
+ }
+}
+
+/** 7.x's base of RequestAbortedError (5.x has no AbortError export; its
+ * RequestAbortedError extends UndiciError directly, which no 5.x program can
+ * observe through the exported classes). */
+export class AbortError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "AbortError";
+ this.message = message || "The operation was aborted";
+ this.code = "UND_ERR_ABORT";
+ }
+}
+
+export class RequestAbortedError extends AbortError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "AbortError";
+ this.message = message || "Request aborted";
+ this.code = "UND_ERR_ABORTED";
+ }
+}
+
+export class InformationalError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "InformationalError";
+ this.message = message || "Request information";
+ this.code = "UND_ERR_INFO";
+ }
+}
+
+export class RequestContentLengthMismatchError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "RequestContentLengthMismatchError";
+ this.message = message || "Request body length does not match content-length header";
+ this.code = "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH";
+ }
+}
+
+export class ResponseContentLengthMismatchError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ResponseContentLengthMismatchError";
+ this.message = message || "Response body length does not match content-length header";
+ this.code = "UND_ERR_RES_CONTENT_LENGTH_MISMATCH";
+ }
+}
+
+export class ClientDestroyedError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ClientDestroyedError";
+ this.message = message || "The client is destroyed";
+ this.code = "UND_ERR_DESTROYED";
+ }
+}
+
+export class ClientClosedError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ClientClosedError";
+ this.message = message || "The client is closed";
+ this.code = "UND_ERR_CLOSED";
+ }
+}
+
+export class SocketError extends UndiciError {
+ socket: SocketInfo | null;
+ constructor(message?: string, socket?: SocketInfo | null) {
+ super(message);
+ this.name = "SocketError";
+ this.message = message || "Socket error";
+ this.code = "UND_ERR_SOCKET";
+ this.socket = socket ?? null;
+ }
+}
+
+export class NotSupportedError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "NotSupportedError";
+ this.message = message || "Not supported error";
+ this.code = "UND_ERR_NOT_SUPPORTED";
+ }
+}
+
+export class BalancedPoolMissingUpstreamError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "MissingUpstreamError";
+ this.message = message || "No upstream has been added to the BalancedPool";
+ this.code = "UND_ERR_BPL_MISSING_UPSTREAM";
+ }
+}
+
+export class HTTPParserError extends Error {
+ code: string | undefined;
+ data: string | undefined;
+ constructor(message?: string, code?: string, data?: string) {
+ super(message);
+ this.name = "HTTPParserError";
+ this.code = code ? `HPE_${code}` : undefined;
+ this.data = data ? data : undefined;
+ }
+}
+
+export class ResponseExceededMaxSizeError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ResponseExceededMaxSizeError";
+ this.message = message || "Response content exceeded max size";
+ this.code = "UND_ERR_RES_EXCEEDED_MAX_SIZE";
+ }
+}
+
+/** 5.x only (throwOnError). */
+export class ResponseStatusCodeError extends UndiciError {
+ body: unknown;
+ status: number;
+ statusCode: number;
+ headers: Record | null;
+ constructor(
+ message?: string,
+ statusCode?: number,
+ headers?: Record | null,
+ body?: unknown,
+ ) {
+ super(message);
+ this.name = "ResponseStatusCodeError";
+ this.message = message || "Response Status Code Error";
+ this.code = "UND_ERR_RESPONSE_STATUS_CODE";
+ this.body = body;
+ this.status = statusCode ?? 0;
+ this.statusCode = statusCode ?? 0;
+ this.headers = headers ?? null;
+ }
+}
+
+/** 7.x only. */
+export class RequestRetryError extends UndiciError {
+ statusCode: number;
+ data: { count: number };
+ headers: Record;
+ constructor(
+ message: string,
+ code: number,
+ options: { headers?: Record; data?: { count: number } },
+ ) {
+ super(message);
+ this.name = "RequestRetryError";
+ this.message = message || "Request retry error";
+ this.code = "UND_ERR_REQ_RETRY";
+ this.statusCode = code;
+ this.data = options.data ?? { count: 0 };
+ this.headers = options.headers ?? {};
+ }
+}
+
+/** 7.x only. */
+export class ResponseError extends UndiciError {
+ statusCode: number;
+ body: unknown;
+ headers: Record | null;
+ constructor(
+ message: string,
+ code: number,
+ options: { headers?: Record | null; body?: unknown },
+ ) {
+ super(message);
+ this.name = "ResponseError";
+ this.message = message || "Response error";
+ this.code = "UND_ERR_RESPONSE";
+ this.statusCode = code;
+ this.body = options.body;
+ this.headers = options.headers ?? null;
+ }
+}
diff --git a/packages/compiler/shims/undici/core/fetch.ts b/packages/compiler/shims/undici/core/fetch.ts
new file mode 100644
index 000000000..9b287a7bd
--- /dev/null
+++ b/packages/compiler/shims/undici/core/fetch.ts
@@ -0,0 +1,413 @@
+// undici's fetch(). Without a dispatcher (no init.dispatcher and no
+// program-installed global dispatcher) it is the runtime's own fetch, which
+// implements the same WHATWG algorithm. With one, the request goes through
+// that dispatcher (its connector, lookup and pool), following undici's
+// fetch for request headers, redirects and errors; the response body is
+// buffered into a native Response.
+//
+// Known gaps: a Response built here reports `url` as "" and `redirected` as
+// false (the runtime's Response has no settable URL yet), and https requests
+// offer `gzip, deflate` rather than undici's `br, gzip, deflate` (the runtime
+// has no Brotli decoder).
+import { gunzipSync, inflateSync } from "node:zlib";
+import type { Dispatcher } from "./dispatcher.ts";
+import { getGlobalDispatcher, hasCustomGlobalDispatcher } from "./api.ts";
+import type { DispatchHandlers, Profile } from "./types.ts";
+
+/** undici's RequestInit, restricted to the implemented members: string and
+ * byte bodies, and the init members the dispatcher path honors. */
+export interface UndiciRequestInit {
+ method?: string;
+ headers?: Headers | [string, string][] | Record;
+ body?: string | Uint8Array | null;
+ signal?: AbortSignal | null;
+ redirect?: "follow" | "manual" | "error";
+ dispatcher?: Dispatcher;
+}
+
+/** The fetch standard's blocked ports (undici lib/web/fetch/constants.js). */
+const badPorts = new Set([
+ "1",
+ "7",
+ "9",
+ "11",
+ "13",
+ "15",
+ "17",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "25",
+ "37",
+ "42",
+ "43",
+ "53",
+ "69",
+ "77",
+ "79",
+ "87",
+ "95",
+ "101",
+ "102",
+ "103",
+ "104",
+ "109",
+ "110",
+ "111",
+ "113",
+ "115",
+ "117",
+ "119",
+ "123",
+ "135",
+ "137",
+ "139",
+ "143",
+ "161",
+ "179",
+ "389",
+ "427",
+ "465",
+ "512",
+ "513",
+ "514",
+ "515",
+ "526",
+ "530",
+ "531",
+ "532",
+ "540",
+ "548",
+ "554",
+ "556",
+ "563",
+ "587",
+ "601",
+ "636",
+ "989",
+ "990",
+ "993",
+ "995",
+ "1719",
+ "1720",
+ "1723",
+ "2049",
+ "3659",
+ "4045",
+ "4190",
+ "5060",
+ "5061",
+ "6000",
+ "6566",
+ "6665",
+ "6666",
+ "6667",
+ "6668",
+ "6669",
+ "6679",
+ "6697",
+ "10080",
+]);
+
+interface RawResponse {
+ status: number;
+ statusText: string;
+ headers: string[];
+ body: Buffer;
+}
+
+const redirectStatuses = [301, 302, 303, 307, 308];
+const nullBodyStatuses = [101, 204, 205, 304];
+
+export function undiciFetch(
+ input: string | URL,
+ init: UndiciRequestInit | undefined,
+ createGlobal: () => Dispatcher,
+ _profile: Profile,
+): Promise {
+ const explicit = init !== undefined ? init.dispatcher : undefined;
+ if (explicit === undefined && !hasCustomGlobalDispatcher()) {
+ if (init === undefined) return fetch(input);
+ return fetch(input, {
+ method: init.method,
+ headers: init.headers,
+ body: init.body,
+ signal: init.signal ?? undefined,
+ redirect: init.redirect,
+ });
+ }
+ const dispatcher = explicit ?? getGlobalDispatcher(createGlobal);
+ return fetchThrough(dispatcher, input, init ?? {});
+}
+
+function normalizeMethod(method: string): string {
+ const upper = method.toUpperCase();
+ return ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"].includes(upper)
+ ? upper
+ : method;
+}
+
+async function fetchThrough(
+ dispatcher: Dispatcher,
+ input: string | URL,
+ init: UndiciRequestInit,
+): Promise {
+ let url = typeof input === "string" ? new URL(input) : new URL(input.href);
+ let method = normalizeMethod(init.method ?? "GET");
+ const signal = init.signal ?? null;
+ const redirectMode = init.redirect ?? "follow";
+
+ // Request headers: the caller's (as a Headers list: lower-cased, sorted,
+ // combined), then undici fetch's defaults.
+ const headers = headerListOf(init.headers);
+ let body: Buffer | null = null;
+ const rawBody = init.body ?? null;
+ if (rawBody !== null) {
+ if (typeof rawBody === "string") {
+ body = Buffer.from(rawBody);
+ if (!headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8");
+ } else {
+ body = Buffer.from(rawBody.buffer, rawBody.byteOffset, rawBody.byteLength);
+ }
+ }
+ if (!headers.has("accept")) headers.set("accept", "*/*");
+ if (!headers.has("accept-language")) headers.set("accept-language", "*");
+
+ for (let redirects = 0; ; redirects++) {
+ if (signal !== null && signal.aborted) throw abortReason(signal);
+ if (badPorts.has(url.port)) {
+ throw new TypeError("fetch failed", { cause: new Error("bad port") });
+ }
+ const requestHeaders = new HeaderList(headers);
+ if (body !== null) {
+ if (!requestHeaders.has("content-length"))
+ requestHeaders.set("content-length", String(body.length));
+ } else if ((method === "POST" || method === "PUT") && !requestHeaders.has("content-length")) {
+ requestHeaders.set("content-length", "0");
+ }
+ requestHeaders.set("sec-fetch-mode", "cors");
+ if (!requestHeaders.has("user-agent")) requestHeaders.set("user-agent", "undici");
+ // undici also offers br over https; the native runtime has no Brotli
+ // decoder, so only the codings it can decode are offered.
+ if (!requestHeaders.has("accept-encoding"))
+ requestHeaders.set("accept-encoding", "gzip, deflate");
+ const pairs = requestHeaders.pairs();
+
+ let raw: RawResponse;
+ try {
+ raw = await dispatchOnce(dispatcher, url, method, pairs, body, signal);
+ } catch (err) {
+ if (signal !== null && signal.aborted) throw abortReason(signal);
+ throw new TypeError("fetch failed", { cause: err });
+ }
+
+ const responseHeaders = new Headers();
+ for (let i = 0; i + 1 < raw.headers.length; i += 2) {
+ responseHeaders.append(raw.headers[i]!, raw.headers[i + 1]!);
+ }
+ const location = responseHeaders.get("location");
+ if (redirectStatuses.includes(raw.status) && location !== null && redirectMode !== "manual") {
+ if (redirectMode === "error") {
+ throw new TypeError("fetch failed", { cause: new Error("unexpected redirect") });
+ }
+ if (redirects >= 20) {
+ throw new TypeError("fetch failed", { cause: new Error("redirect count exceeded") });
+ }
+ const next = new URL(location, url);
+ if (
+ raw.status === 303 && method !== "GET" && method !== "HEAD"
+ ? true
+ : (raw.status === 301 || raw.status === 302) && method === "POST"
+ ) {
+ method = "GET";
+ body = null;
+ for (const name of [
+ "content-encoding",
+ "content-language",
+ "content-location",
+ "content-type",
+ "content-length",
+ ]) {
+ headers.delete(name);
+ }
+ }
+ if (next.origin !== url.origin) {
+ headers.delete("authorization");
+ headers.delete("proxy-authorization");
+ headers.delete("cookie");
+ headers.delete("host");
+ }
+ url = next;
+ continue;
+ }
+
+ const nullBody = nullBodyStatuses.includes(raw.status) || method === "HEAD";
+ return new Response(
+ nullBody ? null : decode(raw.body, responseHeaders.get("content-encoding")),
+ {
+ status: raw.status,
+ statusText: raw.statusText,
+ headers: responseHeaders,
+ },
+ );
+ }
+}
+
+/** A Fetch header list: lower-cased names, values combined with ", " in
+ * insertion order, serialized sorted by name (set-cookie values stay
+ * separate) — what a Headers object yields when iterated. */
+class HeaderList {
+ names: string[];
+ values: Map;
+
+ constructor(from: HeaderList | null) {
+ this.names = from !== null ? from.names.slice() : [];
+ this.values = new Map();
+ if (from !== null)
+ for (const name of from.names) this.values.set(name, from.values.get(name)!.slice());
+ }
+
+ has(name: string): boolean {
+ return this.values.has(name.toLowerCase());
+ }
+
+ append(name: string, value: string): void {
+ const key = name.toLowerCase();
+ const normalized = value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
+ const existing = this.values.get(key);
+ if (existing !== undefined) existing.push(normalized);
+ else {
+ this.names.push(key);
+ this.values.set(key, [normalized]);
+ }
+ }
+
+ set(name: string, value: string): void {
+ this.delete(name);
+ this.append(name, value);
+ }
+
+ delete(name: string): void {
+ const key = name.toLowerCase();
+ if (!this.values.has(key)) return;
+ this.values.delete(key);
+ this.names = this.names.filter((n) => n !== key);
+ }
+
+ pairs(): string[] {
+ const out: string[] = [];
+ for (const name of this.names.slice().sort()) {
+ const list = this.values.get(name)!;
+ if (name === "set-cookie") for (const value of list) out.push(name, value);
+ else out.push(name, list.join(", "));
+ }
+ return out;
+ }
+}
+
+/** The caller's headers init as a header list. Records and pair arrays read
+ * directly; a Headers object is read through forEach. */
+function headerListOf(
+ init: Headers | [string, string][] | Record | undefined,
+): HeaderList {
+ const list = new HeaderList(null);
+ if (init === undefined) return list;
+ if (init instanceof Headers) {
+ init.forEach((value: string, name: string) => list.append(name, value));
+ } else if (Array.isArray(init)) {
+ for (const pair of init) list.append(pair[0], pair[1]);
+ } else {
+ for (const name of Object.keys(init)) list.append(name, init[name]!);
+ }
+ return list;
+}
+
+function decode(body: Buffer, encoding: string | null): Buffer {
+ if (encoding === null || body.length === 0) return body;
+ let out = body;
+ const codings = encoding
+ .toLowerCase()
+ .split(",")
+ .map((c) => c.trim())
+ .reverse();
+ for (const coding of codings) {
+ if (coding === "gzip" || coding === "x-gzip") out = gunzipSync(out);
+ else if (coding === "deflate") out = inflateSync(out);
+ else return body;
+ }
+ return out;
+}
+
+function abortReason(signal: AbortSignal): Error {
+ const reason: unknown = signal.reason;
+ return reason instanceof Error
+ ? reason
+ : new DOMException("This operation was aborted", "AbortError");
+}
+
+function dispatchOnce(
+ dispatcher: Dispatcher,
+ url: URL,
+ method: string,
+ headers: string[],
+ body: Buffer | null,
+ signal: AbortSignal | null,
+): Promise {
+ return new Promise((resolve, reject) => {
+ let status = 0;
+ let statusText = "";
+ let rawHeaders: string[] = [];
+ const chunks: Buffer[] = [];
+ let settled = false;
+ let abortFn: ((err?: Error) => void) | null = null;
+ const onAbort = (): void => {
+ const abort = abortFn;
+ if (abort !== null && signal !== null) abort(abortReason(signal));
+ };
+ if (signal !== null) signal.addEventListener("abort", onAbort);
+ const finish = (err: Error | null): void => {
+ if (settled) return;
+ settled = true;
+ if (signal !== null) signal.removeEventListener("abort", onAbort);
+ if (err !== null) reject(err);
+ else resolve({ status, statusText, headers: rawHeaders, body: Buffer.concat(chunks) });
+ };
+ const handler: DispatchHandlers = {
+ onConnect: (abort: (err?: Error) => void): void => {
+ abortFn = abort;
+ if (signal !== null && signal.aborted) abort(abortReason(signal));
+ },
+ onHeaders: (
+ statusCode: number,
+ raw: Buffer[],
+ _resume: () => void,
+ text: string,
+ ): boolean => {
+ if (statusCode < 200) return true;
+ status = statusCode;
+ statusText = text;
+ rawHeaders = [];
+ for (const b of raw) rawHeaders.push(b.toString("latin1"));
+ return true;
+ },
+ onData: (chunk: Buffer): boolean => {
+ chunks.push(chunk);
+ return true;
+ },
+ onComplete: (_trailers: string[] | null): void => finish(null),
+ onError: (err: Error): void => finish(err),
+ };
+ dispatcher.dispatch(
+ {
+ origin: url.origin,
+ path: url.pathname + url.search,
+ method,
+ headers,
+ body,
+ },
+ handler,
+ );
+ });
+}
diff --git a/packages/compiler/shims/undici/core/pool.ts b/packages/compiler/shims/undici/core/pool.ts
new file mode 100644
index 000000000..101d5947b
--- /dev/null
+++ b/packages/compiler/shims/undici/core/pool.ts
@@ -0,0 +1,280 @@
+// undici's Pool (lib/dispatcher/pool.js, pool-base.js, pool-stats.js): a
+// set of Clients for one origin, created on demand up to `connections`, with
+// a queue for requests no client can take.
+import { buildConnector } from "./connector.ts";
+import { Client } from "./client.ts";
+import { Dispatcher, DispatcherBase } from "./dispatcher.ts";
+import { InvalidArgumentError } from "./errors.ts";
+import { parseOrigin } from "./util.ts";
+import type {
+ ClientOptions,
+ DispatchHandlers,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./types.ts";
+
+interface QueuedDispatch {
+ opts: DispatchOptions;
+ handler: DispatchHandlers;
+}
+
+/** pool-stats.js: live counters over a pool's clients. */
+export class PoolStats {
+ __pool: PoolBase;
+
+ constructor(pool: PoolBase) {
+ this.__pool = pool;
+ }
+
+ get connected(): number {
+ return this.__pool.__connectedCount();
+ }
+
+ get free(): number {
+ return this.__pool.__freeCount();
+ }
+
+ get pending(): number {
+ return this.__pool.__pendingCount();
+ }
+
+ get queued(): number {
+ return this.__pool.__queuedCount();
+ }
+
+ get running(): number {
+ return this.__pool.__runningCount();
+ }
+
+ get size(): number {
+ return this.__pool.__sizeCount();
+ }
+}
+
+export class PoolBase extends DispatcherBase {
+ __clients: Dispatcher[];
+ __clientNeedDrain: Map;
+ __queue: QueuedDispatch[];
+ __queued: number;
+ __needDrain: boolean;
+ __closedResolve: (() => void) | null;
+ __stats: PoolStats;
+ __onDrain: (origin: URL, targets: Dispatcher[]) => void;
+ __onConnect: (origin: URL, targets: Dispatcher[]) => void;
+ __onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void;
+ __onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void;
+
+ constructor(profile: Profile) {
+ super(profile);
+ this.__clients = [];
+ this.__clientNeedDrain = new Map();
+ this.__queue = [];
+ this.__queued = 0;
+ this.__needDrain = false;
+ this.__closedResolve = null;
+ this.__stats = new PoolStats(this);
+ this.__onDrain = (origin: URL, targets: Dispatcher[]): void => {
+ const client = targets[0]!;
+ let needDrain = false;
+ while (!needDrain) {
+ const item = this.__queue.shift();
+ if (item === undefined) break;
+ this.__queued--;
+ needDrain = !client.dispatch(item.opts, item.handler);
+ }
+ this.__clientNeedDrain.set(client, needDrain);
+ if (!needDrain && this.__needDrain) {
+ this.__needDrain = false;
+ this.emit("drain", origin, [this as Dispatcher, ...targets]);
+ }
+ const closed = this.__closedResolve;
+ if (closed !== null && this.__queue.length === 0) {
+ Promise.all(this.__clients.map((c) => c.close())).then(() => closed());
+ }
+ };
+ this.__onConnect = (origin: URL, targets: Dispatcher[]): void => {
+ this.emit("connect", origin, [this as Dispatcher, ...targets]);
+ };
+ this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("disconnect", origin, [this as Dispatcher, ...targets], err);
+ };
+ this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("connectionError", origin, [this as Dispatcher, ...targets], err);
+ };
+ }
+
+ get stats(): PoolStats {
+ return this.__stats;
+ }
+
+ __busyFlag(): boolean {
+ return this.__needDrain;
+ }
+
+ __connectedCount(): number {
+ return this.__clients.filter((c) => c.__connectedFlag()).length;
+ }
+
+ __freeCount(): number {
+ return this.__clients.filter((c) => c.__connectedFlag() && !this.__clientNeedDrain.get(c))
+ .length;
+ }
+
+ __pendingCount(): number {
+ let ret = this.__queued;
+ for (const c of this.__clients) ret += c.__pendingCount();
+ return ret;
+ }
+
+ __queuedCount(): number {
+ return this.__queued;
+ }
+
+ __runningCount(): number {
+ let ret = 0;
+ for (const c of this.__clients) ret += c.__runningCount();
+ return ret;
+ }
+
+ __sizeCount(): number {
+ let ret = this.__queued;
+ for (const c of this.__clients) ret += c.__sizeCount();
+ return ret;
+ }
+
+ __close(): Promise {
+ if (this.__queue.length === 0) {
+ return Promise.all(this.__clients.map((c) => c.close())).then(() => {});
+ }
+ return new Promise((resolve) => {
+ this.__closedResolve = resolve;
+ });
+ }
+
+ __destroy(err: Error): Promise {
+ for (;;) {
+ const item = this.__queue.shift();
+ if (item === undefined) break;
+ if (item.handler.onError !== undefined) item.handler.onError!(err);
+ }
+ return Promise.all(this.__clients.map((c) => c.destroy(err))).then(() => {});
+ }
+
+ __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
+ const dispatcher = this.__getDispatcher();
+ if (dispatcher === null) {
+ this.__needDrain = true;
+ this.__queue.push({ opts, handler });
+ this.__queued++;
+ } else if (!dispatcher.dispatch(opts, handler)) {
+ this.__clientNeedDrain.set(dispatcher, true);
+ this.__needDrain = this.__getDispatcher() === null;
+ }
+ return !this.__needDrain;
+ }
+
+ __getDispatcher(): Dispatcher | null {
+ return null;
+ }
+
+ __addClient(client: Dispatcher): void {
+ client.on("drain", this.__onDrain);
+ client.on("connect", this.__onConnect);
+ client.on("disconnect", this.__onDisconnect);
+ client.on("connectionError", this.__onConnectionError);
+ this.__clients.push(client);
+ this.__clientNeedDrain.set(client, false);
+ if (this.__needDrain) {
+ process.nextTick(() => {
+ if (this.__needDrain) this.__onDrain(clientUrl(client), [client, this as Dispatcher]);
+ });
+ }
+ }
+}
+
+function clientUrl(client: Dispatcher): URL {
+ return client instanceof Client ? client.__url : new URL("http://localhost");
+}
+
+export class Pool extends PoolBase {
+ __connections: number | null;
+ __url: URL;
+ __options: ClientOptions;
+ __factory: ((origin: URL, opts: object) => Dispatcher) | null;
+
+ constructor(origin: string | URL, options: PoolOptions | undefined, profile: Profile) {
+ super(profile);
+ const opts: PoolOptions = options ?? {};
+ const connections = opts.connections ?? null;
+ if (connections !== null && (!Number.isFinite(connections) || connections < 0)) {
+ throw new InvalidArgumentError("invalid connections");
+ }
+ const connect = opts.connect ?? null;
+ const connector =
+ typeof connect === "function"
+ ? connect
+ : buildConnector(
+ connect !== null
+ ? {
+ ...connect,
+ timeout:
+ connect.timeout !== undefined ? connect.timeout : (opts.connectTimeout ?? 10e3),
+ }
+ : { timeout: opts.connectTimeout ?? 10e3 },
+ );
+ const interceptors = opts.interceptors;
+ if (profile.major < 7 && interceptors !== undefined && interceptors.Pool !== undefined) {
+ this.__interceptors = interceptors.Pool;
+ }
+ this.__connections = connections !== null && connections !== 0 ? connections : null;
+ this.__url = parseOrigin(origin, profile);
+ this.__options = {
+ keepAliveTimeout: opts.keepAliveTimeout,
+ keepAliveMaxTimeout: opts.keepAliveMaxTimeout,
+ keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold,
+ pipelining: opts.pipelining,
+ maxHeaderSize: opts.maxHeaderSize,
+ bodyTimeout: opts.bodyTimeout,
+ headersTimeout: opts.headersTimeout,
+ strictContentLength: opts.strictContentLength,
+ maxRequestsPerClient: opts.maxRequestsPerClient,
+ maxResponseSize: opts.maxResponseSize,
+ localAddress: opts.localAddress,
+ interceptors: interceptors !== undefined ? { Client: interceptors.Client } : undefined,
+ connect: connector,
+ };
+ const factory = opts.factory;
+ this.__factory = factory !== undefined ? factory : null;
+ if (profile.major >= 7) {
+ // 7.x drops a client whose connection attempt failed.
+ this.on("connectionError", (_origin: URL, targets: Dispatcher[], _err: Error) => {
+ for (const target of targets) {
+ const idx = this.__clients.indexOf(target);
+ if (idx !== -1) this.__clients.splice(idx, 1);
+ }
+ });
+ }
+ }
+
+ __getDispatcher(): Dispatcher | null {
+ for (const client of this.__clients) {
+ if (!this.__clientNeedDrain.get(client)) return client;
+ }
+ if (this.__connections === null || this.__clients.length < this.__connections) {
+ const factory = this.__factory;
+ const client =
+ factory !== null
+ ? factory(this.__url, this.__options)
+ : this.__createClient(this.__url, this.__options);
+ this.__addClient(client);
+ return client;
+ }
+ return null;
+ }
+
+ /** The default factory: a Client of the same undici version. */
+ __createClient(origin: URL, options: ClientOptions): Dispatcher {
+ return new Client(origin, options, this.__profile);
+ }
+}
diff --git a/packages/compiler/shims/undici/core/readable.ts b/packages/compiler/shims/undici/core/readable.ts
new file mode 100644
index 000000000..53a7cc14e
--- /dev/null
+++ b/packages/compiler/shims/undici/core/readable.ts
@@ -0,0 +1,268 @@
+// undici's response body (lib/api/readable.js): a readable byte stream fed
+// by the dispatch handler, with the fetch body mixin (text/json/
+// arrayBuffer/blob/bytes) and dump().
+//
+// It follows node:stream's Readable contract for the members it declares
+// (flowing on a 'data' listener or resume(), pause(), 'data'/'end'/'close'/
+// 'error' events and their ordering, destroy(err), back-pressure through
+// push()'s return value), built on EventEmitter: scriptc compiles program
+// classes extending EventEmitter under both its own Node declarations and
+// @types/node, which a node:stream subclass does not get.
+import { EventEmitter } from "node:events";
+import { NotSupportedError, RequestAbortedError } from "./errors.ts";
+import type { Profile } from "./types.ts";
+
+const HIGH_WATER_MARK = 64 * 1024;
+
+export class BodyReadable extends EventEmitter {
+ __resumeFn: () => void;
+ __abortFn: (err: Error) => void;
+ __contentType: string;
+ __consuming: boolean;
+ __profile: Profile;
+ __buffer: Buffer[];
+ __length: number;
+ __flowing: boolean;
+ __ended: boolean;
+ __endEmitted: boolean;
+ __destroyed: boolean;
+ __flowScheduled: boolean;
+ __needsResume: boolean;
+
+ constructor(
+ resume: () => void,
+ abort: (err: Error) => void,
+ contentType: string,
+ profile: Profile,
+ ) {
+ super();
+ this.__resumeFn = resume;
+ this.__abortFn = abort;
+ this.__contentType = contentType;
+ this.__consuming = false;
+ this.__profile = profile;
+ this.__buffer = [];
+ this.__length = 0;
+ this.__flowing = false;
+ this.__ended = false;
+ this.__endEmitted = false;
+ this.__destroyed = false;
+ this.__flowScheduled = false;
+ this.__needsResume = false;
+ // Readable.on('data') switches the stream to flowing mode.
+ this.on("newListener", (event: string) => {
+ if (event === "data" && !this.__flowing) this.resume();
+ });
+ }
+
+ get destroyed(): boolean {
+ return this.__destroyed;
+ }
+
+ get readableEnded(): boolean {
+ return this.__endEmitted;
+ }
+
+ get readableLength(): number {
+ return this.__length;
+ }
+
+ get readableFlowing(): boolean {
+ return this.__flowing;
+ }
+
+ /** Readable.push: queue a chunk (null ends the body). Returns false once
+ * the buffer reaches the high-water mark; the dispatcher then pauses the
+ * connection until the consumer drains it. */
+ push(chunk: Buffer | null): boolean {
+ if (this.__destroyed || this.__ended) return false;
+ if (chunk === null) {
+ this.__ended = true;
+ if (this.__length === 0) this.__scheduleEnd();
+ else this.__scheduleFlow();
+ return false;
+ }
+ if (this.__flowing && this.__length === 0 && this.listenerCount("data") > 0) {
+ this.emit("data", chunk);
+ return true;
+ }
+ this.__buffer.push(chunk);
+ this.__length += chunk.length;
+ if (this.__flowing) this.__scheduleFlow();
+ const below = this.__length < HIGH_WATER_MARK;
+ if (!below) this.__needsResume = true;
+ return below;
+ }
+
+ resume(): this {
+ if (!this.__flowing) {
+ this.__flowing = true;
+ this.__scheduleFlow();
+ }
+ return this;
+ }
+
+ pause(): this {
+ this.__flowing = false;
+ return this;
+ }
+
+ destroy(err?: Error): this {
+ if (this.__destroyed) return this;
+ this.__destroyed = true;
+ let error: Error | null = err ?? null;
+ if (error === null && !this.__endEmitted) error = new RequestAbortedError();
+ if (error !== null) this.__abortFn(error);
+ this.__buffer = [];
+ this.__length = 0;
+ const reported = error;
+ process.nextTick(() => {
+ if (reported !== null) this.emit("error", reported);
+ this.emit("close");
+ });
+ return this;
+ }
+
+ __scheduleFlow(): void {
+ if (this.__flowScheduled) return;
+ this.__flowScheduled = true;
+ process.nextTick(() => {
+ this.__flowScheduled = false;
+ this.__flow();
+ });
+ }
+
+ __flow(): void {
+ while (this.__flowing && !this.__destroyed && this.__buffer.length > 0) {
+ const chunk = this.__buffer.shift()!;
+ this.__length -= chunk.length;
+ this.emit("data", chunk);
+ }
+ if (this.__destroyed) return;
+ if (this.__needsResume && this.__length < HIGH_WATER_MARK) {
+ this.__needsResume = false;
+ this.__resumeFn();
+ }
+ if (this.__ended && this.__length === 0 && this.__flowing) this.__scheduleEnd();
+ }
+
+ __scheduleEnd(): void {
+ process.nextTick(() => {
+ if (this.__endEmitted || this.__destroyed || !this.__flowing || this.__length > 0) return;
+ this.__endEmitted = true;
+ this.emit("end");
+ // autoDestroy: a fully read body closes.
+ process.nextTick(() => {
+ if (this.__destroyed) return;
+ this.__destroyed = true;
+ this.emit("close");
+ });
+ });
+ }
+
+ /** True once text()/json()/arrayBuffer()/bytes()/blob()/dump() started
+ * consuming the body. */
+ get bodyUsed(): boolean {
+ return this.__consuming;
+ }
+
+ text(): Promise {
+ return this.__consume().then((bytes) => decodeText(bytes, this.__profile));
+ }
+
+ // undici declares json() as Promise: callers assign the parsed value
+ // to their own annotated types without a cast, exactly as with undici.
+ // oxlint-disable-next-line typescript/no-explicit-any
+ json(): Promise {
+ return this.__consume().then((bytes) => JSON.parse(decodeText(bytes, this.__profile)));
+ }
+
+ arrayBuffer(): Promise {
+ return this.__consume().then((bytes) => {
+ const out = new ArrayBuffer(bytes.length);
+ new Uint8Array(out).set(bytes);
+ return out;
+ });
+ }
+
+ bytes(): Promise {
+ return this.__consume().then((bytes) => new Uint8Array(bytes));
+ }
+
+ blob(): Promise {
+ return this.__consume().then((bytes) => new Blob([bytes], { type: this.__contentType }));
+ }
+
+ formData(): Promise {
+ return Promise.reject(new NotSupportedError());
+ }
+
+ /** Reads and discards up to `limit` bytes (default 256 KiB on 5.x, 128 KiB
+ * on 7.x), then resolves; read errors are swallowed like undici's. */
+ dump(opts?: { limit: number }): Promise {
+ let limit =
+ opts !== undefined && Number.isFinite(opts.limit)
+ ? opts.limit
+ : this.__profile.major >= 7
+ ? 131072
+ : 262144;
+ this.__consuming = true;
+ return new Promise((resolve) => {
+ if (this.__endEmitted || this.__destroyed) {
+ resolve();
+ return;
+ }
+ this.on("data", (chunk: Buffer) => {
+ limit -= chunk.length;
+ if (limit < 0) {
+ this.destroy();
+ resolve();
+ }
+ });
+ this.on("end", () => resolve());
+ this.on("error", () => resolve());
+ this.on("close", () => resolve());
+ });
+ }
+
+ __consume(): Promise {
+ if (this.bodyUsed) return Promise.reject(new TypeError("unusable"));
+ this.__consuming = true;
+ return new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ let done = false;
+ this.on("data", (chunk: Buffer) => {
+ chunks.push(chunk);
+ });
+ this.on("end", () => {
+ if (done) return;
+ done = true;
+ resolve(Buffer.concat(chunks));
+ });
+ this.on("error", (err: Error) => {
+ if (done) return;
+ done = true;
+ reject(err);
+ });
+ this.on("close", () => {
+ if (done) return;
+ done = true;
+ reject(new RequestAbortedError());
+ });
+ });
+ }
+}
+
+/** 5.x decodes UTF-8 as-is; 7.x strips a leading byte-order mark first. */
+function decodeText(bytes: Buffer, profile: Profile): string {
+ if (
+ profile.major >= 7 &&
+ bytes.length >= 3 &&
+ bytes[0] === 0xef &&
+ bytes[1] === 0xbb &&
+ bytes[2] === 0xbf
+ ) {
+ return bytes.subarray(3).toString("utf8");
+ }
+ return bytes.toString("utf8");
+}
diff --git a/packages/compiler/shims/undici/core/request.ts b/packages/compiler/shims/undici/core/request.ts
new file mode 100644
index 000000000..7f312481b
--- /dev/null
+++ b/packages/compiler/shims/undici/core/request.ts
@@ -0,0 +1,287 @@
+// undici's per-dispatch request record (lib/core/request.js): option
+// validation, header processing, and the handler callbacks with their
+// diagnostics_channel publications.
+import { channels } from "./diagnostics.ts";
+import { InvalidArgumentError, NotSupportedError } from "./errors.ts";
+import type { DispatchHandlers, DispatchOptions, Profile } from "./types.ts";
+
+const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
+const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+const invalidPathRegex = /[^!-ÿ]/;
+
+export class Request {
+ origin: string;
+ method: string;
+ path: string;
+ body: Buffer | null;
+ /** Header name/value pairs in insertion order (host and content-length
+ * are consumed into their own fields, like undici). */
+ headerPairs: string[];
+ host: string | null;
+ contentLength: number | null;
+ contentType: string | null;
+ completed: boolean;
+ aborted: boolean;
+ idempotent: boolean;
+ blocking: boolean;
+ reset: boolean | null;
+ headersTimeout: number | null;
+ bodyTimeout: number | null;
+ throwOnError: boolean;
+ servername: string | null;
+ error: Error | null;
+ abortFn: ((err?: Error) => void) | null;
+ handler: DispatchHandlers;
+ /** The request as diagnostics_channel subscribers see it: one boxed
+ * value per request, so its identity is stable across publications. */
+ diagnostic: unknown;
+
+ constructor(origin: string, opts: DispatchOptions, handler: DispatchHandlers, profile: Profile) {
+ const path = opts.path;
+ const method = opts.method;
+ if (typeof path !== "string") {
+ throw new InvalidArgumentError("path must be a string");
+ } else if (
+ path[0] !== "/" &&
+ !(path.startsWith("http://") || path.startsWith("https://")) &&
+ method !== "CONNECT"
+ ) {
+ throw new InvalidArgumentError("path must be an absolute URL or start with a slash");
+ } else if (invalidPathRegex.test(path)) {
+ throw new InvalidArgumentError("invalid request path");
+ }
+ if (typeof method !== "string") {
+ throw new InvalidArgumentError("method must be a string");
+ } else if (!tokenRegExp.test(method)) {
+ throw new InvalidArgumentError("invalid request method");
+ }
+ if (method === "CONNECT") {
+ throw new NotSupportedError(
+ "scriptc's undici client does not implement CONNECT requests yet",
+ );
+ }
+ const headersTimeout = opts.headersTimeout ?? null;
+ if (headersTimeout !== null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError("invalid headersTimeout");
+ }
+ const bodyTimeout = opts.bodyTimeout ?? null;
+ if (bodyTimeout !== null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError("invalid bodyTimeout");
+ }
+
+ this.origin = origin;
+ this.method = method;
+ this.path = path;
+ this.headersTimeout = headersTimeout;
+ this.bodyTimeout = bodyTimeout;
+ // 7.x dropped throwOnError (the responseError interceptor replaced it).
+ if (profile.major >= 7 && opts.throwOnError !== undefined) {
+ throw new InvalidArgumentError("invalid throwOnError");
+ }
+ this.throwOnError = opts.throwOnError === true;
+
+ const body = opts.body ?? null;
+ if (body === null) {
+ this.body = null;
+ } else if (typeof body === "string") {
+ this.body = body.length > 0 ? Buffer.from(body) : null;
+ } else {
+ this.body =
+ body.byteLength > 0 ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null;
+ }
+
+ this.completed = false;
+ this.aborted = false;
+ this.idempotent = opts.idempotent ?? (method === "HEAD" || method === "GET");
+ this.blocking = opts.blocking ?? (profile.major >= 7 ? method !== "HEAD" : false);
+ this.reset = opts.reset ?? null;
+ this.host = null;
+ this.contentLength = null;
+ this.contentType = null;
+ this.headerPairs = [];
+ this.error = null;
+ this.abortFn = null;
+
+ const headers = opts.headers ?? null;
+ if (headers !== null) {
+ if (Array.isArray(headers)) {
+ if (headers.length % 2 !== 0) throw new InvalidArgumentError("headers array must be even");
+ for (let i = 0; i < headers.length; i += 2) {
+ processHeader(this, headers[i]!, headers[i + 1]!, profile);
+ }
+ } else {
+ for (const key of Object.keys(headers)) {
+ const value = headers[key];
+ if (value === undefined) continue;
+ if (typeof value === "string") {
+ processHeader(this, key, value, profile);
+ } else if (profile.major >= 7) {
+ for (const item of value) processHeader(this, key, item, profile);
+ } else {
+ throw new InvalidArgumentError(`invalid ${key} header`);
+ }
+ }
+ }
+ }
+
+ this.servername = serverName(this.host);
+ this.handler = handler;
+ this.diagnostic = null;
+ if (channels.create.hasSubscribers) channels.create.publish({ request: this.diagnosticView() });
+ }
+
+ diagnosticView(): unknown {
+ let view = this.diagnostic;
+ if (view === null) {
+ view = {
+ origin: this.origin,
+ completed: this.completed,
+ method: this.method,
+ path: this.path,
+ headers: this.headers,
+ };
+ this.diagnostic = view;
+ }
+ return view;
+ }
+
+ /** The DiagnosticsChannel.Request view's raw header block. */
+ get headers(): string {
+ let out = "";
+ for (let i = 0; i < this.headerPairs.length; i += 2) {
+ out += `${this.headerPairs[i]}: ${this.headerPairs[i + 1]}\r\n`;
+ }
+ return out;
+ }
+
+ addHeader(key: string, value: string): Request {
+ this.headerPairs.push(key, value);
+ return this;
+ }
+
+ onBodySent(chunk: Buffer): void {
+ const cb = this.handler.onBodySent;
+ if (cb !== undefined) {
+ try {
+ this.handler.onBodySent!(chunk.length, chunk.length);
+ } catch (err) {
+ this.onError(err as Error);
+ }
+ }
+ }
+
+ onRequestSent(): void {
+ if (channels.bodySent.hasSubscribers)
+ channels.bodySent.publish({ request: this.diagnosticView() });
+ }
+
+ onConnect(abort: (err?: Error) => void): void {
+ if (this.error !== null) {
+ abort(this.error);
+ return;
+ }
+ this.abortFn = abort;
+ if (this.handler.onConnect !== undefined) this.handler.onConnect!(abort);
+ }
+
+ onHeaders(
+ statusCode: number,
+ headers: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean {
+ if (channels.headers.hasSubscribers) {
+ channels.headers.publish({
+ request: this.diagnosticView(),
+ response: { statusCode, headers, statusText },
+ });
+ }
+ if (this.handler.onHeaders === undefined) return true;
+ return this.handler.onHeaders!(statusCode, headers, resume, statusText);
+ }
+
+ onData(chunk: Buffer): boolean {
+ if (this.handler.onData === undefined) return true;
+ return this.handler.onData!(chunk);
+ }
+
+ onComplete(trailers: Buffer[]): void {
+ this.completed = true;
+ if (channels.trailers.hasSubscribers)
+ channels.trailers.publish({ request: this.diagnosticView(), trailers });
+ if (this.handler.onComplete !== undefined) {
+ const raw: string[] = [];
+ for (const t of trailers) raw.push(t.toString("latin1"));
+ this.handler.onComplete!(raw);
+ }
+ }
+
+ onError(error: Error): void {
+ if (channels.error.hasSubscribers)
+ channels.error.publish({ request: this.diagnosticView(), error });
+ if (this.aborted) return;
+ this.aborted = true;
+ if (this.handler.onError !== undefined) this.handler.onError!(error);
+ }
+}
+
+function processHeader(request: Request, key: string, val: string, profile: Profile): void {
+ const lower = key.toLowerCase();
+ if (lower === "host") {
+ // The client writes the Host value into the request head as given.
+ if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
+ if (request.host === null) {
+ request.host = val;
+ return;
+ }
+ if (profile.major >= 7) throw new InvalidArgumentError("duplicate host header");
+ } else if (lower === "content-length") {
+ if (request.contentLength === null) {
+ const n = parseInt(val, 10);
+ if (!Number.isFinite(n) || (profile.major >= 7 && !/^\d+$/.test(val))) {
+ throw new InvalidArgumentError("invalid content-length header");
+ }
+ request.contentLength = n;
+ return;
+ }
+ // The client writes the one Content-Length it validated or computed
+ // from the body; a second field would frame the request twice. Both
+ // profiles reject it with undici 7's error (5.x sends it as given).
+ throw new InvalidArgumentError("duplicate content-length header");
+ }
+ if (lower === "content-type" && request.contentType === null && !headerCharRegex.test(val)) {
+ request.contentType = val;
+ request.headerPairs.push(key, val);
+ return;
+ }
+ if (lower === "transfer-encoding" || lower === "keep-alive" || lower === "upgrade") {
+ throw new InvalidArgumentError(`invalid ${lower} header`);
+ }
+ if (lower === "connection") {
+ if (profile.major < 7) throw new InvalidArgumentError("invalid connection header");
+ for (const token of val.toLowerCase().split(",")) {
+ const trimmed = token.trim();
+ if (!tokenRegExp.test(trimmed)) throw new InvalidArgumentError("invalid connection header");
+ if (trimmed === "close") request.reset = true;
+ }
+ return;
+ }
+ if (lower === "expect") throw new NotSupportedError("expect header not supported");
+ if (!tokenRegExp.test(key)) throw new InvalidArgumentError("invalid header key");
+ if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
+ request.headerPairs.push(key, val);
+}
+
+/** util.getServerName: the Host header without its port; null for IPs. */
+function serverName(host: string | null): string | null {
+ if (host === null || host === "") return null;
+ let name = host;
+ if (name.startsWith("[")) {
+ const idx = name.indexOf("]");
+ if (idx !== -1) name = name.slice(1, idx);
+ return null;
+ }
+ const idx = name.indexOf(":");
+ if (idx !== -1) name = name.slice(0, idx);
+ return /^\d+\.\d+\.\d+\.\d+$/.test(name) ? null : name;
+}
diff --git a/packages/compiler/shims/undici/core/timers.ts b/packages/compiler/shims/undici/core/timers.ts
new file mode 100644
index 000000000..d5028b6eb
--- /dev/null
+++ b/packages/compiler/shims/undici/core/timers.ts
@@ -0,0 +1,78 @@
+// undici 7.x's fast timers (lib/util/timers.js): one unref'd native timer
+// ticking every 499 ms drives every headers/body timeout, so those fire on
+// the first tick at least `delay` after the timer (re)started.
+const RESOLUTION_MS = 1e3;
+const TICK_MS = (RESOLUTION_MS >> 1) - 1;
+const NOT_IN_LIST = -2;
+const TO_BE_CLEARED = -1;
+const PENDING = 0;
+const ACTIVE = 1;
+
+let fastNow = 0;
+// The ticking native timer, held as its refresh closure.
+let refreshTick: (() => void) | null = null;
+const fastTimers: FastTimer[] = [];
+
+function onTick(): void {
+ fastNow += TICK_MS;
+ let idx = 0;
+ let len = fastTimers.length;
+ while (idx < len) {
+ const timer = fastTimers[idx]!;
+ if (timer.state === PENDING) {
+ timer.idleStart = fastNow - TICK_MS;
+ timer.state = ACTIVE;
+ } else if (timer.state === ACTIVE && fastNow >= timer.idleStart + timer.idleTimeout) {
+ timer.state = TO_BE_CLEARED;
+ timer.idleStart = -1;
+ timer.onTimeout();
+ }
+ if (timer.state === TO_BE_CLEARED) {
+ timer.state = NOT_IN_LIST;
+ if (--len !== 0) fastTimers[idx] = fastTimers[len]!;
+ } else {
+ ++idx;
+ }
+ }
+ fastTimers.length = len;
+ if (fastTimers.length !== 0) refreshTimeout();
+}
+
+function refreshTimeout(): void {
+ const refresh = refreshTick;
+ if (refresh !== null) {
+ refresh();
+ } else {
+ const timeout = setTimeout(onTick, TICK_MS);
+ timeout.unref();
+ refreshTick = () => {
+ timeout.refresh();
+ };
+ }
+}
+
+export class FastTimer {
+ state: number;
+ idleTimeout: number;
+ idleStart: number;
+ onTimeout: () => void;
+
+ constructor(callback: () => void, delay: number) {
+ this.state = NOT_IN_LIST;
+ this.idleTimeout = delay;
+ this.idleStart = -1;
+ this.onTimeout = callback;
+ this.refresh();
+ }
+
+ refresh(): void {
+ if (this.state === NOT_IN_LIST) fastTimers.push(this);
+ if (refreshTick === null || fastTimers.length === 1) refreshTimeout();
+ this.state = PENDING;
+ }
+
+ clear(): void {
+ this.state = TO_BE_CLEARED;
+ this.idleStart = -1;
+ }
+}
diff --git a/packages/compiler/shims/undici/core/types.ts b/packages/compiler/shims/undici/core/types.ts
new file mode 100644
index 000000000..652918afa
--- /dev/null
+++ b/packages/compiler/shims/undici/core/types.ts
@@ -0,0 +1,167 @@
+// Types shared by the undici entries. They restate the subset of undici's
+// own declarations (types/dispatcher.d.ts, connector.d.ts, ...) that the
+// shim implements; test/package-shims/undici-types.test.ts checks them
+// against the installed 5.x and 7.x declarations.
+import type { Socket } from "node:net";
+import type { Dispatcher } from "./dispatcher.ts";
+
+/** Response and request header records (undici's IncomingHttpHeaders). */
+export type IncomingHttpHeaders = Record;
+
+/** What a request's `headers` option accepts here: a record or a flat
+ * [name, value, ...] array. */
+export type UndiciHeaders = Record | string[] | null;
+
+/** The payloads the shim writes. Streams, iterables, Blob and FormData
+ * bodies are outside the implemented subset. */
+export type RequestBody = string | Buffer | Uint8Array | null;
+
+export interface DispatchOptions {
+ origin?: string | URL;
+ path: string;
+ method: string;
+ body?: RequestBody;
+ headers?: UndiciHeaders;
+ idempotent?: boolean;
+ blocking?: boolean;
+ headersTimeout?: number | null;
+ bodyTimeout?: number | null;
+ reset?: boolean;
+ throwOnError?: boolean;
+ // Dispatcher.RequestOptions' members live here too: a record passed to
+ // dispatch() is copied to this shape, and interceptors read them back
+ // through a `params as Dispatcher.RequestOptions` cast, as with undici.
+ opaque?: unknown;
+ signal?: AbortSignal | null;
+ onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void;
+ highWaterMark?: number;
+}
+
+/** undici's dispatch-controller (7.x handler API). */
+export interface DispatchController {
+ readonly aborted: boolean;
+ readonly paused: boolean;
+ readonly reason: Error | null;
+ abort(reason: Error): void;
+ pause(): void;
+ resume(): void;
+}
+
+/** Dispatch handlers: the 5.x callbacks, plus 7.x's controller callbacks. */
+export interface DispatchHandlers {
+ onConnect?(abort: (err?: Error) => void): void;
+ onError?(err: Error): void;
+ onHeaders?(
+ statusCode: number,
+ headers: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean;
+ onData?(chunk: Buffer): boolean;
+ onComplete?(trailers: string[] | null): void;
+ onBodySent?(chunkSize: number, totalBytesSent: number): void;
+ onRequestStart?(controller: DispatchController, context: unknown): void;
+ onResponseStart?(
+ controller: DispatchController,
+ statusCode: number,
+ headers: IncomingHttpHeaders,
+ statusMessage?: string,
+ ): void;
+ onResponseData?(controller: DispatchController, chunk: Buffer): void;
+ onResponseEnd?(controller: DispatchController, trailers: IncomingHttpHeaders): void;
+ onResponseError?(controller: DispatchController, error: Error): void;
+}
+
+export type DispatchFn = (options: DispatchOptions, handler: DispatchHandlers) => boolean;
+
+export type DispatchInterceptor = (dispatch: DispatchFn) => DispatchFn;
+
+/** buildConnector's per-connection parameters. */
+export interface ConnectorOptions {
+ hostname: string;
+ host?: string;
+ protocol: string;
+ port: string;
+ servername?: string;
+ localAddress?: string | null;
+}
+
+export type ConnectorCallback = (err: Error | null, socket: Socket | null) => void;
+
+/** A connector: dials, reports through the callback, and returns the socket
+ * (undici 5 and 7 both return it although their types declare void). */
+export type Connector = (options: ConnectorOptions, callback: ConnectorCallback) => Socket;
+
+/** net/tls connect options a connector forwards (the implemented subset). */
+export interface ConnectorBuildOptions {
+ timeout?: number | null;
+ maxCachedSessions?: number | null;
+ socketPath?: string | null;
+ port?: number;
+ rejectUnauthorized?: boolean;
+ ca?: string | Buffer | (string | Buffer)[];
+ servername?: string;
+ /** net.connect's resolver hook: @types/node's LookupFunction is
+ * assignable (method syntax; the callback takes one address or all). */
+ lookup?(
+ hostname: string,
+ options: unknown,
+ callback: (
+ err: NodeJS.ErrnoException | null,
+ address: string | LookupAddress[],
+ family?: number,
+ ) => void,
+ ): void;
+ keepAlive?: boolean;
+ keepAliveInitialDelay?: number;
+ noDelay?: boolean;
+ autoSelectFamily?: boolean;
+ autoSelectFamilyAttemptTimeout?: number;
+}
+
+export interface LookupAddress {
+ address: string;
+ family: number;
+}
+
+export interface ClientOptions {
+ keepAliveTimeout?: number | null;
+ keepAliveMaxTimeout?: number | null;
+ keepAliveTimeoutThreshold?: number | null;
+ pipelining?: number | null;
+ connect?: ConnectorBuildOptions | Connector | null;
+ maxHeaderSize?: number | null;
+ bodyTimeout?: number | null;
+ headersTimeout?: number | null;
+ strictContentLength?: boolean;
+ maxRequestsPerClient?: number;
+ maxResponseSize?: number | null;
+ connectTimeout?: number | null;
+ localAddress?: string;
+ /** 5.x applies `Client` interceptors; 7.x ignores the option. */
+ interceptors?: { Client?: readonly DispatchInterceptor[] | undefined };
+}
+
+export interface PoolOptions extends ClientOptions {
+ factory?(origin: URL, opts: object): Dispatcher;
+ connections?: number | null;
+ interceptors?: {
+ Pool?: readonly DispatchInterceptor[] | undefined;
+ Client?: readonly DispatchInterceptor[] | undefined;
+ };
+}
+
+export interface AgentOptions extends PoolOptions {
+ factory?(origin: string | URL, opts: object): Dispatcher;
+ interceptors?: {
+ Agent?: readonly DispatchInterceptor[] | undefined;
+ Pool?: readonly DispatchInterceptor[] | undefined;
+ Client?: readonly DispatchInterceptor[] | undefined;
+ };
+}
+
+/** Version-dependent behavior, selected by the importing package's installed
+ * undici (the 5.x or 7.x entry). */
+export interface Profile {
+ major: number;
+}
diff --git a/packages/compiler/shims/undici/core/util.ts b/packages/compiler/shims/undici/core/util.ts
new file mode 100644
index 000000000..2fc9ffbf6
--- /dev/null
+++ b/packages/compiler/shims/undici/core/util.ts
@@ -0,0 +1,67 @@
+// Helpers from undici's lib/core/util.js.
+import { InvalidArgumentError } from "./errors.ts";
+import type { IncomingHttpHeaders, Profile } from "./types.ts";
+
+/** util.parseHeaders over raw [name, value, ...] pairs: lower-cased names,
+ * repeated names collected into arrays. 5.x decodes values as UTF-8 and
+ * treats an empty first value as absent; 7.x decodes latin1. */
+export function parseHeaders(raw: readonly string[], profile: Profile): IncomingHttpHeaders {
+ const obj: IncomingHttpHeaders = {};
+ for (let i = 0; i + 1 < raw.length; i += 2) {
+ const key = raw[i]!.toLowerCase();
+ const value = raw[i + 1]!;
+ const existing = obj[key];
+ const absent = profile.major >= 7 ? existing === undefined : !existing;
+ if (absent) {
+ obj[key] = value;
+ } else if (typeof existing === "string") {
+ obj[key] = [existing, value];
+ } else if (existing !== undefined) {
+ existing.push(value);
+ }
+ }
+ return obj;
+}
+
+/** Raw header Buffers as strings (latin1 on 7.x, UTF-8 on 5.x). */
+export function rawHeaderStrings(raw: readonly Buffer[], profile: Profile): string[] {
+ const out: string[] = [];
+ for (let i = 0; i < raw.length; i++) {
+ // Names are ASCII tokens; values follow the version's decoding.
+ out.push(
+ i % 2 === 0 || profile.major >= 7 ? raw[i]!.toString("latin1") : raw[i]!.toString("utf8"),
+ );
+ }
+ return out;
+}
+
+/** util.parseOrigin: an http(s) origin URL. */
+export function parseOrigin(url: string | URL, profile: Profile): URL {
+ const parsed = parseURL(url, profile);
+ if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") {
+ throw new InvalidArgumentError("invalid url");
+ }
+ return parsed;
+}
+
+/** util.parseURL for the string/URL forms. */
+export function parseURL(url: string | URL, profile: Profile): URL {
+ const parsed = typeof url === "string" ? new URL(url) : url;
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
+ // 7.x checks only string inputs; 5.x checks every input.
+ if (profile.major < 7) throw new InvalidArgumentError("invalid protocol");
+ if (typeof url === "string") {
+ throw new InvalidArgumentError(
+ "Invalid URL protocol: the URL must start with `http:` or `https:`.",
+ );
+ }
+ }
+ return parsed;
+}
+
+/** util.parseKeepAliveTimeout: the server's Keep-Alive `timeout=` hint in
+ * milliseconds, or null. */
+export function parseKeepAliveTimeout(value: string): number | null {
+ const m = /timeout=(\d+)/.exec(value);
+ return m !== null ? parseInt(m[1]!, 10) * 1000 : null;
+}
diff --git a/packages/compiler/shims/undici/v5.ts b/packages/compiler/shims/undici/v5.ts
new file mode 100644
index 000000000..b97c65dfa
--- /dev/null
+++ b/packages/compiler/shims/undici/v5.ts
@@ -0,0 +1,119 @@
+// scriptc's built-in implementation of the `undici` package, 5.x API.
+//
+// Programs whose installed undici is 5.x compile this module in its place
+// (see packages/compiler/src/frontend/package-shims.ts). It implements the
+// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool,
+// Agent, request(), buildConnector, errors, the global dispatcher, fetch and
+// the diagnostics_channel publications. Anything else undici exports is not
+// declared here, so using it is a compile-time error.
+import { Agent as CoreAgent } from "./core/agent.ts";
+import {
+ getGlobalDispatcher as coreGetGlobalDispatcher,
+ request as coreRequest,
+ setGlobalDispatcher as coreSetGlobalDispatcher,
+} from "./core/api.ts";
+import type { TopLevelRequestOptions } from "./core/api.ts";
+import { Client as CoreClient } from "./core/client.ts";
+import { buildConnector as coreBuildConnector } from "./core/connector.ts";
+import { Dispatcher } from "./core/dispatcher.ts";
+import type { ResponseData } from "./core/dispatcher.ts";
+import { undiciFetch } from "./core/fetch.ts";
+import type { UndiciRequestInit } from "./core/fetch.ts";
+import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts";
+import type {
+ AgentOptions,
+ ClientOptions,
+ Connector,
+ ConnectorBuildOptions,
+ ConnectorCallback,
+ ConnectorOptions,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./core/types.ts";
+import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts";
+
+const profile: Profile = { major: 5 };
+
+export { Dispatcher };
+export type { DiagnosticsChannel } from "./core/diagnostics-types.ts";
+export * as errors from "./core/errors-v5.ts";
+
+export class Client extends CoreClient {
+ constructor(url: string | URL, options?: Client.Options) {
+ super(url, options, profile);
+ }
+}
+
+export declare namespace Client {
+ type Options = ClientOptions;
+ type SocketInfo = CoreSocketInfo;
+}
+
+export class Pool extends CorePool {
+ constructor(url: string | URL, options?: Pool.Options) {
+ super(url, options, profile);
+ }
+
+ __createClient(origin: URL, options: ClientOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+}
+
+export declare namespace Pool {
+ type Options = PoolOptions;
+ type PoolStats = CorePoolStats;
+}
+
+export class Agent extends CoreAgent {
+ constructor(options?: Agent.Options) {
+ super(options, profile);
+ }
+
+ __createClient(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+
+ __createPool(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Pool(origin, options);
+ }
+}
+
+export declare namespace Agent {
+ type Options = AgentOptions;
+ type DispatchOptions = import("./core/types.ts").DispatchOptions;
+}
+
+export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector {
+ return coreBuildConnector(options ?? {});
+}
+
+export declare namespace buildConnector {
+ type BuildOptions = ConnectorBuildOptions;
+ type Options = ConnectorOptions;
+ type Callback = ConnectorCallback;
+ type connector = Connector;
+}
+
+export function setGlobalDispatcher(dispatcher: Dispatcher): void {
+ coreSetGlobalDispatcher(dispatcher);
+}
+
+export function getGlobalDispatcher(): Dispatcher {
+ return coreGetGlobalDispatcher(() => new Agent());
+}
+
+export function request(
+ url: string | URL,
+ options?: TopLevelRequestOptions,
+): Promise {
+ return coreRequest(url, options, () => new Agent(), profile);
+}
+
+export function fetch(input: string | URL, init?: UndiciRequestInit): Promise {
+ return undiciFetch(input, init, () => new Agent(), profile);
+}
+
+// undici's Headers is the WHATWG class the runtime implements natively.
+export import Headers = globalThis.Headers;
+export type { DispatchOptions };
diff --git a/packages/compiler/shims/undici/v7.ts b/packages/compiler/shims/undici/v7.ts
new file mode 100644
index 000000000..63b1b1f64
--- /dev/null
+++ b/packages/compiler/shims/undici/v7.ts
@@ -0,0 +1,119 @@
+// scriptc's built-in implementation of the `undici` package, 7.x API.
+//
+// Programs whose installed undici is 7.x compile this module in its place
+// (see packages/compiler/src/frontend/package-shims.ts). It implements the
+// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool,
+// Agent, request(), buildConnector, errors, the global dispatcher, fetch and
+// the diagnostics_channel publications. Anything else undici exports is not
+// declared here, so using it is a compile-time error.
+import { Agent as CoreAgent } from "./core/agent.ts";
+import {
+ getGlobalDispatcher as coreGetGlobalDispatcher,
+ request as coreRequest,
+ setGlobalDispatcher as coreSetGlobalDispatcher,
+} from "./core/api.ts";
+import type { TopLevelRequestOptions } from "./core/api.ts";
+import { Client as CoreClient } from "./core/client.ts";
+import { buildConnector as coreBuildConnector } from "./core/connector.ts";
+import { Dispatcher } from "./core/dispatcher.ts";
+import type { ResponseData } from "./core/dispatcher.ts";
+import { undiciFetch } from "./core/fetch.ts";
+import type { UndiciRequestInit } from "./core/fetch.ts";
+import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts";
+import type {
+ AgentOptions,
+ ClientOptions,
+ Connector,
+ ConnectorBuildOptions,
+ ConnectorCallback,
+ ConnectorOptions,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./core/types.ts";
+import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts";
+
+const profile: Profile = { major: 7 };
+
+export { Dispatcher };
+export type { DiagnosticsChannel } from "./core/diagnostics-types.ts";
+export * as errors from "./core/errors-v7.ts";
+
+export class Client extends CoreClient {
+ constructor(url: string | URL, options?: Client.Options) {
+ super(url, options, profile);
+ }
+}
+
+export declare namespace Client {
+ type Options = ClientOptions;
+ type SocketInfo = CoreSocketInfo;
+}
+
+export class Pool extends CorePool {
+ constructor(url: string | URL, options?: Pool.Options) {
+ super(url, options, profile);
+ }
+
+ __createClient(origin: URL, options: ClientOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+}
+
+export declare namespace Pool {
+ type Options = PoolOptions;
+ type PoolStats = CorePoolStats;
+}
+
+export class Agent extends CoreAgent {
+ constructor(options?: Agent.Options) {
+ super(options, profile);
+ }
+
+ __createClient(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+
+ __createPool(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Pool(origin, options);
+ }
+}
+
+export declare namespace Agent {
+ type Options = AgentOptions;
+ type DispatchOptions = import("./core/types.ts").DispatchOptions;
+}
+
+export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector {
+ return coreBuildConnector(options ?? {});
+}
+
+export declare namespace buildConnector {
+ type BuildOptions = ConnectorBuildOptions;
+ type Options = ConnectorOptions;
+ type Callback = ConnectorCallback;
+ type connector = Connector;
+}
+
+export function setGlobalDispatcher(dispatcher: Dispatcher): void {
+ coreSetGlobalDispatcher(dispatcher);
+}
+
+export function getGlobalDispatcher(): Dispatcher {
+ return coreGetGlobalDispatcher(() => new Agent());
+}
+
+export function request(
+ url: string | URL,
+ options?: TopLevelRequestOptions,
+): Promise {
+ return coreRequest(url, options, () => new Agent(), profile);
+}
+
+export function fetch(input: string | URL, init?: UndiciRequestInit): Promise {
+ return undiciFetch(input, init, () => new Agent(), profile);
+}
+
+// undici's Headers is the WHATWG class the runtime implements natively.
+export import Headers = globalThis.Headers;
+export type { DispatchOptions };
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/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts
index 8c9590bee..74d79f7be 100644
--- a/packages/compiler/src/backend/llvm/array-search.ts
+++ b/packages/compiler/src/backend/llvm/array-search.ts
@@ -63,7 +63,9 @@ export function emitArraySearchHelpers(
`define internal double @${arraySearchHelperName(kind)}(ptr %a, ptr %v) ${FN_ATTRS} {`,
`entry:`,
` %lenp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 1`,
- ` %len = load ${sz}, ptr %lenp${sz === "i64" ? ", !range !{i64 0, i64 4294967296}" : ""}`,
+ ...(sz === "i64"
+ ? [` %len32 = load i32, ptr %lenp`, ` %len = zext i32 %len32 to i64`]
+ : [` %len = load i32, ptr %lenp`]),
` %fp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 6`,
` %f32 = load i32, ptr %fp`,
widen,
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/common.ts b/packages/compiler/src/backend/llvm/common.ts
index 8a8778919..e6fc56f6f 100644
--- a/packages/compiler/src/backend/llvm/common.ts
+++ b/packages/compiler/src/backend/llvm/common.ts
@@ -10,7 +10,9 @@ import type {
import type { LlvmEmitterContext } from "./expr-context.js";
/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for
- * field (its static assertions pin `cyc`, `filled` and `data`). */
+ * field (its static assertions pin `len`, `cap`, `cyc`, `filled` and
+ * `data`). `len` and `cap` are i32 on every target; the dense states end
+ * the allocation after the `cap` value slots at `data` (scrArrStates). */
export const SCR_ARR_FIELD = {
rc: 0,
len: 1,
@@ -21,10 +23,92 @@ export const SCR_ARR_FIELD = {
filled: 6,
ops: 7,
data: 8,
- present: 9,
- ext: 10,
+ ext: 9,
} as const;
+/** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a
+ * size-type count. */
+export function loadArrCount(
+ host: Pick,
+ dest: string,
+ ptr: string,
+ attach = "",
+): void {
+ const B = host.B;
+ if (host.sizeType === "i32") {
+ B.line(`${dest} = load i32, ptr ${ptr}${attach}`);
+ return;
+ }
+ const raw = B.tmp();
+ B.line(`${raw} = load i32, ptr ${ptr}${attach}`);
+ B.line(`${dest} = zext i32 ${raw} to ${host.sizeType}`);
+}
+
+/** Stores the size-type count `value` (at most 2^32 - 1) into the i32 `len`
+ * or `cap` of a %ScrArr at `ptr`. */
+export function storeArrCount(
+ host: Pick,
+ value: string,
+ ptr: string,
+ attach = "",
+): void {
+ const B = host.B;
+ let narrow = value;
+ if (host.sizeType !== "i32") {
+ narrow = B.tmp();
+ B.line(`${narrow} = trunc ${host.sizeType} ${value} to i32`);
+ }
+ B.line(`store i32 ${narrow}, ptr ${ptr}${attach}`);
+}
+
+/** The end of the dense state bytes of a %ScrArr into `dest`
+ * (scr_arr_states_end): `cap` value slots of `width` bytes at `data`, then
+ * one state byte per slot in reverse slot order. `cap` is a size-type count. */
+export function scrArrStates(
+ host: Pick,
+ dest: string,
+ data: string,
+ cap: string,
+ width: number,
+): void {
+ host.B.line(
+ `${dest} = getelementptr inbounds [${width + 1} x i8], ptr ${data}, ${host.sizeType} ${cap}`,
+ );
+}
+
+/** The state byte of dense slot `offset` into `dest`, given the end of the
+ * states (scrArrStates): `end - 1 - offset`. */
+export function scrArrStatePtr(
+ host: Pick,
+ dest: string,
+ end: string,
+ offset: string,
+): void {
+ const B = host.B;
+ const back = B.tmp();
+ B.line(`${back} = xor ${host.sizeType} ${offset}, -1`);
+ B.line(`${dest} = getelementptr inbounds i8, ptr ${end}, ${host.sizeType} ${back}`);
+}
+
+/** The state byte of dense slot `offset` of a %ScrArr into `dest`, from its
+ * `data` and size-type `cap`: scrArrStates and scrArrStatePtr in one
+ * address computation, for paths that read a single state. */
+export function scrArrStateAt(
+ host: Pick,
+ dest: string,
+ data: string,
+ cap: string,
+ width: number,
+ offset: string,
+): void {
+ const B = host.B;
+ const back = B.tmp();
+ B.line(`${back} = xor ${host.sizeType} ${offset}, -1`);
+ B.line(
+ `${dest} = getelementptr inbounds [${width + 1} x i8], ptr ${data}, ${host.sizeType} ${cap}, ${host.sizeType} ${back}`,
+ );
+}
+
/** Lines that decide whether the double `index` is an integer in
* `[0, length)`, where `length` is an unsigned `sizeType` count, and give
* that integer as an i64. `%{ok}` is the answer; `%{wide}` is meaningful
diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts
index 6da427841..1b84cf0a0 100644
--- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts
@@ -170,6 +170,10 @@ test.each([32, 64] as const)(
expect(fn.match(new RegExp(`icmp ult ${size} `, "g"))).toHaveLength(3);
expect(fn).toMatch(/store double 0x4004000000000000, ptr /);
expect(fn).toMatch(/zext i1 .* to i8/);
+ // Per push: data + (cap - len), then the state byte `width * cap - 1`
+ // past it; plus the boolean value slot.
+ expect(fn.match(/getelementptr inbounds \[8 x i8\], ptr .* -1$/gm)).toHaveLength(2);
+ expect(fn.match(/getelementptr inbounds \[1 x i8\], ptr .* -1$/gm)).toHaveLength(1);
expect(fn.match(/getelementptr inbounds i8, ptr /g)).toHaveLength(4);
expect(fn.match(/store i8 1, ptr /g)).toHaveLength(3);
expect(fn.match(new RegExp(`add nuw ${size} .*, 1$`, "gm"))).toHaveLength(3);
diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts
index d02d7f7c3..4488f90cb 100644
--- a/packages/compiler/src/backend/llvm/dense-array-access.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-access.ts
@@ -17,7 +17,15 @@
* whose writes extend the bound when they continue it. */
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
-import { f64Lit, SCR_ARR_FIELD, sizeToDouble } from "./common.js";
+import {
+ f64Lit,
+ loadArrCount,
+ SCR_ARR_FIELD,
+ scrArrStatePtr,
+ scrArrStates,
+ sizeToDouble,
+ storeArrCount,
+} from "./common.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { exactInteger, widenInteger } from "./integer-values.js";
@@ -36,7 +44,8 @@ interface DenseGuard {
len: string;
/** The hole-free prefix bound loaded by the guard (i32). */
filled: string;
- /** Dense value and state storage base pointers. */
+ /** The dense value storage base pointer, and the end of the states
+ * (scrArrStates). */
data: string;
states: string;
}
@@ -48,7 +57,9 @@ interface DenseGuard {
* below the hole-free prefix bound branches to `filledLabel` first; both
* that block and the returned (state-checked) dense block may use the
* guard's values. A `proven` index is known to lie below the bound (a dense
- * array loop's fast copy, dense-array-loops.ts) and always takes it. */
+ * array loop's fast copy, dense-array-loops.ts) and always takes it. With
+ * `prefixOnly`, every other index branches to `slow` and no block is left
+ * open: only `offset` and `data` are meaningful. */
function emitDenseGuard(
host: LlvmEmitterContext,
array: string,
@@ -57,7 +68,9 @@ function emitDenseGuard(
slow: string,
requireBelowLen: boolean,
filledLabel: string,
+ width: number,
proven = false,
+ prefixOnly = false,
): DenseGuard {
const B = host.B;
const size = host.sizeType;
@@ -89,7 +102,6 @@ function emitDenseGuard(
cap = B.tmp(),
dataPtr = B.tmp(),
data = B.tmp(),
- statesPtr = B.tmp(),
states = B.tmp();
B.line(
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
@@ -97,26 +109,23 @@ function emitDenseGuard(
host.markMemoryPointer(dataPtr, "array:header");
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
const loadChecked = (): void => {
- B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`);
+ B.line(
+ `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
+ );
host.markMemoryPointer(lenPtr, "array:header");
+ loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr));
B.line(
- `${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
+ `${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.cap}`,
);
- B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 2`);
host.markMemoryPointer(capPtr, "array:header");
- B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
- B.line(
- `${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.present}`,
- );
- host.markMemoryPointer(statesPtr, "array:header");
- B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
+ loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
};
// Reads load the storage bases before branching: the header of a live
// array is always readable, and unconditional loads let LLVM hoist them
// out of loops whose only calls are read-only fallbacks. Writes load the
// fields that only the state-checked path needs inside it, which keeps
// them off a hot loop of prefix stores (that path stores to the header).
- const eagerHeader = requireBelowLen;
+ const eagerHeader = requireBelowLen && !prefixOnly;
if (eagerHeader) loadChecked();
const filledPtr = B.tmp(),
filled = B.tmp(),
@@ -148,6 +157,10 @@ function emitDenseGuard(
B.line(`${t} = and i1 ${integral}, ${inFilled}`);
inFilled = t;
}
+ if (prefixOnly) {
+ B.condBr(inFilled, filledLabel, slow);
+ return { offset, len, filled, data, states };
+ }
const check = B.newLabel("arr.check");
B.condBr(inFilled, filledLabel, check);
B.startBlock(check);
@@ -171,6 +184,10 @@ function emitDenseGuard(
const dense = B.newLabel("arr.dense");
B.condBr(ok, dense, slow);
B.startBlock(dense);
+ // Only the state-checked block needs the states: the address is pure
+ // arithmetic on loaded fields, so computing it here keeps it off the
+ // prefix path.
+ scrArrStates(host, states, data, cap, width);
return { offset, len, filled, data, states };
}
@@ -186,11 +203,8 @@ function statePointer(
host: LlvmEmitterContext,
guard: Pick,
): string {
- const B = host.B;
- const statePtr = B.tmp();
- B.line(
- `${statePtr} = getelementptr inbounds i8, ptr ${guard.states}, ${host.sizeType} ${guard.offset}`,
- );
+ const statePtr = host.B.tmp();
+ scrArrStatePtr(host, statePtr, guard.states, guard.offset);
host.markMemoryPointer(statePtr, "array:present");
return statePtr;
}
@@ -201,6 +215,11 @@ function slotType(acc: ArrayAccess): "i8" | "i64" {
return acc === "bool" ? "i8" : "i64";
}
+/** Bytes per dense slot (scr_arr_slot_width). */
+export function slotWidth(acc: ArrayAccess): number {
+ return acc === "bool" ? 1 : 8;
+}
+
function valuePointer(
host: LlvmEmitterContext,
guard: Pick,
@@ -245,7 +264,7 @@ export function emitDenseSlotUnlink(
const unlink = B.newLabel("arr.unlink"),
swap = B.newLabel("arr.unlink.swap"),
done = B.newLabel("arr.unlink.done");
- const guard = emitDenseGuard(host, array, index, indexExpr, done, true, unlink);
+ const guard = emitDenseGuard(host, array, index, indexExpr, done, true, unlink, 8);
// The state-checked dense block: leave holes and the tail to the store.
B.br(done);
B.startBlock(unlink);
@@ -290,11 +309,29 @@ export function emitDenseArrayGet(
const slow = B.newLabel("arr.get.slow"),
join = B.newLabel("arr.get.join"),
valueLabel = B.newLabel("arr.get.value");
- const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel, proven);
- const state = loadState(host, guard);
- const present = B.tmp();
- B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
- B.condBr(present, valueLabel, slow);
+ // The state of slot i is at data + cap * (width + 1) - 1 - i: an inline
+ // state check indexed by a loop's induction variable makes LLVM keep
+ // -1 - i as a second induction variable, so above the prefix the runtime
+ // getter reads the slot.
+ const induction = indexExpr?.kind === "varRef" && host.integerLoopBindings.has(indexExpr.localId);
+ const guard = emitDenseGuard(
+ host,
+ array,
+ index,
+ indexExpr,
+ slow,
+ true,
+ valueLabel,
+ slotWidth(acc),
+ proven,
+ induction,
+ );
+ if (!induction) {
+ const state = loadState(host, guard);
+ const present = B.tmp();
+ B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
+ B.condBr(present, valueLabel, slow);
+ }
B.startBlock(valueLabel);
const ptr = valuePointer(host, guard, acc);
let fast: string;
@@ -360,6 +397,7 @@ export function emitDenseArrayState(
index: LlValue,
indexExpr: IrExpr | undefined,
kind: "state" | "has",
+ width: number,
): string {
const B = host.B;
const fn = kind === "state" ? "scr_arr_state" : "scr_arr_has";
@@ -368,7 +406,7 @@ export function emitDenseArrayState(
const slow = B.newLabel("arr.state.slow"),
join = B.newLabel("arr.state.join"),
filledLabel = B.newLabel("arr.state.filled");
- const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, filledLabel);
+ const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, filledLabel, width);
const state = loadState(host, guard);
const fast = B.tmp();
if (kind === "state") B.line(`${fast} = uitofp i8 ${state} to double`);
@@ -421,7 +459,17 @@ export function emitDenseArraySet(
const slow = B.newLabel("arr.set.slow"),
join = B.newLabel("arr.set.join"),
filledLabel = B.newLabel("arr.set.filled");
- const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel, proven);
+ const guard = emitDenseGuard(
+ host,
+ array,
+ index,
+ indexExpr,
+ slow,
+ false,
+ filledLabel,
+ slotWidth(acc),
+ proven,
+ );
const { offset, len } = guard;
const storeValue = (valuePtr: string): void => {
if (acc === "f64") {
@@ -482,9 +530,11 @@ export function emitDenseArraySet(
const nextLen = B.tmp(),
lenPtr = B.tmp();
B.line(`${nextLen} = add ${host.sizeType} ${offset}, 1`);
- B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`);
+ B.line(
+ `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
+ );
host.markMemoryPointer(lenPtr, "array:header");
- B.line(`store ${host.sizeType} ${nextLen}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
+ storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr));
B.br(storedLabel);
B.startBlock(storedLabel);
if (acc === "ref" && oldState && old) {
@@ -542,7 +592,7 @@ export function emitDenseArrayPop(
const lenPtr = header(SCR_ARR_FIELD.len),
len = B.tmp(),
none = B.tmp();
- B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
+ loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr));
B.line(`${none} = icmp eq ${size} ${len}, 0`);
const check = B.newLabel("arr.pop.check"),
dense = B.newLabel("arr.pop.dense");
@@ -557,12 +607,15 @@ export function emitDenseArrayPop(
B.startBlock(dense);
const last = B.tmp();
B.line(`${last} = sub nuw ${size} ${len}, 1`);
+ // A packed array's last index is below its capacity (filled <= cap).
const dataPtr = header(SCR_ARR_FIELD.data),
data = B.tmp(),
- statesPtr = header(SCR_ARR_FIELD.present),
+ capPtr = header(SCR_ARR_FIELD.cap),
+ cap = B.tmp(),
states = B.tmp();
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
- B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
+ loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
+ scrArrStates(host, states, data, cap, slotWidth(acc));
const valuePtr = valuePointer(host, { offset: last, data }, acc);
const statePtr = statePointer(host, { offset: last, states });
const slotTy = slotType(acc);
@@ -581,7 +634,7 @@ export function emitDenseArrayPop(
B.line(`${last32} = trunc i64 ${last} to i32`);
}
B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
- B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
+ B.line(`store i32 ${last32}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
return raw;
}
@@ -608,13 +661,13 @@ export function emitDenseArrayPush(
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
- const lenPtr = header(1),
+ const lenPtr = header(SCR_ARR_FIELD.len),
len = B.tmp(),
cap = B.tmp(),
room = B.tmp();
- B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
- const capPtr = header(2);
- B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
+ loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr));
+ const capPtr = header(SCR_ARR_FIELD.cap);
+ loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
B.line(`${room} = icmp ult ${size} ${len}, ${cap}`);
const fast = B.newLabel("arr.push.dense"),
slow = B.newLabel("arr.push.slow"),
@@ -623,11 +676,18 @@ export function emitDenseArrayPush(
B.startBlock(fast);
const dataPtr = header(SCR_ARR_FIELD.data),
data = B.tmp(),
- statesPtr = header(SCR_ARR_FIELD.present),
- states = B.tmp();
+ spare = B.tmp(),
+ near = B.tmp(),
+ statePtr = B.tmp();
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
- B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
- const statePtr = statePointer(host, { offset: len, states });
+ // The state of slot `len` is data + width * cap + (cap - len) - 1. With
+ // `cap - len` formed first, the store addresses it from one register sum.
+ B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`);
+ B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`);
+ B.line(
+ `${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`,
+ );
+ host.markMemoryPointer(statePtr, "array:present");
const valuePtr = valuePointer(host, { offset: len, data }, acc);
if (acc === "f64") {
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
@@ -642,7 +702,7 @@ export function emitDenseArrayPush(
const next = B.tmp(),
fastValue = B.tmp();
B.line(`${next} = add nuw ${size} ${len}, 1`);
- B.line(`store ${size} ${next}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
+ storeArrCount(host, next, lenPtr, host.fieldAliasAttachment(lenPtr));
// An append to a hole-free array keeps it hole-free.
const filledPtr = header(SCR_ARR_FIELD.filled),
filled = B.tmp(),
diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts
index 271a5ddf8..708825344 100644
--- a/packages/compiler/src/backend/llvm/dense-array-loops.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts
@@ -22,7 +22,7 @@ import type { IntegerRange } from "../../ir/integer-ranges.js";
import { preservesArrayLength } from "../../ir/array-length-stability.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import type { LlvmEmitterContext } from "./expr-context.js";
-import { SCR_ARR_FIELD } from "./common.js";
+import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
export const INT32_MAX = 2147483647;
@@ -187,12 +187,10 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
return ptr;
};
const receiver = host.emitStableReceiver(loop.length, []);
- const lenPtr = header(receiver.name, 1),
+ const lenPtr = header(receiver.name, SCR_ARR_FIELD.len),
len = B.tmp(),
small = B.tmp();
- B.line(
- `${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
- );
+ loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr));
B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`);
let valid = small;
for (const [id, ref] of loop.arrays) {
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index 731385825..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`,
`}`,
@@ -1631,10 +1680,10 @@ export class LlDyn {
: []),
]
: [
- { index: 1, type: host.sizeType, name: "length" },
- { index: 2, type: host.sizeType, name: "capacity" },
+ // The dense states follow the data slots: they move with data and capacity.
+ { index: SCR_ARR_FIELD.len, type: "i32", name: "length" },
+ { index: SCR_ARR_FIELD.cap, type: "i32", name: "capacity" },
{ index: SCR_ARR_FIELD.data, type: "ptr" as const, name: "data" },
- { index: SCR_ARR_FIELD.present, type: "ptr" as const, name: "presence" },
{ index: SCR_ARR_FIELD.filled, type: "i32" as const, name: "filled" },
];
members.forEach((member) => {
@@ -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 4e4f11828..6a3df30cd 100644
--- a/packages/compiler/src/backend/llvm/emit-module.ts
+++ b/packages/compiler/src/backend/llvm/emit-module.ts
@@ -436,14 +436,15 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
`%ScrClosure = type { ${emitter.sizeType}, ptr, ${emitter.sizeType}, ptr, i32, i32, ptr, ptr }`,
`%ScrFfiTable = type { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, i8, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`,
`%ScrRegex = type { ${emitter.sizeType}, ptr, ptr, ptr, double, ptr, ptr, ptr }`,
- // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem,
- // cyc, spare, filled, ops, data, present, ext } (field numbers in
- // SCR_ARR_FIELD), where filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor
+ // ScrArr mirrors scr_runtime.h field-for-field: { rc, len (i32), cap
+ // (i32), elem, cyc, spare, filled, ops, data, ext } (field numbers in
+ // SCR_ARR_FIELD), where the dense states follow the cap value slots at
+ // data, filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor
// (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse,
// property and metadata storage (NULL until first used). Live dynamic
// stream commits swap its dense, presence and out-of-line storage while
// preserving the target object's identity.
- `%ScrArr = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, i8, i8, i16, i32, ptr, ptr, ptr, ptr }`,
+ `%ScrArr = type { ${emitter.sizeType}, i32, i32, i8, i8, i16, i32, ptr, ptr, ptr }`,
`%ScrArrOps = type { ptr, ptr, ptr }`,
// The ScrMap prefix { rc, key_kind, val_kind, traced } — the inline
// RC fast paths test the traced byte (shapes.ts).
@@ -684,17 +685,23 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
if (emitter.regexInstances.size > 0) out.push(``);
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
for (const [, inst] of emitter.templateStringsInstances) {
- // One immortal ScrArr per tagged-template site: a [N x ptr] data
- // global of interned cooked-string literals, and the ScrArr header
- // over it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2,
- // no REF entry points). Every dense slot is present; reads retain
- // immortal strings — a no-op.
+ // One immortal ScrArr per tagged-template site: a data global of
+ // interned cooked-string literals followed by their states (the
+ // runtime finds them before data + cap * 9), and the ScrArr header over
+ // it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF
+ // entry points). Every dense slot is present; reads retain immortal
+ // strings — a no-op. Slots are 8 bytes on every target: on 32-bit
+ // targets each holds the pointer in its low half and zero in its high
+ // half, so the states start where the runtime looks for them.
const n = inst.slots.length;
const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
+ const wide = emitter.sizeType === "i64";
+ const slot = wide ? "ptr" : "<{ ptr, i32 }>";
+ const slotValue = (s: string): string =>
+ wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`;
out.push(
- `@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`,
- `@${inst.sym}_present = internal constant [${n} x i8] ${present}`,
- `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, ${emitter.sizeType} ${n}, ${emitter.sizeType} ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`,
+ `@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`,
+ `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr null }`,
);
}
if (emitter.templateStringsInstances.size > 0) out.push(``);
@@ -1333,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/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts
index a5da56f55..da2ffbcab 100644
--- a/packages/compiler/src/backend/llvm/emit-statements.ts
+++ b/packages/compiler/src/backend/llvm/emit-statements.ts
@@ -36,7 +36,7 @@ import { emitDiscardedSplice } from "./expr-containers.js";
import { InternalCompilerError } from "../../errors.js";
import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
-import { f64Lit, llvmCommentText, sizeToDouble } from "./common.js";
+import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js";
import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js";
import {
matchIntegerArrayForLoop,
@@ -440,9 +440,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
done = B.newLabel("arr.len.done");
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
emitter.markMemoryPointer(lenPtr, "array:header");
- B.line(
- `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}${emitter.arrayLengthRange}`,
- );
+ loadArrCount(emitter, len, lenPtr, emitter.fieldAliasAttachment(lenPtr));
B.line(
`${lenNumber} = ${sizeToDouble(emitter.sizeType)} ${emitter.sizeType} ${len} to double`,
);
@@ -1092,12 +1090,26 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.line(
`${lenPtr} = getelementptr inbounds %${integerArrayLoop ? "ScrArr" : "ScrBytes"}, ptr ${receiver.name}, i64 0, i32 1`,
);
- emitter.markMemoryPointer(lenPtr, integerArrayLoop ? "array:header" : "bytes:header");
- B.line(
- `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`,
- );
B.line(`${index} = load ${emitter.sizeType}, ptr ${integerSlot}`);
- B.line(`${inBounds} = icmp ult ${emitter.sizeType} ${index}, ${len}`);
+ if (integerArrayLoop) {
+ // The counter starts at most 2^32 - 1 and steps only while it is
+ // below the length, so it fits the i32 length field: compare in
+ // 32 bits, which lets the compare read the field directly.
+ emitter.markMemoryPointer(lenPtr, "array:header");
+ B.line(`${len} = load i32, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`);
+ let narrow = index;
+ if (emitter.sizeType === "i64") {
+ narrow = B.tmp();
+ B.line(`${narrow} = trunc i64 ${index} to i32`);
+ }
+ B.line(`${inBounds} = icmp ult i32 ${narrow}, ${len}`);
+ } else {
+ emitter.markMemoryPointer(lenPtr, "bytes:header");
+ B.line(
+ `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}`,
+ );
+ B.line(`${inBounds} = icmp ult ${emitter.sizeType} ${index}, ${len}`);
+ }
B.condBr(inBounds, lb, exit);
} else if (countedLoop && integerSlot && countedLimit) {
const index = B.tmp();
@@ -1203,9 +1215,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
// The inline form of scr_arr_len, like the `length` intrinsic.
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr!.name}, i32 0, i32 1`);
emitter.markMemoryPointer(lenPtr, "array:header");
- B.line(
- `${rawLen} = load ${emitter.sizeType}, ptr ${lenPtr}${emitter.fieldAliasAttachment(lenPtr)}${emitter.arrayLengthRange}`,
- );
+ loadArrCount(emitter, rawLen, lenPtr, emitter.fieldAliasAttachment(lenPtr));
B.line(
`${len} = ${sizeToDouble(emitter.sizeType)} ${emitter.sizeType} ${rawLen} to double`,
);
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 3c0ab2378..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,
@@ -621,13 +622,6 @@ export class LlEmitter {
return `, !tbaa !${tag}`;
}
- /** The `!range` attachment of an array length load: a length is at most
- * 2^32 - 1 (the runtime's SCR_ARR_MAX_LENGTH), so conversions to double
- * and back fold. 32-bit targets load the full range anyway. */
- get arrayLengthRange(): string {
- return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : "";
- }
-
/** Whether an element access at `index` of `arr` lies in the hole-free
* prefix: the counter of a dense array loop's fast copy indexing one of
* the arrays its entry test covered. */
@@ -1471,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,
@@ -2822,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(
@@ -3158,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;
}
@@ -5103,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)
@@ -5141,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"),
@@ -5189,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-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts
index 0ca76e2d5..afa69998d 100644
--- a/packages/compiler/src/backend/llvm/expr-containers.ts
+++ b/packages/compiler/src/backend/llvm/expr-containers.ts
@@ -17,7 +17,7 @@ import {
vAdapters,
} from "./shapes.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
-import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
+import { F64_INF, f64Lit, loadArrCount, sizeToDouble } from "./common.js";
import { MUTATING_COLLECTION_METHODS } from "../publication.js";
import { borrowsStringInputs, emitStringInputs } from "./string-lifetimes.js";
import {
@@ -564,9 +564,7 @@ export function emitArrIntrinsic(
t = B.tmp();
B.line(`${p} = getelementptr inbounds %ScrArr, ptr ${r.name}, i32 0, i32 1`);
host.markMemoryPointer(p, "array:header");
- B.line(
- `${len} = load ${host.sizeType}, ptr ${p}${host.fieldAliasAttachment(p)}${host.arrayLengthRange}`,
- );
+ loadArrCount(host, len, p, host.fieldAliasAttachment(p));
B.line(`${t} = ${sizeToDouble(host.sizeType)} ${host.sizeType} ${len} to double`);
return { name: t, type: e.type };
}
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 1790854ef..41ecd189e 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
@@ -306,3 +306,69 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
}
expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/);
});
+
+test.each([32, 64] as const)(
+ "tagged-template string arrays keep 8-byte slots before their states on %i-bit targets",
+ (bits) => {
+ const strings: IrExpr = {
+ kind: "templateStrings",
+ key: "site",
+ cooked: ["a", "b", "c"],
+ type: { kind: "array", elem: STRING },
+ loc,
+ };
+ const mod = fixture();
+ mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }];
+ const llvm = emitLlvmModule(mod, { pointerBits: bits });
+ const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0];
+ // The runtime reads each slot as 8 bytes and finds the states after
+ // cap * 8 bytes of slots (scr_arr_states_end).
+ if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>");
+ else {
+ expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>");
+ expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3);
+ }
+ 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-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 0d2cd90e8..69f4def2e 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.ts
@@ -32,7 +32,7 @@ import {
emitPowerOfTwoRemainder,
emitSignedIntegerRemainder,
} from "./integer-remainder.js";
-import { emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
+import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
export function emitLiteralExpr(
@@ -936,7 +936,14 @@ export function emitContainerExpr(
const idx = host.emitExpr(e.index);
if (e.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array");
- const t = emitDenseArrayState(host, arr.name, idx, e.index, "has");
+ const t = emitDenseArrayState(
+ host,
+ arr.name,
+ idx,
+ e.index,
+ "has",
+ slotWidth(elemAccess(e.arr.type.elem)),
+ );
return { name: t, type: e.type };
}
case "arrayState": {
@@ -944,7 +951,14 @@ export function emitContainerExpr(
const idx = host.emitExpr(e.index);
if (e.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
- const t = emitDenseArrayState(host, arr.name, idx, e.index, "state");
+ const t = emitDenseArrayState(
+ host,
+ arr.name,
+ idx,
+ e.index,
+ "state",
+ slotWidth(elemAccess(e.arr.type.elem)),
+ );
return { name: t, type: e.type };
}
case "arrIntrinsic":
diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
index bbd0226fb..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,
@@ -188,10 +188,10 @@ export function streamTypedRefCommitAdapter(
` call void @scr_arr_outline(ptr %next)`,
];
const storageMembers = [
- { name: "len", index: 1, type: host.sizeType },
- { name: "cap", index: 2, type: host.sizeType },
+ // The dense states follow the data slots, so they move with data and cap.
+ { name: "len", index: SCR_ARR_FIELD.len, type: "i32" },
+ { name: "cap", index: SCR_ARR_FIELD.cap, type: "i32" },
{ name: "data", index: SCR_ARR_FIELD.data, type: "ptr" },
- { name: "present", index: SCR_ARR_FIELD.present, type: "ptr" },
{ name: "filled", index: SCR_ARR_FIELD.filled, type: "i32" },
{ name: "ext", index: SCR_ARR_FIELD.ext, type: "ptr" },
];
@@ -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/integer-clones.ts b/packages/compiler/src/backend/llvm/integer-clones.ts
index f3efa1259..314eae5ea 100644
--- a/packages/compiler/src/backend/llvm/integer-clones.ts
+++ b/packages/compiler/src/backend/llvm/integer-clones.ts
@@ -1009,10 +1009,10 @@ class CloneEmitter {
B.line(
`${field} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
);
- B.line(`${len} = load i64, ptr ${field}`);
- B.line(`${fits} = icmp ule i64 ${len}, 2147483647`);
+ B.line(`${len} = load i32, ptr ${field}`);
+ B.line(`${fits} = icmp ule i32 ${len}, 2147483647`);
this.require(fits);
- B.line(`${value} = trunc i64 ${len} to i32`);
+ B.line(`${value} = add i32 ${len}, 0`);
return value;
}
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..b8335fe04 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):
@@ -472,6 +476,7 @@ export const LIB_FN_SYMS: Record = {
"fs.existsChk": "scr_fs_exists_async",
"fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk",
"net.connectAttempt": "scr_net_connect_attempt",
+ "net.connectLookupDyn": "scr_net_connect_lookup_dyn",
"fs.lchmodSyncChk": "scr_fs_lchmod_sync_chk",
"fsp.lchmodChk": "scr_fsp_lchmod_chk",
"fs.readFileSyncBuf": "scr_fs_read_file_bytes",
@@ -626,6 +631,9 @@ export const LIB_FN_SYMS: Record = {
"insp.regex": "scr_insp_regex",
"insp.date": "scr_insp_date",
"insp.error": "scr_insp_error",
+ "insp.errorHasCause": "scr_insp_error_has_cause",
+ "insp.errorBagHasProperties": "scr_insp_error_bag_has_properties",
+ "insp.errorCause": "scr_insp_error_cause",
"insp.begin": "scr_insp_begin",
"insp.layout": "scr_insp_layout",
"insp.entry": "scr_insp_entry",
@@ -643,6 +651,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).
@@ -1688,6 +1699,7 @@ export const USES_TIMERS_LIB_FNS = new Set([
"net.connect",
"net.connectCb",
"net.connectLookup",
+ "net.connectLookupDyn",
"net.connectAttempt",
"fs.existsChk",
"fs.callbackValue",
diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts
index 6951896d2..b9fd0f54b 100644
--- a/packages/compiler/src/backend/llvm/local-array-reads.ts
+++ b/packages/compiler/src/backend/llvm/local-array-reads.ts
@@ -12,7 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js";
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { ReferenceEffects } from "./reference-effects.js";
-import { SCR_ARR_FIELD, exactIndexLines } from "./common.js";
+import { SCR_ARR_FIELD, exactIndexLines, loadArrCount, scrArrStateAt } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { READ_FALLBACK } from "./dense-array-access.js";
@@ -457,12 +457,14 @@ export function emitProjectedArrayRead(host: LlvmEmitterContext, read: LocalArra
/** Share the checked dense lookup between optional stack boxes and strict
* borrowed reads. Capacity, length, and presence guards precede every load;
- * sparse and noncanonical indices keep their runtime lookup semantics. */
+ * sparse and noncanonical indices keep their runtime lookup semantics.
+ * `induction` says `integerIndex` is a loop's induction variable. */
function emitDenseReferenceArrayRead(
host: LlvmEmitterContext,
array: LlValue,
index: LlValue,
integerIndex: string | null,
+ induction: boolean,
present: (value: string) => void,
no: string,
slow: string,
@@ -476,9 +478,11 @@ function emitDenseReferenceArrayRead(
// noncanonical numeric properties retain the runtime lookup semantics.
const capPtr = B.tmp(),
cap = B.tmp();
- B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 2`);
+ B.line(
+ `${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.cap}`,
+ );
host.markMemoryPointer(capPtr, "array:header");
- B.line(`${cap} = load ${host.sizeType}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
+ loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
let offset: string, inRange: string;
if (integerIndex) {
offset = integerIndex;
@@ -499,6 +503,15 @@ function emitDenseReferenceArrayRead(
B.startBlock(range);
B.br(dense);
B.startBlock(dense);
+ // The state and value paths below both index from `data`: one load here
+ // serves both.
+ const dataPtr = B.tmp(),
+ data = B.tmp();
+ B.line(
+ `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
+ );
+ host.markMemoryPointer(dataPtr, "array:header");
+ B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
// Below the hole-free prefix bound every state is a present value.
const filledPtr = B.tmp(),
filled = B.tmp(),
@@ -516,45 +529,38 @@ function emitDenseReferenceArrayRead(
}
B.line(`${belowFilled} = icmp ult ${host.sizeType} ${offset}, ${filledSized}`);
const valueLabel = B.newLabel("local.array.value");
- B.condBr(belowFilled, valueLabel, lenLabel);
- B.startBlock(lenLabel);
- const lenPtr = B.tmp(),
- len = B.tmp(),
- belowLen = B.tmp();
- B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 1`);
- host.markMemoryPointer(lenPtr, "array:header");
- B.line(
- `${len} = load ${host.sizeType}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
- );
- B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`);
- const stateLabel = B.newLabel("local.array.state");
- B.condBr(belowLen, stateLabel, no);
- B.startBlock(stateLabel);
- const statesPtr = B.tmp(),
- states = B.tmp(),
- statePtr = B.tmp(),
- denseState = B.tmp(),
- densePresent = B.tmp();
- B.line(
- `${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.present}`,
- );
- host.markMemoryPointer(statesPtr, "array:header");
- B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
- B.line(`${statePtr} = getelementptr inbounds i8, ptr ${states}, ${host.sizeType} ${offset}`);
- host.markMemoryPointer(statePtr, "array:present");
- B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
- B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`);
- B.condBr(densePresent, valueLabel, no);
+ if (induction) {
+ // The state of slot i is at data + cap * (width + 1) - 1 - i, and an
+ // inline state check makes LLVM keep -1 - i as a second induction
+ // variable in the loop; above the prefix the runtime reads the slot.
+ B.condBr(belowFilled, valueLabel, slow);
+ } else {
+ B.condBr(belowFilled, valueLabel, lenLabel);
+ B.startBlock(lenLabel);
+ const lenPtr = B.tmp(),
+ len = B.tmp(),
+ belowLen = B.tmp();
+ B.line(
+ `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
+ );
+ host.markMemoryPointer(lenPtr, "array:header");
+ loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr));
+ B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`);
+ const stateLabel = B.newLabel("local.array.state");
+ B.condBr(belowLen, stateLabel, no);
+ B.startBlock(stateLabel);
+ const statePtr = B.tmp(),
+ denseState = B.tmp(),
+ densePresent = B.tmp();
+ scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset);
+ host.markMemoryPointer(statePtr, "array:present");
+ B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
+ B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`);
+ B.condBr(densePresent, valueLabel, no);
+ }
B.startBlock(valueLabel);
- const dataPtr = B.tmp(),
- data = B.tmp(),
- valuePtr = B.tmp(),
+ const valuePtr = B.tmp(),
raw = B.tmp();
- B.line(
- `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
- );
- host.markMemoryPointer(dataPtr, "array:header");
- B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
// Boolean arrays store one byte per element; every other kind an 8-byte slot.
const slotType = loadType === "i8" ? "i8" : "i64";
B.line(
@@ -590,6 +596,7 @@ export function emitBorrowedArrayRead(
array,
index,
integerIndex,
+ loopIndex !== null,
(value) => B.line(`store ptr ${value}, ptr ${slot}`),
slow,
slow,
@@ -632,7 +639,18 @@ export function emitLocalArrayRead(
B.line(`store ptr ${read.borrow ? value : host.retainFrame(value, owned)}, ptr ${localSlot}`);
};
if (inline)
- emitDenseReferenceArrayRead(host, array, index, integerIndex, store, no, slow, join, "ptr");
+ emitDenseReferenceArrayRead(
+ host,
+ array,
+ index,
+ integerIndex,
+ integerIndex !== null,
+ store,
+ no,
+ slow,
+ join,
+ "ptr",
+ );
else B.br(slow);
B.startBlock(slow);
const present = B.newLabel("local.array.slow.value");
@@ -687,6 +705,7 @@ export function emitLocalArrayRead(
array,
index,
integerIndex,
+ integerIndex !== null,
storeValue,
no,
slow,
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..8844921c5 100644
--- a/packages/compiler/src/backend/llvm/shapes.ts
+++ b/packages/compiler/src/backend/llvm/shapes.ts
@@ -1054,6 +1054,7 @@ export function arrNewCall(
elem.kind === "promise" || // promise entries (Promise.all inputs): full REF story
elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace
elem.kind === "netServer" ||
+ elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace
elem.kind === "dyn" || // native collection seeds/drains: traced REF
elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside)
(elem.kind === "array" && traceAdapter(host, elem) !== null);
@@ -1249,6 +1250,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 +1263,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 +1332,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 +1348,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 +1524,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-abi.ts b/packages/compiler/src/backend/runtime-abi.ts
index 21edd235f..67be86571 100644
--- a/packages/compiler/src/backend/runtime-abi.ts
+++ b/packages/compiler/src/backend/runtime-abi.ts
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
-export const RUNTIME_ABI_VERSION = 22 as const;
-export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v22" as const;
+export const RUNTIME_ABI_VERSION = 23 as const;
+export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v23" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
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/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts
index 96503e2db..e80d48e28 100644
--- a/packages/compiler/src/backend/runtime-pack-targets.test.ts
+++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
- runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" },
+ runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts
index cd342f85b..7a24facd6 100644
--- a/packages/compiler/src/backend/runtime-pack.test.ts
+++ b/packages/compiler/src/backend/runtime-pack.test.ts
@@ -122,7 +122,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
- runtime_abi: { version: 22, marker: "scr_runtime_abi_v22" },
+ runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" },
compiler: {
command: "clang",
identity: "fixture clang",
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/builtin-package-aliases.test.ts b/packages/compiler/src/frontend/builtin-package-aliases.test.ts
new file mode 100644
index 000000000..2d1001bc2
--- /dev/null
+++ b/packages/compiler/src/frontend/builtin-package-aliases.test.ts
@@ -0,0 +1,70 @@
+import { expect, test } from "vitest";
+import { builtinAliasOf, versionedPackageOfKey } from "./builtin-package-aliases.js";
+
+test("aliases name the builtin each checked major's module is on Node 24", () => {
+ expect(builtinAliasOf("isarray", "1.0.0", "")).toEqual({ whole: "Array.isArray" });
+ expect(builtinAliasOf("isarray", "0.0.1", "index.js")).toEqual({ whole: "Array.isArray" });
+ expect(builtinAliasOf("isarray", "3.0.0", "")).toBeNull();
+ expect(builtinAliasOf("safe-buffer", "5.1.2", "")).toEqual({ whole: 'require("buffer")' });
+ expect(builtinAliasOf("process-nextick-args", "2.0.1", "")).toEqual({ whole: "process" });
+ expect(builtinAliasOf("process-nextick-args", "1.0.7", "")).toBeNull();
+ expect(builtinAliasOf("inherits", "2.0.4", "inherits.js")).toEqual({
+ whole: 'require("util").inherits',
+ });
+ // The browser file is the package's own implementation.
+ expect(builtinAliasOf("inherits", "2.0.4", "inherits_browser.js")).toBeNull();
+ expect(builtinAliasOf("util-deprecate", "1.0.2", "node.js")).toEqual({
+ whole: 'require("util").deprecate',
+ });
+ expect(builtinAliasOf("util-deprecate", "1.0.2", "browser.js")).toBeNull();
+});
+
+test("readable-stream aliases follow each major's layout", () => {
+ const rs2 = builtinAliasOf("readable-stream", "2.3.8", "readable.js");
+ expect(rs2?.whole).toBe('require("stream").Readable');
+ expect([...(rs2?.members?.names ?? [])].sort()).toEqual(
+ ["Duplex", "PassThrough", "Readable", "Stream", "Transform", "Writable"].sort(),
+ );
+ expect(builtinAliasOf("readable-stream", "2.3.8", "writable.js")?.whole).toBe(
+ 'require("stream").Writable',
+ );
+ expect(builtinAliasOf("readable-stream", "2.3.8", "lib/_stream_duplex.js")?.whole).toBe(
+ 'require("stream").Duplex',
+ );
+ expect(builtinAliasOf("readable-stream", "2.3.8", "lib/internal/streams/destroy.js")).toBeNull();
+ // 3.x has no root writable.js; its entry also carries finished and pipeline.
+ const rs3 = builtinAliasOf("readable-stream", "3.6.2", "");
+ expect(rs3?.whole).toBe('require("stream").Readable');
+ expect([...(rs3?.members?.names ?? [])]).toContain("pipeline");
+ expect(builtinAliasOf("readable-stream", "3.6.2", "lib/_stream_writable.js")?.whole).toBe(
+ 'require("stream").Writable',
+ );
+ expect(builtinAliasOf("readable-stream", "3.6.2", "writable.js")).toBeNull();
+ expect(builtinAliasOf("readable-stream", "3.6.2", "lib/internal/streams/pipeline.js")).toBeNull();
+ const rs4 = builtinAliasOf("readable-stream", "4.7.0", "lib/ours/index.js");
+ expect(rs4?.whole).toBeNull();
+ expect(rs4?.members?.names.has("pipeline")).toBe(true);
+ expect(rs4?.members?.names.has("promises")).toBe(false);
+ expect(builtinAliasOf("readable-stream", "4.7.0", "transform.js")).toBeNull();
+ expect(builtinAliasOf("readable-stream", "4.7.0", "lib/_stream_transform.js")?.whole).toBe(
+ 'require("stream").Transform',
+ );
+});
+
+test("pnpm module keys name the package, version and file", () => {
+ expect(
+ versionedPackageOfKey("../node_modules/.pnpm/isarray@1.0.0/node_modules/isarray/index.js"),
+ ).toEqual({ name: "isarray", version: "1.0.0", subpath: "index.js" });
+ expect(
+ versionedPackageOfKey(
+ "node_modules/.pnpm/@scope+pkg@2.1.0_peer@1.0.0/node_modules/@scope/pkg/lib/a.js",
+ ),
+ ).toEqual({ name: "@scope/pkg", version: "2.1.0", subpath: "lib/a.js" });
+ // A store entry's own dependency links name other packages.
+ expect(
+ versionedPackageOfKey(
+ "node_modules/.pnpm/from2@2.3.0/node_modules/readable-stream/readable.js",
+ ),
+ ).toBeNull();
+ expect(versionedPackageOfKey("node_modules/isarray/index.js")).toBeNull();
+});
diff --git a/packages/compiler/src/frontend/builtin-package-aliases.ts b/packages/compiler/src/frontend/builtin-package-aliases.ts
new file mode 100644
index 000000000..ccb8dc5a3
--- /dev/null
+++ b/packages/compiler/src/frontend/builtin-package-aliases.ts
@@ -0,0 +1,150 @@
+/* Polyfill packages whose behavior on Node 24 IS a Node builtin. The
+ * package-shim registry (package-shims.ts) consults this table: a require
+ * of such a package, from an installed copy's importer or through a bundled
+ * __commonJS chunk whose path names the package, compiles as the builtin
+ * expression its module evaluates to on Node 24, at the require's position
+ * (loading a builtin runs no code, and none of these packages has an
+ * observable side effect at load).
+ *
+ * Each entry names the major versions whose entry modules were checked:
+ *
+ * - isarray 0.x, 1.x, 2.x: `module.exports = Array.isArray || function …`;
+ * Node always has Array.isArray, so the export is Array.isArray itself.
+ * - safe-buffer 5.x: `module.exports = buffer` when Buffer.from, alloc,
+ * allocUnsafe and allocUnsafeSlow exist (Node 6 and later); the rest of
+ * the module only builds its unexported SafeBuffer fallback.
+ * - process-nextick-args 2.x: `module.exports = process` unless
+ * process.version is v0.x or a pre-1.8 v1.x.
+ * - inherits 2.x: inherits.js exports require("util").inherits (2.0.1
+ * directly, 2.0.3 and later whenever it is a function, always on Node).
+ * - util-deprecate 1.x: node.js (its Node entry) is
+ * `module.exports = require("util").deprecate`.
+ * - readable-stream 2.x, 3.x, 4.x: a documented divergence. The package
+ * ships a JavaScript copy of Node's streams; its READABLE_STREAM=disable
+ * behavior hands out Node's own stream classes instead, and that is what
+ * compiles:
+ * - 2.x readable.js exports stream.Readable carrying Readable, Writable,
+ * Duplex, Transform, PassThrough and Stream; writable.js exports
+ * stream.Writable and duplex/transform/passthrough.js read the entry;
+ * - 3.x (a copy of Node 10's streams) readable.js exports the same
+ * members plus finished and pipeline. Its own disable branch throws on
+ * Node 24 (it Object.assigns every stream export onto stream.Readable,
+ * which inherits the getter-only `promises` accessor), so 3.x maps the
+ * members its default branch exports onto the builtin's;
+ * - 4.x lib/ours/index.js exports a plain object of named stream exports.
+ * The lib/_stream_*.js implementation files map to the matching builtin
+ * classes as well (Node itself loads the package's JavaScript
+ * implementations through them, whatever the environment). Without the
+ * variable Node runs the package's own JavaScript streams. */
+
+export interface BuiltinAlias {
+ /** The expression the module's export evaluates to, or null when only
+ * its member reads have a builtin spelling. */
+ whole: string | null;
+ /** For an export that carries another object's members (readable-stream's
+ * entry): member reads of these names read `object`'s members. */
+ members?: { object: string; names: ReadonlySet };
+}
+
+const STREAM_CLASSES = ["Readable", "Writable", "Duplex", "Transform", "PassThrough"];
+
+/** The members readable-stream 4.x's entry copies from `stream` under
+ * READABLE_STREAM=disable. */
+const RS4_ENTRY_EXPORTS = [
+ "_uint8ArrayToBuffer",
+ "_isUint8Array",
+ "isDisturbed",
+ "isErrored",
+ "isReadable",
+ ...STREAM_CLASSES,
+ "addAbortSignal",
+ "finished",
+ "destroy",
+ "pipeline",
+ "compose",
+ "Stream",
+];
+
+function majorOf(version: string): number {
+ return Number(/^(\d+)\./.exec(version)?.[1] ?? Number.NaN);
+}
+
+function stripJs(subpath: string): string {
+ return subpath.replace(/^\.\//, "").replace(/\.js$/, "");
+}
+
+/** The builtin a package module is on Node 24, or null. `subpath` is the
+ * file inside the package ("" for its main entry). */
+export function builtinAliasOf(pkg: string, version: string, subpath: string): BuiltinAlias | null {
+ const major = majorOf(version);
+ const file = stripJs(subpath);
+ switch (pkg) {
+ case "isarray":
+ return (major === 0 || major === 1 || major === 2) && (file === "" || file === "index")
+ ? { whole: "Array.isArray" }
+ : null;
+ case "safe-buffer":
+ return major === 5 && (file === "" || file === "index")
+ ? { whole: 'require("buffer")' }
+ : null;
+ case "process-nextick-args":
+ return major === 2 && (file === "" || file === "index") ? { whole: "process" } : null;
+ case "inherits":
+ return major === 2 && (file === "" || file === "inherits")
+ ? { whole: 'require("util").inherits' }
+ : null;
+ case "util-deprecate":
+ return major === 1 && (file === "" || file === "node")
+ ? { whole: 'require("util").deprecate' }
+ : null;
+ case "readable-stream": {
+ if (major !== 2 && major !== 3 && major !== 4) return null;
+ const stream = 'require("stream")';
+ const entry = major === 4 ? file === "lib/ours/index" : file === "readable";
+ if (file === "" || entry) {
+ if (major === 4)
+ return { whole: null, members: { object: stream, names: new Set(RS4_ENTRY_EXPORTS) } };
+ const names = new Set([
+ ...STREAM_CLASSES,
+ "Stream",
+ ...(major === 3 ? ["finished", "pipeline"] : []),
+ ]);
+ return { whole: `${stream}.Readable`, members: { object: stream, names } };
+ }
+ for (const cls of STREAM_CLASSES) {
+ const lower = cls.toLowerCase();
+ if (file === `lib/_stream_${lower}` || (major === 2 && file === lower))
+ return { whole: `${stream}.${cls}` };
+ }
+ return null;
+ }
+ default:
+ return null;
+ }
+}
+
+/** Package names the table can alias (a cheap pre-filter). */
+export const BUILTIN_ALIAS_PACKAGES: ReadonlySet = new Set([
+ "isarray",
+ "safe-buffer",
+ "process-nextick-args",
+ "inherits",
+ "util-deprecate",
+ "readable-stream",
+]);
+
+/** The package, version and in-package file a bundler's module key names
+ * when it carries a version (pnpm's `node_modules/.pnpm/@/
+ * node_modules//`), or null. */
+export function versionedPackageOfKey(
+ key: string,
+): { name: string; version: string; subpath: string } | null {
+ const m =
+ /(?:^|\/)node_modules\/\.pnpm\/((?:@[^/+]+\+)?[^/@]+)@([^/_]+)[^/]*\/node_modules\/((?:@[^/]+\/)?[^/]+)\/(.+)$/.exec(
+ key,
+ );
+ if (m === null) return null;
+ const [, storeName, version, name, subpath] = m;
+ if (storeName!.replace("+", "/") !== name) return null;
+ return { name: name!, version: version!, subpath: subpath! };
+}
diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts
index 064cb9b85..3b2f32413 100644
--- a/packages/compiler/src/frontend/dts-paths.ts
+++ b/packages/compiler/src/frontend/dts-paths.ts
@@ -1,5 +1,5 @@
import { createRequire } from "node:module";
-import { join } from "node:path";
+import { dirname, join } from "node:path";
import { npmPackageNameOf } from "./workspace-registry.js";
import { tsgoPath } from "./ts7/session-path.js";
@@ -25,6 +25,15 @@ function declarationPath(name: string): string {
);
}
+/** Directory of the shipped package shims (packages/compiler/shims): the
+ * implementation modules package-shims.ts maps npm packages to. A native
+ * distribution ships them beside its declarations. */
+export function packageShimRoot(): string {
+ return installedDeclarationRoot === null
+ ? join(dirname(require.resolve("@scriptc/compiler/scriptc.d.ts")), "..", "shims")
+ : join(installedDeclarationRoot, "shims");
+}
+
/** Path of the shipped ambient declarations — the always-shipped CORE
* (comptime, setTimeout). Part of EVERY program scriptc
* builds, the project-world preflight program included. */
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..25653f541 100644
--- a/packages/compiler/src/frontend/lowering/builtins/process.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/process.ts
@@ -266,6 +266,53 @@ 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"
+ );
+}
+
+/** Whether any implementation source spells `versions`. Programs that
+ * never do skip the per-read checker queries above entirely. */
+function programMentionsVersions(lowerer: Lowerer): boolean {
+ lowerer.mentionsProcessVersions ??= lowerer.program
+ .getImplementationSourceFiles()
+ .some((sf) => !sf.isDeclarationFile && sf.text.includes("versions"));
+ return lowerer.mentionsProcessVersions;
+}
+
export function lowerProcessStreamProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts
index 7230d18cd..2d531663f 100644
--- a/packages/compiler/src/frontend/lowering/builtins/timers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts
@@ -156,7 +156,11 @@ function isTimerHandleTyped(
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym === undefined || ts.symbolName(sym) !== name) return false;
return sym.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
+ (d) =>
+ // An interface in the fallback and @types/node 22+, a class in
+ // @types/node 20 and older.
+ (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
);
}
function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean {
diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts
index 47cdd834f..6e4d3aba1 100644
--- a/packages/compiler/src/frontend/lowering/call-signatures.ts
+++ b/packages/compiler/src/frontend/lowering/call-signatures.ts
@@ -956,6 +956,28 @@ export function lambdaSignature(
return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) };
}
}
+ // A callback whose arguments arrive as the runtime produced them, wider
+ // than its contextual overload says (dns.lookup's callback when the
+ // options are only known at run time: the address is a string or an
+ // address list): every parameter binds as a checked-dynamic value.
+ // Rest parameters keep their packing and optional or defaulted ones stay
+ // omittable (the prologue applies a default to an undefined argument).
+ // The slot discards the result, so a sync callback returns void and an
+ // async one keeps its promise.
+ if (lowerer.dynParamLambdas.has(node)) {
+ const shapes: ParamShape[] = node.parameters.nodes
+ .filter((param) => !isThisParameter(param))
+ .map((param) =>
+ param.dotDotDotToken
+ ? { type: DYN, mode: "dynRest" }
+ : param.initializer
+ ? { type: DYN, mode: "omittable", bodyType: DYN }
+ : { type: DYN, mode: param.questionToken ? "omittable" : "required" },
+ );
+ const isAsync = node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
+ const ret = isAsync ? lowerer.declaredReturnType(node, node) : VOID;
+ return { shapes, funcType: funcTypeFromParamShapes(shapes, ret) };
+ }
if (node.typeParameters?.nodes && !lowerer.erasingLambdas.has(node)) {
// Generic function-like forms monomorphize only where a static home
// exists: top-level generic function declarations, generic methods
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/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
index 30030b18f..ae8cce4ca 100644
--- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
+++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
@@ -25,6 +25,7 @@ import {
cjsExportDiscardReason,
cjsExportTargetLiteral,
isCjsJsFile,
+ isJsSourceFile,
isModuleExportsAccess,
isRequireStatement,
locOf,
@@ -367,6 +368,50 @@ function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined):
);
}
+/** The ES-module half (a TypeScript module required from JavaScript, such
+ * as a package shim): require() answers its namespace, so a call through
+ * it binds `this` to the namespace. True when the export is a function
+ * that never reads `this` (every declaration of an overload set), a
+ * builtin module's member re-exported from its declarations, or reached
+ * through an `export *` of a module where that holds. */
+function esModuleExportIgnoresThis(
+ lowerer: Lowerer,
+ sf: ts.SourceFile,
+ name: string,
+ depth: number,
+): boolean {
+ const moduleSym = lowerer.checker.getSymbolAtLocation(sf);
+ const exported = moduleSym?.getExports().get(name as ts.__String);
+ if (exported !== undefined) {
+ const target =
+ exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
+ const decls = target.declarations;
+ if (decls.length === 0) return false;
+ return decls.every(
+ (d) =>
+ (ts.isFunctionDeclaration(d) && !functionUsesThis(d)) ||
+ (ts.getSourceFileOfNode(d).isDeclarationFile && ts.isFunctionDeclaration(d)),
+ );
+ }
+ for (const stmt of sf.statements.nodes) {
+ if (
+ !ts.isExportDeclaration(stmt) ||
+ stmt.exportClause !== undefined ||
+ stmt.moduleSpecifier === undefined ||
+ !ts.isStringLiteral(stmt.moduleSpecifier)
+ )
+ continue;
+ const dep = resolveImport(lowerer.program, sf, stmt.moduleSpecifier.text);
+ if (
+ dep !== null &&
+ !dep.isDeclarationFile &&
+ esModuleExportIgnoresThis(lowerer, dep, name, depth + 1)
+ )
+ return true;
+ }
+ return false;
+}
+
/** True when every value the module assigns to its export `name` is a
* function that never reads `this`. A call through the exports object
* (`exports.f(x)`) binds `this` to that object, which the static export
@@ -378,6 +423,7 @@ export function cjsExportCallIgnoresThis(
depth = 0,
): boolean {
if (depth > 16) return false;
+ if (!isJsSourceFile(sf)) return esModuleExportIgnoresThis(lowerer, sf, name, depth);
const values: ts.Expression[] = [];
ts.walkPreorder(sf, (node) => {
if (
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/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts
index 516a62f3c..f3a6b23fa 100644
--- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts
+++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts
@@ -270,8 +270,8 @@ function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string):
}
/** ToString(ToPrimitive(value, hint)) of a union whose arms are program
- * class instances and primitives (`Box | undefined`): each arm converts as
- * it would alone. Null for other unions. */
+ * class instances, URLs and primitives (`Box | undefined`, `string | URL`):
+ * each arm converts as it would alone. Null for other unions. */
export function classUnionToString(
lowerer: Lowerer,
value: IrExpr,
@@ -281,9 +281,11 @@ export function classUnionToString(
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
- if (!arms || !arms.some((arm) => arm.kind === "object")) return null;
+ if (!arms || !arms.some((arm) => arm.kind === "object" || arm.kind === "url")) return null;
const convertible = (arm: IrType): boolean => {
if (PRIMITIVE_KINDS.has(arm.kind)) return true;
+ // A URL arm converts through its href, like String(url).
+ if (arm.kind === "url") return true;
if (arm.kind !== "object") return false;
const info = lowerer.classes.get(arm.className);
return info !== undefined && plainProgramClass(info);
diff --git a/packages/compiler/src/frontend/lowering/coercions/fresh-spread.ts b/packages/compiler/src/frontend/lowering/coercions/fresh-spread.ts
new file mode 100644
index 000000000..656ac3ee1
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/coercions/fresh-spread.ts
@@ -0,0 +1,97 @@
+/* Array spreads whose elements nothing else can reach. */
+import * as ts from "../../ts7/adapter.js";
+import type { IrExpr, IrType, SrcLoc } from "../../../ir/ir.js";
+import type { Lowerer } from "../lowerer.js";
+
+/** Whether the elements a spread inserts are reachable only through it: an
+ * array literal (`...(cond ? [{ ... }] : [])`), or an unexported const
+ * initialized by one that this spread is the only reference to, and that
+ * the spread reads at most once per evaluation of the initializer. Converting such elements
+ * into the destination's element layout copies them, which nothing can
+ * observe. */
+export function freshSpreadSource(lowerer: Lowerer, node: ts.Expression): boolean {
+ let e = node;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ if (ts.isArrayLiteralExpression(e))
+ return e.elements.nodes.every(
+ (x) => !ts.isSpreadElement(x) && (ts.isObjectLiteralExpression(x) || !isReference(x)),
+ );
+ if (ts.isConditionalExpression(e))
+ return freshSpreadSource(lowerer, e.whenTrue) && freshSpreadSource(lowerer, e.whenFalse);
+ if (!ts.isIdentifier(e)) return false;
+ const symbol = lowerer.resolveValueSymbol(e);
+ const decl = symbol?.valueDeclaration;
+ if (!symbol || !decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return false;
+ const list = decl.parent;
+ if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return false;
+ // Other modules can import an exported binding and reach its elements.
+ const statement = list.parent;
+ if (
+ ts.isVariableStatement(statement) &&
+ statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
+ )
+ return false;
+ if (!freshSpreadSource(lowerer, decl.initializer)) return false;
+ // The binding's only use is this spread; an export specifier naming it
+ // (`export { name }`) is another.
+ let scope: ts.Node = list;
+ while (scope.parent && !ts.isBlock(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
+ let uses = 0;
+ const visit = (n: ts.Node): void => {
+ if (uses > 1) return;
+ if (ts.isExportSpecifier(n) && (n.propertyName ?? n.name).text === e.text) {
+ uses = 2;
+ return;
+ }
+ if (ts.isIdentifier(n) && n !== decl.name && n.text === e.text) {
+ if (lowerer.resolveValueSymbol(n) === symbol) uses++;
+ }
+ ts.forEachChild(n, visit);
+ };
+ visit(scope);
+ return uses === 1 && runsOnceInScope(node, scope);
+}
+
+/** Whether `node` runs at most once each time `scope` runs: no loop body,
+ * loop test or update, function, or class field initializer lies between
+ * them. A spread that runs again inserts the same elements again, and
+ * copies would no longer be the same objects. */
+function runsOnceInScope(node: ts.Node, scope: ts.Node): boolean {
+ let child = node;
+ for (let n = node.parent; n && n !== scope; child = n, n = n.parent) {
+ if (ts.isFunctionLike(n) || ts.isPropertyDeclaration(n)) return false;
+ if (ts.isForOfStatement(n) || ts.isForInStatement(n)) {
+ if (child !== n.expression) return false;
+ } else if (ts.isForStatement(n)) {
+ if (child !== n.initializer) return false;
+ } else if (ts.isWhileStatement(n) || ts.isDoStatement(n)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function isReference(e: ts.Expression): boolean {
+ return !(
+ ts.isStringLiteral(e) ||
+ ts.isNumericLiteral(e) ||
+ e.kind === ts.SyntaxKind.TrueKeyword ||
+ e.kind === ts.SyntaxKind.FalseKeyword ||
+ e.kind === ts.SyntaxKind.NullKeyword
+ );
+}
+
+/** A fresh array spread converted into the destination's array layout by
+ * the structural width lift, or null when the layouts do not lift. */
+export function liftedFreshSpread(
+ lowerer: Lowerer,
+ node: ts.Expression,
+ source: IrExpr,
+ arrType: IrType,
+ loc: SrcLoc,
+): IrExpr | null {
+ if (source.type.kind !== "array" || arrType.kind !== "array") return null;
+ if (!freshSpreadSource(lowerer, node)) return null;
+ const lift = lowerer.widthLiftPlan(source.type, arrType);
+ return lift ? lowerer.applyWidthLift(lift, source, arrType, loc) : null;
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 53d46d00d..83a184400 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
@@ -1,5 +1,6 @@
import { generatorDrain } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
+import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { InternalCompilerError } from "../../../errors.js";
import {
dynUndefinedExpr,
@@ -142,6 +143,16 @@ export function lowerArraySpreadItems(
if (source.type.kind === "record" && lowerer.shapes.get(source.type.shapeId)?.tuple) {
source = lowerer.widthCoerce(source, arrType) ?? source;
}
+ if (
+ !typeEquals(source.type, arrType) &&
+ !(
+ source.type.kind === "array" &&
+ identityPreservingWidening(lowerer, source.type.elem, elem)
+ )
+ ) {
+ // Fresh elements (an array literal) convert into the element layout.
+ source = liftedFreshSpread(lowerer, node.expression, source, arrType, loc) ?? source;
+ }
if (!typeEquals(source.type, arrType)) {
if (
source.type.kind !== "array" ||
diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
index 12078720e..e9a130a2d 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
@@ -1,3 +1,4 @@
+import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { lowerArrayIteratorValue } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { arrayConcatHelper } from "./array-concat.js";
@@ -793,6 +794,10 @@ export function lowerArrayMethodCall(
loc,
};
}
+ if (!typeEquals(src.type, receiverIr)) {
+ // Fresh elements (an array literal) convert into the element layout.
+ src = liftedFreshSpread(lowerer, spreadArg.expression, src, receiverIr, loc) ?? src;
+ }
if (!typeEquals(src.type, receiverIr)) {
lowerer.unsupported(
"SC1090",
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/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
index 6aa6d6faf..370974c85 100644
--- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
@@ -526,6 +526,55 @@ export function lowerMapForEachCall(
return { kind: "call", callee: helper, args: [receiver, fnArg], type: VOID, loc };
}
+/** True when an arrow's expression body is lowered as a statement: the
+ * function being lowered returns void (`xs.forEach((x) => s.add(x))`) and
+ * the arrow's contextual signature returns void. An arrow that returns the
+ * Set (`(n) => s.add(n)`, a `map` callback) or a body inlined into a
+ * caller that reads its value is not. */
+function setAddArrowResultVoid(lowerer: Lowerer, arrow: ts.ArrowFunction): boolean {
+ if (lowerer.ctx.returnType.kind !== "void") return false;
+ const contextual = lowerer.checker.getContextualType(arrow);
+ if (contextual === undefined) return false;
+ const signatures = lowerer.checker.getCallSignatures(
+ lowerer.checker.getNonNullableType(contextual),
+ );
+ return (
+ signatures.length === 1 &&
+ (lowerer.checker.getReturnTypeOfSignature(signatures[0]!).flags & ts.TypeFlags.Void) !== 0
+ );
+}
+
+/** True when nothing reads an add() call's result: an expression
+ * statement, the expression body of an arrow whose result is void, a `void`
+ * operand, a for header's initializer or incrementor, a comma's left
+ * operand, or a conditional or comma result that is itself discarded. */
+function setAddResultDiscarded(lowerer: Lowerer, call: ts.CallExpression): boolean {
+ let node: ts.Node = call;
+ for (;;) {
+ let parent = node.parent;
+ while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
+ node = parent;
+ parent = parent.parent;
+ }
+ if (parent === undefined) return false;
+ if (ts.isExpressionStatement(parent) || ts.isVoidExpression(parent)) return true;
+ if (ts.isArrowFunction(parent))
+ return parent.body === node && setAddArrowResultVoid(lowerer, parent);
+ if (ts.isForStatement(parent))
+ return parent.initializer === node || parent.incrementor === node;
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken) {
+ if (parent.left === node) return true;
+ node = parent;
+ continue;
+ }
+ if (ts.isConditionalExpression(parent) && parent.condition !== node) {
+ node = parent;
+ continue;
+ }
+ return false;
+ }
+}
+
/** Ambient Set method calls — Map's lowering with the value slot gone.
* `add`/`has`/`delete`/`clear` lower to setIntrinsic; `forEach` desugars
* like Map's over the shared iteration primitives. Null when this isn't
@@ -560,8 +609,8 @@ export function lowerSetMethodCall(
const value = lowerer.lowerExpr(access.expression);
const receiver =
lowerer.runtimeOptionalPropertyReceiver(access.expression, value, receiverIr, name) ?? value;
- // The lib's `add` returns the Set (chaining typechecks); the lowered
- // add is a void statement — fence chained receivers like Map's set.
+ // A used add() result lowers to its receiver (below), so a chained
+ // receiver is a Set; anything else lowering to a non-Set stays fenced.
if (receiver.type.kind !== "set") {
lowerer.noLowering(
"chained Set method calls",
@@ -584,7 +633,28 @@ export function lowerSetMethodCall(
}
if (name === "add") {
const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem);
- return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc };
+ if (setAddResultDiscarded(lowerer, call)) {
+ return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc };
+ }
+ // add() returns its Set: a used result (`s.add(a).add(b)`,
+ // `s.add(x) === s`) reads the receiver once into a slot, adds, and
+ // answers the slot, as Map's set() does.
+ const slot = lowerer.declareHiddenLocal("%setAddReceiver", receiver.type);
+ const ref = varRef(slot.id, receiver.type, loc);
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: slot.id, init: receiver, loc },
+ {
+ kind: "exprStmt",
+ expr: { kind: "setIntrinsic", method: "add", receiver: ref, args: [v], type: VOID, loc },
+ loc,
+ },
+ ],
+ result: ref,
+ type: receiver.type,
+ loc,
+ };
}
if (name === "has" || name === "delete") {
const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem);
diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts
index fb62fb998..092588a6e 100644
--- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts
+++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts
@@ -37,6 +37,8 @@ export interface CycleInitFlag {
/** The declaring module. */
module: ts.SourceFile;
settled: ReadonlySet;
+ /** Local names other files import the binding under, by file name. */
+ localNames?: ReadonlyMap;
/** True for a binding of an import cycle. A binding flagged only for its
* own module's early calls is checked opportunistically: whatever cannot
* be checked keeps the unchecked behavior instead of refusing. */
@@ -45,23 +47,40 @@ export interface CycleInitFlag {
* declaration initializes it (to undefined, which the pointer cannot
* hold), so the flag alone decides its writes (markUninitializedWrites). */
uninitialized?: true;
- /** A class binding: its references are the class's own IR forms and its
- * static field storage (`global` is the flag itself). */
+ /** A class binding: its references are the class's own IR forms and the
+ * static members it reaches, its own and its bases' (`global` is the flag
+ * itself). */
className?: string;
+ /** The class and its base classes, whose `%C.static:` functions a static
+ * member access through the class can name. */
+ staticOwners?: readonly string[];
staticGlobals?: ReadonlySet;
- /** The source references to the class outside its own body. Only IR
- * built for one of them is checked: compiler-generated dispatch (error
- * property helpers, prototype data, reflection) also names the class
- * and must keep working before the class statement ran. */
- references?: readonly SrcLoc[];
+ /** The source references to the class outside its own body, with the
+ * local name each one spells (an import can rename the class; Node's
+ * ReferenceError names that binding). Only IR built for one of them is
+ * checked: compiler-generated dispatch (error property helpers,
+ * prototype data, reflection) also names the class and must keep
+ * working before the class statement ran. */
+ references?: readonly ClassReference[];
}
+/** A source reference to a flagged class and the local name it spells. */
+interface ClassReference {
+ loc: SrcLoc;
+ name: string;
+}
+
+/** The static field storage of a flag without a class. */
+const NO_STATIC_GLOBALS: ReadonlySet = new Set();
+
/** Registers the initialization flags after global collection. Bindings
* whose early access has no exact representation are refused: a `var`
* (Node reads `undefined` there, but module storage only starts when the
- * declaring module's body starts), a class (its references are static), a
- * binding without module storage, or one with a use the lowering resolves
- * from the binding's type (a property key, a `typeof` operand). */
+ * declaring module's body starts), a binding without module storage, a
+ * class that another module's `extends` clause can reach early (the
+ * subclass is static), or one with a use the lowering resolves from the
+ * binding's type (a property key, a `typeof` operand). Other classes get a
+ * flag like their module's early-reached classes. */
export function markCycleEarlyBindings(lowerer: Lowerer): void {
const early = cycleEarlyBindings(
lowerer.program,
@@ -80,7 +99,14 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void {
continue;
}
if (binding.kind === "class") {
- refuse(binding.reference, `the class ${where}`);
+ // The class's references from code that can run before its
+ // statement check its initialization flag: Node's ReferenceError
+ // only when the class is actually reached early.
+ const info = lowerer.classBySymbol.get(binding.symbol);
+ const heritage = earlyHeritageReference(lowerer, binding);
+ if (info !== undefined && !info.generic && heritage === null)
+ registerClassInitFlag(lowerer, binding, info, true);
+ else refuse(heritage ?? binding.reference, `the class ${where}`);
continue;
}
const global = lowerer.globalsBySymbol.get(binding.symbol);
@@ -103,7 +129,7 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void {
for (const binding of moduleEarlyBindings(lowerer.program, lowerer.moduleOrder)) {
if (binding.kind === "class") {
const info = lowerer.classBySymbol.get(binding.symbol);
- if (info !== undefined) registerClassInitFlag(lowerer, binding, info);
+ if (info !== undefined) registerClassInitFlag(lowerer, binding, info, false);
continue;
}
const global = lowerer.globalsBySymbol.get(binding.symbol);
@@ -146,51 +172,149 @@ function registerClassInitFlag(
lowerer: Lowerer,
binding: CycleEarlyBinding,
info: ClassInfo,
+ cycle: boolean,
): void {
const className = info.def.name;
if (lowerer.classInitFlags.has(className)) return;
const flagId = `%g.class.${className}%initialized`;
const global: IrGlobal = { id: flagId, name: "%initialized", type: BOOL, mutable: true };
lowerer.globalsList.push(global);
+ const staticOwners: string[] = [];
+ const staticGlobals = new Set();
+ for (let c: ClassInfo | null = info; c; c = c.base) {
+ staticOwners.push(c.def.name);
+ for (const field of c.staticFields) staticGlobals.add(field.globalId);
+ }
lowerer.classInitFlags.set(className, {
global,
flagId,
name: binding.name.text,
module: binding.module,
settled: binding.settled,
- cycle: false,
+ cycle,
className,
- staticGlobals: new Set(info.staticFields.map((f) => f.globalId)),
- references: classReferences(lowerer, binding.symbol, binding.name),
+ staticOwners,
+ staticGlobals,
+ references: classReferences(lowerer, binding.symbol, binding.name, cycle),
});
}
-/** The source spans of the identifiers naming the class outside its own
- * declaration, in every program file. */
-function classReferences(lowerer: Lowerer, symbol: ts.Symbol, name: ts.Identifier): SrcLoc[] {
- const declaration = name.parent;
- const out: SrcLoc[] = [];
+/** A reference to a cycle class in another class's `extends` clause, in a
+ * module that can run before the class statement: Node evaluates the clause
+ * when that module's class statement runs, but the compiled subclass is
+ * static and has no access to check. Null when there is none. */
+function earlyHeritageReference(lowerer: Lowerer, binding: CycleEarlyBinding): ts.Node | null {
+ const declaration = binding.name.parent;
for (const sf of lowerer.program.getImplementationSourceFiles()) {
- if (sf.isDeclarationFile || !sf.text.includes(name.text)) continue;
+ if (sf.isDeclarationFile || sf === binding.module || binding.settled.has(sf)) continue;
+ const names = classLocalNames(lowerer, sf, binding.symbol, binding.name.text, true);
+ if (names.size === 0) continue;
+ let found: ts.Node | null = null;
+ ts.walkPreorder(sf, (node) => {
+ if (found !== null || node === declaration) return "skip";
+ if (!ts.isHeritageClause(node)) return undefined;
+ // Node evaluates the whole clause when the class statement runs:
+ // any reference inside it (parenthesized, a factory argument)
+ // reaches the class.
+ ts.walkPreorder(node, (inner) => {
+ if (found !== null) return "skip";
+ if (
+ ts.isIdentifier(inner) &&
+ names.has(inner.text) &&
+ resolvesTo(lowerer, inner, binding.symbol, true)
+ )
+ found = inner;
+ return undefined;
+ });
+ return "skip";
+ });
+ if (found !== null) return found;
+ }
+ return null;
+}
+
+/** Whether `node` names `symbol`, through import aliases when `aliases`. */
+function resolvesTo(
+ lowerer: Lowerer,
+ node: ts.Identifier,
+ symbol: ts.Symbol,
+ aliases: boolean,
+): boolean {
+ let referenced = lowerer.checker.getSymbolAtLocation(node);
+ if (aliases && referenced !== undefined && referenced.flags & ts.SymbolFlags.Alias)
+ referenced = lowerer.checker.getAliasedSymbol(referenced);
+ return referenced === symbol;
+}
+
+/** The local names under which `sf` can spell the class: its declared name
+ * (when the file's text contains it) and, with `aliases`, the local names
+ * of the file's imports that resolve to it (`import { Sink as Alias }`, a
+ * default import, a re-exported name). */
+function classLocalNames(
+ lowerer: Lowerer,
+ sf: ts.SourceFile,
+ symbol: ts.Symbol,
+ declared: string,
+ aliases: boolean,
+): Set {
+ const names = new Set();
+ if (sf.text.includes(declared)) names.add(declared);
+ if (!aliases) return names;
+ for (const statement of sf.statements.nodes) {
+ if (!ts.isImportDeclaration(statement)) continue;
+ const clause = statement.importClause;
+ if (clause === undefined) continue;
+ const locals: ts.Identifier[] = [];
+ if (clause.name !== undefined) locals.push(clause.name);
+ const bindings = clause.namedBindings;
+ if (bindings !== undefined && ts.isNamedImports(bindings))
+ for (const element of bindings.elements.nodes) locals.push(element.name);
+ for (const local of locals)
+ if (!names.has(local.text) && resolvesTo(lowerer, local, symbol, true)) names.add(local.text);
+ }
+ return names;
+}
+
+/** The source spans of the identifiers naming the class outside its own
+ * declaration, in every program file. For a class of an import cycle that
+ * includes other modules' import bindings; another module's references to a
+ * class flagged only for its own module's early calls always run after the
+ * class statement. */
+function classReferences(
+ lowerer: Lowerer,
+ symbol: ts.Symbol,
+ name: ts.Identifier,
+ cycle: boolean,
+): ClassReference[] {
+ const declaration = name.parent;
+ const out: ClassReference[] = [];
+ for (const sf of lowerer.program.getImplementationSourceFiles()) {
+ if (sf.isDeclarationFile) continue;
+ // Another module names the class through its import bindings, under
+ // the class's name or a local one.
+ const names = classLocalNames(lowerer, sf, symbol, name.text, cycle);
+ if (names.size === 0) continue;
ts.walkPreorder(sf, (node) => {
if (node === declaration) return "skip";
- if (
- ts.isIdentifier(node) &&
- node.text === name.text &&
- lowerer.checker.getSymbolAtLocation(node) === symbol
- )
- out.push(locOf(node));
+ if (ts.isIdentifier(node) && names.has(node.text) && resolvesTo(lowerer, node, symbol, cycle))
+ out.push({ loc: locOf(node), name: node.text });
return undefined;
});
}
return out;
}
+/** The source reference to the flagged class that `loc`'s IR was built
+ * for, or undefined. */
+function coveredReference(flag: CycleInitFlag, loc: SrcLoc): ClassReference | undefined {
+ return (flag.references ?? []).find(
+ (r) => r.loc.file === loc.file && loc.start <= r.loc.start && r.loc.end <= loc.end,
+ );
+}
+
/** Whether `loc` is the IR of a source reference to the flagged class. */
function coversReference(flag: CycleInitFlag, loc: SrcLoc): boolean {
- return (flag.references ?? []).some(
- (r) => r.file === loc.file && loc.start <= r.start && r.end <= loc.end,
- );
+ return coveredReference(flag, loc) !== undefined;
}
/** The class statement's static initialization, followed by the store of
@@ -218,6 +342,7 @@ function registerInitFlag(
name: binding.name.text,
module: binding.module,
settled: binding.settled,
+ ...(binding.localNames ? { localNames: binding.localNames } : {}),
cycle,
};
lowerer.cycleInitFlags.set(global.id, flag);
@@ -361,16 +486,23 @@ function rewriteFunction(
for (const [id, flag] of lowerer.cycleInitFlags) {
if (runsEarly(flag)) active.set(id, flag);
}
- // Classes, by name; their static field storage joins `active`. A class's
- // own members run only once the class exists, or inside its own
- // definition, where the inner binding is initialized.
+ // Classes, by name, and the static field storage each reaches (a base
+ // class's statics through a subclass too). A class's own members run only
+ // once the class exists, or inside its own definition, where the inner
+ // binding is initialized.
const classes = new Map();
+ const statics = new Map();
for (const [className, flag] of lowerer.classInitFlags) {
if (!runsEarly(flag) || fn.name.startsWith(`%${className}.`)) continue;
classes.set(className, flag);
- if (flag.staticGlobals) for (const id of flag.staticGlobals) active.set(id, flag);
+ for (const id of flag.staticGlobals ?? NO_STATIC_GLOBALS)
+ statics.set(id, [...(statics.get(id) ?? []), flag]);
}
if (active.size === 0 && classes.size === 0) return;
+ const flagOf = (localId: string, loc: SrcLoc): CycleInitFlag | undefined =>
+ active.get(localId) ?? statics.get(localId)?.find((flag) => coversReference(flag, loc));
+ const namesStatic = (flag: CycleInitFlag, fnName: string): boolean =>
+ (flag.staticOwners ?? []).some((owner) => fnName.startsWith(`%${owner}.static:`));
const classFlagOf = (e: IrExpr): CycleInitFlag | undefined => {
if (classes.size === 0) return undefined;
switch (e.kind) {
@@ -384,25 +516,34 @@ function rewriteFunction(
case "call":
for (const [className, flag] of classes) {
if (
- (e.callee.startsWith(`%${className}.static:`) ||
- e.callee === `%dyn.class.instanceof:${className}`) &&
+ (namesStatic(flag, e.callee) || e.callee === `%dyn.class.instanceof:${className}`) &&
coversReference(flag, e.loc)
)
return flag;
}
return undefined;
+ case "closure":
+ // A static method read as a value (`const f = K.m`) reads K.
+ for (const flag of classes.values())
+ if (namesStatic(flag, e.fnName) && coversReference(flag, e.loc)) return flag;
+ return undefined;
default:
return undefined;
}
};
let temps = 0;
const check = (flag: CycleInitFlag, loc: SrcLoc): IrStmt => {
+ // Node's ReferenceError names the binding the reference spells.
+ const name =
+ flag.className !== undefined
+ ? (coveredReference(flag, loc)?.name ?? flag.name)
+ : (flag.localNames?.get(loc.file) ?? flag.name);
const expr: IrExpr = {
kind: "intrinsic",
name: "module.tdzCheck",
args: [
{ kind: "varRef", localId: flag.flagId, type: BOOL, loc },
- { kind: "strLit", value: flag.name, type: STRING, loc },
+ { kind: "strLit", value: name, type: STRING, loc },
],
type: VOID,
loc,
@@ -457,9 +598,8 @@ function rewriteFunction(
};
}
if (e.kind !== "varRef" && e.kind !== "incDec" && e.kind !== "assignExpr") return e;
- const flag = active.get(e.localId);
+ const flag = flagOf(e.localId, e.loc);
if (flag === undefined) return e;
- if (flag.className !== undefined && !coversReference(flag, e.loc)) return e;
// `K.x = v` reads K before it evaluates v.
if (e.kind === "assignExpr" && flag.className === undefined) {
const t = temp(e.value.type);
@@ -477,9 +617,8 @@ function rewriteFunction(
},
stmt: (s: IrStmt): IrStmt => {
if (s.kind !== "assign" || s.initializes) return s;
- const flag = active.get(s.localId);
+ const flag = flagOf(s.localId, s.loc);
if (flag === undefined) return s;
- if (flag.className !== undefined && !coversReference(flag, s.loc)) return s;
// `K.x = v` reads K before it evaluates v.
if (flag.className !== undefined)
return { kind: "block", body: [check(flag, s.loc), s], loc: s.loc };
@@ -525,7 +664,11 @@ function rewriteFunction(
for (const [className, flag] of [...classes]) {
if (flag.module !== initOf || !setsFlag(s, flag)) continue;
classes.delete(className);
- if (flag.staticGlobals) for (const id of flag.staticGlobals) active.delete(id);
+ for (const id of flag.staticGlobals ?? NO_STATIC_GLOBALS) {
+ const rest = statics.get(id)!.filter((other) => other !== flag);
+ if (rest.length > 0) statics.set(id, rest);
+ else statics.delete(id);
+ }
}
const out = rewriteStmt(s);
for (const [id, flag] of [...active]) {
diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
index a80367dd0..56b6e280e 100644
--- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
@@ -462,7 +462,14 @@ export function lowerArrayLiteral(
// A same-family array whose ELEMENT lifts (string[] into a
// (string | symbol)[] literal — per-element wrap/width copy):
// the interned width helper reshapes before the spread copies.
- if (src.type.kind === "array" && !typeEquals(src.type, type)) {
+ // A fixed TUPLE (`[...FILES]` with FILES typed `[string, string,
+ // string]`) spreads its positions in order: the width helper copies
+ // the tuple's fields into an array of the literal's element type.
+ if (
+ (src.type.kind === "array" ||
+ (src.type.kind === "record" && lowerer.shapes.get(src.type.shapeId)?.tuple === true)) &&
+ !typeEquals(src.type, type)
+ ) {
const w = lowerer.widthCoerce(src, type);
if (w) src = w;
}
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/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
index e53d27aa9..932e2e4b6 100644
--- a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
@@ -1,7 +1,8 @@
-/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */
+/* Computed record keys: read-modify-write targets (`r[k] op= v`, `r[k]++`, `r[k] ??= v`) and runtime-keyed destructuring reads. */
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
+ DYN,
F64,
STRING,
typeEquals,
@@ -153,19 +154,30 @@ function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr {
}
/** A key outside its literal type (smuggled through a cast) names no
- * declared field: a fixed-shape record cannot add the property, so the
- * update throws the TypeError of a keyed write miss. */
-function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
+ * declared field: a fixed-shape record cannot add the property, so an
+ * update throws the TypeError of a keyed write miss, and a plain read the
+ * keyed read miss's (no undefined is representable in the field type). */
+function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc, read = false): IrExpr {
const message: IrExpr = {
kind: "strConcat",
left: {
kind: "strConcat",
- left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc },
+ left: {
+ kind: "strLit",
+ value: read ? "record has no key '" : "Cannot add property '",
+ type: STRING,
+ loc,
+ },
right: key,
type: STRING,
loc,
},
- right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc },
+ right: {
+ kind: "strLit",
+ value: read ? "' (typed slot — no undefined is representable)" : "' to a fixed-shape object",
+ type: STRING,
+ loc,
+ },
type: STRING,
loc,
};
@@ -182,8 +194,15 @@ function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
};
}
-/** The current value of a keyed target. */
-export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
+/** The current value of a keyed target. `read` marks a read that no
+ * write follows (a destructuring element), which words a key miss as a
+ * read. */
+export function keyedFieldRead(
+ lowerer: Lowerer,
+ target: KeyedFieldTarget,
+ loc: SrcLoc,
+ read = false,
+): IrExpr {
const { obj, shapeId, key, fieldType } = target;
if (!target.keys) {
const type = target.missNaN
@@ -220,7 +239,7 @@ export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc:
type: fieldType,
loc,
}),
- keyMiss(key, target, loc),
+ keyMiss(key, target, loc, read),
);
}
@@ -269,3 +288,76 @@ export function saveKeyedFieldKey(
): void {
if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey");
}
+
+/** The value a destructuring element with a runtime computed key binds
+ * (`const { [name]: v } = source`), or null when the source's
+ * representation has no keyed read for that key. The key evaluates here,
+ * once, into `out` (element order), and converts with ToPropertyKey:
+ * - a checked-dynamic source (or a JavaScript record, through its checked
+ * view) reads the live property, undefined when absent;
+ * - a pure index-signature record reads its entry, undefined when absent;
+ * - a record whose declared fields include every name the key's literal
+ * type allows, all stored with one type, reads the field the key names. */
+export function runtimeKeyPatternRead(
+ lowerer: Lowerer,
+ keyNode: ts.Expression,
+ source: IrExpr,
+ out: IrStmt[],
+ loc: SrcLoc,
+): IrExpr | null {
+ const save = (value: IrExpr, name: string): IrExpr => {
+ const local = lowerer.declareHiddenLocal(name, value.type);
+ out.push({ kind: "varDecl", localId: local.id, init: value, loc });
+ return { kind: "varRef", localId: local.id, type: value.type, loc };
+ };
+ const jsRecord =
+ source.type.kind === "record" &&
+ isJsSourceFile(keyNode.getSourceFile()) &&
+ lowerer.dynConvertible(source.type);
+ if (source.type.kind === "dyn" || jsRecord) {
+ const raw = lowerer.lowerExpr(keyNode);
+ if (!lowerer.dynConvertible(raw.type)) return null;
+ const key = save(
+ {
+ kind: "libCall",
+ fn: "dyn.propertyKey",
+ args: [lowerer.coerceInto(keyNode, raw, DYN)],
+ type: DYN,
+ loc,
+ },
+ "%patternKey",
+ );
+ return { kind: "dynKeyGet", value: lowerer.coerceToExpected(source, DYN), key, type: DYN, loc };
+ }
+ if (source.type.kind !== "record") return null;
+ const shapeId = source.type.shapeId;
+ const shape = lowerer.shapes.get(shapeId);
+ if (!shape || shape.tuple) return null;
+ if (shape.fields.length === 0 && shape.indexValue) {
+ const raw = lowerer.lowerExpr(keyNode);
+ if (raw.type.kind !== "string") return null;
+ const type =
+ shape.indexValue.kind === "dyn"
+ ? shape.indexValue
+ : lowerer.withUndefinedArmOf(shape.indexValue);
+ if (!type) return null;
+ const key = save(raw, "%patternKey");
+ return { kind: "recordKeyGet", obj: source, shapeId, key, type, loc };
+ }
+ const keys = literalFieldKeys(lowerer, keyNode);
+ const fields = keys?.map((k) => shape.fields.find((f) => f.name === k));
+ const first = fields?.[0];
+ if (!keys || !first || fields!.some((f) => !f || !typeEquals(f.type, first.type))) return null;
+ const raw = lowerer.lowerExpr(keyNode);
+ if (raw.type.kind !== "string") return null;
+ const target: KeyedFieldTarget = {
+ container: "recordKeyed",
+ obj: source,
+ shapeId,
+ field: keys[0]!,
+ keys,
+ key: save(raw, "%patternKey"),
+ fieldType: first.type,
+ };
+ return keyedFieldRead(lowerer, target, loc, true);
+}
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 ed24b72fb..b1fa41424 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -1,4 +1,5 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
+import { staticallySelected } from "../static-typeof.js";
import * as ts from "../../ts7/adapter.js";
import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
@@ -27,6 +28,7 @@ import {
import {
BOOL,
BYTES_ELEMENT_NAME,
+ BYTES_U8,
DYN,
F64,
type IrExpr,
@@ -479,7 +481,10 @@ export function lowerPropertyAccessExpr(
// undefined value so stored probes compose with optional calls.
if (
stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" &&
- !lowerer.checker.getPropertyOfType(lowerer.typeOf(expr.expression), expr.name.text)
+ !lowerer.checker.getPropertyOfType(
+ lowerer.typeOf(staticallySelected(lowerer, expr.expression)),
+ expr.name.text,
+ )
) {
return dynUndefinedExpr(loc);
}
@@ -874,7 +879,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",
@@ -983,7 +988,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",
@@ -1115,7 +1120,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,
@@ -1210,7 +1215,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",
@@ -1629,6 +1634,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(
{
@@ -2210,11 +2247,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
@@ -2728,7 +2834,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,
@@ -3084,7 +3190,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/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts
index f8b8c7604..5b9d05414 100644
--- a/packages/compiler/src/frontend/lowering/expressions/relational.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts
@@ -1380,6 +1380,24 @@ export function lowerInExpression(
if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
refuseExecExceptionCode(lowerer, expr);
}
+ // A literal key that names no declared member of an index-signature
+ // record (`"id" in match.groups`): the same keyed presence probe a
+ // runtime key takes — declared fields by name, then the overflow keys.
+ {
+ const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.right));
+ const shape = recvT?.kind === "record" ? lowerer.shapes.get(recvT.shapeId) : undefined;
+ if (
+ shape?.indexValue &&
+ !shape.tuple &&
+ !OBJECT_PROTOTYPE_KEYS.has(key) &&
+ !shape.fields.some(
+ (f) => f.name === key || f.name === `%get:${key}` || f.name === `%set:${key}`,
+ )
+ ) {
+ const probed = lowerRuntimeKeyIn(lowerer, expr, loc);
+ if (probed) return probed;
+ }
+ }
let recv = lowerer.lowerExpr(expr.right);
const classInfo =
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts
index 46005e228..cc3ac06fb 100644
--- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts
@@ -171,6 +171,21 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
}
if (constructorGlobalTypeof(lowerer, tested) !== null)
return { kind: "strLit", value: "function", type: STRING, loc };
+ // `typeof global.setTimeout` / `typeof globalThis.queueMicrotask`: a
+ // callable standard-library global read through the global object
+ // answers like its bare spelling (JavaScript sources represent the
+ // value as an identity token, so the operand cannot answer).
+ if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) {
+ const name = stdlibGlobalNameOf(lowerer, tested);
+ if (name !== null && name !== "globalThis") {
+ const t = lowerer.typeOf(tested);
+ if (
+ lowerer.checker.getCallSignatures(t).length > 0 ||
+ lowerer.checker.getConstructSignatures(t).length > 0
+ )
+ return { kind: "strLit", value: "function", type: STRING, loc };
+ }
+ }
// `typeof globalThis.AbortSignal.any` (ky's feature probe): a static
// member of a standard-library global, declared callable, whose value
// has no representation. The read has no effects and Node's answer
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..301026253 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -2180,6 +2180,57 @@ function lowerStringMethodCallWithOptionalArgs(
return changed ? { ...lowered, args } : lowered;
}
+/** `.then(String)` / `.then(Number)` / `.then(Boolean)` over a promise of
+ * a number, string or boolean: the ambient constructor converts its first
+ * argument (the settled value), so the handler is an interned closure over
+ * the settled type applying that conversion. The constructors' own value
+ * form takes a string, which a number or boolean settled value is not.
+ * Null for every other handler. */
+function primitiveConversionHandler(
+ lowerer: Lowerer,
+ node: ts.Expression,
+ inner: IrType,
+): IrExpr | null {
+ if (!ts.isIdentifier(node)) return null;
+ const name = node.text;
+ if (name !== "String" && name !== "Number" && name !== "Boolean") return null;
+ // A string settled value already fits the constructors' value form.
+ if (inner.kind !== "f64" && inner.kind !== "bool") return null;
+ if (!lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(node) ?? undefined)) return null;
+ const loc = locOf(node);
+ const ret: IrType = name === "String" ? STRING : name === "Number" ? F64 : BOOL;
+ const fnT = funcOf([inner], ret);
+ const key = `then.${name}:${typeKey(inner)}`;
+ let fnName = lowerer.arrHofHelpers.get(key);
+ if (fnName === undefined) {
+ fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`;
+ lowerer.arrHofHelpers.set(key, fnName);
+ const value = varRef("v.0", inner, loc);
+ let body: IrExpr;
+ if (name === "Boolean") body = { kind: "toBool", operand: value, type: BOOL, loc };
+ else if (name === "String") body = { kind: "toString", operand: value, type: STRING, loc };
+ else if (inner.kind === "f64") body = value;
+ else
+ body = {
+ kind: "ternary",
+ cond: value,
+ then: { kind: "numLit", value: 1, type: F64, loc },
+ else_: { kind: "numLit", value: 0, type: F64, loc },
+ type: F64,
+ loc,
+ };
+ lowerer.liftedFns.push({
+ name: fnName,
+ params: [{ localId: "v.0", name: "v", type: inner }],
+ returnType: ret,
+ locals: [{ id: "v.0", name: "v", type: inner, mutable: false }],
+ body: [{ kind: "return", value: body, loc }],
+ loc,
+ });
+ }
+ return { kind: "closure", fnName, captures: [], type: fnT, loc };
+}
+
function lowerNumberConstructorValue(
lowerer: Lowerer,
argNode: ts.Expression,
@@ -3367,6 +3418,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 &&
@@ -4327,8 +4380,13 @@ function lowerArrayIsArrayCall(
if (lowerer.stdlibGlobalMember(access, "Array") !== "isArray") return null;
if (call.arguments.nodes.length !== 1) return null; // the stdlib chokepoint fences
const argNode = call.arguments.nodes[0]!;
- const arg = lowerer.lowerExpr(argNode);
+ let arg = lowerer.lowerExpr(argNode);
const loc = locOf(call);
+ // A checker-narrowed dyn read arrives as a validated extraction (the
+ // dynCheck bridge). The runtime value may be wider than the flow type
+ // claims (an overload narrower than what arrives), so test the dyn value
+ // itself instead of folding on the extracted type (typeof's rule).
+ if (arg.kind === "dynCheck" && arg.value.type.kind === "dyn") arg = arg.value;
if (arg.type.kind === "dyn") {
return { kind: "dynTest", test: "array", value: arg, type: BOOL, loc };
}
@@ -5781,7 +5839,9 @@ export function lowerPromiseMethodCall(
if (inner.kind === "moduleNs")
markModuleNsHandlerParams(lowerer, call.arguments.nodes[0]!, inner);
- let cb = lowerer.lowerExpr(call.arguments.nodes[0]!);
+ let cb =
+ primitiveConversionHandler(lowerer, call.arguments.nodes[0]!, inner) ??
+ lowerer.lowerExpr(call.arguments.nodes[0]!);
// A TYPED handler on a DYN-settling promise (the tracePromise
// result's `.then((value) => ...)` — the checker's generic
// instantiation typed the parameter, but the settled value is a
@@ -9338,15 +9398,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 +10474,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..7bcb52aaa 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -8,6 +8,7 @@ import {
} from "./constructor-returns.js";
import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { lowerReadlineUiNew } from "./builtins/readline.js";
+import { builtinMemberRequireDecl } from "./builtins/module-bindings.js";
import { checkedClassUnionAssertion } from "./class-assertions.js";
import { collectClassMethodMutations } from "./class-method-mutations.js";
import { InternalCompilerError } from "../../errors.js";
@@ -100,7 +101,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";
@@ -1036,11 +1037,16 @@ export function builtinStreamInfoOf(
// member to the stdlib class symbol. The declaration itself is
// alias plumbing (streamClassAliasDecl — both declaration walks
// skip it).
+ // A `var`/`let` binding of the inline require member
+ // (`var Transform = require("stream").Transform`) is the same alias
+ // plumbing as builtinMemberRequireDecl's: its declaration owns no
+ // storage either way.
const decl = symbol.valueDeclaration;
if (
decl &&
ts.isVariableDeclaration(decl) &&
- (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 &&
+ ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 ||
+ builtinMemberRequireDecl(decl.name, decl.initializer)) &&
decl.initializer !== undefined &&
ts.isPropertyAccessExpression(decl.initializer) &&
!decl.initializer.questionDotToken &&
@@ -1399,7 +1405,8 @@ export function builtinEmitterInfoOf(
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
- (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 &&
+ ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 ||
+ builtinMemberRequireDecl(decl.name, decl.initializer)) &&
decl.initializer !== undefined &&
ts.isPropertyAccessExpression(decl.initializer) &&
!decl.initializer.questionDotToken &&
@@ -5549,8 +5556,18 @@ export function lowerStaticFieldRead(
}
// `C.name` — the JS-observable class name, a compile-time constant on
// the direct spelling (tsc rejects user statics named `name`, so the
- // chain above can never shadow it in TypeScript sources).
+ // chain above can never shadow it in TypeScript sources). A class of an
+ // import cycle reads it from its class object instead, whose reference
+ // checks the class's initialization (checkCycleBindingAccesses).
if (expr.name.text === "name" && info.def.jsName !== undefined) {
+ if (lowerer.classInitFlags.get(info.def.name)?.cycle === true)
+ return {
+ kind: "libCall",
+ fn: "class.name",
+ args: [classValueRef(lowerer, info, expr.expression)],
+ type: STRING,
+ loc,
+ };
return { kind: "strLit", value: info.def.jsName, type: STRING, loc };
}
if (info.callableBase)
@@ -6770,14 +6787,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 +9229,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-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts
index 4cb238352..b8b905718 100644
--- a/packages/compiler/src/frontend/lowering/lower-dgram.ts
+++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts
@@ -441,8 +441,31 @@ function lowerDnsModuleCall(
const hostname = dyn(args[0]!);
const options = args.length === 3 ? dyn(args[1]!) : dynUndefinedExpr(loc);
const last = args[args.length - 1]!;
- const typed = typedResult(last);
- const cb = lowerDnsCallback(lowerer, last, "lookup callbacks");
+ // Options known only at run time (a checked-dynamic value, or one cast
+ // to an overload's option type) decide between Node's two callback
+ // shapes there: the address is a string, or an address list with
+ // `all: true`. An inline callback then binds its parameters as
+ // checked-dynamic values, whatever the overload the checker picked
+ // says, and its body tests what actually arrived.
+ let runtimeOptions = false;
+ if (args.length === 3) {
+ let inner: ts.Expression = args[1]!;
+ while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner))
+ inner = inner.expression;
+ runtimeOptions = lowerer.mapTypeOf(lowerer.typeOf(inner))?.kind === "dyn";
+ }
+ let callbackNode: ts.Expression = last;
+ while (ts.isParenthesizedExpression(callbackNode)) callbackNode = callbackNode.expression;
+ const dynCallback =
+ runtimeOptions && (ts.isArrowFunction(callbackNode) || ts.isFunctionExpression(callbackNode));
+ const typed = dynCallback ? boolLit(false, loc) : typedResult(last);
+ if (dynCallback) lowerer.dynParamLambdas.add(callbackNode);
+ let cb: IrExpr;
+ try {
+ cb = lowerDnsCallback(lowerer, last, "lookup callbacks");
+ } finally {
+ if (dynCallback) lowerer.dynParamLambdas.delete(callbackNode);
+ }
return {
kind: "libCall",
fn: "dns.lookupDyn",
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 76c043f4f..e1096f2fe 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";
@@ -9,6 +11,7 @@ import { InternalCompilerError } from "../../errors.js";
* (FieldTarget). */
import * as ts from "../ts7/adapter.js";
import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands.js";
+import { staticCondition } from "./static-typeof.js";
import { constituentTypes } from "../ts7/checker.js";
import type { Lowerer } from "./lowerer.js";
import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js";
@@ -270,6 +273,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
@@ -1078,6 +1100,11 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
});
if (presence !== null) return presence;
}
+ // Global detection (`typeof window < "u"`, `typeof self !==
+ // "undefined" && …`): a condition whose typeof answers are static
+ // folds, so the untaken arm never lowers (static-typeof.ts).
+ const folded = staticCondition(lowerer, e);
+ if (folded !== null) return { kind: "boolLit", value: folded, type: BOOL, loc: locOf(e) };
if (ts.isBinaryExpression(e)) {
const op = e.operatorToken.kind;
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
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-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts
index a8657f978..4be5d0b7e 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -1,4 +1,12 @@
-import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
+import {
+ dynUndefinedExpr,
+ boolLit,
+ helperLoc,
+ nodeThrowExpr,
+ numLit,
+ strLit,
+ varRef,
+} from "../../ir/build.js";
import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
@@ -13,6 +21,7 @@ import {
type IrExpr,
type IrFunction,
type IrStmt,
+ type IrStrIntrinsicMethod,
type IrType,
RUNTIME_ERROR_CLASSES,
STRING,
@@ -111,13 +120,589 @@ function inspectKey(name: string): string {
function errorRenderable(info: ClassInfo): boolean {
// The builtin classes' name/message/code live in runtime slots (the
// ScrError prefix insp.error reads), not IR fields; only USER-declared
- // fields are own properties the runtime rendering would miss. #private
- // fields are NOT own properties — Node never prints them — so they
- // don't disqualify.
- if (!info.builtinError && info.def.fields.some((f) => !f.name.startsWith("#"))) return false;
+ // fields are own properties the runtime rendering would miss. A user
+ // class's layout repeats its builtin base's prefix fields, which are
+ // those same slots. #private fields are NOT own properties — Node never
+ // prints them — so they don't disqualify.
+ if (!info.builtinError) {
+ const inherited = new Set();
+ for (let c = info.base; c; c = c.base) {
+ if (c.builtinError || c.def.runtime) for (const f of c.def.fields) inherited.add(f.name);
+ }
+ if (info.def.fields.some((f) => !f.name.startsWith("#") && !inherited.has(f.name)))
+ return false;
+ }
return info.subclasses.every((s) => errorRenderable(s));
}
+/** The accessors that change what Node reads for an error's header. The
+ * runtime renders the instance's own name and message slots. */
+const ERROR_HEADER_ACCESSORS = [
+ "get:name",
+ "set:name",
+ "get:message",
+ "set:message",
+ "get:stack",
+ "set:stack",
+];
+
+/** Whether Node's header for an instance of `info`'s hierarchy can differ
+ * from the runtime's slots: a program class among its ancestors or
+ * descendants declares a `name`, `message` or `stack` accessor, or the
+ * program writes a prototype's `name` or `message`. */
+function errorHeaderOverridden(lowerer: Lowerer, info: ClassInfo): boolean {
+ if (lowerer.prototypeMethodAccesses.has("name") || lowerer.prototypeMethodAccesses.has("message"))
+ return true;
+ const declares = (c: ClassInfo): boolean =>
+ !c.builtinError && !c.def.runtime && ERROR_HEADER_ACCESSORS.some((m) => c.methods.has(m));
+ for (let c = info.base; c; c = c.base) if (declares(c)) return true;
+ const below = (c: ClassInfo): boolean => declares(c) || c.subclasses.some(below);
+ return below(info);
+}
+
+/** Error keys Node's error inspector folds into the base or hides. */
+const ERROR_SPECIAL_KEYS = new Set(["name", "message", "stack", "cause", "errors"]);
+
+/** The declared own (enumerable) fields of a user error class, in layout
+ * order (the base chain first). */
+function ownErrorFields(info: ClassInfo): { name: string; type: IrType }[] {
+ // The builtin bases' layout fields (name, message) are runtime slots the
+ // error base already renders, not own enumerable properties.
+ const builtin = new Set();
+ for (let c = info.base; c; c = c.base) {
+ if (c.builtinError || c.def.runtime) for (const f of c.def.fields) builtin.add(f.name);
+ }
+ return info.def.fields.filter(
+ (f) => isClassOwnEnumerableFieldName(f.name) && !builtin.has(f.name),
+ );
+}
+
+/** `info` and its descendants that declare fields, most-derived first (a
+ * runtime instance tests against its own class before an ancestor's). */
+function fieldedErrorClasses(info: ClassInfo): ClassInfo[] {
+ const out: ClassInfo[] = [];
+ const visit = (c: ClassInfo): void => {
+ for (const s of c.subclasses) visit(s);
+ if (!c.builtinError && !c.def.runtime && ownErrorFields(c).length > 0) out.push(c);
+ };
+ visit(info);
+ return out;
+}
+
+/** Why a fielded error class's own properties have no static rendering,
+ * or null when every field renders. `visiting` and `out` are the caller's
+ * inspectSupport walk: a field typed as the class itself (`next?: Chain`)
+ * is a recursive shape, not a new walk. */
+function fieldedErrorReason(
+ lowerer: Lowerer,
+ info: ClassInfo,
+ visiting: Set = new Set(),
+ out: { recursive: boolean } = { recursive: false },
+): string | null {
+ const key = `error:${info.def.name}`;
+ if (visiting.has(key)) {
+ out.recursive = true;
+ return null;
+ }
+ visiting.add(key);
+ try {
+ for (let c: ClassInfo | null = info; c && !c.builtinError && !c.def.runtime; c = c.base) {
+ // Node prints the own properties in creation order, which puts a
+ // declared `cause` among the fields; the cause renders after them.
+ if (c.fieldOrder.some((f) => f.errorCause))
+ return "its field 'cause' is one Node's error inspector treats specially";
+ // A JavaScript class's fields exist once its constructor creates them,
+ // which can be conditional; the layout reads every field.
+ if (c.decl && isJsSourceFile(c.decl.getSourceFile()) && ownErrorFields(c).length > 0)
+ return "its fields are JavaScript properties, which its constructor may not create";
+ }
+ for (const f of ownErrorFields(info)) {
+ if (ERROR_SPECIAL_KEYS.has(f.name))
+ return `its field '${f.name}' is one Node's error inspector treats specially`;
+ const why = inspectSupport(lowerer, f.type, visiting, out);
+ if (why !== null) return `its field '${f.name}': ${why}`;
+ }
+ return null;
+ } finally {
+ visiting.delete(key);
+ }
+}
+
+/** True when an instance of `c` can contain itself: its fields reach `c`
+ * or one of its bases, whose slots can hold a `c` (`next: Chain` in a
+ * subclass of Chain, a field typed as Error). */
+function errorInstanceReachesItself(lowerer: Lowerer, c: ClassInfo): boolean {
+ const holders = new Set();
+ for (let a: ClassInfo | null = c; a; a = a.base) holders.add(a.def.name);
+ const seen = new Set();
+ const walk = (u: IrType): boolean => {
+ for (const x of typeConstituents(lowerer, u)) {
+ if (x.kind === "object" && holders.has(x.className)) return true;
+ const k = typeKey(x);
+ if (!seen.has(k)) {
+ seen.add(k);
+ if (walk(x)) return true;
+ }
+ }
+ return false;
+ };
+ return walk({ kind: "object", className: c.def.name });
+}
+
+/** An error value whose runtime class may be a program-declared subclass,
+ * which may declare fields (`class Bailable extends Error { bail = false }`)
+ * or carry a `cause`: Node prints the error base (`[Bailable [Error]: m]`,
+ * the stackless form compiled errors use) followed by the own properties,
+ * `[Bailable [Error]: m] { bail: true }`, with the usual single-line/
+ * multi-line choice counting the base. Each program class (most-derived
+ * first) is an instanceof arm; other values keep the runtime's error
+ * rendering. Beyond the depth budget an instance with own properties
+ * prints `[Name]`, as Node does for any object with keys; one without
+ * prints its header. */
+function errorInspectHelper(
+ lowerer: Lowerer,
+ t: IrType & { kind: "object" },
+ info: ClassInfo,
+ loc: SrcLoc,
+): string {
+ const key = `insp.userError:${typeKey(t)}`;
+ const existing = lowerer.inspectHelpers.get(key);
+ if (existing) return existing;
+ const name = `%util.insp.${lowerer.inspectHelpers.size}`;
+ lowerer.inspectHelpers.set(key, name);
+ const v = (): IrExpr => varRef("v.0", t, loc);
+ const r = (): IrExpr => varRef("r.0", F64, loc);
+ const d = (): IrExpr => varRef("d.0", F64, loc);
+ const rPlus1 = (): IrExpr => ({
+ kind: "bin",
+ op: "+",
+ left: r(),
+ right: numLit(1, loc),
+ type: F64,
+ loc,
+ });
+ const base = (): IrExpr => ({
+ kind: "libCall",
+ fn: "insp.error",
+ args: [v(), r(), d()],
+ type: STRING,
+ loc,
+ });
+ const error = (): IrExpr => lowerer.upcastTo(v(), "%Error");
+ const hasCause = (): IrExpr => ({
+ kind: "libCall",
+ fn: "insp.errorHasCause",
+ args: [error()],
+ type: BOOL,
+ loc,
+ });
+ const entry = (text: IrExpr): IrStmt => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "insp.entry",
+ args: [text, boolLit(false, loc)],
+ type: VOID,
+ loc,
+ },
+ loc,
+ });
+ /** The own properties of an instance of `c`: its fields, then its
+ * `cause` (an assignment in the constructor or later, or the
+ * constructor's options, which Node lists last as `[cause]`). */
+ const propertiesArm = (
+ c: ClassInfo,
+ display: string,
+ fields: { name: string; type: IrType }[],
+ ): IrStmt[] => {
+ const cls: IrType & { kind: "object" } = { kind: "object", className: c.def.name };
+ const instance: IrExpr =
+ c.def.name === t.className ? v() : { kind: "downcast", value: v(), type: cls, loc };
+ // A class whose fields can hold the instance itself (`next: Chain`)
+ // runs Node's circular machinery, as other cycle-capable objects do:
+ // an instance already on the traversal stack renders [Circular *N],
+ // before the depth answer, and the frame's text gets the [
+ // prefix when the walk found it circular.
+ const onCycle = errorInstanceReachesItself(lowerer, c);
+ const circular = (): IrExpr => varRef("cc.0", F64, loc);
+ const arm: IrStmt[] = [
+ ...(onCycle
+ ? ([
+ {
+ kind: "varDecl",
+ localId: "cc.0",
+ init: { kind: "libCall", fn: "insp.circCheck", args: [v()], type: F64, loc },
+ loc,
+ },
+ {
+ kind: "if",
+ cond: {
+ kind: "bin",
+ op: ">",
+ left: circular(),
+ right: numLit(0, loc),
+ type: BOOL,
+ loc,
+ },
+ then: [
+ {
+ kind: "return",
+ value: {
+ kind: "libCall",
+ fn: "insp.circular",
+ args: [circular()],
+ type: STRING,
+ loc,
+ },
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ },
+ ] satisfies IrStmt[])
+ : []),
+ {
+ kind: "if",
+ cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc },
+ then: [{ kind: "return", value: strLit(`[${display}]`, loc), loc }],
+ else_: null,
+ loc,
+ },
+ {
+ kind: "varDecl",
+ localId: "base.0",
+ init: improvedErrorBase(lowerer, v(), base(), display, loc),
+ loc,
+ },
+ {
+ kind: "exprStmt",
+ expr: { kind: "libCall", fn: "insp.begin", args: [rPlus1()], type: VOID, loc },
+ loc,
+ },
+ ...(onCycle
+ ? ([
+ {
+ kind: "exprStmt",
+ expr: { kind: "libCall", fn: "insp.seenPush", args: [v()], type: VOID, loc },
+ loc,
+ },
+ ] satisfies IrStmt[])
+ : []),
+ ];
+ for (const f of fields) {
+ const read: IrExpr = {
+ kind: "fieldGet",
+ obj: instance,
+ className: c.def.name,
+ field: f.name,
+ type: f.type,
+ loc,
+ };
+ arm.push(
+ entry(
+ concatAll(
+ [
+ strLit(`${inspectKey(f.name)}: `, loc),
+ inspectExpr(lowerer, f.type, read, rPlus1(), d(), loc),
+ ],
+ loc,
+ ),
+ ),
+ );
+ }
+ arm.push({
+ kind: "if",
+ cond: hasCause(),
+ then: [
+ entry({
+ kind: "libCall",
+ fn: "insp.errorCause",
+ args: [error(), rPlus1(), d()],
+ type: STRING,
+ loc,
+ }),
+ ],
+ else_: null,
+ loc,
+ });
+ const ended: IrExpr = {
+ kind: "libCall",
+ fn: "insp.end",
+ args: [
+ varRef("base.0", STRING, loc),
+ strLit("{", loc),
+ strLit("}", loc),
+ rPlus1(),
+ boolLit(false, loc),
+ boolLit(false, loc),
+ ],
+ type: STRING,
+ loc,
+ };
+ arm.push({
+ kind: "return",
+ value: onCycle
+ ? { kind: "libCall", fn: "insp.refWrap", args: [v(), ended], type: STRING, loc }
+ : ended,
+ loc,
+ });
+ return arm;
+ };
+ const body: IrStmt[] = [];
+ for (const c of userErrorClasses(info)) {
+ const display = c.def.jsName ?? c.decl?.name?.text ?? c.def.name.replace(/^%/, "");
+ const fields = ownErrorFields(c);
+ const why = fields.length > 0 ? fieldedErrorReason(lowerer, c) : null;
+ let arm: IrStmt[];
+ if (why !== null) {
+ // An instance of a subclass whose own properties have no rendering
+ // throws a named refusal instead of printing a partial object.
+ arm = [
+ {
+ kind: "return",
+ value: nodeThrowExpr(
+ 0,
+ "SC2020",
+ `inspecting a '${display}' error (${why}) is not supported yet`,
+ STRING,
+ loc,
+ ),
+ loc,
+ },
+ ];
+ } else if (fields.length === 0) {
+ // A field-less program class: without a `cause` it has no own
+ // properties, and Node prints the header alone at any depth.
+ arm = [
+ {
+ kind: "if",
+ cond: { kind: "unary", op: "!", operand: hasCause(), type: BOOL, loc },
+ then: [
+ { kind: "return", value: improvedErrorBase(lowerer, v(), base(), display, loc), loc },
+ ],
+ else_: null,
+ loc,
+ },
+ ...propertiesArm(c, display, fields),
+ ];
+ } else {
+ arm = propertiesArm(c, display, fields);
+ }
+ if (c.def.name === t.className) {
+ body.push({ kind: "block", body: arm, loc });
+ break;
+ }
+ body.push({
+ kind: "if",
+ cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
+ then: arm,
+ else_: null,
+ loc,
+ });
+ }
+ if (body.at(-1)?.kind !== "block") body.push({ kind: "return", value: base(), loc });
+ const fn: IrFunction = {
+ name,
+ params: [
+ { localId: "v.0", name: "v", type: t },
+ { localId: "r.0", name: "r", type: F64 },
+ { localId: "d.0", name: "d", type: F64 },
+ ],
+ returnType: STRING,
+ locals: [
+ { id: "v.0", name: "v", type: t, mutable: false },
+ { id: "r.0", name: "r", type: F64, mutable: false },
+ { id: "d.0", name: "d", type: F64, mutable: false },
+ { id: "base.0", name: "base", type: STRING, mutable: false },
+ { id: "cc.0", name: "cc", type: F64, mutable: false },
+ { id: "name.0", name: "name", type: STRING, mutable: true },
+ { id: "raw.0", name: "raw", type: STRING, mutable: true },
+ ],
+ body,
+ loc,
+ };
+ // Dynamic stores (a JavaScript class's `this.x = ...` outside its
+ // declared fields, or a write through an untyped reference) add a
+ // property bag to a class after this helper was built. The rendering
+ // reads only a `cause` from it, so an instance whose bag holds another
+ // property throws a named refusal instead of printing a partial object.
+ lowerer.shapeOrderHelperFinalizers.push(() => {
+ const bagged = [info, ...userErrorClasses(info)].filter(
+ (c) =>
+ !c.builtinError &&
+ !c.def.runtime &&
+ c.def.fields.some((f) => f.name === DYN_CLASS_PROPERTIES),
+ );
+ if (bagged.length === 0) return;
+ fn.body = [
+ ...bagged.map((c): IrStmt => ({
+ kind: "if",
+ cond: {
+ kind: "logical",
+ op: "&&",
+ left: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
+ right: {
+ kind: "libCall",
+ fn: "insp.errorBagHasProperties",
+ args: [
+ {
+ kind: "fieldGet",
+ obj:
+ c.def.name === t.className
+ ? v()
+ : {
+ kind: "downcast",
+ value: v(),
+ type: { kind: "object", className: c.def.name },
+ loc,
+ },
+ className: c.def.name,
+ field: DYN_CLASS_PROPERTIES,
+ type: DYN,
+ loc,
+ },
+ ],
+ type: BOOL,
+ loc,
+ },
+ type: BOOL,
+ loc,
+ },
+ then: [
+ {
+ kind: "return",
+ value: nodeThrowExpr(
+ 0,
+ "SC2020",
+ `inspecting a '${c.def.jsName ?? c.def.name.replace(/^%/, "")}' error with properties created at run time is not supported yet`,
+ STRING,
+ loc,
+ ),
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ })),
+ ...body,
+ ];
+ });
+ lowerer.liftedFns.push(fn);
+ return name;
+}
+
+/** The program-declared classes among `info` and its descendants, most
+ * derived first (an instance tests against its own class before an
+ * ancestor's). Runtime-provided error classes are not among them. */
+function userErrorClasses(info: ClassInfo): ClassInfo[] {
+ const out: ClassInfo[] = [];
+ const visit = (c: ClassInfo): void => {
+ for (const s of c.subclasses) visit(s);
+ if (!c.builtinError && !c.def.runtime) out.push(c);
+ };
+ visit(info);
+ return out;
+}
+
+/** Node's improveStack over the stackless base (`[Error: m]`): when the
+ * instance's constructor name differs from its `name` and the name ends in
+ * "Error", the header names both — `[Bailable [Error]: m]`, or
+ * `[FetchError: m]` when the constructor name already contains it. */
+function improvedErrorBase(
+ lowerer: Lowerer,
+ value: IrExpr,
+ base: IrExpr,
+ display: string,
+ loc: SrcLoc,
+): IrExpr {
+ const str = (
+ method: IrStrIntrinsicMethod,
+ receiver: IrExpr,
+ args: IrExpr[],
+ type: IrType,
+ ): IrExpr => ({
+ kind: "strIntrinsic",
+ method,
+ receiver,
+ args,
+ type,
+ loc,
+ });
+ const and = (left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "logical",
+ op: "&&",
+ left,
+ right,
+ type: BOOL,
+ loc,
+ });
+ const eq = (left: IrExpr, right: IrExpr, negated = false): IrExpr => ({
+ kind: "strEq",
+ negated,
+ left,
+ right,
+ type: BOOL,
+ loc,
+ });
+ const nameRef = (): IrExpr => varRef("name.0", STRING, loc);
+ const baseRef = (): IrExpr => varRef("raw.0", STRING, loc);
+ const after = (): IrExpr => ({
+ kind: "bin",
+ op: "+",
+ left: str("length", nameRef(), [], F64),
+ right: numLit(1, loc),
+ type: F64,
+ loc,
+ });
+ const next = (): IrExpr => str("charAt", baseRef(), [after()], STRING);
+ const cond = and(
+ and(
+ eq(nameRef(), strLit(display, loc), true),
+ str("endsWith", nameRef(), [strLit("Error", loc)], BOOL),
+ ),
+ and(str("startsWith", baseRef(), [concatAll([strLit("[", loc), nameRef()], loc)], BOOL), {
+ kind: "logical",
+ op: "||",
+ left: eq(next(), strLit(":", loc)),
+ right: eq(next(), strLit("]", loc)),
+ type: BOOL,
+ loc,
+ }),
+ );
+ const rest = (): IrExpr => str("slice", baseRef(), [after()], STRING);
+ const improved: IrExpr = {
+ kind: "ternary",
+ cond: str("includes", strLit(display, loc), [nameRef()], BOOL),
+ then: concatAll([strLit(`[${display}`, loc), rest()], loc),
+ else_: concatAll([strLit(`[${display} [`, loc), nameRef(), strLit("]", loc), rest()], loc),
+ type: STRING,
+ loc,
+ };
+ return {
+ kind: "seqExpr",
+ stmts: [
+ {
+ kind: "varDecl",
+ localId: "name.0",
+ init: {
+ kind: "fieldGet",
+ obj: lowerer.upcastTo(value, "%Error"),
+ className: "%Error",
+ field: "name",
+ type: STRING,
+ loc,
+ },
+ loc,
+ },
+ { kind: "varDecl", localId: "raw.0", init: base, loc },
+ ],
+ result: { kind: "ternary", cond, then: improved, else_: baseRef(), type: STRING, loc },
+ type: STRING,
+ loc,
+ };
+}
+
function isErrorClass(lowerer: Lowerer, className: string): boolean {
if (RUNTIME_ERROR_CLASSES.has(className)) return true;
return lowerer.isSubclassOf(className, "%Error");
@@ -222,9 +807,18 @@ function inspectSupport(
// for those classes (the runtime's capsule view).
if (eventTargetRootOf(info) !== null) return null;
if (isErrorClass(lowerer, t.className)) {
- return errorRenderable(info)
- ? null
- : "error subclasses with declared fields have no inspect lowering yet (the extra own properties are runtime-dynamic)";
+ if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info))
+ return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet";
+ if (errorRenderable(info)) return null;
+ // Subclasses with declared fields render the error base followed
+ // by their own properties. The static class's own fields must
+ // render; a subclass whose fields cannot is a runtime refusal on
+ // its own instances only (see errorInspectHelper).
+ const own = fieldedErrorClasses(info).find((c) => c === info);
+ const ownWhy = own ? fieldedErrorReason(lowerer, own, visiting, out) : null;
+ if (ownWhy !== null)
+ return `'${t.className.replace(/^%(m\d+\.)?/, "")}' error values (${ownWhy})`;
+ return null;
}
// A class with subclasses, or a JavaScript class, renders through its
// checked-dynamic view, which names the instance's own class and holds
@@ -373,6 +967,16 @@ export function inspectExpr(
};
}
if (isErrorClass(lowerer, t.className)) {
+ const info = lowerer.classes.get(t.className);
+ if (info && userErrorClasses(info).length > 0) {
+ return {
+ kind: "call",
+ callee: errorInspectHelper(lowerer, t, info, loc),
+ args: [value, recurse, depth],
+ type: STRING,
+ loc,
+ };
+ }
return {
kind: "libCall",
fn: "insp.error",
@@ -475,8 +1079,17 @@ function typeConstituents(lowerer: Lowerer, u: IrType): IrType[] {
return [u.key, u.value];
case "union":
return lowerer.unions.get(u.unionId)?.arms.slice() ?? [];
- case "object":
- return lowerer.classes.get(u.className)?.def.fields.map((f) => f.type) ?? [];
+ case "object": {
+ const info = lowerer.classes.get(u.className);
+ if (!info) return [];
+ const types = info.def.fields.map((f) => f.type);
+ // An error value can be an instance of a program-declared subclass,
+ // whose own fields its inspection renders (errorInspectHelper).
+ if (isErrorClass(lowerer, u.className))
+ for (const c of userErrorClasses(info))
+ for (const f of ownErrorFields(c)) types.push(f.type);
+ return types;
+ }
default:
return [];
}
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index 6e40ad080..c7bbf958d 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";
@@ -157,6 +158,49 @@ export interface FileParts {
topStmts: ts.Statement[];
}
+/** Preflight's module graph (LoadResult.moduleEdges), for the walks that
+ * append modules outside the startup order, and the set collecting the
+ * inline require() calls that close a cycle there. */
+export interface AppendGraph {
+ edges?: ReadonlyMap | undefined;
+ inlineCycleCalls?: Set;
+}
+
+/** An appended module's edges: its import and export declarations, then
+ * the require() edges preflight resolved — a CommonJS file's only edges
+ * (the compiled CommonJS package a workspace module loaded on demand
+ * imports, and the sibling packages and files IT requires). */
+function appendedEdgesOf(
+ program: ts.Program,
+ graph: AppendGraph | undefined,
+): (sf: ts.SourceFile) => CycleEdge[] {
+ const cache = new Map();
+ return (sf) => {
+ let out = cache.get(sf);
+ if (out !== undefined) return out;
+ out = orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
+ dep !== null && dep !== sf
+ ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
+ : [],
+ );
+ for (const e of graph?.edges?.get(sf) ?? [])
+ if (e.stmt === undefined && e.dep !== sf) out.push(e);
+ cache.set(sf, out);
+ return out;
+ };
+}
+
+/** Lazy inline requires run after their module's top level: their modules
+ * join the order afterwards, as in preflight's walk. */
+function visitLazyEdges(
+ sf: ts.SourceFile,
+ edgesOf: (sf: ts.SourceFile) => CycleEdge[],
+ state: ReadonlyMap,
+ visit: (sf: ts.SourceFile) => void,
+): void {
+ for (const e of edgesOf(sf)) if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep);
+}
+
/** Program modules selected by statically resolved child_process.fork()
* calls join the compiled graph without becoming startup dependencies of
* the parent entry. Their guarded init functions are selected by %main only
@@ -167,8 +211,9 @@ export function appendForkModules(
order: ts.SourceFile[],
targets: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
- appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle);
+ appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle, graph);
}
export function appendWorkerModules(
@@ -176,8 +221,9 @@ export function appendWorkerModules(
order: ts.SourceFile[],
targets: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
- appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle);
+ appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle, graph);
}
function appendEntryModules(
@@ -186,6 +232,7 @@ function appendEntryModules(
targets: ts.SourceFile[],
paths: readonly string[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
if (order.length === 0) return;
const byPath = new Map(
@@ -195,22 +242,24 @@ function appendEntryModules(
for (const sf of order) state.set(sf, "done");
const added: ts.SourceFile[] = [];
const stack: string[] = [];
- const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] =>
- orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
- dep !== null && dep !== sf
- ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
- : [],
- );
- const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf);
+ const staticEdgesOf = appendedEdgesOf(program, graph);
+ const cycleAdmissionReason = makeCycleAdmission(program, (sf) =>
+ staticEdgesOf(sf).filter((e) => e.call === undefined),
+ );
const visit = (sf: ts.SourceFile): void => {
const prior = state.get(sf);
if (prior === "done") return;
state.set(sf, "visiting");
stack.push(sf.fileName);
for (const edge of staticEdgesOf(sf)) {
+ if (edge.lazy === true) continue; // visited after this module completes
const edgeState = state.get(edge.dep);
if (edgeState === "done") continue;
if (edgeState === "visiting") {
+ if (edge.call !== undefined) {
+ graph?.inlineCycleCalls?.add(edge.call);
+ continue;
+ }
const reason = cycleAdmissionReason(sf, edge);
if (reason !== null) {
const cycleStart = stack.indexOf(edge.dep.fileName);
@@ -223,6 +272,7 @@ function appendEntryModules(
stack.pop();
state.set(sf, "done");
added.push(sf);
+ visitLazyEdges(sf, staticEdgesOf, state, visit);
};
const knownTargets = new Set(targets);
@@ -247,8 +297,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/
@@ -314,27 +368,30 @@ export function appendDynamicImportModules(
program: ts.Program,
order: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
if (order.length === 0) return; // no preflight order — sites keep their fences
const state = new Map();
for (const sf of order) state.set(sf, "done");
const added: ts.SourceFile[] = [];
const stack: string[] = [];
- const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] =>
- orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
- dep !== null && dep !== sf
- ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
- : [],
- );
- const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf);
+ const staticEdgesOf = appendedEdgesOf(program, graph);
+ const cycleAdmissionReason = makeCycleAdmission(program, (sf) =>
+ staticEdgesOf(sf).filter((e) => e.call === undefined),
+ );
const visit = (sf: ts.SourceFile): void => {
if (state.get(sf) !== undefined) return;
state.set(sf, "visiting");
stack.push(sf.fileName);
for (const e of staticEdgesOf(sf)) {
+ if (e.lazy === true) continue; // visited after this module completes
const s = state.get(e.dep);
if (s === "done") continue;
if (s === "visiting") {
+ if (e.call !== undefined) {
+ graph?.inlineCycleCalls?.add(e.call);
+ continue;
+ }
// A back edge: Node answers it from the cache. Same admission
// question as preflight's static walk, same fence when refused.
const reason = cycleAdmissionReason(sf, e);
@@ -349,6 +406,7 @@ export function appendDynamicImportModules(
stack.pop();
state.set(sf, "done");
added.push(sf);
+ visitLazyEdges(sf, staticEdgesOf, state, visit);
};
const scanned = new Set();
for (;;) {
@@ -1230,6 +1288,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,
@@ -1245,6 +1333,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,
@@ -1332,6 +1435,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);
@@ -1369,8 +1473,17 @@ export function collectGlobals(
// common/tls's `exports.check = function (certs) {...}` —
// keep func-ness with dyn pieces; everything else is dyn).
const strict = lowerer.checker.getTypeOfSymbol(symbol);
- const t = lowerer.mapTypeOf(strict) ?? dynFallbackType(lowerer, nameNode, strict);
+ let t = lowerer.mapTypeOf(strict) ?? dynFallbackType(lowerer, nameNode, strict);
if (!t || t.kind === "void") lowerer.badType(typeNode, strict);
+ // An export typed as the empty object literal (tsc's enum output
+ // `exports.E || (exports.E = {})` after its `exports.E = void 0`
+ // preamble): TS's `{}` admits any later non-nullish value and the
+ // export is undefined until then, so it rides checked-dynamic
+ // storage like the same-shaped var binding (varBindingType).
+ if (t.kind === "record" && isJsSourceFile(sf)) {
+ const shape = lowerer.shapes.get(t.shapeId);
+ if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) t = DYN;
+ }
// An immutable export source shares its live storage and native
// callable ABI. Mutable sources need a separate snapshot slot:
// later writes to the binding do not change the exported value.
@@ -1611,43 +1724,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
@@ -1681,6 +1760,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
@@ -2604,6 +2693,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
@@ -2648,6 +2792,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 {
@@ -2811,6 +2963,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-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
index 9997eb7d9..dca835d77 100644
--- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts
+++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
@@ -37,6 +37,7 @@
* statement is not modeled — same as top-level const reads through early
* calls). */
import * as ts from "../ts7/adapter.js";
+import { packageShimFor } from "../package-shims.js";
import type { Lowerer } from "./lowerer.js";
import { boundIdentifiersOf } from "./lowerer.js";
import type { FileParts } from "./lower-modules.js";
@@ -253,6 +254,10 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile
sym = lowerer.checker.getSymbolAtLocation(e.name);
}
if (!sym) return null;
+ // The import binding's own declaration stays in view: a package scriptc
+ // implements itself (package-shims.ts) types through a declaration
+ // file the alias resolves to, while the import compiles the shim.
+ const bindingDecls = sym.declarations;
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
for (const d of sym.declarations) {
if (
@@ -284,6 +289,20 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile
}
}
}
+ for (const d of bindingDecls) {
+ if (!ts.isNamespaceImport(d)) continue;
+ const stmt = d.parent?.parent;
+ if (
+ stmt === undefined ||
+ !ts.isImportDeclaration(stmt) ||
+ !ts.isStringLiteral(stmt.moduleSpecifier)
+ )
+ continue;
+ const from = ts.getSourceFileOfNode(d);
+ if (packageShimFor(from.fileName, stmt.moduleSpecifier.text) === null) continue;
+ const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text);
+ if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep;
+ }
return null;
}
diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts
index afae7dd7b..f0da85bf8 100644
--- a/packages/compiler/src/frontend/lowering/lower-server.ts
+++ b/packages/compiler/src/frontend/lowering/lower-server.ts
@@ -930,6 +930,7 @@ function lowerNetConnectOptions(
let port: IrExpr | null = null;
let host: IrExpr | null = null;
let lookup: IrExpr | null = null;
+ let dynLookup: IrExpr | null = null;
let autoSelect = false;
let attempt: IrExpr | null = null;
let optionThrow: IrExpr | null = null;
@@ -1037,7 +1038,23 @@ function lowerNetConnectOptions(
);
}
lookup = lowerer.lowerExpr(initializer);
- if (!lookupFnShapeOk(lowerer, lookup.type)) {
+ // A lookup of Node's own type (net.LookupFunction: options and a
+ // callback taking an address string or list) crosses as a checked-
+ // dynamic function the runtime calls the way Node's dial does.
+ if (
+ !lookupFnShapeOk(lowerer, lookup.type) &&
+ lookup.type.kind === "func" &&
+ lookup.type.params.length === 3 &&
+ canBoxFuncIntoDyn(
+ lookup.type,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ )
+ ) {
+ dynLookup = { kind: "dynFrom", value: lookup, type: DYN, loc: locOf(initializer) };
+ } else if (lookup.type.kind === "dyn") {
+ dynLookup = lookup;
+ } else if (!lookupFnShapeOk(lowerer, lookup.type)) {
lowerer.noLowering(
`a ${member} 'lookup' option of '${lowerer.fmt(lookup.type)}' values`,
prop,
@@ -1080,6 +1097,22 @@ function lowerNetConnectOptions(
loc,
};
}
+ if (dynLookup !== null) {
+ if (args.length !== 1) {
+ lowerer.noLowering(
+ `${member} with a lookup and a connect listener`,
+ expr,
+ "register the listener separately: socket.once('connect', ...)",
+ );
+ }
+ return {
+ kind: "libCall",
+ fn: "net.connectLookupDyn",
+ args: [port, host, dynLookup],
+ type: NETSOCKET_T,
+ loc,
+ };
+ }
if (lookup !== null) {
if (!autoSelect) {
lowerer.noLowering(
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index d03ba7a0e..a53c0564b 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, isPromisePrototypeRead } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
@@ -7,7 +14,9 @@ import {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
} from "./containers/match-row-uses.js";
-import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
+import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
+import { runtimeKeyPatternRead } from "./expressions/keyed-field-targets.js";
+import { declaresHoistedBinding, staticCondition } from "./static-typeof.js";
import { lowerCollectionSpread } from "./containers/collection-methods.js";
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
@@ -686,7 +695,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 +745,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 +760,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
@@ -1532,6 +1554,20 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
}
if (ts.isIfStatement(stmt)) {
const cond = lowerer.lowerCondition(stmt.expression);
+ // A global-detection condition (static-typeof.ts) folds; its untaken
+ // arm is code Node never runs and does not lower, unless it declares a
+ // hoisted binding.
+ const decided = staticCondition(lowerer, stmt.expression);
+ if (decided !== null) {
+ const taken = decided ? stmt.thenStatement : stmt.elseStatement;
+ const untaken = decided ? stmt.elseStatement : stmt.thenStatement;
+ if (untaken === undefined || !declaresHoistedBinding(untaken)) {
+ const arm = taken === undefined ? [] : lowerer.lowerScopedBlock(taken);
+ return decided
+ ? { kind: "if", cond, then: arm, else_: null, loc: locOf(stmt) }
+ : { kind: "if", cond, then: [], else_: arm, loc: locOf(stmt) };
+ }
+ }
// Aliased-typeof narrows (ms's `var type = typeof val` guard): the
// then-branch lowers under what a TRUE condition proves, the
// else-branch under a FALSE one — the var/let alias narrowing the
@@ -2270,12 +2306,21 @@ export function lowerDestructuringDecl(
// destructure property 'a' of 'f(...)' as it is null.") for the source
// spellings the assignment form reproduces; other sources are refused,
// as in the assignment form. JavaScript files keep their existing
- // per-read errors.
- if (
+ // per-read errors, except that a runtime computed first key checks the
+ // source first: Node throws before it evaluates any key, and V8's bare
+ // message names the source.
+ const jsRuntimeKeyCheck =
init.type.kind === "dyn" &&
ts.isObjectBindingPattern(decl.name) &&
- !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
- lowerer.anyOrigin(decl.initializer)
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ startsWithRuntimeKey(lowerer, decl.name) &&
+ destrSpellingOf(decl.initializer) !== null;
+ if (
+ jsRuntimeKeyCheck ||
+ (init.type.kind === "dyn" &&
+ ts.isObjectBindingPattern(decl.name) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ lowerer.anyOrigin(decl.initializer))
) {
const spelling = destrSpellingOf(decl.initializer);
if (spelling === null) {
@@ -2285,7 +2330,7 @@ export function lowerDestructuringDecl(
"destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call)",
);
}
- const first = decl.name.elements.nodes[0];
+ const first = (decl.name as ts.ObjectBindingPattern).elements.nodes[0];
const firstKey =
first === undefined || first.dotDotDotToken !== undefined
? undefined
@@ -2873,6 +2918,27 @@ export function lowerBindingPattern(
return;
}
if (srcType.kind === "dyn") {
+ // A runtime computed first key evaluates after RequireObjectCoercible:
+ // a nullish source throws before the key's side effects run. Later
+ // keys follow a read that already threw.
+ if (ts.isObjectBindingPattern(pattern) && startsWithRuntimeKey(lowerer, pattern)) {
+ const loc = locOf(pattern);
+ for (const unit of ["null", "undefined"] as const) {
+ out.push({
+ kind: "if",
+ cond: { kind: "dynTest", test: unit, value: srcRef(), type: BOOL, loc },
+ then: [
+ {
+ kind: "exprStmt",
+ expr: nodeThrowExpr(1, "", `Cannot read properties of ${unit}`, VOID, loc),
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ });
+ }
+ }
for (const el of pattern.elements.nodes) {
if (el.name === undefined) continue;
const loc = locOf(el);
@@ -2920,16 +2986,21 @@ export function lowerBindingPattern(
ts.isNumericLiteral(prop)
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
: null;
- if (propName === null) {
+ // A runtime computed key (`{ [name]: v }`) reads the live property.
+ const runtimeRead =
+ propName === null && ts.isComputedPropertyName(prop)
+ ? runtimeKeyPatternRead(lowerer, prop.expression, srcRef(), out, loc)
+ : null;
+ if (propName === null && runtimeRead === null) {
lowerer.unsupported(
"SC1031",
el,
"destructuring with computed keys that do not fold to one property name",
);
}
- let value: IrExpr = {
+ let value: IrExpr = runtimeRead ?? {
kind: "dynKeyGet",
- key: { kind: "strLit", value: propName, type: STRING, loc },
+ key: { kind: "strLit", value: propName!, type: STRING, loc },
value: srcRef(),
type: DYN,
loc,
@@ -3187,11 +3258,22 @@ export function lowerBindingPattern(
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
: null;
if (propName === null) {
- lowerer.unsupported(
- "SC1031",
- el,
- "destructuring with computed keys that do not fold to one property name",
- );
+ // A runtime computed key (`{ [name]: v }`): the keyed read of an
+ // index-signature entry, of the declared field a literal-union key
+ // names, or of a JavaScript record's live property.
+ const runtimeRead = ts.isComputedPropertyName(prop)
+ ? runtimeKeyPatternRead(lowerer, prop.expression, srcRef(), out, locOf(el))
+ : null;
+ if (runtimeRead === null) {
+ lowerer.unsupported(
+ "SC1031",
+ el,
+ "destructuring with computed keys that do not fold to one property name",
+ );
+ }
+ const value = el.initializer ? applyBindingDefault(lowerer, el, runtimeRead) : runtimeRead;
+ lowerer.bindPatternTarget(el.name, value, isLet, out);
+ continue;
}
// An ACCESSOR property (%get:/%set: closure slots): the element's
// read IS a getter call — once, at the element's pattern position
@@ -3467,6 +3549,16 @@ function lowerModuleNamespacePattern(
}
}
+/** Whether a pattern's first element has a computed key that does not
+ * fold to one property name, so the key evaluates at run time. */
+function startsWithRuntimeKey(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean {
+ const first = pattern.elements.nodes[0];
+ const key = first?.dotDotDotToken === undefined ? first?.propertyName : undefined;
+ return (
+ key !== undefined && ts.isComputedPropertyName(key) && patternKeyNameOf(lowerer, key) === null
+ );
+}
+
function patternKeyNameOf(lowerer: Lowerer, prop: ts.PropertyName): string | null {
if (ts.isIdentifier(prop) || ts.isPrivateIdentifier(prop)) return prop.text;
if (ts.isComputedPropertyName(prop)) return lowerer.foldedStringKeyOf(prop.expression);
@@ -5475,7 +5567,29 @@ export function lowerVarDecl(
// `let` may be reassigned a different shape later; genuine `any`
// only — every other unmappable keeps its own diagnostic.
const checkerAny = (lowerer.typeOf(decl.name).flags & ts.TypeFlags.Any) !== 0;
+ // An annotation the checker could not resolve (a package type alias that
+ // reads as `any`) over an empty object literal: the binding is a
+ // dictionary the code fills by key (`files[name] = entry`), which the
+ // empty struct the literal would adopt cannot hold. It holds the
+ // checked-dynamic object instead.
+ let emptyAnyDictionary = false;
+ if (checkerAny && decl.type && !isLet && init.type.kind === "record") {
+ let lit: ts.Expression = decl.initializer;
+ while (ts.isParenthesizedExpression(lit)) lit = lit.expression;
+ const shape = lowerer.shapes.get(init.type.shapeId);
+ if (
+ ts.isObjectLiteralExpression(lit) &&
+ lit.properties.nodes.length === 0 &&
+ shape &&
+ shape.fields.length === 0 &&
+ !shape.indexValue
+ ) {
+ type = DYN;
+ emptyAnyDictionary = true;
+ }
+ }
if (
+ !emptyAnyDictionary &&
(type === null || (type.kind === "dyn" && checkerAny)) &&
!isLet &&
(checkerAny ||
@@ -5926,11 +6040,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 {
@@ -6900,6 +7047,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
@@ -7020,16 +7197,12 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
if (!isRelativeSpecifier(spec) && canonicalBuiltinModule(spec) === null) {
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
if (refusal !== null) {
+ // The same throw as the expression form (cjs-inline-require):
+ // an Error carrying Node's MODULE_NOT_FOUND code.
const loc = locOf(expr);
return {
- kind: "throw",
- value: {
- kind: "libCall",
- fn: "error.new",
- args: [{ kind: "strLit", value: refusal.message, type: STRING, loc }],
- type: { kind: "object", className: "%Error" },
- loc,
- },
+ kind: "exprStmt",
+ expr: nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, VOID, loc),
loc,
};
}
@@ -11435,18 +11608,23 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt
}
}
-/** safe-buffer's `var Buffer = buffer.Buffer` at file scope: a `var` alias
- * of the global Buffer is the same alias plumbing as the const spelling
- * when nothing reassigns it and no code that runs before the declaration
- * can read it (the require-binding early-reference analysis), so the
- * hoisted undefined is unobservable. */
+/** safe-buffer's `var Buffer = buffer.Buffer` (or `var pna = process`, the
+ * process-nextick-args binding once it compiles as process) at file scope:
+ * a `var` alias of a surfaced global other than the global object itself
+ * is the same alias plumbing as the const spelling when nothing reassigns
+ * it and no code that runs before the declaration can read it (the
+ * require-binding early-reference analysis), so the hoisted undefined is
+ * unobservable. */
export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (
!ts.isIdentifier(decl.name) ||
!ts.isVariableDeclarationList(decl.parent) ||
(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ||
decl.initializer === undefined ||
- !bufferNamespaceMember(lowerer, decl.initializer)
+ !(
+ bufferNamespaceMember(lowerer, decl.initializer) ||
+ (stdlibGlobalAliasNameOf(lowerer, decl.initializer) ?? "globalThis") !== "globalThis"
+ )
)
return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index a9759b82f..4c0f484d9 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,
@@ -166,6 +171,7 @@ import { resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
import { tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
+import { isBundledShimForwarder } from "../package-shims.js";
import { erasedTypeOnlyImport } from "../type-only-imports.js";
import { literalValues } from "../literal-values.js";
import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js";
@@ -250,6 +256,7 @@ import {
requireSpecOf,
resolveImport,
workspacePackageOfPath,
+ type CycleEdge,
} from "../program.js";
import {
containsRecord,
@@ -802,6 +809,8 @@ export interface LowerOptions {
startupCrash?: StartupCrash | null;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet | undefined;
+ /** LoadResult.moduleEdges. */
+ moduleEdges?: ReadonlyMap | undefined;
/** LIBRARY mode's reachability roots: the profile-mapped exports of the
* entry module. Executable builds root at the entry's top level alone
* (an unreferenced export dead-strips); a library's exports are called from
@@ -999,11 +1008,15 @@ export function lowerToIr(
),
);
};
+ // Inline require() calls closing a cycle inside the appended subgraphs
+ // join preflight's set (the same run-time refusal by name).
+ const inlineRequireCycleCalls = new Set(options.inlineRequireCycleCalls ?? []);
+ const graph = { edges: options.moduleEdges, inlineCycleCalls: inlineRequireCycleCalls };
for (let pass = 0; pass < 32; pass++) {
const before = moduleOrder.length;
- appendDynamicImportModules(program, moduleOrder, onExtraCycle);
- appendForkModules(program, moduleOrder, forkTargets, onExtraCycle);
- appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle);
+ appendDynamicImportModules(program, moduleOrder, onExtraCycle, graph);
+ appendForkModules(program, moduleOrder, forkTargets, onExtraCycle, graph);
+ appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle, graph);
if (moduleOrder.length === before) break;
if (pass === 31) throw new Error("dynamic import and fork module discovery did not converge");
}
@@ -1018,7 +1031,7 @@ export function lowerToIr(
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1042,7 +1055,7 @@ export function lowerToIr(
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1074,7 +1087,7 @@ export function lowerToIr(
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1118,7 +1131,7 @@ export function lowerToIr(
reachable,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1215,7 +1228,7 @@ export function lowerToIr(
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
targetPlatform,
ffiImports,
libraryCallbacks,
@@ -1833,6 +1846,9 @@ export class Lowerer {
* NaN, which lower to plain doubles (see nan-coded-reads.ts). Settled by
* analyzeRuntimeOptionalArrayReads before any body lowers. */
nanCodedReads: NanCodedReads | null = null;
+ /** Whether any implementation source spells `versions`
+ * (programMentionsVersions in builtins/process.ts). */
+ mentionsProcessVersions: boolean | undefined = undefined;
private stringIndexBoundsCache: StringIndexBounds | null = null;
/** Proofs that a string element read names an existing code unit. The
* indexed-read analysis and expression lowering share one instance, so a
@@ -1848,6 +1864,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
@@ -2076,6 +2095,9 @@ export class Lowerer {
/** Whether some program source can define a thenable
* (programMayDefineThenables). Null until first asked. */
thenablesMayBeDefined: boolean | null = null;
+ /** Lambdas lowered with checked-dynamic parameters (lambdaSignature):
+ * callbacks whose arguments are wider than their contextual overload. */
+ readonly dynParamLambdas = new Set();
/** Whether some server in this program may listen on a Unix socket: a
* listen call whose first argument is a string. Computed once on first
* use (null until then); address() answers the path only when true. */
@@ -2212,6 +2234,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)
@@ -2611,6 +2637,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]