mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* The runtime benchmark workloads: one JSON file per workload in
|
||||
* workloads/, named after the workload, next to the entry programs. A file
|
||||
* holds the workload's entry, args and options; changes that add
|
||||
* different workloads add different files. */
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const CATEGORIES = ["cpu", "startup", "server"];
|
||||
|
||||
/* Server workloads run a listening program; startup-bound ones time
|
||||
* several sequential launches per sample. */
|
||||
export const workloadCategory = (w) =>
|
||||
w.kind === "server" ? "server" : (w.launches ?? 1) > 1 ? "startup" : "cpu";
|
||||
|
||||
/* The workloads of the suite at `suite` (a benchmarks/runtime directory),
|
||||
* by category and then name. A checkout from before the per-workload files
|
||||
* lists them in workloads.json, in its own order. */
|
||||
export function readWorkloads(suite) {
|
||||
const legacy = join(suite, "workloads.json");
|
||||
if (existsSync(legacy)) return JSON.parse(readFileSync(legacy, "utf8")).workloads;
|
||||
const dir = join(suite, "workloads");
|
||||
return readdirSync(dir)
|
||||
.filter((file) => file.endsWith(".json"))
|
||||
.map((file) => ({
|
||||
name: file.slice(0, -".json".length),
|
||||
...JSON.parse(readFileSync(join(dir, file), "utf8")),
|
||||
}))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
CATEGORIES.indexOf(workloadCategory(a)) - CATEGORIES.indexOf(workloadCategory(b)) ||
|
||||
(a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/alloc-trees.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/ast-interp.ts",
|
||||
"args": [
|
||||
"5"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/async-pipeline.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/atomics-contended.ts",
|
||||
"args": [
|
||||
"2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/channel-messages.ts",
|
||||
"args": [
|
||||
"200000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entry": "workloads/cli-config.ts",
|
||||
"input": "cli-config",
|
||||
"launches": 20,
|
||||
"args": [
|
||||
"{input}",
|
||||
"--env",
|
||||
"production",
|
||||
"--max-wave",
|
||||
"12"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/csv-numbers.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/cyclic-graphs.ts",
|
||||
"args": [
|
||||
"3"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/functional-pipeline.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"kind": "server",
|
||||
"entry": "workloads/http-api.ts",
|
||||
"args": [],
|
||||
"client": {
|
||||
"script": "clients/http-load.mjs",
|
||||
"args": [
|
||||
"--connections=64",
|
||||
"--threads=2",
|
||||
"--warmup=4000",
|
||||
"--requests=40000"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"entry": "workloads/intern-table.ts",
|
||||
"args": [
|
||||
"table",
|
||||
"1",
|
||||
"100000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"entry": "workloads/intern-table.ts",
|
||||
"args": [
|
||||
"map",
|
||||
"1",
|
||||
"100000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"entry": "workloads/intern-table.ts",
|
||||
"args": [
|
||||
"map",
|
||||
"8",
|
||||
"100000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"entry": "workloads/intern-table.ts",
|
||||
"args": [
|
||||
"table",
|
||||
"8",
|
||||
"100000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"entry": "../builds/inventory-report/main.ts",
|
||||
"input": "inventory-report",
|
||||
"args": [
|
||||
"{input}"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/json-records.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/log-lines.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"entry": "../builds/log-summary/main.ts",
|
||||
"input": "log-summary",
|
||||
"args": [
|
||||
"{input}",
|
||||
"--min-status",
|
||||
"200",
|
||||
"--limit",
|
||||
"12"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/number-maps.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/numeric-kernels.ts",
|
||||
"args": [
|
||||
"0.5"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/optional-fields.ts",
|
||||
"args": [
|
||||
"10"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/published-graphs.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/record-boundaries.ts",
|
||||
"args": [
|
||||
"8"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/records-sort.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/regex-logs.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/region-builds.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/template-render.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entry": "workloads/thread-slots.ts",
|
||||
"args": [
|
||||
"cache",
|
||||
"8",
|
||||
"50000",
|
||||
"20"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entry": "workloads/thread-slots.ts",
|
||||
"args": [
|
||||
"slots",
|
||||
"8",
|
||||
"50000",
|
||||
"20"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/typed-array-scan-workers.ts",
|
||||
"args": [
|
||||
"3"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/typed-array-scan.ts",
|
||||
"args": [
|
||||
"3"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/unicode-lexer.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// A lexer over a large bundled module whose comments and strings are in
|
||||
// several languages: line starts, tokens by kind and comment text, read
|
||||
// with charCodeAt as hand-written scanners do. Each pass relexes the whole
|
||||
// text, as a bundler or an editor does after a change.
|
||||
const comments = [
|
||||
"// Berechnet die Größe des Puffers für übergroße Einträge",
|
||||
"// Calcule la taille maximale avant la prochaine itération",
|
||||
"// バッファのサイズを計算します",
|
||||
"/* 计算缓冲区大小,然后返回结果 */",
|
||||
"// Returns the cached value (fast path) — see the note above",
|
||||
"/** Résumé: the état of the café's ñandú registry */",
|
||||
];
|
||||
const strings = ['"Grüße"', '"naïve façade"', '"東京"', '"ok ✓"', '"plain ascii"', "'mañana'"];
|
||||
const names = ["buffer", "größe", "index", "resultat", "count", "value", "entry", "table"];
|
||||
|
||||
let seed = 11;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function pick(list: readonly string[]): string {
|
||||
return list[Math.floor(random() * list.length)]!;
|
||||
}
|
||||
|
||||
function makeSource(lines: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < lines; i++) {
|
||||
const r = random();
|
||||
if (r < 0.2) out.push(pick(comments));
|
||||
else if (r < 0.6) out.push(` const ${pick(names)}${i} = ${pick(names)}(${Math.floor(random() * 1000)}, ${pick(strings)});`);
|
||||
else if (r < 0.85) out.push(` if (${pick(names)} >= ${i % 97}) return ${pick(names)}.${pick(names)} + 1;`);
|
||||
else out.push(`function ${pick(names)}${i}(a, b) { return a * b - ${i}; }`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
interface Counts {
|
||||
lines: number;
|
||||
identifiers: number;
|
||||
numbers: number;
|
||||
strings: number;
|
||||
comments: number;
|
||||
commentChars: number;
|
||||
punctuation: number;
|
||||
}
|
||||
|
||||
function isIdentifierStart(ch: number): boolean {
|
||||
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || ch === 95 || ch === 36 || ch > 127;
|
||||
}
|
||||
|
||||
function isIdentifierPart(ch: number): boolean {
|
||||
return isIdentifierStart(ch) || (ch >= 48 && ch <= 57);
|
||||
}
|
||||
|
||||
function lex(text: string): Counts {
|
||||
const counts: Counts = { lines: 1, identifiers: 0, numbers: 0, strings: 0, comments: 0, commentChars: 0, punctuation: 0 };
|
||||
let pos = 0;
|
||||
const end = text.length;
|
||||
while (pos < end) {
|
||||
const ch = text.charCodeAt(pos);
|
||||
if (ch === 10) {
|
||||
counts.lines++;
|
||||
pos++;
|
||||
} else if (ch === 32 || ch === 9 || ch === 13) {
|
||||
pos++;
|
||||
} else if (ch === 47 && text.charCodeAt(pos + 1) === 47) {
|
||||
const start = pos;
|
||||
while (pos < end && text.charCodeAt(pos) !== 10) pos++;
|
||||
counts.comments++;
|
||||
counts.commentChars += pos - start;
|
||||
} else if (ch === 47 && text.charCodeAt(pos + 1) === 42) {
|
||||
const start = pos;
|
||||
pos += 2;
|
||||
while (pos < end && !(text.charCodeAt(pos) === 42 && text.charCodeAt(pos + 1) === 47)) {
|
||||
if (text.charCodeAt(pos) === 10) counts.lines++;
|
||||
pos++;
|
||||
}
|
||||
pos += 2;
|
||||
counts.comments++;
|
||||
counts.commentChars += pos - start;
|
||||
} else if (ch === 34 || ch === 39) {
|
||||
pos++;
|
||||
while (pos < end && text.charCodeAt(pos) !== ch) pos++;
|
||||
pos++;
|
||||
counts.strings++;
|
||||
} else if (ch >= 48 && ch <= 57) {
|
||||
while (pos < end && text.charCodeAt(pos) >= 48 && text.charCodeAt(pos) <= 57) pos++;
|
||||
counts.numbers++;
|
||||
} else if (isIdentifierStart(ch)) {
|
||||
while (pos < end && isIdentifierPart(text.charCodeAt(pos))) pos++;
|
||||
counts.identifiers++;
|
||||
} else {
|
||||
counts.punctuation++;
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function lineStarts(text: string): number[] {
|
||||
const starts = [0];
|
||||
for (let pos = 0; pos < text.length; pos++) if (text.charCodeAt(pos) === 10) starts.push(pos + 1);
|
||||
return starts;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const source = makeSource(60000);
|
||||
const passes = Math.round(6 * scale);
|
||||
let last = lex(source);
|
||||
let total = last.identifiers + lineStarts(source).length;
|
||||
for (let i = 1; i < passes; i++) {
|
||||
last = lex(source);
|
||||
total += last.identifiers + lineStarts(source).length;
|
||||
}
|
||||
console.log("units", source.length, "passes", passes, "total", total);
|
||||
console.log(JSON.stringify(last));
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/validate-errors.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/value-objects.ts",
|
||||
"args": [
|
||||
"2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/value-records.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/word-graph.ts",
|
||||
"args": [
|
||||
"2"
|
||||
]
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dd>Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit <code>SC1010</code> blockers. Relative paths resolve from the current working directory. Accepted files end in <code>.d.ts</code>, <code>.d.mts</code>, or <code>.d.cts</code>.</dd>
|
||||
|
||||
<dt><code>--tier <auto|static-only></code></dt>
|
||||
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today, and a checked cast whose value does not match its type throws instead of continuing with the value. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Package</th><th>Versions</th><th>Compiles as</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>isarray</code></td><td>0.x, 1.x, 2.x</td><td><code>Array.isArray</code></td></tr>
|
||||
<tr><td><code>safe-buffer</code></td><td>5.x</td><td><code>require("buffer")</code></td></tr>
|
||||
<tr><td><code>process-nextick-args</code></td><td>2.x</td><td><code>process</code></td></tr>
|
||||
<tr><td><code>inherits</code></td><td>2.x</td><td><code>require("util").inherits</code></td></tr>
|
||||
<tr><td><code>util-deprecate</code></td><td>1.x</td><td><code>require("util").deprecate</code></td></tr>
|
||||
<tr><td><code>readable-stream</code></td><td>2.x, 3.x, 4.x</td><td><code>require("stream")</code> and its classes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
`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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -178,6 +178,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/buffer-construction.mdx</include>
|
||||
|
||||
### readable-stream
|
||||
|
||||
<include>../limitations/readable-stream.mdx</include>
|
||||
|
||||
### HTTP headers
|
||||
|
||||
<include>../limitations/http-headers.mdx</include>
|
||||
|
||||
@@ -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.
|
||||
@@ -1,5 +1,12 @@
|
||||
Casts from values whose type is only known at runtime validate the value.
|
||||
For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`.
|
||||
A value that matches the type uses the type's native layout.
|
||||
When the cast initializes a `const` or `let` binding declared in a function or block, and the binding is only read by the code after the cast, a value that does not match continues as the value it is, as under Node.js: with `JSON.parse(s) as Config` and `{"port": "eighty"}`, `config.port` is the string `"eighty"`, missing properties read `undefined`, and extra properties stay visible to `Object.keys`, `JSON.stringify`, and `util.inspect`.
|
||||
That code then runs on dynamic values, which is slower than native records.
|
||||
A statement of it that has no dynamic lowering throws a `TypeError` naming the construct and its location when such a value reaches it.
|
||||
|
||||
In the other cases a mismatch throws a catchable `TypeError` identifying the offending path, such as `expected number at $.port, got string`: casts that initialize module-level bindings or appear in other positions, and a mismatched value that leaves the code after its cast through a typed parameter, field, return value, or outer binding, where it is checked against that type.
|
||||
A value that matches there continues, copied if it is an object.
|
||||
`--tier=static-only` makes every mismatch throw at the cast.
|
||||
A TypeScript assertion alone does not perform this validation when run under Node.js.
|
||||
|
||||
Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "ee72820d45a79aa154f0",
|
||||
"rowCount": 3880
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
+7
-5
@@ -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(
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"ambient",
|
||||
"shims",
|
||||
"surface-manifest.json"
|
||||
],
|
||||
"publishConfig": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||
@@ -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<string, AgentEntry>;
|
||||
__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<string, AgentEntry>();
|
||||
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<void> {
|
||||
const closing: Promise<void>[] = [];
|
||||
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<void> {
|
||||
const destroying: Promise<void>[] = [];
|
||||
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(() => {});
|
||||
}
|
||||
}
|
||||
@@ -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<ResponseData> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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<Buffer>;
|
||||
}
|
||||
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<Buffer>;
|
||||
}
|
||||
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 };
|
||||
@@ -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"),
|
||||
};
|
||||
@@ -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<string, string>;
|
||||
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<void> {
|
||||
return Promise.reject(new Error("not implemented"));
|
||||
}
|
||||
|
||||
destroy(_err?: Error | null): Promise<void> {
|
||||
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<ResponseData> {
|
||||
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<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
__destroy(_err: Error): Promise<void> {
|
||||
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<ResponseData> {
|
||||
return new Promise<ResponseData>((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<string, string> = {};
|
||||
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<string, string | string[]> = {};
|
||||
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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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<string, string | string[]> | null;
|
||||
constructor(
|
||||
message?: string,
|
||||
statusCode?: number,
|
||||
headers?: Record<string, string | string[]> | 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<string, string | string[]>;
|
||||
constructor(
|
||||
message: string,
|
||||
code: number,
|
||||
options: { headers?: Record<string, string | string[]>; 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<string, string | string[]> | null;
|
||||
constructor(
|
||||
message: string,
|
||||
code: number,
|
||||
options: { headers?: Record<string, string | string[]> | 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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
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<string, string[]>;
|
||||
|
||||
constructor(from: HeaderList | null) {
|
||||
this.names = from !== null ? from.names.slice() : [];
|
||||
this.values = new Map<string, string[]>();
|
||||
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<string, string> | 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<RawResponse> {
|
||||
return new Promise<RawResponse>((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,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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<Dispatcher, boolean>;
|
||||
__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<Dispatcher, boolean>();
|
||||
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<void> {
|
||||
if (this.__queue.length === 0) {
|
||||
return Promise.all(this.__clients.map((c) => c.close())).then(() => {});
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
this.__closedResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
__destroy(err: Error): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string> {
|
||||
return this.__consume().then((bytes) => decodeText(bytes, this.__profile));
|
||||
}
|
||||
|
||||
// undici declares json() as Promise<any>: 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<any> {
|
||||
return this.__consume().then((bytes) => JSON.parse(decodeText(bytes, this.__profile)));
|
||||
}
|
||||
|
||||
arrayBuffer(): Promise<ArrayBuffer> {
|
||||
return this.__consume().then((bytes) => {
|
||||
const out = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(out).set(bytes);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
bytes(): Promise<Uint8Array> {
|
||||
return this.__consume().then((bytes) => new Uint8Array(bytes));
|
||||
}
|
||||
|
||||
blob(): Promise<Blob> {
|
||||
return this.__consume().then((bytes) => new Blob([bytes], { type: this.__contentType }));
|
||||
}
|
||||
|
||||
formData(): Promise<never> {
|
||||
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<void> {
|
||||
let limit =
|
||||
opts !== undefined && Number.isFinite(opts.limit)
|
||||
? opts.limit
|
||||
: this.__profile.major >= 7
|
||||
? 131072
|
||||
: 262144;
|
||||
this.__consuming = true;
|
||||
return new Promise<void>((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<Buffer> {
|
||||
if (this.bodyUsed) return Promise.reject(new TypeError("unusable"));
|
||||
this.__consuming = true;
|
||||
return new Promise<Buffer>((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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, string | string[] | undefined>;
|
||||
|
||||
/** What a request's `headers` option accepts here: a record or a flat
|
||||
* [name, value, ...] array. */
|
||||
export type UndiciHeaders = Record<string, string | string[] | undefined> | 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;
|
||||
}
|
||||
@@ -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=<s>` 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;
|
||||
}
|
||||
@@ -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<ResponseData> {
|
||||
return coreRequest(url, options, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
export function fetch(input: string | URL, init?: UndiciRequestInit): Promise<Response> {
|
||||
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 };
|
||||
@@ -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<ResponseData> {
|
||||
return coreRequest(url, options, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
export function fetch(input: string | URL, init?: UndiciRequestInit): Promise<Response> {
|
||||
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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
mangleCtorThunk,
|
||||
mangleFunction,
|
||||
mangleGlobal,
|
||||
mangleVtArray,
|
||||
mangleVtInstance,
|
||||
mangleVtStruct,
|
||||
} from "../mangle.js";
|
||||
@@ -81,6 +82,12 @@ export class LlClassMeta {
|
||||
* member's struct carries the i32 slot word before its fields
|
||||
* (classSlotWordIndex). */
|
||||
threadSlot = false;
|
||||
/** A member of a program-rooted hierarchy on a 64-bit target: instead of
|
||||
* a vtable word, the rc word holds the class id (classIdRcWord) in its
|
||||
* high 32 bits (CLASS_ID_OFFSET) and the reference count in its low 32
|
||||
* bits, and the hierarchy's vtables are one array indexed by the id
|
||||
* (mangleVtArray). */
|
||||
idInRc = false;
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
@@ -96,6 +103,7 @@ export class LlClassMeta {
|
||||
export function buildClassGraph(
|
||||
mod: IrModule,
|
||||
fnByName: Map<string, IrFunction>,
|
||||
sizeType: "i32" | "i64" = "i64",
|
||||
): Map<string, LlClassMeta> {
|
||||
const metaMap = new Map<string, LlClassMeta>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
@@ -122,6 +130,11 @@ export function buildClassGraph(
|
||||
if (meta.base === null) number(meta, meta);
|
||||
meta.hierarchy = meta.base !== null || meta.children.length > 0;
|
||||
}
|
||||
// The runtime reads the vtable word of runtime-rooted hierarchies (Error,
|
||||
// EventEmitter, EventTarget, Event and the streams) and 32-bit targets
|
||||
// have no spare half in the rc word: those keep the vtable word.
|
||||
for (const meta of metaMap.values())
|
||||
meta.idInRc = sizeType === "i64" && meta.hierarchy && meta.root.def.runtime !== true;
|
||||
const slotRoots = new Set<LlClassMeta>();
|
||||
for (const key of mod.threadSlotKeys ?? []) {
|
||||
const meta = metaMap.get(key);
|
||||
@@ -336,11 +349,12 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null {
|
||||
}
|
||||
|
||||
/** The GEP index after a class's header words: rc at 0, the vtable word
|
||||
* at 1 on hierarchy members, then a runtime prefix (state + display name)
|
||||
* at 1 on hierarchy members that do not keep their class id in the rc
|
||||
* word (LlClassMeta.idInRc), then a runtime prefix (state + display name)
|
||||
* on classes rooted at a runtime base, then the stream-state slot on
|
||||
* stream-rooted ones. */
|
||||
function headerEnd(meta: LlClassMeta): number {
|
||||
if (!meta.hierarchy) return 1;
|
||||
if (!meta.hierarchy || meta.idInRc) return 1;
|
||||
if (streamRooted(meta)) return 5;
|
||||
return runtimePrefixOf(meta) ? 4 : 2;
|
||||
}
|
||||
@@ -445,6 +459,69 @@ export function programLifetimeRc(sizeType: "i32" | "i64"): string {
|
||||
return sizeType === "i32" ? "-2147483648" : "-9223372036854775808";
|
||||
}
|
||||
|
||||
/** Lines that load the preorder number of the dynamic class of `obj`, an
|
||||
* instance of `meta`'s hierarchy whose struct is `struct`, into `pre` (a
|
||||
* `sizeType`): from the high half of the rc word (LlClassMeta.idInRc), or
|
||||
* from the vtable its vtable word points at. */
|
||||
export function classPreLines(
|
||||
meta: LlClassMeta,
|
||||
struct: string,
|
||||
obj: string,
|
||||
pre: string,
|
||||
sizeType: "i32" | "i64",
|
||||
): string[] {
|
||||
if (meta.idInRc)
|
||||
return [
|
||||
` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${pre}.id = load i32, ptr ${pre}.idp`,
|
||||
` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`,
|
||||
` ${pre} = zext i32 ${pre}.n to ${sizeType}`,
|
||||
];
|
||||
return [
|
||||
` ${pre}.vtp = getelementptr inbounds %${struct}, ptr ${obj}, i64 0, i32 1`,
|
||||
` ${pre}.vt = load ptr, ptr ${pre}.vtp`,
|
||||
` ${pre}.prep = getelementptr inbounds %ScrVt, ptr ${pre}.vt, i64 0, i32 0`,
|
||||
` ${pre} = load ${sizeType}, ptr ${pre}.prep`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Lines that load the class id of the LlClassMeta.idInRc instance `obj`
|
||||
* (`group` adds !invariant.group to the load) and point `vt` at its class's
|
||||
* element of the hierarchy's vtable array. */
|
||||
export function classIdVtLines(
|
||||
meta: LlClassMeta,
|
||||
obj: string,
|
||||
vt: string,
|
||||
group: string,
|
||||
): string[] {
|
||||
const t = `${vt}.`;
|
||||
const root = meta.root;
|
||||
const vtt = `%${mangleVtStruct(root.def.name)}`;
|
||||
const n = root.post - root.pre + 1;
|
||||
return [
|
||||
` ${t}idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${t}id = load i32, ptr ${t}idp${group}`,
|
||||
` ${t}ix = sub i32 ${t}id, ${root.pre + 1}`,
|
||||
` ${t}ix64 = zext i32 ${t}ix to i64`,
|
||||
` ${vt} = getelementptr inbounds [${n} x ${vtt}], ptr @${mangleVtArray(root.def.name)}, i64 0, i64 ${t}ix64`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The byte offset of the class id in the rc word of an instance that
|
||||
* keeps it there (LlClassMeta.idInRc): the high half of the little-endian
|
||||
* 64-bit word. The count is the i32 at offset 0. */
|
||||
export const CLASS_ID_OFFSET = 4;
|
||||
|
||||
/** The rc word an LlClassMeta.idInRc instance is created with: the class
|
||||
* id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits
|
||||
* a count of 1, or for a program-lifetime instance the midpoint of the 32-bit
|
||||
* count (as programLifetimeRc is of the 32-bit word). The id is never 0,
|
||||
* 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so
|
||||
* the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */
|
||||
export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
|
||||
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
|
||||
}
|
||||
|
||||
function undefFieldInits(
|
||||
host: ClassHost,
|
||||
meta: LlClassMeta,
|
||||
@@ -524,17 +601,18 @@ export function emitClassShapes(
|
||||
// ScrEmitter* / ScrStream* / ScrEventTargetObj* / ScrEventObj* are the
|
||||
// usual pointer reinterprets.
|
||||
const runtimePrefix = runtimePrefixOf(meta);
|
||||
const prefix = meta.hierarchy
|
||||
? streamRooted(meta)
|
||||
? ["ptr", "ptr", "ptr", "ptr"]
|
||||
: runtimePrefix
|
||||
? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const prefix =
|
||||
meta.hierarchy && !meta.idInRc
|
||||
? streamRooted(meta)
|
||||
? ["ptr", "ptr", "ptr", "ptr"]
|
||||
: runtimePrefix
|
||||
? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys];
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
|
||||
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
|
||||
`; class ${cls.name}${meta.idInRc ? " (class id in rc)" : meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,18 +637,40 @@ export function emitClassShapes(
|
||||
// One constant vtable instance per emitted hierarchy class: interval,
|
||||
// direct release, and the class's dispatch entry for every slot — the
|
||||
// method functions THEMSELVES (no adapters; see the header comment).
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
if (!meta.hierarchy) continue;
|
||||
const vtInit = (meta: LlClassMeta): string => {
|
||||
const entries = vtEntriesFor(meta).map(({ slot, impl }) =>
|
||||
impl === null
|
||||
? `ptr null` // outside the declaring subtree / fully-abstract chain
|
||||
: `ptr @${host.virtualEntry?.(`%${impl.def.name}.${slot.method}`) ?? mangleFunction(`%${impl.def.name}.${slot.method}`)}`,
|
||||
);
|
||||
const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(cls.name)} }`;
|
||||
const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(meta.def.name)} }`;
|
||||
return `{ ${[head, ...entries].join(", ")} }`;
|
||||
};
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
if (!meta.hierarchy || meta.idInRc) continue;
|
||||
defs.push(
|
||||
`@${mangleVtInstance(cls.name)} = internal constant %${mangleVtStruct(meta.root.def.name)} ` +
|
||||
`{ ${[head, ...entries].join(", ")} } ; class ${cls.name}`,
|
||||
`${vtInit(meta)} ; class ${cls.name}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
// A hierarchy whose instances keep their class id in the rc word has
|
||||
// one array of its classes' vtables, in preorder: the vtable of an
|
||||
// instance is the element at its number minus the root's.
|
||||
for (const name of roots) {
|
||||
const root = metaMap.get(name)!;
|
||||
if (!root.idInRc) continue;
|
||||
const members = [...metaMap.values()]
|
||||
.filter((m) => m.root === root)
|
||||
.sort((a, b) => a.pre - b.pre);
|
||||
const vtt = `%${mangleVtStruct(root.def.name)}`;
|
||||
defs.push(
|
||||
`@${mangleVtArray(root.def.name)} = internal constant [${members.length} x ${vtt}] [`,
|
||||
...members.map(
|
||||
(m, i) => ` ${vtt} ${vtInit(m)}${i + 1 < members.length ? "," : ""} ; class ${m.def.name}`,
|
||||
),
|
||||
`]`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
@@ -605,6 +705,7 @@ export function emitClassShapes(
|
||||
release: `@${release}`,
|
||||
destroy: `${teardown}_destroy`,
|
||||
traced: host.tracedShapes.has(`object:${cls.name}`),
|
||||
...(meta.idInRc ? { count: "i32" as const } : {}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -658,7 +759,13 @@ export function emitClassShapes(
|
||||
|
||||
// retain: NULL-tolerant, immortal-skip — layout-generic (rc at 0), so
|
||||
// hierarchy members need no dispatch.
|
||||
defs.push(...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime), ``);
|
||||
// An instance that keeps its class id in the rc word counts in the
|
||||
// low half (CLASS_ID_OFFSET).
|
||||
const count = meta.idInRc ? "i32" : host.sizeType;
|
||||
defs.push(
|
||||
...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime, count),
|
||||
``,
|
||||
);
|
||||
|
||||
// An out-of-line teardown tears typed children down directly
|
||||
// (directChildReleaseLines). It takes the chain length when it has such
|
||||
@@ -714,6 +821,7 @@ export function emitClassShapes(
|
||||
traced,
|
||||
`release ${cls.name}`,
|
||||
lifetime,
|
||||
count,
|
||||
),
|
||||
``,
|
||||
);
|
||||
@@ -723,12 +831,16 @@ export function emitClassShapes(
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load ${host.sizeType}, ptr %o`,
|
||||
` %imm = icmp eq ${host.sizeType} %rc, -1`,
|
||||
` %rc = load ${count}, ptr %o`,
|
||||
` %imm = icmp eq ${count} %rc, -1`,
|
||||
` br i1 %imm, label %done, label %disp`,
|
||||
`disp:`,
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
...(meta.idInRc
|
||||
? classIdVtLines(meta, "%o", "%vt", "")
|
||||
: [
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
]),
|
||||
` %relp = getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2`,
|
||||
` %rel = load ptr, ptr %relp`,
|
||||
` call void %rel(ptr %o) ; the DYNAMIC class's teardown`,
|
||||
@@ -743,10 +855,10 @@ export function emitClassShapes(
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %rc = load ${host.sizeType}, ptr %o`,
|
||||
` %n = sub ${host.sizeType} %rc, 1`,
|
||||
` store ${host.sizeType} %n, ptr %o`,
|
||||
` %dead = icmp eq ${host.sizeType} %n, 0`,
|
||||
` %rc = load ${count}, ptr %o`,
|
||||
` %n = sub ${count} %rc, 1`,
|
||||
` store ${count} %n, ptr %o`,
|
||||
` %dead = icmp eq ${count} %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
@@ -815,10 +927,22 @@ export function emitClassShapes(
|
||||
host,
|
||||
sizeOf,
|
||||
traced && !lifetime ? { type: cycType } : null,
|
||||
meta.hierarchy ? 16 : 8,
|
||||
meta.hierarchy && !meta.idInRc ? 16 : 8,
|
||||
),
|
||||
);
|
||||
nw.push(` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`);
|
||||
if (meta.idInRc) {
|
||||
// The class id in the high half of the rc word, the count in the low
|
||||
// half (classIdRcWord). It never changes while the object lives:
|
||||
// retains, releases and the runtime write only the low half.
|
||||
host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`);
|
||||
nw.push(
|
||||
` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`,
|
||||
` store i64 ${classIdRcWord(meta, lifetime)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`,
|
||||
);
|
||||
} else
|
||||
nw.push(
|
||||
` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`,
|
||||
);
|
||||
if (lifetime && audit) {
|
||||
// --sanitize builds: the RC audit skips its leak report when the
|
||||
// program keeps program-lifetime objects (scr_console.c), and
|
||||
@@ -826,7 +950,7 @@ export function emitClassShapes(
|
||||
host.declare(`declare void @scr_obj_lifetime_note(ptr)`);
|
||||
nw.push(` call void @scr_obj_lifetime_note(ptr %o)`);
|
||||
}
|
||||
if (meta.hierarchy) {
|
||||
if (meta.hierarchy && !meta.idInRc) {
|
||||
// The vtable word is written once, here, and never changes while the
|
||||
// object lives: its store and the virtual-call loads carry
|
||||
// !invariant.group. A recycled block may have held an object of
|
||||
|
||||
@@ -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<LlvmEmitterContext, "B" | "sizeType">,
|
||||
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<LlvmEmitterContext, "B" | "sizeType">,
|
||||
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<LlvmEmitterContext, "B" | "sizeType">,
|
||||
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<LlvmEmitterContext, "B" | "sizeType">,
|
||||
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<LlvmEmitterContext, "B" | "sizeType">,
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<DenseGuard, "offset" | "states">,
|
||||
): 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<DenseGuard, "offset" | "data">,
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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), ``);
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -374,7 +374,7 @@ export function emitSerializationExpr(
|
||||
// falls back to exactly this route whenever the fast path declines.
|
||||
const parsed = e.value;
|
||||
if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") {
|
||||
const fused = host.dyn.jsonParseHelper(e.type);
|
||||
const fused = host.dyn.jsonParseHelper(e.type, e.fallback === true);
|
||||
if (fused) {
|
||||
const text = emitBorrowedInput(host, parsed.args[0]!);
|
||||
const r = B.tmp();
|
||||
@@ -400,6 +400,12 @@ export function emitSerializationExpr(
|
||||
B.line(
|
||||
`${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`,
|
||||
);
|
||||
// A fallback site: a value that fails validation parks as the
|
||||
// checked value instead of throwing (dynCheck.fallback).
|
||||
if (e.fallback) {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`);
|
||||
B.line(`call zeroext i1 @scr_dyn_check_recover(ptr ${dynV.name})`);
|
||||
}
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "../mangle.js";
|
||||
import {
|
||||
classEnvironmentIndex,
|
||||
classIdVtLines,
|
||||
classStructSym,
|
||||
vtEntriesFor,
|
||||
type LlClassMeta,
|
||||
@@ -1193,15 +1194,20 @@ export function emitCallExpr(
|
||||
if (!borrowed.has(index)) host.toFrame(a);
|
||||
});
|
||||
const recv = args[0]!.name;
|
||||
const vtp = B.tmp();
|
||||
const vt = B.tmp();
|
||||
const fnp = B.tmp();
|
||||
const fn = B.tmp();
|
||||
B.line(
|
||||
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
|
||||
);
|
||||
// Set once at allocation (classes.ts launders each new object).
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
|
||||
if (meta.idInRc)
|
||||
for (const line of classIdVtLines(meta, recv, vt, ", !invariant.group !{}"))
|
||||
B.line(line.trimStart());
|
||||
else {
|
||||
const vtp = B.tmp();
|
||||
B.line(
|
||||
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
|
||||
}
|
||||
B.line(
|
||||
`${fnp} = getelementptr inbounds %${mangleVtStruct(meta.root.def.name)}, ptr ${vt}, i64 0, i32 ${slotIdx + 1}`,
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -865,6 +865,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "blob":
|
||||
case "wasm":
|
||||
case "record":
|
||||
case "checkedCast":
|
||||
case "disposable":
|
||||
case "punycode":
|
||||
case "constants":
|
||||
|
||||
@@ -374,6 +374,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.globStrings": "scr_fsp_glob_strings",
|
||||
"fsp.globError": "scr_fsp_glob_error",
|
||||
"fs.realpathSync": "scr_fs_realpath",
|
||||
"fs.realpathValue": "scr_fs_realpath_value",
|
||||
"fs.realpathNativeSync": "scr_fs_realpath_promise",
|
||||
"fs.unlinkSync": "scr_fs_unlink",
|
||||
"fs.chmodSync": "scr_fs_chmod",
|
||||
@@ -437,6 +438,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.concatDyn": "scr_buffer_concat_dyn",
|
||||
"buffer.concatLenDyn": "scr_buffer_concat_len_dyn",
|
||||
"buffer.alloc": "scr_buffer_alloc",
|
||||
"buffer.allocValue": "scr_buffer_alloc_value",
|
||||
"buffer.allocUnsafe": "scr_buffer_alloc_unsafe",
|
||||
"buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow",
|
||||
"buffer.poolFromStr": "scr_buffer_from_str",
|
||||
@@ -444,9 +446,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.poolFromBytes": "scr_buffer_from_bytes",
|
||||
"buffer.poolFromArray": "scr_buffer_from_arr",
|
||||
"buffer.poolFromDyn": "scr_buffer_from_dyn_pooled",
|
||||
"buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled",
|
||||
"buffer.poolSize": "scr_buffer_pool_size",
|
||||
"buffer.setPoolSize": "scr_buffer_set_pool_size",
|
||||
"buffer.byteLenDyn": "scr_buffer_byte_length_dyn",
|
||||
"buffer.byteLenValue": "scr_buffer_byte_length_value",
|
||||
"buffer.byteLenStr": "scr_bytes_byte_length_str",
|
||||
"buffer.isEncoding": "scr_bytes_is_encoding",
|
||||
// The checked-dynamic compare/equals validators (scr_bytes_io.c):
|
||||
@@ -472,6 +476,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"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<string, string> = {
|
||||
"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<string, string> = {
|
||||
"insp.circular": "scr_insp_circular",
|
||||
// Undeclared source properties kept beside a checked record (scr_json.c).
|
||||
"record.sourceHas": "scr_record_source_has",
|
||||
// Checked-cast fallback reads (scr_json.c); take answers +1.
|
||||
"checkedCast.fallbackTaken": "scr_dyn_check_fallback_taken",
|
||||
"checkedCast.fallbackTake": "scr_dyn_check_fallback_take",
|
||||
// WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c):
|
||||
// constructions +1; url.new, the fileURLToPath pair, the win32
|
||||
// pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw).
|
||||
@@ -1688,6 +1699,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"net.connect",
|
||||
"net.connectCb",
|
||||
"net.connectLookup",
|
||||
"net.connectLookupDyn",
|
||||
"net.connectAttempt",
|
||||
"fs.existsChk",
|
||||
"fs.callbackValue",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user