mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Merge main and add the Node 24 util batch
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
|
||||
|
||||
|
||||
@@ -13,9 +13,12 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `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/dynamic-properties.mjs",
|
||||
"args": [
|
||||
"1000000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Untyped JavaScript reading and writing properties of values it never
|
||||
// declares types for: records built by object literals in two key orders,
|
||||
// nested records, property updates, and instances of a class and its
|
||||
// subclass read and written the same way. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "1000000");
|
||||
class Cursor {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
step(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class Marker extends Cursor {
|
||||
constructor(x, y, label) {
|
||||
super(x, y);
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
function makeItem(i) {
|
||||
if (i % 3 === 0)
|
||||
return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } };
|
||||
return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } };
|
||||
}
|
||||
function cost(item) {
|
||||
return item.price * item.qty + item.meta.weight;
|
||||
}
|
||||
function restock(item, round) {
|
||||
item.qty = (item.qty + round) % 7;
|
||||
item.meta.tag = (item.meta.tag + 1) % 7;
|
||||
}
|
||||
function distance(a, b) {
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
}
|
||||
const items = [];
|
||||
for (let i = 0; i < 1024; i++) items.push(makeItem(i));
|
||||
const cursors = [];
|
||||
for (let i = 0; i < 64; i++)
|
||||
cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i));
|
||||
const origin = { x: 0, y: 0 };
|
||||
let checksum = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
const item = items[r & 1023];
|
||||
checksum += cost(item);
|
||||
restock(item, r & 3);
|
||||
checksum += item.meta.tag;
|
||||
const cursor = cursors[r & 63];
|
||||
checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin);
|
||||
if (cursor.x > 100 || cursor.x < -100) cursor.x = 0;
|
||||
if (cursor.y > 100 || cursor.y < -100) cursor.y = 0;
|
||||
}
|
||||
console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label);
|
||||
@@ -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);
|
||||
@@ -58,7 +58,7 @@ The LLVM helper divides an executable's LLVM IR into partitions by its size. Up
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. A function's partition follows from its name, and a small internal function goes with the one function that calls it, so an edit leaves the other functions in their partitions.
|
||||
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. In programs with 32 partitions or more, functions from the same source file are placed in the same partition, so that their calls to each other stay inside it; a file larger than 1 MiB of LLVM text or than an average partition is divided into parts by function name. Other functions, and the functions of smaller programs, are placed by their name, and a small internal function goes with the one function that calls it. An edit leaves almost every other function in its partition.
|
||||
|
||||
These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -21382,9 +21382,9 @@
|
||||
"anchor": "err_invalid_mime_syntax",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23520,9 +23520,9 @@
|
||||
"anchor": "err_throttled",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71946,9 +71946,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71969,9 +71969,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71992,9 +71992,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72015,9 +72015,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72038,9 +72038,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72061,9 +72061,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72176,9 +72176,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72199,9 +72199,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72222,9 +72222,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72245,9 +72245,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72268,9 +72268,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72291,9 +72291,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72314,9 +72314,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72337,9 +72337,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72360,9 +72360,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72636,9 +72636,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72751,9 +72751,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72774,9 +72774,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72797,9 +72797,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -73395,9 +73395,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25723,8 +25723,12 @@
|
||||
"anchor": "dep0044-utilisarray",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-applicable",
|
||||
"evidence": "compiler-chapter-policy:deprecations"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.isArray",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -26011,8 +26015,12 @@
|
||||
"anchor": "dep0060-util_extend",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-applicable",
|
||||
"evidence": "compiler-chapter-policy:deprecations"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util._extend",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -32679,6 +32687,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/6702-ecdh-errors.js",
|
||||
"tests/corpus/6708-dh-errors.js",
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7711-util-throttle.cjs",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/caught-system-error-fields.ts",
|
||||
"tests/corpus/error-stack-frames.ts",
|
||||
@@ -36604,8 +36614,11 @@
|
||||
"anchor": "err_invalid_mime_syntax",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:errors.ERR_INVALID_MIME_SYNTAX",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -38742,8 +38755,11 @@
|
||||
"anchor": "err_throttled",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:errors.ERR_THROTTLED",
|
||||
"tests": [
|
||||
"tests/corpus/7711-util-throttle.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101427,6 +101443,18 @@
|
||||
"tests/corpus/7524-inspect-custom.ts",
|
||||
"tests/corpus/7525-inspect-custom.cjs",
|
||||
"tests/corpus/7526-inspect-options-runtime.ts",
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts",
|
||||
"tests/corpus/7702-util-signal-exit-code.cjs",
|
||||
"tests/corpus/7703-util-aborted.cjs",
|
||||
"tests/corpus/7704-util-aborted.ts",
|
||||
"tests/corpus/7705-util-diff.cjs",
|
||||
"tests/corpus/7706-util-diff.ts",
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts",
|
||||
"tests/corpus/7710-util-debounce.cjs",
|
||||
"tests/corpus/7711-util-throttle.cjs",
|
||||
"tests/corpus/7712-util-debounce-throttle.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js",
|
||||
"tests/corpus/node24-deep-equality-properties.js",
|
||||
"tests/corpus/node24-deep-equality.cjs",
|
||||
@@ -101513,8 +101541,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util._extend",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101536,8 +101568,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.isArray",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101559,8 +101595,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.util.MIMEType",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101582,8 +101622,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mime.toString",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101605,8 +101649,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mime.toJSON",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101628,8 +101676,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.MIMEType.parse",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101743,8 +101795,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.util.MIMEParams",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101766,8 +101822,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.delete",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101789,8 +101849,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.entries",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101812,8 +101876,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.get",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101835,8 +101903,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.has",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101858,8 +101930,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.keys",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101881,8 +101957,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.set",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101904,8 +101984,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.values",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101927,8 +102011,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102327,8 +102415,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.convertProcessSignalToExitCode",
|
||||
"tests": [
|
||||
"tests/corpus/7702-util-signal-exit-code.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102462,8 +102553,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.debounce",
|
||||
"tests": [
|
||||
"tests/corpus/7710-util-debounce.cjs",
|
||||
"tests/corpus/7712-util-debounce-throttle.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102485,8 +102580,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.throttle",
|
||||
"tests": [
|
||||
"tests/corpus/7711-util-throttle.cjs",
|
||||
"tests/corpus/7712-util-debounce-throttle.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102508,8 +102607,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.diff",
|
||||
"tests": [
|
||||
"tests/corpus/7705-util-diff.cjs",
|
||||
"tests/corpus/7706-util-diff.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -103348,8 +103451,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.aborted",
|
||||
"tests": [
|
||||
"tests/corpus/7703-util-aborted.cjs",
|
||||
"tests/corpus/7704-util-aborted.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -187,6 +187,13 @@
|
||||
"node-builtin.util.callbackify": ["tests/corpus/5190-util-callbackify.cjs", "tests/corpus/5191-util-callbackify.ts", "tests/fixtures/node-types/util-callables.ts"],
|
||||
"node-builtin.util.debuglog": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"],
|
||||
"node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"],
|
||||
"node-builtin.util.isArray": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
|
||||
"node-builtin.util._extend": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
|
||||
"node-builtin.util.convertProcessSignalToExitCode": ["tests/corpus/7702-util-signal-exit-code.cjs"],
|
||||
"node-builtin.util.aborted": ["tests/corpus/7703-util-aborted.cjs", "tests/corpus/7704-util-aborted.ts"],
|
||||
"node-builtin.util.diff": ["tests/corpus/7705-util-diff.cjs", "tests/corpus/7706-util-diff.ts"],
|
||||
"node-builtin.util.debounce": ["tests/corpus/7710-util-debounce.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
|
||||
"node-builtin.util.throttle": ["tests/corpus/7711-util-throttle.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
|
||||
"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"],
|
||||
@@ -265,6 +272,10 @@
|
||||
{ "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/corpus/node24-buffer-encoding.ts", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/fixtures/node-types/text-encoding.ts"] },
|
||||
{ "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-text-encode-into.ts", "tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
|
||||
{ "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
|
||||
{ "chapter": "util", "symbols": ["util.MIMEType", "util.MIMEParams", "MIMEType.parse", "mime.toString", "mime.toJSON", "mimeParams.delete", "mimeParams.entries", "mimeParams.get", "mimeParams.has", "mimeParams.keys", "mimeParams.set", "mimeParams.values"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`mimeParams[Symbol.iterator]()`"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_INVALID_MIME_SYNTAX"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_THROTTLED"], "status": "partial", "evidence": ["tests/corpus/7711-util-throttle.cjs"] },
|
||||
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
|
||||
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
@@ -36,6 +37,9 @@ struct Symbol {
|
||||
std::string PromotedHeader;
|
||||
std::vector<unsigned> Refs;
|
||||
std::vector<unsigned> Metadata;
|
||||
// The source-module group the emitter wrote before this definition, or
|
||||
// empty.
|
||||
StringRef Group;
|
||||
unsigned Partition = NoPartition;
|
||||
};
|
||||
|
||||
@@ -70,6 +74,83 @@ uint32_t stableHash(StringRef Name) {
|
||||
return Hash;
|
||||
}
|
||||
|
||||
// Places the functions whose root carries a source-module group by group: a
|
||||
// module's functions call each other most, so keeping them together leaves
|
||||
// fewer calls for the thin link to import across partitions. A group larger
|
||||
// than the mean partition, or than MaxChunkBytes, is divided into a power of
|
||||
// two of chunks by the hash of each root's name, so the division changes only
|
||||
// when the group's size doubles. Chunks are placed largest first, each in the
|
||||
// lighter of two partitions chosen by hashes of its key, after the functions
|
||||
// placed by name. The placement depends only on the module text and the
|
||||
// partition count. Modules with fewer than MinGroupedPartitions partitions
|
||||
// keep the placement by name: with few partitions per core, the balance of
|
||||
// the largest partitions decides the build time more than the imports.
|
||||
void placeGroups(std::vector<Symbol> &Symbols, ArrayRef<unsigned> Roots,
|
||||
unsigned Count) {
|
||||
constexpr unsigned MinGroupedPartitions = 32;
|
||||
if (Count < MinGroupedPartitions)
|
||||
return;
|
||||
auto Bytes = [](const Symbol &S) -> uint64_t {
|
||||
return S.Header.size() + S.Body.size();
|
||||
};
|
||||
std::vector<uint64_t> Load(Count, 0);
|
||||
uint64_t Total = 0;
|
||||
StringMap<uint64_t> GroupBytes;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
Total += Bytes(S);
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (Group.empty())
|
||||
Load[S.Partition] += Bytes(S);
|
||||
else
|
||||
GroupBytes[Group] += Bytes(S);
|
||||
}
|
||||
if (GroupBytes.empty())
|
||||
return;
|
||||
// A chunk is at most MaxChunkBytes, so the code a partition optimizes
|
||||
// together, and the inlining within it, does not grow when a program has
|
||||
// fewer partitions.
|
||||
constexpr uint64_t MaxChunkBytes = 1024 * 1024;
|
||||
uint64_t Chunk =
|
||||
std::min(MaxChunkBytes, std::max<uint64_t>(1, Total / Count));
|
||||
std::vector<std::string> Keys(Symbols.size());
|
||||
StringMap<uint64_t> KeyBytes;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (S.Kind != SymbolKind::Function || Group.empty())
|
||||
continue;
|
||||
uint64_t Chunks = 1;
|
||||
while (Chunks * Chunk < GroupBytes[Group])
|
||||
Chunks *= 2;
|
||||
Keys[I] = Chunks == 1
|
||||
? Group.str()
|
||||
: (Group + "#" +
|
||||
Twine(stableHash(Symbols[Roots[I]].Name) % Chunks))
|
||||
.str();
|
||||
KeyBytes[Keys[I]] += Bytes(S);
|
||||
}
|
||||
std::vector<std::pair<StringRef, uint64_t>> Order;
|
||||
for (const auto &Entry : KeyBytes)
|
||||
Order.push_back({Entry.getKey(), Entry.getValue()});
|
||||
llvm::sort(Order, [](const auto &A, const auto &B) {
|
||||
return A.second != B.second ? A.second > B.second : A.first < B.first;
|
||||
});
|
||||
StringMap<unsigned> Where;
|
||||
for (const auto &[Key, Size] : Order) {
|
||||
unsigned First = stableHash(Key) % Count;
|
||||
unsigned Second = stableHash((Key + "/2").str()) % Count;
|
||||
unsigned Chosen = Load[Second] < Load[First] ? Second : First;
|
||||
Load[Chosen] += Size;
|
||||
Where[Key] = Chosen;
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (!Keys[I].empty())
|
||||
Symbols[I].Partition = Where[Keys[I]];
|
||||
}
|
||||
|
||||
bool startsWithLinkage(StringRef Rest) {
|
||||
for (StringRef Linkage :
|
||||
{"linkonce", "weak", "common", "appending", "extern_weak",
|
||||
@@ -326,6 +407,11 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
return true;
|
||||
};
|
||||
|
||||
// The emitter writes "; scriptc-group <key>" before the definitions of each
|
||||
// source function; the group applies until the next such line, and an
|
||||
// empty key ends it.
|
||||
constexpr StringRef GroupLine = "; scriptc-group";
|
||||
StringRef Group;
|
||||
size_t Pos = 0;
|
||||
while (Pos < Text.size()) {
|
||||
size_t End = Text.find('\n', Pos);
|
||||
@@ -335,10 +421,15 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
size_t Next = End + 1;
|
||||
if (Line.empty() || Line.trim().empty()) {
|
||||
// Blank separator.
|
||||
} else if (Line.starts_with(GroupLine) &&
|
||||
(Line.size() == GroupLine.size() ||
|
||||
Line[GroupLine.size()] == ' ')) {
|
||||
Group = Line.drop_front(GroupLine.size()).trim();
|
||||
} else if (Line.starts_with("define ")) {
|
||||
Symbol S;
|
||||
S.Kind = SymbolKind::Function;
|
||||
S.Header = Line;
|
||||
S.Group = Group;
|
||||
if (!parseFunctionHeader(Line, S))
|
||||
return std::nullopt;
|
||||
// The emitter indents every body line, so the first line that is
|
||||
@@ -466,6 +557,7 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
Referrer[Ref] = I;
|
||||
}
|
||||
}
|
||||
std::vector<unsigned> Roots(Symbols.size());
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
if (Symbols[I].Kind != SymbolKind::Function)
|
||||
continue;
|
||||
@@ -479,8 +571,10 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
break;
|
||||
Root = Parent;
|
||||
}
|
||||
Roots[I] = Root;
|
||||
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
|
||||
}
|
||||
placeGroups(Symbols, Roots, Count);
|
||||
for (const Symbol &S : Symbols) {
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
|
||||
@@ -20,11 +20,15 @@ struct ProgramPartitions {
|
||||
};
|
||||
|
||||
// Divides the LLVM assembly scriptc emits into Count modules without parsing
|
||||
// it, so the partitions can be parsed concurrently. Functions are placed by a
|
||||
// stable hash of their name; an internal function referenced by exactly one
|
||||
// other function stays with that function unless its body is too large to
|
||||
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
|
||||
// emitter's narrow top-level grammar; callers then split a parsed module.
|
||||
// it, so the partitions can be parsed concurrently. In modules of 32 or
|
||||
// more partitions, functions under a "; scriptc-group <key>" line are placed
|
||||
// with the other functions of that source-module group, which is divided
|
||||
// into chunks when it is larger than the mean partition or 1 MiB; other
|
||||
// functions are placed by a stable hash of their name. An internal function referenced by exactly one other function stays
|
||||
// with that function unless its body is too large to inline, and a global
|
||||
// stays with the first function that references it. Returns nullopt for any
|
||||
// construct outside the emitter's narrow top-level grammar; callers then
|
||||
// split a parsed module.
|
||||
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
|
||||
unsigned Count);
|
||||
|
||||
|
||||
@@ -3910,6 +3910,85 @@ declare module "util" {
|
||||
): string;
|
||||
export function parseEnv(content: string): { [key: string]: string | undefined };
|
||||
export function inherits(constructor: unknown, superConstructor: unknown): void;
|
||||
export class MIMEType {
|
||||
constructor(input: string | { toString: () => string });
|
||||
static parse(input: string | { toString: () => string }): MIMEType | null;
|
||||
type: string;
|
||||
subtype: string;
|
||||
readonly essence: string;
|
||||
readonly params: MIMEParams;
|
||||
toString(): string;
|
||||
toJSON(): string;
|
||||
}
|
||||
export class MIMEParams {
|
||||
delete(name: string): void;
|
||||
entries(): IterableIterator<[name: string, value: string]>;
|
||||
get(name: string): string | null;
|
||||
has(name: string): boolean;
|
||||
keys(): IterableIterator<string>;
|
||||
set(name: string, value: string): void;
|
||||
values(): IterableIterator<string>;
|
||||
toString(): string;
|
||||
toJSON(): string;
|
||||
[Symbol.iterator](): IterableIterator<[name: string, value: string]>;
|
||||
}
|
||||
export type DiffEntry = [operation: -1 | 0 | 1, value: string];
|
||||
export function diff(
|
||||
actual: string | readonly string[],
|
||||
expected: string | readonly string[],
|
||||
): DiffEntry[];
|
||||
export interface DebounceOptions {
|
||||
leading?: boolean;
|
||||
rejectOnCancel?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Every call answers a promise of the result of the call it waits for. */
|
||||
export interface DebouncedFunction<T extends (...args: any[]) => any> {
|
||||
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
|
||||
cancel(reason?: unknown): void;
|
||||
flush(): void;
|
||||
ref(): this;
|
||||
unref(): this;
|
||||
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
|
||||
readonly pendingCount: number;
|
||||
}
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
wait: number,
|
||||
options?: DebounceOptions,
|
||||
): DebouncedFunction<T>;
|
||||
export interface ThrottleOptions {
|
||||
concurrency?: number;
|
||||
maxPending?: number;
|
||||
overflow?: "queue" | "drop";
|
||||
signal?: AbortSignal;
|
||||
strict?: boolean;
|
||||
}
|
||||
export interface ThrottledFunction<T extends (...args: any[]) => any> {
|
||||
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
|
||||
cancel(reason?: unknown): void;
|
||||
hasImmediateCapacity(): boolean;
|
||||
ref(): this;
|
||||
unref(): this;
|
||||
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
|
||||
readonly pendingCount: number;
|
||||
readonly activeCount: number;
|
||||
}
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
limit: number,
|
||||
interval: number,
|
||||
options?: ThrottleOptions,
|
||||
): ThrottledFunction<T>;
|
||||
/** Resolves with the abort event once `signal` aborts, or with undefined
|
||||
* when it already has. */
|
||||
export function aborted(signal: AbortSignal, resource: any): Promise<unknown>;
|
||||
/** 128 plus the number of the signal, the exit code of a process it terminates. */
|
||||
export function convertProcessSignalToExitCode(signalCode: string): number;
|
||||
/** @deprecated Since v4.0.0 - Use `Array.isArray()` instead. */
|
||||
export function isArray(object: unknown): object is unknown[];
|
||||
/** @deprecated Since v6.0.0 - Use `Object.assign()` instead. Answers target itself. */
|
||||
export function _extend<T extends object>(target: T, source: object | null | undefined): T;
|
||||
export interface DeprecateOptions {
|
||||
modifyPrototype?: boolean;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -32,6 +32,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.mime ? { mime: true } : {}),
|
||||
...(features.utilTiming ? { utilTiming: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
events: features.processEvents,
|
||||
|
||||
@@ -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`);
|
||||
@@ -2462,12 +2597,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).
|
||||
@@ -2506,8 +2637,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)`,
|
||||
|
||||
@@ -136,6 +136,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.loopCopiesEnabled = true;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
@@ -159,6 +160,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.optionalArrayReads,
|
||||
emitter.referenceWrites,
|
||||
emitter.fnByName,
|
||||
emitter.nodeIndex,
|
||||
)
|
||||
: new Set();
|
||||
emitter.localArrayReads = findLocalArrayReads(
|
||||
@@ -324,7 +326,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
|
||||
const paramIds = new Set(fn.params.map((p) => p.localId));
|
||||
if (emitter.debug === null) {
|
||||
for (const id of findIntegerViews(numericFn)) {
|
||||
for (const id of findIntegerViews(numericFn, emitter.nodeIndex)) {
|
||||
// An i32 slot is its own view.
|
||||
if (emitter.int32Locals.has(id)) continue;
|
||||
const slot = B.slot();
|
||||
@@ -1307,8 +1309,10 @@ export function emitArgPackAndTrampolinePrologue(
|
||||
* code. */
|
||||
export function parkingFunctions(emitter: LlEmitter): Set<string> | null {
|
||||
if (emitter.wasi || emitter.mod.workers === true) return null;
|
||||
for (const decl of emitter.decls) if (/@scr_test_sub\b/.test(decl)) return null;
|
||||
for (const text of emitter.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null;
|
||||
const testSub = (text: string): boolean =>
|
||||
text.includes("@scr_test_sub") && /@scr_test_sub\b/.test(text);
|
||||
for (const decl of emitter.decls) if (testSub(decl)) return null;
|
||||
for (const text of emitter.fnDefText.values()) if (testSub(text)) return null;
|
||||
// Reverse mention edges, then propagate "may park" to every mentioner.
|
||||
const mentionedBy = new Map<string, Set<string>>();
|
||||
const parks = new Set<string>();
|
||||
|
||||
@@ -65,6 +65,7 @@ import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
|
||||
@@ -206,6 +207,26 @@ function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean)
|
||||
];
|
||||
}
|
||||
|
||||
/** The group line written before each function's text. Functions from one
|
||||
* source file share a group, which the LLVM helper keeps in the same
|
||||
* partitions so that their calls to each other stay inside a partition
|
||||
* (native/llvm-codegen/src/partition.h). A group is numbered by the first
|
||||
* function of its file in the module's function order, which does not depend
|
||||
* on the host. Functions without a source file get no group. */
|
||||
function partitionGroupLines(functions: readonly IrFunction[]): string[] {
|
||||
const groups = new Map<string, number>();
|
||||
return functions.map((fn) => {
|
||||
const file = fn.loc.file;
|
||||
if (file === "") return PARTITION_GROUP_END;
|
||||
let group = groups.get(file);
|
||||
if (group === undefined) groups.set(file, (group = groups.size));
|
||||
return `${PARTITION_GROUP_END} ${group}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** Ends the group of the definitions that follow. */
|
||||
const PARTITION_GROUP_END = "; scriptc-group";
|
||||
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
@@ -344,6 +365,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 +419,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()`);
|
||||
@@ -912,9 +936,13 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
for (const line of wrappers) out.push(line);
|
||||
for (const line of asyncDefs) out.push(line);
|
||||
for (const line of emitter.resolveThunkDefs) out.push(line);
|
||||
out.push(fnDefs[0] ?? "");
|
||||
for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!);
|
||||
out.push("");
|
||||
const groups = partitionGroupLines(emitter.mod.functions);
|
||||
for (let i = 0; i < fnDefs.length; i++) {
|
||||
if (i > 0) out.push("");
|
||||
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
|
||||
}
|
||||
if (fnDefs.length === 0) out.push("");
|
||||
out.push(PARTITION_GROUP_END, "");
|
||||
// WebAssembly modules translated into this program, and the registry.
|
||||
for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
|
||||
out.push(line);
|
||||
@@ -1027,6 +1055,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()`] : []),
|
||||
|
||||
@@ -994,7 +994,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
LOOPS.of(emitter).denseLoop = dense;
|
||||
@@ -1018,7 +1018,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).byteWindowsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.integerRanges = window.ranges;
|
||||
@@ -1072,7 +1072,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(safe, fast, slow);
|
||||
B.startBlock(slow);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
B.br(countedJoin);
|
||||
B.startBlock(fast);
|
||||
@@ -2049,7 +2049,7 @@ function emitCallbackVersionedLoop(
|
||||
emitter.callbackVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId);
|
||||
@@ -2062,6 +2062,20 @@ function emitCallbackVersionedLoop(
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
/** The copy of a versioned loop that runs when its guard fails. Its loops
|
||||
* get no speculative or int32 kind copies, which would emit each loop again
|
||||
* for a path that rarely runs. Proven integer loops, which emit one copy,
|
||||
* are kept. */
|
||||
function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
|
||||
const copies = emitter.loopCopiesEnabled;
|
||||
emitter.loopCopiesEnabled = false;
|
||||
try {
|
||||
emitter.emitStmt(s);
|
||||
} finally {
|
||||
emitter.loopCopiesEnabled = copies;
|
||||
}
|
||||
}
|
||||
|
||||
/** Entries in a row whose fast copy bailed before a speculative loop site
|
||||
* stops trying it. */
|
||||
const SPECULATION_BAILS = 8;
|
||||
@@ -2089,7 +2103,7 @@ function emitVersionedLoop(
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
@@ -2120,6 +2134,7 @@ export function emitLoopCopies(
|
||||
const plan =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.speculation === null &&
|
||||
emitter.speculationResume === null
|
||||
? planSpeculativeLoop(
|
||||
@@ -2147,6 +2162,7 @@ export function emitLoopCopies(
|
||||
const kinds =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
|
||||
emitter.int32Kinds === null &&
|
||||
emitter.speculation === null &&
|
||||
@@ -2437,7 +2453,7 @@ function planProvenLoop(
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" }>,
|
||||
): ProvenLoopPlan | null {
|
||||
if (
|
||||
loop.kind !== "for" ||
|
||||
loop.kind === "doWhile" ||
|
||||
!emitter.speculativeLoopsEnabled ||
|
||||
emitter.speculation !== null ||
|
||||
emitter.speculationResume !== null ||
|
||||
|
||||
@@ -97,7 +97,12 @@ import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import {
|
||||
analyzeInt32Slots,
|
||||
dynamicFieldStores,
|
||||
type DynamicFieldStores,
|
||||
type Int32Slots,
|
||||
} from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js";
|
||||
@@ -171,7 +176,7 @@ import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
|
||||
import { IrNodeIndex } from "../../ir/node-index.js";
|
||||
import { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import { lastUseRead } from "./local-extents.js";
|
||||
import { findReboundParameters } from "./rebound-parameters.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
@@ -506,14 +511,24 @@ function versionParameters(mod: IrModule): IrModule {
|
||||
return versionLoopBoundParameters(mod, mangleFunction);
|
||||
}
|
||||
|
||||
/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and
|
||||
* checked), under the inputs LlEmitter gives it (selectInt32Arrays). */
|
||||
function provenInt32Arrays(mod: IrModule): ReadonlySet<string> {
|
||||
const slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores: dynamicFieldStores(mod),
|
||||
initializerBindings: findInitializerBindings(mod),
|
||||
});
|
||||
return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]);
|
||||
/** The int32 slot analysis of one module and the inputs it read: the
|
||||
* constructor's own analyses of that module, which LlEmitter takes over
|
||||
* instead of repeating them when it emits the same module. */
|
||||
export interface Int32SlotAnalysis {
|
||||
readonly mod: IrModule;
|
||||
readonly nodeIndex: IrNodeIndex;
|
||||
readonly initializerBindings: ReturnType<typeof findInitializerBindings>;
|
||||
readonly dynamicStores: DynamicFieldStores;
|
||||
readonly slots: Int32Slots;
|
||||
}
|
||||
|
||||
/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. */
|
||||
function analyzeModuleInt32Slots(mod: IrModule): Int32SlotAnalysis {
|
||||
const nodeIndex = new IrNodeIndex(mod);
|
||||
const initializerBindings = findInitializerBindings(mod, nodeIndex);
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex);
|
||||
const slots = analyzeInt32Slots(mod, { dynamicStores, initializerBindings, nodes: nodeIndex });
|
||||
return { mod, nodeIndex, initializerBindings, dynamicStores, slots };
|
||||
}
|
||||
|
||||
/** The functions whose calls the backend may emit as calls of their
|
||||
@@ -530,8 +545,16 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
// Each round of selectInt32Arrays analyzes the module it marks; the
|
||||
// emitter of the module it returns reuses that round's analysis.
|
||||
const rounds: { last: Int32SlotAnalysis | null } = { last: null };
|
||||
const proven = (marked: IrModule): ReadonlySet<string> => {
|
||||
const analysis = analyzeModuleInt32Slots(marked);
|
||||
rounds.last = analysis;
|
||||
return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]);
|
||||
};
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
const emitted =
|
||||
options.debugSources === undefined
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
@@ -559,12 +582,12 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
),
|
||||
),
|
||||
),
|
||||
provenInt32Arrays,
|
||||
proven,
|
||||
resultEntryTargets,
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
);
|
||||
: mod;
|
||||
const prepared = rounds.last;
|
||||
return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
@@ -598,6 +621,21 @@ export function llStrBytes(text: string): string {
|
||||
|
||||
const NO_BORROWED: ReadonlySet<number> = new Set();
|
||||
|
||||
/** The nodes that write a binding by its id (numberGlobalWriters). */
|
||||
const GLOBAL_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "assignExpr", "incDec"];
|
||||
/** Runtime library calls (readsLoopMetrics). */
|
||||
const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"];
|
||||
|
||||
/** [A-Za-z0-9_] */
|
||||
function isIdentifierByte(code: number): boolean {
|
||||
return (
|
||||
(code >= 48 && code <= 57) ||
|
||||
(code >= 65 && code <= 90) ||
|
||||
(code >= 97 && code <= 122) ||
|
||||
code === 95
|
||||
);
|
||||
}
|
||||
|
||||
/** Runtime entry points (and await-call entries) that may park the running
|
||||
* execution context, as they appear in emitted function text. */
|
||||
export const SUSPENDS =
|
||||
@@ -1423,6 +1461,9 @@ export class LlEmitter {
|
||||
integerTempsEnabled = false;
|
||||
/** Speculative integer loops (speculative-integers.ts) run in this function. */
|
||||
speculativeLoopsEnabled = false;
|
||||
/** False inside the copy of a versioned loop that runs when its guard
|
||||
* fails: loops there get no speculative or int32 kind copies. */
|
||||
loopCopiesEnabled = true;
|
||||
/** The fast copy of a speculative integer loop being emitted: its
|
||||
* integer-computed writes and where each resumes in the ordinary copy. */
|
||||
speculation: {
|
||||
@@ -1562,6 +1603,7 @@ export class LlEmitter {
|
||||
readonly mod: IrModule,
|
||||
options: LlvmTargetOptions,
|
||||
journal: EmitJournal | null = null,
|
||||
int32Analysis: Int32SlotAnalysis | null = null,
|
||||
) {
|
||||
this.targetOptions = options;
|
||||
this.journal = journal;
|
||||
@@ -1572,7 +1614,7 @@ export class LlEmitter {
|
||||
options.debugSources === undefined);
|
||||
// One walk of every body, which the analyses below read instead of
|
||||
// walking the bodies themselves.
|
||||
this.nodeIndex = new IrNodeIndex(mod);
|
||||
this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod);
|
||||
this.stackTraces = observesStacks(mod, this.nodeIndex);
|
||||
this.nullableUnions = new NullableUnions(
|
||||
mod,
|
||||
@@ -1591,14 +1633,18 @@ export class LlEmitter {
|
||||
this.nullableUnions.armTypes(),
|
||||
);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings = findInitializerBindings(mod, this.nodeIndex);
|
||||
this.initializerBindings =
|
||||
int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
this.int32Slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
const dynamicStores =
|
||||
int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
this.int32Slots =
|
||||
int32Analysis?.slots ??
|
||||
analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.liveViewsReadOnly =
|
||||
mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0;
|
||||
this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map();
|
||||
@@ -1781,6 +1827,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();
|
||||
@@ -1803,9 +1850,9 @@ export class LlEmitter {
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName, this.nodeIndex);
|
||||
const traced = computeTraced(mod, dynamicStores, cycleMutability);
|
||||
this.tracedShapes = traced.shapes;
|
||||
this.tracedUnions = traced.unions;
|
||||
@@ -2234,9 +2281,23 @@ export class LlEmitter {
|
||||
this.mangledFunctions.set(mangleBorrowedFunction(fn.name), fn.name);
|
||||
}
|
||||
}
|
||||
for (const match of text.matchAll(/@(sc_b?f_[A-Za-z0-9_]+)/g)) {
|
||||
const callee = this.mangledFunctions.get(match[1]!);
|
||||
// The names `@(sc_b?f_[A-Za-z0-9_]+)` matches, found with indexOf.
|
||||
for (let at = text.indexOf("@sc_"); at >= 0;) {
|
||||
let start = at + 4;
|
||||
if (text.charCodeAt(start) === 98 /* b */) start++;
|
||||
if (text.charCodeAt(start) !== 102 /* f */ || text.charCodeAt(start + 1) !== 95 /* _ */) {
|
||||
at = text.indexOf("@sc_", at + 1);
|
||||
continue;
|
||||
}
|
||||
let end = start + 2;
|
||||
while (end < text.length && isIdentifierByte(text.charCodeAt(end))) end++;
|
||||
if (end === start + 2) {
|
||||
at = text.indexOf("@sc_", at + 1);
|
||||
continue;
|
||||
}
|
||||
const callee = this.mangledFunctions.get(text.slice(at + 1, end));
|
||||
if (callee !== undefined) yield callee;
|
||||
at = text.indexOf("@sc_", end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3568,15 +3629,14 @@ export class LlEmitter {
|
||||
set.add(fn.name);
|
||||
writers.set(localId, set);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind === "assign" || s.kind === "varDecl") note(s.localId);
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") note(e.localId);
|
||||
return true;
|
||||
},
|
||||
eachNodeOfKinds(fn, this.nodeIndex.scan(GLOBAL_WRITE_KINDS), (node) => {
|
||||
if (
|
||||
node.kind === "assign" ||
|
||||
node.kind === "varDecl" ||
|
||||
node.kind === "assignExpr" ||
|
||||
node.kind === "incDec"
|
||||
)
|
||||
note(node.localId);
|
||||
});
|
||||
}
|
||||
this.globalWriters = writers;
|
||||
@@ -5356,13 +5416,21 @@ export class LlEmitter {
|
||||
private loopMetricsRead: boolean | undefined;
|
||||
readsLoopMetrics(): boolean {
|
||||
if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
|
||||
const visitor = {
|
||||
expr: (e: IrExpr): boolean =>
|
||||
!(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
|
||||
stmt: (): boolean => true,
|
||||
};
|
||||
this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
|
||||
return this.loopMetricsRead;
|
||||
let found = false;
|
||||
const scan = this.nodeIndex.scan(LIB_CALL_KINDS);
|
||||
for (const fn of this.mod.functions) {
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (
|
||||
node.kind === "libCall" &&
|
||||
(node.fn === "perf.loopStart" || node.fn === "perf.idleTime")
|
||||
)
|
||||
found = true;
|
||||
return !found;
|
||||
});
|
||||
if (found) break;
|
||||
}
|
||||
this.loopMetricsRead = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
private lastUseMovesAllowed(): boolean {
|
||||
@@ -5730,6 +5798,10 @@ export class LlEmitter {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.kind === "dynInvoke" && e.calleeNameValue === undefined) {
|
||||
emitDynamicExpr(this, e, true);
|
||||
return;
|
||||
}
|
||||
this.emitExpr(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -1461,6 +1461,7 @@ export function emitMapLikeIntrinsic(
|
||||
: emitStringMapGetScalar(host, r.name, stringKey!);
|
||||
found = scalar.found;
|
||||
if (stringKey !== null) rememberMapEntry(host, e, r.name, stringKey, scalar.entry);
|
||||
else rememberMapEntry(host, e, r.name, numberKey!, scalar.entry, "double");
|
||||
readHit = () => {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
@@ -1532,7 +1533,13 @@ export function emitMapLikeIntrinsic(
|
||||
const vAcc = elemAccess(receiverType.value);
|
||||
if (vAcc === "ref") host.toHeap(v);
|
||||
const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name);
|
||||
if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
if (
|
||||
(kAcc === "str" || kAcc === "f64") &&
|
||||
vAcc !== "ref" &&
|
||||
emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
) {
|
||||
// Written through the read's entry, or inserted.
|
||||
} else if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
const bits = B.tmp();
|
||||
B.line(
|
||||
vAcc === "f64"
|
||||
@@ -1540,12 +1547,7 @@ export function emitMapLikeIntrinsic(
|
||||
: `${bits} = zext i1 ${v.name} to i64`,
|
||||
);
|
||||
emitNumberMapSetScalar(host, r.name, k.name, bits, ordinary);
|
||||
} else if (
|
||||
kAcc !== "str" ||
|
||||
vAcc === "ref" ||
|
||||
!emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
)
|
||||
ordinary();
|
||||
} else ordinary();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "add": {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { BYTES_ELEM_NUM, closureIdentityEqual } from "./common.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
@@ -57,6 +58,7 @@ export function emitDynamicExpr(
|
||||
| "caughtNarrow"
|
||||
| "caughtToDyn"
|
||||
>,
|
||||
discarded = false,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
@@ -257,6 +259,18 @@ export function emitDynamicExpr(
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
// A statement-position push drops the new length, so its entry
|
||||
// need not box it.
|
||||
if (discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id") {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_invoke_discard_id(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
B.line(
|
||||
`call void @scr_dyn_invoke_discard_id(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${host.cstr(e.calleeName)})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: VOID };
|
||||
}
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const LOOP_KINDS: readonly IrNodeKind[] = ["for", "forOf", "while", "doWhile"];
|
||||
|
||||
/** Keep a computed ToUint32 view beside an ordinary number when integer
|
||||
* consumers in loops can reuse it. The number remains authoritative for floating
|
||||
@@ -14,9 +17,16 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
* compare converts the stored number back, and LLVM moves that number's
|
||||
* conversion into the join of a NaN-coded typed-array read (`phi [sitofp x,
|
||||
* 0.0]`), where nothing folds it back to the integer. */
|
||||
export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
export function findIntegerViews(fn: IrFunction, nodes?: IrNodeIndex): Set<string> {
|
||||
const wanted = new Set<string>();
|
||||
if (fn.async || fn.generator) return wanted;
|
||||
// Every view is wanted inside a loop: a body without one wants none.
|
||||
let loops = false;
|
||||
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
|
||||
if (statement && LOOP_KINDS.includes(node.kind)) loops = true;
|
||||
return !loops;
|
||||
});
|
||||
if (!loops) return wanted;
|
||||
const initialized = new Set(fn.params.map((p) => p.localId));
|
||||
const excluded = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const CLOSURE_KINDS: readonly IrNodeKind[] = ["closure"];
|
||||
|
||||
/** The local a node rebinds. Writes through the bound reference, such as a
|
||||
* field store or an array push, leave the binding unchanged. Catch bindings
|
||||
@@ -37,7 +40,11 @@ export class LazyCaptures {
|
||||
private readonly pending = new Set<IrFunction>();
|
||||
private sites: Map<string, { owner: string; captures: string[] }[]> | null = null;
|
||||
|
||||
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking the bodies it holds. */
|
||||
private readonly nodes?: IrNodeIndex,
|
||||
) {}
|
||||
|
||||
/** The captures of closure body `fnName` that hold one value for the
|
||||
* closure's whole life: at every creation site the captured binding is
|
||||
@@ -103,16 +110,15 @@ export class LazyCaptures {
|
||||
private creationSites(): Map<string, { owner: string; captures: string[] }[]> {
|
||||
if (this.sites !== null) return this.sites;
|
||||
const sites = new Map<string, { owner: string; captures: string[] }[]>();
|
||||
const scan = this.nodes?.scan(CLOSURE_KINDS);
|
||||
for (const fn of this.functions.values()) {
|
||||
const visit = (node: IrStmt | IrExpr): boolean => {
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (node.kind === "closure") {
|
||||
let list = sites.get(node.fnName);
|
||||
if (!list) sites.set(node.fnName, (list = []));
|
||||
list.push({ owner: fn.name, captures: node.captures });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
everyStmtList(fn.body, { stmt: visit, expr: visit });
|
||||
});
|
||||
}
|
||||
this.sites = sites;
|
||||
return sites;
|
||||
|
||||
@@ -1054,6 +1054,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "punycode":
|
||||
case "constants":
|
||||
case "promise":
|
||||
case "mime":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "test":
|
||||
return emitTestLibCall(host, e);
|
||||
|
||||
@@ -83,6 +83,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"util.styleText": "scr_util_style_text",
|
||||
"util.inherits": "scr_util_inherits",
|
||||
"util.deprecate": "scr_util_deprecate",
|
||||
"util.isArrayDeprecated": "scr_util_is_array_deprecated",
|
||||
"util.extend": "scr_util_extend",
|
||||
"util.signalExitCode": "scr_util_signal_exit_code",
|
||||
"util.aborted": "scr_util_aborted",
|
||||
"util.diff": "scr_util_diff",
|
||||
"util.debounce": "scr_util_debounce",
|
||||
"util.throttle": "scr_util_throttle",
|
||||
"mime.typeNew": "scr_mime_type_new",
|
||||
"mime.typeParse": "scr_mime_type_parse",
|
||||
"mime.paramsNew": "scr_mime_params_new",
|
||||
"mime.is": "scr_mime_is",
|
||||
"util.callbackify": "scr_util_callbackify",
|
||||
"util.promisifyDyn": "scr_util_promisify",
|
||||
"util.debuglog": "scr_util_debuglog",
|
||||
@@ -1086,6 +1097,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"insp.customCallEnd": "scr_insp_custom_call_end",
|
||||
"insp.formatSpecifies": "scr_insp_format_specifies",
|
||||
"insp.formatSpecifiesDyn": "scr_insp_format_specifies_dyn",
|
||||
"insp.dynFormatS": "scr_insp_dyn_format_s",
|
||||
"insp.dynHidden": "scr_insp_dyn_hidden",
|
||||
"fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn",
|
||||
"fs.readFileEncoded": "scr_fs_read_file_encoded",
|
||||
@@ -1721,9 +1733,14 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"buffer.deprecation",
|
||||
// The wrappers schedule warnings and callbacks on the tick queue.
|
||||
"util.deprecate",
|
||||
"util.isArrayDeprecated",
|
||||
"util.extend",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
// The wrapped calls arm timers and settle promises.
|
||||
"util.debounce",
|
||||
"util.throttle",
|
||||
"worker.new",
|
||||
"worker.parentPort",
|
||||
"worker.channel",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { F64, isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { OptionalArrayReads } from "./local-array-reads.js";
|
||||
@@ -57,6 +58,8 @@ export function dynHoleRead(value: IrExpr): (IrExpr & { kind: "arrayGet" }) | nu
|
||||
return same(cond.left.arr, then.arr) && same(cond.left.index, then.index) ? then : null;
|
||||
}
|
||||
|
||||
const LOOP_KINDS: readonly IrNodeKind[] = ["forOf", "for", "while", "doWhile"];
|
||||
|
||||
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
|
||||
* Initialization and unrelated work elsewhere in the function may mutate.
|
||||
* The array remains owned by its unchanged local or global binding; the
|
||||
@@ -72,6 +75,8 @@ export function findLoopArrayBorrows(
|
||||
optionalReads?: OptionalArrayReads,
|
||||
writes?: ReferenceWrites | null,
|
||||
functions?: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking for the loops. */
|
||||
nodes?: IrNodeIndex,
|
||||
): ReadonlySet<IrStmt> {
|
||||
const result = new Set<IrStmt>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -88,9 +93,10 @@ export function findLoopArrayBorrows(
|
||||
(!local.boxed && !local.tdz && (bindings?.has(local.id) || parameters.has(local.id)))
|
||||
);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (loop) => {
|
||||
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
|
||||
if (!statement) return true;
|
||||
const loop = node as IrStmt;
|
||||
{
|
||||
if (loop.kind === "forOf") {
|
||||
const local = locals.get(loop.localId);
|
||||
if (
|
||||
@@ -187,7 +193,7 @@ export function findLoopArrayBorrows(
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -137,12 +137,20 @@ test("a read followed by a write of the same map pairs, and the write reuses the
|
||||
}
|
||||
});
|
||||
|
||||
test("number keys keep the write-after-read lookup instead of pairing", () => {
|
||||
// findReadNumberMapWrites stores such writes into the found entry through
|
||||
// a read-only probe that LLVM merges with the read's.
|
||||
const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(F64))], F64, F64);
|
||||
expect(pairs(mod)).toBe(0);
|
||||
expect(body(mod)).not.toContain("mapfuse.hit");
|
||||
test("number keys pair, and the write compares the key's bits with the read's", () => {
|
||||
for (const value of [F64, BOOL]) {
|
||||
const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(value))], F64, value);
|
||||
expect(pairs(mod)).toBe(1);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("mapfuse.hit");
|
||||
expect(ir).toContain("alloca double ; fused map read: key");
|
||||
expect(ir).toMatch(/icmp eq i64 %t\d+, %t\d+/);
|
||||
// The miss still inserts through the runtime.
|
||||
expect(ir).toMatch(/call void @scr_map_set_f64_(?:f64|bool)\(/);
|
||||
expect(ir).toContain("@sc_map_entry_f64(");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("the single-statement form pairs an unconditional nested read", () => {
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* key match by identity implies the SameValueZero match the set would find.
|
||||
*
|
||||
* Only scalar values (number, boolean) fuse: overwriting them releases
|
||||
* nothing, exactly like the runtime's own overwrite. Only string-keyed
|
||||
* typed maps fuse here: a number-key write after a read of the same key
|
||||
* already probes through the read-only entry lookup that LLVM merges with
|
||||
* the read's (findReadNumberMapWrites, map-read-lifetimes.ts).
|
||||
* nothing, exactly like the runtime's own overwrite. String-keyed and
|
||||
* number-keyed typed maps fuse. A number key matches when its bits are the
|
||||
* read's: such keys are SameValueZero-equal, and a key that is equal with
|
||||
* other bits (-0 for 0) takes the ordinary set.
|
||||
*/
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
@@ -30,7 +30,7 @@ type MapOp = Extract<IrExpr, { kind: "mapIntrinsic" }>;
|
||||
/** One fused read/write pair. The slots exist once the read emits through
|
||||
* an entry path; a write whose read took another path stays ordinary. */
|
||||
export interface MapEntryPair {
|
||||
slots: { map: string; key: string; entry: string } | null;
|
||||
slots: { map: string; key: string; entry: string; keyType: "ptr" | "double" } | null;
|
||||
}
|
||||
|
||||
export interface MapEntryFusion {
|
||||
@@ -38,13 +38,14 @@ export interface MapEntryFusion {
|
||||
writes: Map<IrExpr, MapEntryPair>;
|
||||
}
|
||||
|
||||
/** A typed string-keyed Map read/write with a scalar value. */
|
||||
/** A typed string- or number-keyed Map read/write with a scalar value. */
|
||||
function fusableOp(e: IrExpr, method: "get" | "set"): e is MapOp {
|
||||
if (e.kind !== "mapIntrinsic" || e.method !== method) return false;
|
||||
const receiver = e.receiver.type;
|
||||
if (receiver.kind !== "map") return false;
|
||||
if (receiver.kind !== "map" || receiver.key.kind === "union") return false;
|
||||
if (receiver.value.kind !== "f64" && receiver.value.kind !== "bool") return false;
|
||||
if (mapKeyAccess(receiver.key) !== "str") return false;
|
||||
const access = mapKeyAccess(receiver.key);
|
||||
if (access !== "str" && access !== "f64") return false;
|
||||
return method === "get" ? borrowsMapReadInputs(e) : borrowsMapMutationReceiver(e);
|
||||
}
|
||||
|
||||
@@ -219,23 +220,25 @@ export function findMapEntryFusions(fn: IrFunction): MapEntryFusion {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** After a fused read resolved `entry` for the string `key` in `map`: park
|
||||
* all three for the paired write. */
|
||||
/** After a fused read resolved `entry` for `key` in `map`: park all three
|
||||
* for the paired write. `keyType` is the key's LLVM type: a string's ptr or
|
||||
* a number's double. */
|
||||
export function rememberMapEntry(
|
||||
host: LlvmEmitterContext,
|
||||
read: IrExpr,
|
||||
map: string,
|
||||
key: string,
|
||||
entry: string,
|
||||
keyType: "ptr" | "double" = "ptr",
|
||||
): void {
|
||||
const pair = host.mapEntryFusion?.reads.get(read);
|
||||
if (!pair) return;
|
||||
const B = host.B;
|
||||
if (pair.slots === null) {
|
||||
const slots = { map: B.slot(), key: B.slot(), entry: B.slot() };
|
||||
const slots = { map: B.slot(), key: B.slot(), entry: B.slot(), keyType };
|
||||
B.entryAllocas.push(
|
||||
`${slots.map} = alloca ptr ; fused map read: map`,
|
||||
`${slots.key} = alloca ptr ; fused map read: key`,
|
||||
`${slots.key} = alloca ${keyType} ; fused map read: key`,
|
||||
`${slots.entry} = alloca ptr ; fused map read: entry`,
|
||||
`store ptr null, ptr ${slots.entry}`,
|
||||
);
|
||||
@@ -243,7 +246,7 @@ export function rememberMapEntry(
|
||||
}
|
||||
const slots = pair.slots;
|
||||
B.line(`store ptr ${map}, ptr ${slots.map}`);
|
||||
B.line(`store ptr ${key}, ptr ${slots.key}`);
|
||||
B.line(`store ${slots.keyType} ${key}, ptr ${slots.key}`);
|
||||
B.line(`store ptr ${entry}, ptr ${slots.entry}`);
|
||||
}
|
||||
|
||||
@@ -270,13 +273,20 @@ export function emitFusedMapSet(
|
||||
both = B.tmp(),
|
||||
reuse = B.tmp();
|
||||
B.line(`${parkedMap} = load ptr, ptr ${slots.map}`);
|
||||
B.line(`${parkedKey} = load ptr, ptr ${slots.key}`);
|
||||
B.line(`${parkedKey} = load ${slots.keyType}, ptr ${slots.key}`);
|
||||
B.line(`${entry} = load ptr, ptr ${slots.entry}`);
|
||||
// The entry is valid only between its read and this write.
|
||||
B.line(`store ptr null, ptr ${slots.entry}`);
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
B.line(`${sameMap} = icmp eq ptr ${parkedMap}, ${map}`);
|
||||
B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`);
|
||||
if (slots.keyType === "ptr") B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`);
|
||||
else {
|
||||
const parkedBits = B.tmp(),
|
||||
keyBits = B.tmp();
|
||||
B.line(`${parkedBits} = bitcast double ${parkedKey} to i64`);
|
||||
B.line(`${keyBits} = bitcast double ${key} to i64`);
|
||||
B.line(`${sameKey} = icmp eq i64 ${parkedBits}, ${keyBits}`);
|
||||
}
|
||||
B.line(`${both} = and i1 ${sameMap}, ${sameKey}`);
|
||||
B.line(`${reuse} = select i1 ${found}, i1 ${both}, i1 false`);
|
||||
const hit = B.newLabel("mapfuse.hit"),
|
||||
|
||||
@@ -314,6 +314,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
// Booleans are stored as 0/1, already the widened union payload.
|
||||
const scalar = emitNumberMapGetScalar(host, receiver.name, number);
|
||||
found = scalar.found;
|
||||
rememberMapEntry(host, read.expr, receiver.name, number, scalar.entry, "double");
|
||||
B.line(`store i64 ${scalar.bits}, ptr ${payload}`);
|
||||
}
|
||||
} else if (string !== null && !isRefCounted(read.value)) {
|
||||
|
||||
@@ -110,10 +110,9 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe(
|
||||
"@sc_rc_release_union",
|
||||
);
|
||||
// Array retains inline; array releases stay calls.
|
||||
const array = { kind: "array", elem: { kind: "string" } } as never;
|
||||
expect(retainSym(host, array)).toBe("@sc_rc_retain_arr");
|
||||
expect(releaseSym(host, array)).toBe("@scr_arr_release");
|
||||
expect(releaseSym(host, array)).toBe("@sc_rc_release_arr");
|
||||
expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn");
|
||||
expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn");
|
||||
// Unmirrored families and caught snapshots keep their runtime entry points.
|
||||
@@ -125,6 +124,7 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(declarations).toContain("declare void @scr_str_release(ptr)");
|
||||
expect(declarations).toContain("declare void @scr_cyc_on_release(ptr)");
|
||||
expect(inlineRcDecls(host).sort()).toEqual([
|
||||
"declare void @scr_arr_release(ptr)",
|
||||
"declare void @scr_cyc_on_release(ptr)",
|
||||
"declare void @scr_dyn_release(ptr)",
|
||||
"declare void @scr_dyn_release_survivor(ptr)",
|
||||
@@ -280,6 +280,22 @@ describe("inline release budgets", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("each family's budget applies to its own sites in a function that has both", () => {
|
||||
const strings = body("mixed", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str");
|
||||
// One function: string sites over their budget, dyn sites within theirs.
|
||||
const mixed = strings.replace(
|
||||
" ret void",
|
||||
` call void @sc_rc_release_dyn(ptr %v)\n call void @sc_rc_release_other(ptr %v)\n ret void`,
|
||||
);
|
||||
const other = body("other", 2, "str");
|
||||
const limited = limitInlineReleases([mixed, other], "release").join("\n");
|
||||
const out = limited.slice(0, limited.indexOf("@other("));
|
||||
expect(out).not.toContain("@sc_rc_release_str(");
|
||||
expect(out).toContain("call void @sc_rc_release_dyn(ptr %v)");
|
||||
expect(out).toContain("call void @sc_rc_release_other(ptr %v)");
|
||||
expect(limited).toContain(other);
|
||||
});
|
||||
|
||||
test("a function over the string budget calls the runtime string release at every site", () => {
|
||||
const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str");
|
||||
const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str");
|
||||
|
||||
@@ -550,17 +550,16 @@ interface InlineRcFamily {
|
||||
* scr_dyn_release_survivor for kinds whose candidacy is per value,
|
||||
* including arrays and objects of intact documents (ScrDynRegion).
|
||||
*
|
||||
* Releases stay plain calls for:
|
||||
* - arr: array releases are the most numerous release sites and the
|
||||
* least hot (at most 1% self time across the benchmark suite), and
|
||||
* inlining them roughly doubled the code growth for no measured speedup.
|
||||
* Array retains are inlined. */
|
||||
* The array release inlines the decrement of an array that stays alive
|
||||
* and is not headered (`a->cyc`): those are most array releases in
|
||||
* type-checker workloads, where scr_arr_release's own prologue and checks
|
||||
* took about half of its time. */
|
||||
export const INLINE_RC_FAMILIES: Readonly<Record<string, InlineRcFamily>> = {
|
||||
str: { cycle: "none", retainNull: false, release: "scr_str_release" },
|
||||
bytes: { cycle: "none", retainNull: false, release: "scr_bytes_release" },
|
||||
regex: { cycle: "none", retainNull: false, release: "scr_regex_release" },
|
||||
bigint: { cycle: "none", retainNull: true, release: "scr_bigint_release" },
|
||||
arr: { cycle: "arr", retainNull: false, release: null },
|
||||
arr: { cycle: "arr", retainNull: false, release: "scr_arr_release" },
|
||||
map: { cycle: "map", retainNull: false, release: "scr_map_release" },
|
||||
union: { cycle: "always", retainNull: false, release: "scr_union_release" },
|
||||
closure: { cycle: "always", retainNull: false, release: "scr_closure_release" },
|
||||
@@ -692,34 +691,32 @@ export function limitInlineReleases(
|
||||
parts: readonly string[],
|
||||
mode: InlineRcMode | undefined,
|
||||
): string[] {
|
||||
const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION);
|
||||
return mode === "release"
|
||||
? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION)
|
||||
: dyn;
|
||||
}
|
||||
|
||||
function limitInlineReleaseFamily(
|
||||
parts: readonly string[],
|
||||
family: "dyn" | "str",
|
||||
max: number,
|
||||
): string[] {
|
||||
const call = `call void @${inlineRcName(family, "release")}(`;
|
||||
const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`;
|
||||
const families: readonly ("dyn" | "str")[] = mode === "release" ? ["dyn", "str"] : ["dyn"];
|
||||
const limits = families.map((family) =>
|
||||
family === "dyn" ? INLINE_DYN_RELEASES_PER_FUNCTION : INLINE_STR_RELEASES_PER_FUNCTION,
|
||||
);
|
||||
const calls = families.map((family) => `${inlineRcName(family, "release")}(`);
|
||||
const runtimes = families.map((family) => `call void @${INLINE_RC_FAMILIES[family]!.release}(`);
|
||||
const head = "call void @";
|
||||
const prefix = `${head}${inlineRcName("", "release")}`;
|
||||
// The function each release site belongs to: the last `define` line
|
||||
// before it, counted over the joined text.
|
||||
// before it, counted over the joined text. One walk over each part finds
|
||||
// the sites of every family.
|
||||
const sites = (
|
||||
part: string,
|
||||
each: (offset: number, fn: number) => void,
|
||||
each: (offset: number, family: number, fn: number) => void,
|
||||
first: number,
|
||||
): number => {
|
||||
let fn = first;
|
||||
let define = part.startsWith("define ") ? 0 : part.indexOf("\ndefine ");
|
||||
for (let at = part.indexOf(call); at >= 0; at = part.indexOf(call, at + call.length)) {
|
||||
for (let at = part.indexOf(prefix); at >= 0; at = part.indexOf(prefix, at + prefix.length)) {
|
||||
const family = calls.findIndex((call) => part.startsWith(call, at + head.length));
|
||||
if (family < 0) continue;
|
||||
while (define >= 0 && define < at) {
|
||||
fn++;
|
||||
define = part.indexOf("\ndefine ", define + 1);
|
||||
}
|
||||
each(at, fn);
|
||||
each(at, family, fn);
|
||||
}
|
||||
while (define >= 0) {
|
||||
fn++;
|
||||
@@ -727,29 +724,47 @@ function limitInlineReleaseFamily(
|
||||
}
|
||||
return fn;
|
||||
};
|
||||
const counts = new Map<number, number>();
|
||||
const counts = families.map(() => new Map<number, number>());
|
||||
// The functions each part's sites can belong to.
|
||||
const spans: number[] = [];
|
||||
let fn = 0;
|
||||
for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn);
|
||||
const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f));
|
||||
if (over.size === 0) return [...parts];
|
||||
fn = 0;
|
||||
return parts.map((part) => {
|
||||
const cuts: number[] = [];
|
||||
for (const part of parts) {
|
||||
spans.push(fn);
|
||||
fn = sites(
|
||||
part,
|
||||
(at, f) => {
|
||||
if (over.has(f)) cuts.push(at);
|
||||
},
|
||||
(_, family, f) => counts[family]!.set(f, (counts[family]!.get(f) ?? 0) + 1),
|
||||
fn,
|
||||
);
|
||||
spans.push(fn);
|
||||
}
|
||||
const over = counts.map(
|
||||
(count, family) => new Set([...count].filter(([, n]) => n > limits[family]!).map(([f]) => f)),
|
||||
);
|
||||
if (over.every((set) => set.size === 0)) return [...parts];
|
||||
const anyOver = new Set(over.flatMap((set) => [...set]));
|
||||
const touches = (from: number, to: number): boolean => {
|
||||
for (let f = from; f <= to; f++) if (anyOver.has(f)) return true;
|
||||
return false;
|
||||
};
|
||||
return parts.map((part, index) => {
|
||||
const from = spans[2 * index]!;
|
||||
if (!touches(from, spans[2 * index + 1]!)) return part;
|
||||
const cuts: { at: number; family: number }[] = [];
|
||||
sites(
|
||||
part,
|
||||
(at, family, f) => {
|
||||
if (over[family]!.has(f)) cuts.push({ at, family });
|
||||
},
|
||||
from,
|
||||
);
|
||||
if (cuts.length === 0) return part;
|
||||
let out = "";
|
||||
let from = 0;
|
||||
for (const at of cuts) {
|
||||
out += part.slice(from, at) + runtime;
|
||||
from = at + call.length;
|
||||
let next = 0;
|
||||
for (const cut of cuts) {
|
||||
out += part.slice(next, cut.at) + runtimes[cut.family]!;
|
||||
next = cut.at + head.length + calls[cut.family]!.length;
|
||||
}
|
||||
return out + part.slice(from);
|
||||
return out + part.slice(next);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export function computeMayThrow(
|
||||
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
|
||||
const constructorClasses = new Map<string, string>();
|
||||
const classValueCallers = new Map<string, Set<string>>();
|
||||
const tdzGlobals = (mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id);
|
||||
const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id));
|
||||
for (const cls of mod.classes ?? []) {
|
||||
constructorClasses.set(`%${cls.name}.constructor`, cls.name);
|
||||
for (const m of cls.methods ?? []) {
|
||||
@@ -113,11 +113,10 @@ export function computeMayThrow(
|
||||
if (explain) f.calleeLocs = new Map();
|
||||
// TDZ reads and non-initializing writes can throw ReferenceError.
|
||||
// Capture locals carry the same flag as the declaring binding.
|
||||
const tdzIds = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]);
|
||||
const mutableTdzIds = new Set([
|
||||
...tdzGlobals,
|
||||
...fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id),
|
||||
]);
|
||||
// Every TDZ global counts for both reads and writes; the module's set is
|
||||
// shared by every function.
|
||||
const tdzLocals = new Set(fn.locals.filter((l) => l.tdz).map((l) => l.id));
|
||||
const mutableTdzLocals = new Set(fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id));
|
||||
// Traverse typed executable nodes without copying the IR into unknown.
|
||||
const visit = (rec: IrExpr | IrStmt): boolean => {
|
||||
const threw = f.throws;
|
||||
@@ -133,13 +132,17 @@ export function computeMayThrow(
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
if (tdzIds.has(rec.localId)) f.throws = true;
|
||||
if (tdzGlobals.has(rec.localId) || tdzLocals.has(rec.localId)) f.throws = true;
|
||||
break;
|
||||
case "assign":
|
||||
// A declaration is allowed to fill an empty box. Legacy const
|
||||
// TDZ stores also initialize; only mutable subsequent stores
|
||||
// introduce the new write-side exception edge.
|
||||
if (rec.initializes !== true && mutableTdzIds.has(rec.localId)) f.throws = true;
|
||||
if (
|
||||
rec.initializes !== true &&
|
||||
(tdzGlobals.has(rec.localId) || mutableTdzLocals.has(rec.localId))
|
||||
)
|
||||
f.throws = true;
|
||||
break;
|
||||
case "dynCheck":
|
||||
case "caughtCheck":
|
||||
|
||||
@@ -58,6 +58,10 @@ export interface NativeLinkFeatures {
|
||||
/** events.once with options: scr_events_once_signal.c. Absent (not
|
||||
* false) for programs without it. */
|
||||
eventsOnceSignal?: boolean;
|
||||
/** util.MIMEType and util.MIMEParams: scr_mime.c. */
|
||||
mime?: boolean;
|
||||
/** util.debounce and util.throttle: scr_util_timing.c. */
|
||||
utilTiming?: boolean;
|
||||
/** Buffer.prototype methods called on checked-dynamic values link
|
||||
* scr_bytes_dyn.c; absent (false) for programs without them. */
|
||||
dynBuffer?: boolean;
|
||||
|
||||
@@ -131,6 +131,10 @@ export interface CcOptions {
|
||||
/** events.once with options (events.onceSignal): compiles
|
||||
* scr_events_once_signal.c. */
|
||||
eventsOnceSignal?: boolean;
|
||||
/** util.MIMEType and util.MIMEParams: compiles scr_mime.c. */
|
||||
mime?: boolean;
|
||||
/** util.debounce and util.throttle: compiles scr_util_timing.c. */
|
||||
utilTiming?: boolean;
|
||||
/** Buffer.prototype methods called on checked-dynamic values:
|
||||
* compiles scr_bytes_dyn.c. */
|
||||
dynBuffer?: boolean;
|
||||
|
||||
@@ -246,6 +246,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
// events.once with options registers through the emitter and
|
||||
// event-target units.
|
||||
unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"),
|
||||
unit((u) => u.opts.mime === true, "scr_mime.c"),
|
||||
unit((u) => u.opts.utilTiming === true, "scr_util_timing.c"),
|
||||
// The checked-dynamic HANDLE support unit (listener gate + runtime
|
||||
// adapter closures): every referencing unit is one of the emitter or
|
||||
// net families (http implies net), so handle-free binaries keep
|
||||
|
||||
@@ -89,6 +89,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
declAdopt: boolean;
|
||||
dynForAwait: boolean;
|
||||
eventsOnceSignal: boolean;
|
||||
mime: boolean;
|
||||
utilTiming: boolean;
|
||||
dynBuffer: boolean;
|
||||
builtinValues: boolean;
|
||||
warningReplace: boolean;
|
||||
@@ -288,6 +290,8 @@ export function effectiveRuntimeFeatures(
|
||||
declAdopt: features.declAdopt === true,
|
||||
dynForAwait: features.dynForAwait === true,
|
||||
eventsOnceSignal: features.eventsOnceSignal === true,
|
||||
mime: features.mime === true,
|
||||
utilTiming: features.utilTiming === true,
|
||||
dynBuffer: features.dynBuffer === true,
|
||||
builtinValues: features.builtinValues === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
|
||||
@@ -14,6 +14,10 @@ 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,
|
||||
@@ -191,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
|
||||
@@ -228,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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -296,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;
|
||||
@@ -312,6 +321,7 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
"-std=c11",
|
||||
...driver.targetArgs,
|
||||
...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
|
||||
...(options.flags ?? []),
|
||||
"-I",
|
||||
vendor,
|
||||
"-c",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -75,6 +75,8 @@ export interface EarlyExecutableNativeFeatures {
|
||||
declAdopt?: boolean;
|
||||
dynForAwait?: boolean;
|
||||
eventsOnceSignal?: boolean;
|
||||
mime?: boolean;
|
||||
utilTiming?: boolean;
|
||||
dynBuffer?: boolean;
|
||||
builtinValues?: boolean;
|
||||
events: boolean;
|
||||
|
||||
@@ -270,3 +270,49 @@ test("synchronous tracking restores parents after throws and propagates unstable
|
||||
trackedFileExists(join(dir, "outside.ts"));
|
||||
expect(parent.snapshot()).toEqual(before);
|
||||
});
|
||||
|
||||
test("a settled file is checked by its identity, and any write invalidates it", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
await writeFile(source, "export const answer = 1;\n");
|
||||
const written = fs.statSync(source);
|
||||
// The file was last changed long enough before the read.
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(Date.now() + 5000);
|
||||
const tracker = new FrontendInputTracker();
|
||||
try {
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
const snapshot = tracker.snapshot();
|
||||
const probe = snapshot.probes.find((entry) => entry.op === "file");
|
||||
expect(probe?.op === "file" && probe.identity !== undefined).toBe(true);
|
||||
expect(validFrontendInputSnapshot(snapshot)).toBe(true);
|
||||
const reads = vi.mocked(fs.readFileSync);
|
||||
reads.mockClear();
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(0);
|
||||
// A same-size rewrite that restores the modification time still changes
|
||||
// the change time.
|
||||
await writeFile(source, "export const answer = 2;\n");
|
||||
fs.utimesSync(source, written.atime, written.mtime);
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(false);
|
||||
});
|
||||
|
||||
test("a file changed shortly before the read is hashed on every check", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
await writeFile(source, "export const answer = 1;\n");
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
const snapshot = tracker.snapshot();
|
||||
const probe = snapshot.probes.find((entry) => entry.op === "file");
|
||||
expect(probe?.op === "file" && probe.identity).toBeUndefined();
|
||||
const reads = vi.mocked(fs.readFileSync);
|
||||
reads.mockClear();
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@ import { observeSourceText } from "./ts7/ast-node.js";
|
||||
* behind an old resolution answer.
|
||||
*/
|
||||
export type FrontendInputProbe =
|
||||
| { op: "file"; path: string; digest: string }
|
||||
/** `identity` (fileIdentity) lets a later check skip hashing the file. */
|
||||
| { op: "file"; path: string; digest: string; identity?: string }
|
||||
| { op: "read-error"; path: string }
|
||||
| { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" }
|
||||
| { op: "entries"; path: string; files: string[]; directories: string[] }
|
||||
@@ -53,6 +54,36 @@ function frontendSourceDigest(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
/** How long a file must have been unchanged before a read for its identity
|
||||
* to stand for its content: a later write then gets a newer change time
|
||||
* even on file systems whose timestamps tick once a second or every two. */
|
||||
const IDENTITY_SETTLE_MS = 2000;
|
||||
|
||||
/** The device, inode, size, modification time and change time of `path`. A
|
||||
* write changes the change time, which no tool can set back, so an equal
|
||||
* identity means the content is the one hashed with it. */
|
||||
function fileIdentity(path: string): string | null {
|
||||
try {
|
||||
const info = statSync(path);
|
||||
return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The text of `path` with its identity when the identity was the same before
|
||||
* and after the read and the file had been unchanged for IDENTITY_SETTLE_MS. */
|
||||
function readWithIdentity(path: string): { text: string; identity: string | null } {
|
||||
const readAt = Date.now();
|
||||
const before = statSync(path);
|
||||
const text = readFileSync(path, "utf8");
|
||||
const after = fileIdentity(path);
|
||||
const identity = `${before.dev}:${before.ino}:${before.size}:${before.mtimeMs}:${before.ctimeMs}`;
|
||||
const settled =
|
||||
before.mtimeMs <= readAt - IDENTITY_SETTLE_MS && before.ctimeMs <= readAt - IDENTITY_SETTLE_MS;
|
||||
return { text, identity: settled && after === identity ? identity : null };
|
||||
}
|
||||
|
||||
function pathKind(path: string): Extract<FrontendInputProbe, { op: "kind" }>["kind"] {
|
||||
try {
|
||||
const info = statSync(path);
|
||||
@@ -199,8 +230,11 @@ export function trackedReadFile(path: string): string | null {
|
||||
const virtual = virtualSources.get(path);
|
||||
if (virtual !== undefined) return virtual;
|
||||
try {
|
||||
const text = readFileSync(path, "utf8");
|
||||
record({ op: "file", path, digest: frontendSourceDigest(text) });
|
||||
const { text, identity } = readWithIdentity(path);
|
||||
const digest = frontendSourceDigest(text);
|
||||
record(
|
||||
identity === null ? { op: "file", path, digest } : { op: "file", path, digest, identity },
|
||||
);
|
||||
return text;
|
||||
} catch {
|
||||
record({ op: "read-error", path });
|
||||
@@ -337,6 +371,9 @@ export function frontendInputsStillMatch(
|
||||
}
|
||||
switch (probe.op) {
|
||||
case "file": {
|
||||
// An unchanged identity stands for the content hashed with it.
|
||||
if (probe.identity !== undefined && fileIdentity(probe.path) === probe.identity)
|
||||
return true;
|
||||
try {
|
||||
return frontendSourceDigest(readFileSync(probe.path, "utf8")) === probe.digest;
|
||||
} catch {
|
||||
@@ -443,6 +480,11 @@ export function frontendInputsSemanticallyMatch(
|
||||
probes.push(probe);
|
||||
continue;
|
||||
}
|
||||
if (probe.identity !== undefined && fileIdentity(probe.path) === probe.identity) {
|
||||
currentSources.set(probe.path, previous);
|
||||
probes.push(probe);
|
||||
continue;
|
||||
}
|
||||
let current: string;
|
||||
try {
|
||||
current = readFileSync(probe.path, "utf8");
|
||||
@@ -460,7 +502,7 @@ export function frontendInputsSemanticallyMatch(
|
||||
if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current))
|
||||
return null;
|
||||
changed.push({ path: probe.path, previous, current });
|
||||
probes.push({ ...probe, digest: currentDigest });
|
||||
probes.push({ op: "file", path: probe.path, digest: currentDigest });
|
||||
}
|
||||
const adjusted: FrontendInputSnapshot = { ...snapshot, probes };
|
||||
if (!frontendInputsStillMatch(adjusted, exclusions)) return null;
|
||||
@@ -488,7 +530,11 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front
|
||||
if (typeof value.path !== "string" || typeof value.op !== "string") return false;
|
||||
switch (value.op) {
|
||||
case "file":
|
||||
return typeof value.digest === "string" && /^[0-9a-f]{64}$/.test(value.digest);
|
||||
return (
|
||||
typeof value.digest === "string" &&
|
||||
/^[0-9a-f]{64}$/.test(value.digest) &&
|
||||
(value.identity === undefined || typeof value.identity === "string")
|
||||
);
|
||||
case "read-error":
|
||||
return true;
|
||||
case "kind":
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { dynUndefinedExpr, numLit } from "../../../ir/build.js";
|
||||
import { BOOL, DYN, type IrExpr } from "../../../ir/ir.js";
|
||||
import { builtinImportOf } from "./module-bindings.js";
|
||||
|
||||
/* node:util's MIMEType and MIMEParams are native checked-dynamic handles
|
||||
* (scr_mime.c): both types map to dyn, their getters and setters are
|
||||
* handle property reads and writes, their methods dispatch through the
|
||||
* handle, and these helpers lower the uses a dyn value cannot answer by
|
||||
* itself (construction, MIMEType.parse and instanceof). */
|
||||
|
||||
type MimeClass = "MIMEType" | "MIMEParams";
|
||||
|
||||
/** The MIMEType or MIMEParams class in any node:util spelling. */
|
||||
export function mimeClassOf(lowerer: Lowerer, node: ts.Expression): MimeClass | null {
|
||||
const builtin = ts.isIdentifier(node)
|
||||
? builtinImportOf(lowerer, node)
|
||||
: ts.isPropertyAccessExpression(node) && !node.questionDotToken
|
||||
? lowerer.builtinMemberOf(node)
|
||||
: null;
|
||||
if (builtin?.module !== "util") return null;
|
||||
return builtin.member === "MIMEType" || builtin.member === "MIMEParams" ? builtin.member : null;
|
||||
}
|
||||
|
||||
/** The class of a stdlib MIMEType- or MIMEParams-typed value. */
|
||||
function mimeValueClass(lowerer: Lowerer, node: ts.Expression): MimeClass | null {
|
||||
const type = lowerer.typeOf(node);
|
||||
const sym = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (!sym) return null;
|
||||
const name = ts.symbolName(sym);
|
||||
if (name !== "MIMEType" && name !== "MIMEParams") return null;
|
||||
return lowerer.isStdlibSymbol(sym) ? name : null;
|
||||
}
|
||||
|
||||
/** Every argument in order, as checked-dynamic values; the first `count`
|
||||
* fill the call, missing ones as undefined, and the others still
|
||||
* evaluate. */
|
||||
function mimeArguments(
|
||||
lowerer: Lowerer,
|
||||
nodes: readonly ts.Expression[],
|
||||
count: number,
|
||||
loc: ReturnType<typeof locOf>,
|
||||
): { used: IrExpr[]; extra: IrExpr[] } {
|
||||
const values = nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
|
||||
const used = values.slice(0, count);
|
||||
while (used.length < count) used.push(dynUndefinedExpr(loc));
|
||||
return { used, extra: values.slice(count) };
|
||||
}
|
||||
|
||||
/** The call, after which any surplus arguments evaluate. */
|
||||
function withSurplus(
|
||||
lowerer: Lowerer,
|
||||
call: IrExpr,
|
||||
extra: IrExpr[],
|
||||
loc: ReturnType<typeof locOf>,
|
||||
): IrExpr {
|
||||
if (extra.length === 0) return call;
|
||||
const local = lowerer.declareHiddenLocal("%mimeResult", call.type);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: local.id, init: call, loc },
|
||||
...extra.map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })),
|
||||
],
|
||||
result: { kind: "varRef", localId: local.id, type: call.type, loc },
|
||||
type: call.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `new MIMEType(string[, noThrow])` and `new MIMEParams()`. */
|
||||
export function lowerMimeNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
const cls = mimeClassOf(lowerer, expr.expression);
|
||||
if (cls === null) return null;
|
||||
const nodes = expr.arguments?.nodes ?? [];
|
||||
if (nodes.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(expr);
|
||||
const { used, extra } = mimeArguments(lowerer, nodes, cls === "MIMEType" ? 2 : 0, loc);
|
||||
const call: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: cls === "MIMEType" ? "mime.typeNew" : "mime.paramsNew",
|
||||
args: used,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return withSurplus(lowerer, call, extra, loc);
|
||||
}
|
||||
|
||||
const INSTANCE_METHODS: Record<MimeClass, ReadonlySet<string>> = {
|
||||
MIMEType: new Set(["toString", "toJSON"]),
|
||||
MIMEParams: new Set([
|
||||
"get",
|
||||
"has",
|
||||
"set",
|
||||
"delete",
|
||||
"entries",
|
||||
"keys",
|
||||
"values",
|
||||
"toString",
|
||||
"toJSON",
|
||||
]),
|
||||
};
|
||||
|
||||
/** MIMEType.parse(), and the instance methods, which the handle answers
|
||||
* as methods, so they dispatch through dynInvoke rather than as a member
|
||||
* read and a call. */
|
||||
export function lowerMimeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
|
||||
const access = call.expression;
|
||||
if (access.questionDotToken || call.arguments.nodes.some(ts.isSpreadElement)) return null;
|
||||
const name = access.name.text;
|
||||
const loc = locOf(call);
|
||||
if (name === "parse" && mimeClassOf(lowerer, access.expression) === "MIMEType") {
|
||||
const { used, extra } = mimeArguments(lowerer, call.arguments.nodes, 1, loc);
|
||||
return withSurplus(
|
||||
lowerer,
|
||||
{ kind: "libCall", fn: "mime.typeParse", args: used, type: DYN, loc },
|
||||
extra,
|
||||
loc,
|
||||
);
|
||||
}
|
||||
const cls = mimeValueClass(lowerer, access.expression);
|
||||
if (cls === null || !INSTANCE_METHODS[cls].has(name)) return null;
|
||||
return {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExprExpecting(access.expression, DYN),
|
||||
method: name,
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `v instanceof MIMEType` / `v instanceof MIMEParams`. */
|
||||
export function lowerMimeInstanceof(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null {
|
||||
const cls = mimeClassOf(lowerer, expr.right);
|
||||
if (cls === null) return null;
|
||||
const loc = locOf(expr);
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "mime.is",
|
||||
args: [value, numLit(cls === "MIMEType" ? 0 : 1, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `params`, `params.keys()`, `params.values()` or `params.entries()` over a
|
||||
* MIMEParams: its checked-dynamic view iterates the live entry list, like
|
||||
* URLSearchParams (isSearchParamsIterable). */
|
||||
export function isMimeParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (mimeValueClass(lowerer, node) === "MIMEParams") return true;
|
||||
if (
|
||||
!ts.isCallExpression(node) ||
|
||||
node.arguments.nodes.length !== 0 ||
|
||||
!ts.isPropertyAccessExpression(node.expression)
|
||||
)
|
||||
return false;
|
||||
const name = node.expression.name.text;
|
||||
return (
|
||||
(name === "keys" || name === "values" || name === "entries") &&
|
||||
mimeValueClass(lowerer, node.expression.expression) === "MIMEParams"
|
||||
);
|
||||
}
|
||||
@@ -29,11 +29,13 @@ import { checkFsUrlPaths, isFsPathParam, lowerFallibleFsUrlCall, lowerFsPath } f
|
||||
import { lowerBufferEncodingCall } from "./text-codecs.js";
|
||||
import { lowerUtilTypeCall } from "./util-types.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { type BuiltinModuleFn } from "../surfaces.js";
|
||||
import { type ParamShape } from "../call-signatures.js";
|
||||
import { BYTES_U8, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js";
|
||||
import { BYTES_U8, DYN, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { lowerProcessLoadEnvFile } from "./environment.js";
|
||||
import { lowerFsWatchCall, lowerFsReaddirTypesCall, lowerFsStatOptionsCall } from "./filesystem.js";
|
||||
import { lowerZlibModuleCall } from "./compression.js";
|
||||
@@ -386,7 +388,65 @@ export function lowerBuiltinModuleCall(
|
||||
const lowered = lowerFsReadPathCall(lowerer, expr, loc);
|
||||
if (lowered) return lowered;
|
||||
}
|
||||
const required = fn.params.length - (fn.defaults?.length ?? 0);
|
||||
// util._extend over a typed target: the writes go through its
|
||||
// checked-dynamic view, and the call answers the target itself, as Node
|
||||
// does, when the result type is the target's.
|
||||
if (
|
||||
bi.module === "util" &&
|
||||
bi.member === "_extend" &&
|
||||
expr.arguments.nodes.length >= 1 &&
|
||||
!expr.arguments.nodes.some(ts.isSpreadElement)
|
||||
) {
|
||||
const target = lowerer.lowerExpr(expr.arguments.nodes[0]!);
|
||||
const resultType = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (
|
||||
target.type.kind !== "dyn" &&
|
||||
resultType !== null &&
|
||||
typeKey(resultType) === typeKey(target.type)
|
||||
) {
|
||||
const held = lowerer.declareHiddenLocal("%extendTarget", target.type);
|
||||
const ref = varRef(held.id, target.type, loc);
|
||||
const source = expr.arguments.nodes[1]
|
||||
? lowerer.lowerExprExpecting(expr.arguments.nodes[1], DYN)
|
||||
: dynUndefinedExpr(loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: held.id, init: target, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "util.extend",
|
||||
args: [lowerer.coerceToExpected(ref, DYN), source],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: ref,
|
||||
type: target.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Trailing checked-dynamic parameters whose value adapter defaults to
|
||||
// undefined may be omitted: the call passes undefined, as Node sees it.
|
||||
let undefinedTail = 0;
|
||||
if (!fn.defaults && fn.valueParams) {
|
||||
for (let i = fn.valueParams.length - 1; i >= 0; i--) {
|
||||
const param = fn.valueParams[i]!;
|
||||
if (
|
||||
param.mode !== "optional" ||
|
||||
param.defaultValue !== undefined ||
|
||||
fn.params[i]?.kind !== "dyn"
|
||||
)
|
||||
break;
|
||||
undefinedTail++;
|
||||
}
|
||||
}
|
||||
const required = fn.params.length - (fn.defaults?.length ?? undefinedTail);
|
||||
const hasSpread = expr.arguments.nodes.some(ts.isSpreadElement);
|
||||
if (
|
||||
bi.module === "url" &&
|
||||
@@ -466,12 +526,9 @@ export function lowerBuiltinModuleCall(
|
||||
: {
|
||||
type,
|
||||
mode: "omittable",
|
||||
callDefault: {
|
||||
kind: "strLit",
|
||||
value: fn.defaults![i - required]!,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
callDefault: fn.defaults
|
||||
? { kind: "strLit", value: fn.defaults[i - required]!, type: STRING, loc }
|
||||
: dynUndefinedExpr(loc),
|
||||
},
|
||||
);
|
||||
args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr);
|
||||
@@ -482,7 +539,11 @@ export function lowerBuiltinModuleCall(
|
||||
: lowerer.lowerExprExpecting(a, fn.params[i]),
|
||||
);
|
||||
for (let i = args.length; i < fn.params.length; i++) {
|
||||
const dflt = fn.defaults![i - required]!;
|
||||
if (!fn.defaults) {
|
||||
args.push(dynUndefinedExpr(loc));
|
||||
continue;
|
||||
}
|
||||
const dflt = fn.defaults[i - required]!;
|
||||
args.push({ kind: "strLit", value: dflt, type: STRING, loc });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
@@ -623,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;
|
||||
@@ -640,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;
|
||||
@@ -662,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);
|
||||
|
||||
@@ -1019,7 +1019,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>();
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "../collection-ingestion.js";
|
||||
import { strCharsCall } from "./array-construction.js";
|
||||
import { arrayElementRead } from "./array-iteration.js";
|
||||
import { isMimeParamsIterable } from "../builtins/mime.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js";
|
||||
@@ -1259,8 +1260,8 @@ export function lowerMapSeedNew(
|
||||
);
|
||||
}
|
||||
// new Map(sp) / new Map(sp.entries()): the checked-dynamic view's live
|
||||
// pairs seed the map in order.
|
||||
if (isSearchParamsIterable(lowerer, argNode)) {
|
||||
// pairs seed the map in order, and so do a MIMEParams's entries.
|
||||
if (isSearchParamsIterable(lowerer, argNode) || isMimeParamsIterable(lowerer, argNode)) {
|
||||
return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT);
|
||||
}
|
||||
const input = collectionInput(lowerer, argNode);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -29,6 +29,7 @@ import { lowerTernary } from "./narrowing.js";
|
||||
import { lowerCollectionSpread } from "../containers/collection-methods.js";
|
||||
import { strCharsCall } from "../containers/array-construction.js";
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { isMimeParamsIterable } from "../builtins/mime.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore } from "../array-values.js";
|
||||
@@ -521,7 +522,9 @@ export function lowerArrayLiteral(
|
||||
// `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
|
||||
// checked-dynamic view iterates the live pair list, and the drained
|
||||
// [name, value] pairs convert to the literal's tuple elements.
|
||||
const searchParamsSource = isSearchParamsIterable(lowerer, el.expression);
|
||||
const searchParamsSource =
|
||||
isSearchParamsIterable(lowerer, el.expression) ||
|
||||
isMimeParamsIterable(lowerer, el.expression);
|
||||
if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN);
|
||||
// A native checked iterable can supply scalar elements even when
|
||||
// the checker inferred a typed array from its producer. Drain once
|
||||
|
||||
@@ -1549,6 +1549,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
reference.kind === "map" ||
|
||||
reference.kind === "set" ||
|
||||
reference.kind === "union" ||
|
||||
// A promise of dyn boxes its own promise, so identity holds;
|
||||
// other promise types box a fresh adapter.
|
||||
(reference.kind === "promise" && reference.inner.kind === "dyn") ||
|
||||
DYN_HANDLE_KINDS.has(reference.kind)) &&
|
||||
lowerer.dynConvertible(reference)
|
||||
) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import { lowerReadlineInstanceof } from "../builtins/readline.js";
|
||||
import { lowerKeyObjectInstanceof } from "../builtins/key-object.js";
|
||||
import { lowerCryptoKeyInstanceof } from "../builtins/webcrypto.js";
|
||||
import { lowerNetAddressInstanceof } from "../builtins/net-address.js";
|
||||
import { lowerMimeInstanceof } from "../builtins/mime.js";
|
||||
import { typedArrayHasMember } from "../builtins/typed-array-members.js";
|
||||
import { fenceEarlyNsMemberRef, nsMemberIdentOf } from "../lower-namespaces.js";
|
||||
import { maybePromiseInstanceOf } from "../maybe-promise.js";
|
||||
@@ -96,6 +97,8 @@ const NO_CLASS_INSTANCE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"nullT",
|
||||
]);
|
||||
|
||||
const PRIMITIVE_WRAPPERS = ["Boolean", "Number", "String", "BigInt", "Symbol"] as const;
|
||||
|
||||
/** `x instanceof C` for a program-declared class C. When x's static
|
||||
* class and C are both in extends-hierarchies the test is dynamic — the
|
||||
* O(1) preorder-interval check against the vtable (`instanceOf` node).
|
||||
@@ -187,6 +190,20 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// The primitive wrapper constructors: a compiled program never creates a
|
||||
// boxed primitive (`new Boolean(...)` and friends have no lowering), and
|
||||
// a primitive is never an instance, so the test is false for every value
|
||||
// (typebox's IsBoolean/IsNumber/IsString guards). The operand still runs.
|
||||
if (PRIMITIVE_WRAPPERS.some((name) => lowerer.isStdlibGlobal(expr.right, name))) {
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: value, loc }],
|
||||
result: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.right, "FinalizationRegistry")) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return { kind: "libCall", fn: "finalizationRegistry.is", args: [value], type: BOOL, loc };
|
||||
@@ -230,6 +247,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (cryptoKey) return cryptoKey;
|
||||
const netAddress = lowerNetAddressInstanceof(lowerer, expr);
|
||||
if (netAddress) return netAddress;
|
||||
const mime = lowerMimeInstanceof(lowerer, expr);
|
||||
if (mime) return mime;
|
||||
const readlineInterface = lowerReadlineInstanceof(lowerer, expr);
|
||||
if (readlineInterface) return readlineInterface;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3333,7 +3333,7 @@ function formatSValue(
|
||||
if (t.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "insp.dynS",
|
||||
fn: "insp.dynFormatS",
|
||||
args: [value, numLit(depth, loc)],
|
||||
type: STRING,
|
||||
loc,
|
||||
|
||||
@@ -1184,19 +1184,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.
|
||||
|
||||
@@ -1630,16 +1630,22 @@ export function lowerStaticAbortControllerCall(
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
const access = call.expression;
|
||||
// An optional chain (`controller?.abort()`) lowers the call again once it
|
||||
// has narrowed the receiver (Lowerer.chainHandled).
|
||||
if (
|
||||
call.questionDotToken ||
|
||||
(call.questionDotToken && !lowerer.chainHandled.has(call)) ||
|
||||
call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) ||
|
||||
(!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) ||
|
||||
access.questionDotToken ||
|
||||
(access.questionDotToken && !lowerer.chainHandled.has(access)) ||
|
||||
staticResponseMemberName(lowerer, access) !== "abort"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
|
||||
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(
|
||||
access.questionDotToken
|
||||
? lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression))
|
||||
: lowerer.typeOf(access.expression),
|
||||
);
|
||||
const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol();
|
||||
if (
|
||||
symbol === undefined ||
|
||||
|
||||
@@ -68,6 +68,7 @@ import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js";
|
||||
import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js";
|
||||
import { valueStreamOf } from "./builtins/hash-stream.js";
|
||||
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
|
||||
import { lowerMimeCall, lowerMimeNew } from "./builtins/mime.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { iteratorSlotAdapter } from "./iterator-adapters.js";
|
||||
import { iterableSlotValue } from "./iterable-values.js";
|
||||
@@ -3756,7 +3757,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,
|
||||
@@ -3837,8 +3838,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) =>
|
||||
@@ -3910,7 +3911,8 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, is an owned runtime
|
||||
* handle (a Worker or MessagePort, a fetch Headers/Request/Response, or
|
||||
* handle (a Worker or MessagePort, a fetch Headers/Request/Response or
|
||||
* AbortController/AbortSignal, or
|
||||
* a node:crypto ECDH, DiffieHellman or DiffieHellmanGroup), or is a
|
||||
* WeakMap/WeakSet (native weak collections live in checked-dynamic
|
||||
* storage, as locals of those types do); `any` fields stay refused. */
|
||||
@@ -8102,16 +8104,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));
|
||||
@@ -8301,7 +8311,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
|
||||
@@ -8436,7 +8447,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) {
|
||||
@@ -8554,7 +8566,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
|
||||
@@ -11954,12 +11968,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)
|
||||
@@ -11983,8 +12000,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 {
|
||||
@@ -13326,6 +13344,7 @@ export class Lowerer {
|
||||
lowerEcdhCall(this, expr) ??
|
||||
lowerDiffieHellmanCall(this, expr) ??
|
||||
lowerNetAddressCall(this, expr) ??
|
||||
lowerMimeCall(this, expr) ??
|
||||
lowerFetchElementMethodCall(this, expr) ??
|
||||
lowerDynamicImportCall(this, expr) ??
|
||||
lowerCall(this, expr)
|
||||
@@ -13550,6 +13569,7 @@ export class Lowerer {
|
||||
lowerResponseNew(this, expr) ??
|
||||
lowerBlobNew(this, expr) ??
|
||||
lowerNetAddressNew(this, expr) ??
|
||||
lowerMimeNew(this, expr) ??
|
||||
lowerStaticReadableStreamNew(this, expr) ??
|
||||
lowerStaticTransformStreamNew(this, expr) ??
|
||||
lowerConsoleNew(this, expr);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1741,6 +1741,67 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
],
|
||||
},
|
||||
parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN },
|
||||
aborted: {
|
||||
fn: "util.aborted",
|
||||
params: [DYN, DYN],
|
||||
result: DYN,
|
||||
valueParams: [
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
diff: {
|
||||
fn: "util.diff",
|
||||
params: [DYN, DYN],
|
||||
result: DYN,
|
||||
valueParams: [
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
debounce: {
|
||||
fn: "util.debounce",
|
||||
params: [DYN, DYN, DYN],
|
||||
result: DYN,
|
||||
valueParams: [
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
throttle: {
|
||||
fn: "util.throttle",
|
||||
params: [DYN, DYN, DYN, DYN],
|
||||
result: DYN,
|
||||
valueParams: [
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
convertProcessSignalToExitCode: {
|
||||
fn: "util.signalExitCode",
|
||||
params: [DYN],
|
||||
result: F64,
|
||||
valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }],
|
||||
},
|
||||
// The deprecated helpers report DEP0044 and DEP0060 on their first call.
|
||||
isArray: {
|
||||
fn: "util.isArrayDeprecated",
|
||||
params: [DYN],
|
||||
result: BOOL,
|
||||
valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }],
|
||||
},
|
||||
_extend: {
|
||||
fn: "util.extend",
|
||||
params: [DYN, DYN],
|
||||
result: DYN,
|
||||
valueParams: [
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN },
|
||||
getSystemErrorName: {
|
||||
fn: "util.getSystemErrorName",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -140,6 +140,7 @@ import {
|
||||
} from "./type-only-imports.js";
|
||||
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
import { planDeclProjection, type DeclProjection } from "./decl-projection.js";
|
||||
import { reportShapes } from "./shape-inference/index.js";
|
||||
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
|
||||
import {
|
||||
containsJsx,
|
||||
@@ -1260,6 +1261,10 @@ export function loadProgram(
|
||||
npmStaticPackages.length > 0,
|
||||
projector,
|
||||
);
|
||||
// Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES).
|
||||
const shapes = process.env["SCRIPTC_DEBUG_SHAPES"];
|
||||
if (shapes !== undefined && shapes !== "" && shapes !== "0" && npmStaticPackages.length > 0)
|
||||
reportShapes(load.program, shapes);
|
||||
return {
|
||||
...load,
|
||||
services,
|
||||
|
||||
@@ -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();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1:
|
||||
* the abstract domain of the allocation-site points-to analysis.
|
||||
*
|
||||
* An abstract value is a set of atoms. An atom names a set of runtime
|
||||
* values: a primitive kind, an allocation site (object or array literal, or
|
||||
* a value a builtin creates at one call site), the instances of one class,
|
||||
* one class value, one function (closure site), a builtin object, or ⊤ —
|
||||
* every value the analysis does not track. Value nodes hold atom sets and
|
||||
* flow into each other along inclusion edges; the solver (analyze.ts)
|
||||
* attaches load, store and call constraints that fire as atoms arrive. */
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
|
||||
export type AtomKind =
|
||||
| "prim"
|
||||
| "top"
|
||||
| "object"
|
||||
| "array"
|
||||
| "instance"
|
||||
| "class"
|
||||
| "function"
|
||||
| "builtin"
|
||||
| "promise"
|
||||
| "collection"
|
||||
| "dictionary";
|
||||
|
||||
export interface Atom {
|
||||
readonly id: number;
|
||||
readonly kind: AtomKind;
|
||||
/** Short display name, e.g. `string`, `ZodString`, `{status,value}@types.js:804`. */
|
||||
readonly label: string;
|
||||
/** The syntax that creates the atom: the literal, class or function
|
||||
* declaration, or the builtin call. Undefined for prims, ⊤ and builtins. */
|
||||
readonly node?: ts.Node | undefined;
|
||||
/** builtin atoms: the dotted path (`Object.keys`, `array.push`). */
|
||||
readonly path?: string | undefined;
|
||||
/** method atoms of builtin receivers: the receiver atom. */
|
||||
readonly receiver?: Atom | undefined;
|
||||
/** function atoms: the analysis context of the function's clone (a
|
||||
* method's receiver class), "" for the context-insensitive default. */
|
||||
readonly ctx?: string | undefined;
|
||||
}
|
||||
|
||||
export class AtomTable {
|
||||
readonly atoms: Atom[] = [];
|
||||
private readonly byKey = new Map<string, Atom>();
|
||||
|
||||
intern(key: string, make: (id: number) => Omit<Atom, "id">): Atom {
|
||||
let atom = this.byKey.get(key);
|
||||
if (atom === undefined) {
|
||||
const id = this.atoms.length;
|
||||
atom = { id, ...make(id) };
|
||||
this.atoms.push(atom);
|
||||
this.byKey.set(key, atom);
|
||||
}
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
|
||||
/** Object.prototype members: a read of one of these names on a plain
|
||||
* object resolves on the prototype, never to an absent own key. */
|
||||
export const OBJECT_PROTOTYPE_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"constructor",
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable",
|
||||
"toLocaleString",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"__defineGetter__",
|
||||
"__defineSetter__",
|
||||
"__lookupGetter__",
|
||||
"__lookupSetter__",
|
||||
"__proto__",
|
||||
]);
|
||||
|
||||
/** Global names the analysis models as builtin objects. Any other global is ⊤. */
|
||||
export const MODELED_GLOBALS: ReadonlySet<string> = new Set([
|
||||
"Object",
|
||||
"Array",
|
||||
"JSON",
|
||||
"Math",
|
||||
"Number",
|
||||
"String",
|
||||
"Boolean",
|
||||
"Symbol",
|
||||
"Promise",
|
||||
"Map",
|
||||
"Set",
|
||||
"WeakMap",
|
||||
"WeakSet",
|
||||
"Error",
|
||||
"TypeError",
|
||||
"RangeError",
|
||||
"SyntaxError",
|
||||
"Date",
|
||||
"RegExp",
|
||||
"console",
|
||||
"Reflect",
|
||||
"parseInt",
|
||||
"parseFloat",
|
||||
"isNaN",
|
||||
"isFinite",
|
||||
"encodeURIComponent",
|
||||
"decodeURIComponent",
|
||||
"encodeURI",
|
||||
"decodeURI",
|
||||
"BigInt",
|
||||
"Proxy",
|
||||
]);
|
||||
|
||||
/** Results of string (and number) methods, by name. Anything else is ⊤. */
|
||||
type MethodResult = "string" | "number" | "boolean" | "array";
|
||||
export const STRING_METHOD_RESULTS: ReadonlyMap<string, MethodResult> = new Map<
|
||||
string,
|
||||
MethodResult
|
||||
>([
|
||||
["at", "string"],
|
||||
["charAt", "string"],
|
||||
["charCodeAt", "number"],
|
||||
["codePointAt", "number"],
|
||||
["concat", "string"],
|
||||
["endsWith", "boolean"],
|
||||
["includes", "boolean"],
|
||||
["indexOf", "number"],
|
||||
["lastIndexOf", "number"],
|
||||
["localeCompare", "number"],
|
||||
["match", "array"],
|
||||
["matchAll", "array"],
|
||||
["normalize", "string"],
|
||||
["padEnd", "string"],
|
||||
["padStart", "string"],
|
||||
["repeat", "string"],
|
||||
["replace", "string"],
|
||||
["replaceAll", "string"],
|
||||
["search", "number"],
|
||||
["slice", "string"],
|
||||
["split", "array"],
|
||||
["startsWith", "boolean"],
|
||||
["substring", "string"],
|
||||
["substr", "string"],
|
||||
["toLowerCase", "string"],
|
||||
["toUpperCase", "string"],
|
||||
["toLocaleLowerCase", "string"],
|
||||
["toLocaleUpperCase", "string"],
|
||||
["toString", "string"],
|
||||
["trim", "string"],
|
||||
["trimEnd", "string"],
|
||||
["trimStart", "string"],
|
||||
["valueOf", "string"],
|
||||
["test", "boolean"],
|
||||
["toFixed", "string"],
|
||||
["toPrecision", "string"],
|
||||
["toExponential", "string"],
|
||||
]);
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Shape inference for untyped npm JavaScript
|
||||
* (results/design-shape-inference/DESIGN.md). Stage S1: report only. */
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import { analyzeShapes } from "./analyze.js";
|
||||
import { buildShapeReport } from "./report.js";
|
||||
|
||||
export { analyzeShapes } from "./analyze.js";
|
||||
export { buildShapeReport } from "./report.js";
|
||||
|
||||
/** SCRIPTC_DEBUG_SHAPES: `1` reports package code in hot functions,
|
||||
* `verbose` every package site, `all` user files too, any other value only
|
||||
* files whose path contains it. Writes to stderr; changes nothing else. */
|
||||
export function reportShapes(program: ts.Program, filter: string): void {
|
||||
const start = performance.now();
|
||||
const analysis = analyzeShapes(program, program.getTypeChecker());
|
||||
const report = buildShapeReport(analysis, filter);
|
||||
const ms = (performance.now() - start).toFixed(0);
|
||||
process.stderr.write(
|
||||
`[shapes] ${ms} ms\n${report.lines.map((l) => `[shapes] ${l}`).join("\n")}\n`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/* Shape inference stage S1: the plan the analysis would hand the lowering,
|
||||
* as a report (DESIGN.md §4.5, §5.5). Nothing here changes compilation.
|
||||
*
|
||||
* Per allocation site of package code, the verdict:
|
||||
* P local — a native record would be exact: no reflection, no added
|
||||
* keys, no computed writes, and its objects never escape;
|
||||
* P escaping — exact as a record behind identity-keeping capsules: it
|
||||
* escapes, but the census leaves every later operation
|
||||
* answerable (fields untyped code may write become dyn);
|
||||
* G <reason> — keeps today's dynamic object, with the rule that decided.
|
||||
* Plus class field types, the census, and the hot functions' value types. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { OBJECT_PROTOTYPE_MEMBERS, type Atom } from "./domain.js";
|
||||
import { locLabel, type FunctionInfo, type LiteralSite, type ShapeAnalysis } from "./analyze.js";
|
||||
|
||||
export interface SiteVerdict {
|
||||
readonly site: LiteralSite;
|
||||
readonly label: string;
|
||||
readonly verdict: "P-local" | "P-escaping" | "G";
|
||||
readonly reasons: string[];
|
||||
/** P-escaping: fields untyped code may write (they become dyn slots). */
|
||||
readonly writableByTop: string[];
|
||||
readonly freezable: boolean;
|
||||
readonly brand: boolean;
|
||||
}
|
||||
|
||||
export interface ShapeReport {
|
||||
readonly sites: SiteVerdict[];
|
||||
readonly hot: Set<FunctionInfo>;
|
||||
readonly lines: string[];
|
||||
}
|
||||
|
||||
const MAX_ATOMS_SHOWN = 6;
|
||||
|
||||
function fieldKeys(fields: ReadonlyMap<string, number> | undefined): string[] {
|
||||
return fields === undefined ? [] : [...fields.keys()];
|
||||
}
|
||||
|
||||
function fieldEntries(fields: ReadonlyMap<string, number> | undefined): [string, number][] {
|
||||
return fields === undefined ? [] : [...fields.entries()];
|
||||
}
|
||||
|
||||
export function describeValue(analysis: ShapeAnalysis, node: number): string {
|
||||
const atoms = [...analysis.nodes[node]!.atoms];
|
||||
if (atoms.length === 0) return "∅";
|
||||
const labels = [...new Set(atoms.map((a) => a.label))];
|
||||
labels.sort((a, b) => (a === "⊤" ? 1 : b === "⊤" ? -1 : a.localeCompare(b)));
|
||||
const shown = labels.slice(0, MAX_ATOMS_SHOWN).join(" | ");
|
||||
if (labels.length <= MAX_ATOMS_SHOWN) return shown;
|
||||
return `${shown} | +${labels.length - MAX_ATOMS_SHOWN}${labels.includes("⊤") ? " | ⊤" : ""}`;
|
||||
}
|
||||
|
||||
function atomsOf(analysis: ShapeAnalysis, node: number): Set<Atom> {
|
||||
return analysis.nodes[node]!.atoms;
|
||||
}
|
||||
|
||||
/** Functions reached from call sites in loops (or loop callbacks), and
|
||||
* everything they call: the static hotness heuristic (DESIGN.md §8.1). */
|
||||
export function hotFunctions(analysis: ShapeAnalysis): Set<FunctionInfo> {
|
||||
const hot = new Set<FunctionInfo>();
|
||||
const callees = new Map<FunctionInfo | null, FunctionInfo[]>();
|
||||
const work: FunctionInfo[] = [];
|
||||
for (const c of analysis.calls) {
|
||||
let l = callees.get(c.caller);
|
||||
if (l === undefined) callees.set(c.caller, (l = []));
|
||||
l.push(c.callee);
|
||||
if (c.inLoop && !hot.has(c.callee)) {
|
||||
hot.add(c.callee);
|
||||
work.push(c.callee);
|
||||
}
|
||||
}
|
||||
while (work.length > 0) {
|
||||
const f = work.pop()!;
|
||||
for (const g of callees.get(f) ?? []) {
|
||||
if (!hot.has(g)) {
|
||||
hot.add(g);
|
||||
work.push(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hot;
|
||||
}
|
||||
|
||||
export function buildShapeReport(analysis: ShapeAnalysis, filter: string): ShapeReport {
|
||||
const lines: string[] = [];
|
||||
const all = filter === "all" || filter === "verbose";
|
||||
const verbose = filter === "verbose";
|
||||
const fileFilter = filter !== "1" && !all ? filter : null;
|
||||
const wanted = (n: ts.Node, inPackage: boolean): boolean => {
|
||||
if (fileFilter !== null) return n.getSourceFile().fileName.includes(fileFilter);
|
||||
return all || inPackage;
|
||||
};
|
||||
|
||||
// Census facts: operations whose receiver may be ⊤ reach every escaped atom.
|
||||
|
||||
// Index ops by the site atoms they can reach directly.
|
||||
const byAtom = new Map<Atom, typeof analysis.ops>();
|
||||
const topOps: typeof analysis.ops = [];
|
||||
for (const o of analysis.ops) {
|
||||
const atoms = atomsOf(analysis, o.recv);
|
||||
for (const a of atoms) {
|
||||
if (a.kind !== "object" && a.kind !== "array") continue;
|
||||
let l = byAtom.get(a);
|
||||
if (l === undefined) byAtom.set(a, (l = []));
|
||||
l.push(o);
|
||||
}
|
||||
if (atoms.has(analysis.top)) topOps.push(o);
|
||||
}
|
||||
|
||||
// Key-order groups: sites with the same key set and different orders need brands.
|
||||
const orders = new Map<string, Set<string>>();
|
||||
for (const s of analysis.sites) {
|
||||
if (s.kind !== "object" || s.keys === null) continue;
|
||||
const set = [...s.keys].sort().join(",");
|
||||
let o = orders.get(set);
|
||||
if (o === undefined) orders.set(set, (o = new Set()));
|
||||
o.add(s.keys.join(","));
|
||||
}
|
||||
|
||||
const verdicts: SiteVerdict[] = [];
|
||||
for (const s of analysis.sites) {
|
||||
if (!s.inPackage && !all && fileFilter === null) continue;
|
||||
const reasons: string[] = [];
|
||||
const escapedWhy = analysis.escaped.get(s.atom);
|
||||
const escapes = escapedWhy !== undefined;
|
||||
if (s.hasAccessor) reasons.push("accessor in literal");
|
||||
if (s.hasProto) reasons.push(s.kind === "array" ? "hole in literal" : "__proto__ in literal");
|
||||
if (s.hasComputedKey) reasons.push("computed key in literal");
|
||||
for (const sp of s.spreads)
|
||||
if (atomsOf(analysis, sp).has(analysis.top)) reasons.push("spread of ⊤");
|
||||
const direct = byAtom.get(s.atom) ?? [];
|
||||
const ops = escapes ? [...direct, ...topOps] : direct;
|
||||
const keys = new Set(s.keys ?? []);
|
||||
const fieldsOf = analysis.fields.get(s.atom);
|
||||
const knownKeys =
|
||||
s.keys !== null ? keys : new Set([...fieldKeys(fieldsOf)].filter((k) => k !== "*"));
|
||||
let freezable = false;
|
||||
const seen = new Set<string>();
|
||||
const note = (r: string): void => {
|
||||
if (!seen.has(r)) {
|
||||
seen.add(r);
|
||||
reasons.push(r);
|
||||
}
|
||||
};
|
||||
for (const o of ops) {
|
||||
const viaTop = !atomsOf(analysis, o.recv).has(s.atom);
|
||||
const where = `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`;
|
||||
switch (o.kind) {
|
||||
case "delete":
|
||||
case "defineProperty":
|
||||
case "setPrototypeOf":
|
||||
case "seal":
|
||||
case "proxy":
|
||||
note(`${o.kind} at ${where}`);
|
||||
break;
|
||||
case "assign":
|
||||
note(`Object.assign target at ${where}`);
|
||||
break;
|
||||
case "freeze":
|
||||
freezable = true;
|
||||
break;
|
||||
case "computedWrite":
|
||||
if (!viaTop)
|
||||
note(s.kind === "array" ? `index write at ${where}` : `computed write at ${where}`);
|
||||
break;
|
||||
case "lengthWrite":
|
||||
if (s.kind === "array") note(`length write at ${where}`);
|
||||
break;
|
||||
case "write":
|
||||
if (s.kind === "object" && !viaTop && o.name !== null && !knownKeys.has(o.name))
|
||||
note(`adds key '${o.name}' at ${where}`);
|
||||
break;
|
||||
case "read":
|
||||
if (
|
||||
s.kind === "object" &&
|
||||
!underPromiseGuard(o.node) &&
|
||||
escapes &&
|
||||
o.name !== null &&
|
||||
!viaTop &&
|
||||
!knownKeys.has(o.name) &&
|
||||
!OBJECT_PROTOTYPE_MEMBERS.has(o.name)
|
||||
)
|
||||
note(`reads absent key '${o.name}' at ${where} (escapes)`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
let brand = false;
|
||||
if (s.kind === "object" && s.keys !== null) {
|
||||
const o = orders.get([...s.keys].sort().join(","));
|
||||
if (o !== undefined && o.size > 1) brand = true;
|
||||
}
|
||||
const writableByTop: string[] = [];
|
||||
if (escapes) {
|
||||
if (analysis.topComputedWrites.length > 0) writableByTop.push("*");
|
||||
else for (const k of knownKeys) if (analysis.topWriteNames.has(k)) writableByTop.push(k);
|
||||
}
|
||||
const verdict: SiteVerdict["verdict"] =
|
||||
reasons.length > 0 ? "G" : escapes ? "P-escaping" : "P-local";
|
||||
const label = s.atom.label;
|
||||
if (escapes) reasons.push(`escapes: ${escapedWhy}`);
|
||||
verdicts.push({ site: s, label, verdict, reasons, writableByTop, freezable, brand });
|
||||
}
|
||||
|
||||
const hot = hotFunctions(analysis);
|
||||
const hotNodes = new Set<ts.Node>([...hot].map((f) => f.node));
|
||||
|
||||
// ---- print ----
|
||||
const pkgFns = [...analysis.functions.values()].filter((f) => f.inPackage);
|
||||
lines.push(
|
||||
`analyzed ${analysis.files.length} files (${analysis.packageFiles} package JS), ${analysis.functions.size} functions (${pkgFns.length} in packages), ${analysis.atoms.atoms.length} atoms, ${analysis.nodes.length} value nodes, ${analysis.steps} steps${analysis.budgetExceeded ? " — BUDGET EXCEEDED, results incomplete" : ""}`,
|
||||
);
|
||||
const topNames = [...analysis.topWriteNames.entries()].map(
|
||||
([n, node]) => `${n} (${locLabel(node)})`,
|
||||
);
|
||||
lines.push(
|
||||
`census: writes on ⊤ receivers: ${topNames.length === 0 ? "none" : topNames.slice(0, 20).join(", ")}${topNames.length > 20 ? ` +${topNames.length - 20}` : ""}`,
|
||||
);
|
||||
lines.push(
|
||||
`census: computed writes on ⊤ receivers: ${analysis.topComputedWrites.length === 0 ? "none" : analysis.topComputedWrites.slice(0, 8).map(locLabel).join(", ")}`,
|
||||
);
|
||||
const topReflect = topOps.filter(
|
||||
(o) =>
|
||||
o.kind !== "read" &&
|
||||
o.kind !== "computedRead" &&
|
||||
o.kind !== "write" &&
|
||||
o.kind !== "computedWrite",
|
||||
);
|
||||
lines.push(
|
||||
`census: reflection on ⊤ receivers: ${topReflect.length === 0 ? "none" : topReflect.map((o) => `${o.kind} (${locLabel(o.node)})`).join(", ")}`,
|
||||
);
|
||||
const escapedPkg = [...analysis.escaped.keys()].filter(
|
||||
(a) =>
|
||||
a.node !== undefined &&
|
||||
wanted(a.node, a.node.getSourceFile().fileName.includes("/node_modules/")),
|
||||
);
|
||||
lines.push(
|
||||
`escaped atoms: ${analysis.escaped.size} (${escapedPkg.length} shown below where relevant)`,
|
||||
);
|
||||
const firstEscapes = [...analysis.escaped.entries()]
|
||||
.filter(([, why]) => !why.startsWith("via "))
|
||||
.slice(0, 12);
|
||||
for (const [atom, why] of firstEscapes) lines.push(` escape root: ${atom.label}: ${why}`);
|
||||
|
||||
const counts = { "P-local": 0, "P-escaping": 0, G: 0 };
|
||||
const hotCounts = { "P-local": 0, "P-escaping": 0, G: 0 };
|
||||
const inHot = (n: ts.Node): boolean => {
|
||||
let p: ts.Node | undefined = n.parent;
|
||||
while (p !== undefined) {
|
||||
if (hotNodes.has(p)) return true;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const v of verdicts) {
|
||||
counts[v.verdict]++;
|
||||
if (inHot(v.site.node)) hotCounts[v.verdict]++;
|
||||
}
|
||||
lines.push(
|
||||
`sites: ${verdicts.length} (P local ${counts["P-local"]}, P escaping ${counts["P-escaping"]}, G ${counts.G}); in hot functions: P local ${hotCounts["P-local"]}, P escaping ${hotCounts["P-escaping"]}, G ${hotCounts.G}`,
|
||||
);
|
||||
for (const v of verdicts) {
|
||||
if (!wanted(v.site.node, v.site.inPackage)) continue;
|
||||
if (!verbose && !inHot(v.site.node)) continue;
|
||||
const extra: string[] = [];
|
||||
if (v.brand) extra.push("needs brand (key order)");
|
||||
if (v.freezable) extra.push("freezable");
|
||||
if (v.writableByTop.length > 0) extra.push(`⊤-writable: ${v.writableByTop.join(",")}`);
|
||||
const fields = analysis.fields.get(v.site.atom);
|
||||
const ftypes =
|
||||
v.site.kind === "array"
|
||||
? `elem: ${analysis.elements.has(v.site.atom) ? describeValue(analysis, analysis.elements.get(v.site.atom)!) : "∅"}`
|
||||
: fieldEntries(fields)
|
||||
.filter(([k]) => k !== "*")
|
||||
.map(([k, id]) => `${k}: ${describeValue(analysis, id)}`)
|
||||
.join("; ");
|
||||
lines.push(
|
||||
`site ${v.label}${inHot(v.site.node) ? " [hot]" : ""}: ${v.verdict}${v.reasons.length > 0 ? ` — ${v.reasons.join("; ")}` : ""}${extra.length > 0 ? ` [${extra.join("; ")}]` : ""}`,
|
||||
);
|
||||
if (ftypes.length > 0) lines.push(` ${ftypes}`);
|
||||
}
|
||||
|
||||
// Class fields.
|
||||
for (const atom of analysis.atoms.atoms) {
|
||||
if (atom.kind !== "instance" || atom.node === undefined) continue;
|
||||
const inPackage = atom.node.getSourceFile().fileName.includes("/node_modules/");
|
||||
if (!wanted(atom.node, inPackage)) continue;
|
||||
const fm = analysis.fields.get(atom);
|
||||
if (fm === undefined || fm.size === 0) continue;
|
||||
const esc = analysis.escaped.get(atom);
|
||||
const parts = [...fm.entries()].map(([k, id]) => {
|
||||
const w =
|
||||
esc !== undefined &&
|
||||
!k.startsWith("#") &&
|
||||
(analysis.topComputedWrites.length > 0 || analysis.topWriteNames.has(k))
|
||||
? " (⊤-writable)"
|
||||
: "";
|
||||
return `${k}: ${describeValue(analysis, id)}${w}`;
|
||||
});
|
||||
lines.push(
|
||||
`class ${atom.label}${esc !== undefined ? ` (escapes: ${esc})` : ""}: ${parts.join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Hot functions.
|
||||
const hotPkg = [...hot].filter((info) => wanted(info.node, info.inPackage));
|
||||
lines.push(`hot functions: ${hotPkg.length} (clones per receiver class counted separately)`);
|
||||
for (const info of hotPkg) {
|
||||
const n = info.node;
|
||||
const params = info.params.map((p, i) => `p${i}=${describeValue(analysis, p)}`).join(", ");
|
||||
const flags = [
|
||||
info.usesArguments ? "uses arguments" : "",
|
||||
analysis.escaped.size > 0 && [...analysis.escaped.keys()].some((a) => a.node === n)
|
||||
? "escapes"
|
||||
: "",
|
||||
].filter((x) => x !== "");
|
||||
lines.push(
|
||||
`fn ${info.label}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}: this=${describeValue(analysis, info.thisNode)}${params.length > 0 ? `, ${params}` : ""}, ret=${describeValue(analysis, info.ret)}`,
|
||||
);
|
||||
}
|
||||
const why = process.env["SCRIPTC_DEBUG_SHAPES_WHY"];
|
||||
if (why !== undefined) {
|
||||
for (const info of analysis.functions.values()) {
|
||||
if (!info.label.startsWith(why)) continue;
|
||||
for (const [what, node] of [
|
||||
["ret", info.ret],
|
||||
...info.params.map((p, i) => [`p${i}`, p] as const),
|
||||
] as const) {
|
||||
if (!analysis.nodes[node]!.atoms.has(analysis.top)) continue;
|
||||
lines.push(`why ⊤ in ${info.label} ${what}:`);
|
||||
let cur: number | string | undefined = node;
|
||||
for (let i = 0; i < 40 && typeof cur === "number"; i++) {
|
||||
lines.push(` ${analysis.nodeLabel.get(cur) ?? `node ${cur}`}`);
|
||||
cur = analysis.topCause.get(cur);
|
||||
}
|
||||
if (typeof cur === "string") lines.push(` seed: ${cur}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { sites: verdicts, hot, lines };
|
||||
}
|
||||
|
||||
/** A read inside the true arm of a promise test (`isAsync(x)`,
|
||||
* `x instanceof Promise`): flow-sensitive narrowing removes the record
|
||||
* arms there. Stage S1's locals are flow-insensitive, so the report
|
||||
* recognizes the guard syntactically. */
|
||||
function underPromiseGuard(node: ts.Node): boolean {
|
||||
let child: ts.Node = node;
|
||||
let p = node.parent;
|
||||
while (p !== undefined && !ts.isFunctionLike(p)) {
|
||||
let cond: ts.Expression | undefined;
|
||||
if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression;
|
||||
if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition;
|
||||
if (cond !== undefined && isPromiseTest(cond)) return true;
|
||||
child = p;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPromiseTest(e: ts.Expression): boolean {
|
||||
if (ts.isParenthesizedExpression(e)) return isPromiseTest(e.expression);
|
||||
if (ts.isBinaryExpression(e)) {
|
||||
if (e.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword)
|
||||
return ts.isIdentifier(e.right) && e.right.text === "Promise";
|
||||
if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return isPromiseTest(e.left) || isPromiseTest(e.right);
|
||||
return false;
|
||||
}
|
||||
if (ts.isCallExpression(e) && ts.isIdentifier(e.expression))
|
||||
return /^is(Async|Promise|Thenable)$/.test(e.expression.text);
|
||||
return false;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { tokenEquivalentSource } from "./token-equivalence.js";
|
||||
import { tokenEquivalentSource, tokenPositionEquivalentSource } from "./token-equivalence.js";
|
||||
|
||||
const equivalent = (previous: string, current: string, path = "/src/a.ts") =>
|
||||
tokenEquivalentSource(path, previous, current);
|
||||
const positionEquivalent = (previous: string, current: string, path = "/src/a.ts") =>
|
||||
tokenPositionEquivalentSource(path, previous, current);
|
||||
|
||||
test("whitespace and plain comments are trivia", () => {
|
||||
expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true);
|
||||
@@ -105,3 +107,50 @@ test("text the scanner cannot classify is never equivalent", () => {
|
||||
expect(equivalent("const a = <b/>; // a\n", "const a = <b/>; // b\n", "/src/a.tsx")).toBe(false);
|
||||
expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false);
|
||||
});
|
||||
|
||||
test("a / after a non-null assertion divides", () => {
|
||||
expect(
|
||||
equivalent("const q = data[i]! / 7; // ratio\n", "const q = data[i]! / 7; // the ratio\n"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
equivalent("const q = value! / 7; // ratio\n", "const q = value! / 7; // the ratio\n"),
|
||||
).toBe(true);
|
||||
expect(equivalent("const q = this! / 7; // ratio\n", "const q = this! / 7; // the ratio\n")).toBe(
|
||||
true,
|
||||
);
|
||||
// A prefix `!` negates a regular expression, whose `//` is code.
|
||||
expect(
|
||||
equivalent("const t = !/[//]/.test(s); c = 1;\n", "const t = !/[//]/.test(s); c = 2;\n"),
|
||||
).toBe(false);
|
||||
expect(equivalent("return !/[//]/.test(s); // one\n", "return !/[//]/.test(s); // two\n")).toBe(
|
||||
true,
|
||||
);
|
||||
// After a line break, `!` starts a new expression.
|
||||
expect(equivalent("a\n!/[//]/.test(s); c = 1;\n", "a\n!/[//]/.test(s); c = 2;\n")).toBe(false);
|
||||
// `f()!` and `if (a) !` read the same up to the `/`.
|
||||
expect(equivalent("const q = f()! / 2; // one\n", "const q = f()! / 2; // two\n")).toBe(false);
|
||||
});
|
||||
|
||||
test("position equivalence also keeps every token's line and column", () => {
|
||||
const source = "const a = 1; // one\nconst b = a; /* two */ f(b);\n";
|
||||
// A comment's text within its line, after the line's last token.
|
||||
expect(
|
||||
positionEquivalent(source, "const a = 1; // the first\nconst b = a; /* two */ f(b);\n"),
|
||||
).toBe(true);
|
||||
// Comments and whitespace after the last token, and trailing whitespace.
|
||||
expect(positionEquivalent(source, source + "// appended\n\n")).toBe(true);
|
||||
expect(
|
||||
positionEquivalent(source, "const a = 1; // one\nconst b = a; /* two */ f(b); \n"),
|
||||
).toBe(true);
|
||||
expect(positionEquivalent(source, source.replaceAll("\n", "\r\n"))).toBe(true);
|
||||
// A longer comment before a token moves its column; a new line moves the
|
||||
// lines below it. Both stay token-equivalent.
|
||||
const column = "const a = 1; // one\nconst b = a; /* the second */ f(b);\n";
|
||||
const line = "// header\nconst a = 1; // one\nconst b = a; /* two */ f(b);\n";
|
||||
expect(positionEquivalent(source, column)).toBe(false);
|
||||
expect(positionEquivalent(source, line)).toBe(false);
|
||||
expect(equivalent(source, column)).toBe(true);
|
||||
expect(equivalent(source, line)).toBe(true);
|
||||
// A token edit is never equivalent.
|
||||
expect(positionEquivalent(source, source.replace("1", "2"))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
* before it. After `)` it divides unless the parenthesis closes an `if`,
|
||||
* `while`, `for`, `for await`, or `with` header. After `}`, `++` or
|
||||
* `--` the scanner cannot tell which, so it gives up, as it does for
|
||||
* escaped identifiers, which can spell keywords.
|
||||
* escaped identifiers, which can spell keywords. After `!` in
|
||||
* TypeScript it divides when the `!` follows an operand on the same line
|
||||
* (a non-null assertion, `x! / 2`) and gives up when that operand is a
|
||||
* `)`, `}`, `++` or `--`.
|
||||
* - A `/` with a line break (or a comment that spans lines) since the
|
||||
* previous token gives up: whether automatic semicolon insertion ends the
|
||||
* statement before it decides how it reads.
|
||||
@@ -35,6 +38,8 @@ interface Token {
|
||||
text: string;
|
||||
comment: boolean;
|
||||
lineBreakBefore: boolean;
|
||||
/** The offset of the token's first code unit. */
|
||||
start: number;
|
||||
}
|
||||
|
||||
/** Keywords after which `/` starts a regular expression. */
|
||||
@@ -135,14 +140,20 @@ function scan(path: string, source: string): Token[] | null {
|
||||
// A line break, or a comment spanning lines, since the last token other
|
||||
// than a comment.
|
||||
let breakSinceCode = false;
|
||||
// The previous token and the one before it, and whether a `)` closed a
|
||||
// statement header.
|
||||
const last: { token: string | null; before: string | null } = { token: null, before: null };
|
||||
// The previous token and the one before it, whether a line break came
|
||||
// before the previous token, and whether a `)` closed a statement header.
|
||||
const last: { token: string | null; before: string | null; broke: boolean } = {
|
||||
token: null,
|
||||
before: null,
|
||||
broke: false,
|
||||
};
|
||||
let closedHeader = false;
|
||||
// Every token ends at `index` when it is pushed.
|
||||
const push = (text: string, comment = false): void => {
|
||||
tokens.push({ text, comment, lineBreakBefore });
|
||||
tokens.push({ text, comment, lineBreakBefore, start: index - text.length });
|
||||
lineBreakBefore = false;
|
||||
if (!comment) {
|
||||
last.broke = breakSinceCode;
|
||||
breakSinceCode = false;
|
||||
last.before = last.token;
|
||||
last.token = text;
|
||||
@@ -291,7 +302,19 @@ function scan(path: string, source: string): Token[] | null {
|
||||
else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`"))
|
||||
regex = false;
|
||||
else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false;
|
||||
else regex = true;
|
||||
else if (before === "!" && typeScript && !last.broke && last.before !== null) {
|
||||
// `x! / 2` divides: a `!` right after an operand on the same line is
|
||||
// a non-null assertion. `!/re/.test(s)` negates a regular expression.
|
||||
const operand = last.before;
|
||||
if (operand === ")" || operand === "}" || operand === "++" || operand === "--") return null;
|
||||
regex = !(
|
||||
operand === "]" ||
|
||||
operand.startsWith("'") ||
|
||||
operand.startsWith('"') ||
|
||||
operand.endsWith("`") ||
|
||||
(/^[A-Za-z0-9_$]/.test(operand) && !EXPRESSION_KEYWORDS.has(operand))
|
||||
);
|
||||
} else regex = true;
|
||||
if (regex) {
|
||||
const start = index;
|
||||
index++;
|
||||
@@ -373,9 +396,33 @@ function scan(path: string, source: string): Token[] | null {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** Whether `current` differs from `previous` only in whitespace and in
|
||||
* comments that cannot affect compilation. */
|
||||
export function tokenEquivalentSource(path: string, previous: string, current: string): boolean {
|
||||
/** The line and column of each token's start, with the line terminators
|
||||
* source locations count (backend/source-locations.ts). */
|
||||
function tokenPositions(source: string, tokens: readonly Token[]): number[] {
|
||||
const positions: number[] = [];
|
||||
let line = 0;
|
||||
let lineStart = 0;
|
||||
let offset = 0;
|
||||
for (const token of tokens) {
|
||||
for (; offset < token.start; offset++) {
|
||||
const code = source.charCodeAt(offset);
|
||||
if (code === 13 && source.charCodeAt(offset + 1) === 10) offset++;
|
||||
if (code === 10 || code === 13 || code === 0x2028 || code === 0x2029) {
|
||||
line++;
|
||||
lineStart = offset + 1;
|
||||
}
|
||||
}
|
||||
positions.push(line, token.start - lineStart);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
function equivalentSources(
|
||||
path: string,
|
||||
previous: string,
|
||||
current: string,
|
||||
positions: boolean,
|
||||
): boolean {
|
||||
if (previous === current) return true;
|
||||
const left = scan(path, previous);
|
||||
if (left === null) return false;
|
||||
@@ -387,5 +434,27 @@ export function tokenEquivalentSource(path: string, previous: string, current: s
|
||||
if (a.text !== b.text || a.comment !== b.comment) return false;
|
||||
if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false;
|
||||
}
|
||||
return true;
|
||||
if (!positions) return true;
|
||||
const before = tokenPositions(previous, left);
|
||||
const after = tokenPositions(current, right);
|
||||
return before.every((value, index) => value === after[index]);
|
||||
}
|
||||
|
||||
/** Whether `current` differs from `previous` only in whitespace and in
|
||||
* comments that cannot affect compilation. */
|
||||
export function tokenEquivalentSource(path: string, previous: string, current: string): boolean {
|
||||
return equivalentSources(path, previous, current, false);
|
||||
}
|
||||
|
||||
/** Whether `current` is token-equivalent to `previous` (tokenEquivalentSource)
|
||||
* and every token keeps its line and column: the edit leaves the source
|
||||
* positions debug information records unchanged (a comment's text within
|
||||
* its line, comments and whitespace after the last token, trailing
|
||||
* whitespace). */
|
||||
export function tokenPositionEquivalentSource(
|
||||
path: string,
|
||||
previous: string,
|
||||
current: string,
|
||||
): boolean {
|
||||
return equivalentSources(path, previous, current, true);
|
||||
}
|
||||
|
||||
@@ -219,3 +219,53 @@ test("a supplied file that changes is a different entry", () => {
|
||||
expect(store.entries.size).toBe(2);
|
||||
writeFileSync(fallback, fallbackText);
|
||||
});
|
||||
|
||||
/** The program's files, recording which ones were read. */
|
||||
function recording(texts: Record<string, string>): ((name: string) => SourceFile | undefined) & {
|
||||
read: string[];
|
||||
} {
|
||||
const inner = files(texts);
|
||||
const read: string[] = [];
|
||||
return Object.assign((name: string) => (read.push(name), inner(name)), { read });
|
||||
}
|
||||
|
||||
test("a program of modules stores its answer without reading the supplied files", () => {
|
||||
const store = memoryStore();
|
||||
setLibDiagnosticsStore(store, "checker");
|
||||
const sources = recording({ [main]: "export const n: number = 'x';\n" });
|
||||
reusedLibDiagnostics(
|
||||
sessionProgram({ semantic: new Map([[main, []]]) }),
|
||||
names,
|
||||
supplied,
|
||||
sources,
|
||||
);
|
||||
expect(sources.read).toEqual([main]);
|
||||
const [text] = [...store.entries.values()];
|
||||
expect(JSON.parse(text!).globals).toBeNull();
|
||||
// A later program of modules reuses it, again without reading them.
|
||||
const again = recording({ [main]: "export const n: number = 'x';\n" });
|
||||
const program = sessionProgram({ semantic: new Map([[main, []]]) });
|
||||
expect(reusedLibDiagnostics(program, names, supplied, again)).not.toBeNull();
|
||||
expect(program.asked).toEqual([`checked ${main}`]);
|
||||
expect(again.read).toEqual([main]);
|
||||
});
|
||||
|
||||
test("a script that uses an answer stored without global names collects and stores them", () => {
|
||||
const store = memoryStore();
|
||||
setLibDiagnosticsStore(store, "checker");
|
||||
const semantic = new Map([[main, []]]);
|
||||
reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile);
|
||||
const script = recording({ [main]: "const total = 1;\n" });
|
||||
const program = sessionProgram({ semantic });
|
||||
expect(reusedLibDiagnostics(program, names, supplied, script)).not.toBeNull();
|
||||
expect(program.asked).toEqual([`checked ${main}`]);
|
||||
expect(script.read).toEqual([main, lib, fallback]);
|
||||
const [text] = [...store.entries.values()];
|
||||
expect(new Set(JSON.parse(text!).globals)).toEqual(new Set(["NaN", "Array", "process"]));
|
||||
// A script that declares one of them runs the full check, and the entry keeps its names.
|
||||
const clash = sessionProgram({ semantic });
|
||||
reusedLibDiagnostics(clash, names, supplied, files({ [main]: "declare var NaN: number;\n" }));
|
||||
expect(clash.asked[0]).toBe("getGlobalDiagnostics");
|
||||
expect(store.entries.size).toBe(1);
|
||||
expect(JSON.parse([...store.entries.values()][0]!).globals).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -10,7 +10,14 @@
|
||||
* (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the
|
||||
* order the full check sorts them, by file name. An answer is stored only
|
||||
* when the full check reports no global diagnostics: the global diagnostics
|
||||
* of checking the other files are then the full check's. */
|
||||
* of checking the other files are then the full check's.
|
||||
*
|
||||
* The global names the supplied files declare matter only to programs with
|
||||
* scripts, and collecting them reads every supplied file's syntax tree,
|
||||
* which a program of modules does not otherwise read (about 4 MB at the
|
||||
* peak of a small build). An answer stored by such a program leaves them
|
||||
* out; the first program with scripts that uses the answer collects them
|
||||
* and stores them with it. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { statSync } from "node:fs";
|
||||
import type { Node, SourceFile } from "./ast-types.js";
|
||||
@@ -25,7 +32,7 @@ export interface LibDiagnosticsStore {
|
||||
write(key: string, text: string): void;
|
||||
}
|
||||
|
||||
const SCHEMA = "scriptc.lib-diagnostics.v1";
|
||||
const SCHEMA = "scriptc.lib-diagnostics.v2";
|
||||
|
||||
let configured: { store: LibDiagnosticsStore; checker: string } | null = null;
|
||||
|
||||
@@ -89,7 +96,14 @@ export function reusedLibDiagnostics(
|
||||
const key = entryKey(checker, options, libs);
|
||||
if (key === null) return null;
|
||||
const stored = readEntry(store, key, libs);
|
||||
if (stored !== null && !collides(stored.globals)) {
|
||||
// An answer stored without the supplied files' global names: a program
|
||||
// with scripts collects them and stores them with the answer.
|
||||
let globals = stored?.globals ?? null;
|
||||
if (stored !== null && globals === null && scripts.length > 0) {
|
||||
globals = suppliedGlobalNames(libs, sourceFile);
|
||||
if (globals !== null) writeEntry(store, key, globals, Object.fromEntries(stored.files));
|
||||
}
|
||||
if (stored !== null && (scripts.length === 0 || (globals !== null && !collides(globals)))) {
|
||||
const checked = program.checkedFileDiagnosticData(own);
|
||||
if (checked === null) return null;
|
||||
const byFile = new Map<string, Ts7DiagnosticData[]>(stored.files);
|
||||
@@ -104,20 +118,35 @@ export function reusedLibDiagnostics(
|
||||
const semantic = program.diagnosticData("getSemanticDiagnostics");
|
||||
// A server that cannot check only some files could never use the answer.
|
||||
if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) {
|
||||
const globals = suppliedGlobalNames(libs, sourceFile);
|
||||
if (globals !== null && !collides(globals)) {
|
||||
const names = scripts.length === 0 ? null : suppliedGlobalNames(libs, sourceFile);
|
||||
if (scripts.length === 0 || (names !== null && !collides(names))) {
|
||||
const files: Record<string, Ts7DiagnosticData[]> = {};
|
||||
for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name);
|
||||
try {
|
||||
store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files }));
|
||||
} catch {
|
||||
// Best effort: a failed write is a later miss.
|
||||
}
|
||||
writeEntry(store, key, names, files);
|
||||
}
|
||||
}
|
||||
return { semantic, global };
|
||||
}
|
||||
|
||||
/** Stores an answer: the supplied files' diagnostics, and their global names
|
||||
* when they were collected (null otherwise). Best effort: a failed write is
|
||||
* a later miss. */
|
||||
function writeEntry(
|
||||
store: LibDiagnosticsStore,
|
||||
key: string,
|
||||
globals: ReadonlySet<string> | null,
|
||||
files: Record<string, Ts7DiagnosticData[]>,
|
||||
): void {
|
||||
try {
|
||||
store.write(
|
||||
key,
|
||||
JSON.stringify({ schema: SCHEMA, globals: globals === null ? null : [...globals], files }),
|
||||
);
|
||||
} catch {
|
||||
// A failed write is a later miss.
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a file cannot add to or change the global declarations the
|
||||
* supplied files declare: a module with no `declare global`, no module
|
||||
* augmentation and no UMD `export as namespace` declaration. */
|
||||
@@ -276,7 +305,7 @@ function readEntry(
|
||||
store: LibDiagnosticsStore,
|
||||
key: string,
|
||||
libs: readonly string[],
|
||||
): { files: Map<string, Ts7DiagnosticData[]>; globals: Set<string> } | null {
|
||||
): { files: Map<string, Ts7DiagnosticData[]>; globals: Set<string> | null } | null {
|
||||
let text: string | null;
|
||||
try {
|
||||
text = store.read(key);
|
||||
@@ -290,7 +319,11 @@ function readEntry(
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals))
|
||||
if (
|
||||
entry.schema !== SCHEMA ||
|
||||
entry.files === undefined ||
|
||||
(entry.globals !== null && !Array.isArray(entry.globals))
|
||||
)
|
||||
return null;
|
||||
const files = new Map<string, Ts7DiagnosticData[]>();
|
||||
for (const name of libs) {
|
||||
@@ -298,7 +331,10 @@ function readEntry(
|
||||
if (!Array.isArray(list)) return null;
|
||||
files.set(name, list);
|
||||
}
|
||||
return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) };
|
||||
const globals = Array.isArray(entry.globals)
|
||||
? new Set(entry.globals.filter((name): name is string => typeof name === "string"))
|
||||
: null;
|
||||
return { files, globals };
|
||||
}
|
||||
|
||||
/** Each file's diagnostics (already in the checker's order) in the order the
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1747,9 +1747,17 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx):
|
||||
return { kind: "generator", async: true, ...channels };
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
const FETCH_HANDLE_NAMES: ReadonlySet<string> = new Set([
|
||||
"Headers",
|
||||
"Request",
|
||||
"Response",
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
]);
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response, AbortController,
|
||||
* AbortSignal), optionally in arrays or with null/undefined: checked-dynamic
|
||||
* handles wherever they are stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
@@ -1765,9 +1773,7 @@ export function isFetchHandleType(
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(ts.symbolName(symbol) !== "Headers" &&
|
||||
ts.symbolName(symbol) !== "Request" &&
|
||||
ts.symbolName(symbol) !== "Response") ||
|
||||
!FETCH_HANDLE_NAMES.has(ts.symbolName(symbol)) ||
|
||||
!symbol.declarations.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
@@ -3071,11 +3077,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (!elem) return null;
|
||||
return genResultRecord(elem, VOID, ctx.shapes, ctx.unions);
|
||||
}
|
||||
// node:net's SocketAddress and BlockList: native handles (scr_net.c)
|
||||
// whose getters and methods dispatch through the handle.
|
||||
// node:net's SocketAddress and BlockList (scr_net.c) and node:util's
|
||||
// MIMEType and MIMEParams (scr_mime.c): native handles whose getters and
|
||||
// methods dispatch through the handle.
|
||||
if (
|
||||
((psym !== undefined && ts.symbolName(psym) === "SocketAddress") ||
|
||||
(psym !== undefined && ts.symbolName(psym) === "BlockList")) &&
|
||||
(psym !== undefined && ts.symbolName(psym) === "BlockList") ||
|
||||
(psym !== undefined && ts.symbolName(psym) === "MIMEType") ||
|
||||
(psym !== undefined && ts.symbolName(psym) === "MIMEParams")) &&
|
||||
psym.declarations.some(
|
||||
(d) =>
|
||||
(ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
|
||||
@@ -3084,6 +3093,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return DYN;
|
||||
}
|
||||
// The functions node:util's debounce and throttle return
|
||||
// (scr_util_timing.c): native functions with their own members and
|
||||
// getters.
|
||||
if (
|
||||
psym !== undefined &&
|
||||
(ts.symbolName(psym) === "DebouncedFunction" || ts.symbolName(psym) === "ThrottledFunction") &&
|
||||
psym.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)),
|
||||
)
|
||||
) {
|
||||
return DYN;
|
||||
}
|
||||
// WebCrypto: the crypto global and its subtle object are dynamic
|
||||
// objects, CryptoKey values are native handles (scr_webcrypto.c), and
|
||||
// the algorithm dictionaries the methods take and keys report are plain
|
||||
@@ -7039,6 +7060,43 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A mapped type reached through a type alias whose arguments mention no
|
||||
* type parameter (`Record<never, number>`, `Catalog<typeof data, "image">`,
|
||||
* a module-level alias without arguments):
|
||||
* the checker computed its members from concrete inputs, so an empty
|
||||
* result really is the empty object type, unlike `Partial<T>` inside a
|
||||
* generic body. */
|
||||
function resolvedMappedAlias(t: ts.Type, ctx: TypeMapperCtx): boolean {
|
||||
if (!isMappedShape(t) || t.getAliasSymbol() === undefined) return false;
|
||||
const args = t.getAliasTypeArguments();
|
||||
const seen = new Set<ts.Type>();
|
||||
const concrete = (part: ts.Type): boolean => {
|
||||
if (part.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index)) return false;
|
||||
if (part.flags & (ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional)) return false;
|
||||
if (seen.has(part)) return true;
|
||||
seen.add(part);
|
||||
if (part.isUnionType() || part.isIntersectionType())
|
||||
return ts.constituentTypes(part).every(concrete);
|
||||
if (!part.getAliasTypeArguments().every(concrete)) return false;
|
||||
return (
|
||||
(part.flags & ts.TypeFlags.Object) === 0 ||
|
||||
((part as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) === 0 ||
|
||||
ctx.checker.getTypeArguments(part as ts.TypeReference).every(concrete)
|
||||
);
|
||||
};
|
||||
// An alias without type arguments is concrete when it is declared at
|
||||
// module level (a function body's alias may mention that function's type
|
||||
// parameters).
|
||||
if (args.length === 0)
|
||||
return t
|
||||
.getAliasSymbol()!
|
||||
.declarations.every(
|
||||
(d) =>
|
||||
ts.isTypeAliasDeclaration(d) && (ts.isSourceFile(d.parent) || ts.isModuleBlock(d.parent)),
|
||||
);
|
||||
return args.every(concrete);
|
||||
}
|
||||
|
||||
/** True for MAPPED-type results — `Partial<Config>`, `Record<"a", n>`,
|
||||
* whatever `Pick`/`Omit` reduce to. Their shape is computed by the checker,
|
||||
* not declared anywhere: the only declaration behind them is the utility
|
||||
@@ -7993,8 +8051,12 @@ function mapRecordTypeInner(
|
||||
// still unknown there) — interning `{}` would give a wrong type. The
|
||||
// degenerate empties (`Partial<{}>`, `Omit<C, keyof C>`) go with it.
|
||||
// An INDEX-SIGNATURE shape is exempt: `Record<string, T>` legitimately
|
||||
// has zero declared members — the signature is the shape.
|
||||
if (computed && props.length === 0 && !indexValue) return null;
|
||||
// has zero declared members — the signature is the shape. So is a mapped
|
||||
// alias instantiated over concrete arguments (`Record<never, T>`, a
|
||||
// catalog type whose key filter keeps no key): the checker resolved it,
|
||||
// and it is the empty object type below.
|
||||
if (computed && props.length === 0 && !indexValue && !resolvedMappedAlias(widened, ctx))
|
||||
return null;
|
||||
// An empty object type — `{}` (spelled, the checker's shared
|
||||
// intrinsic, or inferred from an empty object literal), `interface
|
||||
// Empty {}` — is tsc's TOP type over non-nullish values: every number,
|
||||
|
||||
@@ -608,6 +608,8 @@ async function compileExecutableNative(
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.mime ? { mime: true } : {}),
|
||||
...(features.utilTiming ? { utilTiming: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
events: features.events,
|
||||
|
||||
@@ -602,6 +602,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.toUSVString",
|
||||
"util.inherits",
|
||||
"util.deprecate",
|
||||
"util.extend",
|
||||
"util.signalExitCode",
|
||||
"util.diff",
|
||||
"util.debounce",
|
||||
"util.throttle",
|
||||
"mime.typeNew",
|
||||
"mime.typeParse",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
|
||||
@@ -1147,6 +1147,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* wrappers; call sites with a static function type check them back. */
|
||||
"util.inherits": { argTypes: [DYN, DYN], result: VOID },
|
||||
"util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
"util.isArrayDeprecated": { argTypes: [DYN], result: BOOL },
|
||||
"util.extend": { argTypes: [DYN, DYN], result: DYN },
|
||||
"util.signalExitCode": { argTypes: [DYN], result: F64 },
|
||||
"util.aborted": { argTypes: [DYN, DYN], result: DYN },
|
||||
"util.diff": { argTypes: [DYN, DYN], result: DYN },
|
||||
"util.debounce": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"util.throttle": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
"mime.typeNew": { argTypes: [DYN, DYN], result: DYN },
|
||||
"mime.typeParse": { argTypes: [DYN], result: DYN },
|
||||
"mime.paramsNew": { argTypes: [], result: DYN },
|
||||
"mime.is": { argTypes: [DYN, F64], result: BOOL },
|
||||
"util.callbackify": { argTypes: [DYN], result: DYN },
|
||||
/** util.promisify over an untyped function value: a native dyn function
|
||||
* whose calls answer a dyn promise the appended callback settles. */
|
||||
@@ -4516,6 +4527,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** Whether a runtime format string has a '%' before its last character. */
|
||||
"insp.formatSpecifies": { argTypes: [STRING], result: BOOL },
|
||||
"insp.formatSpecifiesDyn": { argTypes: [DYN], result: BOOL },
|
||||
"insp.dynFormatS": { argTypes: [DYN, F64], result: STRING },
|
||||
/** util.format's %o over a checked-dynamic value: inspect with
|
||||
* showHidden. Kinds whose hidden entries have no rendering throw the
|
||||
* 'not supported yet' Error. */
|
||||
|
||||
@@ -130,10 +130,7 @@ export function specializeCallbackCalls(
|
||||
const { written, closureWritten } = rebindings(mod);
|
||||
const copies = new Map<string, IrFunction>();
|
||||
const candidates = new Map<string, Candidate>();
|
||||
let totalNodes = 0;
|
||||
for (const fn of mod.functions) {
|
||||
const nodes = countNodes(fn);
|
||||
totalNodes += nodes;
|
||||
if (
|
||||
!(fn.ownsPrototype || fn.name.startsWith("%arr.")) ||
|
||||
fn.name === mod.entry ||
|
||||
@@ -142,10 +139,11 @@ export function specializeCallbackCalls(
|
||||
fn.captures !== undefined ||
|
||||
fn.classCaptures?.length ||
|
||||
fn.fastRead !== undefined ||
|
||||
fn.speculativeDispatch ||
|
||||
nodes > MAX_FUNCTION_NODES
|
||||
fn.speculativeDispatch
|
||||
)
|
||||
continue;
|
||||
const nodes = countNodes(fn);
|
||||
if (nodes > MAX_FUNCTION_NODES) continue;
|
||||
const rebound = written.get(fn.name)!;
|
||||
const slots: number[] = [];
|
||||
fn.params.forEach((param, index) => {
|
||||
@@ -165,19 +163,30 @@ export function specializeCallbackCalls(
|
||||
const definitions = new Map<string, Map<string, IrExpr>>();
|
||||
const globalIds = new Set(mod.globals?.map((global) => global.id));
|
||||
const globalWrites = new Map<string, IrExpr[]>();
|
||||
// The sum of countNodes over every function, counted by the walk below.
|
||||
let totalNodes = 0;
|
||||
for (const fn of mod.functions) {
|
||||
let nodes = 0;
|
||||
const values = new Map<string, IrExpr[]>();
|
||||
const counts = new Map<string, number>();
|
||||
const record = (id: string, value: IrExpr | null, weight: number): void => {
|
||||
counts.set(id, (counts.get(id) ?? 0) + weight);
|
||||
if (value) values.set(id, [...(values.get(id) ?? []), value]);
|
||||
if (value) {
|
||||
const list = values.get(id);
|
||||
if (list === undefined) values.set(id, [value]);
|
||||
else list.push(value);
|
||||
}
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (stmt) => {
|
||||
nodes++;
|
||||
if (stmt.kind === "varDecl") record(stmt.localId, stmt.init, stmt.init ? 1 : 0);
|
||||
if (stmt.kind === "assign") {
|
||||
if (globalIds.has(stmt.localId))
|
||||
globalWrites.set(stmt.localId, [...(globalWrites.get(stmt.localId) ?? []), stmt.value]);
|
||||
if (globalIds.has(stmt.localId)) {
|
||||
const list = globalWrites.get(stmt.localId);
|
||||
if (list === undefined) globalWrites.set(stmt.localId, [stmt.value]);
|
||||
else list.push(stmt.value);
|
||||
}
|
||||
record(stmt.localId, stmt.value, 1);
|
||||
}
|
||||
if (stmt.kind === "forOf") record(stmt.localId, null, 2);
|
||||
@@ -186,6 +195,7 @@ export function specializeCallbackCalls(
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
nodes++;
|
||||
if (expr.kind === "assignExpr" || expr.kind === "incDec") {
|
||||
record(expr.localId, null, 2);
|
||||
if (globalIds.has(expr.localId)) globalWrites.set(expr.localId, []);
|
||||
@@ -208,6 +218,7 @@ export function specializeCallbackCalls(
|
||||
)
|
||||
single.set(id, list[0]!);
|
||||
definitions.set(fn.name, single);
|
||||
totalNodes += Math.min(nodes, MAX_FUNCTION_NODES + 1);
|
||||
}
|
||||
const globalDefinitions = new Map<string, IrExpr>();
|
||||
for (const global of mod.globals ?? [])
|
||||
|
||||
@@ -538,7 +538,7 @@ export class CaptureFlow {
|
||||
facts.callees.add(e.callee.localId);
|
||||
break;
|
||||
}
|
||||
return everyExprChild(e, expr, (s) => stmt(s, false));
|
||||
return everyExprChild(e, expr, nested);
|
||||
};
|
||||
// `split` mirrors rewrite(): only statement lists reached through
|
||||
// ordinary nesting from the body are expanded. Switch cases share one
|
||||
@@ -599,8 +599,10 @@ export class CaptureFlow {
|
||||
list(s.body, split)
|
||||
);
|
||||
}
|
||||
return everyStmtChild(s, expr, (child) => stmt(child, false));
|
||||
return everyStmtChild(s, expr, nested);
|
||||
};
|
||||
// Statements nested in expressions and in other statements' heads.
|
||||
const nested = (s: IrStmt): boolean => stmt(s, false);
|
||||
list(fn.body, true);
|
||||
return facts;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
if (value && (value.kind === "numLit" || value.kind === "boolLit")) values.set(id, value);
|
||||
else values.delete(id);
|
||||
};
|
||||
for (const fn of mod.functions)
|
||||
// Per function, the candidates it reads: the rewrite below visits only the
|
||||
// functions that read a constant.
|
||||
const readsByFunction = new Map<IrFunction, Set<string>>();
|
||||
for (const fn of mod.functions) {
|
||||
let read: Set<string> | undefined;
|
||||
everyStmtList(fn.body, {
|
||||
// Every node other than a read that names the binding is a write
|
||||
// (assignments, loop and catch bindings, compound updates); only a
|
||||
@@ -48,16 +52,21 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind !== "varRef" && "localId" in e && typeof e.localId === "string")
|
||||
write(e.localId, null);
|
||||
if (e.kind === "varRef") {
|
||||
if (candidates.has(e.localId)) {
|
||||
if (read === undefined) read = new Set();
|
||||
read.add(e.localId);
|
||||
}
|
||||
} else if ("localId" in e && typeof e.localId === "string") write(e.localId, null);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (read !== undefined) readsByFunction.set(fn, read);
|
||||
}
|
||||
const constants = new Map<string, Literal>();
|
||||
for (const [id, value] of values) if (writes.get(id) === 1) constants.set(id, value);
|
||||
if (constants.size === 0) return mod;
|
||||
|
||||
const reads = (e: IrExpr): boolean => !(e.kind === "varRef" && constants.has(e.localId));
|
||||
const rewriteExpr = (e: IrExpr): IrExpr => {
|
||||
if (e.kind === "varRef") {
|
||||
const value = constants.get(e.localId);
|
||||
@@ -68,7 +77,8 @@ export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt);
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn): IrFunction => {
|
||||
if (everyStmtList(fn.body, { stmt: () => true, expr: reads })) return fn;
|
||||
const read = readsByFunction.get(fn);
|
||||
if (read === undefined || ![...read].some((id) => constants.has(id))) return fn;
|
||||
changed = true;
|
||||
return { ...fn, body: fn.body.map(rewriteStmt) };
|
||||
});
|
||||
|
||||
@@ -523,6 +523,12 @@ test("counting-down recursion proves its parameter from the call-site ranges", (
|
||||
);
|
||||
const rising = analyzeInt32Slots(mod([], [up, fn("user", [], [stmt(call("up", [num(-18)]))])]));
|
||||
expect(rising.facts("up").params?.get("d")).toEqual({ min: -18, max: 0 });
|
||||
// The same with the caller first, so the fixpoint analyzes it after the
|
||||
// parameter dropped: the narrowing still sees its argument.
|
||||
const callerFirst = analyzeInt32Slots(
|
||||
mod([], [fn("user", [], [stmt(call("up", [num(-18)]))]), up]),
|
||||
);
|
||||
expect(callerFirst.facts("up").params?.get("d")).toEqual({ min: -18, max: 0 });
|
||||
|
||||
// A negative start counts down past the smallest int32.
|
||||
const negative = analyzeInt32Slots(mod([], [count, even, odd, user([18, -1])]));
|
||||
|
||||
@@ -470,6 +470,7 @@ const DYN_READ_ONLY = new Set<string>([
|
||||
"insp.formatWithOptions",
|
||||
"insp.inspect",
|
||||
"insp.registerCustom",
|
||||
"insp.dynFormatS",
|
||||
"insp.dynHidden",
|
||||
// The dynamic-tier operators run their dyn twins (scr_js_value.c); the
|
||||
// conversions move a value between dyn and jsval, and js.toF64 is
|
||||
@@ -1220,8 +1221,11 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
// Each function's parameter and result writes under its latest analysis:
|
||||
// at the fixpoint, the ranges of every write under the final facts.
|
||||
const lastWrites = new Map<string, Map<string, IntegerRange | null>>();
|
||||
while (queue.length) {
|
||||
const fn = functions.get(queue.pop()!)!;
|
||||
// First in, first out: a function whose slots change while others wait
|
||||
// goes after them, so a caller of many callees is analyzed again once
|
||||
// for all of their changes, not once for each.
|
||||
for (let head = 0; head < queue.length; head++) {
|
||||
const fn = functions.get(queue[head]!)!;
|
||||
queued.delete(fn.name);
|
||||
const params = slots.facts(fn.name).params;
|
||||
if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name);
|
||||
@@ -1675,8 +1679,14 @@ function narrowSlotRanges(
|
||||
// The ranges every function was last analyzed under.
|
||||
let analyzed: Map<string, IntegerRange> = new Map(proven);
|
||||
let verified: Map<string, IntegerRange> = new Map(proven);
|
||||
// Functions whose recorded writes predate their current facts.
|
||||
// Functions whose recorded writes predate their current facts: those
|
||||
// whose parameters rangedParameters proved, and their callers, which
|
||||
// wrote nothing into those parameters while they had no fact.
|
||||
const dirty = new Set(changed);
|
||||
for (const name of changed) {
|
||||
const callers = readersOf(`ret:${name}`);
|
||||
if (callers) for (const caller of callers) dirty.add(caller);
|
||||
}
|
||||
// The narrowing operator: a bound moves at most once from its proven
|
||||
// value (a revived result's from int32), so recursion that shifts its
|
||||
// argument (`f(d - 1)`) cannot walk a bound down one step per round.
|
||||
|
||||
@@ -152,7 +152,7 @@ export function boundedIntegerLoopFacts(
|
||||
e.receiver.kind === "varRef" &&
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return { min: 0, max: Number.MAX_SAFE_INTEGER };
|
||||
return BYTES_LENGTH_RANGE;
|
||||
// Every evaluation of an array length lies in its range, so the
|
||||
// condition bounds each pass even when the body resizes the array.
|
||||
if (isArrayLength(e)) return ARRAY_LENGTH_RANGE;
|
||||
@@ -225,6 +225,12 @@ export function boundedIntegerLoopFacts(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Every length, byteLength and byteOffset of a typed array, DataView or
|
||||
* Buffer: each counts elements or bytes of one allocation, and every
|
||||
* supported target addresses less than 2^48 bytes. Counters and offsets
|
||||
* derived from them then stay exact integers far below 2^53. */
|
||||
export const BYTES_LENGTH_RANGE: IntegerRange = { min: 0, max: 2 ** 48 };
|
||||
|
||||
/** Every length an array can have: SCR_ARR_MAX_LENGTH (scr_array.c). */
|
||||
export const ARRAY_LENGTH_RANGE: IntegerRange = { min: 0, max: 4294967295 };
|
||||
|
||||
@@ -353,6 +359,13 @@ export function guardedLoopCounterFacts(
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return { min: 0, max: STRING_MAX_UNITS };
|
||||
if (
|
||||
e.kind === "bytesIntrinsic" &&
|
||||
(e.method === "length" || e.method === "byteLength") &&
|
||||
e.receiver.kind === "varRef" &&
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return BYTES_LENGTH_RANGE;
|
||||
// An array length can change on any pass, but never leaves its range,
|
||||
// which is all a ceiling needs.
|
||||
if (isArrayLength(e)) return ARRAY_LENGTH_RANGE;
|
||||
@@ -402,9 +415,84 @@ export function guardedLoopCounterFacts(
|
||||
if (Number.isSafeInteger(start.min) && Number.isSafeInteger(max))
|
||||
result.set(id, { min: start.min, max });
|
||||
}
|
||||
// Companion counters: a binding the loop changes only by positive literal
|
||||
// increments that no condition bounds (`out[k++] = ...` beside
|
||||
// `while (i < n)`). A bounded counter that every pass advances before it
|
||||
// can continue limits the number of passes, and each pass adds at most
|
||||
// the companion's increments in the body. Increments inside nested loops
|
||||
// can repeat within a pass, so those companions are not bounded.
|
||||
let passes = Infinity;
|
||||
for (const [id, range] of result) {
|
||||
const hi = ceiling.get(id)!;
|
||||
const step = guaranteedStep(loop.body, id);
|
||||
if (step <= 0) continue;
|
||||
passes = Math.min(passes, Math.floor((hi - range.min) / step) + 1);
|
||||
}
|
||||
if (!Number.isSafeInteger(passes) || passes < 0) return result;
|
||||
const repeated = new Set<string>();
|
||||
for (const inner of nested) for (const id of writtenLocals(inner.body)) repeated.add(id);
|
||||
for (const [id, total] of budget) {
|
||||
const local = locals.get(id),
|
||||
start = initial.get(id);
|
||||
if (result.has(id) || refused.has(id) || repeated.has(id) || ceiling.has(id)) continue;
|
||||
if (!local || local.boxed || local.tdz || !start) continue;
|
||||
const max = start.max + passes * total;
|
||||
if (Number.isSafeInteger(start.min) && Number.isSafeInteger(max))
|
||||
result.set(id, { min: start.min, max });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The increments of counter `id` that every pass of `body` makes before it
|
||||
* can start another pass: those of the leading statements up to the first
|
||||
* one that can continue the loop or holds a nested region (0 when there is
|
||||
* none). A pass that leaves the loop earlier starts no further pass, so the
|
||||
* count bounds the passes that continue. */
|
||||
function guaranteedStep(body: readonly IrStmt[], id: string): number {
|
||||
let step = 0;
|
||||
for (const s of body) {
|
||||
if (s.kind === "assign" && s.localId === id) {
|
||||
const delta = increment(id, s.value);
|
||||
if (delta === null || delta <= 0) return 0;
|
||||
step += delta;
|
||||
continue;
|
||||
}
|
||||
if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.localId === id) {
|
||||
if (s.expr.op !== "+") return 0;
|
||||
step += 1;
|
||||
continue;
|
||||
}
|
||||
if (mayContinue(s)) break;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
/** Whether `s` can start another pass of the enclosing loop before its
|
||||
* following statements run: it holds a `continue`, a loop or another
|
||||
* region whose control flow is not walked here. */
|
||||
function mayContinue(s: IrStmt): boolean {
|
||||
let found = false;
|
||||
const stmt = (x: IrStmt): boolean => {
|
||||
if (
|
||||
x.kind === "continue" ||
|
||||
x.kind === "for" ||
|
||||
x.kind === "while" ||
|
||||
x.kind === "doWhile" ||
|
||||
x.kind === "forOf" ||
|
||||
x.kind === "tryCatch" ||
|
||||
x.kind === "switch" ||
|
||||
(x.kind === "block" && !!x.labels?.length)
|
||||
) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
return everyStmtChild(x, expr, stmt);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt);
|
||||
stmt(s);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The largest value counter `id` takes in one pass of `body`, which starts
|
||||
* at most at `entry`. The caller has checked that the body changes the
|
||||
* counter only by positive literal increments outside switch, exception and
|
||||
|
||||
@@ -828,6 +828,72 @@ test("a while condition on a string's length bounds a counter, and guarded incre
|
||||
expect(run((inside) => [observe(inside), bump, rebound]).after).toBeNull();
|
||||
});
|
||||
|
||||
test("a while counter every pass advances bounds the companions the loop increments", () => {
|
||||
// pos = 0; k = 0; while (pos < text.length) { pos++; observe(k); if (unknown === 1) k++; k++; } observe(k)
|
||||
const text: IrExpr = { loc, kind: "varRef", localId: "text", type: STRING };
|
||||
const length: IrExpr = {
|
||||
loc,
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: text,
|
||||
args: [],
|
||||
type: F64,
|
||||
};
|
||||
const set = (localId: string, value: IrExpr): IrStmt => ({ loc, kind: "assign", localId, value });
|
||||
const bump = (localId: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "incDec", op: "+", prefix: false, localId, type: F64 },
|
||||
});
|
||||
const maybe = (then: IrStmt[]): IrStmt => ({
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition("===", ref("unknown"), num(1)),
|
||||
then,
|
||||
else_: null,
|
||||
});
|
||||
const run = (body: (inside: IrExpr) => IrStmt[]) => {
|
||||
const inside = ref("k"),
|
||||
after = ref("k");
|
||||
const loop: IrStmt = {
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("<", ref("pos"), length),
|
||||
body: body(inside),
|
||||
};
|
||||
const f = fn([set("pos", num(0)), set("k", num(0)), loop, observe(after)]);
|
||||
f.locals.push(
|
||||
{ id: "pos", name: "pos", type: F64, mutable: true },
|
||||
{ id: "k", name: "k", type: F64, mutable: true },
|
||||
{ id: "text", name: "text", type: STRING, mutable: false },
|
||||
);
|
||||
const facts = analyzeIntegerRangeFacts(f);
|
||||
return {
|
||||
inside: facts.ranges.get(inside),
|
||||
after: facts.ranges.get(after),
|
||||
loop: facts.loopRanges.get(loop)?.get("k"),
|
||||
};
|
||||
};
|
||||
// At most 2^31 - 1 passes, each adding at most 2.
|
||||
const bounded = { min: 0, max: 4294967294 };
|
||||
expect(run((inside) => [bump("pos"), observe(inside), maybe([bump("k")]), bump("k")])).toEqual({
|
||||
inside: bounded,
|
||||
after: bounded,
|
||||
loop: bounded,
|
||||
});
|
||||
// A pass that can continue before advancing the counter, or an increment a
|
||||
// nested loop repeats, bounds nothing.
|
||||
const skip: IrStmt = maybe([{ loc, kind: "continue" }]);
|
||||
expect(run((inside) => [skip, bump("pos"), observe(inside), bump("k")]).after).toBeNull();
|
||||
const repeat: IrStmt = {
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("===", ref("unknown"), num(1)),
|
||||
body: [bump("k")],
|
||||
};
|
||||
expect(run((inside) => [bump("pos"), observe(inside), repeat]).after).toBeNull();
|
||||
});
|
||||
|
||||
test("a nested while loop may advance a counter its own condition bounds", () => {
|
||||
// while (pos < text.length) { observe(pos); while (pos < text.length && unknown === 1) pos++; pos += 2; }
|
||||
const text: IrExpr = { loc, kind: "varRef", localId: "text", type: STRING };
|
||||
@@ -1323,7 +1389,7 @@ test("a for loop bounds cursors it resets and copies, and their writes keep the
|
||||
}
|
||||
});
|
||||
|
||||
test("a cursor of a loop over a typed array keeps its writes exact up to 2^53 - 1", () => {
|
||||
test("a cursor of a loop over a typed array keeps its writes within the largest length", () => {
|
||||
// for (x = 0; x < bytes.length; x++) changes = changes + 1;
|
||||
const bytes: IrExpr = {
|
||||
loc,
|
||||
@@ -1355,7 +1421,7 @@ test("a cursor of a loop over a typed array keeps its writes exact up to 2^53 -
|
||||
{ id: "changes", name: "changes", type: F64, mutable: true },
|
||||
{ id: "bytes", name: "bytes", type: { kind: "bytes", elem: "u8" }, mutable: false },
|
||||
);
|
||||
// The sum of the header range passes 2^53 - 1; the write takes the loop
|
||||
// range instead.
|
||||
expect(analyzeIntegerRanges(f).get(counted)).toEqual({ min: 0, max: Number.MAX_SAFE_INTEGER });
|
||||
// The sum of the header range passes the largest typed array length; the
|
||||
// write takes the loop range instead.
|
||||
expect(analyzeIntegerRanges(f).get(counted)).toEqual({ min: 1, max: 2 ** 48 });
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { IrExpr, IrFunction, IrStmt } from "./ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js";
|
||||
import { byteNumberAccess, byteNumberRange } from "./byte-numbers.js";
|
||||
import {
|
||||
BYTES_LENGTH_RANGE,
|
||||
boundedIntegerLoopFacts,
|
||||
guardedLoopCounterFacts,
|
||||
integerPathFallsThrough,
|
||||
@@ -821,7 +822,7 @@ export function analyzeIntegerRangeFacts(
|
||||
const numeric = byteNumberAccess(e);
|
||||
if (numeric && !numeric.write) range = byteNumberRange(numeric);
|
||||
else if (e.method === "length" || e.method === "byteLength" || e.method === "byteOffset")
|
||||
range = { min: 0, max: Number.MAX_SAFE_INTEGER };
|
||||
range = BYTES_LENGTH_RANGE;
|
||||
else if (e.method === "get" && e.invalidNaN !== true && e.receiver.type.kind === "bytes") {
|
||||
const elem = e.receiver.type.elem;
|
||||
if (elem === "u8" || elem === "u8c") range = { min: 0, max: 255 };
|
||||
@@ -1075,6 +1076,19 @@ export function analyzeIntegerRangeFacts(
|
||||
// inside and after the loop.
|
||||
const guarded =
|
||||
s.kind === "while" ? guardedLoopCounterFacts(s, loopLocals, facts) : noInvariants;
|
||||
// The same bounds hold on entry and at every write in the loop,
|
||||
// which is what a proven integer loop needs (loopRanges). A loop
|
||||
// analyzed again keeps the bindings every analysis bounds.
|
||||
if (s.kind === "while") {
|
||||
const previous = loopRanges.get(s);
|
||||
if (!previous) loopRanges.set(s, new Map(guarded));
|
||||
else
|
||||
for (const [id, range] of previous) {
|
||||
const other = guarded.get(id);
|
||||
if (other) previous.set(id, union(range, other));
|
||||
else previous.delete(id);
|
||||
}
|
||||
}
|
||||
const loop = new Map(facts);
|
||||
havocWith(loop, [s], loop, guarded);
|
||||
for (const [id, range] of guarded) set(loop, id, range);
|
||||
|
||||
@@ -126,6 +126,140 @@ test("property helpers whose class branches only test capsules are cache sites",
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
});
|
||||
|
||||
test("bags converted between dyn and jsval are still the receiver", () => {
|
||||
// A program that declares a class returns its bags as jsvals, and the
|
||||
// helpers read them through js.toDyn.
|
||||
const toDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.toDyn",
|
||||
args: [value],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const fromDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [value],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
});
|
||||
const jsvalBag = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.js",
|
||||
["v"],
|
||||
[capsuleBranch("v"), ret(fromDyn(ref("v")))],
|
||||
);
|
||||
const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]);
|
||||
const getter = getHelper(
|
||||
"%dyn.class.property.0",
|
||||
"x",
|
||||
[capsuleBranch("p.0")],
|
||||
call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))),
|
||||
);
|
||||
const setter = setHelper("%dyn.class.property.1", "y");
|
||||
const write = setter.body[0] as Extract<IrStmt, { kind: "exprStmt" }>;
|
||||
const keySet = write.expr as Extract<IrExpr, { kind: "libCall" }>;
|
||||
keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0")));
|
||||
// A converted value that is not the receiver stays a helper call.
|
||||
const foreign = getHelper(
|
||||
"%dyn.class.property.2",
|
||||
"x",
|
||||
[],
|
||||
toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)),
|
||||
);
|
||||
const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
});
|
||||
|
||||
test("a bag helper may first rebind its parameter to a bag of it", () => {
|
||||
// Programs with subclasses normalize the value before the class branches.
|
||||
const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]);
|
||||
const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc });
|
||||
const normalized = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.normalized",
|
||||
["v"],
|
||||
[rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))],
|
||||
);
|
||||
const replaced = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.replaced",
|
||||
["v"],
|
||||
[rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))],
|
||||
);
|
||||
const sites = sitesOf(
|
||||
normalize,
|
||||
normalized,
|
||||
replaced,
|
||||
getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))),
|
||||
getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))),
|
||||
// The property helper itself normalizes its receiver first, too.
|
||||
getHelper("%dyn.class.property.2", "x", [
|
||||
{ kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc },
|
||||
capsuleBranch("p.0"),
|
||||
]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" });
|
||||
});
|
||||
|
||||
test("keyed helpers may test capsules between the key's coercion and the read", () => {
|
||||
const read = (object: IrExpr, key: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
});
|
||||
const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc },
|
||||
then: [ret(read(object, "p.key"))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const keyed = (name: string, between: IrStmt[]): IrFunction =>
|
||||
helper(
|
||||
name,
|
||||
["p.0", "p.key"],
|
||||
[
|
||||
capsuleBranch("p.0"),
|
||||
guard("symbol", "p.key", call(identityBag.name, ref("p.0"))),
|
||||
guard("nullish", "p.0", ref("p.0")),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "key.string",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [ref("p.key")],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
...between,
|
||||
ret(read(call(identityBag.name, ref("p.0")), "key.string")),
|
||||
],
|
||||
["key.string"],
|
||||
);
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc },
|
||||
then: [ret(ref("p.0"))],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
const sites = sitesOf(
|
||||
keyed("%dyn.class.computed.0", []),
|
||||
keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]),
|
||||
keyed("%dyn.class.computed.2", [errorBranch]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" });
|
||||
expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" });
|
||||
expect(sites.site("%dyn.class.computed.2")).toBeNull();
|
||||
});
|
||||
|
||||
test("any other prefix or tail keeps the helper call", () => {
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
@@ -237,3 +371,57 @@ test("bag helpers that exit early for values no class branch matches are still t
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.3")).toBeNull();
|
||||
});
|
||||
|
||||
test("helpers that answer plain values first with the same operation are cache sites", () => {
|
||||
const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({
|
||||
kind: "dynTest",
|
||||
test,
|
||||
negated: true,
|
||||
value: ref("p.0"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: test,
|
||||
then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const store = (key: string): IrStmt[] => [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [ref("p.0"), str(key), ref("p.1")],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
ret(null),
|
||||
];
|
||||
const load = (key: string): IrStmt[] => [
|
||||
ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }),
|
||||
];
|
||||
const sites = sitesOf(
|
||||
setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]),
|
||||
getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]),
|
||||
// An exit for another key, or one plain values do not take.
|
||||
setHelper("%dyn.class.property.2", "x", [exit(store("y"))]),
|
||||
setHelper("%dyn.class.property.3", "x", [
|
||||
exit(store("x"), {
|
||||
kind: "dynTest",
|
||||
test: "classCapsule",
|
||||
value: ref("p.0"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.3")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -61,13 +61,68 @@ export class JsIcSites {
|
||||
if (fn.params.some((p) => p.type.kind !== "dyn")) return null;
|
||||
const receiver = fn.params[0]?.localId;
|
||||
if (receiver === undefined) return null;
|
||||
const second = fn.params[1]?.localId;
|
||||
const exits: JsIcSite[] = [];
|
||||
let i = 0;
|
||||
while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++;
|
||||
for (; i < fn.body.length; i++) {
|
||||
const s = fn.body[i]!;
|
||||
if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue;
|
||||
const exit = this.plainExit(s, receiver, second);
|
||||
if (exit === null) break;
|
||||
exits.push(exit);
|
||||
}
|
||||
const tail = fn.body.slice(i);
|
||||
if (fn.params.length === 1) return this.getTail(tail, receiver);
|
||||
if (fn.params.length !== 2) return null;
|
||||
const second = fn.params[1]!.localId;
|
||||
return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
|
||||
let site: JsIcSite | null;
|
||||
if (fn.params.length === 1) site = this.getTail(tail, receiver);
|
||||
else if (fn.params.length !== 2 || second === undefined) return null;
|
||||
else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
|
||||
if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null;
|
||||
// An exit answers plain values with the operation the site caches.
|
||||
return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null;
|
||||
}
|
||||
|
||||
/** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no
|
||||
* else: an exit that runs the helper's generic operation directly on a
|
||||
* value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R,
|
||||
* K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a
|
||||
* bag of it). */
|
||||
private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null {
|
||||
const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean =>
|
||||
e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver);
|
||||
if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null;
|
||||
let body = s.then;
|
||||
const inner = body[0];
|
||||
if (
|
||||
body.length === 1 &&
|
||||
inner?.kind === "if" &&
|
||||
inner.else_ === null &&
|
||||
negated(inner.cond, "funcKind")
|
||||
)
|
||||
body = inner.then;
|
||||
const [first, ret] = body;
|
||||
if (body.length === 1 && first?.kind === "return") {
|
||||
const read = unwrapFromDyn(first.value);
|
||||
if (
|
||||
read?.kind !== "dynKeyGet" ||
|
||||
read.key.kind !== "strLit" ||
|
||||
!this.bagOf(read.value, receiver)
|
||||
)
|
||||
return null;
|
||||
return { kind: "get", key: read.key.value };
|
||||
}
|
||||
if (
|
||||
body.length !== 2 ||
|
||||
first?.kind !== "exprStmt" ||
|
||||
ret?.kind !== "return" ||
|
||||
ret.value !== null
|
||||
)
|
||||
return null;
|
||||
const call = first.expr;
|
||||
if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null;
|
||||
const [bag, key, stored] = call.args;
|
||||
if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null;
|
||||
if (value === undefined || !isVar(stored, value)) return null;
|
||||
return { kind: "set", key: key.value };
|
||||
}
|
||||
|
||||
/** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ?
|
||||
@@ -109,10 +164,14 @@ export class JsIcSites {
|
||||
/** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key);
|
||||
* if (receiver == null) return dynKeyGet(receiver, key);
|
||||
* const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read
|
||||
* possibly under js.fromDyn. */
|
||||
* possibly under js.fromDyn. Capsule branches may precede the final read:
|
||||
* a program that declares classes looks the string key up in each class's
|
||||
* key table there. */
|
||||
private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null {
|
||||
const [symbol, nullish, decl, ret] = tail;
|
||||
if (tail.length !== 4) return null;
|
||||
const [symbol, nullish, decl] = tail;
|
||||
const ret = tail[tail.length - 1];
|
||||
if (tail.length < 4) return null;
|
||||
if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null;
|
||||
if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null;
|
||||
if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key))
|
||||
return null;
|
||||
@@ -159,9 +218,12 @@ export class JsIcSites {
|
||||
}
|
||||
|
||||
/** Whether `e` is the receiver itself for every ordinary object or array:
|
||||
* the receiver, or a chain of property-bag helpers applied to it. */
|
||||
* the receiver, or a chain of property-bag helpers applied to it. A
|
||||
* conversion between the dyn and jsval forms keeps the object it holds. */
|
||||
private bagOf(e: IrExpr, receiver: string): boolean {
|
||||
if (isVar(e, receiver)) return true;
|
||||
if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn"))
|
||||
return e.args.length === 1 && this.bagOf(e.args[0]!, receiver);
|
||||
return (
|
||||
e.kind === "call" &&
|
||||
e.args.length === 1 &&
|
||||
@@ -182,6 +244,13 @@ export class JsIcSites {
|
||||
return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param);
|
||||
}
|
||||
|
||||
/** `param = <bag of param>`, such as the subclass normalization
|
||||
* `value = %dyn.class.normalize(value)`, which leaves an ordinary object
|
||||
* or array in `param`. */
|
||||
private rebinding(s: IrStmt, param: string): boolean {
|
||||
return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param);
|
||||
}
|
||||
|
||||
/** A property-bag helper that returns its argument unless a capsule
|
||||
* branch is taken. */
|
||||
private identityHelper(name: string): boolean {
|
||||
@@ -204,7 +273,10 @@ export class JsIcSites {
|
||||
identity =
|
||||
fn.body
|
||||
.slice(0, -1)
|
||||
.every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) &&
|
||||
.every(
|
||||
(s) =>
|
||||
this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param),
|
||||
) &&
|
||||
last?.kind === "return" &&
|
||||
last.value !== null &&
|
||||
this.bagOf(last.value, param);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isBorrowSafeMath } from "./analysis.js";
|
||||
import type { IrExpr, IrFunction, IrLibFn, IrStmt } from "./ir.js";
|
||||
import { everyStmtList } from "./traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
|
||||
|
||||
/** Expression kinds that run no user code of their own and never consult
|
||||
* the ambient receiver stack. Calls are admitted here and resolved through
|
||||
@@ -85,6 +85,25 @@ const RECEIVER_FREE_STMTS = new Set<IrStmt["kind"]>([
|
||||
"runtimeFence",
|
||||
]);
|
||||
|
||||
/** The kinds whose nodes the readers walk answers true for without looking
|
||||
* at them: every receiver-free statement, and the receiver-free expressions
|
||||
* other than calls, library calls, string conversions and data intrinsics. */
|
||||
const TRIVIALLY_RECEIVER_FREE = new Set<string>([
|
||||
...RECEIVER_FREE_STMTS,
|
||||
...[...RECEIVER_FREE_EXPRS].filter(
|
||||
(kind) =>
|
||||
kind !== "call" &&
|
||||
kind !== "new" &&
|
||||
kind !== "virtualCall" &&
|
||||
kind !== "libCall" &&
|
||||
kind !== "toString" &&
|
||||
kind !== "arrIntrinsic" &&
|
||||
kind !== "strIntrinsic" &&
|
||||
kind !== "mapIntrinsic" &&
|
||||
kind !== "setIntrinsic",
|
||||
),
|
||||
]);
|
||||
|
||||
/** Runtime library calls beyond scalar Math that only compute or build and
|
||||
* park a native error: the f64[] folds behind Math.max/min(...xs),
|
||||
* `new Error(message)` (scr_error_new), and the checked-read TypeError
|
||||
@@ -133,7 +152,11 @@ function opaqueOperand(e: IrExpr): boolean {
|
||||
export class AmbientReceiverReaders {
|
||||
private readers: Set<string> | null = null;
|
||||
|
||||
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking the bodies it holds. */
|
||||
private readonly nodes?: IrNodeIndex,
|
||||
) {}
|
||||
|
||||
ignores(fn: IrFunction): boolean {
|
||||
if (fn.async || fn.generator) return false;
|
||||
@@ -153,6 +176,7 @@ export class AmbientReceiverReaders {
|
||||
}
|
||||
const readers = new Set<string>();
|
||||
const callers = new Map<string, string[]>();
|
||||
const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE);
|
||||
for (const fn of this.functions.values()) {
|
||||
const callees = new Set<string>();
|
||||
let reads = fn.async === true || fn.generator !== undefined;
|
||||
@@ -161,32 +185,36 @@ export class AmbientReceiverReaders {
|
||||
callees.add(name);
|
||||
return true;
|
||||
};
|
||||
const expr = (expr: IrExpr): boolean => {
|
||||
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
|
||||
switch (expr.kind) {
|
||||
case "call":
|
||||
return edge(expr.callee);
|
||||
case "new":
|
||||
return edge(`%${expr.className}.constructor`);
|
||||
case "virtualCall": {
|
||||
// Any class's override may run: admit only when every
|
||||
// same-named method is itself receiver-free.
|
||||
const targets = byMethod.get(expr.method) ?? [];
|
||||
return targets.length > 0 && targets.every(edge);
|
||||
}
|
||||
case "libCall":
|
||||
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
|
||||
case "toString":
|
||||
return !opaqueOperand(expr.operand);
|
||||
default: {
|
||||
const operands = dataIntrinsicOperands(expr);
|
||||
return operands === null || !operands.some(opaqueOperand);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!reads)
|
||||
reads = !everyStmtList(fn.body, {
|
||||
stmt: (stmt) => RECEIVER_FREE_STMTS.has(stmt.kind),
|
||||
expr: (expr) => {
|
||||
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
|
||||
switch (expr.kind) {
|
||||
case "call":
|
||||
return edge(expr.callee);
|
||||
case "new":
|
||||
return edge(`%${expr.className}.constructor`);
|
||||
case "virtualCall": {
|
||||
// Any class's override may run: admit only when every
|
||||
// same-named method is itself receiver-free.
|
||||
const targets = byMethod.get(expr.method) ?? [];
|
||||
return targets.length > 0 && targets.every(edge);
|
||||
}
|
||||
case "libCall":
|
||||
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
|
||||
case "toString":
|
||||
return !opaqueOperand(expr.operand);
|
||||
default: {
|
||||
const operands = dataIntrinsicOperands(expr);
|
||||
return operands === null || !operands.some(opaqueOperand);
|
||||
}
|
||||
}
|
||||
},
|
||||
eachNodeOfKinds(fn, scan, (node, statement) => {
|
||||
const free = statement
|
||||
? RECEIVER_FREE_STMTS.has((node as IrStmt).kind)
|
||||
: expr(node as IrExpr);
|
||||
if (!free) reads = true;
|
||||
return free;
|
||||
});
|
||||
if (reads) readers.add(fn.name);
|
||||
for (const callee of callees) {
|
||||
|
||||
@@ -102,6 +102,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"punycode.warn",
|
||||
// DEP0005 for the deprecated Buffer constructor.
|
||||
"buffer.deprecation",
|
||||
// util.aborted answers a checked-dynamic promise, and so do the
|
||||
// functions util.debounce and util.throttle return.
|
||||
"util.aborted",
|
||||
"util.debounce",
|
||||
"util.throttle",
|
||||
"timers.immediatePromise",
|
||||
// The performance timeline (scr_perf.c, linked with this unit for its
|
||||
// MaxPerformanceEntryBufferExceededWarning).
|
||||
@@ -143,7 +148,10 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
"util.deprecate",
|
||||
"util.diff",
|
||||
"util.extend",
|
||||
"util.inherits",
|
||||
"util.isArrayDeprecated",
|
||||
"util.styleText",
|
||||
]);
|
||||
|
||||
@@ -172,7 +180,10 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.callbackify",
|
||||
"util.debuglog",
|
||||
"util.deprecate",
|
||||
"util.diff",
|
||||
"util.extend",
|
||||
"util.inherits",
|
||||
"util.isArrayDeprecated",
|
||||
]);
|
||||
|
||||
/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */
|
||||
@@ -715,6 +726,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."];
|
||||
|
||||
@@ -981,9 +996,20 @@ const RUNTIME_FEATURE_RULES = {
|
||||
(mod.classes ?? []).some(
|
||||
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
|
||||
),
|
||||
fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS],
|
||||
// util.debounce and util.throttle register options.signal's listener.
|
||||
fns: [
|
||||
"events.addAbortListener",
|
||||
"util.aborted",
|
||||
"util.debounce",
|
||||
"util.throttle",
|
||||
...ABORT_SIGNAL_LIB_FNS,
|
||||
],
|
||||
prefixes: ["event."],
|
||||
}),
|
||||
/** util.MIMEType and util.MIMEParams (scr_mime.c). */
|
||||
mime: rule({ prefixes: ["mime."] }),
|
||||
/** util.debounce and util.throttle (scr_util_timing.c). */
|
||||
utilTiming: rule({ fns: ["util.debounce", "util.throttle"] }),
|
||||
/** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */
|
||||
eventsOnceSignal: rule({ fns: ["events.onceSignal"] }),
|
||||
fetch: rule({ prefixes: ["fetch."] }),
|
||||
@@ -1063,9 +1089,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 +1101,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
|
||||
@@ -1165,6 +1196,8 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
http: false,
|
||||
inspect: false,
|
||||
legacyTextDecoder: false,
|
||||
mime: false,
|
||||
utilTiming: false,
|
||||
mockTimers: false,
|
||||
net: false,
|
||||
netLookup: false,
|
||||
@@ -1177,6 +1210,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
qs: false,
|
||||
regex: false,
|
||||
searchParams: false,
|
||||
urlIdna: false,
|
||||
stdioExtended: false,
|
||||
stream: false,
|
||||
streamDyn: false,
|
||||
|
||||
@@ -118,7 +118,12 @@ export function resolveSelfCaptures(mod: IrModule): IrModule {
|
||||
}
|
||||
if (resolved.size === 0) return mod;
|
||||
|
||||
// Only the resolved bodies and the functions that create their closures
|
||||
// change.
|
||||
const touched = new Set(resolved.keys());
|
||||
for (const name of resolved.keys()) for (const site of sites.get(name)!) touched.add(site.owner);
|
||||
const rewritten = mod.functions.map((fn) => {
|
||||
if (!touched.has(fn.name)) return fn;
|
||||
const own = resolved.get(fn.name);
|
||||
const self = own === undefined ? null : fn.captures![own]!;
|
||||
let changed = false;
|
||||
|
||||
@@ -118,22 +118,11 @@ export class NativeCache {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a marker or entry exists, without reading or checking it. */
|
||||
/** Whether an 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);
|
||||
@@ -178,7 +167,7 @@ export class NativeCache {
|
||||
"object",
|
||||
"sanitizer",
|
||||
"executable",
|
||||
"linked",
|
||||
"kept",
|
||||
"binary",
|
||||
"dsym",
|
||||
"coverage",
|
||||
|
||||
@@ -407,7 +407,9 @@ export class NativeCompiler {
|
||||
ffi,
|
||||
executablePack,
|
||||
);
|
||||
const restored = executableCache?.restore(stagedOutput) ?? false;
|
||||
// An unchanged rebuild keeps the executable the last build left.
|
||||
const kept = executableCache?.keep(output) ?? false;
|
||||
const restored = kept || (executableCache?.restore(stagedOutput) ?? false);
|
||||
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
|
||||
if (!restored) {
|
||||
let programObjects = [object];
|
||||
@@ -524,7 +526,10 @@ export class NativeCompiler {
|
||||
irPath = join(options.outDir, `${stem}.ir.json`);
|
||||
writeTextPartsSync(irPath, prepared.ir!);
|
||||
}
|
||||
renameSync(stagedOutput, output);
|
||||
if (!kept) {
|
||||
renameSync(stagedOutput, output);
|
||||
executableCache?.recordOutput(output);
|
||||
}
|
||||
if (target.platform === "darwin" && (optimization !== "dev" || options.strip))
|
||||
rmSync(output + ".dSYM", { recursive: true, force: true });
|
||||
return {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user