mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Merge main
This commit is contained in:
@@ -217,17 +217,22 @@ jobs:
|
||||
# worker budget covers both processes; file and case shard axes stay
|
||||
# separate so every fixture still runs exactly once in each lane.
|
||||
# The tests run in a transient scope capped below the runner's 16 GB, so
|
||||
# the runner itself keeps memory: past 14 GB of RAM the scope swaps, as
|
||||
# the whole runner did before (a sanitized shard peaks near 16 GB), and
|
||||
# when RAM and swap run out the kernel kills the scope's largest process.
|
||||
# the runner itself keeps memory: past 15 GB of RAM the scope swaps, as
|
||||
# the whole runner did before, and when RAM and swap run out the kernel
|
||||
# kills the scope's largest process. A sanitized shard peaks near 16 GB,
|
||||
# and the sanitized self-hosting native-frontend build alone needs 15 GB
|
||||
# (its clang compile of the frontend takes 10.5 GB).
|
||||
# OOMPolicy=continue leaves the rest of the scope running, so vitest
|
||||
# reports the killed worker and the job log names the test.
|
||||
# reports the killed worker and the job log names the test. Node sizes
|
||||
# its default heap from the memory it may use, which the scope lowers
|
||||
# below 4 GB; the workers keep the 4 GB heap they had on the bare
|
||||
# runner.
|
||||
- name: Tests (corpus and integration suites)
|
||||
run: |
|
||||
ulimit -c 0
|
||||
if [ -d /run/systemd/system ]; then
|
||||
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=14G -p OOMPolicy=continue \
|
||||
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" node scripts/ci-test.mjs
|
||||
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=15G -p OOMPolicy=continue \
|
||||
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" NODE_OPTIONS="--max-old-space-size=4096" node scripts/ci-test.mjs
|
||||
fi
|
||||
node scripts/ci-test.mjs
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `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 |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` |
|
||||
| `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 |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/json-unions.ts",
|
||||
"args": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// JSON text parsed into discriminated unions: an event log with eight kinds
|
||||
// of event and a nested expression tree with nine kinds of node, cast with
|
||||
// `JSON.parse(text) as T`, then aggregated and evaluated. Argument: the
|
||||
// number of rounds.
|
||||
type Event =
|
||||
| { kind: "click"; target: string; x: number; y: number }
|
||||
| { key: string; kind: "key"; repeat: boolean }
|
||||
| { kind: "scroll"; offset: number }
|
||||
| { kind: "resize"; height: number; width: number }
|
||||
| { kind: "focus"; target: string }
|
||||
| { kind: "blur"; target: string }
|
||||
| { kind: "load"; ms: number; url: string }
|
||||
| { code: number; kind: "error"; message: string };
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "var"; name: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add"; left: Expr; right: Expr }
|
||||
| { kind: "sub"; left: Expr; right: Expr }
|
||||
| { kind: "mul"; left: Expr; right: Expr }
|
||||
| { kind: "max"; items: Expr[] }
|
||||
| { else: Expr; kind: "if"; test: Expr; then: Expr }
|
||||
| { body: Expr; kind: "let"; name: string; value: Expr };
|
||||
|
||||
let seed = 12345;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
const targets = ["menu", "search", "button", "list", "footer"];
|
||||
function eventText(i: number): string {
|
||||
const target = targets[i % targets.length]!;
|
||||
switch (i % 8) {
|
||||
case 0:
|
||||
return `{"kind":"click","target":"${target}","x":${i % 640},"y":${i % 480}}`;
|
||||
case 1:
|
||||
return `{"key":"k${i % 26}","kind":"key","repeat":${i % 3 === 0}}`;
|
||||
case 2:
|
||||
return `{"kind":"scroll","offset":${(i * 7) % 2000}}`;
|
||||
case 3:
|
||||
return `{"height":${400 + (i % 300)},"kind":"resize","width":${600 + (i % 500)}}`;
|
||||
case 4:
|
||||
return `{"kind":"focus","target":"${target}"}`;
|
||||
case 5:
|
||||
return `{"kind":"blur","target":"${target}"}`;
|
||||
case 6:
|
||||
return `{"kind":"load","ms":${i % 900},"url":"/page/${i % 50}"}`;
|
||||
default:
|
||||
return `{"code":${400 + (i % 100)},"kind":"error","message":"failed ${i % 13}"}`;
|
||||
}
|
||||
}
|
||||
|
||||
const names = ["a", "b", "c", "d"];
|
||||
function exprText(depth: number): string {
|
||||
const pick = depth <= 0 ? Math.floor(random() * 2) : Math.floor(random() * 9);
|
||||
switch (pick) {
|
||||
case 0:
|
||||
return `{"kind":"num","value":${Math.floor(random() * 100)}}`;
|
||||
case 1:
|
||||
return `{"kind":"var","name":"${names[Math.floor(random() * names.length)]!}"}`;
|
||||
case 2:
|
||||
return `{"kind":"neg","operand":${exprText(depth - 1)}}`;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5: {
|
||||
const kind = pick === 3 ? "add" : pick === 4 ? "sub" : "mul";
|
||||
return `{"kind":"${kind}","left":${exprText(depth - 1)},"right":${exprText(depth - 1)}}`;
|
||||
}
|
||||
case 6:
|
||||
return `{"items":[${exprText(depth - 1)},${exprText(depth - 2)},${exprText(depth - 2)}],"kind":"max"}`;
|
||||
case 7:
|
||||
return `{"else":${exprText(depth - 2)},"kind":"if","test":${exprText(depth - 1)},"then":${exprText(depth - 2)}}`;
|
||||
default:
|
||||
return `{"body":${exprText(depth - 1)},"kind":"let","name":"${names[Math.floor(random() * names.length)]!}","value":${exprText(depth - 2)}}`;
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(e: Expr, env: Map<string, number>): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "var":
|
||||
return env.get(e.name) ?? 1;
|
||||
case "neg":
|
||||
return -evaluate(e.operand, env);
|
||||
case "add":
|
||||
return (evaluate(e.left, env) + evaluate(e.right, env)) % 1000003;
|
||||
case "sub":
|
||||
return (evaluate(e.left, env) - evaluate(e.right, env)) % 1000003;
|
||||
case "mul":
|
||||
return (evaluate(e.left, env) * evaluate(e.right, env)) % 1000003;
|
||||
case "max": {
|
||||
let best = -Infinity;
|
||||
for (const item of e.items) best = Math.max(best, evaluate(item, env));
|
||||
return best;
|
||||
}
|
||||
case "if":
|
||||
return evaluate(e.test, env) > 0 ? evaluate(e.then, env) : evaluate(e.else, env);
|
||||
case "let": {
|
||||
const inner = new Map(env);
|
||||
inner.set(e.name, evaluate(e.value, env));
|
||||
return evaluate(e.body, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "6");
|
||||
const eventsText = `[${Array.from({ length: 60000 }, (_, i) => eventText(i)).join(",")}]`;
|
||||
const treesText = `[${Array.from({ length: 400 }, () => exprText(9)).join(",")}]`;
|
||||
let clicks = 0;
|
||||
let keys = 0;
|
||||
let scrolled = 0;
|
||||
let area = 0;
|
||||
let loadMs = 0;
|
||||
let errors = 0;
|
||||
let focus = 0;
|
||||
let total = 0;
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const events = JSON.parse(eventsText) as Event[];
|
||||
for (const event of events) {
|
||||
switch (event.kind) {
|
||||
case "click":
|
||||
clicks += event.x + event.y;
|
||||
break;
|
||||
case "key":
|
||||
if (event.repeat) keys++;
|
||||
break;
|
||||
case "scroll":
|
||||
scrolled += event.offset;
|
||||
break;
|
||||
case "resize":
|
||||
area += (event.width * event.height) % 1000;
|
||||
break;
|
||||
case "focus":
|
||||
case "blur":
|
||||
focus += event.target.length;
|
||||
break;
|
||||
case "load":
|
||||
loadMs += event.ms;
|
||||
break;
|
||||
case "error":
|
||||
errors += event.code % 7;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const trees = JSON.parse(treesText) as Expr[];
|
||||
const env = new Map([
|
||||
["a", 3],
|
||||
["b", 5],
|
||||
]);
|
||||
for (const tree of trees) total = (total + evaluate(tree, env)) % 1000003;
|
||||
}
|
||||
console.log(clicks, keys, scrolled, area, loadMs, errors, focus, total);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/keyed-option-writes.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Class methods whose names are also keys of option records written through
|
||||
// element accesses: `enabled[name] = true` over a union of literal names and
|
||||
// `totals["count"] = n`. The writes store booleans and numbers into plain
|
||||
// records, so the methods keep their native calls.
|
||||
type Step = "scale" | "offset" | "clamp";
|
||||
|
||||
const enabled: Record<Step, boolean> = { scale: false, offset: false, clamp: false };
|
||||
function enable(name: Step): void {
|
||||
enabled[name] = true;
|
||||
}
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
|
||||
class Transform {
|
||||
private readonly factor: number;
|
||||
private readonly shift: number;
|
||||
constructor(factor: number, shift: number) {
|
||||
this.factor = factor;
|
||||
this.shift = shift;
|
||||
}
|
||||
scale(x: number): number {
|
||||
return enabled.scale ? x * this.factor : x;
|
||||
}
|
||||
offset(x: number): number {
|
||||
return enabled.offset ? x + this.shift : x;
|
||||
}
|
||||
clamp(x: number): number {
|
||||
if (!enabled.clamp) return x;
|
||||
return x < 0 ? 0 : x > 1000 ? 1000 : x;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter {
|
||||
private seen = 0;
|
||||
private sum = 0;
|
||||
add(value: number): void {
|
||||
this.seen++;
|
||||
this.sum += value;
|
||||
}
|
||||
count(): number {
|
||||
return this.seen;
|
||||
}
|
||||
mean(): number {
|
||||
return this.seen === 0 ? 0 : this.sum / this.seen;
|
||||
}
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const steps: Step[] = ["scale", "offset", "clamp"];
|
||||
for (const step of steps) enable(step);
|
||||
|
||||
const transforms: Transform[] = [];
|
||||
for (let i = 0; i < 16; i++) transforms.push(new Transform(1 + (i % 5) * 0.25, (i % 7) - 3));
|
||||
const counters: Counter[] = [];
|
||||
for (let i = 0; i < 8; i++) counters.push(new Counter());
|
||||
|
||||
let seed = 12345;
|
||||
let checksum = 0;
|
||||
const rounds = Math.floor(2_000_000 * scale);
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
const t = transforms[seed & 15]!;
|
||||
const value = t.clamp(t.offset(t.scale((seed >>> 8) % 1200)));
|
||||
const counter = counters[(seed >>> 4) & 7]!;
|
||||
counter.add(value);
|
||||
checksum += value;
|
||||
if ((i & 65535) === 0) totals["count"] = counter.count();
|
||||
}
|
||||
|
||||
let mean = 0;
|
||||
let seen = 0;
|
||||
for (const counter of counters) {
|
||||
mean += counter.mean();
|
||||
seen += counter.count();
|
||||
}
|
||||
console.log("seen", seen, "count", totals["count"], "mean", (mean / counters.length).toFixed(4), "checksum", checksum);
|
||||
@@ -4,4 +4,5 @@
|
||||
The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`.
|
||||
Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer.
|
||||
`zlib.constants` members and their module-level copies read as Node's values.
|
||||
`util.inspect`, `instanceof zlib.Gzip`, and Brotli and Zstd compression are unsupported.
|
||||
The Brotli one-shots `brotliCompress` and `brotliDecompress` and their `Sync` forms, and the `BrotliCompress` and `BrotliDecompress` streams, accept Node's options, including `params` and `dictionary`, and produce the same bytes as Node.
|
||||
`util.inspect` and Zstd compression are unsupported.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
- **URL:** Hosts of special URLs that hold non-ASCII text, or percent-escapes that decode to it, throw `ERR_INVALID_URL`, and `xn--` labels are kept without Punycode validation.
|
||||
Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
|
||||
- **URL:** Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
|
||||
`URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors.
|
||||
`href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code.
|
||||
Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
|
||||
A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
|
||||
Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
|
||||
JavaScript object spreads copy represented own symbol fields.
|
||||
Equal registry keys share a field across modules.
|
||||
|
||||
@@ -18268,9 +18268,9 @@
|
||||
"anchor": "err_brotli_invalid_param",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84302,9 +84302,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84325,9 +84325,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84349,7 +84349,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84372,7 +84372,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84395,7 +84395,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84418,7 +84418,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84441,7 +84441,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84464,7 +84464,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84487,7 +84487,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84510,7 +84510,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84533,7 +84533,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84556,7 +84556,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84579,7 +84579,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84602,7 +84602,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84625,7 +84625,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84716,9 +84716,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84739,9 +84739,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84969,9 +84969,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84992,9 +84992,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85015,9 +85015,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85038,9 +85038,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85499,7 +85499,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -85521,9 +85521,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85567,9 +85567,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85590,9 +85590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85613,9 +85613,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85636,9 +85636,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32679,6 +32679,7 @@
|
||||
"tests": [
|
||||
"tests/corpus/6702-ecdh-errors.js",
|
||||
"tests/corpus/6708-dh-errors.js",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/caught-system-error-fields.ts",
|
||||
"tests/corpus/error-stack-frames.ts",
|
||||
"tests/corpus/error-stack-limit.ts",
|
||||
@@ -33471,8 +33472,11 @@
|
||||
"anchor": "err_brotli_invalid_param",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:errors.ERR_BROTLI_INVALID_PARAM",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -115760,7 +115764,10 @@
|
||||
"tests/corpus/5377-zlib-stream-backpressure.cjs",
|
||||
"tests/corpus/5378-zlib-sync-options.cjs",
|
||||
"tests/corpus/5379-zlib-callback-options.ts",
|
||||
"tests/corpus/5415-zlib-dynamic-inputs.cjs"
|
||||
"tests/corpus/5415-zlib-dynamic-inputs.cjs",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -116243,8 +116250,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:zlib.zlib.BrotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -116266,8 +116276,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:zlib.zlib.BrotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -116785,8 +116799,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.createBrotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -116808,8 +116825,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.createBrotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117115,8 +117136,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117138,8 +117162,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117161,8 +117188,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117184,8 +117215,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -118202,8 +118237,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:zlib"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:zlib.BrotliOptions",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -118271,8 +118309,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -118294,8 +118335,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -118317,8 +118361,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -118340,8 +118388,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
|
||||
@@ -652,7 +652,14 @@ function publicDetail(tier) {
|
||||
source,
|
||||
)
|
||||
) {
|
||||
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.";
|
||||
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.";
|
||||
}
|
||||
if (
|
||||
/^compiler-feature:zlib\.(?:zlib\.(?:BrotliCompress|BrotliDecompress)|BrotliOptions)$/.test(
|
||||
source,
|
||||
)
|
||||
) {
|
||||
return "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().";
|
||||
}
|
||||
if (source === "compiler-feature:zlib.zlib.constants") {
|
||||
return "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value.";
|
||||
|
||||
@@ -190,6 +190,12 @@
|
||||
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
|
||||
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
|
||||
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"],
|
||||
"node-builtin.zlib.brotliCompressSync": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
|
||||
"node-builtin.zlib.brotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
|
||||
"node-builtin.zlib.createBrotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
|
||||
"node-builtin.zlib.brotliDecompressSync": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
|
||||
"node-builtin.zlib.brotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
|
||||
"node-builtin.zlib.createBrotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
|
||||
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
|
||||
"stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"],
|
||||
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
|
||||
@@ -404,6 +410,9 @@
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_BROTLI_INVALID_PARAM"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliDecompress`"], "status": "partial", "evidence": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"] },
|
||||
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
|
||||
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
|
||||
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
|
||||
|
||||
+41
-25
@@ -4872,9 +4872,9 @@ declare module "node:readline/promises" {
|
||||
* inputs with Node's default options; libz links only into zlib-using
|
||||
* binaries. Callback forms use the executable worker pool. Options stay
|
||||
* declared so reached uses receive a named SC2020 refusal. The Deflate,
|
||||
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip streams are
|
||||
* runtime Transform streams; Brotli and Zstd remain outside the lowered
|
||||
* surface. */
|
||||
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, Unzip, BrotliCompress
|
||||
* and BrotliDecompress streams are runtime Transform streams; Zstd remains
|
||||
* outside the lowered surface. */
|
||||
declare module "zlib" {
|
||||
import { Transform } from "stream";
|
||||
export interface ZlibOptions {
|
||||
@@ -4931,50 +4931,66 @@ declare module "zlib" {
|
||||
): void;
|
||||
export function unzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer;
|
||||
export function crc32(data: string | Uint8Array, value?: number): number;
|
||||
export function brotliCompressSync(data: string | Uint8Array): Buffer;
|
||||
export function brotliDecompressSync(data: Uint8Array): Buffer;
|
||||
/* Brotli needs a codec the static runtime does not vendor: these are
|
||||
* declared so their uses receive the named refusal. */
|
||||
/* Brotli over the vendored brotli 1.2.0 (scr_brotli.c): `params` keys
|
||||
* are the numeric BROTLI_PARAM_* and BROTLI_DECODER_PARAM_* constants. */
|
||||
export interface BrotliOptions {
|
||||
[option: string]: unknown;
|
||||
}
|
||||
export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void;
|
||||
export function brotliDecompress(data: Uint8Array, callback: ZlibCallback): void;
|
||||
export function createBrotliCompress(options?: ZlibOptions): Transform;
|
||||
export function createBrotliDecompress(options?: ZlibOptions): Transform;
|
||||
export class BrotliCompress extends Transform {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class BrotliDecompress extends Transform {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export function brotliCompress(
|
||||
data: string | Uint8Array,
|
||||
options: BrotliOptions,
|
||||
callback: ZlibCallback,
|
||||
): void;
|
||||
export function brotliCompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
|
||||
export function brotliDecompress(data: string | Uint8Array, callback: ZlibCallback): void;
|
||||
export function brotliDecompress(
|
||||
data: string | Uint8Array,
|
||||
options: BrotliOptions,
|
||||
callback: ZlibCallback,
|
||||
): void;
|
||||
export function brotliDecompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
|
||||
/* The stream classes: runtime Transform streams over the zlib engine
|
||||
* (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build
|
||||
* the same stream; options are validated at runtime with Node's rules. */
|
||||
* the same stream; options are validated at runtime with Node's rules.
|
||||
* Brotli streams have no params(), as in Node. */
|
||||
class ZlibBase extends Transform {
|
||||
readonly bytesWritten: number;
|
||||
flush(kind?: number, callback?: () => void): void;
|
||||
flush(callback: () => void): void;
|
||||
params(level: number, strategy: number, callback?: () => void): void;
|
||||
close(callback?: () => void): void;
|
||||
reset(): void;
|
||||
}
|
||||
export class Deflate extends ZlibBase {
|
||||
class Zlib extends ZlibBase {
|
||||
params(level: number, strategy: number, callback?: () => void): void;
|
||||
}
|
||||
export class BrotliCompress extends ZlibBase {
|
||||
constructor(options?: BrotliOptions);
|
||||
}
|
||||
export class BrotliDecompress extends ZlibBase {
|
||||
constructor(options?: BrotliOptions);
|
||||
}
|
||||
export function createBrotliCompress(options?: BrotliOptions): BrotliCompress;
|
||||
export function createBrotliDecompress(options?: BrotliOptions): BrotliDecompress;
|
||||
export class Deflate extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Inflate extends ZlibBase {
|
||||
export class Inflate extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Gzip extends ZlibBase {
|
||||
export class Gzip extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Gunzip extends ZlibBase {
|
||||
export class Gunzip extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class DeflateRaw extends ZlibBase {
|
||||
export class DeflateRaw extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class InflateRaw extends ZlibBase {
|
||||
export class InflateRaw extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Unzip extends ZlibBase {
|
||||
export class Unzip extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export function createDeflate(options?: ZlibOptions): Deflate;
|
||||
|
||||
@@ -20,6 +20,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
inspect: features.inspect,
|
||||
dynInvoke: features.dynInvoke,
|
||||
|
||||
@@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => {
|
||||
const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
|
||||
expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
|
||||
});
|
||||
|
||||
test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => {
|
||||
const wrap = (id: string): IrExpr => ({
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref(id, record),
|
||||
type: optional,
|
||||
loc,
|
||||
});
|
||||
const leaf = helper("leaf");
|
||||
const outer = helper("outer");
|
||||
outer.params[0]!.type = outer.locals[0]!.type = record;
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc },
|
||||
ret(call("leaf", [wrap("cell")])),
|
||||
];
|
||||
const functions = new Map([leaf, outer].map((fn) => [fn.name, fn]));
|
||||
const analyzeWith = (pointer: boolean) =>
|
||||
analyzeCallLifetimes(
|
||||
functions,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => pointer,
|
||||
);
|
||||
// A boxed union builds a new box around the payload: a whole-value use.
|
||||
expect(analyzeWith(false).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(false).locals.get("outer")).toEqual(new Set());
|
||||
// A pointer union passes the payload pointer itself.
|
||||
expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0]));
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"]));
|
||||
// A callee that keeps its parameter keeps the wrapped binding too.
|
||||
leaf.body.push(ret(ref("value")));
|
||||
expect(analyzeWith(true).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set());
|
||||
});
|
||||
|
||||
test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => {
|
||||
const wrapCell: IrExpr = {
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref("cell", record),
|
||||
type: optional,
|
||||
loc,
|
||||
};
|
||||
const outer = helper("outer", []);
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
ret(call("leaf", [wrapCell])),
|
||||
];
|
||||
const read = (value: IrExpr): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: value,
|
||||
shapeId: "cell",
|
||||
field: "x",
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => {
|
||||
const result = analyzeCallLifetimes(
|
||||
new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])),
|
||||
(className) => className === "Node",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => true,
|
||||
);
|
||||
return result.locals.get("outer")?.has("cell") !== true;
|
||||
};
|
||||
const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body });
|
||||
// Tag tests and field reads of the narrowed payload leave it in place.
|
||||
expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false);
|
||||
expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false);
|
||||
// So do field reads through a checked narrowing, which throws on the
|
||||
// other arm (`value.x` of a `T | undefined` parameter).
|
||||
const checked: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: tag("value"),
|
||||
then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc },
|
||||
else_: narrow("value"),
|
||||
type: record,
|
||||
loc,
|
||||
};
|
||||
expect(keeps(leafWith([ret(read(checked))]))).toBe(false);
|
||||
// Returning it keeps the payload.
|
||||
const returnsChecked = leafWith([ret(checked)]);
|
||||
returnsChecked.returnType = record;
|
||||
expect(keeps(returnsChecked)).toBe(true);
|
||||
// Returning the narrowed payload, or a narrowed nullish read, keeps it,
|
||||
// although the union itself is only projected.
|
||||
const returned = leafWith([ret(narrow("value"))]);
|
||||
returned.returnType = record;
|
||||
expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0]));
|
||||
expect(keeps(returned)).toBe(true);
|
||||
const defaulted = leafWith([
|
||||
ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }),
|
||||
]);
|
||||
defaulted.locals.push(local("other", record));
|
||||
expect(keeps(defaulted)).toBe(true);
|
||||
// So does a nullable-pointer field store of the union.
|
||||
const self: IrType = { kind: "object", className: "Node" };
|
||||
const stored = leafWith([
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: ref("this", self),
|
||||
className: "Node",
|
||||
field: "next",
|
||||
value: ref("value"),
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
stored.params = [{ localId: "value", name: "value", type: optional }];
|
||||
stored.locals.push(local("this", self));
|
||||
expect(keeps(stored)).toBe(true);
|
||||
// A narrowed payload passed on, or the union passed on, is in place when
|
||||
// the next callee keeps it in place.
|
||||
const inner = (body: IrStmt[], type: IrType = record): IrFunction => {
|
||||
const fn = helper("inner");
|
||||
fn.params[0]!.type = fn.locals[0]!.type = type;
|
||||
fn.body = body;
|
||||
return fn;
|
||||
};
|
||||
const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]);
|
||||
expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false);
|
||||
const returnsRecord = inner([ret(ref("value", record))]);
|
||||
returnsRecord.returnType = record;
|
||||
expect(keeps(passNarrowed, returnsRecord)).toBe(true);
|
||||
const passUnion = leafWith([ret(call("inner", [ref("value")]))]);
|
||||
expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false);
|
||||
const innerReturned = inner([ret(narrow("value"))], optional);
|
||||
innerReturned.returnType = record;
|
||||
expect(keeps(passUnion, innerReturned)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js";
|
||||
interface ForwardedUse {
|
||||
callee: string;
|
||||
index: number;
|
||||
/** The binding reached the call inside a nullable-pointer union
|
||||
* (PointerWrapTest): the callee's parameter holds the binding itself as
|
||||
* its payload, so the callee must keep the payload, not only the union,
|
||||
* in place. */
|
||||
wrapped: boolean;
|
||||
}
|
||||
|
||||
interface Uses {
|
||||
@@ -21,6 +26,13 @@ interface Uses {
|
||||
written: Set<string>;
|
||||
declarations: Map<string, number>;
|
||||
forwards: Map<string, ForwardedUse[]>;
|
||||
/** Union bindings whose payload a use may keep: a narrowed read that is
|
||||
* not projected or passed on, a narrowed nullish read, a store into a
|
||||
* nullable-pointer field. The union itself can still be only projected. */
|
||||
payloadKept: Set<string>;
|
||||
/** Narrowed payloads of union bindings passed directly as call arguments:
|
||||
* the payload stays in place if the callee's parameter is proved. */
|
||||
payloadForwards: Map<string, ForwardedUse[]>;
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
@@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: ()
|
||||
* reads the record and keeps no reference to it. */
|
||||
export type RecordDynCopyTest = (shapeId: string) => boolean;
|
||||
|
||||
/** Wraps into a nullable-pointer union whose value is the payload pointer
|
||||
* itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the
|
||||
* callee's parameter unchanged. */
|
||||
export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean;
|
||||
|
||||
function collectUses(
|
||||
fn: IrFunction,
|
||||
nullableField: NullableFieldTest,
|
||||
registerReturn: boolean,
|
||||
inlineField: InlineFieldTest,
|
||||
recordDynCopy: RecordDynCopyTest,
|
||||
pointerWrap: PointerWrapTest,
|
||||
): Uses {
|
||||
const uses: Uses = {
|
||||
invalid: new Set(),
|
||||
written: new Set(),
|
||||
declarations: new Map(),
|
||||
forwards: new Map(),
|
||||
payloadKept: new Set(),
|
||||
payloadForwards: new Map(),
|
||||
};
|
||||
const forward = (
|
||||
table: Map<string, ForwardedUse[]>,
|
||||
localId: string,
|
||||
use: ForwardedUse,
|
||||
): void => {
|
||||
let forwards = table.get(localId);
|
||||
if (!forwards) table.set(localId, (forwards = []));
|
||||
forwards.push(use);
|
||||
};
|
||||
/** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a
|
||||
* checked narrowing `unionIsTag(x) ? <libCall> : unionNarrow(x)` (either
|
||||
* order), whose other branch is visited as any use. */
|
||||
function narrowedBinding(value: IrExpr): string | null {
|
||||
if (value.kind === "unionNarrow")
|
||||
return value.value.kind === "varRef" ? value.value.localId : null;
|
||||
if (
|
||||
value.kind !== "ternary" ||
|
||||
value.cond.kind !== "unionIsTag" ||
|
||||
value.cond.value.kind !== "varRef"
|
||||
)
|
||||
return null;
|
||||
const id = value.cond.value.localId;
|
||||
if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id)
|
||||
return expr(value.else_) ? id : null;
|
||||
if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id)
|
||||
return expr(value.then) ? id : null;
|
||||
return null;
|
||||
}
|
||||
const aliases: { alias: string; source: string }[] = [];
|
||||
const stringInput = (value: IrExpr): boolean => {
|
||||
if (value.kind === "varRef" && value.type.kind === "string") return true;
|
||||
@@ -145,6 +193,12 @@ function collectUses(
|
||||
if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput);
|
||||
break;
|
||||
case "unionNarrow":
|
||||
// The payload may escape; the union itself does not.
|
||||
if (node.value.kind === "varRef") {
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "unionIsTag":
|
||||
if (node.value.kind === "varRef") return true;
|
||||
break;
|
||||
@@ -161,12 +215,16 @@ function collectUses(
|
||||
node.left.kind === "varRef" &&
|
||||
node.left.type.kind === "union" &&
|
||||
node.type.kind !== "union"
|
||||
)
|
||||
) {
|
||||
uses.payloadKept.add(node.left.localId);
|
||||
return expr(node.right);
|
||||
}
|
||||
break;
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
if (node.obj.kind === "varRef") return true;
|
||||
// A field read of a binding, or of a union binding's narrowed
|
||||
// payload, keeps neither.
|
||||
if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true;
|
||||
break;
|
||||
case "dynFrom":
|
||||
// A copied record converts field by field; a live capsule keeps it.
|
||||
@@ -200,13 +258,19 @@ function collectUses(
|
||||
return node.args.every(expr);
|
||||
case "call":
|
||||
node.args.forEach((arg, index) => {
|
||||
if (arg.kind !== "varRef") {
|
||||
const narrowed = narrowedBinding(arg);
|
||||
if (narrowed !== null) {
|
||||
forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false });
|
||||
return;
|
||||
}
|
||||
const wrapped =
|
||||
arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg);
|
||||
const value = wrapped ? arg.value : arg;
|
||||
if (value.kind !== "varRef") {
|
||||
expr(arg);
|
||||
return;
|
||||
}
|
||||
let forwards = uses.forwards.get(arg.localId);
|
||||
if (!forwards) uses.forwards.set(arg.localId, (forwards = []));
|
||||
forwards.push({ callee: node.callee, index });
|
||||
forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped });
|
||||
});
|
||||
return true;
|
||||
case "assignExpr":
|
||||
@@ -250,8 +314,11 @@ function collectUses(
|
||||
node.value.kind === "varRef" &&
|
||||
node.value.type.kind === "union" &&
|
||||
nullableField(node.className, node.field)
|
||||
)
|
||||
) {
|
||||
// The field retains the payload.
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return expr(node.obj);
|
||||
}
|
||||
const stored = fieldStore(
|
||||
node.obj,
|
||||
node.value,
|
||||
@@ -345,10 +412,14 @@ export function analyzeCallLifetimes(
|
||||
registerReturns: ReadonlySet<string> = new Set(),
|
||||
inlineField: InlineFieldTest = NO_INLINE_FIELDS,
|
||||
recordDynCopy: RecordDynCopyTest = () => false,
|
||||
pointerWrap: PointerWrapTest = () => false,
|
||||
): CallLifetimes {
|
||||
const usesByFunction = new Map<string, Uses>();
|
||||
const lazyCaptures = new LazyCaptures(functions);
|
||||
const nodes = new Map<string, Parameter[]>();
|
||||
/** Union parameters that keep their payload in place too, which a
|
||||
* pointer-wrapped argument needs (ForwardedUse.wrapped). */
|
||||
const payloadNodes = new Map<string, (Parameter | undefined)[]>();
|
||||
const unsafe: Parameter[] = [];
|
||||
const consumed: { fn: string; index: number }[] = [];
|
||||
const result: CallLifetimes = {
|
||||
@@ -361,7 +432,7 @@ export function analyzeCallLifetimes(
|
||||
};
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.generator !== undefined && fn.classCaptures === undefined) {
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy);
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap);
|
||||
const locals = new Map(fn.locals.map((local) => [local.id, local]));
|
||||
const stable = new Set<string>();
|
||||
for (const param of fn.params) {
|
||||
@@ -386,6 +457,7 @@ export function analyzeCallLifetimes(
|
||||
registerReturns.has(fn.name),
|
||||
inlineField,
|
||||
recordDynCopy,
|
||||
pointerWrap,
|
||||
);
|
||||
usesByFunction.set(fn.name, uses);
|
||||
// Class-capturing bodies get only local facts (computed below from
|
||||
@@ -460,23 +532,50 @@ export function analyzeCallLifetimes(
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
payloadNodes.set(
|
||||
fn.name,
|
||||
fn.params.map((param) => {
|
||||
if (param.type.kind !== "union") return undefined;
|
||||
const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() };
|
||||
if (!node.safe) unsafe.push(node);
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
}
|
||||
/** The fact a forwarded use needs from the callee's parameter. */
|
||||
const target = (use: ForwardedUse): Parameter | undefined =>
|
||||
use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index];
|
||||
for (const fn of functions.values()) {
|
||||
const params = nodes.get(fn.name);
|
||||
const uses = usesByFunction.get(fn.name);
|
||||
if (!params || !uses) continue;
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => {
|
||||
if (!node.safe) return;
|
||||
for (const use of uses.forwards.get(param.localId) ?? []) {
|
||||
const callee = nodes.get(use.callee)?.[use.index];
|
||||
for (const callee of callees) {
|
||||
if (!callee) {
|
||||
node.safe = false;
|
||||
unsafe.push(node);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
callee.callers.add(node);
|
||||
}
|
||||
};
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const forwards = uses.forwards.get(param.localId) ?? [];
|
||||
depend(node, forwards.map(target));
|
||||
const payload = payloadNodes.get(fn.name)?.[index];
|
||||
if (!payload) return;
|
||||
// The payload stays in place when the union does, when every callee
|
||||
// the union reaches keeps its payload too, and when every callee its
|
||||
// narrowed payload reaches proves that parameter.
|
||||
depend(payload, [
|
||||
node,
|
||||
...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]),
|
||||
...(uses.payloadForwards.get(param.localId) ?? []).map(
|
||||
(use) => nodes.get(use.callee)?.[use.index],
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < unsafe.length; i++) {
|
||||
@@ -508,7 +607,7 @@ export function analyzeCallLifetimes(
|
||||
)
|
||||
continue;
|
||||
const forwards = uses.forwards.get(local.id) ?? [];
|
||||
if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) {
|
||||
if (forwards.every((use) => target(use)?.safe === true)) {
|
||||
projected.add(local.id);
|
||||
if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id);
|
||||
}
|
||||
|
||||
@@ -193,6 +193,11 @@ function builtinStreamKind(className: string): number | null {
|
||||
return kinds[className] ?? null;
|
||||
}
|
||||
|
||||
/** Unions whose discriminant has fewer string literals keep one literal test
|
||||
* per arm (dispatchValues): one runtime lookup costs about what a few
|
||||
* literal tests do. */
|
||||
const DISCRIMINANT_DISPATCH_VALUES = 4;
|
||||
|
||||
export class LlDyn {
|
||||
private readonly dynMatchers = new Map<string, string>();
|
||||
private readonly dynBuilders = new Map<string, string>();
|
||||
@@ -413,6 +418,109 @@ export class LlDyn {
|
||||
return order;
|
||||
}
|
||||
|
||||
/** The string literals of `def`'s discriminant when its arms are chosen
|
||||
* by reading the discriminant once (unionArmsByDiscriminant), or null
|
||||
* when the union keeps one literal test per arm: some literal is not a
|
||||
* string, or there are fewer than DISCRIMINANT_DISPATCH_VALUES. */
|
||||
private dispatchValues(def: IrUnionDef): string[] | null {
|
||||
const values: string[] = [];
|
||||
for (const guard of def.discriminant?.cases ?? []) {
|
||||
if (guard.values.length === 0) return null;
|
||||
for (const value of guard.values) {
|
||||
if (typeof value !== "string") return null;
|
||||
if (!values.includes(value)) values.push(value);
|
||||
}
|
||||
}
|
||||
return values.length < DISCRIMINANT_DISPATCH_VALUES ? null : values;
|
||||
}
|
||||
|
||||
/** Branches to `hits[tag]` for the first arm in `order` that `%d` matches
|
||||
* (unionArmMatch), or to `none` when no arm does, for
|
||||
* a union whose discriminant literals are `values` (dispatchValues). The
|
||||
* discriminant is read once (scr_dyn_discriminant_case, against the
|
||||
* constant `table`), and only the arms whose literal it holds and the arms
|
||||
* without a literal are tested, in `order`: every other arm's literal test
|
||||
* fails. An arm's failed check can run a getter that changes the
|
||||
* discriminant, so it is read again after each one; when its case
|
||||
* changed, the tests continue arm by arm after the failed arm, as they
|
||||
* would have run from the first arm. A value the runtime cannot classify
|
||||
* that way (a typed capsule, an accessor, an inherited discriminant) takes
|
||||
* the tests arm by arm. */
|
||||
private unionArmsByDiscriminant(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
values: readonly string[],
|
||||
order: readonly number[],
|
||||
hits: readonly string[],
|
||||
none: string,
|
||||
preserveRefs: boolean,
|
||||
table: string,
|
||||
): void {
|
||||
const discriminant = def.discriminant!;
|
||||
const literalsOf = (tag: number): readonly (string | number | boolean)[] | null =>
|
||||
discriminant.cases.find((candidate) => candidate.tag === tag)?.values ?? null;
|
||||
const host = this.host;
|
||||
this.defs.push(
|
||||
`${table} = private unnamed_addr constant [${values.length} x ptr] [${values.map((value) => `ptr ${host.internLiteral(value)}`).join(", ")}]`,
|
||||
``,
|
||||
);
|
||||
host.declare(`declare i32 @scr_dyn_discriminant_case(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
const classify = (): string => {
|
||||
const index = B.tmp();
|
||||
B.line(
|
||||
`${index} = call i32 @scr_dyn_discriminant_case(ptr %d, ptr ${host.internLiteral(discriminant.field)}, ptr ${table}, ${host.sizeType} ${values.length})`,
|
||||
);
|
||||
return index;
|
||||
};
|
||||
// The arm-by-arm tests, entered at any arm (`from[i]` tests order[i]
|
||||
// and the arms after it).
|
||||
const tests = B.newLabel("du.tests");
|
||||
const from = order.map((_, i) => (i === 0 ? tests : B.newLabel("du.t")));
|
||||
const after = (position: number): string =>
|
||||
position + 1 < order.length ? from[position + 1]! : none;
|
||||
// Tests the arms a value in case `k` (-1: no literal) can match.
|
||||
const tryCase = (k: number): void => {
|
||||
const value = k < 0 ? null : values[k]!;
|
||||
const positions = order
|
||||
.map((_, position) => position)
|
||||
.filter((position) => {
|
||||
const literals = literalsOf(order[position]!);
|
||||
return literals === null || (value !== null && literals.includes(value));
|
||||
});
|
||||
if (positions.length === 0) {
|
||||
B.br(none);
|
||||
return;
|
||||
}
|
||||
positions.forEach((position, index) => {
|
||||
const tag = order[position]!;
|
||||
const failed = B.newLabel("du.c.f");
|
||||
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs);
|
||||
B.startBlock(failed);
|
||||
const next = index === positions.length - 1 ? none : B.newLabel("du.c.n");
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq i32 ${classify()}, ${k}`);
|
||||
B.condBr(same, next, after(position));
|
||||
if (next !== none) B.startBlock(next);
|
||||
});
|
||||
};
|
||||
const index = classify();
|
||||
const unmatched = B.newLabel("du.none");
|
||||
const cases = values.map(() => B.newLabel("du.case"));
|
||||
B.terminate(
|
||||
`switch i32 ${index}, label %${tests} [ i32 -1, label %${unmatched} ${cases.map((label, k) => `i32 ${k}, label %${label}`).join(" ")} ]`,
|
||||
);
|
||||
values.forEach((_, k) => {
|
||||
B.startBlock(cases[k]!);
|
||||
tryCase(k);
|
||||
});
|
||||
B.startBlock(unmatched);
|
||||
tryCase(-1);
|
||||
order.forEach((tag, position) => {
|
||||
B.startBlock(from[position]!);
|
||||
this.unionArmMatch(B, def, tag, hits[tag]!, after(position), preserveRefs);
|
||||
});
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
private unionArmMatch(
|
||||
@@ -450,6 +558,18 @@ export class LlDyn {
|
||||
});
|
||||
B.startBlock(structure);
|
||||
}
|
||||
this.unionArmStructure(B, def, tag, yes, no, preserveRefs);
|
||||
}
|
||||
|
||||
/** The part of unionArmMatch past the arm's literal test. */
|
||||
private unionArmStructure(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
tag: number,
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs: boolean,
|
||||
): void {
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
||||
@@ -1278,11 +1398,26 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in canonical order; any full match answers true.
|
||||
const yes = B.newLabel("dm.y");
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const no = B.newLabel("dm.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
def.arms.map((_, tag) => tag),
|
||||
def.arms.map(() => yes),
|
||||
no,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
B.startBlock(no);
|
||||
} else
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
B.terminate(`ret i1 false`);
|
||||
B.startBlock(yes);
|
||||
B.terminate(`ret i1 true`);
|
||||
@@ -2543,12 +2678,8 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
for (const i of this.unionArmOrder(def)) {
|
||||
const build = (i: number): void => {
|
||||
const arm = def.arms[i]!;
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
||||
// A matched unit arm builds nothing: THE interned immortal
|
||||
// instance (rc == SIZE_MAX — no retain owed).
|
||||
@@ -2587,8 +2718,36 @@ export class LlDyn {
|
||||
);
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
}
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
};
|
||||
const order = this.unionArmOrder(def);
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const hits = def.arms.map(() => B.newLabel("dcu.h"));
|
||||
const unmatched = B.newLabel("dcu.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
order,
|
||||
hits,
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
build(i);
|
||||
}
|
||||
B.startBlock(unmatched);
|
||||
} else
|
||||
for (const i of order) {
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
build(i);
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
if (preserveRefs) {
|
||||
B.line(
|
||||
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
||||
|
||||
@@ -344,6 +344,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Fetch-referencing programs register native fetch before main.
|
||||
// Runtime selection compiles scr_fetch.c on the same predicate.
|
||||
const usesFetch = runtimeFeatures.fetch;
|
||||
// URL programs give the host parser the IDNA unit's ToASCII.
|
||||
const usesUrlIdna = runtimeFeatures.urlIdna;
|
||||
const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa;
|
||||
// The process verdict has the same precedence as the C reference
|
||||
// emitter: node:test owns the final status when present; otherwise an
|
||||
@@ -396,6 +398,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
if (usesHttp2) emitter.declare(`declare void @scr_http2_dyn_install()`);
|
||||
if (usesHttp) emitter.declare(`declare void @scr_http_dyn_install()`);
|
||||
if (usesFetch) emitter.declare(`declare void @scr_fetch_install()`);
|
||||
if (usesUrlIdna) emitter.declare(`declare void @scr_url_idna_install()`);
|
||||
|
||||
if (usesNodeTest) {
|
||||
emitter.declare(`declare i32 @scr_test_exit_code()`);
|
||||
@@ -1027,6 +1030,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
...(emitter.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []),
|
||||
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
|
||||
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
|
||||
...(usesUrlIdna ? [` call void @scr_url_idna_install()`] : []),
|
||||
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
|
||||
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
|
||||
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
|
||||
|
||||
@@ -1785,6 +1785,7 @@ export class LlEmitter {
|
||||
const shape = this.recordsById.get(shapeId);
|
||||
return shape !== undefined && !recordDynKeepsIdentity(shape);
|
||||
},
|
||||
(wrap) => this.borrowableNullableWrap(wrap) === "ref",
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
typeKey,
|
||||
type IrClassDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
@@ -276,9 +277,7 @@ export function streamTypedRefCommitAdapter(
|
||||
);
|
||||
});
|
||||
}
|
||||
const symbols = new Map(
|
||||
(meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]),
|
||||
);
|
||||
const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol]));
|
||||
const fields = meta.def.fields.filter(
|
||||
(field) =>
|
||||
(isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) &&
|
||||
@@ -298,13 +297,15 @@ export function streamTypedRefCommitAdapter(
|
||||
const inline = host.inlineUnions.classField(t.className, field.name);
|
||||
const fieldTy = int32 ? "i32" : llFieldType(field.type, inline);
|
||||
const checkTy = host.llType(field.type);
|
||||
const symbolGlobal = symbols.get(field.name);
|
||||
if (symbolGlobal) {
|
||||
const symbolKey = symbols.get(field.name);
|
||||
if (symbolKey) {
|
||||
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const keyLines = symbolKeyLines(host, symbolKey, `f${index}_keyraw`);
|
||||
lines.push(
|
||||
` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`,
|
||||
...keyLines.load,
|
||||
` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`,
|
||||
...keyLines.release,
|
||||
);
|
||||
if (field.type.kind === "symbol") {
|
||||
// A base constructor can mutate the instance before this
|
||||
@@ -332,7 +333,7 @@ export function streamTypedRefCommitAdapter(
|
||||
lines.push(
|
||||
` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
|
||||
);
|
||||
if (meta.def.tracksOwnFields && !symbolGlobal)
|
||||
if (meta.def.tracksOwnFields && !symbolKey)
|
||||
lines.push(
|
||||
` %f${index}_present = icmp ne ptr %${raw}, null`,
|
||||
` br i1 %f${index}_present, label %f${index}_read, label %${after}`,
|
||||
@@ -343,7 +344,7 @@ export function streamTypedRefCommitAdapter(
|
||||
` %${undef} = call ptr @scr_dyn_undefined()`,
|
||||
` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`,
|
||||
` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`,
|
||||
...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
...(symbolKey ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
...host.pendingTestLines(`%${pending}`),
|
||||
` br i1 %${pending}, label %done, label %${store}`,
|
||||
`${store}:`,
|
||||
@@ -498,16 +499,18 @@ export function streamTypedRefCommitAdapter(
|
||||
? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)`
|
||||
: `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`,
|
||||
);
|
||||
[...symbols.values()].forEach((globalId, index) =>
|
||||
[...symbols.values()].forEach((symbol, index) => {
|
||||
const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`);
|
||||
lines.push(
|
||||
` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`,
|
||||
...keyLines.load,
|
||||
` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`,
|
||||
...keyLines.release,
|
||||
meta.def.tracksOwnFields
|
||||
? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})`
|
||||
: ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
|
||||
` call void @scr_dyn_release_v(ptr %bag_sym${index})`,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
|
||||
lines.push(
|
||||
` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`,
|
||||
@@ -704,6 +707,27 @@ export function liveDynUnionRefAdapter(
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** The raw ScrSym of a class's symbol field key into `%name`: a module
|
||||
* key is borrowed from its global, a well-known one comes from the
|
||||
* runtime's registry (+1, released by the `release` lines once the key
|
||||
* value holds it). */
|
||||
function symbolKeyLines(
|
||||
host: LlvmEmitterContext,
|
||||
symbol: NonNullable<IrClassDef["symbolFields"]>[number],
|
||||
name: string,
|
||||
): { load: string[]; release: string[] } {
|
||||
if ("globalId" in symbol)
|
||||
return { load: [` %${name} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`], release: [] };
|
||||
host.declare("declare ptr @scr_sym_well_known(ptr)");
|
||||
host.declare("declare void @scr_sym_release(ptr)");
|
||||
return {
|
||||
load: [
|
||||
` %${name} = call ptr @scr_sym_well_known(ptr ${host.internLiteral(symbol.wellKnown)})`,
|
||||
],
|
||||
release: [` call void @scr_sym_release(ptr %${name})`],
|
||||
};
|
||||
}
|
||||
|
||||
export function streamTypedRefBoxValue(
|
||||
host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
@@ -928,8 +952,10 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
|
||||
} else if (inline) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
|
||||
else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
|
||||
B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`);
|
||||
const keyLines = symbolKeyLines(host, symbol, rawKey.slice(1));
|
||||
for (const line of keyLines.load) B.line(line.trim());
|
||||
const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey);
|
||||
for (const line of keyLines.release) B.line(line.trim());
|
||||
const afterSymbol =
|
||||
meta.def.tracksOwnFields || field.type.kind === "symbol"
|
||||
? B.newLabel("symbol.after")
|
||||
@@ -1518,9 +1544,26 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
|
||||
// The constructor's name, as util.inspect prints before the fields.
|
||||
const jsName = meta.def.jsName ?? meta.def.name.replace(/^%/, "");
|
||||
const className = jsName === "" ? "null" : host.internLiteral(jsName);
|
||||
// Its Symbol.toStringTag getter's string, for Object.prototype.toString
|
||||
// and util.inspect.
|
||||
let stringTag = "null";
|
||||
if (meta.def.stringTagHelper) {
|
||||
// The runtime lends the instance; the helper takes its own reference.
|
||||
stringTag = `@${snapshot}_ctag`;
|
||||
const retain = vAdapters(host.shapeHost, { kind: "object", className: meta.def.name }).retain;
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr ${stringTag}(ptr %p) ${FN_ATTRS} { ; Symbol.toStringTag of ${meta.def.name}`,
|
||||
`entry:`,
|
||||
` %owned = call ptr ${retain}(ptr %p)`,
|
||||
` %tag = call ptr @${mangleFunction(meta.def.stringTagHelper)}(ptr %owned)`,
|
||||
` ret ptr %tag`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
host.resolveThunkDefs.push(
|
||||
`@${names} = internal constant [${members.length} x ptr] [${literals.join(", ")}]`,
|
||||
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className} }`,
|
||||
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className}, ptr ${stringTag} }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -575,6 +575,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"zlib.deflateSync": "scr_zlib_deflate",
|
||||
"zlib.inflateSync": "scr_zlib_inflate",
|
||||
"zlib.crc32": "scr_zlib_crc32",
|
||||
"zlib.brotliInstall": "scr_brotli_install",
|
||||
// The CA-store unit. get/set throw (an unknown type name, a
|
||||
// certificate-free set) and take the generic path's pending check.
|
||||
"tlsca.root": "scr_tls_ca_root",
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface NativeLinkFeatures {
|
||||
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
|
||||
stdioExtended?: boolean;
|
||||
zlib: boolean;
|
||||
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
assert: boolean;
|
||||
inspect: boolean;
|
||||
dynInvoke: boolean;
|
||||
|
||||
@@ -85,6 +85,9 @@ export interface CcOptions {
|
||||
* compares round-trips and fixed-blob inflation, never raw deflate
|
||||
* output. */
|
||||
zlib?: boolean;
|
||||
/** The program uses node:zlib's Brotli forms (with zlib): compiles
|
||||
* scr_brotli.c and links the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
/** The program uses node:assert (index.ts detects assert.* libCalls on
|
||||
* the IR): compiles scr_assert.c — the zlib gating precedent, so
|
||||
* assert-free binaries keep their exact size class. scr_regex.c calls
|
||||
|
||||
@@ -82,15 +82,18 @@ import {
|
||||
currentVendorCacheBuildIdentity,
|
||||
vendorBuildCacheRoot,
|
||||
tlsArchivePath,
|
||||
brotliArchivePath,
|
||||
lreObjectPaths,
|
||||
zlibObjectPaths,
|
||||
stageVendorInputs,
|
||||
ensureTlsArchive,
|
||||
ensureBrotliArchive,
|
||||
ensureLreObjects,
|
||||
ensureZlibObjects,
|
||||
vendorEngineDir,
|
||||
vendorZlibDir,
|
||||
vendorTlsDir,
|
||||
vendorBrotliDir,
|
||||
} from "./vendor.js";
|
||||
import { execFileAsync, subprocessFailureDetail, CcCompileError } from "./process.js";
|
||||
import { implicitLinkerFingerprint } from "./linker-fingerprint.js";
|
||||
@@ -119,6 +122,7 @@ interface RuntimeUnitContext {
|
||||
lreObjects: readonly string[];
|
||||
zlibObjects: readonly string[];
|
||||
tlsArchive: string | null;
|
||||
brotliArchive: string | null;
|
||||
}
|
||||
|
||||
/** An optional runtime input: runtime sources compiled when `when` holds,
|
||||
@@ -222,6 +226,18 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
: []
|
||||
: [],
|
||||
),
|
||||
// Brotli (scr_brotli.c) plugs into the zlib engine and links the
|
||||
// vendored brotli archive.
|
||||
unitArgs((u) =>
|
||||
u.brotliArchive
|
||||
? [
|
||||
"-I",
|
||||
join(vendorBrotliDir(), "include"),
|
||||
u.rt(join(u.rtDir, "scr_brotli.c")),
|
||||
u.brotliArchive,
|
||||
]
|
||||
: [],
|
||||
),
|
||||
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
@@ -398,6 +414,7 @@ export async function compileCInternal(
|
||||
const dh = opts.dh ?? false;
|
||||
const pki = opts.pki ?? false;
|
||||
const mbedtls = tls || cipher || ecdh || dh || pki;
|
||||
const brotli = (opts.zlib ?? false) && opts.brotli === true;
|
||||
const driver = resolveCc();
|
||||
const darwinDebugSymbols = needsDarwinDebugSymbols(
|
||||
targetPlatform(driver),
|
||||
@@ -637,6 +654,9 @@ export async function compileCInternal(
|
||||
let tlsArchive = mbedtls
|
||||
? tlsArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
|
||||
: null;
|
||||
let brotliArchive = brotli
|
||||
? brotliArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
|
||||
: null;
|
||||
|
||||
// ours would collide; see scr_regex.c) — the standalone objects are for
|
||||
// static builds only.
|
||||
@@ -674,6 +694,23 @@ export async function compileCInternal(
|
||||
: await stageVendorInputs(materialize, join(stageRoot, "tls"));
|
||||
tlsArchive = paths[0]!;
|
||||
}
|
||||
if (brotli) {
|
||||
const cachedArchive = brotliArchivePath(
|
||||
sanitize,
|
||||
driver,
|
||||
vendorBuildIdentity,
|
||||
materializeCacheRoot,
|
||||
);
|
||||
protectCachedArtifact(cacheWarmPaths, cachedArchive);
|
||||
const materialize = async (): Promise<string[]> => [
|
||||
await ensureBrotliArchive(sanitize, driver, vendorBuildIdentity, materializeCacheRoot),
|
||||
];
|
||||
const paths =
|
||||
stageRoot === undefined
|
||||
? await materialize()
|
||||
: await stageVendorInputs(materialize, join(stageRoot, "brotli"));
|
||||
brotliArchive = paths[0]!;
|
||||
}
|
||||
if (regex) {
|
||||
for (const object of lreObjectPaths(
|
||||
sanitize,
|
||||
@@ -760,6 +797,7 @@ export async function compileCInternal(
|
||||
lreObjects,
|
||||
zlibObjects,
|
||||
tlsArchive,
|
||||
brotliArchive,
|
||||
}),
|
||||
// A .ll program TU (the LLVM backend) deliberately carries no target
|
||||
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
||||
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
||||
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js";
|
||||
import { BROTLI_VERSION, MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js";
|
||||
import { stableTestMemo } from "./session.js";
|
||||
|
||||
const implementationPath = fileURLToPath(import.meta.url);
|
||||
@@ -134,6 +134,7 @@ async function runtimeFingerprintFresh(rtDir: string): Promise<string> {
|
||||
.update(QJS_COMMIT)
|
||||
.update(MBEDTLS_VERSION)
|
||||
.update(ZLIB_VERSION)
|
||||
.update(BROTLI_VERSION)
|
||||
.update("\0backend-recipe\0");
|
||||
for (const path of NATIVE_RECIPE_IMPLEMENTATION_PATHS) {
|
||||
h.update(basename(path))
|
||||
@@ -164,6 +165,7 @@ async function runtimeFingerprintInputGroups(
|
||||
{ label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") },
|
||||
{ label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") },
|
||||
{ label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") },
|
||||
{ label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") },
|
||||
{ label: "curl", dir: join(rtDir, "..", "vendor", "curl") },
|
||||
].map(async (group) => {
|
||||
const names = (await nativeSourceFiles(group.dir, true))
|
||||
|
||||
@@ -6,6 +6,9 @@ import { resolvedToolIdentity } from "./tool-identity.js";
|
||||
export const {
|
||||
vendorEngineDir,
|
||||
vendorTlsDir,
|
||||
vendorBrotliDir,
|
||||
brotliArchivePath,
|
||||
ensureBrotliArchive,
|
||||
vendorBuildCacheRoot,
|
||||
vendorCacheTargetFlavor,
|
||||
vendorCacheBuildIdentity,
|
||||
|
||||
@@ -34,7 +34,7 @@ interface RuntimePackUnit {
|
||||
}
|
||||
|
||||
interface RuntimePackArchive extends RuntimePackArtifact {
|
||||
id: "quickjs" | "libregexp" | "zlib" | "mbedtls";
|
||||
id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls";
|
||||
predicate: RuntimePredicate;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
builtinValues: boolean;
|
||||
warningReplace: boolean;
|
||||
stdioExtended: boolean;
|
||||
brotli: boolean;
|
||||
nativeFetch: boolean;
|
||||
netEffective: boolean;
|
||||
httpEffective: boolean;
|
||||
@@ -291,6 +292,7 @@ export function effectiveRuntimeFeatures(
|
||||
builtinValues: features.builtinValues === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
brotli: features.brotli === true,
|
||||
nativeFetch,
|
||||
netEffective,
|
||||
httpEffective,
|
||||
|
||||
@@ -14,10 +14,15 @@ import type { CcDriver } from "./native/driver.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** The section flags the libregexp/libunicode objects build with (the
|
||||
* runtime pack's executable_section_elimination flags). */
|
||||
const LRE_SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"];
|
||||
|
||||
import {
|
||||
QJS_COMMIT,
|
||||
MBEDTLS_VERSION,
|
||||
ZLIB_VERSION,
|
||||
BROTLI_VERSION,
|
||||
LRE_SOURCES,
|
||||
ZLIB_SOURCES,
|
||||
} from "./vendor-inputs.js";
|
||||
@@ -25,6 +30,7 @@ export {
|
||||
QJS_COMMIT,
|
||||
MBEDTLS_VERSION,
|
||||
ZLIB_VERSION,
|
||||
BROTLI_VERSION,
|
||||
LRE_SOURCES,
|
||||
ZLIB_SOURCES,
|
||||
} from "./vendor-inputs.js";
|
||||
@@ -189,13 +195,13 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
(sanitize ? "asan" : "plain") +
|
||||
(driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
|
||||
`-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-${flavor}`);
|
||||
const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-sections-${flavor}`);
|
||||
return LRE_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
|
||||
}
|
||||
|
||||
/** The libregexp objects for one flavor, compiled lazily on the first
|
||||
* regex-using build (~1s) and cached under
|
||||
* <build-cache>/vendor/<commit>-lre-<flavor>-<target>-<toolchain>/*.o — with the same atomic-rename
|
||||
* <build-cache>/vendor/<commit>-lre-sections-<flavor>-<target>-<toolchain>/*.o — with the same atomic-rename
|
||||
* publish (parallel first builds race safely; losers discard their work).
|
||||
* Plain is -Os to keep regex-using binaries small; asan matches the final link so
|
||||
* the sanitized lane instruments the matcher too. Cross targets get their
|
||||
@@ -226,6 +232,10 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
vendor: vendorEngineDir(),
|
||||
sources: LRE_SOURCES,
|
||||
objects: lreObjectPaths(sanitize, driver, buildIdentity, cacheRoot),
|
||||
// Section elimination keeps only the libunicode tables a program
|
||||
// reaches: the URL host parser's NFC links the normalization tables
|
||||
// without the regex property ones.
|
||||
flags: LRE_SECTION_FLAGS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,6 +304,7 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
vendor: string;
|
||||
sources: readonly string[];
|
||||
objects: string[];
|
||||
flags?: readonly string[];
|
||||
}): Promise<string[]> {
|
||||
const { sanitize, driver, cacheRoot, flavor, name, vendor, sources, objects } = options;
|
||||
if ((await Promise.all(objects.map(validVendorArtifact))).every(Boolean)) return objects;
|
||||
@@ -310,6 +321,7 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
"-std=c11",
|
||||
...driver.targetArgs,
|
||||
...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
|
||||
...(options.flags ?? []),
|
||||
"-I",
|
||||
vendor,
|
||||
"-c",
|
||||
@@ -418,9 +430,98 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
return archive;
|
||||
}
|
||||
|
||||
function vendorBrotliDir(): string {
|
||||
return join(runtimeSrcDir(), "..", "vendor", "brotli", "c");
|
||||
}
|
||||
|
||||
function brotliArchivePath(
|
||||
sanitize: boolean,
|
||||
driver: CcDriver,
|
||||
buildIdentity: string,
|
||||
cacheRoot: string = vendorBuildCacheRoot(),
|
||||
): string {
|
||||
const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
return join(cacheRoot, `brotli-${BROTLI_VERSION}-${flavor}`, "libbrotli.a");
|
||||
}
|
||||
|
||||
/** The brotli archive for one flavor, compiled lazily on the first
|
||||
* Brotli-using build and cached under
|
||||
* <build-cache>/vendor/brotli-<version>-<flavor>-<target>-<toolchain>/libbrotli.a,
|
||||
* with the mbedTLS archive's recipe: per-TU compiles of c/common, c/dec
|
||||
* and c/enc against c/include, then one `ar rcs`. dec/ and enc/ share
|
||||
* file names, so each object is named after its path (dec-static_init.o). */
|
||||
async function ensureBrotliArchive(
|
||||
sanitize: boolean,
|
||||
driver: CcDriver,
|
||||
buildIdentity: string,
|
||||
cacheRoot: string = vendorBuildCacheRoot(),
|
||||
): Promise<string> {
|
||||
const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
const compileArgv = isZigDriver(driver) ? driver.argv : ["clang"];
|
||||
const arArgv = isZigDriver(driver) ? [...driver.argv.slice(0, 1), "ar"] : ["ar"];
|
||||
const vendor = vendorBrotliDir();
|
||||
const archive = brotliArchivePath(sanitize, driver, buildIdentity, cacheRoot);
|
||||
if (await validVendorArtifact(archive)) return archive;
|
||||
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
const buildDir = await mkdtemp(join(tmpdir(), `scriptc-vendor-brotli-${flavor}-`));
|
||||
try {
|
||||
const sources = (
|
||||
await Promise.all(
|
||||
["common", "dec", "enc"].map(async (dir) =>
|
||||
(await readdir(join(vendor, dir)))
|
||||
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
|
||||
.map((name) => `${dir}/${name}`),
|
||||
),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.sort();
|
||||
const object = (src: string): string =>
|
||||
join(buildDir, `${src.replaceAll("/", "-").replace(/\.c$/, "")}.o`);
|
||||
const cflags = [
|
||||
"-std=c11",
|
||||
...driver.targetArgs,
|
||||
...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
|
||||
"-I",
|
||||
join(vendor, "include"),
|
||||
];
|
||||
const width = availableParallelism();
|
||||
for (let i = 0; i < sources.length; i += width) {
|
||||
await Promise.all(
|
||||
sources
|
||||
.slice(i, i + width)
|
||||
.map((src) =>
|
||||
execFileAsync(compileArgv[0] ?? "clang", [
|
||||
...compileArgv.slice(1),
|
||||
...cflags,
|
||||
"-c",
|
||||
join(vendor, src),
|
||||
"-o",
|
||||
object(src),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
await execFileAsync(arArgv[0] ?? "ar", [
|
||||
...arArgv.slice(1),
|
||||
"rcs",
|
||||
join(buildDir, "libbrotli.a"),
|
||||
...sources.map(object),
|
||||
]);
|
||||
await publishVendorArtifact(join(buildDir, "libbrotli.a"), archive);
|
||||
} finally {
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
}
|
||||
return archive;
|
||||
}
|
||||
|
||||
return {
|
||||
vendorEngineDir,
|
||||
vendorTlsDir,
|
||||
vendorBrotliDir,
|
||||
brotliArchivePath,
|
||||
ensureBrotliArchive,
|
||||
vendorBuildCacheRoot,
|
||||
vendorCacheTargetFlavor,
|
||||
vendorCacheBuildIdentity,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/** The pinned QuickJS, mbedTLS, and zlib inputs used by native recipes. */
|
||||
/** The pinned QuickJS, mbedTLS, zlib, and brotli inputs used by native recipes. */
|
||||
export const QJS_COMMIT = "3c8f3d68953955950074c41c6e4d999562ae82a7";
|
||||
export const MBEDTLS_VERSION = "3.6.7";
|
||||
export const ZLIB_VERSION = "1.3.1";
|
||||
/** The brotli release Node v24.22.0 bundles (packages/runtime/vendor/brotli). */
|
||||
export const BROTLI_VERSION = "1.2.0";
|
||||
/** Exact translation-unit membership shared by cache builds and external
|
||||
* source-pack recipes. */
|
||||
export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const;
|
||||
|
||||
@@ -709,7 +709,8 @@ export class Int32ElementContracts {
|
||||
const findChainsIn = (st: IrStmt): boolean => everyStmtChild(st, findChains, findChainsIn);
|
||||
for (const st of fn.body) findChainsIn(st);
|
||||
// Parameters whose callers are not all visible hold opaque arrays.
|
||||
if (this.valueEscaped.has(fn.name) || this.runtimeVirtual(fn.name))
|
||||
const runtimeVirtual = this.runtimeVirtual;
|
||||
if (this.valueEscaped.has(fn.name) || runtimeVirtual(fn.name))
|
||||
for (const p of fn.params)
|
||||
if (this.holds(p.type)) {
|
||||
const why = this.valueEscaped.has(fn.name)
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
warningReplace?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
zlib: boolean;
|
||||
brotli?: boolean;
|
||||
assert: boolean;
|
||||
inspect: boolean;
|
||||
dynInvoke: boolean;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
funcOf,
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
STRING,
|
||||
@@ -40,6 +41,36 @@ const ZLIB_CALLBACK_FNS: Readonly<Record<string, IrLibFn | undefined>> = {
|
||||
unzip: "zlib.unzipCb",
|
||||
};
|
||||
|
||||
/* Node's BROTLI_DECODE and BROTLI_ENCODE modes. */
|
||||
const BROTLI_DECODE = 8;
|
||||
const BROTLI_ENCODE = 9;
|
||||
|
||||
/** The Brotli one-shots: every form takes the options entries. */
|
||||
const BROTLI_SYNC_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"brotliCompressSync",
|
||||
"brotliDecompressSync",
|
||||
]);
|
||||
const BROTLI_CALLBACK_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"brotliCompress",
|
||||
"brotliDecompress",
|
||||
]);
|
||||
|
||||
/** A Brotli mode connects the codec (scr_brotli.c) to the zlib engine
|
||||
* before any module initializer runs. */
|
||||
function noteBrotliMode(lowerer: Lowerer, mode: number, loc: SrcLoc): void {
|
||||
if (mode !== BROTLI_DECODE && mode !== BROTLI_ENCODE) return;
|
||||
const installs = (stmt: IrStmt): boolean =>
|
||||
stmt.kind === "exprStmt" &&
|
||||
stmt.expr.kind === "libCall" &&
|
||||
stmt.expr.fn === "zlib.brotliInstall";
|
||||
if (lowerer.mainPrelude.some(installs)) return;
|
||||
lowerer.mainPrelude.push({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "zlib.brotliInstall", args: [], type: VOID, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
const ZLIB_STREAM_CLASS = "%Transform";
|
||||
const ZLIB_STREAM_T: IrType = { kind: "object", className: ZLIB_STREAM_CLASS };
|
||||
|
||||
@@ -115,6 +146,7 @@ export function lowerZlibStreamNew(
|
||||
}
|
||||
const options =
|
||||
args[0] !== undefined ? zlibOptionsValue(lowerer, args[0], what, loc) : dynUndefinedExpr(loc);
|
||||
noteBrotliMode(lowerer, mode, loc);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "zlib.streamNew",
|
||||
@@ -154,8 +186,8 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when `access` names a member the zlib stream base class declares
|
||||
* (bytesWritten, flush, params, close, reset). */
|
||||
/** True when `access` names a member the zlib stream base classes declare
|
||||
* (bytesWritten, flush, close and reset on ZlibBase; params on Zlib). */
|
||||
export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
if (!symbol) return false;
|
||||
@@ -163,7 +195,7 @@ export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessEx
|
||||
(d) =>
|
||||
d.parent !== undefined &&
|
||||
ts.isClassDeclaration(d.parent) &&
|
||||
d.parent.name?.text === "ZlibBase" &&
|
||||
(d.parent.name?.text === "ZlibBase" || d.parent.name?.text === "Zlib") &&
|
||||
isZlibFallbackDecl(lowerer, d.parent),
|
||||
);
|
||||
}
|
||||
@@ -281,12 +313,17 @@ const ZLIB_ONE_SHOT_MODES: Readonly<Record<string, number | undefined>> = {
|
||||
deflateRaw: 5,
|
||||
inflateRaw: 6,
|
||||
unzip: 7,
|
||||
brotliDecompressSync: BROTLI_DECODE,
|
||||
brotliCompressSync: BROTLI_ENCODE,
|
||||
brotliDecompress: BROTLI_DECODE,
|
||||
brotliCompress: BROTLI_ENCODE,
|
||||
};
|
||||
|
||||
/** True for a zlib callback one-shot util.promisify converts (deflate,
|
||||
* inflate, deflateRaw, inflateRaw, gzip, gunzip and unzip). */
|
||||
* inflate, deflateRaw, inflateRaw, gzip, gunzip, unzip, brotliCompress and
|
||||
* brotliDecompress). */
|
||||
export function isPromisifiedZlibMember(member: string): boolean {
|
||||
return ZLIB_CALLBACK_FNS[member] !== undefined;
|
||||
return ZLIB_CALLBACK_FNS[member] !== undefined || BROTLI_CALLBACK_MEMBERS.has(member);
|
||||
}
|
||||
|
||||
/** A call through `promisify(zlib.gzip)` and the other callback one-shots:
|
||||
@@ -311,6 +348,7 @@ export function lowerPromisifiedZlibCall(
|
||||
);
|
||||
}
|
||||
const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc);
|
||||
noteBrotliMode(lowerer, ZLIB_ONE_SHOT_MODES[member]!, loc);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: input.dyn ? "zlib.promiseDyn" : "zlib.promise",
|
||||
@@ -334,6 +372,8 @@ const ZLIB_STREAM_FACTORIES: Readonly<Record<string, number | undefined>> = {
|
||||
createDeflateRaw: 5,
|
||||
createInflateRaw: 6,
|
||||
createUnzip: 7,
|
||||
createBrotliDecompress: BROTLI_DECODE,
|
||||
createBrotliCompress: BROTLI_ENCODE,
|
||||
};
|
||||
|
||||
export function lowerZlibModuleCall(
|
||||
@@ -356,6 +396,9 @@ export function lowerZlibModuleCall(
|
||||
loc,
|
||||
);
|
||||
}
|
||||
if (BROTLI_SYNC_MEMBERS.has(bi.member) || BROTLI_CALLBACK_MEMBERS.has(bi.member)) {
|
||||
return lowerBrotliOneShot(lowerer, expr, bi.member, loc);
|
||||
}
|
||||
const syncFn = ZLIB_SYNC_FNS[bi.member];
|
||||
if (syncFn !== undefined) {
|
||||
if (expr.arguments.nodes.length === 0 || expr.arguments.nodes.length > 2) {
|
||||
@@ -502,6 +545,57 @@ export function lowerZlibModuleCall(
|
||||
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
|
||||
}
|
||||
|
||||
/** brotliCompressSync(data[, options]), brotliCompress(data[, options],
|
||||
* callback) and the decompress pair: the options entries with Node's
|
||||
* BROTLI_ENCODE or BROTLI_DECODE mode, an absent options value passing
|
||||
* undefined. */
|
||||
function lowerBrotliOneShot(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
member: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const args = expr.arguments.nodes;
|
||||
const mode = ZLIB_ONE_SHOT_MODES[member]!;
|
||||
const sync = BROTLI_SYNC_MEMBERS.has(member);
|
||||
if (sync ? args.length === 0 || args.length > 2 : args.length !== 2 && args.length !== 3) {
|
||||
lowerer.noLowering(
|
||||
`zlib.${member} with ${args.length} arguments`,
|
||||
expr,
|
||||
sync
|
||||
? `${member}(data) and ${member}(data, options) are supported`
|
||||
: `${member}(data, callback) and ${member}(data, options, callback) are supported`,
|
||||
);
|
||||
}
|
||||
const input = zlibInput(lowerer, args[0]!, loc);
|
||||
const withOptions = sync ? args.length === 2 : args.length === 3;
|
||||
const options = withOptions
|
||||
? zlibOptionsValue(lowerer, args[1]!, `zlib.${member}`, loc)
|
||||
: dynUndefinedExpr(loc);
|
||||
noteBrotliMode(lowerer, mode, loc);
|
||||
if (sync) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: input.dyn ? "zlib.syncOptsDyn" : "zlib.syncOpts",
|
||||
args: [numLit(mode, loc), input.value, options],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: input.dyn ? "zlib.cbOptsDyn" : "zlib.cbOpts",
|
||||
args: [
|
||||
numLit(mode, loc),
|
||||
input.value,
|
||||
options,
|
||||
errorFirstBytesCallback(lowerer, args[withOptions ? 2 : 1]!, "zlib"),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The class name of a zlib stream mode (Node's constructor name). */
|
||||
export function zlibClassName(mode: number): string {
|
||||
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
|
||||
|
||||
@@ -403,7 +403,10 @@ export function lowerWebAssemblyValueOf(
|
||||
recv: IrExpr,
|
||||
loc: IrExpr["loc"],
|
||||
): IrExpr | null {
|
||||
const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile };
|
||||
const ctx = {
|
||||
checker: lowerer.checker,
|
||||
isStdlibFile: (sf: ts.SourceFile) => lowerer.isStdlibFile(sf),
|
||||
};
|
||||
if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null;
|
||||
lowerer.usesWebAssembly = true;
|
||||
return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { primitiveConstructorOr } from "./lower-exprs.js";
|
||||
import { TO_STRING_TAG_FIELD, wellKnownSymbolOfField } from "./symbol-fields.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrParam,
|
||||
@@ -153,6 +155,41 @@ const STREAM_NUM_PROPERTIES = new Set([
|
||||
const streamProperty = (name: string): boolean =>
|
||||
STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name);
|
||||
|
||||
/** `(instance) => string`, the string a class's inherited
|
||||
* Symbol.toStringTag getter answers, which the runtime reads for
|
||||
* Object.prototype.toString and util.inspect of an instance seen as
|
||||
* `unknown`. None for a class without a string getter, or whose own tag
|
||||
* field shadows it. */
|
||||
function stringTagHelper(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrFunction | null {
|
||||
if (info.fields.has(TO_STRING_TAG_FIELD)) return null;
|
||||
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
if (getter?.sig.ret.kind !== "string") return null;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const owner = getter.declarer.def.name;
|
||||
return {
|
||||
name: `%class.stringTag:${info.def.name}`,
|
||||
params: [{ localId: "this", name: "this", type }],
|
||||
locals: [{ id: "this", name: "this", type, mutable: false }],
|
||||
returnType: STRING,
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(varRef("this", type, loc), owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function symbolMemberKey(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
@@ -1375,10 +1412,27 @@ export class ClassDynamicDispatch {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
if (info.symbolFields && info.def.symbolFields === undefined) {
|
||||
info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : [];
|
||||
});
|
||||
info.def.symbolFields = [...info.symbolFields].flatMap(
|
||||
([symbol, field]): NonNullable<IrClassDef["symbolFields"]> => {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
if (global?.type.kind === "symbol") return [{ field, globalId: global.id }];
|
||||
const wellKnown = wellKnownSymbolOfField(field);
|
||||
return wellKnown === null ? [] : [{ field, wellKnown }];
|
||||
},
|
||||
);
|
||||
}
|
||||
const tagged = (current: ClassInfo): ClassInfo[] => [
|
||||
current,
|
||||
...current.subclasses.flatMap(tagged),
|
||||
];
|
||||
for (const current of tagged(info)) {
|
||||
if (current.def.stringTagHelper !== undefined) continue;
|
||||
const fn = stringTagHelper(lowerer, current, functions[0]!.loc);
|
||||
if (!fn) continue;
|
||||
current.def.stringTagHelper = fn.name;
|
||||
this.generated.add(fn);
|
||||
lowerer.liftedFns.push(fn);
|
||||
changed = true;
|
||||
}
|
||||
if (this.prototypeHelper && !this.prototypeClasses.has(className)) {
|
||||
this.prototypeClasses.add(className);
|
||||
@@ -3662,10 +3716,29 @@ export class ClassDynamicDispatch {
|
||||
name,
|
||||
lowerer.coerceToExpected(varRef(global.id, global.type, loc), DYN),
|
||||
);
|
||||
} else if (wellKnownSymbolOfField(name) !== null) {
|
||||
// A field keyed by a well-known symbol (Symbol.toStringTag).
|
||||
symbols.set(name, {
|
||||
kind: "dynFrom",
|
||||
type: DYN,
|
||||
loc,
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "sym.wellKnown",
|
||||
args: [
|
||||
{ kind: "strLit", value: wellKnownSymbolOfField(name)!, type: STRING, loc },
|
||||
],
|
||||
type: SYMBOL_T,
|
||||
loc,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
|
||||
for (const name of [...owner.methods.keys()])
|
||||
for (const method of [...owner.methods.keys()]) {
|
||||
// Accessors key by the property they define (`get [Symbol.toStringTag]`).
|
||||
const name =
|
||||
method.startsWith("get:") || method.startsWith("set:") ? method.slice(4) : method;
|
||||
if (name.startsWith("sym:")) {
|
||||
symbols.set(name, {
|
||||
kind: "dynFrom",
|
||||
@@ -3680,6 +3753,7 @@ export class ClassDynamicDispatch {
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const branches: IrStmt[] = [];
|
||||
if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) {
|
||||
|
||||
@@ -36,9 +36,20 @@ export function collectClassMethodMutations(
|
||||
const computedWrites: {
|
||||
key: ts.Expression | undefined;
|
||||
receiver: ts.Expression | undefined;
|
||||
/** The written element access, when the write has one. */
|
||||
target?: ts.ElementAccessExpression;
|
||||
site: ts.Node;
|
||||
}[] = [];
|
||||
const propertyWrites: { target: ts.PropertyAccessExpression; site: ts.Node }[] = [];
|
||||
/** Writes and deletes of a method name through an element access with a
|
||||
* known key (`o["m"] = f`, `o[k] = f` with `k: "m" | "n"`), judged like
|
||||
* the named writes in propertyWrites. */
|
||||
const keyedWrites: {
|
||||
name: string;
|
||||
receiver: ts.Expression;
|
||||
target: ts.ElementAccessExpression;
|
||||
site: ts.Node;
|
||||
}[] = [];
|
||||
const reflection: ts.PropertyAccessExpression[] = [];
|
||||
const globals: ts.Identifier[] = [];
|
||||
const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = [];
|
||||
@@ -274,13 +285,32 @@ export function collectClassMethodMutations(
|
||||
sameBinding(receiver, keys.arguments.nodes[0]!)
|
||||
);
|
||||
};
|
||||
const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => {
|
||||
/** A write of the known key `name`: through an element access, a write
|
||||
* to a method name is judged like a named write (keyedWrites). */
|
||||
const markKeyed = (
|
||||
name: string,
|
||||
site: ts.Node,
|
||||
receiver?: ts.Expression,
|
||||
target?: ts.ElementAccessExpression,
|
||||
): void => {
|
||||
if (receiver && target && methods.has(name) && name !== "prototype" && name !== "__proto__") {
|
||||
receiverFields.add(name);
|
||||
keyedWrites.push({ name, receiver, target, site });
|
||||
} else markName(name, site);
|
||||
};
|
||||
const markKey = (
|
||||
key: ts.Expression,
|
||||
site: ts.Node,
|
||||
receiver?: ts.Expression,
|
||||
target?: ts.ElementAccessExpression,
|
||||
): void => {
|
||||
const value = unwrap(key);
|
||||
if (ts.isStringLiteralLike(value)) markName(value.text, site);
|
||||
else if (ts.isNumericLiteral(value)) markName(String(Number(value.text)), site);
|
||||
if (ts.isStringLiteralLike(value)) markKeyed(value.text, site, receiver, target);
|
||||
else if (ts.isNumericLiteral(value))
|
||||
markKeyed(String(Number(value.text)), site, receiver, target);
|
||||
// The key keeps its assertions: `slots[at as number]` and `slots[at!]`
|
||||
// write a number key whatever the type of `at`.
|
||||
else computedWrites.push({ key, receiver, site });
|
||||
else computedWrites.push(target ? { key, receiver, target, site } : { key, receiver, site });
|
||||
};
|
||||
const markFields = (value: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => {
|
||||
value = unwrap(value);
|
||||
@@ -307,7 +337,7 @@ export function collectClassMethodMutations(
|
||||
propertyWrites.push({ target, site });
|
||||
} else markName(name, site, arrow);
|
||||
} else if (ts.isElementAccessExpression(target))
|
||||
markKey(target.argumentExpression, site, target.expression);
|
||||
markKey(target.argumentExpression, site, target.expression, target);
|
||||
else if (ts.isArrayLiteralExpression(target)) {
|
||||
for (const element of target.elements.nodes) {
|
||||
if (ts.isSpreadElement(element)) markTarget(element.expression, site);
|
||||
@@ -426,7 +456,19 @@ export function collectClassMethodMutations(
|
||||
(ts.isElementAccessExpression(base) &&
|
||||
ts.isStringLiteralLike(base.argumentExpression) &&
|
||||
base.argumentExpression.text === "prototype");
|
||||
if (prototype) {
|
||||
// `Object.prototype.toString.call(value)` calls the builtin and
|
||||
// keeps no reference to it, so it replaces nothing.
|
||||
const builtinCall =
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
ts.isPropertyAccessExpression(base) &&
|
||||
ts.isIdentifier(unwrap(base.expression)) &&
|
||||
lowerer.isStdlibGlobal(unwrap(base.expression) as ts.Identifier, "Object") &&
|
||||
ts.isPropertyAccessExpression(node.parent) &&
|
||||
node.parent.expression === node &&
|
||||
(node.parent.name.text === "call" || node.parent.name.text === "apply") &&
|
||||
ts.isCallExpression(node.parent.parent) &&
|
||||
node.parent.parent.expression === node.parent;
|
||||
if (prototype && !builtinCall) {
|
||||
if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node);
|
||||
else if (ts.isStringLiteralLike(node.argumentExpression))
|
||||
lowerer.noteMethodAccess(node.argumentExpression.text, node);
|
||||
@@ -611,7 +653,7 @@ export function collectClassMethodMutations(
|
||||
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
|
||||
else lowerer.noteMethodAccess(name, site);
|
||||
}
|
||||
for (const { key, receiver, site } of computedWrites) {
|
||||
for (const { key, receiver, target, site } of computedWrites) {
|
||||
// A class method declared with a well-known symbol key is not a write;
|
||||
// any other write with one can replace that method.
|
||||
const symbolName = key ? wellKnownMethodName(unwrap(key)) : null;
|
||||
@@ -628,7 +670,7 @@ export function collectClassMethodMutations(
|
||||
const type = key ? lowerer.typeOf(key) : undefined;
|
||||
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null));
|
||||
if (names) {
|
||||
for (const name of names) markName(String(name), site);
|
||||
for (const name of names) markKeyed(String(name), site, receiver, target);
|
||||
continue;
|
||||
}
|
||||
if (key && receiver && preservesExistingMethod(key, receiver, site)) continue;
|
||||
@@ -650,6 +692,19 @@ export function collectClassMethodMutations(
|
||||
markUnknownKey(site);
|
||||
}
|
||||
}
|
||||
lowerer.checker.prefetchClassCollection(
|
||||
keyedWrites.flatMap((write) => [write.target, write.receiver]),
|
||||
[],
|
||||
);
|
||||
for (const { name, receiver, target, site } of keyedWrites) {
|
||||
if (
|
||||
!dictionaryView(receiver) &&
|
||||
(functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(receiver, name))
|
||||
)
|
||||
continue;
|
||||
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
|
||||
else lowerer.noteMethodAccess(name, site);
|
||||
}
|
||||
if (numericKey) {
|
||||
for (const name of [...methods, ...arrowFields]) {
|
||||
if (String(Number(name)) === name) markName(name, numericKey);
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import { TO_STRING_TAG_FIELD } from "./symbol-fields.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js";
|
||||
import { dateValueMethod } from "./builtins/dates.js";
|
||||
@@ -29,7 +30,15 @@ type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString";
|
||||
* Date`, or through Object.prototype.toString ("[object Object]"). */
|
||||
type Plan =
|
||||
| { method: ConversionMethod; ret: IrType }
|
||||
| { method: null; date?: "toString" | "valueOf" };
|
||||
| { method: null; date?: "toString" | "valueOf"; tag?: ToStringTag };
|
||||
|
||||
/** Where Object.prototype.toString finds a class's Symbol.toStringTag: a
|
||||
* string field (`[Symbol.toStringTag] = "LRUCache"`) or a getter that
|
||||
* returns a string, declared on `owner`. */
|
||||
interface ToStringTag {
|
||||
owner: ClassInfo;
|
||||
getter: boolean;
|
||||
}
|
||||
|
||||
const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]);
|
||||
|
||||
@@ -52,6 +61,17 @@ export function classToStringConversion(
|
||||
);
|
||||
}
|
||||
|
||||
/** Object.prototype.toString of a program class instance: "[object " +
|
||||
* its Symbol.toStringTag + "]" (an own tag field, else an inherited
|
||||
* getter), or "[object Object]". The class's own conversion methods do not
|
||||
* take part. Null as for classToStringConversion, and for Date subclasses. */
|
||||
export function classObjectTag(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr | null {
|
||||
if (value.type.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || dateRootOf(info) !== null) return null;
|
||||
return classConversion(lowerer, value, "tag", node, STRING, (tag) => tag);
|
||||
}
|
||||
|
||||
/** ToNumber(ToPrimitive(value, "number")) of a program class instance
|
||||
* (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then
|
||||
* `toString`, and NaN for "[object Object]". Null as for
|
||||
@@ -122,7 +142,7 @@ function mayBeBigint(lowerer: Lowerer, type: IrType): boolean {
|
||||
function classConversion(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
hint: ConversionHint,
|
||||
hint: ConversionHint | "tag",
|
||||
node: ts.Node,
|
||||
resultType: IrType,
|
||||
finish: (primitive: IrExpr) => IrExpr,
|
||||
@@ -131,7 +151,7 @@ function classConversion(
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || !plainProgramClass(info)) return null;
|
||||
const classes = subtree(lowerer, info);
|
||||
for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
if (hint !== "tag") for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)]));
|
||||
// A class whose plan differs from its base's starts a group; the deepest
|
||||
// groups are tested first, so each instance takes its own class's plan.
|
||||
@@ -164,12 +184,52 @@ function classConversion(
|
||||
};
|
||||
return finish(dateValueMethod(plan.date, time, loc)!);
|
||||
}
|
||||
if (plan.method === null && plan.tag) {
|
||||
// Object.prototype.toString: "[object " + the tag + "]".
|
||||
const owner = plan.tag.owner.def.name;
|
||||
const tag: IrExpr = plan.tag.getter
|
||||
? accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(object, owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
)
|
||||
: {
|
||||
kind: "fieldGet",
|
||||
obj: lowerer.upcastTo(object, owner),
|
||||
className: owner,
|
||||
field: TO_STRING_TAG_FIELD,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return finish({
|
||||
kind: "strConcat",
|
||||
left: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: "[object ", type: STRING, loc },
|
||||
right: tag,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "strLit", value: "]", type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (plan.method === null)
|
||||
return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc });
|
||||
const args: IrExpr[] =
|
||||
plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : [];
|
||||
return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc));
|
||||
};
|
||||
// "[object Object]" reads nothing of the instance, but the receiver
|
||||
// expression still runs, once.
|
||||
const own = plans.get(info)!;
|
||||
if (tops.length === 0 && own.method === null && !own.date && !own.tag && !isSafeToRepeat(value))
|
||||
prefix.push({ kind: "exprStmt", expr: value, loc });
|
||||
let result = convert(info, receiver);
|
||||
for (let i = tops.length - 1; i >= 0; i--) {
|
||||
const top = tops[i]!;
|
||||
@@ -228,10 +288,28 @@ function depth(info: ClassInfo): number {
|
||||
}
|
||||
|
||||
function samePlan(a: Plan, b: Plan): boolean {
|
||||
return (
|
||||
a.method === b.method &&
|
||||
(a.method !== null || b.method !== null || a.date === (b as { date?: string }).date)
|
||||
);
|
||||
if (a.method !== null || b.method !== null) return a.method === b.method;
|
||||
return a.date === b.date && a.tag?.owner === b.tag?.owner && a.tag?.getter === b.tag?.getter;
|
||||
}
|
||||
|
||||
/** The class's Symbol.toStringTag, when it has one: the class at the top
|
||||
* of the chain whose layout holds the tag field (an own property, which
|
||||
* shadows any getter), or its getter. Other tag types would make
|
||||
* Object.prototype.toString answer "Object" or the value depending on the
|
||||
* run, and are refused. */
|
||||
function toStringTagOf(lowerer: Lowerer, info: ClassInfo, node: ts.Node): ToStringTag | null {
|
||||
const type = info.fields.get(TO_STRING_TAG_FIELD);
|
||||
if (type !== undefined) {
|
||||
if (type.kind !== "string") refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
|
||||
let owner = info;
|
||||
while (owner.base?.fields.has(TO_STRING_TAG_FIELD)) owner = owner.base;
|
||||
return { owner, getter: false };
|
||||
}
|
||||
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
if (!getter) return null;
|
||||
if (getter.sig.ret.kind !== "string")
|
||||
refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
|
||||
return { owner: getter.declarer, getter: true };
|
||||
}
|
||||
|
||||
/** A conversion method the class declares or inherits (a field of that
|
||||
@@ -246,7 +324,16 @@ function declared(
|
||||
return found ? { ret: found.sig.ret } : null;
|
||||
}
|
||||
|
||||
function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan {
|
||||
function planOf(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
hint: ConversionHint | "tag",
|
||||
node: ts.Node,
|
||||
): Plan {
|
||||
if (hint === "tag") {
|
||||
const tag = toStringTagOf(lowerer, info, node);
|
||||
return tag ? { method: null, tag } : { method: null };
|
||||
}
|
||||
const exotic = declared(lowerer, info, "sym:toPrimitive");
|
||||
if (exotic) {
|
||||
if (!primitive(lowerer, exotic.ret))
|
||||
@@ -271,7 +358,8 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t
|
||||
refuse(lowerer, node, info, `a '${method}' method that can return an object`);
|
||||
return { method, ret: found.ret };
|
||||
}
|
||||
return { method: null };
|
||||
const tag = toStringTagOf(lowerer, info, node);
|
||||
return tag ? { method: null, tag } : { method: null };
|
||||
}
|
||||
|
||||
function primitive(lowerer: Lowerer, type: IrType): boolean {
|
||||
@@ -292,8 +380,6 @@ function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.N
|
||||
if (lowerer.prototypeMethodAccesses.has(method))
|
||||
refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`);
|
||||
}
|
||||
if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag"))
|
||||
refuse(lowerer, node, info, "a Symbol.toStringTag");
|
||||
}
|
||||
|
||||
function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never {
|
||||
|
||||
@@ -1020,7 +1020,9 @@ class FusedLoop {
|
||||
private callee(arg: IrExpr & { type: FuncType }): Callee {
|
||||
const known = this.callees.find((c) => c.arg === arg);
|
||||
if (known) return known;
|
||||
const target = this.inlineTarget(arg);
|
||||
// Called as a plain function: the loop is not the target's receiver.
|
||||
const inlineTarget = this.inlineTarget;
|
||||
const target = inlineTarget(arg);
|
||||
let callee: Callee;
|
||||
if (target && arg.kind === "closure") {
|
||||
const captures = new Map<string, string>();
|
||||
|
||||
@@ -1182,10 +1182,10 @@ function bindSource(
|
||||
|
||||
/** Where chain code allocates its locals: a lifted helper function's own
|
||||
* frame, or hidden locals of the function being lowered (a fused for-of). */
|
||||
interface ChainFrame {
|
||||
local(base: string, type: IrType, mutable: boolean): string;
|
||||
abstract class ChainFrame {
|
||||
abstract local(base: string, type: IrType, mutable: boolean): string;
|
||||
/** `id = id + 1` over a number local. */
|
||||
bump(id: string): IrStmt;
|
||||
abstract bump(id: string): IrStmt;
|
||||
}
|
||||
|
||||
/** `id = id + 1` over a number local. */
|
||||
@@ -1206,11 +1206,13 @@ function bumpLocal(id: string, loc: SrcLoc): IrStmt {
|
||||
}
|
||||
|
||||
/** Hidden locals of the function being lowered. */
|
||||
class HostFrame implements ChainFrame {
|
||||
class HostFrame extends ChainFrame {
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
private readonly loc: SrcLoc,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
local(base: string, type: IrType, mutable: boolean): string {
|
||||
const local = this.lowerer.declareHiddenLocal(`%${base}`, type);
|
||||
local.mutable = mutable;
|
||||
@@ -1223,11 +1225,13 @@ class HostFrame implements ChainFrame {
|
||||
|
||||
/** The locals and parameters of a lifted helper function under
|
||||
* construction. */
|
||||
export class LiftedFrame implements ChainFrame {
|
||||
export class LiftedFrame extends ChainFrame {
|
||||
readonly locals: IrLocal[] = [];
|
||||
readonly params: IrParam[] = [];
|
||||
private n = 0;
|
||||
constructor(private readonly loc: SrcLoc) {}
|
||||
constructor(private readonly loc: SrcLoc) {
|
||||
super();
|
||||
}
|
||||
local(base: string, type: IrType, mutable: boolean): string {
|
||||
const id = `${base}.${this.n++}`;
|
||||
this.locals.push({ id, name: base, type, mutable });
|
||||
|
||||
@@ -707,11 +707,17 @@ function rewriteFunction(
|
||||
* unchecked. */
|
||||
export function refuseUncheckedCycleReads(
|
||||
lowerer: Lowerer,
|
||||
classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[],
|
||||
classes: readonly {
|
||||
baseValueGlobal?: string;
|
||||
symbolFields?: readonly ({ globalId: string } | { wellKnown: string })[];
|
||||
}[],
|
||||
): void {
|
||||
if (lowerer.cycleInitFlags.size === 0) return;
|
||||
for (const cls of classes) {
|
||||
const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)];
|
||||
const ids = [
|
||||
cls.baseValueGlobal,
|
||||
...(cls.symbolFields ?? []).map((f) => ("globalId" in f ? f.globalId : undefined)),
|
||||
];
|
||||
for (const id of ids) {
|
||||
const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id);
|
||||
if (flag === undefined) continue;
|
||||
|
||||
@@ -645,8 +645,7 @@ function extendsParameter(
|
||||
info: GenericFnInfo,
|
||||
): boolean {
|
||||
const params = new Set<ts.Symbol>();
|
||||
const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } };
|
||||
for (const param of fn.parameters?.nodes ?? []) {
|
||||
for (const param of info.decl.parameters.nodes) {
|
||||
const symbol = ts.isIdentifier(param.name)
|
||||
? lowerer.checker.getSymbolAtLocation(param.name)
|
||||
: undefined;
|
||||
@@ -672,8 +671,7 @@ function declaredInside(node: ts.Node, owner: ts.Node): boolean {
|
||||
* return gives anything else (or nothing). Nested functions and classes keep
|
||||
* their own returns. */
|
||||
function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null {
|
||||
const fn = info.decl as ts.Node & { body?: ts.Node };
|
||||
const body = fn.body;
|
||||
const body = info.decl.body;
|
||||
if (!body) return null;
|
||||
const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => {
|
||||
let expr = node;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import { extendInstantiationPath, MAX_INSTANTIATION_RECURSION } from "./instantiation-path.js";
|
||||
|
||||
/** A stand-in declaration: paths compare declarations by identity only. */
|
||||
const node = (): ts.Node => ({}) as ts.Node;
|
||||
|
||||
test("independent declarations and roots do not consume a recursive budget", () => {
|
||||
const declaration = {};
|
||||
const declaration = node();
|
||||
for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++)
|
||||
expect(extendInstantiationPath(null, declaration)).not.toBeNull();
|
||||
let path = extendInstantiationPath(null, declaration)!;
|
||||
for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++)
|
||||
path = extendInstantiationPath(path, {})!;
|
||||
path = extendInstantiationPath(path, node())!;
|
||||
expect(extendInstantiationPath(path, declaration)).not.toBeNull();
|
||||
});
|
||||
|
||||
test("bounds fresh recursive demands across intervening declarations", () => {
|
||||
const first = {};
|
||||
const second = {};
|
||||
const first = node();
|
||||
const second = node();
|
||||
let path = extendInstantiationPath(null, first)!;
|
||||
for (let i = 1; i < MAX_INSTANTIATION_RECURSION; i++) {
|
||||
path = extendInstantiationPath(path, second)!;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** Queued bodies preserve their demand ancestry. Independent concrete uses
|
||||
* do not consume a recursion budget; only fresh instances on one path do. */
|
||||
export interface InstantiationPath {
|
||||
declaration: object;
|
||||
declaration: ts.Node;
|
||||
parent: InstantiationPath | null;
|
||||
}
|
||||
|
||||
@@ -9,7 +11,7 @@ export const MAX_INSTANTIATION_RECURSION = 100;
|
||||
|
||||
export function extendInstantiationPath(
|
||||
parent: InstantiationPath | null,
|
||||
declaration: object,
|
||||
declaration: ts.Node,
|
||||
): InstantiationPath | null {
|
||||
let repetitions = 0;
|
||||
for (let path = parent; path; path = path.parent) {
|
||||
|
||||
@@ -187,7 +187,7 @@ import {
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerChildMethodValue, lowerChildPropertyCall } from "./handle-properties.js";
|
||||
import { classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { classObjectTag, classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { structuralReadType } from "./class-unions.js";
|
||||
import {
|
||||
@@ -3116,6 +3116,10 @@ function lowerObjectPrototypeCall(
|
||||
const immediate = immediateObjectTagReceiver(lowerer, receiverNode);
|
||||
const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode);
|
||||
const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type);
|
||||
if (tag === null && immediate === null && call.arguments.nodes.length === 1) {
|
||||
const classTag = classObjectTag(lowerer, receiver, receiverNode);
|
||||
if (classTag) return classTag;
|
||||
}
|
||||
// A generator object answers its tag through its checked capsule.
|
||||
if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") {
|
||||
lowerer.noLowering(
|
||||
|
||||
@@ -8161,7 +8161,10 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri
|
||||
member === "asyncIterator" ||
|
||||
member === "dispose" ||
|
||||
member === "asyncDispose" ||
|
||||
member === "toPrimitive"
|
||||
member === "toPrimitive" ||
|
||||
// A toStringTag field keeps its symbol field name; its accessor and
|
||||
// method forms take the method slot name.
|
||||
(member === "toStringTag" && !ts.isPropertyDeclaration(name.parent))
|
||||
) {
|
||||
return `sym:${member}`;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -45,9 +46,9 @@ import {
|
||||
shapeKeyOrders,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { symbolFieldDisplayName } from "./symbol-fields.js";
|
||||
import { symbolFieldDisplayName, TO_STRING_TAG_FIELD } from "./symbol-fields.js";
|
||||
import { eventTargetRootOf } from "./lower-event-target.js";
|
||||
import { dateRootOf } from "./builtins/date-classes.js";
|
||||
import { lowerSystemErrorMap } from "./lower-system-errors.js";
|
||||
@@ -2028,8 +2029,86 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
// observable way — Node's inspect omits them entirely (verified),
|
||||
// so a class whose only fields are private prints as `C {}`.
|
||||
const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name));
|
||||
// A Symbol.toStringTag getter (an own tag field is listed instead)
|
||||
// brackets the tag after the name, `Name [tag] {`, when it is a
|
||||
// nonempty string other than the name. A getter that throws shows
|
||||
// no tag, as in Node.
|
||||
const tagGetter = info.fields.has(TO_STRING_TAG_FIELD)
|
||||
? null
|
||||
: findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
let named: (plain: string, make: (head: IrExpr) => IrExpr) => IrExpr = (plain) =>
|
||||
strLit(plain, loc);
|
||||
if (tagGetter?.sig.ret.kind === "string") {
|
||||
const owner = tagGetter.declarer.def.name;
|
||||
locals.push(
|
||||
{ id: "tag.0", name: "tag", type: STRING, mutable: true },
|
||||
{ id: "tagError.0", name: "tagError", type: CAUGHT, mutable: false },
|
||||
);
|
||||
body.push(
|
||||
{ kind: "varDecl", localId: "tag.0", init: strLit("", loc), loc },
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "tag.0",
|
||||
value: accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(v(), owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
catchBody: [],
|
||||
catchLocalId: "tagError.0",
|
||||
finallyBody: null,
|
||||
loc,
|
||||
},
|
||||
);
|
||||
const tag = (): IrExpr => varRef("tag.0", STRING, loc);
|
||||
const shown: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "strEq",
|
||||
negated: true,
|
||||
left: tag(),
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: {
|
||||
kind: "strEq",
|
||||
negated: true,
|
||||
left: tag(),
|
||||
right: strLit(display, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const head = concatAll(
|
||||
[strLit(`${display === "" ? "Object" : display} [`, loc), tag(), strLit("]", loc)],
|
||||
loc,
|
||||
);
|
||||
named = (plain, make) => ({
|
||||
kind: "ternary",
|
||||
cond: shown,
|
||||
then: make(head),
|
||||
else_: strLit(plain, loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const after = (text: string) => (head: IrExpr) => concatAll([head, strLit(text, loc)], loc);
|
||||
if (visible.length === 0) {
|
||||
body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))];
|
||||
body.push(ret(named(display === "" ? "{}" : `${display} {}`, after(" {}"))));
|
||||
break;
|
||||
}
|
||||
const get = (field: string, type: IrType): IrExpr => ({
|
||||
@@ -2040,7 +2119,22 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
body = [depthGate(display === "" ? "[Object]" : `[${display}]`), ...begin()];
|
||||
body.push(
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc },
|
||||
then: [
|
||||
ret(
|
||||
named(display === "" ? "[Object]" : `[${display}]`, (head) =>
|
||||
concatAll([strLit("[", loc), head, strLit("]", loc)], loc),
|
||||
),
|
||||
),
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...begin(),
|
||||
);
|
||||
// def.fields carries layout order: the base chain first, then own —
|
||||
// exactly the own-property insertion order of a constructor that
|
||||
// assigns in declaration order (SEMANTICS.md 36's stance). SYMBOL-
|
||||
@@ -2069,7 +2163,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
ret(
|
||||
end(
|
||||
strLit("", loc),
|
||||
strLit(display === "" ? "{" : `${display} {`, loc),
|
||||
named(display === "" ? "{" : `${display} {`, after(" {")),
|
||||
strLit("}", loc),
|
||||
false,
|
||||
boolLit(false, loc),
|
||||
|
||||
@@ -1183,19 +1183,22 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
|
||||
}
|
||||
if (e.kind === ts.SyntaxKind.NullKeyword) return true;
|
||||
if (ts.isIdentifier(e) && e.text === "undefined") return true;
|
||||
if (ts.isConditionalExpression(e)) {
|
||||
return (
|
||||
jsDynHoldableInitializer(lowerer, e.whenTrue) &&
|
||||
jsDynHoldableInitializer(lowerer, e.whenFalse)
|
||||
);
|
||||
}
|
||||
// The arms of a conditional or logical expression: checked values, or
|
||||
// strings, numbers and booleans, which box into the checked-dynamic
|
||||
// result (`const APP_NAME = config.name || "pi"`).
|
||||
const arm = (x: ts.Expression): boolean => {
|
||||
if (jsDynHoldableInitializer(lowerer, x)) return true;
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(x))?.kind;
|
||||
return kind === "string" || kind === "f64" || kind === "bool";
|
||||
};
|
||||
if (ts.isConditionalExpression(e)) return arm(e.whenTrue) && arm(e.whenFalse);
|
||||
if (
|
||||
ts.isBinaryExpression(e) &&
|
||||
(e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
e.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
||||
e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
) {
|
||||
return jsDynHoldableInitializer(lowerer, e.left) && jsDynHoldableInitializer(lowerer, e.right);
|
||||
return arm(e.left) && arm(e.right);
|
||||
}
|
||||
// A member of a checked module value is itself checked, including a
|
||||
// callable whose inferred overloads cannot describe one native ABI.
|
||||
|
||||
@@ -3761,7 +3761,7 @@ export class Lowerer {
|
||||
positionOf: (node) => this.positions.of(node),
|
||||
shapes: this.shapes,
|
||||
unions: this.unions,
|
||||
classNamer: this.classNamer,
|
||||
classNamer: (decl) => this.classNamer(decl),
|
||||
resolveTypeParam: this.typeParamResolver,
|
||||
resolveTypeParamTs: this.typeParamTsResolver,
|
||||
isErasing: () => this.erasingDepth > 0,
|
||||
@@ -3842,8 +3842,8 @@ export class Lowerer {
|
||||
},
|
||||
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
|
||||
classImplementedProtocol: (type) => this.classImplementedProtocol(type),
|
||||
isStdlibFile: this.isStdlibFile,
|
||||
isNpmFile: this.isNpmFile,
|
||||
isStdlibFile: (sf) => this.isStdlibFile(sf),
|
||||
isNpmFile: (sf) => this.isNpmFile(sf),
|
||||
// The declaration-typed boundary's generated types (decl-projection.ts)
|
||||
// describe data, like an explicitly mapped external type surface.
|
||||
isExternalTypeFile: (sf) =>
|
||||
@@ -8365,16 +8365,24 @@ export class Lowerer {
|
||||
lower: () => IrFunction | null;
|
||||
}
|
||||
>();
|
||||
const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] =>
|
||||
roots.filter((root) => root !== undefined && root !== null);
|
||||
const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] =>
|
||||
bodyRoots(...decl.parameters.nodes.map((param) => param.initializer), decl.body);
|
||||
const classCtorRoots = (info: ClassInfo): ts.Node[] =>
|
||||
bodyRoots(
|
||||
...(info.ctor?.parameters?.nodes ?? []).map((param) => param.initializer),
|
||||
info.ctor?.body,
|
||||
...info.fieldOrder.map((field) => field.initializer),
|
||||
);
|
||||
// Roots are collected with plain pushes: spread arguments into a rest
|
||||
// parameter pass the nodes as dynamic values.
|
||||
const addRoot = (roots: ts.Node[], root: ts.Node | undefined | null): void => {
|
||||
if (root !== undefined && root !== null) roots.push(root);
|
||||
};
|
||||
const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => {
|
||||
const roots: ts.Node[] = [];
|
||||
for (const param of decl.parameters.nodes) addRoot(roots, param.initializer);
|
||||
addRoot(roots, decl.body);
|
||||
return roots;
|
||||
};
|
||||
const classCtorRoots = (info: ClassInfo): ts.Node[] => {
|
||||
const roots: ts.Node[] = [];
|
||||
for (const param of info.ctor?.parameters?.nodes ?? []) addRoot(roots, param.initializer);
|
||||
addRoot(roots, info.ctor?.body);
|
||||
for (const field of info.fieldOrder) addRoot(roots, field.initializer);
|
||||
return roots;
|
||||
};
|
||||
const classMemberRoots = (info: ClassInfo): ts.Node[] => {
|
||||
const roots = classCtorRoots(info);
|
||||
for (const { member } of this.classMethodMembers(info)) roots.push(...functionRoots(member));
|
||||
@@ -8551,7 +8559,8 @@ export class Lowerer {
|
||||
register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop));
|
||||
}
|
||||
};
|
||||
for (const name of collectionEdges) this.onEdge(name);
|
||||
const onEdge = this.onEdge;
|
||||
for (const name of collectionEdges) onEdge(name);
|
||||
// Generic instances queued by the bodies above lower here (an instance
|
||||
// body fires edges of its own and can queue further instances of
|
||||
// either kind — function instances and class instantiations drain to
|
||||
@@ -8686,7 +8695,8 @@ export class Lowerer {
|
||||
// profile-mapped exports are called from outside the graph, so they
|
||||
// seed the worklist beside the init bodies. Unknown names are inert
|
||||
// (the export-map resolution reports them as SC4002 later).
|
||||
for (const root of extraRoots ?? []) this.onEdge?.(root);
|
||||
const rootEdge = this.onEdge;
|
||||
for (const root of extraRoots ?? []) rootEdge?.(root);
|
||||
const shuffle = lowerShuffle();
|
||||
const drainUnits = (): void => {
|
||||
while (queue.length > 0) {
|
||||
@@ -8804,7 +8814,9 @@ export class Lowerer {
|
||||
/** Reachability hook (see emitReachable): fires when lowering resolves a
|
||||
* reference to a lowerable body. */
|
||||
noteEdge(name: string): void {
|
||||
if (this.onEdge) this.onEdge(name);
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const onEdge = this.onEdge;
|
||||
if (onEdge) onEdge(name);
|
||||
}
|
||||
|
||||
/** A subclass constructor's super() edge: the base constructor runs, but
|
||||
@@ -12204,12 +12216,15 @@ export class Lowerer {
|
||||
|
||||
/** Method-like members with their collected method-map names. Body
|
||||
* lowering omits abstract declarations; ABI analysis must include them
|
||||
* because an abstract-typed call selects the same virtual slot. */
|
||||
*classMethodMembers(
|
||||
* because an abstract-typed call selects the same virtual slot. An array,
|
||||
* not a generator: a generator's yielded values are dynamic values, which
|
||||
* would make every syntax node one. */
|
||||
classMethodMembers(
|
||||
info: ClassInfo,
|
||||
includeAbstract = false,
|
||||
): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> {
|
||||
if (!info.decl) return; // builtin error classes: runtime-provided bodies
|
||||
): { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] {
|
||||
const members: { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] = [];
|
||||
if (!info.decl) return members; // builtin error classes: runtime-provided bodies
|
||||
for (const member of info.decl.members.nodes) {
|
||||
const fnLike =
|
||||
ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)
|
||||
@@ -12233,8 +12248,9 @@ export class Lowerer {
|
||||
: `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${baseName}`;
|
||||
const signature = info.methods.get(mName);
|
||||
if (!signature || (!fnLike.body && !(includeAbstract && signature.abstract))) continue;
|
||||
yield { mName, member: fnLike };
|
||||
members.push({ mName, member: fnLike });
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
lowerClassCtor(info: ClassInfo): IrFunction {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import { SiteDependencies } from "./site-dependencies.js";
|
||||
|
||||
/** A stand-in node fact: facts are compared by identity. */
|
||||
const node = (): ts.Node => ({}) as ts.Node;
|
||||
|
||||
test("a changed fact makes the sites that read it stale", () => {
|
||||
const dependencies = new SiteDependencies(3);
|
||||
const a = {};
|
||||
const a = node();
|
||||
const b = "key";
|
||||
dependencies.begin(0);
|
||||
dependencies.read(a);
|
||||
@@ -20,8 +24,8 @@ test("a changed fact makes the sites that read it stale", () => {
|
||||
|
||||
test("a visit that changes a fact makes every reader of its facts stale", () => {
|
||||
const dependencies = new SiteDependencies(3);
|
||||
const shared = {};
|
||||
const own = {};
|
||||
const shared = node();
|
||||
const own = node();
|
||||
dependencies.begin(0);
|
||||
dependencies.read(shared);
|
||||
dependencies.end(false);
|
||||
@@ -37,7 +41,7 @@ test("a visit that changes a fact makes every reader of its facts stale", () =>
|
||||
|
||||
test("reads outside a visit are not recorded", () => {
|
||||
const dependencies = new SiteDependencies(1);
|
||||
const fact = {};
|
||||
const fact = node();
|
||||
dependencies.read(fact);
|
||||
dependencies.changed(fact);
|
||||
expect(dependencies.isStale(0)).toBe(false);
|
||||
@@ -45,7 +49,7 @@ test("reads outside a visit are not recorded", () => {
|
||||
|
||||
test("a site visited again is listed once per fact", () => {
|
||||
const dependencies = new SiteDependencies(3);
|
||||
const fact = {};
|
||||
const fact = node();
|
||||
for (const site of [0, 2, 1, 2, 0]) {
|
||||
dependencies.begin(site);
|
||||
dependencies.read(fact);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** The facts each site of a fixed-point scan read on its last visit, so that
|
||||
* a pass visits again only the sites whose facts changed since.
|
||||
*
|
||||
@@ -13,14 +15,19 @@
|
||||
* that with a pass over every site once a pass changes nothing, and resumes
|
||||
* when that pass changes a fact: a fact this tracking misses costs passes,
|
||||
* not a different result. */
|
||||
/** A fact a site reads: a symbol, a syntax node, or the name of a derived
|
||||
* fact. A typed union keeps nodes and symbols native values; as `unknown`
|
||||
* every syntax node would become a dynamic value. */
|
||||
export type SiteFact = ts.Node | ts.Symbol | string;
|
||||
|
||||
export class SiteDependencies {
|
||||
/** Each fact's entry: the visit that last read it, so that a visit lists a
|
||||
* fact once, followed by the sites that read it, ascending. */
|
||||
private readonly readers = new Map<unknown, number[]>();
|
||||
private readonly readers = new Map<SiteFact, number[]>();
|
||||
private readonly stale: Uint8Array;
|
||||
private site = -1;
|
||||
private visit = 0;
|
||||
private facts: unknown[] = [];
|
||||
private facts: SiteFact[] = [];
|
||||
|
||||
constructor(siteCount: number) {
|
||||
this.stale = new Uint8Array(siteCount);
|
||||
@@ -46,7 +53,7 @@ export class SiteDependencies {
|
||||
}
|
||||
|
||||
/** The site being visited read `fact`. */
|
||||
read(fact: unknown): void {
|
||||
read(fact: SiteFact): void {
|
||||
if (this.site < 0) return;
|
||||
const entry = this.readers.get(fact);
|
||||
if (entry === undefined) {
|
||||
@@ -72,7 +79,7 @@ export class SiteDependencies {
|
||||
}
|
||||
|
||||
/** `fact` changed: every site that read it is visited again. */
|
||||
changed(fact: unknown): void {
|
||||
changed(fact: SiteFact): void {
|
||||
const entry = this.readers.get(fact);
|
||||
if (entry !== undefined) for (let i = 1; i < entry.length; i++) this.stale[entry[i]!] = 1;
|
||||
}
|
||||
|
||||
@@ -182,11 +182,28 @@ export class StringIndexBounds {
|
||||
private readonly identifiers = new Map<ts.Node, Map<string, ts.Identifier[]>>();
|
||||
|
||||
constructor(
|
||||
private readonly symbolAt: (node: ts.Node) => ts.Symbol | null,
|
||||
private readonly declarationOf: (symbol: ts.Symbol) => ts.Node | undefined,
|
||||
private readonly isString: (node: ts.Expression) => boolean,
|
||||
private readonly lookupSymbol: (node: ts.Node) => ts.Symbol | null,
|
||||
private readonly lookupDeclaration: (symbol: ts.Symbol) => ts.Node | undefined,
|
||||
private readonly stringTyped: (node: ts.Expression) => boolean,
|
||||
) {}
|
||||
|
||||
// The lookups are called as plain functions: the instance is not their
|
||||
// receiver.
|
||||
private symbolAt(node: ts.Node): ts.Symbol | null {
|
||||
const lookup = this.lookupSymbol;
|
||||
return lookup(node);
|
||||
}
|
||||
|
||||
private declarationOf(symbol: ts.Symbol): ts.Node | undefined {
|
||||
const lookup = this.lookupDeclaration;
|
||||
return lookup(symbol);
|
||||
}
|
||||
|
||||
private isString(node: ts.Expression): boolean {
|
||||
const test = this.stringTyped;
|
||||
return test(node);
|
||||
}
|
||||
|
||||
/** True when `read` (an element access on a string receiver) is proven
|
||||
* to name an existing code unit. */
|
||||
inBounds(read: ts.ElementAccessExpression): boolean {
|
||||
|
||||
@@ -1635,6 +1635,12 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
unzip: { fn: "zlib.unzipCb", params: [], result: VOID },
|
||||
unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 },
|
||||
crc32: { fn: "zlib.crc32", params: [], result: F64 },
|
||||
// Brotli (special-cased in lowerZlibModuleCall): every form takes the
|
||||
// options entries with Node's BROTLI_ENCODE or BROTLI_DECODE mode.
|
||||
brotliCompress: { fn: "zlib.cbOpts", params: [], result: VOID },
|
||||
brotliCompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 },
|
||||
brotliDecompress: { fn: "zlib.cbOpts", params: [], result: VOID },
|
||||
brotliDecompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 },
|
||||
// The stream factories (special-cased in lowerZlibModuleCall): each
|
||||
// answers a runtime Transform stream over the zlib engine.
|
||||
createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
|
||||
@@ -1644,6 +1650,8 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
|
||||
createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
|
||||
createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
|
||||
createBrotliCompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
|
||||
createBrotliDecompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM },
|
||||
},
|
||||
url: {
|
||||
fileURLToPath: {
|
||||
@@ -3058,17 +3066,6 @@ export function builtinModulesArrayLit(loc: { file: string; start: number; end:
|
||||
};
|
||||
}
|
||||
|
||||
/** Member-specific hints for RECOGNIZED builtin modules whose member has
|
||||
* no lowering. The default-options one-shot zlib/deflate-raw/gzip codecs
|
||||
* lower for strings and Buffers; the remaining surface points at them. */
|
||||
const ZLIB_HINT =
|
||||
"the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, " +
|
||||
"and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface";
|
||||
/** Brotli has no codec in the static runtime (zlib is the only vendored
|
||||
* compression library). */
|
||||
const BROTLI_HINT =
|
||||
"Brotli needs a Brotli codec, which the static runtime does not vendor — " + ZLIB_HINT;
|
||||
|
||||
const ASSERT_MODULE_HINTS: Record<string, string | undefined> = {
|
||||
ifError:
|
||||
"test explicitly instead: assert.strictEqual(err, null) / " +
|
||||
@@ -3130,16 +3127,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
|
||||
"a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " +
|
||||
"in a non-FIPS build — getFips() answers 0 here",
|
||||
},
|
||||
zlib: {
|
||||
brotliCompressSync: BROTLI_HINT,
|
||||
brotliDecompressSync: BROTLI_HINT,
|
||||
brotliCompress: BROTLI_HINT,
|
||||
brotliDecompress: BROTLI_HINT,
|
||||
createBrotliCompress: BROTLI_HINT,
|
||||
createBrotliDecompress: BROTLI_HINT,
|
||||
BrotliCompress: BROTLI_HINT,
|
||||
BrotliDecompress: BROTLI_HINT,
|
||||
},
|
||||
http2: {
|
||||
connect:
|
||||
"HTTP/2 client sessions have no lowering — the lowered http2 surface is the SERVER side: " +
|
||||
|
||||
@@ -77,6 +77,8 @@ export function fenceSymbolFieldCopy(
|
||||
}
|
||||
|
||||
export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null {
|
||||
const wellKnown = wellKnownSymbolKeyOf(lowerer, key);
|
||||
if (wellKnown !== undefined) return wellKnown;
|
||||
if (
|
||||
!ts.isIdentifier(key) &&
|
||||
(!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name))
|
||||
@@ -90,6 +92,53 @@ export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSym
|
||||
return sym ? classSymbolKeyOfSymbol(lowerer, sym) : null;
|
||||
}
|
||||
|
||||
/** Well-known symbols a class field may be keyed by: those no protocol
|
||||
* consults on an instance, or whose protocol the lowering reads from the
|
||||
* field (Symbol.toStringTag, by the class string conversions). A field
|
||||
* keyed by any other well-known symbol (Symbol.iterator, toPrimitive,
|
||||
* hasInstance, ...) would be skipped by its protocol, so it keeps the
|
||||
* computed-key refusal. */
|
||||
const FIELD_WELL_KNOWN_SYMBOLS: ReadonlySet<string> = new Set(["toStringTag"]);
|
||||
|
||||
/** The key of `Symbol.<name>` on the standard global: undefined when the
|
||||
* expression is not one, null when it is a well-known symbol a field may
|
||||
* not be keyed by. The identity is the lib's declaration of the property,
|
||||
* one symbol for every program reference. */
|
||||
function wellKnownSymbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
key: ts.Expression,
|
||||
): ClassSymbolKey | null | undefined {
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
key.questionDotToken ||
|
||||
!ts.isIdentifier(key.expression) ||
|
||||
!ts.isIdentifier(key.name) ||
|
||||
!lowerer.isStdlibGlobal(key.expression, "Symbol")
|
||||
)
|
||||
return undefined;
|
||||
const name = key.name.text;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(key.name);
|
||||
if (!sym || !(lowerer.typeOf(key).flags & ts.TypeFlags.UniqueESSymbol)) return undefined;
|
||||
if (!FIELD_WELL_KNOWN_SYMBOLS.has(name)) return null;
|
||||
return { sym, identity: sym, fieldName: wellKnownSymbolFieldName(name) };
|
||||
}
|
||||
|
||||
/** The layout name of a field keyed by the well-known `Symbol.<name>`: a
|
||||
* reserved symbol-field name, printed as Node spells the key. */
|
||||
export function wellKnownSymbolFieldName(name: string): string {
|
||||
return `${FIELD_PREFIX}Symbol(Symbol.${name})`;
|
||||
}
|
||||
|
||||
/** The well-known symbol a field name is keyed by (`toStringTag`), or null. */
|
||||
export function wellKnownSymbolOfField(field: string): string | null {
|
||||
const match = /^%symbol:Symbol\(Symbol\.(\w+)\)$/.exec(field);
|
||||
return match === null ? null : match[1]!;
|
||||
}
|
||||
|
||||
/** The layout name of a `Symbol.toStringTag` field. */
|
||||
export const TO_STRING_TAG_FIELD = wellKnownSymbolFieldName("toStringTag");
|
||||
|
||||
/** A literal Symbol()/Symbol.for() initialized once at module scope. The
|
||||
* whole-file proof also covers var declarations emitted by bundlers. Keys
|
||||
* cannot be used before initialization: a computed class field evaluates
|
||||
|
||||
@@ -254,13 +254,18 @@ function aliasedPackageName(pkgDir: string): string | null {
|
||||
export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
delete pkg["types"];
|
||||
delete pkg["typings"];
|
||||
const transform = (v: unknown): void => {
|
||||
if (v === null || typeof v !== "object") return;
|
||||
if (Array.isArray(v)) {
|
||||
for (const item of v) transform(item);
|
||||
return;
|
||||
}
|
||||
const obj = v as Record<string, unknown>;
|
||||
const transform = (v: unknown): unknown => {
|
||||
if (v === null || typeof v !== "object") return v;
|
||||
if (Array.isArray(v)) return v.map(transform);
|
||||
// The result is a new object, built in key order; the first entry of a
|
||||
// key wins.
|
||||
const entries: [string, unknown][] = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (key: string, value: unknown): void => {
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
entries.push([key, value]);
|
||||
};
|
||||
// 0. The "node" condition HOISTS: Node's runtime resolution matches it
|
||||
// (yaml's `{ types, node: "./dist/index.js", default: "./browser/
|
||||
// index.js" }` runs the node dist; the browser dist is a DIFFERENT
|
||||
@@ -270,24 +275,18 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
// under import+require (either matcher finds it there), an object's
|
||||
// entries splice inline — earlier real keys keep winning, exactly
|
||||
// Node's first-match-in-key-order rule.
|
||||
if (obj["node"] !== undefined) {
|
||||
const entries = Object.entries(obj);
|
||||
for (const k of Object.keys(obj)) delete obj[k];
|
||||
for (const [k, val] of entries) {
|
||||
if (k !== "node") {
|
||||
if (!(k in obj)) obj[k] = val;
|
||||
continue;
|
||||
}
|
||||
const inner: Record<string, unknown> =
|
||||
typeof val === "string"
|
||||
? { import: val, require: val }
|
||||
: val !== null && typeof val === "object" && !Array.isArray(val)
|
||||
? (val as Record<string, unknown>)
|
||||
: {};
|
||||
for (const [ik, iv] of Object.entries(inner)) {
|
||||
if (!(ik in obj)) obj[ik] = iv;
|
||||
}
|
||||
for (const [key, value] of Object.entries(v as Record<string, unknown>)) {
|
||||
if (key !== "node") {
|
||||
add(key, value);
|
||||
continue;
|
||||
}
|
||||
const inner: Record<string, unknown> =
|
||||
typeof value === "string"
|
||||
? { import: value, require: value }
|
||||
: value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
for (const [innerKey, innerValue] of Object.entries(inner)) add(innerKey, innerValue);
|
||||
}
|
||||
// 1. The types claims go AFTER the hoist: a node object's own
|
||||
// "types" key (uuid's `node: { types: "./dist/index.d.ts", default:
|
||||
@@ -295,15 +294,17 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
// in place tsgo follows it, maps the hidden declaration to its
|
||||
// same-stem JS (dist/index.js, the browser build) and disagrees with
|
||||
// this resolver's runtime answer.
|
||||
delete obj["types"];
|
||||
delete obj["typings"];
|
||||
for (const value of Object.values(obj)) transform(value);
|
||||
return Object.fromEntries(
|
||||
entries
|
||||
.filter(([key]) => key !== "types" && key !== "typings")
|
||||
.map(([key, value]): [string, unknown] => [key, transform(value)]),
|
||||
);
|
||||
};
|
||||
if (pkg["exports"] !== undefined) transform(pkg["exports"]);
|
||||
if (pkg["exports"] !== undefined) pkg["exports"] = transform(pkg["exports"]);
|
||||
// The "imports" field maps "#" specifiers with the same condition objects
|
||||
// (chalk 5's `"#supports-color": { node, default }`), so Node's "node"
|
||||
// target hoists there too.
|
||||
if (pkg["imports"] !== undefined) transform(pkg["imports"]);
|
||||
if (pkg["imports"] !== undefined) pkg["imports"] = transform(pkg["imports"]);
|
||||
}
|
||||
|
||||
/** npmStaticTransformPkgJson over a package.json TEXT (the fs shadow's
|
||||
|
||||
@@ -3927,10 +3927,14 @@ function exactCycleRefusal7(
|
||||
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
||||
return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
||||
}
|
||||
// `export * from` binds each name to the declaring member's own
|
||||
// binding, like a named re-export, so only `export * as ns` (a
|
||||
// namespace object of a member) has state of its own.
|
||||
if (
|
||||
ts.isExportDeclaration(stmt) &&
|
||||
!stmt.isTypeOnly &&
|
||||
(stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause))
|
||||
stmt.exportClause !== undefined &&
|
||||
ts.isNamespaceExport(stmt.exportClause)
|
||||
) {
|
||||
const dep = targetOf(stmt);
|
||||
if (dep !== null && members.has(dep)) {
|
||||
|
||||
@@ -52,8 +52,10 @@ export class FrontendServices {
|
||||
}
|
||||
private sourceParser(): Ts7SourceParser {
|
||||
this.ensureOpen();
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const createApi = this.createApi;
|
||||
return (this.parser ??= new Ts7SourceParser(
|
||||
(options) => this.createApi({ ...options, collectTiming: false }),
|
||||
(options) => createApi({ ...options, collectTiming: false }),
|
||||
this.cwd,
|
||||
));
|
||||
}
|
||||
@@ -62,9 +64,10 @@ export class FrontendServices {
|
||||
}
|
||||
evaluateComptime(source: string, timeoutMs: number): unknown {
|
||||
this.ensureOpen();
|
||||
if (this.comptimeEvaluator === undefined)
|
||||
const evaluate = this.comptimeEvaluator;
|
||||
if (evaluate === undefined)
|
||||
throw new Error("this compiler host does not provide compile-time evaluation");
|
||||
return this.comptimeEvaluator(source, timeoutMs);
|
||||
return evaluate(source, timeoutMs);
|
||||
}
|
||||
createProgramHost(options?: Ts7HostOptions): Ts7Host {
|
||||
this.ensureOpen();
|
||||
|
||||
@@ -64,7 +64,9 @@ export interface SemanticQuery {
|
||||
export class SemanticProgram {
|
||||
constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {}
|
||||
getSourceFile(path: string): AstNode | undefined {
|
||||
return this.sourceFile(path);
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const sourceFile = this.sourceFile;
|
||||
return sourceFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ export class Ts7SourceParser {
|
||||
}
|
||||
|
||||
private connect(): Ts7Api {
|
||||
return this.createApi({
|
||||
// Called as a plain function: the instance is not its receiver.
|
||||
const createApi = this.createApi;
|
||||
return createApi({
|
||||
cwd: this.cwd,
|
||||
fs: {
|
||||
readFile: (path) => this.files.get(tsgoPath(path)) ?? null,
|
||||
|
||||
@@ -596,6 +596,7 @@ async function compileExecutableNative(
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
inspect: features.inspect,
|
||||
dynInvoke: features.dynInvoke,
|
||||
|
||||
@@ -3193,6 +3193,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* the +1 %Transform stream. flush takes the kind value, params the level
|
||||
* and strategy values; the Cb forms move their () => void callback. The
|
||||
* receivers are borrowed. */
|
||||
/** Connects scr_brotli.c to the zlib engine (%main, before any module). */
|
||||
"zlib.brotliInstall": { argTypes: [], result: VOID },
|
||||
"zlib.streamNew": { argTypes: [F64, DYN], result: VOID },
|
||||
"zlib.streamBytesWritten": { argTypes: [null], result: F64 },
|
||||
/** `stream instanceof zlib.Gzip`: the borrowed stream and the class's
|
||||
|
||||
@@ -157,10 +157,12 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"classes[].errorNameAfter": "other",
|
||||
"classes[].prototypeDataHelper": "reference",
|
||||
"classes[].instancePrototypeHelper": "reference",
|
||||
"classes[].stringTagHelper": "reference",
|
||||
"classes[].localCaptures[].name": "other",
|
||||
"classes[].localCaptures[].localId": "other",
|
||||
"classes[].symbolFields[].field": "other",
|
||||
"classes[].symbolFields[].globalId": "other",
|
||||
"classes[].symbolFields[].wellKnown": "other",
|
||||
"classes[].fields[].name": "other",
|
||||
"classes[].methods[]": "method",
|
||||
"classes[].prototypeNames[]": "other",
|
||||
@@ -282,6 +284,10 @@ const REFERENCE_FIXTURES: Record<string, () => IrModule> = {
|
||||
...module(),
|
||||
classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }],
|
||||
}),
|
||||
"classes[].stringTagHelper": () => ({
|
||||
...module(),
|
||||
classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }],
|
||||
}),
|
||||
"lib.exports[].fnName": () => ({
|
||||
...module(),
|
||||
lib: {
|
||||
|
||||
@@ -24,6 +24,7 @@ export function moduleFunctionReferences(mod: IrModule): string[] {
|
||||
for (const cls of mod.classes ?? []) {
|
||||
if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper);
|
||||
if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper);
|
||||
if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -1498,6 +1498,9 @@ export interface IrClassDef {
|
||||
prototypeDataHelper?: string;
|
||||
/** Native instance to its actual prototype, including factory captures and subclasses. */
|
||||
instancePrototypeHelper?: string;
|
||||
/** Native instance to the string its inherited Symbol.toStringTag getter
|
||||
* answers, read by the runtime for an instance seen as `unknown`. */
|
||||
stringTagHelper?: string;
|
||||
/** Public JavaScript fields record their creation in the shared property
|
||||
* bag. Its descriptors preserve presence and insertion order independently
|
||||
* of native layout order; field values remain in their native slots. */
|
||||
@@ -1511,8 +1514,10 @@ export interface IrClassDef {
|
||||
* dynamic write keeps `name` and `message` where the bag recorded them
|
||||
* (scr_error_dyn_follow_bag). */
|
||||
errorBagOrder?: boolean;
|
||||
/** Stable module symbol identities for public instance layout fields. */
|
||||
symbolFields?: { field: string; globalId: string }[];
|
||||
/** Stable symbol identities for public instance layout fields: the
|
||||
* module global holding the key, or the name of the well-known symbol
|
||||
* (`toStringTag` for `Symbol.toStringTag`). */
|
||||
symbolFields?: ({ field: string } & ({ globalId: string } | { wellKnown: string }))[];
|
||||
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
|
||||
* helpers, and vtable live in the runtime (ScrError / scr_error_*), so
|
||||
* backends emit no definitions for it — only the preorder-interval
|
||||
@@ -1685,6 +1690,8 @@ export const ZLIB_STREAM_MODES: ReadonlyMap<string, number> = new Map([
|
||||
["DeflateRaw", 5],
|
||||
["InflateRaw", 6],
|
||||
["Unzip", 7],
|
||||
["BrotliDecompress", 8],
|
||||
["BrotliCompress", 9],
|
||||
]);
|
||||
|
||||
export interface IrRecordShape {
|
||||
|
||||
@@ -715,6 +715,10 @@ interface RuntimeFeatureRule {
|
||||
/** Gives every rule in the table one type. */
|
||||
const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r;
|
||||
|
||||
/* The node:url and WHATWG URL entry points: every URL value comes from
|
||||
* one, and parsing one can reach the UTS #46 host processing. */
|
||||
const URL_PREFIXES: readonly string[] = ["url."];
|
||||
|
||||
/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */
|
||||
const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."];
|
||||
|
||||
@@ -1063,9 +1067,10 @@ const RUNTIME_FEATURE_RULES = {
|
||||
/** scr_url_legacy.c parses and stringifies queries. */
|
||||
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
|
||||
/** scr_url_legacy.c lowercases and normalizes hostnames with the regex
|
||||
* unit's libunicode, NODE_DEBUG matching in scr_util_callable.c and
|
||||
* readline's terminal unit use the regex unit, and locale case mapping
|
||||
* runs its default conversion. */
|
||||
* unit's libunicode, the WHATWG URL host parser runs UTS #46 through
|
||||
* scr_idna.c (which links with the regex unit), NODE_DEBUG matching in
|
||||
* scr_util_callable.c and readline's terminal unit use the regex unit,
|
||||
* and locale case mapping runs its default conversion. */
|
||||
regex: rule({
|
||||
fns: [
|
||||
...REGEX_LIB_FNS,
|
||||
@@ -1074,13 +1079,17 @@ const RUNTIME_FEATURE_RULES = {
|
||||
"rl.terminal",
|
||||
"str.localeCase",
|
||||
],
|
||||
prefixes: URL_PREFIXES,
|
||||
expr: (node, _tables) =>
|
||||
node.kind === "regexLit" ||
|
||||
node.kind === "regexIntrinsic" ||
|
||||
(node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)),
|
||||
types: ["regex"],
|
||||
types: ["regex", "url"],
|
||||
}),
|
||||
searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }),
|
||||
/** URL programs install the IDNA unit's ToASCII for the WHATWG host
|
||||
* parser (scr_url_idna_install). */
|
||||
urlIdna: rule({ prefixes: URL_PREFIXES, types: ["url"] }),
|
||||
/** stdin setEncoding / string data listeners / stdio 'resize' events. A
|
||||
* typed string 'data' listener takes setEncoding-decoded chunks; the
|
||||
* process stream object's setEncoding and 'resize' members are reached
|
||||
@@ -1121,6 +1130,9 @@ const RUNTIME_FEATURE_RULES = {
|
||||
* scr_webcrypto.c. */
|
||||
webcrypto: rule({ prefixes: ["webcrypto."] }),
|
||||
zlib: rule({ prefixes: ["zlib."] }),
|
||||
/** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when
|
||||
* the program uses Brotli (scr_brotli.c). */
|
||||
brotli: rule({ fns: ["zlib.brotliInstall"] }),
|
||||
};
|
||||
|
||||
type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES;
|
||||
@@ -1174,6 +1186,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
qs: false,
|
||||
regex: false,
|
||||
searchParams: false,
|
||||
urlIdna: false,
|
||||
stdioExtended: false,
|
||||
stream: false,
|
||||
streamDyn: false,
|
||||
@@ -1190,6 +1203,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
wasm: false,
|
||||
webcrypto: false,
|
||||
zlib: false,
|
||||
brotli: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -502,10 +502,29 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const symbol of cls.symbolFields ?? []) {
|
||||
const global = mod.globals?.find((global) => global.id === symbol.globalId);
|
||||
if (cls.stringTagHelper !== undefined) {
|
||||
const helper = functionsByName.get(cls.stringTagHelper);
|
||||
const receiver = helper?.params[0]?.type;
|
||||
if (
|
||||
global?.type.kind !== "symbol" ||
|
||||
!helper ||
|
||||
helper.params.length !== 1 ||
|
||||
receiver?.kind !== "object" ||
|
||||
receiver.className !== cls.name ||
|
||||
helper.returnType.kind !== "string" ||
|
||||
helper.captures !== undefined ||
|
||||
helper.classCaptures !== undefined
|
||||
) {
|
||||
errors.push({
|
||||
message: `class ${cls.name}: string tag helper must be a noncapturing (instance) => string function`,
|
||||
loc: cls.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const symbol of cls.symbolFields ?? []) {
|
||||
const global =
|
||||
"globalId" in symbol ? mod.globals?.find((global) => global.id === symbol.globalId) : null;
|
||||
if (
|
||||
("globalId" in symbol ? global?.type.kind !== "symbol" : symbol.wellKnown === "") ||
|
||||
!cls.fields.some((field) => field.name === symbol.field)
|
||||
) {
|
||||
errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc });
|
||||
|
||||
@@ -31,6 +31,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
|
||||
["zlib.cbOpts", "the async node:zlib callback surface"],
|
||||
["zlib.promise", "the async node:zlib callback surface (util.promisify)"],
|
||||
["zlib.stream", "the node:zlib stream classes"],
|
||||
["zlib.brotliInstall", "node:zlib's Brotli forms"],
|
||||
["process.stdoutWriteBytesCb", "process.stdout.write completion callbacks"],
|
||||
["process.stderrWriteBytesCb", "process.stderr.write completion callbacks"],
|
||||
["timers.", "the timers surface (setTimeout family)"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
@@ -88,14 +89,19 @@ export class NativeCache {
|
||||
}
|
||||
}
|
||||
|
||||
write(family: string, key: string, bytes: string | Uint8Array): void {
|
||||
write(
|
||||
family: string,
|
||||
key: string,
|
||||
bytes: string | Uint8Array,
|
||||
digest = contentDigest(bytes),
|
||||
): void {
|
||||
let stage: string | null = null;
|
||||
try {
|
||||
const directory = join(this.root, family);
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
stage = mkdtempSync(join(directory, ".write-"));
|
||||
writeFileSync(join(stage, "payload"), bytes);
|
||||
writeFileSync(join(stage, "digest"), contentDigest(bytes) + "\n");
|
||||
writeFileSync(join(stage, "digest"), digest + "\n");
|
||||
const path = join(directory, key);
|
||||
// Concurrent writers publish identical content under the same key;
|
||||
// a reader between the two renames simply treats it as a miss.
|
||||
@@ -112,6 +118,29 @@ export class NativeCache {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a marker or entry exists, without reading or checking it. */
|
||||
has(family: string, key: string): boolean {
|
||||
return existsSync(join(this.root, family, key));
|
||||
}
|
||||
|
||||
/** Records an empty marker that has() finds. */
|
||||
mark(family: string, key: string): void {
|
||||
try {
|
||||
const directory = join(this.root, family);
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(directory, key), "");
|
||||
} catch {
|
||||
/* Cache failures never change a build's result. */
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes `bytes` under their content digest, and returns the digest. */
|
||||
writeContent(family: string, bytes: Uint8Array): string {
|
||||
const digest = contentDigest(bytes);
|
||||
this.write(family, digest, bytes, digest);
|
||||
return digest;
|
||||
}
|
||||
|
||||
/** Publish text parts without joining them into one string. */
|
||||
writeParts(family: string, key: string, parts: Generator<string>): void {
|
||||
let stage: string | null = null;
|
||||
@@ -149,6 +178,7 @@ export class NativeCache {
|
||||
"object",
|
||||
"sanitizer",
|
||||
"executable",
|
||||
"linked",
|
||||
"binary",
|
||||
"dsym",
|
||||
"coverage",
|
||||
|
||||
@@ -56,7 +56,7 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js";
|
||||
import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { evaluateNativeComptime } from "./comptime.js";
|
||||
import { contentDigest, fileDigest, type NativeCache } from "./cache.js";
|
||||
import { contentDigest, fileDigest, partsDigest, type NativeCache } from "./cache.js";
|
||||
import {
|
||||
nativeProfileKey,
|
||||
nativeProfileRefusal,
|
||||
@@ -168,6 +168,7 @@ export class NativeCompiler {
|
||||
outputKind: "obj" | "asm",
|
||||
partitions = 1,
|
||||
profile?: NativeProfileOptions,
|
||||
llvmDigest: () => string = () => fileDigest(input),
|
||||
): string[] {
|
||||
const toolchain = this.toolchain;
|
||||
const outputs = nativePartitionPaths(output, partitions);
|
||||
@@ -186,7 +187,7 @@ export class NativeCompiler {
|
||||
key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: fileDigest(input),
|
||||
llvm: llvmDigest(),
|
||||
outputKind,
|
||||
optimization,
|
||||
source,
|
||||
@@ -253,6 +254,7 @@ export class NativeCompiler {
|
||||
stage: string,
|
||||
library: boolean,
|
||||
profile?: NativeProfileOptions,
|
||||
llvmDigest?: () => string,
|
||||
): string[] {
|
||||
// Library archives merge dev shards relocatably; executables link the
|
||||
// helper's program partitions directly.
|
||||
@@ -263,7 +265,16 @@ export class NativeCompiler {
|
||||
const split = llvm === null ? null : splitLlvmLibraryProgram(llvm);
|
||||
if (split === null) {
|
||||
const partitions = library ? 1 : nativeProgramPartitions(statSync(input).size);
|
||||
return this.emitObject(input, output, source, optimization, "obj", partitions, profile);
|
||||
return this.emitObject(
|
||||
input,
|
||||
output,
|
||||
source,
|
||||
optimization,
|
||||
"obj",
|
||||
partitions,
|
||||
profile,
|
||||
llvmDigest,
|
||||
);
|
||||
}
|
||||
const objects: string[] = [];
|
||||
for (const [index, shard] of split.shards.entries()) {
|
||||
@@ -358,6 +369,9 @@ export class NativeCompiler {
|
||||
return { ok: true, artifact: { kind: "ir", path: output } };
|
||||
}
|
||||
const { llvm, features } = prepared;
|
||||
// The executable and object caches key the same LLVM text.
|
||||
let llvmDigest: string | undefined;
|
||||
const digestLlvm = (): string => (llvmDigest ??= partsDigest(llvm));
|
||||
const inputDirectory = join(stage, "input");
|
||||
mkdirSync(inputDirectory);
|
||||
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
|
||||
@@ -385,7 +399,14 @@ export class NativeCompiler {
|
||||
const executableCache =
|
||||
executablePack === null
|
||||
? null
|
||||
: openNativeExecutableCache(this.cache, toolchain, llvm, options, ffi, executablePack);
|
||||
: openNativeExecutableCache(
|
||||
this.cache,
|
||||
toolchain,
|
||||
digestLlvm,
|
||||
options,
|
||||
ffi,
|
||||
executablePack,
|
||||
);
|
||||
const restored = executableCache?.restore(stagedOutput) ?? false;
|
||||
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
|
||||
if (!restored) {
|
||||
@@ -408,6 +429,7 @@ export class NativeCompiler {
|
||||
stage,
|
||||
false,
|
||||
profile,
|
||||
digestLlvm,
|
||||
);
|
||||
else
|
||||
this.emitObject(
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, expect, test } from "vitest";
|
||||
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
|
||||
import { NativeCache } from "./cache.js";
|
||||
import { openNativeExecutableCache } from "./executable-cache.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
const saved = new Map<string, string | undefined>();
|
||||
beforeEach(() => {
|
||||
for (const name of ["SCRIPTC_LINKER", "SCRIPTC_LLVM_HELPER", "CC", "LDFLAGS", "LIBRARY_PATH"]) {
|
||||
saved.set(name, process.env[name]);
|
||||
delete process.env[name];
|
||||
}
|
||||
});
|
||||
afterEach(() => {
|
||||
for (const [name, value] of saved) {
|
||||
if (value === undefined) delete process.env[name];
|
||||
else process.env[name] = value;
|
||||
}
|
||||
saved.clear();
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** A toolchain whose linker is a driver script named `driver`; it answers
|
||||
* `-print-prog-name=clang` with its own path. */
|
||||
function fixture(platform: string, driver: string) {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-executable-cache-open-"));
|
||||
directories.push(root);
|
||||
const bin = join(root, "bin");
|
||||
const helper = join(root, "helper");
|
||||
const runtime = join(root, "runtime");
|
||||
for (const directory of [bin, helper, runtime]) mkdirSync(directory);
|
||||
const linker = join(bin, driver);
|
||||
writeFileSync(linker, '#!/bin/sh\necho "$0"\n');
|
||||
chmodSync(linker, 0o755);
|
||||
writeFileSync(join(helper, "codegen"), "helper");
|
||||
writeFileSync(join(helper, "package.json"), "{}");
|
||||
writeFileSync(join(runtime, "package.json"), "{}");
|
||||
writeFileSync(join(runtime, "runtime-pack.json"), "{}");
|
||||
const toolchain = {
|
||||
compilerVersion: "test",
|
||||
target: { platform },
|
||||
helperExecutable: join(helper, "codegen"),
|
||||
helperPackageRoot: helper,
|
||||
runtimePackRoot: runtime,
|
||||
linker,
|
||||
linkerArgs: [],
|
||||
dsymutil: "dsymutil",
|
||||
} as unknown as NativeToolchain;
|
||||
const pack = {
|
||||
root: runtime,
|
||||
manifest: { licenses: [] },
|
||||
packageText: "{}",
|
||||
manifestText: "{}",
|
||||
selected: { runtime: [], archives: [] },
|
||||
flavor: "release",
|
||||
} as unknown as NativeRuntimeSelection;
|
||||
const open = () =>
|
||||
openNativeExecutableCache(
|
||||
new NativeCache(join(root, "cache")),
|
||||
toolchain,
|
||||
() => "llvm digest",
|
||||
{ outPath: join(root, "program"), outDir: root, optimization: "release" },
|
||||
null,
|
||||
pack,
|
||||
);
|
||||
return { open };
|
||||
}
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"Linux builds that link with a clang driver use the executable cache",
|
||||
() => {
|
||||
expect(fixture("linux", "clang").open()).not.toBeNull();
|
||||
expect(fixture("linux", "clang-22").open()).not.toBeNull();
|
||||
expect(fixture("linux", "gcc").open()).toBeNull();
|
||||
expect(fixture("windows", "clang").open()).toBeNull();
|
||||
expect(fixture("wasi", "clang").open()).toBeNull();
|
||||
// On darwin only Apple's driver at /usr/bin/clang qualifies.
|
||||
expect(fixture("darwin", "clang").open()).toBeNull();
|
||||
process.env["SCRIPTC_LLVM_HELPER"] = "/elsewhere/codegen";
|
||||
expect(fixture("linux", "clang").open()).toBeNull();
|
||||
},
|
||||
);
|
||||
@@ -222,3 +222,61 @@ test("a failed or empty link trace cannot publish a completed executable", () =>
|
||||
});
|
||||
expect(entry.trace([], f.stage)).toBe(false);
|
||||
});
|
||||
|
||||
test.each(["ld", "crt", "libc"] as const)(
|
||||
"a Linux key's second link records the linker, startup objects and libc; replacing the %s misses",
|
||||
(input) => {
|
||||
const f = fixture();
|
||||
const lib = join(f.root, "lib");
|
||||
mkdirSync(lib);
|
||||
const paths = {
|
||||
ld: join(f.root, "ld"),
|
||||
crt: join(lib, "Scrt1.o"),
|
||||
libc: join(lib, "libc.so.6"),
|
||||
};
|
||||
writeFileSync(paths.ld, "linker");
|
||||
writeFileSync(paths.crt, "startup object");
|
||||
writeFileSync(paths.libc, "c library");
|
||||
const nonshared = join(lib, "libc_nonshared.a");
|
||||
writeFileSync(nonshared, "archive");
|
||||
const program = join(f.stage, "program.o");
|
||||
writeFileSync(program, "program");
|
||||
// With -v, clang prints the linker job it runs, as -### would; GNU ld's
|
||||
// -t names each file it loads, and an archive member as archive(member).
|
||||
const job = ` "${paths.ld}" "-pie" "-o" "${f.output}" "${paths.crt}" "-L${lib}" "${program}" "-lc"`;
|
||||
const loaded = [paths.crt, program, paths.libc, `${nonshared}(atexit.oS)`].join("\n");
|
||||
vi.mocked(spawnSync).mockImplementation((_command, args) => {
|
||||
const verbose = (args as string[]).includes("-v");
|
||||
return {
|
||||
pid: 1,
|
||||
status: 0,
|
||||
signal: null,
|
||||
output: [],
|
||||
stdout: verbose ? loaded + "\n" : "",
|
||||
stderr: verbose ? job + "\n" : "",
|
||||
};
|
||||
});
|
||||
const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true);
|
||||
// A key's first link is not traced; the caller links the output itself.
|
||||
expect(open().trace(["-o", f.output], f.stage)).toBe(false);
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
expect(open().restore(f.destination)).toBe(false);
|
||||
// Its next link traces both the driver and the linker in one run.
|
||||
const entry = open();
|
||||
expect(entry.trace(["-o", f.output], f.stage)).toBe(true);
|
||||
expect(spawnSync).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(spawnSync).mock.calls[0]![1]).toEqual(["-o", f.output, "-v", "-Wl,-t"]);
|
||||
entry.publish(f.output);
|
||||
const recorded = (
|
||||
JSON.parse(f.cache.read("executable", f.key)!.toString()) as { inputs: { path: string }[] }
|
||||
).inputs.map((saved) => saved.path);
|
||||
expect(recorded).toEqual(expect.arrayContaining([...Object.values(paths), nonshared, lib]));
|
||||
expect(recorded).not.toContain(program);
|
||||
expect(open().restore(f.destination)).toBe(true);
|
||||
const before = statSync(paths[input]);
|
||||
writeFileSync(paths[input] + ".new", "x".repeat(before.size));
|
||||
utimesSync(paths[input] + ".new", before.atime, before.mtime);
|
||||
renameSync(paths[input] + ".new", paths[input]);
|
||||
expect(open().restore(f.destination)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { runNativeTool } from "../backend/native-tools.js";
|
||||
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { contentDigest, NativeCache, partsDigest } from "./cache.js";
|
||||
import { contentDigest, NativeCache } from "./cache.js";
|
||||
|
||||
interface InputIdentity {
|
||||
path: string;
|
||||
@@ -48,9 +48,22 @@ function identity(path: string): InputIdentity {
|
||||
};
|
||||
}
|
||||
|
||||
function sameIdentity(a: InputIdentity, b: InputIdentity): boolean {
|
||||
return (
|
||||
a.path === b.path &&
|
||||
a.canonical === b.canonical &&
|
||||
a.kind === b.kind &&
|
||||
a.dev === b.dev &&
|
||||
a.ino === b.ino &&
|
||||
a.size === b.size &&
|
||||
a.mtime === b.mtime &&
|
||||
a.ctime === b.ctime
|
||||
);
|
||||
}
|
||||
|
||||
function stillMatches(inputs: InputIdentity[]): boolean {
|
||||
try {
|
||||
return inputs.every((input) => JSON.stringify(identity(input.path)) === JSON.stringify(input));
|
||||
return inputs.every((input) => sameIdentity(identity(input.path), input));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -165,6 +178,8 @@ function stageSymbols(bytes: Buffer, binary: string): void {
|
||||
export class NativeExecutableCache {
|
||||
private inputs: InputIdentity[];
|
||||
private traced = false;
|
||||
/** Linux: no entry exists and the key was never linked before. */
|
||||
private readonly firstLink: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly cache: NativeCache,
|
||||
@@ -172,23 +187,28 @@ export class NativeExecutableCache {
|
||||
private readonly linker: string,
|
||||
private readonly debug: boolean,
|
||||
paths: string[],
|
||||
/** Linux: trace with one verbose link, from a key's second link on. */
|
||||
private readonly linux = false,
|
||||
) {
|
||||
this.inputs = [...new Set(paths)].sort().map(identity);
|
||||
this.firstLink = linux && !cache.has("executable", key) && !cache.has("linked", key);
|
||||
// A first link neither restores nor traces, so it needs no identities.
|
||||
this.inputs = this.firstLink ? [] : [...new Set(paths)].sort().map(identity);
|
||||
}
|
||||
|
||||
restore(output: string): boolean {
|
||||
if (this.firstLink) return false;
|
||||
try {
|
||||
const metadata = this.cache.read("executable", this.key);
|
||||
if (metadata === null) return false;
|
||||
const entry: unknown = JSON.parse(metadata.toString("utf8"));
|
||||
if (!validEntry(entry) || !stillMatches(entry.inputs) || !stillMatches(this.inputs))
|
||||
return false;
|
||||
const saved = new Map(entry.inputs.map((input) => [input.path, input]));
|
||||
if (
|
||||
!this.inputs.every((input) =>
|
||||
entry.inputs.some(
|
||||
(saved) => saved.path === input.path && JSON.stringify(saved) === JSON.stringify(input),
|
||||
),
|
||||
)
|
||||
!this.inputs.every((input) => {
|
||||
const recorded = saved.get(input.path);
|
||||
return recorded !== undefined && sameIdentity(recorded, input);
|
||||
})
|
||||
)
|
||||
return false;
|
||||
if (this.debug !== (entry.symbols !== null)) return false;
|
||||
@@ -209,14 +229,24 @@ export class NativeExecutableCache {
|
||||
* Directories from the driver's search line detect a newly added candidate.
|
||||
* Private build inputs already belong to the LLVM/runtime cache identity.
|
||||
* A true result means the requested output was linked successfully and its
|
||||
* inputs remained stable. The caller can publish that output directly. */
|
||||
* inputs remained stable. The caller can publish that output directly. On
|
||||
* false, the caller links the output itself. */
|
||||
trace(args: string[], stage: string): boolean {
|
||||
this.traced = false;
|
||||
try {
|
||||
// On Linux a key's first link only records that it was linked; a later
|
||||
// link of the same key traces, so a build that runs once traces nothing.
|
||||
if (this.firstLink) {
|
||||
this.cache.mark("linked", this.key);
|
||||
return false;
|
||||
}
|
||||
const roots = [resolve(stage), realpathSync(stage)];
|
||||
const dry = toolOutput(this.linker, [...args, "-###"]);
|
||||
const driverInputs = tracePaths(dry, true, roots).map(identity);
|
||||
const linked = toolOutput(this.linker, [...args, "-Wl,-t"]);
|
||||
let driverInputs = this.linux
|
||||
? []
|
||||
: tracePaths(toolOutput(this.linker, [...args, "-###"]), true, roots).map(identity);
|
||||
// On Linux, -v prints the jobs that -### would, while the link runs.
|
||||
const linked = toolOutput(this.linker, [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]);
|
||||
if (this.linux) driverInputs = tracePaths(linked, true, roots).map(identity);
|
||||
const paths = tracePaths(linked, false, roots);
|
||||
if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs))
|
||||
return false;
|
||||
@@ -233,10 +263,8 @@ export class NativeExecutableCache {
|
||||
if (!this.traced || !stillMatches(this.inputs)) return;
|
||||
const binary = readFileSync(output);
|
||||
const symbols = this.debug ? readSymbols(output) : null;
|
||||
const binaryKey = contentDigest(binary);
|
||||
const symbolsKey = symbols === null ? null : contentDigest(symbols);
|
||||
this.cache.write("binary", binaryKey, binary);
|
||||
if (symbols !== null && symbolsKey !== null) this.cache.write("dsym", symbolsKey, symbols);
|
||||
const binaryKey = this.cache.writeContent("binary", binary);
|
||||
const symbolsKey = symbols === null ? null : this.cache.writeContent("dsym", symbols);
|
||||
if (!stillMatches(this.inputs)) return;
|
||||
const entry: ExecutableEntry = {
|
||||
schema: "scriptc.native-executable.v1",
|
||||
@@ -254,16 +282,18 @@ export class NativeExecutableCache {
|
||||
export function openNativeExecutableCache(
|
||||
cache: NativeCache | null,
|
||||
toolchain: NativeToolchain,
|
||||
llvm: readonly string[],
|
||||
llvmDigest: () => string,
|
||||
options: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
pack: NativeRuntimeSelection,
|
||||
): NativeExecutableCache | null {
|
||||
// Match the established complete-cache contract: default Apple driver,
|
||||
// known runtime inputs, and no wrapper or caller-supplied library searches.
|
||||
// Match the established complete-cache contract: the default Apple driver
|
||||
// or a Linux clang driver, known runtime inputs, and no wrapper or
|
||||
// caller-supplied library searches.
|
||||
const platform = toolchain.target.platform;
|
||||
if (
|
||||
cache === null ||
|
||||
toolchain.target.platform !== "darwin" ||
|
||||
(platform !== "darwin" && platform !== "linux") ||
|
||||
options.sanitize ||
|
||||
// The key names a profile by path, not by its bytes.
|
||||
options.profileGenerate !== undefined ||
|
||||
@@ -277,9 +307,23 @@ export function openNativeExecutableCache(
|
||||
return null;
|
||||
try {
|
||||
const linker = commandPath(toolchain.linker);
|
||||
if (realpathSync(linker) !== "/usr/bin/clang") return null;
|
||||
const effective = commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim());
|
||||
const debug = options.optimization === "dev" && !options.strip;
|
||||
// Apple's driver at /usr/bin/clang, or a clang driver on Linux. Both
|
||||
// name every input of a link in their -### and -Wl,-t traces.
|
||||
if (
|
||||
platform === "darwin"
|
||||
? realpathSync(linker) !== "/usr/bin/clang"
|
||||
: !/^clang(-\d+)?$/.test(basename(realpathSync(linker)))
|
||||
)
|
||||
return null;
|
||||
// Apple's /usr/bin/clang is a shim for the active developer directory's
|
||||
// driver; a Linux clang driver is itself the driver.
|
||||
const effective =
|
||||
platform === "darwin"
|
||||
? commandPath(runNativeTool(linker, ["-print-prog-name=clang"]).trim())
|
||||
: linker;
|
||||
// Linux keeps debug information in the executable; darwin keeps it in a
|
||||
// separate dSYM, which the cache stores beside the executable.
|
||||
const debug = platform === "darwin" && options.optimization === "dev" && !options.strip;
|
||||
const paths = [
|
||||
linker,
|
||||
effective,
|
||||
@@ -291,6 +335,7 @@ export function openNativeExecutableCache(
|
||||
(artifact) => join(pack.root, artifact.path),
|
||||
),
|
||||
];
|
||||
if (platform === "linux") paths.push(join(pack.root, "runtime-pack.json"));
|
||||
if (debug) {
|
||||
const dsymutil = commandPath(toolchain.dsymutil);
|
||||
if (realpathSync(dsymutil) !== "/usr/bin/dsymutil") return null;
|
||||
@@ -299,11 +344,16 @@ export function openNativeExecutableCache(
|
||||
const key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: partsDigest(llvm),
|
||||
llvm: llvmDigest(),
|
||||
options,
|
||||
toolchain,
|
||||
runtimeIdentity: pack.packageText,
|
||||
runtimeManifest: pack.manifestText,
|
||||
// On Linux the manifest's identity names it: restore checks that
|
||||
// identity again, as it does every other input.
|
||||
runtimeManifest:
|
||||
platform === "darwin"
|
||||
? pack.manifestText
|
||||
: identity(join(pack.root, "runtime-pack.json")),
|
||||
linker: identity(linker),
|
||||
effective: identity(effective),
|
||||
environment: toolchainEnvironmentFingerprint(),
|
||||
@@ -311,7 +361,7 @@ export function openNativeExecutableCache(
|
||||
path: process.env["PATH"] ?? null,
|
||||
}),
|
||||
);
|
||||
return new NativeExecutableCache(cache, key, linker, debug, paths);
|
||||
return new NativeExecutableCache(cache, key, linker, debug, paths, platform === "linux");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
builtinValues: false,
|
||||
warningReplace: false,
|
||||
stdioExtended: false,
|
||||
brotli: false,
|
||||
regex: true,
|
||||
copying: false,
|
||||
textDecoderLegacy: false,
|
||||
|
||||
@@ -94,6 +94,7 @@ export function buildSanitizedRuntime(
|
||||
const quickjs = join(vendor, "quickjs-ng");
|
||||
const zlib = join(vendor, "zlib");
|
||||
const mbedtls = join(vendor, "mbedtls");
|
||||
const brotli = join(vendor, "brotli", "c");
|
||||
const cacheIdentity = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
@@ -137,6 +138,8 @@ export function buildSanitizedRuntime(
|
||||
zlib,
|
||||
"-I",
|
||||
join(mbedtls, "include"),
|
||||
"-I",
|
||||
join(brotli, "include"),
|
||||
...(toolchain.target.platform === "linux" ? ["-ffunction-sections", "-fdata-sections"] : []),
|
||||
];
|
||||
const runtimeObjects: string[] = [];
|
||||
@@ -173,9 +176,23 @@ export function buildSanitizedRuntime(
|
||||
.filter((file) => file.endsWith(".c") && !file.startsWith("."))
|
||||
.sort();
|
||||
args = ["-std=c11", "-I", join(mbedtls, "include"), "-I", directory];
|
||||
} else if (name === "brotli") {
|
||||
directory = brotli;
|
||||
sources = ["common", "dec", "enc"]
|
||||
.flatMap((dir) =>
|
||||
readdirSync(join(brotli, dir))
|
||||
.filter((file) => file.endsWith(".c") && !file.startsWith("."))
|
||||
.map((file) => `${dir}/${file}`),
|
||||
)
|
||||
.sort();
|
||||
args = ["-std=c11", "-I", join(brotli, "include")];
|
||||
} else throw new NativeCodegenError("SC3003", `unsupported runtime vendor archive: ${name}`);
|
||||
const objects = sources.map((source) =>
|
||||
compile(join(directory, source), name + "-" + source.replace(/\.c$/, ".o"), args),
|
||||
compile(
|
||||
join(directory, source),
|
||||
name + "-" + source.replaceAll("/", "-").replace(/\.c$/, ".o"),
|
||||
args,
|
||||
),
|
||||
);
|
||||
const output = join(stage, "libscriptc-" + name + ".a");
|
||||
runNativeTool(
|
||||
|
||||
@@ -2882,22 +2882,6 @@
|
||||
"status": "static",
|
||||
"note": "recognized module (bare and node:-prefixed specifiers)"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.BrotliCompress",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.BrotliCompress",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.BrotliDecompress",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.BrotliDecompress",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.DEFLATE",
|
||||
"kind": "node-builtin",
|
||||
@@ -3651,33 +3635,25 @@
|
||||
"id": "node-builtin.zlib.brotliCompress",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.brotliCompress",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.brotliCompressSync",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.brotliCompressSync",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.brotliDecompress",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.brotliDecompress",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.brotliDecompressSync",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.brotliDecompressSync",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.crc32",
|
||||
@@ -3689,17 +3665,13 @@
|
||||
"id": "node-builtin.zlib.createBrotliCompress",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.createBrotliCompress",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.createBrotliDecompress",
|
||||
"kind": "node-builtin",
|
||||
"name": "zlib.createBrotliDecompress",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms with Node's options, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.zlib.createDeflate",
|
||||
|
||||
@@ -9,9 +9,10 @@ import { inlineRcOption } from "../src/backend/llvm/shapes.js";
|
||||
|
||||
// Programs whose function bodies request module-level helpers and tables
|
||||
// that the chunk journal (emit-journal.ts) carries: checked-dynamic
|
||||
// conversions, inspection, publication walkers, live record views, stream
|
||||
// adapters, regex instances, colliding literal digests, interned C strings,
|
||||
// speculative loop sites, typed message listeners and readers.
|
||||
// conversions (with their discriminant tables), inspection, publication
|
||||
// walkers, live record views, stream adapters, regex instances, colliding
|
||||
// literal digests, interned C strings, speculative loop sites, typed message
|
||||
// listeners and readers.
|
||||
const PROGRAMS = [
|
||||
"1003-json-parse-unions.ts",
|
||||
"114-live-record-delete.ts",
|
||||
@@ -21,6 +22,7 @@ const PROGRAMS = [
|
||||
"4900-console-instances.ts",
|
||||
"5612-readable-from-iterables.cjs",
|
||||
"5702-publish-candidate-buffers.ts",
|
||||
"6305-json-discriminant-dispatch.ts",
|
||||
"7321-speculative-loop-bail-sites.ts",
|
||||
"7360-worker-typed-records.ts",
|
||||
"node24-text-utilities-coercion.js",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
import { everyStmtList } from "../src/ir/traverse.js";
|
||||
|
||||
async function lowerSource(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-keyed-writes-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Calls in `run` that go through a dynamic value. */
|
||||
function dynamicCalls(mod: IrModule): number {
|
||||
const fn = mod.functions.find((f) => f.name === "run");
|
||||
if (!fn) throw new Error("no run function");
|
||||
let n = 0;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "dynCall" || e.kind === "dynInvoke") n++;
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
const shape = `
|
||||
type Step = "scale" | "clamp";
|
||||
class Transform {
|
||||
scale(x: number): number {
|
||||
return x * 2;
|
||||
}
|
||||
clamp(x: number): number {
|
||||
return x < 0 ? 0 : x;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
test("element writes of data under method names keep the methods native", async () => {
|
||||
const mod = await lowerSource(`${shape}
|
||||
const enabled: Record<Step, boolean> = { scale: false, clamp: false };
|
||||
function enable(name: Step): void {
|
||||
enabled[name] = true;
|
||||
}
|
||||
const totals: Record<string, number> = {};
|
||||
function run(t: Transform): number {
|
||||
enable("scale");
|
||||
totals["clamp"] = 1;
|
||||
return t.clamp(t.scale(3));
|
||||
}
|
||||
console.log(run(new Transform()));
|
||||
`);
|
||||
expect(dynamicCalls(mod)).toBe(0);
|
||||
});
|
||||
|
||||
test("element writes through untyped receivers can still replace a method", async () => {
|
||||
const mod = await lowerSource(`${shape}
|
||||
function replace(target: object, name: Step): void {
|
||||
(target as any)[name] = (x: number) => x + 100;
|
||||
}
|
||||
function run(t: Transform): number {
|
||||
replace(t, "scale");
|
||||
return t.clamp(t.scale(3));
|
||||
}
|
||||
console.log(run(new Transform()));
|
||||
`);
|
||||
expect(dynamicCalls(mod)).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -51,14 +51,12 @@ test("decompression options lower for run-time validation", () => {
|
||||
expect(diags).toEqual([]);
|
||||
});
|
||||
|
||||
test("Brotli keeps its SC2020 refusal", () => {
|
||||
test("Brotli options lower for run-time validation", () => {
|
||||
const diags = diagnostics(`
|
||||
import { brotliCompressSync } from "node:zlib";
|
||||
console.log(brotliCompressSync("payload").length);
|
||||
import { brotliCompressSync, constants } from "node:zlib";
|
||||
console.log(brotliCompressSync("payload", { params: { [constants.BROTLI_PARAM_QUALITY]: 4 } }).length);
|
||||
`);
|
||||
expect(diags).toHaveLength(1);
|
||||
expect(diags[0]!.code).toBe("SC2020");
|
||||
expect(diags[0]!.message).toContain("brotliCompressSync");
|
||||
expect(diags).toEqual([]);
|
||||
});
|
||||
|
||||
test("union writes keep the refusal for incompatible field storage", () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6305-json-discriminant-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6306-discriminant-getter-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7660-pointer-union-forwarded-arguments.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7663-pointer-union-stored-payload.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7715-zlib-brotli-decompress.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7716-zlib-brotli.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7751-class-well-known-symbol-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8105-url-idna-hosts.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8129-brotli-long-param-name.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/services.ts",
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/runtime.ts",
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/sdk.ts",
|
||||
"<repo>/tests/corpus/8213-cycle-star-reexport/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8215-js-any-logical-module-bindings/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
*/
|
||||
|
||||
const any = (...features) => ({ any: features });
|
||||
const all = (...features) => ({ all: features });
|
||||
|
||||
// ld64 dead-strips Mach-O symbol subsections without an ELF-style compile
|
||||
// flag. Keep this checked-in metadata alongside the matrix so pack build and
|
||||
@@ -43,6 +44,7 @@ export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors(
|
||||
{ id: "quickjs", predicate: false },
|
||||
{ id: "libregexp", predicate: "regex" },
|
||||
{ id: "zlib", predicate: "zlibEffective" },
|
||||
{ id: "brotli", predicate: all("zlib", "brotli") },
|
||||
{ id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh", "pki") },
|
||||
],
|
||||
system_libraries: [{ name: "System", predicate: true }],
|
||||
|
||||
@@ -72,6 +72,7 @@ async function build() {
|
||||
const quickjs = join(vendorRoot, "quickjs-ng");
|
||||
const zlib = join(vendorRoot, "zlib");
|
||||
const mbedtls = join(vendorRoot, "mbedtls");
|
||||
const brotli = join(vendorRoot, "brotli", "c");
|
||||
const QJS_SOURCES = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"];
|
||||
const LRE_SOURCES = ["libregexp.c", "libunicode.c"];
|
||||
const ZLIB_SOURCES = [
|
||||
@@ -112,20 +113,23 @@ async function build() {
|
||||
return parallelMap(items, width, task);
|
||||
}
|
||||
|
||||
// Archive members are named by basename, so a source in a subdirectory
|
||||
// (brotli's dec/ and enc/ share file names) is named after its path.
|
||||
const objectName = (source) => source.replaceAll("/", "-").replace(/\.c$/, ".o");
|
||||
async function archive(id, sources, sourceRoot, flags) {
|
||||
const root = join(stagedOutputRoot, "vendor", id);
|
||||
const objectRoot = join(root, "objects");
|
||||
await parallel(sources, async (source) => {
|
||||
await compile(
|
||||
join(sourceRoot, source),
|
||||
join(objectRoot, source.replace(/\.c$/, ".o")),
|
||||
join(objectRoot, objectName(source)),
|
||||
unalignedVendorSources.has(source)
|
||||
? flags.filter((flag) => !codeAlignment.includes(flag))
|
||||
: flags,
|
||||
);
|
||||
});
|
||||
const output = join(root, `libscriptc-${id}.a`);
|
||||
const objects = sources.map((source) => join(objectRoot, source.replace(/\.c$/, ".o")));
|
||||
const objects = sources.map((source) => join(objectRoot, objectName(source)));
|
||||
await createDeterministicArchive(archiver, output, objects);
|
||||
await rm(objectRoot, { recursive: true, force: true });
|
||||
return {
|
||||
@@ -181,6 +185,7 @@ async function build() {
|
||||
...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c"
|
||||
? ["-I", zlib]
|
||||
: []),
|
||||
...(unit.source === "scr_brotli.c" ? ["-I", join(brotli, "include")] : []),
|
||||
];
|
||||
const unitFlags = [
|
||||
...commonFlags,
|
||||
@@ -268,6 +273,17 @@ async function build() {
|
||||
const mbedtlsSources = (await readdir(join(mbedtls, "library")))
|
||||
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
|
||||
.sort();
|
||||
const brotliSources = (
|
||||
await Promise.all(
|
||||
["common", "dec", "enc"].map(async (dir) =>
|
||||
(await readdir(join(brotli, dir)))
|
||||
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
|
||||
.map((name) => `${dir}/${name}`),
|
||||
),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.sort();
|
||||
const archives = [
|
||||
await archive("quickjs", QJS_SOURCES, quickjs, [
|
||||
"-target",
|
||||
@@ -301,6 +317,15 @@ async function build() {
|
||||
"-I",
|
||||
zlib,
|
||||
]),
|
||||
await archive("brotli", brotliSources, brotli, [
|
||||
"-target",
|
||||
RUNTIME_PACK_MATRIX.target.llvm_triple,
|
||||
"-std=c11",
|
||||
"-Os",
|
||||
...codeAlignment,
|
||||
"-I",
|
||||
join(brotli, "include"),
|
||||
]),
|
||||
await archive("mbedtls", mbedtlsSources, join(mbedtls, "library"), [
|
||||
"-target",
|
||||
RUNTIME_PACK_MATRIX.target.llvm_triple,
|
||||
@@ -325,6 +350,7 @@ async function build() {
|
||||
],
|
||||
[join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"],
|
||||
[join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"],
|
||||
[join(vendorRoot, "brotli", "LICENSE"), "artifacts/licenses/brotli.txt", "MIT"],
|
||||
[join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"],
|
||||
];
|
||||
await Promise.all(
|
||||
|
||||
@@ -83,6 +83,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
// Blob and File box their reads as dyn promises through scr_async_dyn.c.
|
||||
["scr_blob.c", "dynAsync"],
|
||||
["scr_zlib.c", "zlib"],
|
||||
["scr_brotli.c", all("zlib", "brotli")],
|
||||
["scr_events.c", "events"],
|
||||
["scr_readline.c", "events"],
|
||||
["scr_readline_tty.c", all("events", "regex")],
|
||||
@@ -227,6 +228,7 @@ export function createRuntimePackMatrix({
|
||||
{ id: "quickjs", predicate: false },
|
||||
{ id: "libregexp", predicate: "regex" },
|
||||
{ id: "zlib", predicate: "zlibEffective" },
|
||||
{ id: "brotli", predicate: all("zlib", "brotli") },
|
||||
{ id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh", "pki") },
|
||||
];
|
||||
const matrix = withLibraryRuntimeFlavors(
|
||||
|
||||
@@ -85,6 +85,7 @@ async function build() {
|
||||
const quickjs = join(vendorRoot, "quickjs-ng");
|
||||
const zlib = join(vendorRoot, "zlib");
|
||||
const mbedtls = join(vendorRoot, "mbedtls");
|
||||
const brotli = join(vendorRoot, "brotli", "c");
|
||||
const qjsSources = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"];
|
||||
const lreSources = ["libregexp.c", "libunicode.c"];
|
||||
const zlibSources = [
|
||||
@@ -131,6 +132,9 @@ async function build() {
|
||||
const width = Math.max(1, Math.min(8, availableParallelism()));
|
||||
return parallelMap(items, width, task);
|
||||
};
|
||||
// Archive members are named by basename, so a source in a subdirectory
|
||||
// (brotli's dec/ and enc/ share file names) is named after its path.
|
||||
const objectName = (source) => source.replaceAll("/", "-").replace(/\.c$/, ".o");
|
||||
const archive = async (id, sources, sourceRoot, flags) => {
|
||||
process.stdout.write(`building ${packageManifest.name} ${id} archive\n`);
|
||||
const root = join(stagedOutputRoot, "vendor", id);
|
||||
@@ -138,7 +142,7 @@ async function build() {
|
||||
await parallel(sources, async (source) =>
|
||||
compile(
|
||||
join(sourceRoot, source),
|
||||
join(objectRoot, source.replace(/\.c$/, ".o")),
|
||||
join(objectRoot, objectName(source)),
|
||||
unalignedVendorSources.has(source)
|
||||
? flags.filter((flag) => !codeAlignment.includes(flag))
|
||||
: flags,
|
||||
@@ -148,7 +152,7 @@ async function build() {
|
||||
await createDeterministicArchive(
|
||||
archiver,
|
||||
output,
|
||||
sources.map((source) => join(objectRoot, source.replace(/\.c$/, ".o"))),
|
||||
sources.map((source) => join(objectRoot, objectName(source))),
|
||||
archiverArgs,
|
||||
);
|
||||
await rm(objectRoot, { recursive: true, force: true });
|
||||
@@ -232,6 +236,7 @@ async function build() {
|
||||
...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c"
|
||||
? ["-I", zlib]
|
||||
: []),
|
||||
...(unit.source === "scr_brotli.c" ? ["-I", join(brotli, "include")] : []),
|
||||
];
|
||||
// Worker variants link only into executables, whose own TLS block
|
||||
// holds every runtime thread-local: on ELF the initial-exec model
|
||||
@@ -334,6 +339,17 @@ async function build() {
|
||||
const mbedtlsSources = (await readdir(join(mbedtls, "library")))
|
||||
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
|
||||
.sort();
|
||||
const brotliSources = (
|
||||
await Promise.all(
|
||||
["common", "dec", "enc"].map(async (dir) =>
|
||||
(await readdir(join(brotli, dir)))
|
||||
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
|
||||
.map((name) => `${dir}/${name}`),
|
||||
),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.sort();
|
||||
// Vendored QuickJS reads `clock_gettime` for its monotonic clock. WASI
|
||||
// exposes that declaration only when its process-clock emulation ABI is
|
||||
// selected, so vendor sources inherit the target's runtime defines too.
|
||||
@@ -370,6 +386,10 @@ async function build() {
|
||||
? [
|
||||
await archive("libregexp", lreSources, quickjs, [
|
||||
...vendorTarget,
|
||||
// Section elimination keeps only the libunicode tables a
|
||||
// program reaches: the URL host parser's NFC links the
|
||||
// normalization tables without the regex property ones.
|
||||
...matrix.executable_section_elimination.compile_flags,
|
||||
"-std=c11",
|
||||
"-Os",
|
||||
"-I",
|
||||
@@ -388,6 +408,17 @@ async function build() {
|
||||
]),
|
||||
]
|
||||
: []),
|
||||
...(requestedArchives.has("brotli")
|
||||
? [
|
||||
await archive("brotli", brotliSources, brotli, [
|
||||
...vendorTarget,
|
||||
"-std=c11",
|
||||
"-Os",
|
||||
"-I",
|
||||
join(brotli, "include"),
|
||||
]),
|
||||
]
|
||||
: []),
|
||||
...(requestedArchives.has("mbedtls")
|
||||
? [
|
||||
await archive("mbedtls", mbedtlsSources, join(mbedtls, "library"), [
|
||||
@@ -417,6 +448,7 @@ async function build() {
|
||||
"BSD-3-Clause",
|
||||
],
|
||||
[join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"],
|
||||
[join(vendorRoot, "brotli", "LICENSE"), "artifacts/licenses/brotli.txt", "MIT"],
|
||||
[join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"],
|
||||
];
|
||||
licensed.push([
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
/* node:zlib's Brotli codec over the vendored brotli 1.2.0 (the version Node
|
||||
* v24.22.0 bundles, so compressed bytes match Node's). Node's
|
||||
* BrotliEncoderContext and BrotliDecoderContext (src/node_zlib.cc) plug into
|
||||
* scr_zlib.c's engine as a codec, so the one-shot, callback, promise and
|
||||
* stream forms run exactly as their zlib counterparts do. The option ladder
|
||||
* is lib/zlib.js's Brotli() followed by ZlibBase(). Linked only when the
|
||||
* program uses Brotli; scr_brotli_install, which %main calls first,
|
||||
* connects it to scr_zlib.c. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#include <brotli/decode.h>
|
||||
#include <brotli/encode.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
enum { SCR_BROTLI_DECODE = 8, SCR_BROTLI_ENCODE = 9 };
|
||||
/* kMaxBrotliParam + 1: the largest BROTLI_PARAM_* value is
|
||||
* BROTLI_PARAM_NDIRECT (8). An unset slot holds 0xFFFFFFFF, as Node's
|
||||
* Uint32Array filled with -1 does. */
|
||||
#define SCR_BROTLI_PARAMS 9
|
||||
#define SCR_BROTLI_UNSET 0xFFFFFFFFu
|
||||
|
||||
typedef struct {
|
||||
bool encode;
|
||||
BrotliEncoderState *enc;
|
||||
BrotliEncoderPreparedDictionary *prepared;
|
||||
BrotliDecoderState *dec;
|
||||
/* The dictionary stays alive while the encoder's prepared dictionary or
|
||||
* the decoder holds it. */
|
||||
unsigned char *dict;
|
||||
size_t dict_len;
|
||||
int flush;
|
||||
bool enc_ok;
|
||||
BrotliDecoderResult dec_result;
|
||||
BrotliDecoderErrorCode dec_error;
|
||||
char dec_code[64];
|
||||
} ScrBrotli;
|
||||
|
||||
static void scr_brotli_oom(void) {
|
||||
scr_trap("scriptc: out of memory\n");
|
||||
}
|
||||
|
||||
static bool scr_brotli_fail(ScrZlibError *out, const char *message, const char *code, int err) {
|
||||
out->message = message;
|
||||
out->code = code;
|
||||
out->err = err;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void scr_brotli_close(ScrBrotli *b) {
|
||||
if (b->enc) BrotliEncoderDestroyInstance(b->enc);
|
||||
if (b->prepared) BrotliEncoderDestroyPreparedDictionary(b->prepared);
|
||||
if (b->dec) BrotliDecoderDestroyInstance(b->dec);
|
||||
b->enc = NULL;
|
||||
b->prepared = NULL;
|
||||
b->dec = NULL;
|
||||
}
|
||||
|
||||
/* Init(dictionary): a fresh instance with the dictionary attached. As in
|
||||
* Node, the decoder's last error survives a re-initialization. True with
|
||||
* *out filled on failure. */
|
||||
static bool scr_brotli_init(ScrBrotli *b, ScrZlibError *out) {
|
||||
scr_brotli_close(b);
|
||||
if (b->encode) {
|
||||
b->enc = BrotliEncoderCreateInstance(NULL, NULL, NULL);
|
||||
b->enc_ok = false;
|
||||
if (!b->enc) return scr_brotli_fail(out, "Could not initialize Brotli instance", "ERR_ZLIB_INITIALIZATION_FAILED", -1);
|
||||
if (b->dict_len == 0) return false;
|
||||
b->prepared = BrotliEncoderPrepareDictionary(BROTLI_SHARED_DICTIONARY_RAW, b->dict_len, b->dict, BROTLI_MAX_QUALITY,
|
||||
NULL, NULL, NULL);
|
||||
if (!b->prepared) return scr_brotli_fail(out, "Failed to prepare brotli dictionary", "ERR_ZLIB_DICTIONARY_LOAD_FAILED", -1);
|
||||
if (!BrotliEncoderAttachPreparedDictionary(b->enc, b->prepared))
|
||||
return scr_brotli_fail(out, "Failed to attach brotli dictionary", "ERR_ZLIB_DICTIONARY_LOAD_FAILED", -1);
|
||||
return false;
|
||||
}
|
||||
b->dec = BrotliDecoderCreateInstance(NULL, NULL, NULL);
|
||||
if (!b->dec) return scr_brotli_fail(out, "Could not initialize Brotli instance", "ERR_ZLIB_INITIALIZATION_FAILED", -1);
|
||||
if (b->dict_len != 0 && !BrotliDecoderAttachDictionary(b->dec, BROTLI_SHARED_DICTIONARY_RAW, b->dict_len, b->dict))
|
||||
return scr_brotli_fail(out, "Failed to attach brotli dictionary", "ERR_ZLIB_DICTIONARY_LOAD_FAILED", -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void scr_brotli_free(void *state) {
|
||||
ScrBrotli *b = state;
|
||||
if (!b) return;
|
||||
scr_brotli_close(b);
|
||||
free(b->dict);
|
||||
free(b);
|
||||
}
|
||||
|
||||
/* DoThreadPoolWork: runs on a worker thread and touches only the state. */
|
||||
static void scr_brotli_work(void *state, int flush, const unsigned char *in, uint32_t in_len, unsigned char *out,
|
||||
uint32_t out_len, uint32_t *avail_in, uint32_t *avail_out) {
|
||||
ScrBrotli *b = state;
|
||||
size_t in_left = in_len, out_left = out_len;
|
||||
const uint8_t *next_in = in;
|
||||
uint8_t *next_out = out;
|
||||
b->flush = flush;
|
||||
if (b->encode) {
|
||||
b->enc_ok = BrotliEncoderCompressStream(b->enc, (BrotliEncoderOperation)flush, &in_left, &next_in, &out_left,
|
||||
&next_out, NULL) != BROTLI_FALSE;
|
||||
} else {
|
||||
b->dec_result = BrotliDecoderDecompressStream(b->dec, &in_left, &next_in, &out_left, &next_out, NULL);
|
||||
if (b->dec_result == BROTLI_DECODER_RESULT_ERROR) {
|
||||
b->dec_error = BrotliDecoderGetErrorCode(b->dec);
|
||||
snprintf(b->dec_code, sizeof b->dec_code, "ERR_%s", BrotliDecoderErrorString(b->dec_error));
|
||||
}
|
||||
}
|
||||
*avail_in = (uint32_t)in_left;
|
||||
*avail_out = (uint32_t)out_left;
|
||||
}
|
||||
|
||||
/* GetErrorInfo. */
|
||||
static bool scr_brotli_error(const void *state, ScrZlibError *out) {
|
||||
const ScrBrotli *b = state;
|
||||
if (b->encode) {
|
||||
if (!b->enc_ok) return scr_brotli_fail(out, "Compression failed", "ERR_BROTLI_COMPRESSION_FAILED", -1);
|
||||
return false;
|
||||
}
|
||||
if (b->dec_error != BROTLI_DECODER_NO_ERROR)
|
||||
return scr_brotli_fail(out, "Decompression failed", b->dec_code, (int)b->dec_error);
|
||||
/* zlib's answer for a truncated stream; Brotli has no code of its own. */
|
||||
if (b->flush == BROTLI_OPERATION_FINISH && b->dec_result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT)
|
||||
return scr_brotli_fail(out, "unexpected end of file", "Z_BUF_ERROR", -5);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ResetStream is Init() without a dictionary: as in Node, the reset
|
||||
* instance drops the dictionary and the parameters. */
|
||||
static bool scr_brotli_reset(void *state, ScrZlibError *out) {
|
||||
ScrBrotli *b = state;
|
||||
free(b->dict);
|
||||
b->dict = NULL;
|
||||
b->dict_len = 0;
|
||||
return scr_brotli_init(b, out);
|
||||
}
|
||||
|
||||
static const ScrZlibCodec scr_brotli_codec = {
|
||||
scr_brotli_work,
|
||||
scr_brotli_error,
|
||||
scr_brotli_reset,
|
||||
scr_brotli_free,
|
||||
};
|
||||
|
||||
/* ── lib/zlib.js Brotli() ──────────────────────────────────────────── */
|
||||
|
||||
static const ScrDyn *scr_brotli_opt(const ScrDyn *opts, const char *key) {
|
||||
if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL;
|
||||
return scr_dyn_obj_get((ScrDyn *)opts, key, strlen(key));
|
||||
}
|
||||
|
||||
/* Uint32Array element assignment: ToUint32 of a number or boolean. */
|
||||
static uint32_t scr_brotli_uint32(const ScrDyn *v) {
|
||||
if (v->kind == SCR_DYN_BOOL) return v->v.b ? 1 : 0;
|
||||
double d = v->v.num;
|
||||
if (!isfinite(d)) return 0;
|
||||
double m = fmod(trunc(d), 4294967296.0);
|
||||
if (m < 0) m += 4294967296.0;
|
||||
return (uint32_t)m;
|
||||
}
|
||||
|
||||
/* ERR_BROTLI_INVALID_PARAM: the whole key, then Node's text. */
|
||||
static void scr_brotli_invalid_param(const ScrStr *key) {
|
||||
static const char suffix[] = " is not a valid Brotli parameter";
|
||||
size_t len = key->len + sizeof suffix - 1;
|
||||
char *message = malloc(len);
|
||||
if (!message) scr_brotli_oom();
|
||||
memcpy(message, key->data, key->len);
|
||||
memcpy(message + key->len, suffix, sizeof suffix - 1);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, message, len, "ERR_BROTLI_INVALID_PARAM");
|
||||
free(message);
|
||||
}
|
||||
|
||||
/* opts.params: each own key must name a parameter (0..8) once, and each
|
||||
* value must be a number or boolean. */
|
||||
static bool scr_brotli_params(const ScrDyn *params, uint32_t values[SCR_BROTLI_PARAMS]) {
|
||||
for (size_t i = 0; i < SCR_BROTLI_PARAMS; i++) values[i] = SCR_BROTLI_UNSET;
|
||||
if (params == NULL || !scr_dyn_truthy(params)) return true;
|
||||
ScrDyn *view = params->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(params) : NULL;
|
||||
if (scr_exc_pending()) return false;
|
||||
const ScrDyn *p = view ? view : params;
|
||||
if (p->kind == SCR_DYN_STR) {
|
||||
/* Object.keys of a string names its indices; the first value is a
|
||||
* one-character string, which the type check rejects. */
|
||||
ScrStr *first = scr_str_new(p->v.str->data, 1);
|
||||
ScrDyn *value = scr_dyn_new_str(first);
|
||||
scr_str_release(first);
|
||||
scr_dyn_prop_type_fail("options.params[key]", "of type number", value);
|
||||
scr_dyn_release(value);
|
||||
scr_dyn_release(view);
|
||||
return false;
|
||||
}
|
||||
if (p->kind != SCR_DYN_OBJ && p->kind != SCR_DYN_ARR) {
|
||||
scr_dyn_release(view);
|
||||
return true;
|
||||
}
|
||||
ScrDyn *keys = scr_dyn_own_keys(p);
|
||||
bool ok = keys != NULL;
|
||||
size_t count = ok && keys->kind == SCR_DYN_ARR ? keys->v.arr.len : 0;
|
||||
for (size_t i = 0; ok && i < count; i++) {
|
||||
const ScrDyn *key = keys->v.arr.items[i];
|
||||
if (key->kind != SCR_DYN_STR) continue;
|
||||
double index = NAN;
|
||||
if (!scr_dyn_number_coerce_js(key, &index)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (isnan(index) || index < 0 || index > SCR_BROTLI_PARAMS - 1 || trunc(index) != index ||
|
||||
values[(int)index] != SCR_BROTLI_UNSET) {
|
||||
scr_brotli_invalid_param(key->v.str);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
ScrDyn *value = scr_dyn_obj_read(p, key->v.str->data, key->v.str->len);
|
||||
if (!value) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (value->kind != SCR_DYN_NUM && value->kind != SCR_DYN_BOOL) {
|
||||
scr_dyn_prop_type_fail("options.params[key]", "of type number", value);
|
||||
scr_dyn_release(value);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
values[(int)index] = scr_brotli_uint32(value);
|
||||
scr_dyn_release(value);
|
||||
}
|
||||
scr_dyn_release(keys);
|
||||
scr_dyn_release(view);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* opts.dictionary: an ArrayBufferView, or an ArrayBuffer read as a Buffer.
|
||||
* A copy (or NULL for none) in *dict. */
|
||||
static bool scr_brotli_dictionary(const ScrDyn *d, unsigned char **dict, size_t *dict_len) {
|
||||
*dict = NULL;
|
||||
*dict_len = 0;
|
||||
if (d == NULL || d->kind == SCR_DYN_UNDEF) return true;
|
||||
ScrBytes *bytes = NULL;
|
||||
if (d->kind == SCR_DYN_BYTES) {
|
||||
bytes = scr_bytes_raw_view(d->v.bytes);
|
||||
} else if (scr_buffer_storage_is(d)) {
|
||||
bytes = scr_array_buffer_view(SCR_BYTES_U8, (ScrDyn *)d, scr_dyn_undefined(), scr_dyn_undefined());
|
||||
if (!bytes) return false;
|
||||
} else {
|
||||
scr_dyn_prop_type_fail("options.dictionary", "an instance of Buffer, TypedArray, DataView, or ArrayBuffer", d);
|
||||
return false;
|
||||
}
|
||||
if (bytes->len) {
|
||||
*dict = malloc(bytes->len);
|
||||
if (!*dict) scr_brotli_oom();
|
||||
memcpy(*dict, bytes->data, bytes->len);
|
||||
*dict_len = bytes->len;
|
||||
}
|
||||
scr_bytes_release(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void scr_brotli_init_failed(void) {
|
||||
static const char message[] = "Initialization failed";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_ZLIB_INITIALIZATION_FAILED");
|
||||
}
|
||||
|
||||
static bool scr_brotli_options_object(int mode, const ScrDyn *opts, ScrZlibOptions *out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
out->chunk_size = 16384;
|
||||
out->flush = BROTLI_OPERATION_PROCESS;
|
||||
out->finish_flush = BROTLI_OPERATION_FINISH;
|
||||
out->max_output = 9007199254740991.0;
|
||||
uint32_t params[SCR_BROTLI_PARAMS];
|
||||
if (!scr_brotli_params(scr_brotli_opt(opts, "params"), params)) return false;
|
||||
unsigned char *dict;
|
||||
size_t dict_len;
|
||||
if (!scr_brotli_dictionary(scr_brotli_opt(opts, "dictionary"), &dict, &dict_len)) return false;
|
||||
ScrBrotli *b = calloc(1, sizeof *b);
|
||||
if (!b) scr_brotli_oom();
|
||||
b->encode = mode == SCR_BROTLI_ENCODE;
|
||||
b->dict = dict;
|
||||
b->dict_len = dict_len;
|
||||
ScrZlibError err;
|
||||
bool failed = scr_brotli_init(b, &err);
|
||||
for (int i = 0; !failed && i < SCR_BROTLI_PARAMS; i++) {
|
||||
if (params[i] == SCR_BROTLI_UNSET) continue;
|
||||
failed = b->encode ? !BrotliEncoderSetParameter(b->enc, (BrotliEncoderParameter)i, params[i])
|
||||
: !BrotliDecoderSetParameter(b->dec, (BrotliDecoderParameter)i, params[i]);
|
||||
}
|
||||
if (failed) {
|
||||
scr_brotli_free(b);
|
||||
scr_brotli_init_failed();
|
||||
return false;
|
||||
}
|
||||
out->codec_state = b;
|
||||
out->codec_state_free = scr_brotli_free;
|
||||
/* ZlibBase's `if (opts)`: a falsy value reads no more options. */
|
||||
if (opts == NULL || !scr_dyn_truthy(opts)) return true;
|
||||
return scr_zlib_base_options(opts, BROTLI_OPERATION_EMIT_METADATA, out);
|
||||
}
|
||||
|
||||
/* A typed options object is read through its materialized view. */
|
||||
static bool scr_brotli_options(int mode, const ScrDyn *opts, ScrZlibOptions *out) {
|
||||
if (opts == NULL || opts->kind != SCR_DYN_TYPED_REF) return scr_brotli_options_object(mode, opts, out);
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(opts);
|
||||
bool ok = false;
|
||||
if (scr_exc_pending()) memset(out, 0, sizeof *out);
|
||||
else ok = scr_brotli_options_object(mode, view, out);
|
||||
scr_dyn_release(view);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Moves the prepared instance out of the options. */
|
||||
static ScrZlibEngine *scr_brotli_engine(int mode, ScrZlibOptions *o) {
|
||||
ScrBrotli *b = o->codec_state;
|
||||
o->codec_state = NULL;
|
||||
return scr_zlib_engine_codec(mode, &scr_brotli_codec, b, o->reject_garbage);
|
||||
}
|
||||
|
||||
static const ScrZlibBrotliHooks scr_brotli_hooks = {scr_brotli_options, scr_brotli_engine};
|
||||
|
||||
void scr_brotli_install(void) {
|
||||
scr_zlib_brotli_hooks = &scr_brotli_hooks;
|
||||
}
|
||||
@@ -398,6 +398,10 @@ typedef struct ScrDynTypedClassOps {
|
||||
size_t count;
|
||||
ScrStr *const *names; /* immortal member-name strings */
|
||||
ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */
|
||||
/* The string an inherited Symbol.toStringTag getter answers for the
|
||||
* instance (+1), or NULL with a pending exception from the getter;
|
||||
* NULL when the class has no such getter. */
|
||||
ScrStr *(*string_tag)(void *instance);
|
||||
} ScrDynTypedClassOps;
|
||||
|
||||
/* A compiled JavaScript class whose instances record their own properties
|
||||
@@ -677,6 +681,14 @@ void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry
|
||||
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value);
|
||||
bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value);
|
||||
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value);
|
||||
/* The case a union's checked match takes from its string discriminant: the
|
||||
* index in `values` of the string `d`'s own data property `key` holds, or
|
||||
* -1 when every literal test above fails (`d` is not an object, or its own
|
||||
* data property `key` holds no string among `values`), or -2 when only the
|
||||
* tests themselves can tell (a typed capsule, an accessor, or a property
|
||||
* `d` does not own). Borrowed args. */
|
||||
int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values,
|
||||
size_t count);
|
||||
|
||||
/* Object.keys/values/entries over the checked-dynamic tree: JS own-key order (array-index
|
||||
* keys ascending first), dyn-array results (+1); values/entries RETAIN
|
||||
@@ -921,6 +933,7 @@ void scr_dyn_install_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_has_instance_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_dispose_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key);
|
||||
/* True when key is Symbol.asyncDispose (registered by the symbol unit when
|
||||
* the program names it); handle symbol_get ops outside that unit use it. */
|
||||
bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key);
|
||||
@@ -1084,6 +1097,9 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec);
|
||||
/* Attach the prototype member names of a class instance capsule
|
||||
* (idempotent), so `in` sees its methods and accessors. */
|
||||
void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops);
|
||||
/* A class instance capsule's inherited Symbol.toStringTag string from its
|
||||
* class's getter (+1); NULL when it has none or the getter threw. */
|
||||
ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d);
|
||||
/* Attach the bag and prototype hooks of a capsule of a bag-tracked class
|
||||
* instance (idempotent). */
|
||||
void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops);
|
||||
|
||||
@@ -144,6 +144,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d, bool to_string)
|
||||
return;
|
||||
|
||||
case SCR_DYN_TYPED_REF: {
|
||||
/* ToString of a class instance that converts through
|
||||
* Object.prototype.toString shows its Symbol.toStringTag getter's
|
||||
* string. */
|
||||
if (to_string && scr_dyn_typed_ref_put_tag(b, d)) return;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
|
||||
scr_dyn_display_buf(b, materialized, to_string);
|
||||
scr_dyn_release(materialized);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* UTS #46 domain processing for node:url — the conversions Node applies to
|
||||
* url.parse() hostnames (ToASCII), url.domainToASCII()/domainToUnicode()
|
||||
* and url.format()'s unicode option: nontransitional mapping, NFC, label
|
||||
* validity (leading combining marks, ContextJ, and the RFC 5893 bidi rule),
|
||||
* and Punycode. It matches Node 24's implementation, including its
|
||||
* WHATWG URL hosts and url.parse() hostnames (ToASCII),
|
||||
* url.domainToASCII()/domainToUnicode() and url.format()'s unicode option:
|
||||
* nontransitional mapping, NFC, label validity (leading combining marks,
|
||||
* ContextJ, and the RFC 5893 bidi rule), and Punycode. It matches Node 24's implementation, including its
|
||||
* validity-data snapshot and the shortcuts its label checks take; the
|
||||
* differential corpus and test/url-legacy-cases.txt (generated from Node)
|
||||
* are the oracle.
|
||||
@@ -511,3 +511,6 @@ ScrStr *scr_idna_to_unicode(const char *p, size_t n) {
|
||||
}
|
||||
return scr_jb_finish(&out);
|
||||
}
|
||||
|
||||
/* The URL host parser's UTS #46 ToASCII (scr_url.c). */
|
||||
void scr_url_idna_install(void) { scr_url_host_to_ascii = &scr_idna_to_ascii; }
|
||||
|
||||
@@ -1258,6 +1258,20 @@ static bool insp_tag_names_class(const char *tag, const char *cls) {
|
||||
memcmp(tag + tag_len - cls_len, cls, cls_len) == 0;
|
||||
}
|
||||
|
||||
/* The Symbol.toStringTag a class instance inherits from its class's
|
||||
* getter (+1), which Node brackets after the class name when it is a
|
||||
* nonempty string other than the name. An own tag field is listed with
|
||||
* the fields instead. A getter that throws shows no tag, as in Node. */
|
||||
static ScrStr *insp_inherited_tag(const ScrDyn *instance, const ScrStr *name) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_class_tag(instance);
|
||||
if (!tag && scr_exc_pending()) scr_exc_clear();
|
||||
if (tag && (tag->len == 0 || (tag->len == name->len && memcmp(tag->data, name->data, name->len) == 0))) {
|
||||
scr_str_release(tag);
|
||||
return NULL;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`,
|
||||
* `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */
|
||||
/* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not
|
||||
@@ -1979,10 +1993,23 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
free(cls);
|
||||
} else if (name && materialized->kind == SCR_DYN_OBJ &&
|
||||
scr_dyn_obj_get(materialized, "%error", 6) == NULL) {
|
||||
char *cls = malloc(name->len + 1);
|
||||
ScrStr *tag = insp_inherited_tag(d, name);
|
||||
if (scr_exc_pending()) {
|
||||
scr_dyn_release(materialized);
|
||||
return scr_str_new("", 0);
|
||||
}
|
||||
size_t tag_len = tag ? tag->len + 3 : 0;
|
||||
char *cls = malloc(name->len + tag_len + 1);
|
||||
if (!cls) scr_trap("scriptc: out of memory\n");
|
||||
memcpy(cls, name->data, name->len);
|
||||
cls[name->len] = '\0';
|
||||
if (tag) {
|
||||
/* Node's `Name [tag]` prefix. */
|
||||
memcpy(cls + name->len, " [", 2);
|
||||
memcpy(cls + name->len + 2, tag->data, tag->len);
|
||||
cls[name->len + tag_len - 1] = ']';
|
||||
scr_str_release(tag);
|
||||
}
|
||||
cls[name->len + tag_len] = '\0';
|
||||
size_t nseen = g_nseen;
|
||||
g_pending_seen = instance;
|
||||
out = scr_insp_obj(materialized, recurse, depth, cls);
|
||||
|
||||
@@ -202,6 +202,19 @@ void scr_dyn_install_has_instance_symbol(ScrDyn *key) {
|
||||
scr_atexit(scr_has_instance_symbol_cleanup);
|
||||
}
|
||||
|
||||
/* Symbol.toStringTag, installed when the program first evaluates it: no
|
||||
* object holds the key before then. */
|
||||
static SCR_TL ScrDyn *scr_to_string_tag_symbol;
|
||||
static void scr_to_string_tag_symbol_cleanup(void) {
|
||||
scr_dyn_release(scr_to_string_tag_symbol);
|
||||
scr_to_string_tag_symbol = NULL;
|
||||
}
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) {
|
||||
if (scr_to_string_tag_symbol) return;
|
||||
scr_to_string_tag_symbol = scr_dyn_retain(key);
|
||||
scr_atexit(scr_to_string_tag_symbol_cleanup);
|
||||
}
|
||||
|
||||
void scr_dyn_install_iterator_symbol(ScrDyn *key) {
|
||||
if (scr_iterator_symbol) return;
|
||||
scr_iterator_symbol = scr_dyn_retain(key);
|
||||
@@ -1273,10 +1286,14 @@ void scr_dyn_release(ScrDyn *d) {
|
||||
return;
|
||||
}
|
||||
#ifndef SCR_RC_AUDIT
|
||||
/* Number and string boxes own at most one string, are never cycle
|
||||
* candidates and cannot be weak keys: return them to the free list. */
|
||||
/* Number and string boxes own at most one string and cannot be weak
|
||||
* keys: return them to the free list. Their releases never buffer them,
|
||||
* but a collector pass can (a box that traces its payload reaches them,
|
||||
* and scr_xg_release buffers what it settles), so a buffered box leaves
|
||||
* its candidate buffer first. */
|
||||
if ((d->kind == SCR_DYN_NUM || d->kind == SCR_DYN_STR) && !d->prototype &&
|
||||
!d->symbol_properties && !d->symbol_keys && scr_dyn_free_count < SCR_DYN_FREE_MAX) {
|
||||
scr_cyc_dead(d);
|
||||
if (d->kind == SCR_DYN_STR) scr_str_release(d->v.str);
|
||||
d->v.str = (ScrStr *)scr_dyn_free_misc;
|
||||
scr_dyn_free_misc = d;
|
||||
@@ -1422,6 +1439,24 @@ bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value) {
|
||||
return matches;
|
||||
}
|
||||
|
||||
int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values,
|
||||
size_t count) {
|
||||
if (!d) return -1;
|
||||
if (d->kind == SCR_DYN_TYPED_REF) return -2;
|
||||
if (d->kind != SCR_DYN_OBJ) return -1;
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &d->v.obj.entries[i];
|
||||
if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue;
|
||||
if (entry->accessor) return -2;
|
||||
const ScrDyn *member = entry->value;
|
||||
if (!member || member->kind != SCR_DYN_STR) return -1;
|
||||
for (size_t k = 0; k < count; k++)
|
||||
if (scr_str_eq(member->v.str, values[k])) return (int32_t)k;
|
||||
return -1;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value) {
|
||||
ScrDyn *member = scr_dyn_discriminant(d, key);
|
||||
bool matches = member && member->kind == SCR_DYN_BOOL && member->v.b == value;
|
||||
@@ -3932,6 +3967,12 @@ static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t
|
||||
return false;
|
||||
}
|
||||
|
||||
ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d) {
|
||||
if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return NULL;
|
||||
const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d);
|
||||
return ops && ops->string_tag ? ops->string_tag(d->v.typed_ref.ptr) : NULL;
|
||||
}
|
||||
|
||||
bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) {
|
||||
if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return false;
|
||||
const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d);
|
||||
@@ -4021,6 +4062,9 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) {
|
||||
return scr_dyn_typed_ref_element(d, i);
|
||||
}
|
||||
|
||||
static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d);
|
||||
static ScrStr *scr_dyn_tag_string(const ScrStr *tag);
|
||||
|
||||
ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
|
||||
/* An instance of a class declared `extends Date` converts through its
|
||||
* toString, declared or inherited. With an exception pending the view is
|
||||
@@ -4034,6 +4078,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
|
||||
scr_str_release(empty);
|
||||
return out;
|
||||
}
|
||||
/* A class instance converting through Object.prototype.toString shows
|
||||
* its Symbol.toStringTag getter's string. */
|
||||
ScrStr *class_tag = scr_dyn_typed_ref_conversion_tag(d);
|
||||
if (class_tag || scr_exc_pending()) {
|
||||
ScrStr *text = class_tag ? scr_dyn_tag_string(class_tag) : scr_str_new("", 0);
|
||||
scr_str_release(class_tag);
|
||||
ScrDyn *out = scr_dyn_new_str(text);
|
||||
scr_str_release(text);
|
||||
return out;
|
||||
}
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
|
||||
const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL;
|
||||
if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view;
|
||||
@@ -8322,11 +8376,66 @@ void scr_dyn_throw(ScrDyn *d) {
|
||||
scr_dyn_trace_v, scr_dyn_is_object(d));
|
||||
}
|
||||
|
||||
static ScrStr *scr_dyn_tag_string(const ScrStr *tag) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, "[object ");
|
||||
scr_jb_write(&b, tag->data, tag->len);
|
||||
scr_jb_putc(&b, ']');
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
|
||||
/* The Symbol.toStringTag getter's string of a class instance whose
|
||||
* ToString is Object.prototype.toString (its class has no toString; the
|
||||
* string hint never reaches valueOf, since that answers a string): +1, or
|
||||
* NULL otherwise or with the getter's exception pending. */
|
||||
static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d) {
|
||||
ScrStr *to_string_key = scr_str_new("toString", 8);
|
||||
bool own_conversion = scr_dyn_typed_ref_class_has(d, to_string_key);
|
||||
scr_str_release(to_string_key);
|
||||
return own_conversion ? NULL : scr_dyn_typed_ref_class_tag(d);
|
||||
}
|
||||
|
||||
bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d);
|
||||
if (!tag) return scr_exc_pending();
|
||||
scr_jb_puts(b, "[object ");
|
||||
scr_jb_write(b, tag->data, tag->len);
|
||||
scr_jb_putc(b, ']');
|
||||
scr_str_release(tag);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Object.prototype.toString of an object whose own or inherited
|
||||
* @@toStringTag is a string (+1), NULL otherwise or with a pending
|
||||
* exception from its getter. */
|
||||
static ScrStr *scr_dyn_string_tag(const ScrDyn *d) {
|
||||
ScrStr *getter_tag = scr_dyn_typed_ref_class_tag(d);
|
||||
if (getter_tag) {
|
||||
ScrStr *out = scr_dyn_tag_string(getter_tag);
|
||||
scr_str_release(getter_tag);
|
||||
return out;
|
||||
}
|
||||
if (scr_exc_pending() || !scr_to_string_tag_symbol) return NULL;
|
||||
ScrDyn *tag = scr_dyn_symbol_key_get(d, scr_to_string_tag_symbol, true);
|
||||
if (!tag) return NULL;
|
||||
ScrStr *out = tag->kind == SCR_DYN_STR ? scr_dyn_tag_string(tag->v.str) : NULL;
|
||||
scr_dyn_release(tag);
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_PROXY) {
|
||||
scr_dyn_proxy_unsupported("Symbol.toStringTag lookup");
|
||||
return NULL;
|
||||
}
|
||||
/* Object.prototype.toString answers a string @@toStringTag over the
|
||||
* builtin tag of a plain object or a class instance. */
|
||||
if ((d->kind == SCR_DYN_OBJ || (d->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_error(d) &&
|
||||
!scr_dyn_generator(d)))) {
|
||||
ScrStr *tag = scr_dyn_string_tag(d);
|
||||
if (tag || scr_exc_pending()) return tag;
|
||||
}
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14);
|
||||
if (scr_dyn_generator(d)) {
|
||||
@@ -9669,6 +9778,13 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
|
||||
return NULL;
|
||||
}
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d);
|
||||
if (tag) {
|
||||
ScrStr *out = scr_dyn_tag_string(tag);
|
||||
scr_str_release(tag);
|
||||
return out;
|
||||
}
|
||||
if (scr_exc_pending()) return NULL;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_string_view(d);
|
||||
ScrStr *out = scr_dyn_string_coerce_js(materialized);
|
||||
scr_dyn_release(materialized);
|
||||
@@ -9689,7 +9805,7 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
|
||||
scr_error_release(error);
|
||||
return text;
|
||||
}
|
||||
return scr_str_new("[object Object]", 15);
|
||||
return scr_dyn_object_tag(d);
|
||||
}
|
||||
if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; }
|
||||
/* OrdinaryToPrimitive performs a method call, not a bare function
|
||||
|
||||
@@ -119,6 +119,11 @@ _Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == s
|
||||
"LLVM backend expects ScrJsonBuf to start with { data, len, cap }");
|
||||
|
||||
void scr_jb_init(ScrJsonBuf *b);
|
||||
/* String conversion of a class instance whose ToString is
|
||||
* Object.prototype.toString and whose class has a Symbol.toStringTag
|
||||
* getter: appends "[object Tag]" and answers true, also with the getter's
|
||||
* exception pending; false when the ordinary conversion applies. */
|
||||
bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d);
|
||||
/* Release a buffer's text without finishing it (error paths). */
|
||||
void scr_jb_abandon(ScrJsonBuf *b);
|
||||
/* Append a borrowed byte span verbatim, including embedded NUL bytes. */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user