mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Merge main
This commit is contained in:
@@ -172,7 +172,9 @@ jobs:
|
||||
SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }}
|
||||
# Match macOS, where Apple ASan has no LeakSanitizer: the RC audit owns
|
||||
# leak detection, including intentional abandonment. Plain shards also
|
||||
# build some sanitized probes, so this applies to both flavors.
|
||||
# build some sanitized probes, so this applies to both flavors. The
|
||||
# san shards' case-sharded corpus turns LeakSanitizer on
|
||||
# (scripts/ci-test-plan.mjs, tests/harness/lsan.supp).
|
||||
ASAN_OPTIONS: detect_leaks=0
|
||||
# Within-file partition for the large case-driven harnesses
|
||||
# (differential, llvm-differential, npm, server, Test262, coverage, and
|
||||
|
||||
@@ -18,6 +18,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
|
||||
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
|
||||
|
||||
@@ -147,6 +147,13 @@
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "channel-messages",
|
||||
"entry": "workloads/channel-messages.ts",
|
||||
"args": [
|
||||
"200000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atomics-contended",
|
||||
"entry": "workloads/atomics-contended.ts",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// MessageChannel messages within one thread: the program posts the given
|
||||
// number of messages, half of them numbers and half small objects, before
|
||||
// the port delivers any, so all of them are queued at once. Argument: the
|
||||
// number of messages.
|
||||
import { MessageChannel } from "node:worker_threads";
|
||||
|
||||
const count = Number(process.argv[2] ?? "200000");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
let received = 0;
|
||||
let sum = 0;
|
||||
port1.on("message", (value: number | { id: number; name: string }) => {
|
||||
sum += typeof value === "number" ? value : value.id + value.name.length;
|
||||
if (++received === count) {
|
||||
console.log(`${count} messages, sum ${sum}`);
|
||||
port1.close();
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (i % 2 === 0) port2.postMessage(i);
|
||||
else port2.postMessage({ id: i, name: "item" });
|
||||
}
|
||||
@@ -18,6 +18,10 @@
|
||||
The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end.
|
||||
In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself.
|
||||
`rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`.
|
||||
`rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node.
|
||||
In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported.
|
||||
In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`.
|
||||
A terminal interface's prompt that is an object or a symbol, `getPrompt()` while the prompt is not a string, and `getCursorPos()` with an object or symbol prompt are unsupported.
|
||||
`readline.cursorTo`, `moveCursor`, `clearLine`, and `clearScreenDown` write to `process.stdout`, `process.stderr`, a writable stream, or nothing for `null` and `undefined`; their callbacks take no parameters.
|
||||
The `readline/promises` `Readline` class writes to the same outputs.
|
||||
`readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences.
|
||||
|
||||
@@ -58969,7 +58969,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59001,7 +59003,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59229,7 +59233,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59290,7 +59296,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59322,7 +59330,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59405,7 +59415,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:readline.createInterface",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59544,7 +59555,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:rl.close",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59574,7 +59586,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59604,7 +59618,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59634,7 +59650,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59664,7 +59682,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59694,7 +59714,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59724,7 +59746,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59754,7 +59778,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59784,7 +59810,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59904,7 +59932,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59934,7 +59964,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59964,7 +59996,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -60022,7 +60056,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -60052,7 +60088,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82492,7 +82492,10 @@
|
||||
"tests/corpus/5791-readline-keypress-interfaces.cjs",
|
||||
"tests/corpus/5792-readline-keypress-listeners.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -82546,7 +82549,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82577,7 +82582,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82797,7 +82804,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82879,7 +82888,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82910,7 +82921,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82990,7 +83003,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:readline.createInterface",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83146,7 +83160,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:rl.close",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83175,7 +83190,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83204,7 +83221,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83233,7 +83252,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83262,7 +83283,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83291,7 +83314,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83320,7 +83345,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83349,7 +83376,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83378,7 +83407,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83494,7 +83525,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83523,7 +83556,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83552,7 +83587,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83608,7 +83645,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83637,7 +83676,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
|
||||
@@ -199,13 +199,13 @@
|
||||
{"chapter": "events", "symbols": ["CustomEvent", "detail"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts"]},
|
||||
{"chapter": "events", "symbols": ["events.errorMonitor"], "status": "partial", "evidence": ["tests/corpus/5134-events-error-monitor.ts", "tests/corpus/5136-events-error-monitor-string-name.ts", "tests/corpus/5137-events-error-monitor-computed-name.js", "tests/corpus/5138-events-error-monitor-shared-listener.ts"]},
|
||||
{"chapter": "events", "symbols": ["events.addAbortListener"], "status": "partial", "evidence": ["tests/corpus/5133-abort-signal-event-target.ts"]},
|
||||
{"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["'history'", "rl.line"], "status": "partial", "evidence": ["tests/corpus/5780-readline-history-options.ts", "tests/corpus/5781-readline-history-options.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["'SIGINT'", "'SIGTSTP'", "'SIGCONT'", "line", "cursor", "completer"], "status": "partial", "evidence": ["tests/corpus/5782-readline-terminal-editing.ts", "tests/corpus/5783-readline-terminal-completion.cjs", "tests/corpus/5784-readline-terminal-promises.ts", "tests/corpus/5785-readline-terminal-keys.ts", "tests/corpus/5786-readline-terminal-history.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readline.cursorTo", "readline.moveCursor", "readline.clearLine", "readline.clearScreenDown", "rl.getCursorPos"], "status": "partial", "evidence": ["tests/corpus/5787-readline-cursor-helpers.cjs", "tests/corpus/5789-readline-cursor-pos.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readlinePromises.Readline", "rl.clearLine", "rl.clearScreenDown", "rl.commit", "rl.cursorTo", "rl.moveCursor", "rl.rollback"], "status": "partial", "evidence": ["tests/corpus/5788-readline-promises-readline.ts", "tests/corpus/5789-readline-cursor-pos.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readline.emitKeypressEvents"], "status": "partial", "evidence": ["tests/corpus/5790-readline-keypress-events.ts", "tests/corpus/5791-readline-keypress-interfaces.cjs", "tests/corpus/5792-readline-keypress-listeners.ts"]},
|
||||
{"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts"]},
|
||||
{"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]},
|
||||
{"chapter": "stream", "symbols": ["stream.addAbortSignal"], "status": "partial", "evidence": ["tests/corpus/5300-stream-add-abort-signal.ts", "tests/corpus/5301-stream-add-abort-signal-errors.cjs"]},
|
||||
{"chapter": "stream", "symbols": ["readable.map", "readable.filter", "readable.forEach", "readable.toArray", "readable.some", "readable.find", "readable.every", "readable.flatMap", "readable.drop", "readable.take", "readable.reduce"], "status": "partial", "evidence": ["tests/corpus/5335-stream-operators-values.ts", "tests/corpus/5336-stream-operators-promise-timing.ts", "tests/corpus/5337-stream-operators-concurrency.ts", "tests/corpus/5338-stream-operators-checks.cjs"]},
|
||||
{"chapter": "stream", "symbols": ["stream.Readable.from"], "status": "partial", "evidence": ["tests/corpus/5340-readable-from-options.ts", "tests/corpus/5341-readable-from-dynamic.cjs", "tests/corpus/5420-readable-from-generators.ts", "tests/corpus/5421-readable-from-generators.cjs", "tests/corpus/5610-readable-from-sets-and-views.ts", "tests/corpus/5611-readable-from-class-iterables.ts", "tests/corpus/5612-readable-from-iterables.cjs"]},
|
||||
@@ -348,7 +348,7 @@
|
||||
{ "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fsPromises.realpath(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2940-fsp-realpath.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fsPromises.readdir(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2942-fsp-readdir-dirent.ts"] },
|
||||
{ "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts", "tests/corpus/5876-readline-dispose-instanceof.ts"] },
|
||||
{ "chapter": "timers", "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`readable[Symbol.asyncIterator]()`", "`readable.iterator([options])`"], "status": "partial", "evidence": ["tests/corpus/1746-stream-for-await.ts", "tests/corpus/2861-readable-iterator-close.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`writable.destroy([error])`"], "status": "partial", "evidence": ["tests/corpus/2897-child-stdin-destroy.ts"] },
|
||||
@@ -416,7 +416,7 @@
|
||||
{ "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] },
|
||||
{ "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
{ "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] },
|
||||
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts", "tests/corpus/5878-readline-dynamic-streams.mjs"] },
|
||||
{ "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
|
||||
{ "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] }
|
||||
]
|
||||
|
||||
+10
-2
@@ -4006,7 +4006,10 @@ declare module "readline" {
|
||||
meta?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
export interface Interface extends Disposable {
|
||||
export class Interface implements Disposable {
|
||||
/* createInterface creates interfaces; instanceof tells the two
|
||||
* modules' classes apart. */
|
||||
protected constructor();
|
||||
/* terminal says whether the interface edits lines as a terminal;
|
||||
* line and cursor are the terminal's (line is '' otherwise). history
|
||||
* is the history option itself (or the interface's own empty array);
|
||||
@@ -4121,7 +4124,10 @@ declare module "readline/promises" {
|
||||
/* The readline Interface with the promise question: it resolves with
|
||||
* the next line, rejects with an AbortError when options.signal aborts,
|
||||
* and with ERR_USE_AFTER_CLOSE after close(). */
|
||||
export interface Interface extends Disposable {
|
||||
export class Interface implements Disposable {
|
||||
/* createInterface creates interfaces; instanceof tells the two
|
||||
* modules' classes apart. */
|
||||
protected constructor();
|
||||
/* terminal says whether the interface edits lines as a terminal;
|
||||
* line and cursor are the terminal's (line is '' otherwise). history
|
||||
* is the history option itself (or the interface's own empty array);
|
||||
@@ -5238,6 +5244,8 @@ declare module "tls" {
|
||||
getProtocol(): string | null;
|
||||
/* OpenSSL's suite name, the IANA name and the suite's version */
|
||||
getCipher(): CipherNameAndProtocol;
|
||||
/* whether the handshake resumed a cached session */
|
||||
isSessionReused(): boolean;
|
||||
}
|
||||
export interface CipherNameAndProtocol {
|
||||
name: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitTruncF64 } from "./trunc.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
|
||||
/** Supported LLVM targets are little-endian. All memory accesses use align
|
||||
* 1: Buffer fields and DataView windows can start at arbitrary byte offsets. */
|
||||
@@ -79,7 +80,7 @@ export function emitByteNumber(
|
||||
capacity = B.tmp();
|
||||
room = B.tmp();
|
||||
B.line(`${lengthPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`);
|
||||
B.line(`${length} = load ${size}, ptr ${lengthPtr}`);
|
||||
B.line(`${length} = load ${size}, ptr ${lengthPtr}${bytesHeaderAlias(host)}`);
|
||||
B.line(`${room} = icmp uge ${size} ${length}, ${spec.width}`);
|
||||
B.line(`${capacity} = sub ${size} ${length}, ${spec.width}`);
|
||||
if (integer) {
|
||||
@@ -221,14 +222,14 @@ export function emitByteNumber(
|
||||
}
|
||||
}
|
||||
const stored = endianBits(host, raw, spec.width, little);
|
||||
B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1`);
|
||||
B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1${bytesElementAlias(host)}`);
|
||||
if (!spec.dataView) {
|
||||
fastResult = B.tmp();
|
||||
B.line(`${fastResult} = fadd double ${offset.name}, ${f64Lit(spec.width)}`);
|
||||
}
|
||||
} else {
|
||||
const loaded = B.tmp();
|
||||
B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1`);
|
||||
B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1${bytesElementAlias(host)}`);
|
||||
const raw = endianBits(host, loaded, spec.width, little);
|
||||
fastResult = B.tmp();
|
||||
if (spec.floating) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** Alias categories of typed-array storage. A ScrBytes header (length,
|
||||
* data pointer, shared backing) is fixed at construction and only ever
|
||||
* written by runtime code, never by emitted code. Element storage is one
|
||||
* category for every element type: views of different types may share a
|
||||
* buffer. Neither overlaps the other, a native field, array storage or a
|
||||
* module global's slot, so element stores leave header loads and global
|
||||
* receiver slots loop-invariant. Untagged accesses and calls still alias
|
||||
* every category. */
|
||||
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:header");
|
||||
}
|
||||
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:elements");
|
||||
}
|
||||
|
||||
/** A module global's slot is its own category for whole-slot loads of a
|
||||
* scalar or typed-array binding. Stores stay untagged (and so alias it). */
|
||||
export function globalSlotAlias(host: LlvmEmitterContext, id: string, type: string): string {
|
||||
return type === "bytes" || type === "f64" ? host.memoryAliasAttachment(`global:${id}`) : "";
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { matchIntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import { matchBytesLoopVersion, type BytesLoopVersion } from "./bytes-loop-versions.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
const loc = { file: "versions.ts", start: 0, end: 0 };
|
||||
const f64s: IrType = { kind: "bytes", elem: "f64" };
|
||||
const i32s: IrType = { kind: "bytes", elem: "i32" };
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string, type: IrType = F64): IrExpr => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
const bin = (op: string, left: IrExpr, right: IrExpr): IrExpr =>
|
||||
({ kind: "bin", op, left, right, type: F64, loc }) as IrExpr;
|
||||
const get = (receiver: IrExpr, index: IrExpr): IrExpr => ({
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver,
|
||||
args: [index],
|
||||
invalidNaN: true,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const set = (arr: IrExpr, index: IrExpr, value: IrExpr): IrStmt => ({
|
||||
kind: "bytesSet",
|
||||
arr,
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const counted = (
|
||||
body: IrStmt[],
|
||||
limit: IrExpr = ref("n"),
|
||||
counter = "i",
|
||||
): IrStmt & { kind: "for" } => ({
|
||||
kind: "for",
|
||||
init: { kind: "varDecl", localId: counter, init: num(0), loc },
|
||||
cond: { kind: "bin", op: "<", left: ref(counter), right: limit, type: BOOL, loc },
|
||||
update: { kind: "assign", localId: counter, value: bin("+", ref(counter), num(1)), loc },
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
const local = (
|
||||
id: string,
|
||||
type: IrType,
|
||||
mutable = false,
|
||||
extra: Partial<IrLocal> = {},
|
||||
): IrLocal => ({
|
||||
id,
|
||||
name: id,
|
||||
type,
|
||||
mutable,
|
||||
...extra,
|
||||
});
|
||||
|
||||
function fn(body: IrStmt[], extra: IrLocal[] = []): IrFunction {
|
||||
return {
|
||||
name: "kernel",
|
||||
params: [
|
||||
{ localId: "out", name: "out", type: f64s },
|
||||
{ localId: "src", name: "src", type: f64s },
|
||||
{ localId: "table", name: "table", type: i32s },
|
||||
{ localId: "n", name: "n", type: F64 },
|
||||
{ localId: "k", name: "k", type: F64 },
|
||||
],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
local("out", f64s),
|
||||
local("src", f64s),
|
||||
local("table", i32s),
|
||||
local("n", F64),
|
||||
local("k", F64),
|
||||
local("i", F64, true),
|
||||
local("j", F64, true),
|
||||
...extra,
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function match(
|
||||
f: IrFunction,
|
||||
loop: IrStmt & { kind: "for" },
|
||||
captured: string[] = [],
|
||||
): BytesLoopVersion | null {
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
const locals = new Map(f.locals.map((l) => [l.id, l]));
|
||||
return matchBytesLoopVersion(loop, matchIntegerCountedForLoop(loop, locals, ranges), {
|
||||
locals,
|
||||
invariantGlobals: new Set(),
|
||||
captured: new Set(captured),
|
||||
ranges,
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
test("a counted loop's affine accesses get one guard and exact operand ranges", () => {
|
||||
const shifted = bin("+", ref("i"), num(1));
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]);
|
||||
const version = match(fn([loop]), loop)!;
|
||||
expect(version).not.toBeNull();
|
||||
expect(version.bounds.size).toBe(2);
|
||||
expect(version.receivers.map((r) => r.ref.kind === "varRef" && r.ref.localId)).toEqual([
|
||||
"out",
|
||||
"src",
|
||||
]);
|
||||
expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
]);
|
||||
// `n` is unproven: the guard bounds the counter so `i + 1` stays exact.
|
||||
expect(version.counted?.limitGuarded).toBe(true);
|
||||
expect(version.counterBound).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER });
|
||||
});
|
||||
|
||||
test("statically ranged indices compare one constant with the length", () => {
|
||||
const masked = bin("&", bin("^", ref("i"), ref("k")), num(255));
|
||||
const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]);
|
||||
const version = match(fn([loop]), loop)!;
|
||||
expect(version.fixed.map((c) => [version.receivers[c.receiver]!.ref, c.end])).toEqual([
|
||||
[ref("out", f64s), 0],
|
||||
[ref("table", i32s), 255],
|
||||
]);
|
||||
expect(version.affine).toEqual([]);
|
||||
expect(version.counted).toBeNull();
|
||||
});
|
||||
|
||||
test("scaled counters and body temps resolve to their receiver and index", () => {
|
||||
const temps = [local("view", f64s), local("at", F64)];
|
||||
const at = bin("+", bin("*", ref("i"), num(4)), num(3));
|
||||
const loop = counted([
|
||||
{ kind: "varDecl", localId: "view", init: ref("src", f64s), loc },
|
||||
{ kind: "varDecl", localId: "at", init: at, loc },
|
||||
set(ref("view", f64s), ref("at"), num(1)),
|
||||
// (The integer counted loop needs one direct counter index.)
|
||||
set(ref("out", f64s), ref("i"), num(0)),
|
||||
]);
|
||||
const version = match(fn([loop], temps), loop)!;
|
||||
expect(version.receivers.map((r) => r.ref)).toEqual([ref("src", f64s), ref("out", f64s)]);
|
||||
expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([
|
||||
[4, 3, 3],
|
||||
[1, 0, 0],
|
||||
]);
|
||||
expect(version.ranges.has(at)).toBe(true);
|
||||
});
|
||||
|
||||
test("negative factors, rebinding, captures and outer loops are not versioned", () => {
|
||||
// `i * -1` is -0 at zero, so it is not treated as an affine operand.
|
||||
const negative = counted([set(ref("out", f64s), bin("*", ref("i"), num(-1)), num(1))]);
|
||||
expect(match(fn([negative]), negative)).toBeNull();
|
||||
const rebound = counted([
|
||||
set(ref("out", f64s), ref("i"), num(1)),
|
||||
{ kind: "assign", localId: "out", value: ref("src", f64s), loc },
|
||||
]);
|
||||
expect(match(fn([rebound]), rebound)).toBeNull();
|
||||
const plain = counted([set(ref("out", f64s), ref("i"), num(1))]);
|
||||
expect(match(fn([plain]), plain, ["out"])).toBeNull();
|
||||
const inner = counted([set(ref("out", f64s), ref("j"), num(1))], ref("n"), "j");
|
||||
const outer = counted([inner]);
|
||||
expect(match(fn([outer]), outer)).toBeNull();
|
||||
expect(match(fn([outer]), inner)).not.toBeNull();
|
||||
});
|
||||
|
||||
test("emitted loops version the body and tag typed-array memory", () => {
|
||||
const loop = counted([
|
||||
set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))),
|
||||
]);
|
||||
const kernel = fn([loop]);
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const llvm = emitLlvmModule(mod);
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(llvm)![0];
|
||||
expect(body).toContain("bytes.version.fast");
|
||||
expect(body).toContain("bytes.version.slow");
|
||||
// The checked copy keeps its per-element tests; the guard reads lengths.
|
||||
expect(body).toContain("bytes.index.invalid");
|
||||
expect(body).toMatch(/store double [^\n]*, align 1, !tbaa !\d+/);
|
||||
expect(body).toMatch(/load double, ptr [^\n]*, align 1, !tbaa !\d+/);
|
||||
expect(body).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+/);
|
||||
expect(body).toMatch(/load ptr, ptr [^\n]*, !tbaa !\d+/);
|
||||
const elements = /store double [^\n]*!tbaa !(\d+)/.exec(body)![1];
|
||||
const header = /load ptr, ptr [^\n]*!tbaa !(\d+)/.exec(body)![1];
|
||||
expect(elements).not.toBe(header);
|
||||
});
|
||||
|
||||
test("Float16Array accesses version in elements and share the element tag", () => {
|
||||
const f16s: IrType = { kind: "bytes", elem: "f16" };
|
||||
const withHalves = (body: IrStmt[]): IrFunction => {
|
||||
const f = fn(body, [local("half", f16s)]);
|
||||
return { ...f, params: [...f.params, { localId: "half", name: "half", type: f16s }] };
|
||||
};
|
||||
// half[i] = src[i + 1]; out[i] = half[i] * 2
|
||||
const loop = counted([
|
||||
set(ref("half", f16s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))),
|
||||
set(ref("out", f64s), ref("i"), bin("*", get(ref("half", f16s), ref("i")), num(2))),
|
||||
]);
|
||||
const kernel = withHalves([loop]);
|
||||
const version = match(kernel, loop)!;
|
||||
expect(version.bounds.size).toBe(4);
|
||||
expect(version.receivers.map((r) => r.ref)).toEqual([
|
||||
ref("half", f16s),
|
||||
ref("src", f64s),
|
||||
ref("out", f64s),
|
||||
]);
|
||||
// Element offsets against the element count, whatever the element size.
|
||||
expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 0, 0],
|
||||
]);
|
||||
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
expect(fast).toContain("call double @scr_f16_to_f64");
|
||||
expect(fast).toContain("call zeroext i16 @scr_f16_from_f64");
|
||||
expect(fast).not.toContain("bytes.index");
|
||||
const halfStore = /store i16 [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1];
|
||||
const halfLoad = /load i16, ptr [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1];
|
||||
const doubleStore = /store double [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1];
|
||||
// Views of any element type may share a buffer: one element category.
|
||||
expect(halfStore).toBe(doubleStore);
|
||||
expect(halfLoad).toBe(doubleStore);
|
||||
// The checked copy keeps its per-element tests.
|
||||
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
|
||||
});
|
||||
|
||||
test("bodies compiled without optimization keep one checked loop", () => {
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), ref("i")))]);
|
||||
const boxes = Array.from({ length: 512 }, (_, k) => local(`box${k}`, F64, true, { boxed: true }));
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
fn([loop], boxes),
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
expect(body.split("\n", 1)[0]).toContain("optnone");
|
||||
expect(body).not.toContain("bytes.version");
|
||||
expect(body).toContain("bytes.index.invalid");
|
||||
});
|
||||
@@ -0,0 +1,578 @@
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength } from "./expr-bytes.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
|
||||
/** One loop-invariant integer operand of an affine index. */
|
||||
interface Term {
|
||||
expr: IrExpr;
|
||||
/** 1 or -1. */
|
||||
sign: number;
|
||||
}
|
||||
|
||||
interface KeyedTerm extends Term {
|
||||
key: string;
|
||||
}
|
||||
|
||||
/** Every access of `receiver` at a static index range ends below `end + 1`. */
|
||||
interface FixedCheck {
|
||||
receiver: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** Every access of `receiver` at `scale * counter + Σ terms + offset` has an
|
||||
* offset in [minOffset, maxEnd - width + 1]; `maxEnd` includes the width. */
|
||||
interface AffineCheck {
|
||||
receiver: number;
|
||||
scale: number;
|
||||
terms: KeyedTerm[];
|
||||
minOffset: number;
|
||||
maxEnd: number;
|
||||
}
|
||||
|
||||
export interface BytesLoopVersion {
|
||||
/** Typed-array accesses that are in bounds whenever the guard holds. */
|
||||
bounds: ReadonlySet<Node>;
|
||||
/** Root receiver bindings, read once before the loop. */
|
||||
receivers: { ref: VarRef; global: boolean }[];
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** The counted loop whose iteration space the affine checks cover. */
|
||||
counted: IntegerCountedForLoop | null;
|
||||
/** The guard also requires every counter value within [-bound, bound]. */
|
||||
counterBound: number | null;
|
||||
/** Integer ranges of affine index operands that hold in the unchecked
|
||||
* loop (where the guard bounds the counter). */
|
||||
ranges: ReadonlyMap<IrExpr, IntegerRange>;
|
||||
}
|
||||
|
||||
export interface BytesLoopFacts {
|
||||
/** The function's own locals (not module globals). */
|
||||
locals: ReadonlyMap<string, IrLocal>;
|
||||
/** Module globals whose slot no call can replace: stable constants and
|
||||
* the private bindings of a guarded initializer. */
|
||||
invariantGlobals: ReadonlySet<string>;
|
||||
captured: ReadonlySet<string>;
|
||||
ranges: IntegerRanges;
|
||||
/** Locals definitely initialized when the loop is entered. */
|
||||
initialized: ReadonlySet<string>;
|
||||
/** Accesses already proved in bounds without a guard. */
|
||||
proven: ReadonlySet<Node>;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
const MAX_FIXED_END = 2 ** 32;
|
||||
const MAX_CHECKS = 8;
|
||||
const MAX_OFFSET = 2 ** 31;
|
||||
const SAFE = Number.MAX_SAFE_INTEGER;
|
||||
const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]);
|
||||
|
||||
/** Loop versioning for typed-array bounds. A typed array's length and data
|
||||
* pointer are fixed at construction (ScrBytes has no resize, transfer or
|
||||
* detach), so an access is in bounds for the whole loop when its receiver
|
||||
* binding and index operands cannot change while it runs. One test before
|
||||
* the loop then covers every iteration:
|
||||
*
|
||||
* - a statically ranged integer index `[lo, hi]` (lo >= 0) needs
|
||||
* `hi < length`, such as `table[x & 0xff]` or a constant-trip counter;
|
||||
* - an index `k * counter + invariant + c` of a counted loop needs its first
|
||||
* and last values in bounds, such as `out[base + i]` for `i < n` or
|
||||
* `view.getUint32(i * 4)`.
|
||||
*
|
||||
* Only small synchronous loops are versioned (nested loops included), and
|
||||
* the emitter bounds how deeply versions nest. The checked loop runs
|
||||
* whenever the guard fails, and keeps every out-of-bounds read, ignored
|
||||
* write and NaN-index behavior. */
|
||||
export function matchBytesLoopVersion(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
counted: IntegerCountedForLoop | null,
|
||||
facts: BytesLoopFacts,
|
||||
): BytesLoopVersion | null {
|
||||
const written = new Set<string>();
|
||||
const temps = new Map<string, IrExpr>();
|
||||
let cost = 0;
|
||||
const scanExpr = (e: IrExpr): boolean => {
|
||||
if (++cost > MAX_COST) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") written.add(e.localId);
|
||||
return everyExprChild(e, scanExpr, scanStmt);
|
||||
};
|
||||
const scanStmt = (s: IrStmt): boolean => {
|
||||
if (++cost > MAX_COST) return false;
|
||||
switch (s.kind) {
|
||||
case "forOf":
|
||||
written.add(s.localId);
|
||||
break;
|
||||
case "varDecl": {
|
||||
if (temps.has(s.localId) || written.has(s.localId)) temps.delete(s.localId);
|
||||
else if (s.init) temps.set(s.localId, s.init);
|
||||
written.add(s.localId);
|
||||
break;
|
||||
}
|
||||
case "assign":
|
||||
written.add(s.localId);
|
||||
break;
|
||||
case "tryCatch":
|
||||
if (s.catchLocalId) written.add(s.catchLocalId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, scanExpr, scanStmt);
|
||||
};
|
||||
// The guard runs before the initializer, so a binding the initializer
|
||||
// writes is not invariant either: `for (let i = (a = b, 0); ...)` must
|
||||
// not keep the bounds the guard proved for the old `a`.
|
||||
if (
|
||||
(loop.init !== null && !scanStmt(loop.init)) ||
|
||||
!loop.body.every(scanStmt) ||
|
||||
(loop.cond !== null && !scanExpr(loop.cond)) ||
|
||||
(loop.update !== null && !scanStmt(loop.update))
|
||||
)
|
||||
return null;
|
||||
// A body temp stands for its initializer only when it is a constant
|
||||
// binding assigned exactly once, by that declaration.
|
||||
const declared = new Map<string, number>();
|
||||
const countDecls = (s: IrStmt): boolean => {
|
||||
if (s.kind === "varDecl") declared.set(s.localId, (declared.get(s.localId) ?? 0) + 1);
|
||||
return everyStmtChild(s, countExprDecls, countDecls);
|
||||
};
|
||||
const countExprDecls = (e: IrExpr): boolean => everyExprChild(e, countExprDecls, countDecls);
|
||||
loop.body.every(countDecls);
|
||||
const temp = (id: string): IrExpr | null => {
|
||||
const local = facts.locals.get(id);
|
||||
const init = temps.get(id);
|
||||
if (
|
||||
!init ||
|
||||
!local ||
|
||||
local.mutable ||
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
facts.captured.has(id) ||
|
||||
declared.get(id) !== 1
|
||||
)
|
||||
return null;
|
||||
return init;
|
||||
};
|
||||
const counter = counted?.localId ?? null;
|
||||
// A binding keeps its loop-entry value for the whole loop.
|
||||
const invariant = (id: string): boolean => {
|
||||
if (written.has(id) || id === counter) return false;
|
||||
const local = facts.locals.get(id);
|
||||
if (local)
|
||||
return !local.boxed && !local.tdz && !facts.captured.has(id) && facts.initialized.has(id);
|
||||
return facts.invariantGlobals.has(id);
|
||||
};
|
||||
const resolving = new Set<string>();
|
||||
const root = (e: IrExpr): VarRef | null => {
|
||||
if (e.kind !== "varRef" || e.type.kind !== "bytes") return null;
|
||||
const init = temp(e.localId);
|
||||
if (init) {
|
||||
if (resolving.has(e.localId)) return null;
|
||||
resolving.add(e.localId);
|
||||
const result = root(init);
|
||||
resolving.delete(e.localId);
|
||||
return result;
|
||||
}
|
||||
return invariant(e.localId) ? e : null;
|
||||
};
|
||||
// A pure integer operand evaluated again before the loop.
|
||||
const invariantKey = (e: IrExpr): string | null => {
|
||||
if (!facts.ranges.get(e)) return null;
|
||||
if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null;
|
||||
if (e.kind === "varRef")
|
||||
return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null;
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
INVARIANT_OPS.has(e.op) &&
|
||||
e.left.type.kind === "f64" &&
|
||||
e.right.type.kind === "f64"
|
||||
) {
|
||||
const left = invariantKey(e.left),
|
||||
right = invariantKey(e.right);
|
||||
return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
interface Affine {
|
||||
/** The counter's coefficient; 0 when the value is loop-invariant. */
|
||||
counter: number;
|
||||
terms: KeyedTerm[];
|
||||
offset: number;
|
||||
}
|
||||
// Every operand of an affine index is `counter * k + Σ terms + offset`.
|
||||
// Invariant terms need proven ranges. Operands that involve the counter
|
||||
// need none: the guard bounds the counter so that each of them is a safe
|
||||
// integer, which makes the index's double arithmetic exact.
|
||||
const shapes = new Map<IrExpr, Affine | null>();
|
||||
const affine = (e: IrExpr): Affine | null => {
|
||||
if (shapes.has(e)) return shapes.get(e)!;
|
||||
const result = affineShape(e);
|
||||
shapes.set(e, result);
|
||||
return result;
|
||||
};
|
||||
const affineShape = (e: IrExpr): Affine | null => {
|
||||
if (e.kind === "numLit")
|
||||
return Number.isSafeInteger(e.value) && Math.abs(e.value) <= MAX_OFFSET
|
||||
? { counter: 0, terms: [], offset: e.value }
|
||||
: null;
|
||||
if (e.kind === "varRef") {
|
||||
if (e.localId === counter) return { counter: 1, terms: [], offset: 0 };
|
||||
const init = temp(e.localId);
|
||||
if (init) {
|
||||
if (resolving.has(e.localId)) return null;
|
||||
resolving.add(e.localId);
|
||||
const result = affine(init);
|
||||
resolving.delete(e.localId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
(e.op === "+" || e.op === "-") &&
|
||||
e.left.type.kind === "f64" &&
|
||||
e.right.type.kind === "f64"
|
||||
) {
|
||||
const left = affine(e.left),
|
||||
right = affine(e.right);
|
||||
if (left && right && (left.counter === 0 || right.counter === 0)) {
|
||||
const sign = e.op === "+" ? 1 : -1;
|
||||
const offset = left.offset + sign * right.offset;
|
||||
if (Math.abs(offset) > MAX_OFFSET) return null;
|
||||
const terms = [
|
||||
...left.terms,
|
||||
...right.terms.map((t) => ({ expr: t.expr, sign: sign * t.sign, key: t.key })),
|
||||
];
|
||||
const result = { counter: left.counter + sign * right.counter, terms, offset };
|
||||
if (result.counter !== 0 || facts.ranges.get(e)) return result;
|
||||
}
|
||||
}
|
||||
// A positive constant multiple of the counter: `i * 4`, `4 * i`,
|
||||
// `(i + 1) * 8`. (A negative factor turns a zero into -0.)
|
||||
if (e.kind === "bin" && e.op === "*") {
|
||||
const rightLiteral = e.right.kind === "numLit";
|
||||
const scaled = rightLiteral ? e.left : e.right;
|
||||
const literal = rightLiteral ? e.right : e.left;
|
||||
const factor = literal.kind === "numLit" ? literal.value : 0;
|
||||
if (
|
||||
literal.kind === "numLit" &&
|
||||
scaled.type.kind === "f64" &&
|
||||
Number.isSafeInteger(factor) &&
|
||||
factor > 0 &&
|
||||
factor <= MAX_OFFSET
|
||||
) {
|
||||
const inner = affine(scaled);
|
||||
if (inner && inner.counter !== 0 && inner.terms.length === 0) {
|
||||
const counterScale = inner.counter * factor,
|
||||
offset = inner.offset * factor;
|
||||
if (Math.abs(counterScale) <= MAX_OFFSET && Math.abs(offset) <= MAX_OFFSET)
|
||||
return { counter: counterScale, terms: [], offset };
|
||||
}
|
||||
}
|
||||
}
|
||||
const key = invariantKey(e);
|
||||
return key !== null ? { counter: 0, terms: [{ expr: e, sign: 1, key }], offset: 0 } : null;
|
||||
};
|
||||
// The operands of an index that involve the counter.
|
||||
const chain = (e: IrExpr, out: Set<IrExpr>): void => {
|
||||
if (!shapes.get(e)?.counter) return;
|
||||
out.add(e);
|
||||
if (e.kind === "varRef") {
|
||||
const init = temp(e.localId);
|
||||
if (init) chain(init, out);
|
||||
} else if (e.kind === "bin") {
|
||||
chain(e.left, out);
|
||||
chain(e.right, out);
|
||||
}
|
||||
};
|
||||
const termMagnitude = (terms: Term[]): number =>
|
||||
terms.reduce((sum, t) => {
|
||||
const r = facts.ranges.get(t.expr)!;
|
||||
return sum + Math.max(Math.abs(r.min), Math.abs(r.max));
|
||||
}, 0);
|
||||
|
||||
const receivers: { ref: VarRef; global: boolean }[] = [];
|
||||
const receiverIndex = new Map<string, number>();
|
||||
const fixed = new Map<number, { check: FixedCheck; nodes: Node[] }>();
|
||||
const affineChecks = new Map<
|
||||
string,
|
||||
{ check: AffineCheck; nodes: Node[]; operands: Set<IrExpr> }
|
||||
>();
|
||||
const receiverOf = (ref: VarRef): number => {
|
||||
let index = receiverIndex.get(ref.localId);
|
||||
if (index === undefined) {
|
||||
index = receivers.length;
|
||||
receiverIndex.set(ref.localId, index);
|
||||
receivers.push({ ref, global: !facts.locals.has(ref.localId) });
|
||||
}
|
||||
return index;
|
||||
};
|
||||
const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => {
|
||||
if (facts.proven.has(node)) return;
|
||||
const ref = root(array);
|
||||
if (!ref) return;
|
||||
const shape = counter !== null ? affine(index) : null;
|
||||
if (shape && shape.counter !== 0) {
|
||||
const operands = new Set<IrExpr>();
|
||||
chain(index, operands);
|
||||
const terms = [...shape.terms].sort((a, b) =>
|
||||
a.key < b.key ? -1 : a.key > b.key ? 1 : a.sign - b.sign,
|
||||
);
|
||||
const receiver = receiverOf(ref);
|
||||
const key = `${receiver}|${shape.counter}|${terms.map((t) => `${t.sign}${t.key}`).join(",")}`;
|
||||
const end = shape.offset + width - 1;
|
||||
const group = affineChecks.get(key);
|
||||
if (group) {
|
||||
group.check.minOffset = Math.min(group.check.minOffset, shape.offset);
|
||||
group.check.maxEnd = Math.max(group.check.maxEnd, end);
|
||||
group.nodes.push(node);
|
||||
for (const operand of operands) group.operands.add(operand);
|
||||
} else
|
||||
affineChecks.set(key, {
|
||||
check: { receiver, scale: shape.counter, terms, minOffset: shape.offset, maxEnd: end },
|
||||
nodes: [node],
|
||||
operands,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const range = facts.ranges.get(index);
|
||||
if (!range) return;
|
||||
const end = range.max + width - 1;
|
||||
// Only a length a buffer can have: an outer loop must not pay for a
|
||||
// guard that bounds an inner loop's unbounded counter.
|
||||
if (range.min < 0 || end >= MAX_FIXED_END) return;
|
||||
const receiver = receiverOf(ref);
|
||||
const group = fixed.get(receiver);
|
||||
if (group) {
|
||||
group.check.end = Math.max(group.check.end, end);
|
||||
group.nodes.push(node);
|
||||
} else fixed.set(receiver, { check: { receiver, end }, nodes: [node] });
|
||||
};
|
||||
const visitExpr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "bytesIntrinsic") {
|
||||
const numeric = byteNumberAccess(e);
|
||||
if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width);
|
||||
else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1);
|
||||
}
|
||||
return everyExprChild(e, visitExpr, visitStmt);
|
||||
};
|
||||
const visitStmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "bytesSet") access(s, s.arr, s.index, 1);
|
||||
return everyStmtChild(s, visitExpr, visitStmt);
|
||||
};
|
||||
loop.body.every(visitStmt);
|
||||
// Bound the counter so every counter operand stays a safe integer.
|
||||
let bound = SAFE;
|
||||
const affineGroups = [...affineChecks.values()].filter((group) => {
|
||||
let limit = SAFE;
|
||||
for (const operand of group.operands) {
|
||||
const shape = shapes.get(operand)!;
|
||||
const room = SAFE - termMagnitude(shape.terms) - Math.abs(shape.offset);
|
||||
limit = Math.min(limit, Math.floor(room / Math.abs(shape.counter)));
|
||||
}
|
||||
if (limit < 0 || !counted) return false;
|
||||
bound = Math.min(bound, limit);
|
||||
return true;
|
||||
});
|
||||
const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS);
|
||||
const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length);
|
||||
if (fixedKept.length === 0 && affineKept.length === 0) return null;
|
||||
const bounds = new Set<Node>();
|
||||
const used = new Set<number>();
|
||||
const ranges = new Map<IrExpr, IntegerRange>();
|
||||
const counterLow = Math.max(counted?.range.min ?? 0, -bound),
|
||||
counterHigh = Math.min(counted?.range.max ?? 0, bound);
|
||||
for (const group of fixedKept) {
|
||||
used.add(group.check.receiver);
|
||||
for (const node of group.nodes) bounds.add(node);
|
||||
}
|
||||
for (const group of affineKept) {
|
||||
used.add(group.check.receiver);
|
||||
for (const node of group.nodes) bounds.add(node);
|
||||
for (const operand of group.operands) {
|
||||
if (facts.ranges.get(operand)) continue;
|
||||
const shape = shapes.get(operand)!;
|
||||
const a = shape.counter * counterLow,
|
||||
b = shape.counter * counterHigh;
|
||||
let min = Math.min(a, b) + shape.offset,
|
||||
max = Math.max(a, b) + shape.offset;
|
||||
for (const term of shape.terms) {
|
||||
const r = facts.ranges.get(term.expr)!;
|
||||
min += term.sign > 0 ? r.min : -r.max;
|
||||
max += term.sign > 0 ? r.max : -r.min;
|
||||
}
|
||||
if (counterLow <= counterHigh) ranges.set(operand, { min, max });
|
||||
}
|
||||
}
|
||||
// Renumber the receivers the kept checks use.
|
||||
const order = [...used].sort((a, b) => a - b);
|
||||
const renumber = new Map(order.map((old, index) => [old, index]));
|
||||
const needsBound =
|
||||
affineKept.length > 0 &&
|
||||
counted !== null &&
|
||||
(counted.range.min < -bound || counted.range.max > bound);
|
||||
return {
|
||||
bounds,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
})),
|
||||
affine: affineKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
scale: g.check.scale,
|
||||
terms: g.check.terms,
|
||||
minOffset: g.check.minOffset,
|
||||
maxEnd: g.check.maxEnd,
|
||||
})),
|
||||
counted: affineKept.length > 0 ? counted : null,
|
||||
counterBound: needsBound ? bound : null,
|
||||
ranges,
|
||||
};
|
||||
}
|
||||
|
||||
/** Branches to `fast` when every access the version proves is in bounds for
|
||||
* every iteration, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set). */
|
||||
export function emitBytesLoopVersionGuard(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
fast: string,
|
||||
slow: string,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
if (cond === "true") return;
|
||||
const ok = B.newLabel("bytes.version.check");
|
||||
B.condBr(cond, ok, slow);
|
||||
B.startBlock(ok);
|
||||
};
|
||||
const counted = version.counted;
|
||||
if (counted?.guarded) next(emitCountedLoopSafety(host, counted));
|
||||
const receivers = version.receivers.map(({ ref, global }) => {
|
||||
const value = host.emitReadReceiver(ref).name;
|
||||
if (global) {
|
||||
const set = B.tmp();
|
||||
B.line(`${set} = icmp ne ptr ${value}, null`);
|
||||
next(set);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const lengths = receivers.map((receiver) => {
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
if (host.sizeType === "i64") return len;
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
|
||||
return wide;
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
for (const check of version.fixed) {
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`);
|
||||
conditions.push(ok);
|
||||
}
|
||||
if (counted && version.affine.length > 0) {
|
||||
// The first and last counter values, in ascending order. The last value
|
||||
// of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ *
|
||||
// step`. A loop that runs no iteration may compute anything here.
|
||||
const startValue = host.emitExpr(counted.start);
|
||||
const startInteger = exactInteger(host, startValue, counted.start);
|
||||
let start: string;
|
||||
if (startInteger) start = widenInteger(host, startInteger);
|
||||
else {
|
||||
// The counted-loop safety test above proved an exact integer.
|
||||
start = B.tmp();
|
||||
B.line(`${start} = fptosi double ${startValue.name} to i64`);
|
||||
}
|
||||
const limit = emitCountedLoopLimit(host, counted);
|
||||
const step = Math.abs(counted.step);
|
||||
const edge = B.tmp();
|
||||
B.line(`${edge} = add i64 ${limit}, ${counted.step > 0 ? -1 : 1}`);
|
||||
let last = edge;
|
||||
if (step !== 1) {
|
||||
const span = B.tmp(),
|
||||
whole = B.tmp(),
|
||||
covered = B.tmp();
|
||||
last = B.tmp();
|
||||
B.line(
|
||||
counted.step > 0
|
||||
? `${span} = sub i64 ${edge}, ${start}`
|
||||
: `${span} = sub i64 ${start}, ${edge}`,
|
||||
);
|
||||
B.line(`${whole} = sdiv i64 ${span}, ${step}`);
|
||||
B.line(`${covered} = mul i64 ${whole}, ${step}`);
|
||||
B.line(`${last} = ${counted.step > 0 ? "add" : "sub"} i64 ${start}, ${covered}`);
|
||||
}
|
||||
const low = counted.step > 0 ? start : last;
|
||||
const high = counted.step > 0 ? last : start;
|
||||
if (version.counterBound !== null) {
|
||||
const above = B.tmp(),
|
||||
below = B.tmp();
|
||||
B.line(`${above} = icmp sge i64 ${low}, ${-version.counterBound}`);
|
||||
B.line(`${below} = icmp sle i64 ${high}, ${version.counterBound}`);
|
||||
conditions.push(above, below);
|
||||
}
|
||||
for (const check of version.affine) {
|
||||
let base = "0";
|
||||
for (const term of check.terms) {
|
||||
const value = host.emitExpr(term.expr);
|
||||
const integer = exactInteger(host, value, term.expr);
|
||||
if (!integer) {
|
||||
// Unreachable: every term has a proven range. Stay checked.
|
||||
B.br(slow);
|
||||
B.startBlock(B.newLabel("bytes.version.dead"));
|
||||
return;
|
||||
}
|
||||
const wide = widenInteger(host, integer);
|
||||
const sum = B.tmp();
|
||||
B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${wide}`);
|
||||
base = sum;
|
||||
}
|
||||
// Products wrap only for a loop with no iteration: within the
|
||||
// iteration space every scaled counter is a proven safe integer.
|
||||
const scaledLow = scaleCounter(host, low, check.scale),
|
||||
scaledHigh = scaleCounter(host, high, check.scale);
|
||||
const first = B.tmp(),
|
||||
final = B.tmp(),
|
||||
nonNegative = B.tmp(),
|
||||
below = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${first} = add i64 ${check.scale > 0 ? scaledLow : scaledHigh}, ${base}`);
|
||||
B.line(`${final} = add i64 ${check.scale > 0 ? scaledHigh : scaledLow}, ${base}`);
|
||||
const firstOffset = B.tmp(),
|
||||
lastEnd = B.tmp();
|
||||
B.line(`${firstOffset} = add i64 ${first}, ${check.minOffset}`);
|
||||
B.line(`${lastEnd} = add i64 ${final}, ${check.maxEnd}`);
|
||||
B.line(`${nonNegative} = icmp sge i64 ${firstOffset}, 0`);
|
||||
// Lengths are at most 2^53 elements: a signed comparison is exact.
|
||||
B.line(`${below} = icmp slt i64 ${lastEnd}, ${lengths[check.receiver]}`);
|
||||
B.line(`${both} = and i1 ${nonNegative}, ${below}`);
|
||||
conditions.push(both);
|
||||
}
|
||||
}
|
||||
let valid = "true";
|
||||
for (const cond of conditions) {
|
||||
if (valid === "true") valid = cond;
|
||||
else {
|
||||
const all = B.tmp();
|
||||
B.line(`${all} = and i1 ${valid}, ${cond}`);
|
||||
valid = all;
|
||||
}
|
||||
}
|
||||
if (valid === "true") B.br(fast);
|
||||
else B.condBr(valid, fast, slow);
|
||||
}
|
||||
|
||||
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
|
||||
if (scale === 1) return value;
|
||||
const scaled = host.B.tmp();
|
||||
host.B.line(`${scaled} = mul i64 ${value}, ${scale}`);
|
||||
return scaled;
|
||||
}
|
||||
@@ -42,3 +42,54 @@ export function emitCountedLoopLimit(
|
||||
B.line(`${limit} = add nsw i64 ${integer}, ${ascending ? 1 : -1}`);
|
||||
return limit;
|
||||
}
|
||||
|
||||
/** The run-time test of a guarded counted loop: an unproven limit is within
|
||||
* the integer form's finite range and an unproven start is an exact safe
|
||||
* integer. Returns `true` when nothing needs testing. */
|
||||
export function emitCountedLoopSafety(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
): string {
|
||||
const B = host.B;
|
||||
let safe = "true";
|
||||
if (loop.limitGuarded) {
|
||||
const bound = host.emitExpr(loop.limit);
|
||||
safe = B.tmp();
|
||||
B.line(
|
||||
`${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`,
|
||||
);
|
||||
}
|
||||
if (loop.startGuarded) {
|
||||
// An exact integer of at most 2^53 - 1 in magnitude, and not -0
|
||||
// (the bit patterns of the value and its round trip agree).
|
||||
// NaN and infinities fail the ordered range tests.
|
||||
const start = host.emitExpr(loop.start);
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
wide = B.tmp(),
|
||||
back = B.tmp(),
|
||||
bits = B.tmp(),
|
||||
backBits = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
inRange = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
|
||||
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
|
||||
B.line(`${inRange} = and i1 ${low}, ${high}`);
|
||||
B.line(`${raw} = fptosi double ${start.name} to i64`);
|
||||
B.line(`${wide} = freeze i64 ${raw}`);
|
||||
B.line(`${back} = sitofp i64 ${wide} to double`);
|
||||
B.line(`${bits} = bitcast double ${start.name} to i64`);
|
||||
B.line(`${backBits} = bitcast double ${back} to i64`);
|
||||
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
if (safe === "true") safe = ok;
|
||||
else {
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${safe}, ${ok}`);
|
||||
safe = both;
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
@@ -762,7 +762,9 @@ export class LlDyn {
|
||||
B.terminate(`ret i1 true`);
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
if (t.kind !== "dyn") {
|
||||
// A union matches a capsule of another type through its arms below: a
|
||||
// class capsule in a `C | undefined` field matches the `C` arm.
|
||||
if (t.kind !== "dyn" && t.kind !== "union") {
|
||||
const view = this.viewFn(wrapped);
|
||||
this.host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const kind = this.kindOf(B, "%d");
|
||||
@@ -3263,8 +3265,9 @@ export class LlDyn {
|
||||
}
|
||||
B.startBlock(labels.get(DYN_KIND.BYTES)!);
|
||||
{
|
||||
// Buffer-flavored values coerce utf8; plain Uint8Array joins
|
||||
// its elements.
|
||||
// Buffer-flavored values coerce utf8; typed arrays join their
|
||||
// elements and a DataView answers "[object DataView]" (the
|
||||
// runtime's conversion).
|
||||
const bufp = B.tmp();
|
||||
const bufRaw = B.tmp();
|
||||
const isBuf = B.tmp();
|
||||
@@ -3286,10 +3289,9 @@ export class LlDyn {
|
||||
B.line(`call void @scr_str_release(ptr ${txt})`);
|
||||
B.br(done);
|
||||
B.startBlock(lJoin);
|
||||
const bts = this.payloadOf(B, "%d", "ptr");
|
||||
host.declare(`declare ptr @scr_bytes_join(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_to_string(ptr, ptr)`);
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = call ptr @scr_bytes_join(ptr ${bts}, ptr ${host.internLiteral(",")})`);
|
||||
B.line(`${joined} = call ptr @scr_dyn_to_string(ptr %d, ptr null)`);
|
||||
this.putScrStr(B, "%b", joined);
|
||||
B.line(`call void @scr_str_release(ptr ${joined})`);
|
||||
B.br(done);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
import { integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
|
||||
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
|
||||
u8: { kind: 0, le: false },
|
||||
@@ -93,10 +94,7 @@ export function emitBytesIndex(
|
||||
B.line(`${narrow} = trunc i64 ${wide} to ${host.sizeType}`);
|
||||
return narrow;
|
||||
}
|
||||
const lenPtr = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(`${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}`);
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
if (integer) {
|
||||
const wideIndex = widenInteger(host, integer);
|
||||
const wideLength = host.sizeType === "i64" ? len : B.tmp();
|
||||
@@ -145,11 +143,20 @@ export function emitBytesIndex(
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** The element count of a typed array, in the target's size type. */
|
||||
export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string {
|
||||
const p = host.B.tmp();
|
||||
const len = host.B.tmp();
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
return len;
|
||||
}
|
||||
|
||||
export function emitBytesData(host: LlvmEmitterContext, receiver: string): string {
|
||||
const p = host.B.tmp();
|
||||
const data = host.B.tmp();
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 3`);
|
||||
host.B.line(`${data} = load ptr, ptr ${p}`);
|
||||
host.B.line(`${data} = load ptr, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -160,10 +167,7 @@ export function emitBytesLength(
|
||||
bytes: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const p = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${p}`);
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
const size = BYTES_ELEMENT_SIZE[elem];
|
||||
const count = bytes && size !== 1 ? B.tmp() : len;
|
||||
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
|
||||
@@ -187,7 +191,7 @@ function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): strin
|
||||
isShared = B.tmp(),
|
||||
expected = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`);
|
||||
B.line(`${storage} = load ptr, ptr ${slot}`);
|
||||
B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`);
|
||||
B.line(`${isShared} = icmp ne ptr ${storage}, null`);
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`);
|
||||
const shared = B.newLabel("bytes.shared"),
|
||||
@@ -257,9 +261,15 @@ function emitBytesLoad(
|
||||
inBounds: boolean,
|
||||
invalid: string | undefined,
|
||||
): LlValue {
|
||||
if (host.mod.workers) {
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
return emitSharedOrLocalLoad(host, elem, receiver, idx);
|
||||
}
|
||||
// The header is immutable, so the data pointer loads beside the length,
|
||||
// ahead of the bounds branch: an unconditional load LICM can hoist.
|
||||
const data = host.emitBytesData(receiver);
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
if (host.mod.workers) return emitSharedOrLocalLoad(host, elem, receiver, idx);
|
||||
return emitBytesLoadAt(host, elem, receiver, idx);
|
||||
return emitBytesLoadAt(host, elem, data, idx);
|
||||
}
|
||||
|
||||
/** Worker programs: the element at a checked index of a view that may be
|
||||
@@ -285,7 +295,7 @@ function emitSharedOrLocalLoad(
|
||||
const sharedDone = B.newLabel("bytes.shared.done");
|
||||
B.br(sharedDone);
|
||||
B.startBlock(local);
|
||||
const own = emitBytesLoadAt(host, elem, receiver, idx);
|
||||
const own = emitBytesLoadAt(host, elem, host.emitBytesData(receiver), idx);
|
||||
const localDone = B.newLabel("bytes.local.done");
|
||||
B.br(localDone);
|
||||
B.startBlock(localDone);
|
||||
@@ -306,15 +316,15 @@ function emitSharedOrLocalLoad(
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
}
|
||||
|
||||
/** The element at a checked index of a thread-owned view. */
|
||||
/** The element at a checked index of a thread-owned view, given its data
|
||||
* pointer. */
|
||||
function emitBytesLoadAt(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
data: string,
|
||||
idx: string,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const data = host.emitBytesData(receiver);
|
||||
const p = B.tmp();
|
||||
if (elem === "f16") {
|
||||
// Half-precision bits widen exactly; the runtime converts them.
|
||||
@@ -322,7 +332,7 @@ function emitBytesLoadAt(
|
||||
const raw = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load i16, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load i16, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${out} = call double @scr_f16_to_f64(i16 zeroext ${raw})`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
@@ -333,7 +343,7 @@ function emitBytesLoadAt(
|
||||
const wide = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load i${bits}, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load i${bits}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`);
|
||||
B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`);
|
||||
return { name: out, type: F64, uint32: wide };
|
||||
@@ -342,20 +352,20 @@ function emitBytesLoadAt(
|
||||
const raw = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load float, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load float, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${out} = fpext float ${raw} to double`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
if (elem === "f64") {
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${out} = load double, ptr ${p}, align 1`);
|
||||
B.line(`${out} = load double, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
const raw = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load i32, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load i32, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${out} = ${elem === "i32" ? "sitofp" : "uitofp"} i32 ${raw} to double`);
|
||||
return { name: out, type: F64, uint32: raw };
|
||||
}
|
||||
@@ -465,6 +475,7 @@ export function emitBytesSet(
|
||||
const B = host.B;
|
||||
// Invalid integer-indexed writes are ignored after evaluating the RHS.
|
||||
const done = B.newLabel("bytes.store.done");
|
||||
const data = host.emitBytesData(receiver);
|
||||
const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds);
|
||||
if (host.mod.workers) {
|
||||
host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`);
|
||||
@@ -487,7 +498,6 @@ export function emitBytesSet(
|
||||
elem === "f16" || elem === "f32" || elem === "f64"
|
||||
? null
|
||||
: host.emitToUint32(value.name, expr, value.uint32);
|
||||
const data = host.emitBytesData(receiver);
|
||||
const p = B.tmp();
|
||||
if (elem === "f16") {
|
||||
// One round-to-nearest-even step from the double, in the runtime.
|
||||
@@ -495,24 +505,24 @@ export function emitBytesSet(
|
||||
const narrowed = B.tmp();
|
||||
B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`);
|
||||
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i16 ${narrowed}, ptr ${p}, align 1`);
|
||||
B.line(`store i16 ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else if (BYTES_ELEMENT_SIZE[elem] < 4) {
|
||||
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
|
||||
const byte = B.tmp();
|
||||
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
|
||||
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i${bits} ${byte}, ptr ${p}, align 1`);
|
||||
B.line(`store i${bits} ${byte}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else if (elem === "f32") {
|
||||
const narrowed = B.tmp();
|
||||
B.line(`${narrowed} = fptrunc double ${value.name} to float`);
|
||||
B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store float ${narrowed}, ptr ${p}, align 1`);
|
||||
B.line(`store float ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else if (elem === "f64") {
|
||||
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store double ${value.name}, ptr ${p}, align 1`);
|
||||
B.line(`store double ${value.name}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else {
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i32 ${stored!}, ptr ${p}, align 1`);
|
||||
B.line(`store i32 ${stored!}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
}
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
|
||||
@@ -528,7 +528,7 @@ export function emitStableReceiver(
|
||||
if (b.kind !== "boxed") {
|
||||
if (b.kind === "global") host.checkGlobalTdz(receiver.localId);
|
||||
const value = host.B.tmp();
|
||||
host.B.line(`${value} = load ptr, ptr ${b.slot}`);
|
||||
host.B.line(`${value} = load ptr, ptr ${b.slot}${host.slotAlias(receiver.localId)}`);
|
||||
return { name: value, type: receiver.type };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -116,7 +116,7 @@ export function emitLiteralExpr(
|
||||
return host.own({ name: host.adoptHeap(v, e.type), type: e.type });
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}`);
|
||||
B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}${host.slotAlias(e.localId)}`);
|
||||
const integerView = host.integerViews.get(e.localId);
|
||||
if (integerView !== undefined) {
|
||||
// Snapshot both representations before later operands can write
|
||||
@@ -558,6 +558,28 @@ export function regexLiteralTemplate(
|
||||
return `@${re.sym}`;
|
||||
}
|
||||
|
||||
/** A string equality between `s.charAt(i)` and a literal of exactly one
|
||||
* UTF-16 code unit: the charAt call and the unit. */
|
||||
function charAtUnitTest(
|
||||
e: ExprOf<"strEq">,
|
||||
): { charAt: ExprOf<"strIntrinsic">; code: number } | null {
|
||||
let text: string;
|
||||
let other: IrExpr;
|
||||
if (e.right.kind === "strLit") {
|
||||
text = e.right.value;
|
||||
other = e.left;
|
||||
} else if (e.left.kind === "strLit") {
|
||||
text = e.left.value;
|
||||
other = e.right;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (text.length !== 1) return null;
|
||||
if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1)
|
||||
return null;
|
||||
return { charAt: other, code: text.charCodeAt(0) };
|
||||
}
|
||||
|
||||
export function emitStringExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -600,6 +622,26 @@ export function emitStringExpr(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "strEq": {
|
||||
// `s.charAt(i) === "c"` (and `s[i]`, which lowers to charAt) for a
|
||||
// one-unit literal compares the code unit: charCodeAt converts the
|
||||
// index the same way, and an index out of range answers NaN, which
|
||||
// equals no unit, as "" equals no one-unit literal.
|
||||
const unit = charAtUnitTest(e);
|
||||
if (unit !== null) {
|
||||
const code = host.emitExpr({
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver: unit.charAt.receiver,
|
||||
args: unit.charAt.args,
|
||||
type: F64,
|
||||
loc: unit.charAt.loc,
|
||||
});
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
const [l, r] = emitStringInputs(host, [e.left, e.right]);
|
||||
const eq = B.tmp();
|
||||
// A literal side compares its length and bytes inline.
|
||||
|
||||
@@ -83,12 +83,19 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
}
|
||||
// A signal that is not an AbortSignal throws ERR_INVALID_ARG_TYPE.
|
||||
if (mode & 8) host.emitPendingCheck();
|
||||
// readline/promises interfaces are instances of that module's class.
|
||||
if (mode & 32) {
|
||||
host.declare(`declare void @scr_rl_mark_promises(double)`);
|
||||
B.line(`call void @scr_rl_mark_promises(double ${t})`);
|
||||
}
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "rl.question") {
|
||||
if (e.fn === "rl.question" || e.fn === "rl.questionDyn") {
|
||||
// The answer callback MOVES into the interface's registry; throws
|
||||
// Node's use-after-close error (the may-throw seed).
|
||||
// Node's use-after-close error (the may-throw seed), and with a
|
||||
// checked-dynamic query that is not a string, ERR_INVALID_ARG_TYPE.
|
||||
host.usesTimers = true;
|
||||
const fn = e.fn === "rl.question" ? "scr_rl_question" : "scr_rl_question_dyn";
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func");
|
||||
@@ -101,29 +108,37 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
? "scr_rl_answer_thunk_dyn"
|
||||
: "scr_rl_answer_thunk_str";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_rl_question(double, ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @${fn}(double, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_rl_question(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`,
|
||||
`call void @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "rl.questionAsync" || e.fn === "rl.questionAsyncSignal") {
|
||||
// The promise question never throws: closed interfaces and aborted
|
||||
// signals reject the answered promise (+1).
|
||||
if (
|
||||
e.fn === "rl.questionAsync" ||
|
||||
e.fn === "rl.questionAsyncSignal" ||
|
||||
e.fn === "rl.questionAsyncDyn" ||
|
||||
e.fn === "rl.questionAsyncSignalDyn"
|
||||
) {
|
||||
// The promise question never throws: closed interfaces, aborted
|
||||
// signals and queries that are not strings reject the answered
|
||||
// promise (+1).
|
||||
host.usesTimers = true;
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const dyn = e.fn === "rl.questionAsyncDyn" || e.fn === "rl.questionAsyncSignalDyn";
|
||||
const fn = dyn ? "scr_rl_question_promise_dyn" : "scr_rl_question_promise";
|
||||
let signal = "null";
|
||||
let add = "null";
|
||||
if (e.fn === "rl.questionAsyncSignal") {
|
||||
if (e.fn === "rl.questionAsyncSignal" || e.fn === "rl.questionAsyncSignalDyn") {
|
||||
signal = args[2]!.name;
|
||||
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
|
||||
add = "@scr_events_add_abort_listener";
|
||||
}
|
||||
host.declare(`declare ptr @scr_rl_question_promise(double, ptr, ptr, ptr)`);
|
||||
host.declare(`declare ptr @${fn}(double, ptr, ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @scr_rl_question_promise(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`,
|
||||
`${t} = call ptr @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`,
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
@@ -1097,6 +1097,30 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
B.line(`${t} = call ptr @scr_str_from_char_code_one(double ${code.name})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (
|
||||
e.fn === "string.fromCharCode" &&
|
||||
packed.kind === "arrayLit" &&
|
||||
packed.elems.length <= 16 &&
|
||||
!packed.spreads?.length
|
||||
) {
|
||||
// A short argument list passes its codes in a stack array.
|
||||
const codes = packed.elems.map((elem) => host.emitExpr(elem));
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca [${Math.max(codes.length, 1)} x double]`);
|
||||
codes.forEach((code, i) => {
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds [${Math.max(codes.length, 1)} x double], ptr ${slot}, i64 0, i64 ${i}`,
|
||||
);
|
||||
B.line(`store double ${code.name}, ptr ${p}`);
|
||||
});
|
||||
host.declare(`declare ptr @scr_str_from_char_codes(ptr, ${host.sizeType})`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @scr_str_from_char_codes(ptr ${slot}, ${host.sizeType} ${codes.length})`,
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
// One packed f64[] (the frontend built it) or one bytes value (the
|
||||
// spread-typed-array form); +1 string.
|
||||
const base =
|
||||
|
||||
@@ -994,6 +994,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// family have non-generic shapes (adapter/variadic) — special-cased in
|
||||
// emitLibCall.
|
||||
"emitter.new": "scr_emitter_new",
|
||||
"emitter.className": "scr_emitter_class_name",
|
||||
"emitter.ctor": "scr_emitter_init",
|
||||
"emitter.off": "scr_emitter_off",
|
||||
"emitter.offDyn": "scr_emitter_off_dyn",
|
||||
@@ -1039,6 +1040,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"rl.prompt": "scr_rl_prompt",
|
||||
"rl.promptWith": "scr_rl_prompt_with",
|
||||
"rl.setPrompt": "scr_rl_set_prompt",
|
||||
"rl.setPromptDyn": "scr_rl_set_prompt_dyn",
|
||||
"rl.getPrompt": "scr_rl_get_prompt",
|
||||
"rl.write": "scr_rl_write",
|
||||
"rl.writeKey": "scr_rl_write_key",
|
||||
@@ -1054,6 +1056,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"rl.terminal": "scr_rl_terminal",
|
||||
"rl.line": "scr_rl_line",
|
||||
"rl.isTerminal": "scr_rl_is_terminal",
|
||||
"rl.isPromises": "scr_rl_is_promises",
|
||||
"rl.inputFromDyn": "scr_stream_rl_from_dyn",
|
||||
"rl.outputFromDyn": "scr_stream_rl_from_dyn",
|
||||
"rl.cursor": "scr_rl_cursor",
|
||||
"rl.seq": "scr_rl_seq",
|
||||
"rl.moves": "scr_rl_moves",
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
/* LlEmitter's receivers: receiver reads, and the analyses that decide when calls, receivers and returns borrow. */
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrType,
|
||||
isRefCounted,
|
||||
} from "../../ir/ir.js";
|
||||
import type { LlValue } from "./expr-context.js";
|
||||
import { guardedValue } from "./borrowed-receivers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js";
|
||||
import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js";
|
||||
import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
|
||||
import { emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
|
||||
import { borrowableInputs } from "./borrowed-inputs.js";
|
||||
import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js";
|
||||
|
||||
/** Read a receiver whose value is consumed immediately by a field or tag
|
||||
* load. No user code may run between this read and its consumer. Locals
|
||||
* and their projections already have an owner; other expressions keep
|
||||
* their ordinary statement-frame ownership. The caller must retain any
|
||||
* reference result before evaluating another expression. */
|
||||
export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
|
||||
if (!isRefCounted(e.type)) return emitter.emitExpr(e);
|
||||
if (e.kind === "strLit") return { name: emitter.internLiteral(e.value), type: e.type };
|
||||
const guarded = guardedValue(e);
|
||||
if (guarded !== null) {
|
||||
// Throwing guards run in place, exactly as the sequence emits them.
|
||||
for (const s of (e as IrExpr & { kind: "seqExpr" }).stmts) emitter.emitStmt(s);
|
||||
return emitter.emitReadReceiver(guarded);
|
||||
}
|
||||
if (e.kind === "varRef") {
|
||||
emitter.materializeSplitLocal(e.localId);
|
||||
const binding = emitter.binding(e.localId);
|
||||
if (binding.kind === "boxed") {
|
||||
// A projection consumes this pointer before any user code runs.
|
||||
// Keep TDZ checks and caught-value conversion on the owned path.
|
||||
if (!emitter.canBorrowReceiver(e)) return emitter.emitExpr(e);
|
||||
const box = emitter.loadBox(binding.slot);
|
||||
const payload = emitter.B.tmp();
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${payload} = getelementptr inbounds %ScrBox, ptr ${box}, i32 0, i32 5`);
|
||||
emitter.B.line(`${value} = load ptr, ptr ${payload}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (binding.kind === "global") emitter.checkGlobalTdz(e.localId);
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${value} = load ptr, ptr ${binding.slot}${emitter.slotAlias(e.localId)}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "dynCheck" && e.exact) return emitter.emitExactCapsuleObject(e.value, e.type);
|
||||
if (e.kind === "chainRecv") {
|
||||
// The chain's bind slot holds its receiver unchanged until the body
|
||||
// finishes, owned by the slot itself or by the chain's receiver.
|
||||
const bound = emitter.chainSlots.get(e.id);
|
||||
if (!bound)
|
||||
throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${value} = load ${emitter.llType(bound.type)}, ptr ${bound.name}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "arrayGet" && emitter.canBorrowReceiver(e)) {
|
||||
// The array keeps the element alive until the consumer; a missing
|
||||
// element still traps exactly like the owned read.
|
||||
const slot = emitter.B.slot();
|
||||
emitter.B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
emitBorrowedArrayRead(emitter, e, slot);
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${value} = load ptr, ptr ${slot}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
const wrap = emitter.borrowableNullableWrap(e);
|
||||
if (wrap === "unit" && e.kind === "unionWrap")
|
||||
return { name: emitter.unitInstanceRef(e.unionId, e.tag), type: e.type };
|
||||
if (wrap === "ref" && e.kind === "unionWrap")
|
||||
return { name: emitter.emitReadReceiver(e.value).name, type: e.type };
|
||||
const read = matchMapRead(e, emitter.boxedUnionsById);
|
||||
if (read) {
|
||||
const result = emitStackMapRead(emitter, read);
|
||||
if (result.owner) emitter.ownSlot(result.owner.slot, result.owner.type);
|
||||
return result.value;
|
||||
}
|
||||
// A borrowed-return helper (such as an optional element read) lends its
|
||||
// result like any borrowed-return call.
|
||||
if (e.kind === "call" && emitter.borrowedReturns.has(e.callee) && emitter.canBorrowReceiver(e))
|
||||
return emitBorrowedResultCall(emitter, e);
|
||||
const arrayRead = emitter.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead);
|
||||
const narrow = emitter.checkedNarrows.get(e);
|
||||
const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null;
|
||||
if (narrowed) return narrowed;
|
||||
if (canStackUnion(e, emitter.boxedUnionsById)) return emitStackUnion(emitter, e).value;
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = emitter.emitUnionProjection(e.value);
|
||||
emitUnionNarrowGuard(emitter, union.name, e.unionId, e.tag);
|
||||
return { name: emitter.unionPeek(union.name, e.unionId), type: e.type };
|
||||
}
|
||||
if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
|
||||
// Prefix layouts: a class cast reads the operand's own pointer; a
|
||||
// downcast verifies its target class (narrow-guards.ts).
|
||||
const receiver = emitter.emitReadReceiver(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object")
|
||||
emitDowncastGuard(emitter, receiver.name, e.value.type.className, e.type.className);
|
||||
return { name: receiver.name, type: e.type };
|
||||
}
|
||||
// A nullable-pointer field has no box to borrow. Projections use
|
||||
// emitUnionProjection; other borrowers get an owned heap box.
|
||||
if (emitter.nullableFieldGet(e)) return emitter.emitExpr(e);
|
||||
if (e.kind === "recordGet" || e.kind === "fieldGet") {
|
||||
const receiver = emitter.emitReadReceiver(e.obj);
|
||||
if (e.kind === "recordGet") {
|
||||
// An inline union field answers a private stack box.
|
||||
const { ptr, type, inline } = emitter.recordFieldPtr(receiver.name, e.shapeId, e.field);
|
||||
return { name: emitter.loadRecordField(ptr, type, inline), type: e.type };
|
||||
}
|
||||
const { ptr, type, inline } = emitter.classFieldPtr(receiver.name, e.className, e.field);
|
||||
if (emitter.int32Slots.isField(e.className, e.field))
|
||||
return emitter.loadInt32Field(ptr, e.type);
|
||||
return { name: emitter.loadField(ptr, type, inline), type: e.type };
|
||||
}
|
||||
if (
|
||||
e.kind === "ternary" &&
|
||||
emitter.canBorrowReceiver(e.then) &&
|
||||
emitter.canBorrowReceiver(e.else_)
|
||||
) {
|
||||
// Checked projections lower to a ternary with an always-throwing
|
||||
// arm. Keep that check at the use, but borrow the successful arm.
|
||||
const B = emitter.B;
|
||||
const cond = emitter.emitExpr(e.cond);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const yes = B.newLabel("read.then"),
|
||||
no = B.newLabel("read.else"),
|
||||
join = B.newLabel("read.join");
|
||||
B.condBr(cond.name, yes, no);
|
||||
for (const [label, value] of [
|
||||
[yes, e.then],
|
||||
[no, e.else_],
|
||||
] as const) {
|
||||
B.startBlock(label);
|
||||
emitter.frames.push([]);
|
||||
const result = emitter.emitReadReceiver(value);
|
||||
B.line(`store ptr ${result.name}, ptr ${slot}`);
|
||||
emitter.releaseTemps(emitter.frames.pop()!);
|
||||
B.br(join);
|
||||
}
|
||||
B.startBlock(join);
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = load ptr, ptr ${slot}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
// Scalar optional values and register results answer a private stack
|
||||
// box. A ternary of borrowable arms above keeps lending its arm.
|
||||
if (
|
||||
(e.kind === "optChain" ||
|
||||
e.kind === "nullish" ||
|
||||
e.kind === "ternary" ||
|
||||
e.kind === "arrIntrinsic") &&
|
||||
isScalarOptionalSource(e, emitter.unionSources)
|
||||
)
|
||||
return emitScalarOptional(emitter, e);
|
||||
if (e.kind === "call" && emitter.scalarResultCall(e))
|
||||
return emitCallExpr(emitter, e, false, true);
|
||||
if (e.kind === "call" && emitter.canBorrowReceiver(e)) return emitBorrowedResultCall(emitter, e);
|
||||
return emitter.emitExpr(e);
|
||||
}
|
||||
|
||||
/** A local stable binding keeps its value alive while later arguments
|
||||
* and the callee execute. A proven local union box cannot escape or be
|
||||
* rebound during a call; its independent payload owner keeps it alive.
|
||||
* Other writable bindings need an owned snapshot. */
|
||||
export function canBorrowCallArgument(emitter: LlEmitter, value: IrExpr): boolean {
|
||||
if (value.kind === "strLit") return true;
|
||||
// An exact capsule owns its instance while the capsule's binding holds it.
|
||||
if (value.kind === "dynCheck" && value.exact)
|
||||
return value.value.kind === "varRef" && emitter.canBorrowCallArgument(value.value);
|
||||
// An inline union field has no box to lend.
|
||||
if (emitter.inlineFieldGet(value)) return false;
|
||||
// Class casts reinterpret the same pointer (prefix layouts).
|
||||
if ((value.kind === "upcast" || value.kind === "downcast") && value.value.type.kind === "object")
|
||||
return emitter.canBorrowCallArgument(value.value);
|
||||
if (value.kind === "seqExpr") {
|
||||
const guarded = guardedValue(value);
|
||||
return guarded !== null && emitter.canBorrowCallArgument(guarded);
|
||||
}
|
||||
// A nullable-union wrap reinterprets its payload pointer; an immortal
|
||||
// unit constant needs no owner at all.
|
||||
const wrap = emitter.borrowableNullableWrap(value);
|
||||
if (wrap === "unit") return true;
|
||||
if (wrap === "ref" && value.kind === "unionWrap")
|
||||
return emitter.canBorrowCallArgument(value.value);
|
||||
// Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose
|
||||
// failing arm always throws; the successful arm projects the same
|
||||
// stable owner. The throwing arm's typed dummy owns nothing.
|
||||
if (value.kind === "unionNarrow") return emitter.canBorrowCallArgument(value.value);
|
||||
if (value.kind === "libCall") return value.fn === "error.nodeThrow";
|
||||
// A final field never changes outside constructors: while its object is
|
||||
// borrowable, so is the field's value. Nullable-pointer fields have no
|
||||
// box to lend and keep their owning path.
|
||||
if (
|
||||
value.kind === "fieldGet" &&
|
||||
emitter.finalFieldBorrows &&
|
||||
isRefCounted(value.type) &&
|
||||
emitter.finalFields.isFinal(value.className, value.field) &&
|
||||
!emitter.nullableFieldGet(value)
|
||||
)
|
||||
return emitter.canBorrowReceiver(value.obj) && emitter.canBorrowCallArgument(value.obj);
|
||||
if (value.kind === "ternary")
|
||||
return (
|
||||
emitter.canBorrowReceiver(value.then) &&
|
||||
emitter.canBorrowReceiver(value.else_) &&
|
||||
emitter.canBorrowCallArgument(value.then) &&
|
||||
emitter.canBorrowCallArgument(value.else_)
|
||||
);
|
||||
// A construction-only field of a stable owner keeps its value until the
|
||||
// owner dies: no later operand or callee can replace and release it.
|
||||
if ((value.kind === "fieldGet" || value.kind === "recordGet") && emitter.immutableRead(value))
|
||||
return emitter.canBorrowCallArgument(value.obj);
|
||||
// An optional chain's bound receiver stays unchanged until its body
|
||||
// finishes (see emitReadReceiver).
|
||||
if (value.kind === "chainRecv") return emitter.chainSlots.has(value.id);
|
||||
if (value.kind !== "varRef") return false;
|
||||
const binding = emitter.binding(value.localId);
|
||||
// A module constant owns its value for the rest of the program.
|
||||
if (binding.kind === "global") return emitter.stableGlobals.has(value.localId);
|
||||
// So does the environment for a capture nothing ever rebinds.
|
||||
if (binding.kind === "boxed")
|
||||
return emitter.stableCaptureIds.has(value.localId) && emitter.canBorrowReceiver(value);
|
||||
return (
|
||||
binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
(!binding.local.mutable ||
|
||||
emitter.stableCallBindings.has(value.localId) ||
|
||||
emitter.borrowedParameters.has(value.localId) ||
|
||||
emitter.localUnionStorage.has(value.localId)) &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz
|
||||
);
|
||||
}
|
||||
|
||||
export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
switch (e.kind) {
|
||||
case "chainRecv":
|
||||
return emitter.chainSlots.has(e.id);
|
||||
case "varRef": {
|
||||
const binding = emitter.binding(e.localId);
|
||||
return (
|
||||
binding.kind !== "boxed" ||
|
||||
(!binding.local!.tdz &&
|
||||
binding.type.kind === e.type.kind &&
|
||||
e.type.kind !== "caught" &&
|
||||
isRefCounted(e.type))
|
||||
);
|
||||
}
|
||||
case "unionNarrow":
|
||||
// Class casts reinterpret the same pointer (prefix layouts).
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
return emitter.canBorrowReceiver(e.value);
|
||||
// An inline union field has no box to lend: projections read it
|
||||
// through emitReadReceiver's private stack box.
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return !emitter.inlineFieldGet(e) && emitter.canBorrowReceiver(e.obj);
|
||||
case "ternary":
|
||||
return emitter.canBorrowReceiver(e.then) && emitter.canBorrowReceiver(e.else_);
|
||||
case "seqExpr": {
|
||||
const guarded = guardedValue(e);
|
||||
return guarded !== null && emitter.canBorrowReceiver(guarded);
|
||||
}
|
||||
// The runtime always sets a pending exception; its unreachable
|
||||
// typed dummy is null and owns no receiver.
|
||||
case "libCall":
|
||||
return e.fn === "error.nodeThrow";
|
||||
case "unionWrap": {
|
||||
const wrap = emitter.borrowableNullableWrap(e);
|
||||
return wrap === "unit" || (wrap === "ref" && emitter.canBorrowReceiver(e.value));
|
||||
}
|
||||
// A borrowed-return result stays reachable from its borrowed
|
||||
// arguments until the consumer runs. Each argument is lent without a
|
||||
// snapshot, so neither the callee nor a later argument may replace
|
||||
// what it was read from: a callee that preserves every edge cannot,
|
||||
// and any other one must leave the argument's locations unwritten.
|
||||
case "call": {
|
||||
if (!emitter.borrowedReturns.has(e.callee)) return false;
|
||||
if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false;
|
||||
const preserving = emitter.referenceEffects.functions.has(e.callee);
|
||||
if (preserving)
|
||||
return e.args.every((arg) => !isRefCounted(arg.type) || emitter.canBorrowReceiver(arg));
|
||||
if (emitter.referenceWrites === null) return false;
|
||||
const lent = borrowableInputs(
|
||||
emitter,
|
||||
e.args,
|
||||
false,
|
||||
emitter.referenceWrites.summary(e.callee),
|
||||
);
|
||||
return e.args.every((arg, i) => !isRefCounted(arg.type) || lent[i] === true);
|
||||
}
|
||||
// A required element read: the (borrowed) array owns the element
|
||||
// while an edge-preserving index computes.
|
||||
case "arrayGet":
|
||||
return (
|
||||
e.arr.type.kind === "array" &&
|
||||
isRefCounted(e.arr.type.elem) &&
|
||||
emitter.canBorrowReceiver(e.arr) &&
|
||||
emitter.referenceEffects.preserves(e.index)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Functions returning a walk of their borrowed parameters return it
|
||||
* without a reference (+0) from their borrowing body; direct callers
|
||||
* either consume it as a receiver/walk or retain it at once, and the owned
|
||||
* adapter retains before releasing its parameters. Candidates preserve
|
||||
* every heap edge (so the result stays reachable from the arguments),
|
||||
* borrow every reference parameter and return plain pointers. The set
|
||||
* grows to a fixpoint because a call to a borrowed-return function with
|
||||
* walk arguments is itself a walk; walk facts are then recomputed with
|
||||
* the final set, admitting only parameters whose convention borrows. */
|
||||
export function analyzeBorrowedReturns(emitter: LlEmitter): void {
|
||||
const pointer = (type: IrType): boolean => emitter.plainReference(type);
|
||||
const borrowedIds = (fn: IrFunction): Set<string> => {
|
||||
const indexes = emitter.callLifetimes.borrowed.get(fn.name);
|
||||
return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
|
||||
};
|
||||
const hostFor = (fn: IrFunction): WalkBorrowHost => {
|
||||
const allowed = borrowedIds(fn);
|
||||
const host = {
|
||||
pointerLocal: (local: IrLocal) => pointer(local.type),
|
||||
borrowsWithoutOwning: (e: IrExpr) => emitter.borrowsWithoutOwning(e),
|
||||
borrowedReturn: (callee: string) => emitter.borrowedReturns.has(callee),
|
||||
parameterAllowed: (id: string) => allowed.has(id),
|
||||
};
|
||||
return emitter.referenceEffects.functions.has(fn.name)
|
||||
? host
|
||||
: emitter.unwrittenWalkHost(fn, host);
|
||||
};
|
||||
// With ReferenceWrites, a body that writes none of its walks' locations
|
||||
// qualifies like one that preserves every edge.
|
||||
const preserving = [...emitter.fnByName.values()].filter(
|
||||
(fn) => emitter.referenceEffects.functions.has(fn.name) || emitter.referenceWrites !== null,
|
||||
);
|
||||
const candidates = preserving.filter((fn) => {
|
||||
if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures) return false;
|
||||
// A borrowing vtable slot dispatches to the borrowing body (or its
|
||||
// virtual adapter) directly, and virtual callers own the result.
|
||||
if (emitter.implSlotBorrowed(fn.name).size > 0) return false;
|
||||
const indexes = emitter.callLifetimes.borrowed.get(fn.name);
|
||||
return (
|
||||
indexes !== undefined &&
|
||||
indexes.size > 0 &&
|
||||
fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i))
|
||||
);
|
||||
});
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const fn of candidates) {
|
||||
if (emitter.borrowedReturns.has(fn.name)) continue;
|
||||
if (!analyzeWalks(fn, hostFor(fn)).returnsWalk) continue;
|
||||
emitter.borrowedReturns.add(fn.name);
|
||||
// Nested borrowed returns extend the locations a caller depends on.
|
||||
emitter.borrowedReturnKeyCache.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (emitter.borrowedReturns.size === 0) return;
|
||||
for (const fn of preserving) {
|
||||
const walks = analyzeWalks(fn, hostFor(fn)).locals;
|
||||
if (walks.size > 0) emitter.walkBorrowsByFunction.set(fn.name, walks);
|
||||
}
|
||||
}
|
||||
|
||||
export function fieldAliasMetadata(emitter: LlEmitter): string[] {
|
||||
if (emitter.fieldAliasTags.size === 0) return [];
|
||||
const lines = ['!0 = !{!"scriptc native fields"}'];
|
||||
for (const tag of emitter.fieldAliasTags.values()) {
|
||||
lines.push(
|
||||
`!${tag - 1} = !{!"field.${tag}", !0, i64 0}`,
|
||||
`!${tag} = !{!${tag - 1}, !${tag - 1}, i64 0}`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** A private stack union source with an independently owned payload. */
|
||||
export function canStackReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
return emitter.isStackUnionSource(e);
|
||||
}
|
||||
|
||||
/** True for union sources that emitReadReceiver can produce as a private
|
||||
* stack box with an independently owned payload (no heap union). */
|
||||
export function isStackUnionSource(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
if (e.type.kind !== "union") return false;
|
||||
return (
|
||||
canStackUnion(e, emitter.boxedUnionsById) ||
|
||||
matchMapRead(e, emitter.boxedUnionsById) !== null ||
|
||||
emitter.optionalArrayReads.get(e) !== null ||
|
||||
isScalarOptionalSource(e, emitter.unionSources) ||
|
||||
emitter.scalarResultCall(e) ||
|
||||
emitter.inlineFieldGet(e)
|
||||
);
|
||||
}
|
||||
@@ -98,6 +98,218 @@ import { CacheInputsChangedError } from "./session.js";
|
||||
import { ensureRuntimeObjects, stageRuntimeObjects, clearCcCaches } from "./runtime-objects.js";
|
||||
import { localizeLibraryObjects } from "./object-merge.js";
|
||||
|
||||
/** What the runtime-unit table reads: the build options, the target driver,
|
||||
* the units other options imply, and the vendor inputs resolved for this
|
||||
* build. */
|
||||
interface RuntimeUnitContext {
|
||||
opts: CcOptions;
|
||||
driver: ReturnType<typeof resolveCc>;
|
||||
rt: (path: string) => string;
|
||||
rtDir: string;
|
||||
regex: boolean;
|
||||
nativeFetch: boolean;
|
||||
net: boolean;
|
||||
http: boolean;
|
||||
tls: boolean;
|
||||
tlsCa: boolean;
|
||||
cipher: boolean;
|
||||
lreObjects: readonly string[];
|
||||
zlibObjects: readonly string[];
|
||||
tlsArchive: string | null;
|
||||
}
|
||||
|
||||
/** An optional runtime input: runtime sources compiled when `when` holds,
|
||||
* or command-line arguments computed from the build. */
|
||||
interface RuntimeUnit {
|
||||
when: (u: RuntimeUnitContext) => boolean | undefined;
|
||||
sources: readonly string[];
|
||||
args?: (u: RuntimeUnitContext) => readonly string[];
|
||||
}
|
||||
|
||||
const unit = (when: RuntimeUnit["when"], ...sources: string[]): RuntimeUnit => ({ when, sources });
|
||||
const unitArgs = (args: (u: RuntimeUnitContext) => readonly string[]): RuntimeUnit => ({
|
||||
when: () => true,
|
||||
sources: [],
|
||||
args,
|
||||
});
|
||||
|
||||
/** The optional runtime inputs, in link order. The order is part of the
|
||||
* executable: a new unit goes next to the units it references. */
|
||||
const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit(
|
||||
(u) => u.opts.workers,
|
||||
"scr_worker.c",
|
||||
"scr_worker_events.c",
|
||||
"scr_mailbox.c",
|
||||
"scr_message.c",
|
||||
),
|
||||
unit((u) => u.opts.copying, "scr_copying.c"),
|
||||
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
|
||||
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
|
||||
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
|
||||
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
|
||||
// GetUserNameA), iphlpapi (GetAdaptersAddresses behind
|
||||
// os.networkInterfaces), and ws2_32 (inet_ntop there; the socket
|
||||
// units ride the same import). Never present on the default path, so
|
||||
// the historical line cannot change.
|
||||
unitArgs((u) =>
|
||||
targetPlatform(u.driver) === "win32"
|
||||
? [u.rt(join(u.rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"]
|
||||
: [],
|
||||
),
|
||||
// musl deliberately has no libc-identification predefine; resolveCc's
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
unit((u) => isMuslTarget(u.driver), "scr_musl.c"),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
unitArgs((u) =>
|
||||
u.regex
|
||||
? [
|
||||
"-I",
|
||||
vendorEngineDir(),
|
||||
u.rt(join(u.rtDir, "scr_regex.c")),
|
||||
u.rt(join(u.rtDir, "scr_glob.c")),
|
||||
u.rt(join(u.rtDir, "scr_idna.c")),
|
||||
...u.lreObjects,
|
||||
]
|
||||
: [],
|
||||
),
|
||||
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
|
||||
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
|
||||
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
|
||||
unit((u) => u.opts.dc, "scr_dc.c"),
|
||||
unit((u) => u.opts.dynTier, "scr_js_value.c"),
|
||||
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynAsync ||
|
||||
u.opts.dynInvoke ||
|
||||
u.opts.workers ||
|
||||
u.opts.dc ||
|
||||
u.opts.fileHandle ||
|
||||
u.nativeFetch,
|
||||
"scr_async_dyn.c",
|
||||
),
|
||||
// The performance timeline (every perf_hooks user-timing call selects
|
||||
// dynAsync, whose unit carries the timeline's buffer warning).
|
||||
unit((u) => u.opts.dynAsync, "scr_perf.c"),
|
||||
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
|
||||
// their reads as dyn promises).
|
||||
unit((u) => u.opts.dynAsync, "scr_blob.c"),
|
||||
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
|
||||
// libz on the default host-clang build, vendored objects on Zig builds)
|
||||
// also serves the native fetch's gzip decoder — spread exactly once.
|
||||
unitArgs((u) =>
|
||||
u.opts.zlib
|
||||
? isZigDriver(u.driver)
|
||||
? ["-I", vendorZlibDir(), u.rt(join(u.rtDir, "scr_zlib.c")), ...u.zlibObjects]
|
||||
: [u.rt(join(u.rtDir, "scr_zlib.c"))]
|
||||
: u.nativeFetch
|
||||
? isZigDriver(u.driver)
|
||||
? ["-I", vendorZlibDir(), ...u.zlibObjects]
|
||||
: []
|
||||
: [],
|
||||
),
|
||||
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"),
|
||||
unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.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
|
||||
// their exact size class.
|
||||
unit((u) => u.opts.emitter || u.opts.workers || u.net, "scr_dyn_handle.c"),
|
||||
unit((u) => u.opts.symbol, "scr_symbol.c"),
|
||||
unit((u) => u.opts.assert && u.opts.bigint, "scr_bigint_assert.c"),
|
||||
unit((u) => u.opts.qs, "scr_qs.c"),
|
||||
// The legacy url unit references the regex, querystring and dyn-async
|
||||
// units; the url.parse family's IR sets all three.
|
||||
unit((u) => u.regex && u.opts.qs && u.opts.dynAsync, "scr_url_legacy.c"),
|
||||
unit((u) => u.opts.parseArgs, "scr_util.c"),
|
||||
unit((u) => u.opts.parseArgs && u.opts.symbol, "scr_util_compare.c"),
|
||||
unit((u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync, "scr_util_style.c"),
|
||||
unit(
|
||||
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
|
||||
"scr_util_callable.c",
|
||||
),
|
||||
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
|
||||
// The zlib stream classes reference both the zlib engine and the
|
||||
// stream unit.
|
||||
unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"),
|
||||
unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
// epoll on Linux, WSAPoll on Windows — each TU is empty off its
|
||||
// platform, so all three link whenever a poller-using unit does and
|
||||
// the others cost nothing (ws2_32 rides the unconditional win32 libs
|
||||
// above).
|
||||
unit((u) => u.net || u.opts.dgram, "scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c"),
|
||||
unit((u) => u.net, "scr_net.c"),
|
||||
unit((u) => u.http, "scr_http.c"),
|
||||
unit((u) => u.opts.http2, "scr_http2.c"),
|
||||
unit((u) => u.opts.dgram, "scr_dgram.c"),
|
||||
unit((u) => u.opts.watch, "scr_watch.c"),
|
||||
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
|
||||
unit((u) => u.opts.nodeTest, "scr_test.c"),
|
||||
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
|
||||
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
|
||||
// references its default-set override unconditionally.
|
||||
unit((u) => u.tlsCa, "scr_tls_ca.c"),
|
||||
unitArgs((u) =>
|
||||
u.tlsArchive
|
||||
? [
|
||||
"-I",
|
||||
join(vendorTlsDir(), "include"),
|
||||
...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []),
|
||||
...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []),
|
||||
u.tlsArchive,
|
||||
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
|
||||
// The unconditional win32 libs above do not carry it.
|
||||
// Never present on the default path, so the historical TLS
|
||||
// link line cannot change.
|
||||
...(targetPlatform(u.driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
// scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries.
|
||||
// This is independent of the mbedTLS archive: getCACertificates-only
|
||||
// programs need crypt32 too, while TLS programs imply the CA unit.
|
||||
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
unit((u) => u.nativeFetch, "scr_fetch.c"),
|
||||
];
|
||||
|
||||
/** The arguments of the optional runtime inputs, in link order. */
|
||||
function runtimeUnitArgs(u: RuntimeUnitContext): string[] {
|
||||
return RUNTIME_UNITS.flatMap((entry) =>
|
||||
entry.args
|
||||
? [...entry.args(u)]
|
||||
: entry.when(u)
|
||||
? entry.sources.map((source) => u.rt(join(u.rtDir, source)))
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
/** The runtime's optional compile-time definitions, in command-line order.
|
||||
* Both the program build and the runtime-object flags use this list. */
|
||||
const RUNTIME_DEFINES: readonly {
|
||||
define: string;
|
||||
when: (opts: CcOptions) => boolean | undefined;
|
||||
}[] = [
|
||||
{ define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
{ define: "SCR_YIN", when: (opts) => opts.youngNursery },
|
||||
];
|
||||
|
||||
function runtimeDefineArgs(opts: CcOptions): string[] {
|
||||
return RUNTIME_DEFINES.filter((d) => d.when(opts)).map((d) => `-D${d.define}`);
|
||||
}
|
||||
|
||||
export async function compileCInternal(
|
||||
opts: CcOptions,
|
||||
cacheWarmOnly: boolean,
|
||||
@@ -444,10 +656,7 @@ export async function compileCInternal(
|
||||
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
|
||||
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
...runtimeDefineArgs(opts),
|
||||
"-fno-math-errno",
|
||||
// The runtime object model uses type-punned C: a hierarchy
|
||||
// upcast is a raw pointer cast, so one object's header (rc, vt) and
|
||||
@@ -465,146 +674,22 @@ export async function compileCInternal(
|
||||
"-I",
|
||||
rtDir,
|
||||
...runtimeSources.map((f) => rt(join(rtDir, f))),
|
||||
...(opts.workers
|
||||
? ["scr_worker.c", "scr_worker_events.c", "scr_mailbox.c", "scr_message.c"].map((f) =>
|
||||
rt(join(rtDir, f)),
|
||||
)
|
||||
: []),
|
||||
...(opts.copying ? [rt(join(rtDir, "scr_copying.c"))] : []),
|
||||
...(opts.fileHandle ? [rt(join(rtDir, "scr_file_handle.c"))] : []),
|
||||
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
|
||||
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
|
||||
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
|
||||
// GetUserNameA), iphlpapi (GetAdaptersAddresses behind
|
||||
// os.networkInterfaces), and ws2_32 (inet_ntop there; the socket
|
||||
// units ride the same import). Never present on the default path, so
|
||||
// the historical line cannot change.
|
||||
...(targetPlatform(driver) === "win32"
|
||||
? [rt(join(rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"]
|
||||
: []),
|
||||
// musl deliberately has no libc-identification predefine; resolveCc's
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
...(isMuslTarget(driver) ? [rt(join(rtDir, "scr_musl.c"))] : []),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
...(regex
|
||||
? [
|
||||
"-I",
|
||||
vendorEngineDir(),
|
||||
rt(join(rtDir, "scr_regex.c")),
|
||||
rt(join(rtDir, "scr_glob.c")),
|
||||
rt(join(rtDir, "scr_idna.c")),
|
||||
...lreObjects,
|
||||
]
|
||||
: []),
|
||||
...(opts.assert || regex || opts.symbol ? [rt(join(rtDir, "scr_assert.c"))] : []),
|
||||
...(opts.inspect
|
||||
? [rt(join(rtDir, "scr_inspect.c")), rt(join(rtDir, "scr_console_native.c"))]
|
||||
: []),
|
||||
...(opts.dynInvoke || opts.workers || nativeFetch ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
|
||||
...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []),
|
||||
...(opts.dynTier === true ? [rt(join(rtDir, "scr_js_value.c"))] : []),
|
||||
...(opts.eventTarget || nativeFetch ? [rt(join(rtDir, "scr_event_target.c"))] : []),
|
||||
...(opts.dynAsync || opts.dynInvoke || opts.workers || opts.dc || opts.fileHandle || nativeFetch
|
||||
? [rt(join(rtDir, "scr_async_dyn.c"))]
|
||||
: []),
|
||||
// The performance timeline (every perf_hooks user-timing call selects
|
||||
// dynAsync, whose unit carries the timeline's buffer warning).
|
||||
...(opts.dynAsync ? [rt(join(rtDir, "scr_perf.c"))] : []),
|
||||
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
|
||||
// their reads as dyn promises).
|
||||
...(opts.dynAsync ? [rt(join(rtDir, "scr_blob.c"))] : []),
|
||||
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
|
||||
// libz on the default host-clang build, vendored objects on Zig builds)
|
||||
// also serves the native fetch's gzip decoder — spread exactly once.
|
||||
...(opts.zlib
|
||||
? isZigDriver(driver)
|
||||
? ["-I", vendorZlibDir(), rt(join(rtDir, "scr_zlib.c")), ...zlibObjects]
|
||||
: [rt(join(rtDir, "scr_zlib.c"))]
|
||||
: nativeFetch
|
||||
? isZigDriver(driver)
|
||||
? ["-I", vendorZlibDir(), ...zlibObjects]
|
||||
: []
|
||||
: []),
|
||||
|
||||
// through the native installation hooks — the emitted main calls its
|
||||
// installer exactly then.
|
||||
...(opts.events ? [rt(join(rtDir, "scr_events.c")), rt(join(rtDir, "scr_readline.c"))] : []),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
...(opts.events && regex ? [rt(join(rtDir, "scr_readline_tty.c"))] : []),
|
||||
...(opts.emitter || opts.workers ? [rt(join(rtDir, "scr_events_emitter.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
|
||||
// their exact size class.
|
||||
...(opts.emitter || opts.workers || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []),
|
||||
...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []),
|
||||
...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []),
|
||||
...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []),
|
||||
// The legacy url unit references the regex, querystring and dyn-async
|
||||
// units; the url.parse family's IR sets all three.
|
||||
...(regex && opts.qs && opts.dynAsync ? [rt(join(rtDir, "scr_url_legacy.c"))] : []),
|
||||
...(opts.parseArgs ? [rt(join(rtDir, "scr_util.c"))] : []),
|
||||
...(opts.parseArgs && opts.symbol ? [rt(join(rtDir, "scr_util_compare.c"))] : []),
|
||||
...(opts.parseArgs && opts.inspect && opts.dynAsync
|
||||
? [rt(join(rtDir, "scr_util_style.c"))]
|
||||
: []),
|
||||
...(opts.parseArgs && opts.inspect && opts.dynAsync && opts.dynInvoke && regex
|
||||
? [rt(join(rtDir, "scr_util_callable.c"))]
|
||||
: []),
|
||||
...(opts.stream ? [rt(join(rtDir, "scr_stream.c")), rt(join(rtDir, "scr_fs_stream.c"))] : []),
|
||||
// The zlib stream classes reference both the zlib engine and the
|
||||
// stream unit.
|
||||
...(opts.zlib && opts.stream ? [rt(join(rtDir, "scr_zlib_stream.c"))] : []),
|
||||
...(opts.stream && opts.dynAsync ? [rt(join(rtDir, "scr_stream_ops.c"))] : []),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
...(opts.stream && opts.fileHandle ? [rt(join(rtDir, "scr_fs_stream_fh.c"))] : []),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
// epoll on Linux, WSAPoll on Windows — each TU is empty off its
|
||||
// platform, so all three link whenever a poller-using unit does and
|
||||
// the others cost nothing (ws2_32 rides the unconditional win32 libs
|
||||
// above).
|
||||
...(net || opts.dgram
|
||||
? [
|
||||
rt(join(rtDir, "scr_loop_kqueue.c")),
|
||||
rt(join(rtDir, "scr_loop_epoll.c")),
|
||||
rt(join(rtDir, "scr_loop_wsapoll.c")),
|
||||
]
|
||||
: []),
|
||||
...(net ? [rt(join(rtDir, "scr_net.c"))] : []),
|
||||
...(http ? [rt(join(rtDir, "scr_http.c"))] : []),
|
||||
...((opts.http2 ?? false) ? [rt(join(rtDir, "scr_http2.c"))] : []),
|
||||
...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []),
|
||||
...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
|
||||
...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
|
||||
...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
|
||||
...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []),
|
||||
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
|
||||
// references its default-set override unconditionally.
|
||||
...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
|
||||
...(tlsArchive
|
||||
? [
|
||||
"-I",
|
||||
join(vendorTlsDir(), "include"),
|
||||
...(tls ? [rt(join(rtDir, "scr_tls.c"))] : []),
|
||||
...(cipher ? [rt(join(rtDir, "scr_cipher.c"))] : []),
|
||||
tlsArchive,
|
||||
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
|
||||
// The unconditional win32 libs above do not carry it.
|
||||
// Never present on the default path, so the historical TLS
|
||||
// link line cannot change.
|
||||
...(targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
]
|
||||
: []),
|
||||
// scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries.
|
||||
// This is independent of the mbedTLS archive: getCACertificates-only
|
||||
// programs need crypt32 too, while TLS programs imply the CA unit.
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
...(nativeFetch ? [rt(join(rtDir, "scr_fetch.c"))] : []),
|
||||
...runtimeUnitArgs({
|
||||
opts,
|
||||
driver,
|
||||
rt,
|
||||
rtDir,
|
||||
regex,
|
||||
nativeFetch,
|
||||
net,
|
||||
http,
|
||||
tls,
|
||||
tlsCa,
|
||||
cipher,
|
||||
lreObjects,
|
||||
zlibObjects,
|
||||
tlsArchive,
|
||||
}),
|
||||
// A .ll program TU (the LLVM backend) deliberately carries no target
|
||||
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
|
||||
// triple exactly as it does for .c) — silence the -Woverride-module
|
||||
@@ -647,10 +732,7 @@ export async function compileCInternal(
|
||||
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
|
||||
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
...runtimeDefineArgs(opts),
|
||||
"-fno-math-errno",
|
||||
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
|
||||
"-Wno-deprecated-declarations",
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/* Members a type assertion attaches to record values. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { type IrType, typeEquals } from "../../ir/ir.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
|
||||
/** `route as Route & { order: number; internal: boolean }` followed by
|
||||
* `view.internal = true` adds a property to the very object `route`
|
||||
* names: Node has no layouts, so every later read of `route` (its JSON,
|
||||
* its keys, a spread) sees the member. Native records are closed structs,
|
||||
* and the asserted intersection maps to a different struct, so neither a
|
||||
* conversion (a copy that severs the write from `route`) nor an erasure
|
||||
* (a struct without the slot) is faithful.
|
||||
*
|
||||
* This pass finds such assertions before any body lowers and gives each
|
||||
* record arm of the operand's type the asserted members it does not
|
||||
* declare, as optional fields. Values the program builds hold them absent,
|
||||
* which is invisible to every key surface (`in`, Object.keys, JSON,
|
||||
* inspect, spread). The asserted type then refines the stored arms
|
||||
* instead of naming new ones (hosted-narrowing.ts), so the assertion keeps
|
||||
* the value and its writes land in the stored object.
|
||||
*
|
||||
* Only the plain form qualifies: one intersection whose constituents are
|
||||
* the operand's own type and type literals of required property
|
||||
* signatures, over plain data types (object types, aliases and interfaces;
|
||||
* not classes or the standard library's types). Arms that are tuples or
|
||||
* carry an index signature, members whose type does not map, and members
|
||||
* two assertions type differently are left alone: those assertions keep
|
||||
* their exact-shape diagnostics. Assertions whose operand already declares
|
||||
* every member are skipped before any type maps, so programs without the
|
||||
* form intern exactly the types they did before. */
|
||||
export function extendAssertedRecordMembers(
|
||||
lowerer: Lowerer,
|
||||
files: readonly ts.SourceFile[],
|
||||
): void {
|
||||
const additions = new Map<string, Map<string, IrType>>();
|
||||
const conflicts = new Set<string>();
|
||||
const consider = (node: ts.AsExpression | ts.TypeAssertion): void => {
|
||||
let typeNode: ts.TypeNode = node.type;
|
||||
while (ts.isParenthesizedTypeNode(typeNode)) typeNode = typeNode.type;
|
||||
if (!ts.isIntersectionTypeNode(typeNode)) return;
|
||||
const parts = typeNode.types.nodes;
|
||||
const literals = parts.filter((p) => ts.isTypeLiteralNode(p));
|
||||
const bases = parts.filter((p) => !ts.isTypeLiteralNode(p));
|
||||
if (literals.length === 0 || bases.length !== 1) return;
|
||||
const memberNodes: { name: string; type: ts.TypeNode }[] = [];
|
||||
for (const literal of literals) {
|
||||
if (!ts.isTypeLiteralNode(literal)) return;
|
||||
for (const member of literal.members.nodes) {
|
||||
// A required member: the form that writes through the view. An
|
||||
// optional one only reads, and keeps its exact-shape fence.
|
||||
if (!ts.isPropertySignatureDeclaration(member) || !member.type || member.questionToken)
|
||||
return;
|
||||
const name = ts.isIdentifier(member.name)
|
||||
? member.name.text
|
||||
: ts.isStringLiteral(member.name)
|
||||
? member.name.text
|
||||
: null;
|
||||
if (name === null || name.startsWith("%")) return;
|
||||
memberNodes.push({ name, type: member.type });
|
||||
}
|
||||
}
|
||||
// Checker-only gate first: an assertion every constituent of whose
|
||||
// operand type already declares the members (`e as Expr & { kind:
|
||||
// "mul" }`, the refinement idiom) attaches nothing, and mapping its
|
||||
// types here would intern them out of the usual order.
|
||||
const operandTs = lowerer.typeOf(node.expression);
|
||||
const constituents = operandTs.isUnionType() ? ts.constituentTypes(operandTs) : [operandTs];
|
||||
const declares = (t: ts.Type, name: string): boolean =>
|
||||
lowerer.checker.getPropertyOfType(t, name) !== undefined;
|
||||
if (constituents.every((t) => memberNodes.every((m) => declares(t, m.name)))) return;
|
||||
// Plain data types only: object literal types, aliases and interfaces
|
||||
// (not classes, and not the standard library's types).
|
||||
const dataType = (t: ts.Type): boolean => {
|
||||
const symbol = t.getAliasSymbol() ?? t.getSymbol();
|
||||
return (
|
||||
!!symbol &&
|
||||
(symbol.flags & ts.SymbolFlags.Class) === 0 &&
|
||||
symbol.declarations.length > 0 &&
|
||||
symbol.declarations.every(
|
||||
(d) =>
|
||||
!lowerer.isStdlibFile(d.getSourceFile()) &&
|
||||
(ts.isTypeLiteralNode(d) ||
|
||||
ts.isInterfaceDeclaration(d) ||
|
||||
ts.isTypeAliasDeclaration(d)),
|
||||
)
|
||||
);
|
||||
};
|
||||
if (!constituents.every(dataType)) return;
|
||||
const operand = lowerer.mapTypeOf(operandTs);
|
||||
const base = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(bases[0]!));
|
||||
if (!operand || !base || !typeEquals(operand, base)) return;
|
||||
const arms =
|
||||
operand.kind === "union" ? (lowerer.unions.get(operand.unionId)?.arms ?? []) : [operand];
|
||||
if (arms.length === 0) return;
|
||||
const shapeIds: string[] = [];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind !== "record") return;
|
||||
const shape = lowerer.shapes.get(arm.shapeId);
|
||||
if (!shape || shape.tuple || shape.indexValue) return;
|
||||
shapeIds.push(arm.shapeId);
|
||||
}
|
||||
const members: { name: string; type: IrType }[] = [];
|
||||
for (const { name, type } of memberNodes) {
|
||||
if (shapeIds.every((id) => lowerer.shapes.get(id)!.fields.some((f) => f.name === name)))
|
||||
continue;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(type));
|
||||
if (!mapped || mapped.kind === "void") return;
|
||||
const optional = lowerer.withUndefinedArmOf(mapped);
|
||||
if (!optional) return;
|
||||
members.push({ name, type: optional });
|
||||
}
|
||||
for (const shapeId of shapeIds) {
|
||||
const shape = lowerer.shapes.get(shapeId)!;
|
||||
for (const member of members) {
|
||||
if (shape.fields.some((f) => f.name === member.name)) continue;
|
||||
let added = additions.get(shapeId);
|
||||
if (!added) additions.set(shapeId, (added = new Map()));
|
||||
const prior = added.get(member.name);
|
||||
if (prior && !typeEquals(prior, member.type)) conflicts.add(`${shapeId}:${member.name}`);
|
||||
else added.set(member.name, member.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) consider(node);
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
for (const sf of files) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
// Cheap text gate: the form needs an intersection with a type literal.
|
||||
if (!/&\s*\{/.test(sf.text)) continue;
|
||||
visit(sf);
|
||||
}
|
||||
for (const [shapeId, added] of additions) {
|
||||
const extra = [...added]
|
||||
.filter(([name]) => !conflicts.has(`${shapeId}:${name}`))
|
||||
.map(([name, type]) => ({ name, type }));
|
||||
lowerer.shapes.extendShape(shapeId, extra);
|
||||
}
|
||||
if (additions.size > 0) keepStoredTypeOfAssertedGlobals(lowerer, files);
|
||||
}
|
||||
|
||||
/** A module-level `const view = value as T & { member: X }` binding keeps
|
||||
* the stored type of `value` once the members are attached, exactly as a
|
||||
* local binding does (lowerVarDecl): the assertion keeps the object, and
|
||||
* writes through the binding must reach it. Globals took their checker
|
||||
* (or annotated) type during collection, before the members existed; an
|
||||
* annotation the stored type hosts gives way to it the same way. */
|
||||
function keepStoredTypeOfAssertedGlobals(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
|
||||
for (const sf of files) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
for (const statement of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(statement)) continue;
|
||||
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
||||
for (const decl of statement.declarationList.declarations.nodes) {
|
||||
if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isAsExpression(init) && !ts.isTypeAssertion(init)) continue;
|
||||
const global = lowerer.globalOf(decl.name);
|
||||
const stored = lowerer.mapTypeOf(lowerer.typeOf(init.expression));
|
||||
if (!global || !stored || (stored.kind !== "union" && stored.kind !== "record")) continue;
|
||||
if (hostsAll(lowerer, stored, global.type)) global.type = stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, CAUGHT, DYN, F64, type IrExpr, type IrType, STRING } from "../../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
STRING,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
|
||||
"actual",
|
||||
@@ -126,7 +135,13 @@ export function lowerErrorCodeProperty(
|
||||
declared,
|
||||
);
|
||||
}
|
||||
const systemField = ["errno", "syscall", "path", "dest"].includes(expr.name.text);
|
||||
// A program class under the receiver's class can carry its own `code`
|
||||
// (a declared field or a property added through an untyped reference),
|
||||
// which Node reads instead of the errno code: read through the error's
|
||||
// dynamic view, which answers both.
|
||||
const systemField =
|
||||
["errno", "syscall", "path", "dest"].includes(expr.name.text) ||
|
||||
(expr.name.text === "code" && programClassMayOwnCode(lowerer, recvT.className));
|
||||
if (
|
||||
!systemField &&
|
||||
expr.name.text !== "code" &&
|
||||
@@ -162,7 +177,9 @@ export function lowerErrorCodeProperty(
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING),
|
||||
expr.name.text === "code"
|
||||
? lowerer.envValueType()
|
||||
: lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING),
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -184,6 +201,19 @@ export function lowerErrorCodeProperty(
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a class declared in the program, `className` or one of its
|
||||
* subclasses, has its own `code` field or a dynamic property bag. The
|
||||
* runtime error classes keep `code` in the errno slot. */
|
||||
function programClassMayOwnCode(lowerer: Lowerer, className: string): boolean {
|
||||
for (const [name, info] of lowerer.classes) {
|
||||
if (name.startsWith("%")) continue;
|
||||
if (name !== className && !lowerer.isSubclassOf(name, className)) continue;
|
||||
if (info.def.fields.some((f) => f.name === "code" || f.name === DYN_CLASS_PROPERTIES))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether values of `type` are errors: class instances whose class is
|
||||
* %Error or extends it. A subclass of a class that does not extend Error
|
||||
* cannot be an error either, so the answer depends on the static class
|
||||
|
||||
@@ -18,6 +18,7 @@ import { READLINE_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "../surfaces.j
|
||||
import { streamSidesOf } from "../lower-stream.js";
|
||||
import type { IrLibFn } from "../../../ir/builtin-signatures.js";
|
||||
import { voidizedCallback } from "../lower-server.js";
|
||||
import { builtinImportOf } from "./module-bindings.js";
|
||||
|
||||
/** The ambient module declaring `d`, or null. */
|
||||
function ambientModuleOf(d: ts.Node): string | null {
|
||||
@@ -270,6 +271,80 @@ function lowerReadlineUiCall(
|
||||
return { kind: "seqExpr", stmts, result: recv, type: F64, loc };
|
||||
}
|
||||
|
||||
/** The module ("readline" or "readline/promises") whose Interface class
|
||||
* `node` names, or null. */
|
||||
function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | null {
|
||||
const builtin = ts.isIdentifier(node)
|
||||
? builtinImportOf(lowerer, node)
|
||||
: ts.isPropertyAccessExpression(node) && !node.questionDotToken
|
||||
? lowerer.builtinMemberOf(node)
|
||||
: null;
|
||||
if (builtin?.member !== "Interface") return null;
|
||||
return builtin.module === "readline" || builtin.module === "readline/promises"
|
||||
? builtin.module
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `rl instanceof readline.Interface` and `rl instanceof
|
||||
* readlinePromises.Interface` over a readline Interface: an interface is
|
||||
* an instance of the class of the module that created it (Node's
|
||||
* readline/promises Interface does not extend node:readline's). Null for
|
||||
* other instanceof tests. */
|
||||
export function lowerReadlineInstanceof(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
): IrExpr | null {
|
||||
const module = interfaceClassModule(lowerer, expr.right);
|
||||
if (module === null || !isReadlineTyped(lowerer, expr.left)) return null;
|
||||
const loc = locOf(expr);
|
||||
const recv = lowerer.lowerExprExpecting(expr.left, F64);
|
||||
const isPromises: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "rl.isPromises",
|
||||
args: [recv],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if (module === "readline/promises") return isPromises;
|
||||
return { kind: "unary", op: "!", operand: isPromises, type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** `rl[Symbol.dispose]()` on a readline Interface: Node's dispose method
|
||||
* calls close(), which does nothing on a closed interface. Null for other
|
||||
* element-access calls. */
|
||||
export function lowerReadlineDisposeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
const access = call.expression;
|
||||
if (!ts.isElementAccessExpression(access) || access.questionDotToken || call.questionDotToken) {
|
||||
return null;
|
||||
}
|
||||
let key = access.argumentExpression;
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
lowerer.stdlibGlobalMember(key, "Symbol") !== "dispose"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!isReadlineTyped(lowerer, access.expression)) return null;
|
||||
if (call.arguments.nodes.length > 0) {
|
||||
lowerer.noLowering("readline.Interface[Symbol.dispose] with arguments", call);
|
||||
}
|
||||
const recv = lowerer.lowerExprExpecting(access.expression, F64);
|
||||
return { kind: "libCall", fn: "rl.close", args: [recv], type: VOID, loc: locOf(call) };
|
||||
}
|
||||
|
||||
/** A question query or setPrompt argument: the string, or for a value
|
||||
* that need not be one (JavaScript), the checked-dynamic value, which the
|
||||
* runtime writes the way Node's kWriteToOutput does (dyn: true). */
|
||||
function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; dyn: boolean } {
|
||||
if (lowerer.typeOf(node).flags & ts.TypeFlags.StringLike) {
|
||||
return { value: lowerer.lowerExprExpecting(node, STRING), dyn: false };
|
||||
}
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "string") return { value, dyn: false };
|
||||
return { value: lowerer.coerceInto(node, value, DYN), dyn: true };
|
||||
}
|
||||
|
||||
/** Method calls on readline Interface receivers (both modules):
|
||||
* - question(query, cb) — the callback form; question(query, { signal }?)
|
||||
* — the promise form (readline/promises);
|
||||
@@ -303,18 +378,20 @@ export function lowerReadlineMethodCall(
|
||||
args.length <= 2
|
||||
) {
|
||||
const recv = receiver();
|
||||
const query = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
const query = lowerPromptArg(lowerer, args[0]!);
|
||||
const signal = questionSignal(lowerer, args[1]);
|
||||
const type = { kind: "promise", inner: STRING } as const;
|
||||
if (signal === null) {
|
||||
return { kind: "libCall", fn: "rl.questionAsync", args: [recv, query], type, loc };
|
||||
const fn = query.dyn ? "rl.questionAsyncDyn" : "rl.questionAsync";
|
||||
return { kind: "libCall", fn, args: [recv, query.value], type, loc };
|
||||
}
|
||||
const sig = lowerer.lowerExprExpecting(signal, DYN);
|
||||
return { kind: "libCall", fn: "rl.questionAsyncSignal", args: [recv, query, sig], type, loc };
|
||||
const fn = query.dyn ? "rl.questionAsyncSignalDyn" : "rl.questionAsyncSignal";
|
||||
return { kind: "libCall", fn, args: [recv, query.value, sig], type, loc };
|
||||
}
|
||||
if (name === "question" && args.length === 2) {
|
||||
const recv = receiver();
|
||||
const query = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
const query = lowerPromptArg(lowerer, args[0]!);
|
||||
const cb = lowerer.lowerExpr(args[1]!);
|
||||
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) {
|
||||
lowerer.unsupported(
|
||||
@@ -331,7 +408,8 @@ export function lowerReadlineMethodCall(
|
||||
`question callbacks whose parameter is not 'string' (got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "rl.question", args: [recv, query, cb], type: VOID, loc };
|
||||
const fn = query.dyn ? "rl.questionDyn" : "rl.question";
|
||||
return { kind: "libCall", fn, args: [recv, query.value, cb], type: VOID, loc };
|
||||
}
|
||||
const simple: Record<string, "rl.close" | "rl.pause" | "rl.resume" | "rl.prompt"> = {
|
||||
close: "rl.close",
|
||||
@@ -366,8 +444,9 @@ export function lowerReadlineMethodCall(
|
||||
}
|
||||
if (name === "setPrompt" && args.length === 1) {
|
||||
const recv = receiver();
|
||||
const prompt = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return { kind: "libCall", fn: "rl.setPrompt", args: [recv, prompt], type: VOID, loc };
|
||||
const prompt = lowerPromptArg(lowerer, args[0]!);
|
||||
const fn = prompt.dyn ? "rl.setPromptDyn" : "rl.setPrompt";
|
||||
return { kind: "libCall", fn, args: [recv, prompt.value], type: VOID, loc };
|
||||
}
|
||||
if (name === "getPrompt" && args.length === 0) {
|
||||
return { kind: "libCall", fn: "rl.getPrompt", args: [receiver()], type: STRING, loc };
|
||||
@@ -871,6 +950,25 @@ export function lowerReadlineCursorCall(
|
||||
return { kind: "seqExpr", stmts, result, type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** A checked-dynamic input or output (a JavaScript function's result, an
|
||||
* object's member, an element that may be undefined) as the stream it
|
||||
* holds: the runtime refuses a value that is not a stream with that
|
||||
* side, and answers null for an output of null or undefined. Other
|
||||
* values pass through. */
|
||||
function streamFromDyn(lowerer: Lowerer, node: ts.Expression, output: boolean): IrExpr {
|
||||
let v = lowerer.lowerExpr(node);
|
||||
if (v.type.kind === "union") v = lowerer.coerceInto(node, v, DYN);
|
||||
if (v.type.kind !== "dyn") return v;
|
||||
const loc = locOf(node);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: output ? "rl.outputFromDyn" : "rl.inputFromDyn",
|
||||
args: [v, boolLit(output, loc)],
|
||||
type: { kind: "object", className: output ? "%Writable" : "%Readable" },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The interface's input: process.stdin (null) or a readable stream. */
|
||||
function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
if (
|
||||
@@ -879,7 +977,7 @@ function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const v = lowerer.lowerExpr(node);
|
||||
const v = streamFromDyn(lowerer, node, false);
|
||||
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
|
||||
const sides = streamSidesOf(lowerer, info);
|
||||
if (sides !== "r" && sides !== "rw") {
|
||||
@@ -904,7 +1002,7 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" |
|
||||
return "none";
|
||||
}
|
||||
if (ts.isIdentifier(node) && node.text === "undefined") return "none";
|
||||
const v = lowerer.lowerExpr(node);
|
||||
const v = streamFromDyn(lowerer, node, true);
|
||||
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
|
||||
const sides = streamSidesOf(lowerer, info);
|
||||
if (sides !== "w" && sides !== "rw") {
|
||||
@@ -923,8 +1021,8 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" |
|
||||
* stream; the output process.stdout, a writable stream, or absent
|
||||
* (prompts are then not written). rl.create's arguments: the mode literal
|
||||
* (1 stream input, 2 stream output, 4 stdout output, 8 signal, 16
|
||||
* prompt), then the input and output streams when present, crlfDelay,
|
||||
* the prompt and the signal when present. An interface that can be a
|
||||
* prompt, 32 node:readline/promises), then the input and output streams
|
||||
* when present, crlfDelay, the prompt and the signal when present. An interface that can be a
|
||||
* terminal one (a `terminal` option that is not false, or none with
|
||||
* process.stdout as the output, which is a terminal when it is a TTY)
|
||||
* goes through rl.terminal with the completer. */
|
||||
@@ -1107,7 +1205,8 @@ export function lowerReadlineCreateCall(
|
||||
(typeof output === "object" ? 2 : 0) |
|
||||
(output === "stdout" ? 4 : 0) |
|
||||
(signal !== null ? 8 : 0) |
|
||||
(prompt !== null ? 16 : 0);
|
||||
(prompt !== null ? 16 : 0) |
|
||||
(promises ? 32 : 0);
|
||||
const args: IrExpr[] = [numLit(mode, loc)];
|
||||
if (input !== null) args.push(input);
|
||||
if (typeof output === "object") args.push(output);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { type ClassInfo } from "../lower-classes.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../../ir/ir.js";
|
||||
|
||||
/** `x.constructor` on an instance typed as EventEmitter or a stream class
|
||||
* in a TypeScript file: the receiver's runtime ClassInfo, or null. The
|
||||
* runtime records each instance's Node class name (EventEmitter, Readable,
|
||||
* ReadStream, Gzip, Socket, a program subclass's name), which the
|
||||
* lowerings below read with emitter.className. */
|
||||
export function runtimeConstructorReceiver(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { receiver: ts.Expression; info: ClassInfo } | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isPropertyAccessExpression(e) || e.questionDotToken) return null;
|
||||
if (e.name.text !== "constructor" || isJsSourceFile(e.getSourceFile())) return null;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(e.expression));
|
||||
if (mapped?.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(mapped.className);
|
||||
if (!info?.def.runtime || (!info.builtinEmitter && info.builtinStream === undefined)) return null;
|
||||
return { receiver: e.expression, info };
|
||||
}
|
||||
|
||||
/** `x.constructor.name`: the instance's class name. */
|
||||
export function lowerRuntimeConstructorName(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "emitter.className",
|
||||
args: [lowerer.lowerExpr(receiver)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `x.constructor === C` for EventEmitter or a stream class C: true when
|
||||
* the instance's own class is C, which needs both the vtable (C's own
|
||||
* interval, not a subclass's) and the class name (fs, zlib, crypto, child
|
||||
* process and worker streams share the base classes' vtables). */
|
||||
export function lowerRuntimeConstructorIs(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
target: ClassInfo,
|
||||
negated: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const value = lowerer.lowerExpr(receiver);
|
||||
const local = lowerer.declareHiddenLocal("%constructorOf", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
const test = exactClassTest(lowerer, ref, target, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: negated ? { kind: "unary", op: "!", operand: test, type: BOOL, loc } : test,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `x.constructor` as a value: the class object of the instance's own class
|
||||
* among EventEmitter and the stream classes, as the receiver's static
|
||||
* class's class value. Instances of the other runtime classes (fs, zlib,
|
||||
* crypto, child process and worker streams) and of program subclasses
|
||||
* report SC1090 at run time. */
|
||||
export function lowerRuntimeConstructor(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
info: ClassInfo,
|
||||
): IrExpr {
|
||||
const loc = locOf(access);
|
||||
const resultT: IrType = { kind: "classval", className: info.def.name };
|
||||
const value = lowerer.lowerExpr(access.expression);
|
||||
const local = lowerer.declareHiddenLocal("%constructorOf", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
let result: IrExpr = refusalCall(lowerer, resultT, loc);
|
||||
for (const candidate of valueClassesUnder(info)) {
|
||||
lowerer.defineClassValueConstruct(candidate);
|
||||
lowerer.defineClassValueConstruct(info);
|
||||
const classRef: IrExpr = {
|
||||
kind: "classRef",
|
||||
className: candidate.def.name,
|
||||
type: { kind: "classval", className: candidate.def.name },
|
||||
loc,
|
||||
};
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: exactClassTest(lowerer, ref, candidate, loc),
|
||||
then: candidate === info ? classRef : { kind: "upcast", value: classRef, type: resultT, loc },
|
||||
else_: result,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `info` and its runtime descendants whose values construct. */
|
||||
function valueClassesUnder(info: ClassInfo): ClassInfo[] {
|
||||
const out: ClassInfo[] = [];
|
||||
const visit = (c: ClassInfo): void => {
|
||||
if (!c.def.runtime) return;
|
||||
if (c.valueCtorParams !== undefined) out.push(c);
|
||||
for (const s of c.subclasses) visit(s);
|
||||
};
|
||||
visit(info);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The instance is of class `target` itself: inside its interval, outside
|
||||
* each direct subclass's (runtime or program), and named as it. */
|
||||
function exactClassTest(lowerer: Lowerer, ref: IrExpr, target: ClassInfo, loc: SrcLoc): IrExpr {
|
||||
let test: IrExpr = {
|
||||
kind: "instanceOf",
|
||||
value: ref,
|
||||
className: target.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
for (const sub of target.subclasses) {
|
||||
test = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: test,
|
||||
right: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: { kind: "instanceOf", value: ref, className: sub.def.name, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const name: IrExpr = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: { kind: "libCall", fn: "emitter.className", args: [ref], type: STRING, loc },
|
||||
right: { kind: "strLit", value: target.def.name.slice(1), type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return { kind: "logical", op: "&&", left: test, right: name, type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** The run-time refusal for an instance whose class has no class value. */
|
||||
function refusalCall(lowerer: Lowerer, type: IrType, loc: SrcLoc): IrExpr {
|
||||
const name = `%constructor.refusal:${type.kind === "classval" ? type.className : ""}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: type,
|
||||
body: [
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC1090",
|
||||
message:
|
||||
"reading 'constructor' of an fs, zlib, crypto, child process or worker stream, or of an instance of a program subclass of EventEmitter or a stream class, is not supported yet",
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [], type, loc };
|
||||
}
|
||||
@@ -46,12 +46,15 @@ export type ParamMode = "required" | "omittable" | "rest" | "dynRest" | "argumen
|
||||
* otherwise. `bodyType` is present exactly for DEFAULTED program params:
|
||||
* the plain T the body sees after the prologue applies the default (see
|
||||
* declareParams). `callDefault` is the compile-time default value for a
|
||||
* table-backed builtin's omittable slot. */
|
||||
* table-backed builtin's omittable slot. `entryNarrow` marks a dyn ABI slot
|
||||
* inherited from a JavaScript base method whose TypeScript override declares
|
||||
* a typed parameter: the prologue narrows the checked value to that type. */
|
||||
export interface ParamShape {
|
||||
type: IrType;
|
||||
mode: ParamMode;
|
||||
bodyType?: IrType;
|
||||
callDefault?: IrExpr;
|
||||
entryNarrow?: IrType;
|
||||
}
|
||||
|
||||
export interface FnSig {
|
||||
@@ -740,7 +743,11 @@ function appendArgumentsParameter(
|
||||
const classMethod =
|
||||
ts.isMethodDeclaration(node) &&
|
||||
(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent));
|
||||
if (!classMethod && !isNodeEsmFile(node.getSourceFile(), lowerer.program)) {
|
||||
if (
|
||||
!classMethod &&
|
||||
!isNodeEsmFile(node.getSourceFile(), lowerer.program) &&
|
||||
!unmappedArguments(node)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
@@ -751,6 +758,35 @@ function appendArgumentsParameter(
|
||||
shapes.push({ type: DYN, mode });
|
||||
}
|
||||
|
||||
/** Whether `arguments` inside `node` is Node's unmapped object: strict code
|
||||
* (a 'use strict' prologue on the file or an enclosing function, or a class
|
||||
* body) or a non-simple parameter list (defaults, rest, destructuring). Its
|
||||
* elements then never alias the parameters, which is the ES module case. */
|
||||
function unmappedArguments(node: ts.SignatureDeclaration): boolean {
|
||||
if (
|
||||
node.parameters.nodes.some(
|
||||
(p) =>
|
||||
p.initializer !== undefined || p.dotDotDotToken !== undefined || !ts.isIdentifier(p.name),
|
||||
)
|
||||
)
|
||||
return true;
|
||||
const useStrict = (statements: readonly ts.Statement[]): boolean => {
|
||||
for (const statement of statements) {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
|
||||
return false;
|
||||
if (statement.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (let n: ts.Node | undefined = node; n; n = n.parent) {
|
||||
if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return true;
|
||||
if (ts.isFunctionLike(n) && n.body && ts.isBlock(n.body) && useStrict(n.body.statements.nodes))
|
||||
return true;
|
||||
if (ts.isSourceFile(n)) return useStrict(n.statements.nodes);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Signature checks + param shapes + IR func type for any lambda-like
|
||||
* node. The func type's params are the ABI types: optional/default params
|
||||
* become `T | undefined`, and typed rest becomes one packed-array slot.
|
||||
|
||||
@@ -22,13 +22,18 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
isModuleExportsAccess,
|
||||
isRequireStatement,
|
||||
locOf,
|
||||
npmStaticDepSf7,
|
||||
resolveImport,
|
||||
} from "../program.js";
|
||||
import { cjsWholeExportValueOf, inlineProgramRequireOf } from "./cjs-inline-require.js";
|
||||
import { isCjsModuleGlobal } from "./lower-node-module.js";
|
||||
import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js";
|
||||
|
||||
/** True for the CommonJS wrapper's `exports` binding (not shadowed by any
|
||||
@@ -428,3 +433,128 @@ export function cjsExportCallIgnoresThis(
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/** The value behind a binding of a CommonJS member export (an `import {
|
||||
* name }` or a top-level `const { name } = require(...)`) written once as
|
||||
* `module.exports.<name> = <value>` or `exports.<name> = <value>`, when the
|
||||
* module exports a value whole and the member is a property added to it
|
||||
* (`exports = module.exports = Client; exports.StatsD = Client`). The
|
||||
* checker gives such a binding a synthesized property symbol, so the
|
||||
* module's statements answer. The value is a binding of the exporting
|
||||
* module or a primitive literal, which the use site reads again. Follows
|
||||
* `module.exports = require("./x")` re-exports. Null for any other binding. */
|
||||
export function cjsMemberAliasValueOf(lowerer: Lowerer, ref: ts.Identifier): ts.Expression | null {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(ref);
|
||||
if (!sym) return null;
|
||||
const decls = sym.declarations;
|
||||
// `import { StatsD } from "pkg"`
|
||||
const specifier = sym.flags & ts.SymbolFlags.Alias ? decls.find(ts.isImportSpecifier) : undefined;
|
||||
if (specifier !== undefined) {
|
||||
const importDecl = specifier.parent?.parent?.parent;
|
||||
if (
|
||||
importDecl === undefined ||
|
||||
!ts.isImportDeclaration(importDecl) ||
|
||||
!ts.isStringLiteral(importDecl.moduleSpecifier)
|
||||
)
|
||||
return null;
|
||||
const spec = importDecl.moduleSpecifier.text;
|
||||
const from = importDecl.getSourceFile();
|
||||
const dep =
|
||||
resolveImport(lowerer.program, from, spec, "import") ??
|
||||
npmStaticDepSf7(lowerer.program, from, spec, "import");
|
||||
if (dep === null) return null;
|
||||
return memberAliasIn(lowerer, dep, (specifier.propertyName ?? specifier.name).text, 0);
|
||||
}
|
||||
// `const { shout } = require("./lib")` at a module's top level
|
||||
const element = decls.find(ts.isBindingElement);
|
||||
if (element === undefined || element.dotDotDotToken || !ts.isIdentifier(element.name))
|
||||
return null;
|
||||
const pattern = element.parent;
|
||||
const declaration = pattern?.parent;
|
||||
if (
|
||||
pattern === undefined ||
|
||||
!ts.isObjectBindingPattern(pattern) ||
|
||||
declaration === undefined ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
declaration.initializer === undefined ||
|
||||
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
const key = element.propertyName ?? element.name;
|
||||
if (!ts.isIdentifier(key)) return null;
|
||||
const req = inlineProgramRequireOf(lowerer, declaration.initializer);
|
||||
if (req === null) return null;
|
||||
return memberAliasIn(lowerer, req.dep, key.text, 0);
|
||||
}
|
||||
|
||||
function memberAliasIn(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
name: string,
|
||||
depth: number,
|
||||
): ts.Expression | null {
|
||||
const member = memberWritesIn(lowerer, sf, name, depth);
|
||||
// One write: a member written again would be a runtime slot.
|
||||
return member !== null && member.writes === 1 ? member.value : null;
|
||||
}
|
||||
|
||||
/** The writes of member `name` in `sf` and in the modules it re-exports
|
||||
* through `module.exports = require("./x")`, which share its exports
|
||||
* object. `value` is the last write's value when the use site can read
|
||||
* it again. Null when the module does not export a value whole. */
|
||||
function memberWritesIn(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
name: string,
|
||||
depth: number,
|
||||
): { value: ts.Expression | null; writes: number } | null {
|
||||
if (depth > 32 || sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const whole = cjsWholeExportValueOf(sf);
|
||||
if (whole === null) return null;
|
||||
let value: ts.Expression | null = null;
|
||||
let writes = 0;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
if (inner !== null) {
|
||||
// The re-exported module runs first; this module's own writes follow.
|
||||
const dep = memberWritesIn(lowerer, inner.dep, name, depth + 1);
|
||||
if (dep === null) return null;
|
||||
({ value, writes } = dep);
|
||||
}
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind !== "member" || !ts.isIdentifier(cjs.name) || cjs.name.text !== name) continue;
|
||||
writes++;
|
||||
if (cjsExportDiscardReason(stmt) !== null) return null;
|
||||
let written: ts.Expression = cjs.value;
|
||||
while (ts.isParenthesizedExpression(written)) written = written.expression;
|
||||
value = rereadableMemberValue(lowerer, stmt, written) ? written : null;
|
||||
}
|
||||
return { value, writes };
|
||||
}
|
||||
|
||||
/** True when reading `value` again at the use site observes what the
|
||||
* member write `stmt` stored: a primitive literal, or a module binding
|
||||
* declared before the write and never reassigned. */
|
||||
function rereadableMemberValue(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.Statement,
|
||||
value: ts.Expression,
|
||||
): boolean {
|
||||
if (
|
||||
ts.isStringLiteral(value) ||
|
||||
ts.isNumericLiteral(value) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(value) ||
|
||||
value.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
value.kind === ts.SyntaxKind.FalseKeyword
|
||||
)
|
||||
return true;
|
||||
if (!ts.isIdentifier(value)) return false;
|
||||
const sym = lowerer.resolveValueSymbol(value);
|
||||
if (sym === null || sym.declarations.length !== 1) return false;
|
||||
const decl = sym.declarations[0]!;
|
||||
if (decl.getSourceFile() !== stmt.getSourceFile()) return false;
|
||||
// A function declaration is initialized when the module starts; any
|
||||
// other binding must be initialized before the write stores it.
|
||||
if (!ts.isFunctionDeclaration(decl) && decl.end > stmt.pos) return false;
|
||||
return bindingNeverReassigned(lowerer, sym, decl);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
observedPrototypeMethod,
|
||||
reflectClassPrototype,
|
||||
} from "./class-prototypes.js";
|
||||
import { perEvaluationPrivateNames } from "./private-brands.js";
|
||||
import {
|
||||
errorPropertyRead,
|
||||
errorPropertyWrite,
|
||||
@@ -2885,10 +2886,12 @@ export class ClassDynamicDispatch {
|
||||
for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]);
|
||||
for (const [symbol, name] of symbolEntries) {
|
||||
const declaration = symbol.valueDeclaration;
|
||||
// Module-scope consts and CommonJS key-table properties
|
||||
// (classSymbolKeyOfSymbol) both hold their key in a global.
|
||||
if (
|
||||
declaration &&
|
||||
ts.isVariableDeclaration(declaration) &&
|
||||
ts.isIdentifier(declaration.name)
|
||||
((ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name)) ||
|
||||
ts.isPropertyAssignment(declaration))
|
||||
) {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
if (global)
|
||||
@@ -4311,19 +4314,43 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
};
|
||||
if (accessor && info.decl) {
|
||||
const method = classMethodValue(lowerer, info.decl, info, member, loc);
|
||||
if (method) {
|
||||
const call: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectApply",
|
||||
args: [
|
||||
lowerer.coerceToExpected(method, DYN),
|
||||
actualReceiver,
|
||||
{ kind: "dynArrLit", elems: write ? [varRef("p.1", DYN, loc)] : [], type: DYN, loc },
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// An accessor whose private names belong to one evaluation runs as
|
||||
// the instance's own evaluation's prototype accessor.
|
||||
const declarer = accessor.declarer;
|
||||
const prototype = perEvaluationPrivateNames(declarer)
|
||||
? classPrototypeData(lowerer, declarer, loc, upcastTo(lowerer, receiver, declarer.def.name))
|
||||
: null;
|
||||
const method = prototype ? null : classMethodValue(lowerer, info.decl, info, member, loc);
|
||||
if (prototype || method) {
|
||||
const call: IrExpr = prototype
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: write ? "dyn.reflectSet" : "dyn.reflectGet",
|
||||
args: [
|
||||
prototype,
|
||||
lowerer.coerceToExpected(key, DYN),
|
||||
...(write ? [varRef("p.1", DYN, loc)] : []),
|
||||
actualReceiver,
|
||||
],
|
||||
type: write ? BOOL : DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectApply",
|
||||
args: [
|
||||
lowerer.coerceToExpected(method!, DYN),
|
||||
actualReceiver,
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
elems: write ? [varRef("p.1", DYN, loc)] : [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{
|
||||
kind: "if",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js";
|
||||
import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js";
|
||||
import { errorToStringMethod } from "./error-methods.js";
|
||||
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
|
||||
import { perEvaluationPrivateNames } from "./private-brands.js";
|
||||
|
||||
export interface ClassMethodValueSelection {
|
||||
expression: ts.PropertyAccessExpression;
|
||||
@@ -51,18 +52,20 @@ export function lowerClassMethodValue(
|
||||
}
|
||||
const loc = locOf(expr);
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const declared = findMethodOn(lowerer, info, method);
|
||||
// A JS checker may name a class while the value is held in checked
|
||||
// storage. Read its actual property so own replacements and getters
|
||||
// participate, and never pass a dynamic value to a native selector.
|
||||
if (receiver.type.kind === "dyn")
|
||||
// participate, and never pass a dynamic value to a native selector. A
|
||||
// method whose private names belong to one evaluation is that
|
||||
// evaluation's prototype method as well.
|
||||
if (receiver.type.kind === "dyn" || (!!declared && perEvaluationPrivateNames(declared.declarer)))
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: receiver,
|
||||
value: lowerer.coerceToExpected(receiver, DYN),
|
||||
key: { kind: "strLit", value: method, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const declared = findMethodOn(lowerer, info, method);
|
||||
// Abstract declarations have no prototype function. Concrete descendants
|
||||
// provide the extracted value through the same reachability-driven selector.
|
||||
let value = declared?.sig.abstract
|
||||
@@ -537,10 +540,13 @@ export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunctio
|
||||
!(expr.kind === "closure" && expr.captures.includes(thisParam.localId)),
|
||||
});
|
||||
if (usesThis) continue;
|
||||
// A method that never uses this remains callable when detached.
|
||||
// A method that never uses this remains callable when detached. Without
|
||||
// class captures it needs no class environment, so the detached copy
|
||||
// drops the (empty) capture list, which requires a receiver.
|
||||
const name = `%method.receiverless:${target.name}`;
|
||||
const { classCaptures: _classCaptures, ...detached } = target;
|
||||
functions.push({
|
||||
...target,
|
||||
...detached,
|
||||
name,
|
||||
params: target.params.slice(1),
|
||||
locals: target.locals.filter((local) => local.id !== thisParam.localId),
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type ClassInfo,
|
||||
} from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { newFnCtx } from "./lowerer.js";
|
||||
import { PoisonError, newFnCtx } from "./lowerer.js";
|
||||
import { classMethodValue } from "./class-method-values.js";
|
||||
|
||||
/** Data added to a top-level class prototype has shared identity and remains
|
||||
@@ -376,14 +376,37 @@ export function reflectClassPrototype(
|
||||
const method = ts.isMethodDeclaration(member)
|
||||
? name
|
||||
: `${ts.isGetAccessor(member) ? "get" : "set"}:${name}`;
|
||||
const value = classMethodValue(lowerer, info.decl, info, method, loc);
|
||||
// A member whose value or key has no lowering refuses the reflection
|
||||
// at run time; its own diagnostic must not escape this walk (the
|
||||
// walk runs from emitReachable, after statement fences are placed).
|
||||
let before = lowerer.diags.length;
|
||||
let value: IrExpr | null;
|
||||
try {
|
||||
value = classMethodValue(lowerer, info.decl, info, method, loc);
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError)) throw error;
|
||||
lowerer.diags.splice(before);
|
||||
return refuse(
|
||||
"prototype reflection with a member that has no lowering is not supported yet",
|
||||
);
|
||||
}
|
||||
if (!value || !lowerer.dynConvertible(value.type))
|
||||
return refuse(
|
||||
"prototype reflection with a member that cannot cross a checked value boundary is not supported yet",
|
||||
);
|
||||
const key: IrExpr = ts.isComputedPropertyName(member.name)
|
||||
? lowerer.lowerExpr(member.name.expression)
|
||||
: { kind: "strLit", value: member.name.text, type: STRING, loc };
|
||||
let key: IrExpr;
|
||||
before = lowerer.diags.length;
|
||||
try {
|
||||
key = ts.isComputedPropertyName(member.name)
|
||||
? lowerer.lowerExpr(member.name.expression)
|
||||
: { kind: "strLit", value: member.name.text, type: STRING, loc };
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError)) throw error;
|
||||
lowerer.diags.splice(before);
|
||||
return refuse(
|
||||
"prototype reflection with a computed member key that has no lowering is not supported yet",
|
||||
);
|
||||
}
|
||||
initialization.then.push(
|
||||
define(
|
||||
key,
|
||||
@@ -556,6 +579,15 @@ function prototypeProtectedKeys(
|
||||
const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : [];
|
||||
const entry = entries.find(([, field]) => field === name);
|
||||
const declaration = entry ? entry[0].valueDeclaration : undefined;
|
||||
// A CommonJS key-table property holds its key in the table's global.
|
||||
const tableKey =
|
||||
declaration && ts.isPropertyAssignment(declaration)
|
||||
? lowerer.globalsBySymbol.get(entry![0])
|
||||
: undefined;
|
||||
if (tableKey) {
|
||||
keys.push({ kind: "varRef", localId: tableKey.id, type: tableKey.type, loc });
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
|
||||
@@ -66,6 +66,10 @@ import { lowerAbstractEquality } from "../abstract-equality.js";
|
||||
import { lowerInExpression } from "./relational.js";
|
||||
import { lowerDynTypeofTest, lowerErrorCodeTypeofTest } from "./typeof.js";
|
||||
import { builtinClassRefOf, sameBuiltinClass } from "../builtins/class-values.js";
|
||||
import {
|
||||
lowerRuntimeConstructorIs,
|
||||
runtimeConstructorReceiver,
|
||||
} from "../builtins/runtime-constructor.js";
|
||||
import { tryLowerIndexedComparison } from "../indexed-comparison.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "../strict-equality.js";
|
||||
import { withUnitArm } from "../../type-mapper.js";
|
||||
@@ -1322,6 +1326,24 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
lowerUnionTypeofTest(lowerer, expr, loc);
|
||||
if (test) return test;
|
||||
}
|
||||
// `x.constructor === Readable` for an instance of EventEmitter or a
|
||||
// stream class: the instance's own class, compared without a class value.
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
for (const [side, other] of [
|
||||
[expr.left, expr.right],
|
||||
[expr.right, expr.left],
|
||||
] as const) {
|
||||
const ctor = runtimeConstructorReceiver(lowerer, side);
|
||||
const target = ctor ? builtinClassRefOf(lowerer, other) : null;
|
||||
if (ctor && (target?.kind === "emitter" || target?.kind === "stream")) {
|
||||
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
return lowerRuntimeConstructorIs(lowerer, ctor.receiver, target.info, negated, loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
// `C === EventEmitter` between two builtin class references: the
|
||||
// classes are compile-time values, so the answer folds.
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* Narrowings that refine record arms without naming a stored arm. */
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { type IrExpr, type IrType, type SrcLoc, typeEquals } from "../../../ir/ir.js";
|
||||
|
||||
/** A checker narrowing can refine the arms of a union of records with an
|
||||
* intersection: a predicate `r is Route & { path: string }`, or the
|
||||
* `"path" in r` narrowing, which intersects arms that do not declare the
|
||||
* name with `Record<"path", unknown>`. Each narrowed arm then maps to a
|
||||
* new record shape that is not an arm of the stored union, although the
|
||||
* value never changed layout. A stored record arm HOSTS a narrowed record
|
||||
* when both name the same members, each narrowed member refined to a
|
||||
* subset of the stored slot type (or to `unknown`); a member an assertion
|
||||
* attached to the stored shape (assertion-members.ts) may be missing from
|
||||
* the narrowed one. A required member the stored arm does not declare
|
||||
* cannot exist on its values: native records are closed, so `in` already
|
||||
* answers false there and the narrowed arm is uninhabited.
|
||||
*
|
||||
* Only unions of two or more record arms without a literal discriminant
|
||||
* qualify: discriminated unions narrow by their discriminant, and a
|
||||
* record with unit arms keeps its own predicate-strengthening reads.
|
||||
*
|
||||
* Returns the stored arms that can hold a value of the narrowed type, or
|
||||
* null when the union does not qualify, when some narrowed arm is neither
|
||||
* a stored arm nor a record, or when no stored arm is excluded (there is
|
||||
* nothing to narrow). */
|
||||
export function hostedArmTags(
|
||||
lowerer: Lowerer,
|
||||
fromId: string,
|
||||
narrowed: IrType,
|
||||
): Set<number> | null {
|
||||
const from = lowerer.unions.get(fromId);
|
||||
if (!from || !recordUnion(lowerer, fromId)) return null;
|
||||
const narrowedArms =
|
||||
narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed];
|
||||
if (!narrowedArms || narrowedArms.length === 0) return null;
|
||||
const allowed = new Set<number>();
|
||||
let refined = false;
|
||||
for (const arm of narrowedArms) {
|
||||
const direct = lowerer.armTag(fromId, arm);
|
||||
if (direct >= 0) {
|
||||
allowed.add(direct);
|
||||
continue;
|
||||
}
|
||||
if (arm.kind !== "record") return null;
|
||||
refined = true;
|
||||
from.arms.forEach((stored, tag) => {
|
||||
if (stored.kind === "record" && recordHosts(lowerer, stored.shapeId, arm.shapeId)) {
|
||||
allowed.add(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!refined || allowed.size === 0 || allowed.size === from.arms.length) return null;
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/** A union of at least two record arms without a literal discriminant. */
|
||||
function recordUnion(lowerer: Lowerer, unionId: string): boolean {
|
||||
const def = lowerer.unions.get(unionId);
|
||||
if (!def || def.discriminant) return false;
|
||||
return def.arms.filter((arm) => arm.kind === "record").length >= 2;
|
||||
}
|
||||
|
||||
/** Whether every value of `narrowed` (a checker refinement of a value
|
||||
* stored as `storage`) lives in the stored layout: each narrowed arm is a
|
||||
* stored arm or a record a stored record arm hosts (see hostedArmTags),
|
||||
* and at least one needs the hosting. A union storage must qualify as in
|
||||
* hostedArmTags; a single record only when an assertion attached members
|
||||
* to it. A binding initialized by such an assertion keeps the stored type,
|
||||
* so its member writes reach the asserted object. */
|
||||
export function hostsAll(lowerer: Lowerer, storage: IrType, narrowed: IrType): boolean {
|
||||
if (typeEquals(storage, narrowed)) return false;
|
||||
let storedArms: readonly IrType[];
|
||||
if (storage.kind === "union") {
|
||||
if (!recordUnion(lowerer, storage.unionId)) return false;
|
||||
storedArms = lowerer.unions.get(storage.unionId)!.arms;
|
||||
} else if (storage.kind === "record" && lowerer.shapes.attachedNames(storage.shapeId).length) {
|
||||
storedArms = [storage];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const narrowedArms =
|
||||
narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed];
|
||||
if (!narrowedArms || narrowedArms.length === 0) return false;
|
||||
let refined = false;
|
||||
for (const arm of narrowedArms) {
|
||||
if (storedArms.some((stored) => typeEquals(stored, arm))) continue;
|
||||
if (arm.kind !== "record") return false;
|
||||
const hosted = storedArms.some(
|
||||
(stored) => stored.kind === "record" && recordHosts(lowerer, stored.shapeId, arm.shapeId),
|
||||
);
|
||||
if (!hosted) return false;
|
||||
refined = true;
|
||||
}
|
||||
return refined;
|
||||
}
|
||||
|
||||
function recordHosts(lowerer: Lowerer, storedId: string, narrowedId: string): boolean {
|
||||
const stored = lowerer.shapes.get(storedId);
|
||||
const narrowed = lowerer.shapes.get(narrowedId);
|
||||
if (!stored || !narrowed || stored.tuple || narrowed.tuple) return false;
|
||||
if (stored.indexValue || narrowed.indexValue) return false;
|
||||
const attached = lowerer.shapes.attachedNames(storedId);
|
||||
for (const sf of stored.fields) {
|
||||
if (!narrowed.fields.some((nf) => nf.name === sf.name) && !attached.includes(sf.name))
|
||||
return false;
|
||||
}
|
||||
for (const nf of narrowed.fields) {
|
||||
const sf = stored.fields.find((f) => f.name === nf.name);
|
||||
if (!sf || !slotRefines(lowerer, nf.type, sf.type)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Whether a stored slot of type `stored` can be read as `narrowed`: the
|
||||
* same type, `unknown`, or a subset of the stored union's arms (a read
|
||||
* site narrows from the stored slot exactly as it does for a predicate
|
||||
* strengthening one record). */
|
||||
function slotRefines(lowerer: Lowerer, narrowed: IrType, stored: IrType): boolean {
|
||||
if (typeEquals(narrowed, stored) || narrowed.kind === "dyn") return true;
|
||||
if (stored.kind !== "union") return false;
|
||||
if (narrowed.kind !== "union") return lowerer.armTag(stored.unionId, narrowed) >= 0;
|
||||
const arms = lowerer.unions.get(narrowed.unionId)?.arms;
|
||||
return !!arms && arms.every((arm) => lowerer.armTag(stored.unionId, arm) >= 0);
|
||||
}
|
||||
|
||||
/** The stored view of a hosted narrowing (see hostedArmTags): one stored
|
||||
* record arm extracted with the checked helper, or the sub-union of the
|
||||
* hosting arms re-tagged with the excluded arms as traps. Both keep the
|
||||
* value's stored layout, so member reads and writes reach the same
|
||||
* object. Null when the narrowing is not a hosted one. */
|
||||
export function hostedStorageView(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
narrowed: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const fromId = value.type.unionId;
|
||||
const allowed = hostedArmTags(lowerer, fromId, narrowed);
|
||||
if (!allowed) return null;
|
||||
const from = lowerer.unions.get(fromId)!;
|
||||
if (allowed.size === 1) {
|
||||
const arm = from.arms[[...allowed][0]!]!;
|
||||
const helper = lowerer.narrowedArmHelper(fromId, arm, loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, value, arm, loc) : null;
|
||||
}
|
||||
const arms = from.arms.filter((_, tag) => allowed.has(tag));
|
||||
const subId = lowerer.unions.transform(from, arms);
|
||||
const trappable = new Set<number>();
|
||||
from.arms.forEach((_, tag) => {
|
||||
if (!allowed.has(tag)) trappable.add(tag);
|
||||
});
|
||||
const helper = lowerer.unionRetagHelper(fromId, subId, loc, trappable);
|
||||
if (!helper) return null;
|
||||
const type: IrType = { kind: "union", unionId: subId };
|
||||
return { kind: "call", callee: helper, args: [value], type, loc };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
|
||||
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import * as posix from "node:path/posix";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
@@ -859,6 +860,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
lowerer.checker.getSymbolAtLocation(expr),
|
||||
);
|
||||
}
|
||||
// An import of a CommonJS member export whose value is a binding of the
|
||||
// exporting module (`module.exports = Client; module.exports.StatsD =
|
||||
// Client`): the member holds that binding's value.
|
||||
const memberAlias = cjsMemberAliasValueOf(lowerer, expr);
|
||||
if (memberAlias !== null) return lowerer.lowerExpr(memberAlias);
|
||||
// Preflight guarantees no unresolved identifiers; anything else here
|
||||
// is a blocked declaration's binding (the SC2004 cascade) or a
|
||||
// binding form we don't model yet.
|
||||
|
||||
@@ -29,6 +29,7 @@ import { isOptionalProcessStreamProperty } from "../builtins/process.js";
|
||||
import { literalValues } from "../../literal-values.js";
|
||||
import { literalUnionArm } from "../../union-discriminants.js";
|
||||
import { checkedArmExtraction } from "../coercions/unions.js";
|
||||
import { hostedStorageView, hostsAll } from "./hosted-narrowing.js";
|
||||
import { nodeThrowExpr, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import {
|
||||
@@ -659,6 +660,12 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
};
|
||||
}
|
||||
}
|
||||
// A narrowing that refines record arms with an intersection keeps the
|
||||
// stored layout: view the value as the stored arms that can hold it.
|
||||
if (narrowed?.kind === "union") {
|
||||
const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
// A checker type narrowed to a UNIT arm (the `=== undefined` branch)
|
||||
// also stays union-typed: a unit arm has no payload to extract, and
|
||||
// nothing useful reads such a value anyway. (Standalone undefined maps
|
||||
@@ -668,6 +675,14 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
return expr;
|
||||
}
|
||||
const tag = lowerer.armTag(expr.type.unionId, narrowed);
|
||||
if (
|
||||
tag < 0 &&
|
||||
narrowed.kind === "record" &&
|
||||
!lowerer.unions.get(expr.type.unionId)?.discriminant
|
||||
) {
|
||||
const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
if (tag < 0) {
|
||||
// The checker may refine fields inside a discriminated record too.
|
||||
// That creates a different mapped shape, but does not change the
|
||||
@@ -926,6 +941,10 @@ export function runtimeOptionalReceiverRead(
|
||||
loc,
|
||||
);
|
||||
if (!helper) {
|
||||
// Intersection-refined record arms read through their stored
|
||||
// layout (the predicate `r is Route & { path: string }`).
|
||||
const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
if (hosted) return hosted;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -946,6 +965,10 @@ export function runtimeOptionalReceiverRead(
|
||||
// path below; mixed storage also needs to validate its concrete class.
|
||||
if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view;
|
||||
}
|
||||
if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) {
|
||||
const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
const undefTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
|
||||
const def = lowerer.unions.get(local.type.unionId);
|
||||
if (!def || undefTag < 0)
|
||||
@@ -1151,6 +1174,18 @@ export function withRuntimeOptionalNarrowed<T>(
|
||||
* memory);
|
||||
* - dyn → anything else (closures, class instances, void) is rejected:
|
||||
* those types cannot be found inside a JSON dyn. */
|
||||
/** A class instance, or a union of class instances and unit arms (a
|
||||
* runtime-optional binding such as `const node = stack.pop()!`). */
|
||||
function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "object") return true;
|
||||
if (type.kind !== "union") return false;
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
|
||||
return (
|
||||
arms.some((arm) => arm.kind === "object") &&
|
||||
arms.every((arm) => arm.kind === "object" || isUnitType(arm))
|
||||
);
|
||||
}
|
||||
|
||||
/** `Object.fromEntries(...) as T` with T a declared record (the generic
|
||||
* `transform<T extends Record<string, unknown>>(t: T): T` idiom): the
|
||||
* entries build a pure index-signature record, which has no layout in
|
||||
@@ -1272,8 +1307,10 @@ export function lowerAsExpression(
|
||||
if (target?.kind === "dyn" && (targetTs0.flags & ts.TypeFlags.Object) !== 0) {
|
||||
// A class instance asserted to a class-backed protocol view (a type
|
||||
// class instances flow into): the same identity-keeping box an
|
||||
// implicit flow into the view uses.
|
||||
if (inner.type.kind === "object" && lowerer.classImplementedProtocol(targetTs0))
|
||||
// implicit flow into the view uses. A runtime-optional binding of a
|
||||
// class (`const node = stack.pop()!`) boxes the same way, and its
|
||||
// undefined arm stays undefined, as in Node.
|
||||
if (classInstanceOrAbsent(lowerer, inner.type) && lowerer.classImplementedProtocol(targetTs0))
|
||||
return lowerer.coerceInto(expr, inner, DYN);
|
||||
const sourceShape =
|
||||
inner.type.kind === "record" ? lowerer.shapes.get(inner.type.shapeId) : undefined;
|
||||
@@ -1332,6 +1369,12 @@ export function lowerAsExpression(
|
||||
}
|
||||
const rebuilt = freshEntriesRecordAssertion(lowerer, expr, inner, target);
|
||||
if (rebuilt) return rebuilt;
|
||||
// `job as Job & { startedAt: number }` on a record: the asserted
|
||||
// record refines the stored one, so the assertion keeps the object
|
||||
// (a width copy would sever writes through the asserted view).
|
||||
if (inner.type.kind === "record" && target && hostsAll(lowerer, inner.type, target)) {
|
||||
return inner;
|
||||
}
|
||||
// Static assertions normally erase, but record layouts are
|
||||
// monomorphic: a consumer selected from the asserted shape must see
|
||||
// that shape physically. Reuse the ordinary slot coercion so plain
|
||||
@@ -1358,6 +1401,12 @@ export function lowerAsExpression(
|
||||
return lowerer.checkedNarrowCall(helper, inner, target, locOf(expr));
|
||||
}
|
||||
}
|
||||
// An intersection refining the stored record arms keeps the value
|
||||
// (hosted-narrowing.ts): the stored arms that can hold it.
|
||||
if (target && (target.kind === "union" || target.kind === "record")) {
|
||||
const hosted = hostedStorageView(lowerer, inner, target, locOf(expr));
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
}
|
||||
// `node.parent as Sub | undefined`: a union target whose class arm is
|
||||
// narrower than the stored class gets the same checked class view as
|
||||
|
||||
@@ -214,6 +214,37 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
|
||||
!nullish && !and && element.kind === "f64"
|
||||
? arrayNumberRead(receiver, key, elementLoc)
|
||||
: arrayValueRead(lowerer, receiver, key, element, elementLoc);
|
||||
} else if (
|
||||
receiver.type.kind === "record" &&
|
||||
key.type.kind === "string" &&
|
||||
pureIndexShapeOf(lowerer, receiver.type.shapeId) !== null
|
||||
) {
|
||||
// `timings[phase] ??= v` over a pure index-signature record
|
||||
// (`Record<string, number>`): the keyed read answers the value or
|
||||
// undefined for a missing key, and the write inserts or replaces
|
||||
// the key, exactly the two halves `o[k]` and `o[k] = v` lower to.
|
||||
const shapeId = receiver.type.shapeId;
|
||||
const indexValue = pureIndexShapeOf(lowerer, shapeId)!;
|
||||
key = save(key, "%shortCircuitKey");
|
||||
const readType =
|
||||
indexValue.kind === "dyn" ? indexValue : lowerer.withUndefinedArmOf(indexValue);
|
||||
if (!readType) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
target,
|
||||
"short-circuit assignment through this indexed receiver",
|
||||
);
|
||||
}
|
||||
storage = indexValue;
|
||||
read = { kind: "recordKeyGet", obj: receiver, shapeId, key, type: readType, loc };
|
||||
write = (value) => ({
|
||||
kind: "recordKeySet",
|
||||
obj: receiver,
|
||||
shapeId,
|
||||
key,
|
||||
value: lowerer.coerceToExpected(value, indexValue),
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -265,3 +296,11 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
|
||||
}
|
||||
return finish({ kind: "nullish", left: read, right, type: read.type, loc });
|
||||
}
|
||||
|
||||
/** The value type of a pure index-signature record shape (no declared
|
||||
* fields, not a tuple), or null. */
|
||||
function pureIndexShapeOf(lowerer: Lowerer, shapeId: string): IrType | null {
|
||||
const shape = lowerer.shapes.get(shapeId);
|
||||
if (!shape || shape.tuple || shape.fields.length > 0 || !shape.indexValue) return null;
|
||||
return shape.indexValue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { localPrivateBrandClass } from "../private-brands.js";
|
||||
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
|
||||
import {
|
||||
ARRAY_METHODS,
|
||||
@@ -76,6 +77,11 @@ import { lifetimeRegionMemberOf } from "../../experimental-import.js";
|
||||
import { lowerInternTableProperty } from "../intern-tables.js";
|
||||
import { lowerThreadSlotsProperty } from "../thread-slots.js";
|
||||
import { builtinClassRefOf, runtimeClassValueOfType } from "../builtins/class-values.js";
|
||||
import {
|
||||
lowerRuntimeConstructor,
|
||||
lowerRuntimeConstructorName,
|
||||
runtimeConstructorReceiver,
|
||||
} from "../builtins/runtime-constructor.js";
|
||||
import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js";
|
||||
import { lowerClassPrototypeData } from "../class-prototypes.js";
|
||||
import { lowerEnumAccess } from "../lower-enums.js";
|
||||
@@ -290,6 +296,16 @@ export function lowerPropertyAccessExpr(
|
||||
const ref = builtinClassRefOf(lowerer, expr.expression);
|
||||
if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc };
|
||||
}
|
||||
// `x.constructor.name` and `x.constructor` on an instance of
|
||||
// EventEmitter or a stream class (builtins/runtime-constructor.ts).
|
||||
if (expr.name.text === "name" && !expr.questionDotToken) {
|
||||
const ctor = runtimeConstructorReceiver(lowerer, expr.expression);
|
||||
if (ctor) return lowerRuntimeConstructorName(lowerer, ctor.receiver, loc);
|
||||
}
|
||||
{
|
||||
const ctor = runtimeConstructorReceiver(lowerer, expr);
|
||||
if (ctor) return lowerRuntimeConstructor(lowerer, expr, ctor.info);
|
||||
}
|
||||
// `C.name` through a runtime-provided class VALUE (a `typeof Readable`
|
||||
// parameter): the class object's stored name.
|
||||
if (
|
||||
@@ -3087,6 +3103,22 @@ export function fieldTarget(
|
||||
fieldType: F64,
|
||||
};
|
||||
}
|
||||
// `this.#x` in a function-local class's prototype method: the field of
|
||||
// the brand-checked receiver (checkedPrivateReceiver).
|
||||
if (
|
||||
ts.isPrivateIdentifier(access.name) &&
|
||||
access.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
lowerer.resolveThis()?.type.kind === "dyn"
|
||||
) {
|
||||
const owner = localPrivateBrandClass(lowerer, access.name);
|
||||
if (owner) {
|
||||
const ownerType: IrType & { kind: "object" } = {
|
||||
kind: "object",
|
||||
className: owner.info.def.name,
|
||||
};
|
||||
return classFieldTarget(lowerer, access.expression, ownerType, access.name.text);
|
||||
}
|
||||
}
|
||||
const stored = ts.isIdentifier(access.expression)
|
||||
? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type)
|
||||
: undefined;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/* instanceof and in, including private-name and runtime-key in. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
checkedPrivateIn,
|
||||
localPrivateBrandClass,
|
||||
primitiveOperand,
|
||||
privateInMessage,
|
||||
} from "../private-brands.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -17,20 +23,22 @@ import {
|
||||
UNDEFINED_T,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
import { refuseWebAssemblyUse } from "../builtins/webassembly.js";
|
||||
import { lowerSocketInstanceOf } from "../lower-server.js";
|
||||
import { builtinClassRefOf } from "../builtins/class-values.js";
|
||||
import { lowerZlibInstanceOf } from "../builtins/compression.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { lowerDisposableStackInstanceOf } from "../builtins/disposable-stack.js";
|
||||
import { lowerBlobInstanceof } from "../builtins/blob.js";
|
||||
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 { fenceEarlyNsMemberRef, nsMemberIdentOf } from "../lower-namespaces.js";
|
||||
import { maybePromiseInstanceOf } from "../maybe-promise.js";
|
||||
import { isSafeToDiscard } from "./evaluation-safety.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { type ClassInfo, storedClassValueType } from "../lower-classes.js";
|
||||
import { classInstanceOf } from "../class-dynamic-dispatch.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
@@ -172,6 +180,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (cryptoKey) return cryptoKey;
|
||||
const netAddress = lowerNetAddressInstanceof(lowerer, expr);
|
||||
if (netAddress) return netAddress;
|
||||
const readlineInterface = lowerReadlineInstanceof(lowerer, expr);
|
||||
if (readlineInterface) return readlineInterface;
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
@@ -1038,6 +1048,76 @@ function lowerPrivateIn(
|
||||
);
|
||||
}
|
||||
}
|
||||
// A checked value in JavaScript (`#x in this` inside a method, whose
|
||||
// receiver is the actual call-site value): the brand is present exactly
|
||||
// when the value is a class instance at or below the declarer. A typed
|
||||
// reference carries the class it was boxed under: the declarer or a
|
||||
// descendant answers true; an ancestor answers per instance.
|
||||
if (recv.type.kind === "dyn" && recv.kind === "varRef" && isJsSourceFile(expr.getSourceFile())) {
|
||||
// A function-local class has one brand per evaluation.
|
||||
if (info.localClass) {
|
||||
const owner = localPrivateBrandClass(lowerer, priv);
|
||||
if (!owner) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'${pname} in …' inside a function-local class that is not a named declaration`,
|
||||
);
|
||||
}
|
||||
return checkedPrivateIn(lowerer, owner.info, owner.decl, pname, recv, loc);
|
||||
}
|
||||
const typedRefIs = (className: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [
|
||||
recv,
|
||||
{ kind: "strLit", value: typeKey({ kind: "object", className }), type: STRING, loc },
|
||||
],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const or = (left: IrExpr | null, right: IrExpr): IrExpr =>
|
||||
left === null ? right : { kind: "logical", op: "||", left, right, type: BOOL, loc };
|
||||
let test: IrExpr | null = null;
|
||||
const below = (c: ClassInfo): void => {
|
||||
test = or(test, typedRefIs(c.def.name));
|
||||
for (const sub of c.subclasses) below(sub);
|
||||
};
|
||||
below(info);
|
||||
for (let base = info.base; base; base = base.base) {
|
||||
const baseType: IrType = { kind: "object", className: base.def.name };
|
||||
test = or(test, {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: typedRefIs(base.def.name),
|
||||
right: {
|
||||
kind: "instanceOf",
|
||||
value: { kind: "dynCheck", value: recv, type: baseType, loc },
|
||||
className: declName,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// Only objects carry private names: a primitive operand throws Node's
|
||||
// TypeError.
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: primitiveOperand(recv, loc),
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [numLit(1, loc), strLit("", loc), privateInMessage(pname, recv, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
else_: test!,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (recv.type.kind !== "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1349,13 +1429,14 @@ export function lowerInExpression(
|
||||
// arm answers membership STATICALLY (a declared non-optional field is
|
||||
// always present, an undeclared name never is), so the whole test
|
||||
// collapses to runtime TAG tests — tsc's own narrowing then types the
|
||||
// branches (the discriminating-`in` idiom). Optional (undefined-armed)
|
||||
// fields and tuple/index-signature arms answer per VALUE, not per arm
|
||||
// — those unions keep the fence below.
|
||||
// branches (the discriminating-`in` idiom). An arm declaring the key
|
||||
// as an optional (undefined-armed) slot answers per VALUE instead:
|
||||
// its tag test joins that payload's presence test (see
|
||||
// perValueUnionIn). Tuple/index-signature arms keep the fence below.
|
||||
if (recv.type.kind === "union") {
|
||||
const unionId = recv.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms ?? [];
|
||||
const answers: { tag: number; has: boolean }[] = [];
|
||||
const answers: { tag: number; has: boolean | "slot"; arm: IrType }[] = [];
|
||||
let staticAnswers = arms.length > 0;
|
||||
for (const arm of arms) {
|
||||
const tag = lowerer.armTag(unionId, arm);
|
||||
@@ -1372,14 +1453,17 @@ export function lowerInExpression(
|
||||
f.type.kind === "union" &&
|
||||
lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0
|
||||
) {
|
||||
staticAnswers = false; // an optional slot: presence is per-value
|
||||
break;
|
||||
answers.push({ tag, has: "slot", arm }); // presence is per-value
|
||||
continue;
|
||||
}
|
||||
// Accessor properties are own properties to `in` (Node answers
|
||||
// true without invoking the getter) — either slot present makes
|
||||
// the name a member.
|
||||
const acc = shape.fields.some((x) => x.name === `%get:${key}` || x.name === `%set:${key}`);
|
||||
answers.push({ tag, has: f !== undefined || acc || inherited });
|
||||
answers.push({ tag, has: f !== undefined || acc || inherited, arm });
|
||||
}
|
||||
if (staticAnswers && answers.some((a) => a.has === "slot")) {
|
||||
return perValueUnionIn(lowerer, recv, unionId, key, answers, loc);
|
||||
}
|
||||
if (staticAnswers) {
|
||||
const pureRecv =
|
||||
@@ -1531,6 +1615,75 @@ export function lowerInExpression(
|
||||
);
|
||||
}
|
||||
|
||||
/** `"k" in u` over a union of fixed record shapes where some arm declares
|
||||
* `k` as an optional slot. Arms without the name answer false and arms
|
||||
* with an always-present member answer true, both by tag; an optional
|
||||
* slot answers its payload's own-property presence (an explicit
|
||||
* undefined is present, an omitted or deleted property is not), read
|
||||
* through the checked arm extraction the tag test has just proven. The
|
||||
* receiver evaluates once: a non-repeatable one is stabilized in a
|
||||
* hidden local first. */
|
||||
function perValueUnionIn(
|
||||
lowerer: Lowerer,
|
||||
recv: IrExpr,
|
||||
unionId: string,
|
||||
key: string,
|
||||
answers: readonly { tag: number; has: boolean | "slot"; arm: IrType }[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const recvType = recv.type;
|
||||
const stable = isSafeToRepeat(recv) ? null : lowerer.declareHiddenLocal("%inRecv", recvType);
|
||||
const ref = (): IrExpr => (stable ? varRef(stable.id, recvType, loc) : recv);
|
||||
const isTag = (tag: number): IrExpr => ({
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: ref(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
let out: IrExpr | null = null;
|
||||
for (const a of answers) {
|
||||
if (a.has === false) continue;
|
||||
let test = isTag(a.tag);
|
||||
if (a.has === "slot" && a.arm.kind === "record") {
|
||||
test = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: test,
|
||||
right: {
|
||||
kind: "recordHas",
|
||||
obj: {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: a.tag,
|
||||
value: ref(),
|
||||
type: a.arm,
|
||||
loc,
|
||||
},
|
||||
shapeId: a.arm.shapeId,
|
||||
field: key,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
out = out ? { kind: "logical", op: "||", left: out, right: test, type: BOOL, loc } : test;
|
||||
}
|
||||
const result: IrExpr = out ?? { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
if (!stable) return result;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: stable.id, init: recv, loc }],
|
||||
result,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The runtime-key `in` (see lowerInExpression): `k in r` where k is a
|
||||
* runtime string and r an index-signature record — an interned
|
||||
* `%rec.haskey.<n>(k, r)` walks the declared names (a string-equality
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Increment, decrement and prefix unary operators, and compound field updates. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { isPrivateWriteReceiver } from "../private-brands.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
type FieldTarget,
|
||||
@@ -524,6 +525,19 @@ export function fieldSetStmt(
|
||||
loc: SrcLoc,
|
||||
blame: ts.Node,
|
||||
): IrStmt {
|
||||
// A write through a brand-checked receiver (checkedPrivateReceiver): the
|
||||
// value evaluates first, then the brand check throws, as in Node.
|
||||
if (isPrivateWriteReceiver(target.obj) && value.kind !== "varRef") {
|
||||
const stored = lowerer.declareHiddenLocal("%privateWriteValue", value.type);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: stored.id, init: value, loc },
|
||||
fieldSetStmt(lowerer, target, varRef(stored.id, stored.type, loc), loc, blame),
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A member write through an unassigned deferred-init field: the value
|
||||
// still evaluates first, then the write throws Node's TypeError.
|
||||
const deferredSlot =
|
||||
|
||||
@@ -224,12 +224,30 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
const loops: number[] = [];
|
||||
let loopIds = 0;
|
||||
const loopKey = (): string => loops.join("/");
|
||||
/** Bindings initialized by another binding's reference (`const view =
|
||||
* value`, the snapshot a union-receiver write takes), so writes through
|
||||
* either count as writes through the one object. */
|
||||
const aliases = new Map<string, { target: string; fn: string }>();
|
||||
const bindingOf = (obj: IrExpr): string | null => {
|
||||
let o = obj;
|
||||
while (o.kind === "unionNarrow" || o.kind === "downcast") o = o.value;
|
||||
if (o.kind !== "varRef") return null;
|
||||
let id = o.localId;
|
||||
const seen = new Set<string>();
|
||||
for (;;) {
|
||||
const alias = aliases.get(id);
|
||||
if (!alias || alias.fn !== fnName || inits.get(id)?.count !== 1 || seen.has(id)) return id;
|
||||
seen.add(id);
|
||||
id = alias.target;
|
||||
}
|
||||
};
|
||||
const noteInit = (id: string, init: IrExpr): void => {
|
||||
const previous = inits.get(id);
|
||||
if (previous !== undefined) {
|
||||
previous.count++;
|
||||
return;
|
||||
}
|
||||
if (init.kind === "varRef") aliases.set(id, { target: init.localId, fn: fnName });
|
||||
const fresh =
|
||||
init.kind === "recordLit" ||
|
||||
init.kind === "recordClone" ||
|
||||
@@ -315,7 +333,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
field: st.field,
|
||||
kind: st.value.kind === "fieldAbsent" ? "delete" : "write",
|
||||
fn: fnName,
|
||||
binding: st.obj.kind === "varRef" ? st.obj.localId : null,
|
||||
binding: bindingOf(st.obj),
|
||||
loop: loopKey(),
|
||||
pos,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js";
|
||||
import { lowerGenMethodCall } from "./lower-generators.js";
|
||||
import { lowerClassSymbolMethodCall } from "./symbol-methods.js";
|
||||
import { lowerReadlineDisposeCall } from "./builtins/readline.js";
|
||||
import { errorToStringCall } from "./error-methods.js";
|
||||
import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js";
|
||||
import {
|
||||
@@ -1683,6 +1684,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
if (timersDispose) return timersDispose;
|
||||
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
|
||||
if (symbolMethod) return symbolMethod;
|
||||
const readlineDispose = lowerReadlineDisposeCall(lowerer, expr);
|
||||
if (readlineDispose) return readlineDispose;
|
||||
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression);
|
||||
lowerer.fenceStaticResponseMember(expr.expression, "call");
|
||||
lowerer.fenceStaticHeadersMember(expr.expression, "call");
|
||||
@@ -6769,6 +6772,99 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Whether `Object.assign(instance, record)` can copy every field of the
|
||||
* record shape into a field the class declares with the same type: a plain
|
||||
* record (no tuple, index signature or accessor slots) and a compiled
|
||||
* class whose chain has no builtin or runtime-provided storage and no
|
||||
* accessor for the written names. */
|
||||
function classAssignPlan(lowerer: Lowerer, className: string, srcShapeId: string): boolean {
|
||||
const info = lowerer.classes.get(className);
|
||||
const shape = lowerer.shapes.get(srcShapeId);
|
||||
if (!info || !shape || shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape))
|
||||
return false;
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
if (c.builtinError || c.builtinEmitter || c.builtinStream !== undefined || c.def.runtime)
|
||||
return false;
|
||||
}
|
||||
return shape.fields.every((f) => {
|
||||
const declared = info.fields.get(f.name);
|
||||
return (
|
||||
declared !== undefined &&
|
||||
(typeEquals(declared, f.type) || lowerer.widthLiftPlan(f.type, declared) !== null) &&
|
||||
!lowerer.findMethodOn(info, f.name) &&
|
||||
!lowerer.findMethodOn(info, `get:${f.name}`) &&
|
||||
!lowerer.findMethodOn(info, `set:${f.name}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Interned `%obj.assignClass.<n>(o, s)` — Object.assign's per-field
|
||||
* write of a record into a class instance (classAssignPlan's gate):
|
||||
* optional source fields write only when present, and the instance
|
||||
* returns. */
|
||||
function classAssignHelper(
|
||||
lowerer: Lowerer,
|
||||
className: string,
|
||||
srcShapeId: string,
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (!classAssignPlan(lowerer, className, srcShapeId)) return null;
|
||||
const key = `obj.assignClass:${className}:${srcShapeId}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const sShape = lowerer.shapes.get(srcShapeId)!;
|
||||
const oT: IrType = { kind: "object", className };
|
||||
const sT: IrType = { kind: "record", shapeId: srcShapeId };
|
||||
const oRef: IrExpr = { kind: "varRef", localId: "o.0", type: oT, loc };
|
||||
const sRef: IrExpr = { kind: "varRef", localId: "s.0", type: sT, loc };
|
||||
const body: IrStmt[] = [];
|
||||
const info = lowerer.classes.get(className)!;
|
||||
for (const f of sShape.fields) {
|
||||
const declared = info.fields.get(f.name)!;
|
||||
const read: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: srcShapeId,
|
||||
field: f.name,
|
||||
type: f.type,
|
||||
loc,
|
||||
};
|
||||
const lift = typeEquals(declared, f.type) ? null : lowerer.widthLiftPlan(f.type, declared);
|
||||
const set: IrStmt = {
|
||||
kind: "fieldSet",
|
||||
obj: oRef,
|
||||
className,
|
||||
field: f.name,
|
||||
value: lift === null ? read : lowerer.applyWidthLift(lift, read, declared, loc),
|
||||
loc,
|
||||
};
|
||||
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
|
||||
body.push(
|
||||
present.kind === "recordHas"
|
||||
? { kind: "if", cond: present, then: [set], else_: null, loc }
|
||||
: set,
|
||||
);
|
||||
}
|
||||
body.push({ kind: "return", value: oRef, loc });
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "o.0", name: "o", type: oT },
|
||||
{ localId: "s.0", name: "s", type: sT },
|
||||
],
|
||||
returnType: oT,
|
||||
locals: [
|
||||
{ id: "o.0", name: "o", type: oT, mutable: true },
|
||||
{ id: "s.0", name: "s", type: sT, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Interned `%obj.assign.<n>(t, s)` — Object.assign's per-field copy
|
||||
* over signature-free records (every source field lands on a same-named,
|
||||
* same-typed target field — the caller's gate): optional source fields
|
||||
@@ -7410,6 +7506,28 @@ function lowerObjectStaticCall(
|
||||
return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
|
||||
}
|
||||
}
|
||||
// A CLASS-INSTANCE target and a plain record source whose fields are
|
||||
// all fields the class declares with the same types
|
||||
// (`Object.assign(this, { retries: 3 })`): the per-field write into
|
||||
// the instance, returning it — the instance mutates in place.
|
||||
if (tProbe?.type.kind === "object" && sProbe?.type.kind === "record") {
|
||||
const helper = classAssignPlan(lowerer, tProbe.type.className, sProbe.type.shapeId);
|
||||
if (helper) {
|
||||
const loc = locOf(call);
|
||||
const target = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
const source = lowerer.lowerExpr(call.arguments.nodes[1]!);
|
||||
if (target.type.kind === "object" && source.type.kind === "record") {
|
||||
const name = classAssignHelper(
|
||||
lowerer,
|
||||
target.type.className,
|
||||
source.type.shapeId,
|
||||
loc,
|
||||
);
|
||||
if (name)
|
||||
return { kind: "call", callee: name, args: [target, source], type: target.type, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tProbe?.type.kind === "record" && sProbe?.type.kind === "record") {
|
||||
const tShape = lowerer.shapes.get(tProbe.type.shapeId);
|
||||
const sShape = lowerer.shapes.get(sProbe.type.shapeId);
|
||||
@@ -7500,6 +7618,25 @@ function lowerObjectStaticCall(
|
||||
const srcNode = spread ? argNode.expression : argNode;
|
||||
const rawSource = lowerer.lowerExpr(srcNode);
|
||||
fenceSymbolFieldCopy(lowerer, srcNode, rawSource.type, spread);
|
||||
// A class instance copied into a fresh object
|
||||
// (`Object.assign({}, instance)`): the result is a plain
|
||||
// object holding the instance's own fields, never an
|
||||
// instance, so a result the checker types as the class has
|
||||
// no representation (the checked cast would throw).
|
||||
if (
|
||||
rawSource.type.kind === "object" &&
|
||||
freshLiteralTarget &&
|
||||
!ts.isExpressionStatement(call.parent)
|
||||
) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT !== null && resultT.kind === "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
srcNode,
|
||||
`Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const src = lowerer.coerceToExpected(rawSource, DYN);
|
||||
if (src.type.kind !== "dyn") {
|
||||
ok = false;
|
||||
|
||||
@@ -3368,6 +3368,56 @@ export function collectClassShapeInner(
|
||||
.map((p): ParamShape => ({ type: p.type, mode: "omittable" })),
|
||||
);
|
||||
}
|
||||
// A TypeScript override of a JavaScript method (`class T extends
|
||||
// Agent` over an npm-static package) keeps the base's checked-value
|
||||
// ABI: each untyped base slot stays dyn, and the override's typed
|
||||
// parameter narrows from it at entry with a checked conversion. An
|
||||
// untyped base return takes the override's value boxed. Calls on
|
||||
// a statically typed receiver see the same ABI.
|
||||
if (
|
||||
overridden &&
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
overridden.declarer.decl !== null &&
|
||||
isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
|
||||
member.asteriskToken === undefined &&
|
||||
!overridden.sig.async &&
|
||||
!overridden.sig.gen &&
|
||||
shapes.length <= overridden.sig.params.length &&
|
||||
shapes.every((p, i) => {
|
||||
const inherited = overridden.sig.params[i]!;
|
||||
return (
|
||||
(p.mode === "required" || p.mode === "omittable") &&
|
||||
p.bodyType === undefined &&
|
||||
(inherited.mode === "required" || inherited.mode === "omittable") &&
|
||||
(typeEquals(p.type, inherited.type) ||
|
||||
(inherited.type.kind === "dyn" && lowerer.dynConvertible(p.type)))
|
||||
);
|
||||
}) &&
|
||||
overridden.sig.params
|
||||
.slice(shapes.length)
|
||||
.every(
|
||||
(p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable"),
|
||||
)
|
||||
) {
|
||||
for (let i = 0; i < shapes.length; i++) {
|
||||
const own = shapes[i]!;
|
||||
if (overridden.sig.params[i]!.type.kind === "dyn" && own.type.kind !== "dyn")
|
||||
shapes[i] = { type: DYN, mode: own.mode, entryNarrow: own.type };
|
||||
}
|
||||
shapes.push(
|
||||
...overridden.sig.params
|
||||
.slice(shapes.length)
|
||||
.map((p): ParamShape => ({ type: p.type, mode: "omittable" })),
|
||||
);
|
||||
if (
|
||||
overridden.sig.ret.kind === "dyn" &&
|
||||
ft.ret.kind !== "dyn" &&
|
||||
ft.ret.kind !== "void" &&
|
||||
lowerer.dynConvertible(ft.ret)
|
||||
)
|
||||
ft.ret = DYN;
|
||||
ft.params = shapes.map((shape) => shape.type);
|
||||
}
|
||||
if (
|
||||
overridden &&
|
||||
member.asteriskToken === undefined &&
|
||||
@@ -5412,6 +5462,75 @@ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Each evaluation of a function-local class creates fresh private names,
|
||||
* while the compiled class shares one layout across evaluations. A
|
||||
* JavaScript class's prototype methods are closures per evaluation, so the
|
||||
* names agree when every private member is an instance field and every use
|
||||
* is on `this` (privateNamesOnlyOnThis): native methods only run on their
|
||||
* own evaluation's instances, and the closures, which receive any `this`,
|
||||
* check the receiver's evaluation against the class's own binding
|
||||
* (private-brands.ts), so the class must be a named declaration (undici's
|
||||
* webidl FastIterableIterator). The checker rejects writes to that binding. */
|
||||
function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean {
|
||||
if (!isJsSourceFile(expr.getSourceFile()) || !ts.isClassDeclaration(expr) || !expr.name)
|
||||
return false;
|
||||
if (
|
||||
expr.members.nodes.some(
|
||||
(member) =>
|
||||
member.name !== undefined &&
|
||||
ts.isPrivateIdentifier(member.name) &&
|
||||
!ts.isPropertyDeclaration(member),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return privateNamesOnlyOnThis(expr);
|
||||
}
|
||||
|
||||
/** Every private name is used only on `this` inside the class's own instance
|
||||
* members (`this.#x`, `#x in this`), outside nested classes and non-arrow
|
||||
* functions, and the class has no static members that could hand instances
|
||||
* across evaluations. */
|
||||
function privateNamesOnlyOnThis(expr: ts.ClassLikeDeclaration): boolean {
|
||||
let ok = true;
|
||||
for (const member of expr.members.nodes) {
|
||||
if (ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) return false;
|
||||
ts.walkPreorder(member, (node) => {
|
||||
if (!ok) return "stop";
|
||||
// A nested class or non-arrow function rebinds `this`: any private
|
||||
// name inside it disqualifies.
|
||||
if (
|
||||
node !== member &&
|
||||
(ts.isClassDeclaration(node) ||
|
||||
ts.isClassExpression(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isFunctionDeclaration(node))
|
||||
) {
|
||||
ts.walkPreorder(node, (inner) => {
|
||||
if (ts.isPrivateIdentifier(inner)) ok = false;
|
||||
return ok ? undefined : "stop";
|
||||
});
|
||||
return "skip";
|
||||
}
|
||||
if (ts.isPrivateIdentifier(node)) {
|
||||
const parent = node.parent;
|
||||
const onThis =
|
||||
parent === member ||
|
||||
(ts.isPropertyAccessExpression(parent) &&
|
||||
parent.name === node &&
|
||||
parent.expression.kind === ts.SyntaxKind.ThisKeyword) ||
|
||||
(ts.isBinaryExpression(parent) &&
|
||||
parent.left === node &&
|
||||
parent.operatorToken.kind === ts.SyntaxKind.InKeyword &&
|
||||
parent.right.kind === ts.SyntaxKind.ThisKeyword);
|
||||
if (!onThis) ok = false;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
if (!ok) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Collect a class expression once per enclosing specialization. Function-local
|
||||
* evaluations allocate fresh constructor objects; top-level expressions use
|
||||
* immortal templates. Static initialization remains limited to whole top-level
|
||||
@@ -5449,7 +5568,8 @@ export function lowerClassExpressionInfo(
|
||||
}
|
||||
if (
|
||||
local &&
|
||||
expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name))
|
||||
expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) &&
|
||||
!localPrivateFieldsAgree(expr)
|
||||
) {
|
||||
lowerer.unsupported("SC1090", expr, "function-local classes with private brands");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* ToBoolean/ToString coercion helpers, and field/element reads and writes
|
||||
* (FieldTarget). */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands.js";
|
||||
import { constituentTypes } from "../ts7/checker.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js";
|
||||
@@ -458,6 +459,20 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const local = lowerer.resolveThis();
|
||||
if (local) {
|
||||
const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
// `this.#x` in a function-local class's prototype method, whose `this`
|
||||
// is the call-site value: the field lives on the native instance of
|
||||
// this evaluation (checkedPrivateReceiver).
|
||||
const access = expr.parent;
|
||||
if (
|
||||
local.type.kind === "dyn" &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.expression === expr &&
|
||||
ts.isPrivateIdentifier(access.name)
|
||||
) {
|
||||
const owner = localPrivateBrandClass(lowerer, access.name);
|
||||
if (owner)
|
||||
return checkedPrivateReceiver(lowerer, owner.info, owner.decl, access, value, loc);
|
||||
}
|
||||
const initialized = lowerer.ctx.superInitialized;
|
||||
if (initialized)
|
||||
return {
|
||||
|
||||
@@ -2011,6 +2011,47 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S
|
||||
return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc);
|
||||
}
|
||||
|
||||
/** %d and %i over a checked-dynamic argument, as Node's formatWithOptions
|
||||
* reads it: a bigint prints as `${n}n`, a symbol as NaN, and any other
|
||||
* value as `number` (Number(arg) for %d, parseInt(String(arg)) for %i)
|
||||
* formatted. The argument evaluates once. */
|
||||
function formatDynInteger(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
number: (held: IrExpr) => IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const held = lowerer.declareHiddenLocal("%formatArg", DYN);
|
||||
const ref = (): IrExpr => varRef(held.id, DYN, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: held.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "bigint", value: ref(), type: BOOL, loc },
|
||||
then: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "libCall", fn: "dyn.toStringCoerce", args: [ref()], type: STRING, loc },
|
||||
right: strLit("n", loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "symbol", value: ref(), type: BOOL, loc },
|
||||
then: strLit("NaN", loc),
|
||||
else_: { kind: "libCall", fn: "insp.f64", args: [number(ref())], type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** %d and %i over the argument types Node formats without a number
|
||||
* conversion: a bigint prints as `${n}n` (formatBigInt) and a symbol as NaN;
|
||||
* null is Number(null) (0) under %d and parseInt("null") (NaN) under %i, and
|
||||
@@ -2141,19 +2182,20 @@ export function lowerFormatCall(
|
||||
};
|
||||
}
|
||||
// A checked-dynamic argument (`console.log('... actual %d.',
|
||||
// context.actual)` — test/common's exit report): VALIDATE it as a
|
||||
// number (dynCheck) and format. A non-number dyn value throws the
|
||||
// catchable TypeError where Node would print its ToNumber (NaN
|
||||
// for objects) — it throws instead of returning a wrong result
|
||||
// (SEMANTICS.md).
|
||||
// context.actual)` — test/common's exit report): Number(arg).
|
||||
if (value.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "insp.f64",
|
||||
args: [{ kind: "dynCheck", value, type: F64, loc }],
|
||||
type: STRING,
|
||||
return formatDynInteger(
|
||||
lowerer,
|
||||
value,
|
||||
(held) => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.numberConstructor",
|
||||
args: [held],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
};
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`util.format %d of '${lowerer.fmt(value.type)}' values`,
|
||||
@@ -2185,6 +2227,24 @@ export function lowerFormatCall(
|
||||
};
|
||||
return { kind: "libCall", fn: "insp.f64", args: [parsed], type: STRING, loc };
|
||||
}
|
||||
// parseInt(String(arg)) over a checked-dynamic argument.
|
||||
if (value.type.kind === "dyn") {
|
||||
return formatDynInteger(
|
||||
lowerer,
|
||||
value,
|
||||
(held) => ({
|
||||
kind: "libCall",
|
||||
fn: "num.parseInt",
|
||||
args: [
|
||||
{ kind: "libCall", fn: "dyn.toStringCoerce", args: [held], type: STRING, loc },
|
||||
numLit(0, loc),
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(`util.format %i of '${lowerer.fmt(value.type)}' values`, node);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2476,6 +2476,20 @@ function lowerNetSocketMethodCall(
|
||||
? value
|
||||
: lowerer.coerceInto(call, value, declared);
|
||||
}
|
||||
if (name === "isSessionReused" && args.length === 0) {
|
||||
// TLSSocket.isSessionReused(): the client never offers a cached
|
||||
// session (no session resumption, a documented TLS divergence), so
|
||||
// no handshake resumes one. A plain socket asserted to be a
|
||||
// TLSSocket answers false too, where Node has no such method.
|
||||
const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: receiver, loc }],
|
||||
result: boolLit(false, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "resetAndDestroy") {
|
||||
// An RST close; a connecting socket resets once it connects. Answers
|
||||
// the socket, Node's chaining.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
import {
|
||||
matchAllRowsReadByElements,
|
||||
storedMatchAllRowsReadByElements,
|
||||
@@ -1476,6 +1477,18 @@ function lowerLabeled(lowerer: Lowerer, stmt: ts.LabeledStatement): IrStmt | IrS
|
||||
return { kind: "block", body, labels, loc: locOf(stmt) };
|
||||
}
|
||||
|
||||
// Value kinds a `throw` converts to a checked-dynamic value, so the catch
|
||||
// binding sees the thrown object rather than a type-erased reference.
|
||||
const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"array",
|
||||
"union",
|
||||
"map",
|
||||
"set",
|
||||
"bytes",
|
||||
"func",
|
||||
"regex",
|
||||
]);
|
||||
|
||||
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
|
||||
@@ -1654,14 +1667,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
value = lowerer.coerceToExpected(value, DYN);
|
||||
} else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) {
|
||||
value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) };
|
||||
} else if (
|
||||
(value.type.kind === "array" || value.type.kind === "union") &&
|
||||
lowerer.dynConvertible(value.type)
|
||||
) {
|
||||
// Arrays, and unions over records and arrays, keep their identity
|
||||
// and contents the same way: the catch binding, `Array.isArray`,
|
||||
} else if (THROWN_AS_DYN.has(value.type.kind) && lowerer.dynConvertible(value.type)) {
|
||||
// Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes and
|
||||
// unions over them keep their identity and contents the same way:
|
||||
// the catch binding, `typeof`, `instanceof`, `Array.isArray`,
|
||||
// inspection and typed views read the thrown object.
|
||||
value = lowerer.coerceToExpected(value, DYN);
|
||||
value = lowerer.coerceInto(stmt.expression, value, DYN);
|
||||
}
|
||||
if (value.type.kind === "date") {
|
||||
lowerer.unsupported(
|
||||
@@ -5471,6 +5482,23 @@ export function lowerVarDecl(
|
||||
settledType = init.type;
|
||||
}
|
||||
}
|
||||
// `const view = route as Route & { order: number }`: the asserted type
|
||||
// refines the record arms the value is stored in (the assertion keeps
|
||||
// that value, see hosted-narrowing.ts). The binding keeps the stored
|
||||
// type, so writes through it reach the asserted object. An annotation
|
||||
// the stored type hosts (`const view: Route & { order: number } = ...`)
|
||||
// keeps it too: coercing into the annotated layout would copy the
|
||||
// value and read members the writes have not created yet.
|
||||
if (
|
||||
!isLet &&
|
||||
decl.initializer &&
|
||||
isAssertionInitializer(decl.initializer) &&
|
||||
(settledType.kind === "union" || settledType.kind === "record") &&
|
||||
(init.type.kind === "union" || init.type.kind === "record") &&
|
||||
hostsAll(lowerer, init.type, settledType)
|
||||
) {
|
||||
settledType = init.type;
|
||||
}
|
||||
// An exact class behind a generic interface keeps its native
|
||||
// representation so generic calls can specialize against its body.
|
||||
if (
|
||||
@@ -6455,7 +6483,10 @@ function lowerCjsExportStatement(
|
||||
// storage — collectGlobals skipped it; importers re-resolve through
|
||||
// cjsMemberExportClassSymbol): the statement lowers to nothing.
|
||||
if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) {
|
||||
return { kind: "block", body: [], loc };
|
||||
// An inline require still evaluates its module here.
|
||||
const req = inlineProgramRequireOf(lowerer, cjs.value);
|
||||
const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null;
|
||||
return init ?? { kind: "block", body: [], loc };
|
||||
}
|
||||
// The tsc VOID-INIT PREAMBLE (`exports.leaf = void 0;` with the real
|
||||
// `exports.leaf = leaf;` further down — every tsc-emitted dist file
|
||||
@@ -6916,12 +6947,13 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
if (
|
||||
ts.isBinaryExpression(expr) &&
|
||||
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isCjsExportsIdentifier(lowerer, expr.left)
|
||||
isCjsExportsIdentifier(lowerer, expr.left) &&
|
||||
cjsExportAssignmentOf(stmtNode)?.kind !== "table"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.left,
|
||||
"rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports.<name> = … and module.exports = value",
|
||||
"rebinding 'exports' without module.exports (exports = value): members attached through 'exports' afterwards are properties of that value, which module.exports does not hold — write exports = module.exports = value",
|
||||
);
|
||||
}
|
||||
const cjs = cjsExportAssignmentOf(stmtNode);
|
||||
@@ -11198,3 +11230,9 @@ export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclarat
|
||||
requireEarlyReference(lowerer.program, decl) === null
|
||||
);
|
||||
}
|
||||
|
||||
function isAssertionInitializer(node: ts.Expression): boolean {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return ts.isAsExpression(e) || ts.isTypeAssertion(e);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extendAssertedRecordMembers } from "./assertion-members.js";
|
||||
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
|
||||
import {
|
||||
lifetimeRegionType,
|
||||
@@ -129,7 +130,8 @@ import {
|
||||
} from "../tier-analysis.js";
|
||||
import { DeferredModuleInitializers } from "./deferred-module-initializers.js";
|
||||
import { finalizeClassMethodValues } from "./class-method-values.js";
|
||||
import type { ClassSymbolKey } from "./symbol-fields.js";
|
||||
import { finalizePrivateBrands } from "./private-brands.js";
|
||||
import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js";
|
||||
import type { HttpClientFnBinding } from "./lower-server.js";
|
||||
export type { WidthLift } from "./width-lift.js";
|
||||
export { newFnCtx, type FnCtx } from "./function-context.js";
|
||||
@@ -299,6 +301,7 @@ import {
|
||||
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
|
||||
import {
|
||||
cjsNamespaceModule,
|
||||
cjsWholeExportValueOf,
|
||||
inlineNamespaceRequireOf,
|
||||
inlineProgramRequireOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
@@ -1961,6 +1964,9 @@ export class Lowerer {
|
||||
* their symbol/node keys; no module-level cache should retain a pass. */
|
||||
readonly classSymbolKeys = new Map<ts.Symbol, ClassSymbolKey | null>();
|
||||
readonly registeredClassSymbols = new Map<string, ts.Symbol>();
|
||||
/** Symbol-key identities by description, in resolution order (the slot
|
||||
* ordinals of key-table keys — symbolKeyOf). */
|
||||
readonly symbolFieldIdentities = new Map<string, ts.Symbol[]>();
|
||||
readonly httpClientFnBindings = new Map<ts.Symbol, HttpClientFnBinding>();
|
||||
streamPropMutatedSyms: Set<ts.Symbol> | null = null;
|
||||
requestInitPropMutatedSyms: Set<ts.Symbol> | null = null;
|
||||
@@ -2115,6 +2121,51 @@ export class Lowerer {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
/** A TypeScript object literal handed to a JavaScript checked-value
|
||||
* parameter whose inferred type is open (`constructor({ factory, ...options
|
||||
* } = {})` — the rest element admits any property): the literal names a
|
||||
* property the contextual JS type does not declare, so it has no record
|
||||
* layout there and is built as the plain object the JS side receives. */
|
||||
private excessJsContextualProperty(
|
||||
literal: ts.ObjectLiteralExpression,
|
||||
contextual: readonly ts.Type[],
|
||||
): boolean {
|
||||
const objects = contextual.filter(
|
||||
(part) => !(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)),
|
||||
);
|
||||
if (objects.length === 0) return false;
|
||||
// Only a destructuring pattern with a rest element is open; other
|
||||
// inferred JS types keep their record lowering.
|
||||
const fromJs = objects.every((part) => {
|
||||
const sym = part.getSymbol();
|
||||
return (
|
||||
sym !== undefined &&
|
||||
sym.declarations.some((d) => {
|
||||
if (!isJsSourceFile(d.getSourceFile())) return false;
|
||||
// The pattern itself, or the `= {}` default of a pattern parameter.
|
||||
const pattern =
|
||||
ts.isObjectLiteralExpression(d) &&
|
||||
d.parent !== undefined &&
|
||||
ts.isParameter(d.parent) &&
|
||||
d.parent.initializer === d
|
||||
? d.parent.name
|
||||
: d;
|
||||
return (
|
||||
ts.isObjectBindingPattern(pattern) &&
|
||||
pattern.elements.nodes.some((element) => element.dotDotDotToken !== undefined)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
if (!fromJs) return false;
|
||||
return literal.properties.nodes.some((prop) => {
|
||||
if (!prop.name || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)))
|
||||
return false;
|
||||
const name = prop.name.text;
|
||||
return objects.every((part) => !this.checker.getPropertyOfType(part, name));
|
||||
});
|
||||
}
|
||||
|
||||
/** The index value of a keyed read of a record's index signature that
|
||||
* can answer undefined for a missing key while its checker type does not
|
||||
* admit undefined, or null. A literal key naming a declared field is a
|
||||
@@ -3719,13 +3770,18 @@ export class Lowerer {
|
||||
? d.parent
|
||||
: null;
|
||||
if (!assign || assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
|
||||
if (!ts.isIdentifier(assign.right)) return null;
|
||||
const stmt = assign.parent;
|
||||
if (!stmt || !ts.isExpressionStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null;
|
||||
if (!isJsSourceFile(stmt.parent)) return null;
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind !== "member" || cjs.expr !== assign) return null;
|
||||
if (cjsExportDiscardReason(stmt) !== null) return null;
|
||||
// `module.exports.C = require('./c')` over a module whose whole export
|
||||
// is a class identifier: the same alias plumbing; the statement keeps
|
||||
// the require's module evaluation at its position.
|
||||
const required = this.memberExportRequiredClass(assign.right);
|
||||
if (required !== undefined) return required;
|
||||
if (!ts.isIdentifier(assign.right)) return null;
|
||||
let value = this.checker.getSymbolAtLocation(assign.right);
|
||||
if (!value) return null;
|
||||
if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value);
|
||||
@@ -3733,6 +3789,22 @@ export class Lowerer {
|
||||
return isClass ? value : null;
|
||||
}
|
||||
|
||||
/** For an inline program `require("./c")`: the class symbol its module's
|
||||
* kept `module.exports = <class identifier>` names, or null when the
|
||||
* whole export is something else. Undefined when `expr` is not such a
|
||||
* require. */
|
||||
memberExportRequiredClass(expr: ts.Expression): ts.Symbol | null | undefined {
|
||||
const req = inlineProgramRequireOf(this, expr);
|
||||
if (req === null) return undefined;
|
||||
const whole = cjsWholeExportValueOf(req.dep);
|
||||
if (whole === null || !ts.isIdentifier(whole.rhs)) return null;
|
||||
let value = this.checker.getSymbolAtLocation(whole.rhs);
|
||||
if (!value) return null;
|
||||
if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value);
|
||||
const isClass = value.declarations.some((decl) => ts.isClassDeclaration(decl));
|
||||
return isClass ? value : null;
|
||||
}
|
||||
|
||||
/** The CommonJS JS module a DEFAULT-import binding's declaration loads
|
||||
* (`import d from "./lib.cjs"`), or null: Node's ESM-CJS interop binds
|
||||
* the default to module.exports — exactly a require binding — so those
|
||||
@@ -6710,6 +6782,10 @@ export class Lowerer {
|
||||
}
|
||||
this.collectProgram(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
extendAssertedRecordMembers(
|
||||
this,
|
||||
parts.map((fp) => fp.sf),
|
||||
);
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
@@ -6856,6 +6932,7 @@ export class Lowerer {
|
||||
finishModule(functions: IrFunction[]): LowerResult {
|
||||
if (this.diags.length === 0) fuseArrayPipelines(this, functions);
|
||||
finalizeClassMethodValues(this, functions);
|
||||
finalizePrivateBrands(this, functions);
|
||||
trackClassFieldCreation(this, functions);
|
||||
|
||||
// Globals typed by a class that never REGISTERED (a JS class whose
|
||||
@@ -7116,6 +7193,10 @@ export class Lowerer {
|
||||
};
|
||||
this.collectProgram(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
extendAssertedRecordMembers(
|
||||
this,
|
||||
parts.map((fp) => fp.sf),
|
||||
);
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
@@ -8037,6 +8118,17 @@ export class Lowerer {
|
||||
return this.checker.getUnknownType();
|
||||
}
|
||||
}
|
||||
// A symbol-keyed field read through a CommonJS key-table key reads as
|
||||
// the field's layout type (tableKeyFieldType).
|
||||
if (ts.isElementAccessExpression(node) && isJsSourceFile(node.getSourceFile())) {
|
||||
const field = tableKeyFieldType(this, node);
|
||||
if (field) {
|
||||
if (field.kind === "f64") return this.checker.getNumberType();
|
||||
if (field.kind === "string") return this.checker.getStringType();
|
||||
if (field.kind === "bool") return this.checker.getBooleanType();
|
||||
return this.checker.getUnknownType();
|
||||
}
|
||||
}
|
||||
// A checked JS method receives its actual call-site receiver. Its
|
||||
// declaration's inferred class type must not turn property reads back
|
||||
// into native field loads when the method is borrowed by another object.
|
||||
@@ -9671,7 +9763,8 @@ export class Lowerer {
|
||||
builtinOption("SpawnOptions") ||
|
||||
builtinOption("UrlObject") ||
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x))
|
||||
!this.mapTypeOf(this.typeOf(x)) ||
|
||||
this.excessJsContextualProperty(x, parts)
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
}
|
||||
@@ -10958,6 +11051,21 @@ export class Lowerer {
|
||||
return;
|
||||
}
|
||||
const name = (decl.name as ts.Identifier).text;
|
||||
if (shape.entryNarrow) {
|
||||
// A typed override parameter over an inherited checked-value slot:
|
||||
// the body local narrows from the dyn argument at entry.
|
||||
const loc = locOf(decl);
|
||||
const slot = this.declareHiddenLocal(name, shape.type);
|
||||
params.push({ localId: slot.id, name, type: shape.type });
|
||||
const narrowed = this.coerceInto(
|
||||
decl.name,
|
||||
{ kind: "varRef", localId: slot.id, type: shape.type, loc },
|
||||
shape.entryNarrow,
|
||||
);
|
||||
const body = this.declareLocal(decl.name, name, shape.entryNarrow, true);
|
||||
prologue.push({ kind: "varDecl", localId: body.id, init: narrowed, loc });
|
||||
return;
|
||||
}
|
||||
if (shape.mode === "omittable" && shape.bodyType && decl.initializer) {
|
||||
const abi = shape.type;
|
||||
if (abi.kind === "dyn") {
|
||||
|
||||
@@ -25,24 +25,228 @@ function exportedFunction(decl: ts.Node): ts.Node | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a function's own body reads `this` (arrows share it; other
|
||||
* functions and classes have their own). */
|
||||
function readsOwnThis(fn: ts.Node): boolean {
|
||||
let found = false;
|
||||
/** Whether `node` is `f.apply(this, ...)` or `f.call(this, ...)`, which
|
||||
* passes the receiver on to `f`. */
|
||||
function forwardsThis(node: ts.Node): node is ts.CallExpression & {
|
||||
expression: ts.PropertyAccessExpression;
|
||||
} {
|
||||
if (!ts.isCallExpression(node)) return false;
|
||||
const callee = node.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return false;
|
||||
if (callee.name.text !== "apply" && callee.name.text !== "call") return false;
|
||||
return node.arguments.nodes[0]?.kind === ts.SyntaxKind.ThisKeyword;
|
||||
}
|
||||
|
||||
/* The arguments a call gave a function's parameters, while the analysis
|
||||
* follows the call into the function: each with the bindings of its own
|
||||
* scope. */
|
||||
type Bindings = ReadonlyMap<ts.Symbol, { expr: ts.Expression; bindings: Bindings }>;
|
||||
const noBindings: Bindings = new Map();
|
||||
|
||||
// How many calls and functions deep the analysis follows a forwarded
|
||||
// `this`: enough for a wrapper over a function factory, as Effect's
|
||||
// `dual(3, body).apply(this, arguments)`.
|
||||
const maxDepth = 6;
|
||||
|
||||
/** Whether `fn`'s own body uses `this` only to pass it on with `.apply` or
|
||||
* `.call` to functions that ignore it (and so also when it doesn't use
|
||||
* `this` at all). */
|
||||
function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth: number): boolean {
|
||||
let ignores = true;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (!ignores) return;
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
ignores = false;
|
||||
return;
|
||||
}
|
||||
if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node))
|
||||
return;
|
||||
if (forwardsThis(node)) {
|
||||
if (!ignoresThis(lowerer, node.expression.expression, bindings, depth + 1)) {
|
||||
ignores = false;
|
||||
return;
|
||||
}
|
||||
visit(node.expression.expression);
|
||||
for (const arg of node.arguments.nodes.slice(1)) visit(arg);
|
||||
return;
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
fn.forEachChild(visit);
|
||||
return ignores;
|
||||
}
|
||||
|
||||
/** The function `name` provably holds: a function declaration, a const
|
||||
* initialized with a function, or an import of one. */
|
||||
function functionOfName(
|
||||
lowerer: Lowerer,
|
||||
name: ts.Identifier,
|
||||
bindings: Bindings,
|
||||
): { fn: ts.Node | null; bound?: { expr: ts.Expression; bindings: Bindings } } {
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) return { fn: null };
|
||||
const bound = bindings.get(symbol);
|
||||
if (bound) return { fn: null, bound };
|
||||
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const decls = symbol.declarations;
|
||||
if (decls.length !== 1) return { fn: null };
|
||||
const decl = decls[0]!;
|
||||
if (ts.isFunctionDeclaration(decl)) return { fn: decl };
|
||||
if (
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0
|
||||
) {
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { fn: init };
|
||||
}
|
||||
return { fn: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `fn` (with its nested functions) may change what its parameter
|
||||
* `param` holds: an assignment, update, destructuring or for-in/for-of
|
||||
* target that writes it, a declaration of the same name, a write to an
|
||||
* element of `arguments` (which aliases parameters outside strict mode), or
|
||||
* a call of `eval`.
|
||||
*/
|
||||
function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param);
|
||||
const refersTo = (node: ts.Node): boolean =>
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === param.text || lowerer.checker.getSymbolAtLocation(node) === symbol);
|
||||
// Whether writing `target` writes the parameter: the name itself, or the
|
||||
// name inside a destructuring pattern.
|
||||
const writes = (target: ts.Node): boolean => {
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (refersTo(target)) return true;
|
||||
if (
|
||||
ts.isElementAccessExpression(target) &&
|
||||
ts.isIdentifier(target.expression) &&
|
||||
target.expression.text === "arguments"
|
||||
)
|
||||
return true;
|
||||
if (ts.isArrayLiteralExpression(target)) return target.elements.nodes.some(writes);
|
||||
if (ts.isObjectLiteralExpression(target))
|
||||
return target.properties.nodes.some((p) =>
|
||||
ts.isShorthandPropertyAssignment(p)
|
||||
? refersTo(p.name)
|
||||
: ts.isPropertyAssignment(p)
|
||||
? writes(p.initializer)
|
||||
: ts.isSpreadAssignment(p) && writes(p.expression),
|
||||
);
|
||||
if (ts.isSpreadElement(target)) return writes(target.expression);
|
||||
// A default in a pattern: `[body = other] = values`.
|
||||
if (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
||||
return writes(target.left);
|
||||
return false;
|
||||
};
|
||||
const declaresName = (node: ts.Node): boolean => {
|
||||
const name = (node as { name?: ts.Node }).name;
|
||||
return name !== undefined && ts.isIdentifier(name) && name.text === param.text;
|
||||
};
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
(ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
|
||||
writes(node.left)) ||
|
||||
((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
||||
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
node.operator === ts.SyntaxKind.MinusMinusToken) &&
|
||||
writes(node.operand)) ||
|
||||
((ts.isForOfStatement(node) || ts.isForInStatement(node)) &&
|
||||
!ts.isVariableDeclarationList(node.initializer) &&
|
||||
writes(node.initializer)) ||
|
||||
((ts.isVariableDeclaration(node) ||
|
||||
ts.isBindingElement(node) ||
|
||||
ts.isParameter(node) ||
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isClassDeclaration(node)) &&
|
||||
declaresName(node)) ||
|
||||
(ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "eval")
|
||||
) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
// The function's own parameters declare the name; their defaults can write it.
|
||||
const params = (fn as { parameters?: ts.NodeArray<ts.ParameterDeclaration> }).parameters;
|
||||
for (const p of params?.nodes ?? []) if (p.initializer) visit(p.initializer);
|
||||
const body = (fn as { body?: ts.Node }).body;
|
||||
if (body !== undefined) visit(body);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The expressions `fn` returns from its own body. */
|
||||
function returnedExpressions(fn: ts.Node): ts.Expression[] | null {
|
||||
const body = (fn as { body?: ts.Node }).body;
|
||||
if (body === undefined) return null;
|
||||
if (!ts.isBlock(body)) return [body as ts.Expression];
|
||||
const result: ts.Expression[] = [];
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
|
||||
if (ts.isReturnStatement(node) && node.expression) result.push(node.expression);
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
body.forEachChild(visit);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether the function `expr` evaluates to provably ignores the `this`
|
||||
* it is called with. */
|
||||
function ignoresThis(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
bindings: Bindings,
|
||||
depth: number,
|
||||
): boolean {
|
||||
if (depth > maxDepth) return false;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (ts.isArrowFunction(expr)) return true;
|
||||
if (ts.isFunctionExpression(expr)) return ignoresOwnThis(lowerer, expr, bindings, depth);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const { fn, bound } = functionOfName(lowerer, expr, bindings);
|
||||
if (bound) return ignoresThis(lowerer, bound.expr, bound.bindings, depth + 1);
|
||||
if (fn === null) return false;
|
||||
return ts.isArrowFunction(fn) || ignoresOwnThis(lowerer, fn, noBindings, depth + 1);
|
||||
}
|
||||
// A call of a known function: what it returns, with its parameters bound
|
||||
// to this call's arguments.
|
||||
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
|
||||
const { fn } = functionOfName(lowerer, expr.expression, bindings);
|
||||
if (fn === null) return false;
|
||||
const params = (fn as { parameters?: ts.NodeArray<ts.ParameterDeclaration> }).parameters;
|
||||
const inner = new Map(bindings);
|
||||
for (const [i, param] of (params?.nodes ?? []).entries()) {
|
||||
if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return false;
|
||||
const arg = expr.arguments.nodes[i];
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (arg === undefined || ts.isSpreadElement(arg) || symbol === undefined) return false;
|
||||
// The parameter holds the argument for as long as the function runs.
|
||||
if (mayRebind(lowerer, fn, param.name)) return false;
|
||||
inner.set(symbol, { expr: arg, bindings });
|
||||
}
|
||||
const returned = returnedExpressions(fn);
|
||||
return returned !== null && returned.every((r) => ignoresThis(lowerer, r, inner, depth + 1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether calling `fn` with another `this` than Node's can change what it
|
||||
* does: it reads `this`, or passes it on to a function that may. */
|
||||
function readsOwnThis(lowerer: Lowerer, fn: ts.Node): boolean {
|
||||
return !ignoresOwnThis(lowerer, fn, noBindings, 0);
|
||||
}
|
||||
|
||||
export function refuseModuleMemberThis(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -54,7 +258,7 @@ export function refuseModuleMemberThis(
|
||||
if (symbol === undefined) return;
|
||||
for (const decl of symbol.declarations) {
|
||||
const fn = exportedFunction(decl);
|
||||
if (fn === null || !readsOwnThis(fn)) continue;
|
||||
if (fn === null || !readsOwnThis(lowerer, fn)) continue;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
RUNTIME_ERROR_CLASSES,
|
||||
STRING,
|
||||
typeKey,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { helperLoc, strLit, varRef } from "../../ir/build.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
|
||||
/** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and
|
||||
* `write` return the receiver as the declaring class for a field access. */
|
||||
type BrandUse = "in" | "read" | "write";
|
||||
|
||||
const PREFIX = "%private.";
|
||||
|
||||
/** A function-local JavaScript class with private names. Its prototype
|
||||
* methods and accessors exist once per evaluation and check their receiver's
|
||||
* evaluation, so a method or accessor taken from an instance is the function
|
||||
* of that instance's own evaluation. */
|
||||
export function perEvaluationPrivateNames(info: ClassInfo): boolean {
|
||||
const declaration = info.localClass ? info.decl : null;
|
||||
return (
|
||||
!!declaration &&
|
||||
isJsSourceFile(declaration.getSourceFile()) &&
|
||||
declaration.members.nodes.some(
|
||||
(member) => member.name !== undefined && ts.isPrivateIdentifier(member.name),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** The function-local class declaring a private name, when its private names
|
||||
* need a per-evaluation brand check: the nearest enclosing class that
|
||||
* declares the name, as JavaScript scoping resolves it. */
|
||||
export function localPrivateBrandClass(
|
||||
lowerer: Lowerer,
|
||||
priv: ts.PrivateIdentifier,
|
||||
): { info: ClassInfo; decl: ts.ClassDeclaration & { name: ts.Identifier } } | null {
|
||||
for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) {
|
||||
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue;
|
||||
const declares = node.members.nodes.some((member) => {
|
||||
const name = (member as { name?: ts.PropertyName }).name;
|
||||
return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text;
|
||||
});
|
||||
if (!declares) continue;
|
||||
if (!ts.isClassDeclaration(node) || !node.name) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
const info =
|
||||
lowerer.currentClass?.decl === node
|
||||
? lowerer.currentClass
|
||||
: symbol
|
||||
? lowerer.classBySymbol.get(symbol)
|
||||
: undefined;
|
||||
if (!info?.localClass) return null;
|
||||
return { info, decl: node as ts.ClassDeclaration & { name: ts.Identifier } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Each evaluation of a function-local class creates fresh private names,
|
||||
* while one compiled class serves every evaluation. Its JavaScript prototype
|
||||
* methods are closures per evaluation whose `this` is a checked value, so a
|
||||
* private name on `this` is present exactly when the receiver is an instance
|
||||
* of the class (or a subclass) whose `%classEnvironment` field holds the
|
||||
* class value of the closure's own evaluation. The class's own binding names
|
||||
* that value (lowerClassExpressionInfo accepts only named declarations, whose
|
||||
* binding the checker keeps from being assigned). */
|
||||
export function checkedPrivateReceiver(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
decl: ts.ClassDeclaration & { name: ts.Identifier },
|
||||
access: ts.PropertyAccessExpression,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read";
|
||||
return brandCall(lowerer, info, decl, use, access.name.text, receiver, loc);
|
||||
}
|
||||
|
||||
/** Whether a checked value is a primitive: `#x in v` throws for those
|
||||
* before any brand lookup. */
|
||||
export function primitiveOperand(value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return (["nullish", "number", "string", "boolean", "bigint", "symbol"] as const)
|
||||
.map((test): IrExpr => ({ kind: "dynTest", test, value, type: BOOL, loc }))
|
||||
.reduce((left, right) => ({ kind: "logical", op: "||", left, right, type: BOOL, loc }));
|
||||
}
|
||||
|
||||
/** Node's TypeError message for `#x in v` on a primitive, which spells the
|
||||
* value as String(v) does. */
|
||||
export function privateInMessage(name: string, value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "strConcat",
|
||||
left: strLit(`Cannot use 'in' operator to search for '${name}' in `, loc),
|
||||
right: { kind: "libCall", fn: "dyn.stringConstructor", args: [value], type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A receiver checkedPrivateReceiver built for a field write. */
|
||||
export function isPrivateWriteReceiver(expr: IrExpr): boolean {
|
||||
return expr.kind === "call" && expr.callee.startsWith(`${PREFIX}write:`);
|
||||
}
|
||||
|
||||
/** `#x in v` for a checked value inside a function-local class: the brand
|
||||
* test above, after the TypeError a primitive operand throws. */
|
||||
export function checkedPrivateIn(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
decl: ts.ClassDeclaration & { name: ts.Identifier },
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
return brandCall(lowerer, info, decl, "in", name, receiver, loc);
|
||||
}
|
||||
|
||||
function brandCall(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
decl: ts.ClassDeclaration & { name: ts.Identifier },
|
||||
use: BrandUse,
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const className = info.def.name;
|
||||
const classType: IrType = { kind: "classval", className };
|
||||
const resultType: IrType = use === "in" ? BOOL : { kind: "object", className };
|
||||
const helper = `${PREFIX}${use}:${className}:${name}`;
|
||||
if (!lowerer.valueHelpers.has(helper)) {
|
||||
const at = helperLoc(loc);
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "value.0", name: "value", type: DYN },
|
||||
{ localId: "class.0", name: "class", type: classType },
|
||||
],
|
||||
returnType: resultType,
|
||||
locals: [
|
||||
{ id: "value.0", name: "value", type: DYN, mutable: false },
|
||||
{ id: "class.0", name: "class", type: classType, mutable: false },
|
||||
],
|
||||
// finalizePrivateBrands fills the body once every class is collected,
|
||||
// including subclasses declared after this use.
|
||||
body: [],
|
||||
loc: at,
|
||||
});
|
||||
lowerer.valueHelpers.set(helper, helper);
|
||||
}
|
||||
const evaluation = lowerer.classValueRef(info, decl.name);
|
||||
return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc };
|
||||
}
|
||||
|
||||
/** A plain assignment, a destructuring target, or a for-in/of target writes
|
||||
* the field. Compound assignments and updates read it first, and Node
|
||||
* reports the failed read. */
|
||||
function isPrivateWriteTarget(access: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = access;
|
||||
for (;;) {
|
||||
const parent = node.parent;
|
||||
if (ts.isParenthesizedExpression(parent)) {
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
if (ts.isBinaryExpression(parent))
|
||||
return parent.left === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
if (ts.isForOfStatement(parent) || ts.isForInStatement(parent))
|
||||
return parent.initializer === node;
|
||||
// Inside a destructuring pattern: keep walking to the assignment.
|
||||
if (
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isSpreadElement(parent) ||
|
||||
(ts.isPropertyAssignment(parent) && parent.initializer === node) ||
|
||||
ts.isObjectLiteralExpression(parent) ||
|
||||
ts.isSpreadAssignment(parent)
|
||||
) {
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fill the brand helpers: one branch per class a checked reference to an
|
||||
* instance can be boxed under (the declarer, its subclasses, and its
|
||||
* ancestors when the instance is at or below the declarer). */
|
||||
export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
for (const fn of functions) {
|
||||
if (!fn.name.startsWith(PREFIX) || fn.body.length > 0) continue;
|
||||
// `%private.<use>:<class>:<#name>`; class names may contain colons.
|
||||
const spec = fn.name.slice(PREFIX.length);
|
||||
const use = spec.slice(0, spec.indexOf(":")) as BrandUse;
|
||||
const className = spec.slice(use.length + 1, spec.lastIndexOf(":"));
|
||||
const name = spec.slice(spec.lastIndexOf(":") + 1);
|
||||
const loc = fn.loc;
|
||||
const value = varRef("value.0", DYN, loc);
|
||||
const evaluation = varRef("class.0", { kind: "classval", className }, loc);
|
||||
const ownerType: IrType = { kind: "object", className };
|
||||
const body: IrStmt[] = [];
|
||||
let matches = 0;
|
||||
for (const candidate of lowerer.classes.values()) {
|
||||
const candidateName = candidate.def.name;
|
||||
if (RUNTIME_ERROR_CLASSES.has(candidateName)) continue;
|
||||
const below = candidateName === className || lowerer.isSubclassOf(candidateName, className);
|
||||
const above = lowerer.isSubclassOf(className, candidateName);
|
||||
if (!below && !above) continue;
|
||||
const candidateType: IrType = { kind: "object", className: candidateName };
|
||||
const checked: IrExpr = { kind: "dynCheck", value, type: candidateType, loc };
|
||||
const local = `receiver.${matches++}`;
|
||||
fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false });
|
||||
const receiver = varRef(local, ownerType, loc);
|
||||
const sameEvaluation: IrExpr = {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: {
|
||||
kind: "fieldGet",
|
||||
obj: receiver,
|
||||
className,
|
||||
field: `%classEnvironment:${className}`,
|
||||
type: evaluation.type,
|
||||
loc,
|
||||
},
|
||||
right: evaluation,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const match = (typed: IrExpr): IrStmt[] => [
|
||||
{ kind: "varDecl", localId: local, init: typed, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: sameEvaluation,
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: use === "in" ? { kind: "boolLit", value: true, type: BOOL, loc } : receiver,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const then: IrStmt[] = below
|
||||
? match(
|
||||
candidateName === className
|
||||
? checked
|
||||
: { kind: "upcast", value: checked, type: ownerType, loc },
|
||||
)
|
||||
: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "instanceOf", value: checked, className, type: BOOL, loc },
|
||||
then: match({ kind: "downcast", value: checked, type: ownerType, loc }),
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [value, strLit(typeKey(candidateType), loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then,
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const typeError = (message: IrExpr): IrStmt => ({
|
||||
kind: "throw",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "error.new",
|
||||
args: [message],
|
||||
type: { kind: "object", className: "%TypeError" },
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
if (use === "in") {
|
||||
body.push(
|
||||
{
|
||||
kind: "if",
|
||||
cond: primitiveOperand(value, loc),
|
||||
then: [typeError(privateInMessage(name, value, loc))],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
);
|
||||
} else {
|
||||
const verb = use === "read" ? "reading" : "setting";
|
||||
const action = use === "read" ? "Cannot read properties" : "Cannot set properties";
|
||||
for (const unit of ["undefined", "null"] as const) {
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: unit, value, type: BOOL, loc },
|
||||
then: [typeError(strLit(`${action} of ${unit} (${verb} '${name}')`, loc))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
body.push(
|
||||
typeError(
|
||||
strLit(
|
||||
use === "read"
|
||||
? `Cannot read private member ${name} from an object whose class did not declare it`
|
||||
: `Cannot write private member ${name} to an object whose class did not declare it`,
|
||||
loc,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
fn.body = body;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { bindingEarlyUse7 } from "../program.js";
|
||||
import {
|
||||
bindingEarlyUse7,
|
||||
cjsExportDiscardReason,
|
||||
isCjsExportTableLiteral,
|
||||
isModuleExportsAccess,
|
||||
} from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
|
||||
@@ -39,7 +44,7 @@ function constantSymbolDescription(
|
||||
}
|
||||
|
||||
export function symbolFieldDisplayName(field: string): string {
|
||||
return field.slice(FIELD_PREFIX.length);
|
||||
return field.slice(FIELD_PREFIX.length).replace(/^\d+:/, "");
|
||||
}
|
||||
|
||||
/** CopyDataProperties includes symbols, unlike the string-keyed view used
|
||||
@@ -96,6 +101,11 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
cache.set(sym, null);
|
||||
const decls = sym.declarations;
|
||||
const decl = sym.valueDeclaration;
|
||||
if (decls.length === 1 && decl && ts.isPropertyAssignment(decl)) {
|
||||
const key = cjsTableSymbolKeyOf(lowerer, sym, decl);
|
||||
if (key) cache.set(sym, key);
|
||||
return key;
|
||||
}
|
||||
if (decls.length !== 1 || !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name))
|
||||
return null;
|
||||
const list = decl.parent;
|
||||
@@ -124,21 +134,9 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
cache.set(sym, alias);
|
||||
return alias;
|
||||
}
|
||||
if (!init || !ts.isCallExpression(init) || init.questionDotToken) return null;
|
||||
const callee = init.expression;
|
||||
const registered =
|
||||
ts.isPropertyAccessExpression(callee) && !callee.questionDotToken && callee.name.text === "for";
|
||||
const root = registered ? callee.expression : callee;
|
||||
if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null;
|
||||
const arg =
|
||||
init.arguments.nodes.length === 0
|
||||
? null
|
||||
: init.arguments.nodes.length === 1
|
||||
? init.arguments.nodes[0]!
|
||||
: undefined;
|
||||
if (arg === undefined || (registered && arg === null)) return null;
|
||||
const description = arg === null ? "" : constantSymbolDescription(lowerer, arg);
|
||||
if (description === null) return null;
|
||||
const call = init ? literalSymbolCallOf(lowerer, init) : null;
|
||||
if (!call) return null;
|
||||
const { registered, description } = call;
|
||||
// Ordinary Symbol() is a fresh identity per evaluation, so only the
|
||||
// module-scope declaration above can name one fixed layout slot.
|
||||
const sf = decl.getSourceFile();
|
||||
@@ -184,6 +182,18 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
) !== null
|
||||
)
|
||||
return null;
|
||||
const key = symbolKeyOf(lowerer, sym, registered, description);
|
||||
cache.set(sym, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function symbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
sym: ts.Symbol,
|
||||
registered: boolean,
|
||||
description: string,
|
||||
table = false,
|
||||
): ClassSymbolKey {
|
||||
// A registry name denotes one symbol even when several source bindings
|
||||
// initialize it. Intern those names into a representative checker symbol;
|
||||
// the layout table then has one identity-keyed domain for both forms.
|
||||
@@ -193,16 +203,175 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
if (existing) identity = existing;
|
||||
else lowerer.registeredClassSymbols.set(description, sym);
|
||||
}
|
||||
const key: ClassSymbolKey = {
|
||||
// Distinct identities can share a description (undici's core and mock
|
||||
// key tables both hold a `Symbol('close')`, and MockClient inherits one
|
||||
// and declares the other). A key-table key whose description an earlier
|
||||
// identity already holds takes an ordinal that keeps its slot apart; the
|
||||
// printable name drops it. Const keys keep their plain names.
|
||||
let identities = lowerer.symbolFieldIdentities.get(description);
|
||||
if (!identities) lowerer.symbolFieldIdentities.set(description, (identities = []));
|
||||
let ordinal = identities.indexOf(identity);
|
||||
if (ordinal < 0) ordinal = identities.push(identity) - 1;
|
||||
return {
|
||||
sym,
|
||||
identity,
|
||||
// Symbol slots are not string properties. The reserved prefix keeps
|
||||
// them out of the native class's string-keyed dynamic view, including
|
||||
// Object.keys/JSON serialization and string-property writeback.
|
||||
fieldName: `${FIELD_PREFIX}Symbol(${description})`,
|
||||
fieldName: `${FIELD_PREFIX}${table && ordinal > 0 ? `${ordinal}:` : ""}Symbol(${description})`,
|
||||
};
|
||||
cache.set(sym, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/** `Symbol(<constant>)` or `Symbol.for(<constant>)` on the standard global. */
|
||||
function literalSymbolCallOf(
|
||||
lowerer: Lowerer,
|
||||
init: ts.Expression,
|
||||
): { registered: boolean; description: string } | null {
|
||||
if (!ts.isCallExpression(init) || init.questionDotToken) return null;
|
||||
const callee = init.expression;
|
||||
const registered =
|
||||
ts.isPropertyAccessExpression(callee) && !callee.questionDotToken && callee.name.text === "for";
|
||||
const root = registered ? callee.expression : callee;
|
||||
if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null;
|
||||
const arg =
|
||||
init.arguments.nodes.length === 0
|
||||
? null
|
||||
: init.arguments.nodes.length === 1
|
||||
? init.arguments.nodes[0]!
|
||||
: undefined;
|
||||
if (arg === undefined || (registered && arg === null)) return null;
|
||||
const description = arg === null ? "" : constantSymbolDescription(lowerer, arg);
|
||||
return description === null ? null : { registered, description };
|
||||
}
|
||||
|
||||
/** A key table property: `kX: Symbol('x')` in a JS module's effective
|
||||
* top-level `module.exports = { ... }` literal (undici's core/symbols.js).
|
||||
* The module evaluates once, so the property names one symbol identity for
|
||||
* every requirer, as a module-scope const does. Requirers can observe the
|
||||
* table only after its assignment completes, provided nothing before it
|
||||
* can run code (a require there could close a cycle and publish the
|
||||
* initial empty exports object instead). Any write to the property, or
|
||||
* replacement through `exports`/`module.exports`, keeps it unstable. */
|
||||
function cjsTableSymbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
sym: ts.Symbol,
|
||||
decl: ts.PropertyAssignment,
|
||||
): ClassSymbolKey | null {
|
||||
const table = decl.parent;
|
||||
if (!ts.isObjectLiteralExpression(table) || !ts.isIdentifier(decl.name)) return null;
|
||||
const assign = table.parent;
|
||||
if (
|
||||
!ts.isBinaryExpression(assign) ||
|
||||
assign.right !== table ||
|
||||
!isCjsExportTableLiteral(table) ||
|
||||
!ts.isExpressionStatement(assign.parent)
|
||||
)
|
||||
return null;
|
||||
const stmt = assign.parent;
|
||||
const sf = stmt.getSourceFile();
|
||||
if (cjsExportDiscardReason(stmt) !== null) return null;
|
||||
const call = literalSymbolCallOf(lowerer, decl.initializer);
|
||||
if (!call) return null;
|
||||
const inert = (expr: ts.Expression | undefined): boolean => {
|
||||
if (!expr) return true;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
return (
|
||||
ts.isIdentifier(expr) ||
|
||||
ts.isArrowFunction(expr) ||
|
||||
ts.isFunctionExpression(expr) ||
|
||||
ts.isStringLiteralLike(expr) ||
|
||||
ts.isNumericLiteral(expr) ||
|
||||
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
expr.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
expr.kind === ts.SyntaxKind.NullKeyword ||
|
||||
literalSymbolCallOf(lowerer, expr) !== null
|
||||
);
|
||||
};
|
||||
const statements = sf.statements.nodes;
|
||||
const prefixInert = statements
|
||||
.slice(0, statements.indexOf(stmt))
|
||||
.every(
|
||||
(statement) =>
|
||||
(ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) ||
|
||||
ts.isFunctionDeclaration(statement) ||
|
||||
(ts.isVariableStatement(statement) &&
|
||||
statement.declarationList.declarations.nodes.every(
|
||||
(d) => ts.isIdentifier(d.name) && inert(d.initializer),
|
||||
)),
|
||||
);
|
||||
if (!prefixInert) return null;
|
||||
for (const property of table.properties.nodes) {
|
||||
if (property === decl) break;
|
||||
if (ts.isShorthandPropertyAssignment(property)) continue;
|
||||
if (
|
||||
!ts.isPropertyAssignment(property) ||
|
||||
ts.isComputedPropertyName(property.name) ||
|
||||
!inert(property.initializer)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
// Requirers can reach the property through a checker-synthesized symbol
|
||||
// (the export= type's member); the declaration's own symbol is the one
|
||||
// identity every requirer and the table's export global share.
|
||||
const canonical = lowerer.checker.getSymbolAtLocation(decl.name) ?? sym;
|
||||
if (tablePropertyWritten(lowerer, new Set([sym, canonical]), decl.name.text, sf)) return null;
|
||||
return { ...symbolKeyOf(lowerer, canonical, call.registered, call.description, true), sym };
|
||||
}
|
||||
|
||||
function tablePropertyWritten(
|
||||
lowerer: Lowerer,
|
||||
symbols: ReadonlySet<ts.Symbol>,
|
||||
name: string,
|
||||
home: ts.SourceFile,
|
||||
): boolean {
|
||||
let written = false;
|
||||
const targets = (node: ts.Node): boolean => {
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text !== name) return false;
|
||||
const recv = node.expression;
|
||||
if (
|
||||
node.getSourceFile() === home &&
|
||||
((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))
|
||||
)
|
||||
return true;
|
||||
const target = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
return target !== undefined && symbols.has(target);
|
||||
}
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
const key = node.argumentExpression;
|
||||
if (!ts.isStringLiteralLike(key) || key.text !== name) return false;
|
||||
const target = lowerer.checker.getSymbolAtLocation(node);
|
||||
return target !== undefined && symbols.has(target);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const sf of lowerer.program.getSourceFiles()) {
|
||||
if (written) break;
|
||||
if (sf.isDeclarationFile || !sf.text.includes(name)) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (written) return "stop";
|
||||
let target: ts.Node | undefined;
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
target = node.left;
|
||||
else if (ts.isDeleteExpression(node)) target = node.expression;
|
||||
else if (
|
||||
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
||||
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
node.operator === ts.SyntaxKind.MinusMinusToken)
|
||||
)
|
||||
target = node.operand;
|
||||
if (target && targets(target)) {
|
||||
written = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): boolean {
|
||||
@@ -245,3 +414,29 @@ function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): bo
|
||||
});
|
||||
return written;
|
||||
}
|
||||
|
||||
/** The layout type of a symbol-keyed field read through a CommonJS
|
||||
* key-table key (`this[kCount]` with `kCount` from a `module.exports = {
|
||||
* kCount: Symbol('count') }` table), or null. The checker types such keys
|
||||
* as plain `symbol`, so it answers the read with the union of every
|
||||
* symbol-keyed property of the class; the field's own type is the read's
|
||||
* type. Keys declared as consts keep their unique-symbol checker types. */
|
||||
export function tableKeyFieldType(
|
||||
lowerer: Lowerer,
|
||||
node: ts.ElementAccessExpression,
|
||||
): IrType | null {
|
||||
const key = node.argumentExpression;
|
||||
if (!ts.isIdentifier(key) && !ts.isPropertyAccessExpression(key)) return null;
|
||||
const keyType = lowerer.checker.getTypeAtLocation(key);
|
||||
if (!(keyType.flags & ts.TypeFlags.ESSymbol) || keyType.flags & ts.TypeFlags.UniqueESSymbol)
|
||||
return null;
|
||||
const resolved = classSymbolKeyOf(lowerer, key);
|
||||
if (!resolved) return null;
|
||||
const decl = resolved.identity.valueDeclaration;
|
||||
if (!decl || !ts.isPropertyAssignment(decl)) return null;
|
||||
const receiver = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(node.expression));
|
||||
if (receiver?.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(receiver.className);
|
||||
const field = info?.symbolFields?.get(resolved.identity);
|
||||
return field === undefined ? null : (info!.fields.get(field) ?? null);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
threadSlotsMemberOf,
|
||||
} from "../experimental-import.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { withUndefinedArm } from "../type-mapper.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { locOf } from "../program.js";
|
||||
@@ -238,10 +239,16 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression):
|
||||
const args = [name, receiver, key];
|
||||
switch (member) {
|
||||
case "get": {
|
||||
// The slots hold values in the layout of the type the constructor registered (`family.value`,
|
||||
// which `set` stores with). The receiver's annotation can name a structurally wider value
|
||||
// type, so the value read is converted to the call's type like any value flowing into it.
|
||||
const resultTs = lowerer.typeOf(expr);
|
||||
const result = lowerer.mapTypeOf(resultTs);
|
||||
if (result === null) lowerer.badType(expr, resultTs);
|
||||
return { kind: "intrinsic", name: "slots.get", args, type: result!, loc };
|
||||
const stored = withUndefinedArm(family.value, lowerer.unions);
|
||||
if (stored === null) lowerer.badType(expr, resultTs);
|
||||
const read: IrExpr = { kind: "intrinsic", name: "slots.get", args, type: stored!, loc };
|
||||
return typeKey(stored!) === typeKey(result!) ? read : lowerer.coerceInto(expr, read, result!);
|
||||
}
|
||||
case "has":
|
||||
case "delete":
|
||||
|
||||
@@ -5967,10 +5967,15 @@ export function isModuleExportsAccess(expr: ts.Expression): expr is ts.PropertyA
|
||||
* - member: `exports.f = <expr>` / `module.exports.f = <expr>`.
|
||||
* Null for everything else. Callers gate on top-level position in a JS
|
||||
* module file — these forms anywhere else keep their generic fences. */
|
||||
export function cjsExportAssignmentOf(
|
||||
stmt: ts.Statement,
|
||||
):
|
||||
| { kind: "table"; obj: ts.ObjectLiteralExpression | null; expr: ts.BinaryExpression }
|
||||
export function cjsExportAssignmentOf(stmt: ts.Statement):
|
||||
| {
|
||||
kind: "table";
|
||||
obj: ts.ObjectLiteralExpression | null;
|
||||
expr: ts.BinaryExpression;
|
||||
/** `exports = module.exports = value`: the `exports` binding names
|
||||
* the new value too, so later `exports.f =` members attach to it. */
|
||||
rebindsExports?: true;
|
||||
}
|
||||
| { kind: "member"; name: ts.MemberName; value: ts.Expression; expr: ts.BinaryExpression }
|
||||
| null {
|
||||
if (!ts.isExpressionStatement(stmt)) return null;
|
||||
@@ -5979,6 +5984,20 @@ export function cjsExportAssignmentOf(
|
||||
if (isModuleExportsAccess(e.left)) {
|
||||
return { kind: "table", obj: ts.isObjectLiteralExpression(e.right) ? e.right : null, expr: e };
|
||||
}
|
||||
if (ts.isIdentifier(e.left) && e.left.text === "exports") {
|
||||
let inner: ts.Expression = e.right;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
if (
|
||||
ts.isBinaryExpression(inner) &&
|
||||
inner.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isModuleExportsAccess(inner.left) &&
|
||||
// `exports = module.exports = {}` is the fresh-table reset, which the
|
||||
// export walk handles on its own.
|
||||
!ts.isObjectLiteralExpression(inner.right)
|
||||
) {
|
||||
return { kind: "table", obj: null, expr: inner, rebindsExports: true };
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(e.left) && !e.left.questionDotToken) {
|
||||
const recv = e.left.expression;
|
||||
const isExports = ts.isIdentifier(recv) && recv.text === "exports";
|
||||
@@ -6110,6 +6129,8 @@ export function cjsExportDiscardReason(stmt: ts.Statement): string | null {
|
||||
return "export members attached before a 'module.exports =' assignment (Node replaces the export object — importers never see this member; attach it to module.exports AFTER the table)";
|
||||
}
|
||||
const left = mine.expr.left as ts.PropertyAccessExpression;
|
||||
const last = cjsExportAssignmentOf(stmts[lastTable]!);
|
||||
if (last?.kind === "table" && last.rebindsExports === true) return null;
|
||||
return ts.isIdentifier(left.expression) && left.expression.text === "exports"
|
||||
? "'exports.f =' after a 'module.exports =' assignment ('exports' still references the replaced object — Node discards this member; write module.exports.f instead)"
|
||||
: null;
|
||||
|
||||
@@ -325,7 +325,7 @@ export class ShapeRegistry {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
shape.declaredOrder = declaredOrder;
|
||||
shape.declaredOrder = this.withExtendedNames(shape.id, declaredOrder);
|
||||
this.declaredOrderPriority.set(shape.id, this.currentDeclaredOrderPriority);
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ export class ShapeRegistry {
|
||||
}
|
||||
const shape = this.byId.get(shapeId);
|
||||
if (!shape) continue;
|
||||
shape.declaredOrder = best.order;
|
||||
shape.declaredOrder = this.withExtendedNames(shapeId, best.order);
|
||||
this.declaredOrderPriority.set(shapeId, best.priority);
|
||||
}
|
||||
for (const finalize of this.derivedDeclaredOrderFinalizers) finalize();
|
||||
@@ -374,7 +374,7 @@ export class ShapeRegistry {
|
||||
// still obeys first-writer-wins.
|
||||
if (shape?.declaredOrder !== originalOrder) return;
|
||||
const next = order();
|
||||
if (next !== undefined) shape.declaredOrder = next;
|
||||
if (next !== undefined) shape.declaredOrder = this.withExtendedNames(shape.id, next);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -482,6 +482,53 @@ export class ShapeRegistry {
|
||||
get(shapeId: string): IrRecordShape | undefined {
|
||||
return this.byId.get(shapeId);
|
||||
}
|
||||
|
||||
/** Names extendShape added to a shape, which every key-order table the
|
||||
* shape adopts lists after its declared names. */
|
||||
private readonly extendedNames = new Map<string, string[]>();
|
||||
|
||||
/** The members extendShape attached to a shape (empty for most). */
|
||||
attachedNames(shapeId: string): readonly string[] {
|
||||
return this.extendedNames.get(shapeId) ?? [];
|
||||
}
|
||||
|
||||
private withExtendedNames(shapeId: string, order: string[]): string[] {
|
||||
const extra = this.extendedNames.get(shapeId);
|
||||
if (!extra) return order;
|
||||
const missing = extra.filter((name) => !order.includes(name));
|
||||
return missing.length === 0 ? order : [...order, ...missing];
|
||||
}
|
||||
|
||||
/** Adds optional fields to an interned shape in place: members a type
|
||||
* assertion attaches to the shape's values (`r as R & { tag: string }`
|
||||
* followed by `view.tag = ...`; see assertion-members.ts). Every value
|
||||
* of the shape holds them absent until a write through such a view, so
|
||||
* the shape keeps its id, both structural keys resolve to it, and its
|
||||
* key-order table lists the new names after the declared ones. Runs
|
||||
* before any body lowers; existing layouts change, so the revision
|
||||
* advances like a closing recursive placeholder. */
|
||||
extendShape(shapeId: string, extra: readonly { name: string; type: IrType }[]): void {
|
||||
const shape = this.byId.get(shapeId);
|
||||
if (!shape || shape.tuple || extra.length === 0) return;
|
||||
const fresh = extra.filter((f) => !shape.fields.some((x) => x.name === f.name));
|
||||
if (fresh.length === 0) return;
|
||||
shape.fields = [...shape.fields, ...fresh].sort((a, b) =>
|
||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
||||
);
|
||||
this.extendedNames.set(shapeId, [
|
||||
...(this.extendedNames.get(shapeId) ?? []),
|
||||
...fresh.map((f) => f.name),
|
||||
]);
|
||||
if (shape.declaredOrder)
|
||||
shape.declaredOrder = this.withExtendedNames(shapeId, shape.declaredOrder);
|
||||
const candidates = this.declaredOrderCandidates.get(shapeId);
|
||||
if (candidates) {
|
||||
for (const c of candidates) c.order = this.withExtendedNames(shapeId, c.order);
|
||||
}
|
||||
this.revision++;
|
||||
const key = this.keyOf(shape.fields, false, shape.indexValue);
|
||||
if (!this.byKey.has(key)) this.byKey.set(key, shapeId);
|
||||
}
|
||||
}
|
||||
|
||||
/** The frontend's union interner — mirrors ShapeRegistry. A union's
|
||||
@@ -2038,12 +2085,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// {}` — classNamer spells them "%anon" (unique per file), the same
|
||||
// name their collection registered.
|
||||
!classDecl.getSourceFile().isDeclarationFile &&
|
||||
checker.getConstructSignatures(widened).length === 0
|
||||
checker.getConstructSignatures(widened).length === 0 &&
|
||||
// A TypeScript class declared OUTSIDE the lowered program (reached
|
||||
// through `import type` alone) never registers, and the program can
|
||||
// hold no instance of it: its instance type maps below as the
|
||||
// structural type it describes, like an interface.
|
||||
(ctx.isProgramFile(classDecl.getSourceFile()) || isJsSourceFile(classDecl.getSourceFile()))
|
||||
) {
|
||||
// Declared OUTSIDE the lowered program (reached through the type world
|
||||
// alone — jsdoc `typeof import()` over a never-required module): the
|
||||
// class will never register, so the instance type is unmappable, not
|
||||
// an object type naming a struct that cannot exist.
|
||||
// A JavaScript class outside the lowered program (jsdoc `typeof
|
||||
// import()` over a never-required module) stays unmappable: JSDoc
|
||||
// types are not checked, so its slots fall back to the dynamic value.
|
||||
if (!ctx.isProgramFile(classDecl.getSourceFile())) return null;
|
||||
// The class inside a MIXIN function: per-call-site instantiations
|
||||
// share this one node — the CURRENT instantiation (collecting or
|
||||
@@ -5903,8 +5954,8 @@ function coveredOptionalFieldsShape(
|
||||
const memberArms = unions.get(optionalSlot.unionId)?.arms ?? [];
|
||||
const slotArms =
|
||||
slot.kind === "union" ? (unions.get(slot.unionId)?.arms ?? []) : isUnitType(slot) ? [] : [slot];
|
||||
const definedArms = memberArms.filter((arm) => arm.kind !== "undefinedT");
|
||||
const slotDefined = slotArms.filter((arm) => arm.kind !== "undefinedT");
|
||||
const definedArms = memberArms.filter((arm): boolean => arm.kind !== "undefinedT");
|
||||
const slotDefined = slotArms.filter((arm): boolean => arm.kind !== "undefinedT");
|
||||
if (
|
||||
definedArms.length === 0 ||
|
||||
definedArms.length !== slotDefined.length ||
|
||||
|
||||
@@ -448,6 +448,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"https.requestFn",
|
||||
"https.requestFnCb",
|
||||
"rl.question",
|
||||
"rl.questionDyn",
|
||||
"rl.checkOptions",
|
||||
"rl.checkCompleter",
|
||||
"readable.fromArr",
|
||||
@@ -470,6 +471,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"rl.resume",
|
||||
"rl.prompt",
|
||||
"rl.promptWith",
|
||||
"rl.setPromptDyn",
|
||||
"rl.getPrompt",
|
||||
"rl.cursorPos",
|
||||
"rl.inputFromDyn",
|
||||
"rl.outputFromDyn",
|
||||
"rl.write",
|
||||
"rl.writeKey",
|
||||
"rl.cursor",
|
||||
|
||||
@@ -3499,6 +3499,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* throws err when unhandled. count/countFn/names/listeners/getMax/
|
||||
* setMax/setDefaultMax/getDefaultMax are the introspection surface. */
|
||||
"emitter.new": { argTypes: [], result: VOID },
|
||||
/** `x.constructor.name` of an emitter or stream (a borrowed object of
|
||||
* EventEmitter's hierarchy): the +1 class name the instance records. */
|
||||
"emitter.className": { argTypes: [null], result: STRING },
|
||||
"emitter.ctor": { argTypes: [null], result: VOID },
|
||||
"emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID },
|
||||
"emitter.off": { argTypes: [null, STRING, null], result: VOID },
|
||||
@@ -4016,6 +4019,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
argTypes: [F64, STRING, DYN],
|
||||
result: { kind: "promise", inner: STRING },
|
||||
},
|
||||
/** The question forms with a checked-dynamic query (a JavaScript value
|
||||
* that need not be a string). */
|
||||
"rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID },
|
||||
"rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } },
|
||||
"rl.questionAsyncSignalDyn": {
|
||||
argTypes: [F64, DYN, DYN],
|
||||
result: { kind: "promise", inner: STRING },
|
||||
},
|
||||
"rl.close": { argTypes: [F64], result: VOID },
|
||||
"rl.pause": { argTypes: [F64], result: VOID },
|
||||
"rl.resume": { argTypes: [F64], result: VOID },
|
||||
@@ -4023,6 +4034,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** prompt(preserveCursor): [handle, preserveCursor] */
|
||||
"rl.promptWith": { argTypes: [F64, BOOL], result: VOID },
|
||||
"rl.setPrompt": { argTypes: [F64, STRING], result: VOID },
|
||||
"rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID },
|
||||
"rl.getPrompt": { argTypes: [F64], result: STRING },
|
||||
"rl.write": { argTypes: [F64, STRING], result: VOID },
|
||||
/** write(data, key): [handle, hasData, data, hasKey, hasName, name,
|
||||
@@ -4055,6 +4067,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID },
|
||||
"rl.line": { argTypes: [F64], result: STRING },
|
||||
"rl.isTerminal": { argTypes: [F64], result: BOOL },
|
||||
"rl.isPromises": { argTypes: [F64], result: BOOL },
|
||||
/** createInterface's input or output as a checked-dynamic value:
|
||||
* [value, output] — the stream; refused when it is not one. */
|
||||
"rl.inputFromDyn": { argTypes: [DYN, BOOL], result: { kind: "object", className: "%Readable" } },
|
||||
"rl.outputFromDyn": {
|
||||
argTypes: [DYN, BOOL],
|
||||
result: { kind: "object", className: "%Writable" },
|
||||
},
|
||||
"rl.cursor": { argTypes: [F64], result: F64 },
|
||||
/** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq
|
||||
* [kind (0-3), the first and second numbers, whether the second is
|
||||
|
||||
@@ -3891,6 +3891,9 @@ function isJsonSafeAt(
|
||||
const shape = getRecord(t.shapeId);
|
||||
if (!shape) return false;
|
||||
if (recordTextCodecClass(shape) !== null) return false;
|
||||
// Accessor slots are not function-valued properties: Node's
|
||||
// JSON.stringify invokes the getter (see accessorSlotProp).
|
||||
if (shapeHasAccessorSlots(shape)) return false;
|
||||
// The recursive knot: answer true and let the rest of the graph
|
||||
// decide (any unsafe constituent ends the traversal immediately).
|
||||
if (visiting.has(t.shapeId)) return true;
|
||||
@@ -4228,6 +4231,9 @@ function canBoxDynCompositeAt(
|
||||
const shape = getRecord(t.shapeId);
|
||||
if (!shape) return false;
|
||||
if (recordTextCodecClass(shape) !== null) return false;
|
||||
// Accessor slots hold the getter and setter closures, not property
|
||||
// values (see accessorSlotProp).
|
||||
if (shapeHasAccessorSlots(shape)) return false;
|
||||
// Recursive shapes answer coinductively, like isJsonSafeType.
|
||||
if (visiting.has(t.shapeId)) return true;
|
||||
visiting.add(t.shapeId);
|
||||
|
||||
@@ -9,59 +9,6 @@ import {
|
||||
} from "./ir.js";
|
||||
import { everyModuleNode } from "./traverse.js";
|
||||
|
||||
export interface RuntimeFeatures {
|
||||
regex: boolean;
|
||||
copying: boolean;
|
||||
legacyTextDecoder: boolean;
|
||||
fileHandle: boolean;
|
||||
fetch: boolean;
|
||||
eventTarget: boolean;
|
||||
processEvents: boolean;
|
||||
/** process.emitWarning is assigned (process.setEmitWarning). Declared
|
||||
* in the literal's order: JSON.stringify of the features record must
|
||||
* match between the Node-run and self-hosted compilers. */
|
||||
warningReplace: boolean;
|
||||
/** stdin setEncoding / string data listeners / stdio 'resize' events. */
|
||||
stdioExtended: boolean;
|
||||
emitter: boolean;
|
||||
stream: boolean;
|
||||
zlib: boolean;
|
||||
dc: boolean;
|
||||
assert: boolean;
|
||||
dynInvoke: boolean;
|
||||
dynAsync: boolean;
|
||||
/** The program has dynamic-tier jsval operations (js.* builtins, the
|
||||
* jsval pass): links the scr_js_* units (two-tier design I3). */
|
||||
dynTier: boolean;
|
||||
/** The program enters AsyncLocalStorage stores (store.run, enterWith,
|
||||
* a diagnostics_channel store binding) or reads the test runner's
|
||||
* context (getTestContext): its timer, immediate, tick and
|
||||
* queueMicrotask scheduling goes through the runtime's *_als entry
|
||||
* points, which carry the active stores into each callback. */
|
||||
asyncContext: boolean;
|
||||
inspect: boolean;
|
||||
childProcess: boolean;
|
||||
net: boolean;
|
||||
symbol: boolean;
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
parseArgs: boolean;
|
||||
fsWatch: boolean;
|
||||
nodeTest: boolean;
|
||||
/** The program calls mock.timers members: its timer calls go through
|
||||
* scr_test.c's mock-aware entry points. */
|
||||
mockTimers: boolean;
|
||||
dgram: boolean;
|
||||
http: boolean;
|
||||
http2: boolean;
|
||||
tls: boolean;
|
||||
tlsCa: boolean;
|
||||
cipher: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */
|
||||
webcrypto: boolean;
|
||||
}
|
||||
|
||||
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// DEP0040 rides the dyn-async unit's warning machinery.
|
||||
"punycode.warn",
|
||||
@@ -204,6 +151,8 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fileHandle.createWriteStream",
|
||||
"fs.createReadStream",
|
||||
"fs.createWriteStream",
|
||||
"rl.inputFromDyn",
|
||||
"rl.outputFromDyn",
|
||||
"sp.finished",
|
||||
"sp.pipeline",
|
||||
"sp.pipelineStages",
|
||||
@@ -311,9 +260,6 @@ const COPYING_BYTES_METHODS: ReadonlySet<string> = new Set([
|
||||
"with",
|
||||
]);
|
||||
|
||||
const startsWithAny = (fn: string, prefixes: readonly string[]): boolean =>
|
||||
prefixes.some((prefix) => fn.startsWith(prefix));
|
||||
|
||||
/** Inspect types and expressions together in one traversal. This is a fresh
|
||||
* snapshot: callers may mutate an IR module between compilation passes. */
|
||||
/** The checked-dynamic undefined value (dynUndefinedExpr). */
|
||||
@@ -393,60 +339,366 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set<string>): boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
|
||||
* process signal/exit listeners and the piped-stdin surface. */
|
||||
const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.stdio",
|
||||
"process.onSignal",
|
||||
"process.offSignal",
|
||||
"process.onExit",
|
||||
"process.offExit",
|
||||
"stdin.onData",
|
||||
"stdin.onEnd",
|
||||
"stdin.onError",
|
||||
"stdin.nextChunk",
|
||||
// node:readline rides the stdin unit (scr_readline.c links beside
|
||||
// scr_events.c under the same gate).
|
||||
"rl.create",
|
||||
"rl.createSignal",
|
||||
"rl.question",
|
||||
"rl.questionAsync",
|
||||
"rl.questionAsyncSignal",
|
||||
"rl.questionDyn",
|
||||
"rl.questionAsyncDyn",
|
||||
"rl.questionAsyncSignalDyn",
|
||||
"rl.close",
|
||||
"rl.pause",
|
||||
"rl.resume",
|
||||
"rl.prompt",
|
||||
"rl.promptWith",
|
||||
"rl.setPrompt",
|
||||
"rl.setPromptDyn",
|
||||
"rl.getPrompt",
|
||||
"rl.write",
|
||||
"rl.writeKey",
|
||||
"rl.on",
|
||||
"rl.iterOpen",
|
||||
"rl.iterNext",
|
||||
"rl.iterReturn",
|
||||
"rl.checkOptions",
|
||||
"rl.checkCompleter",
|
||||
"rl.setOptions",
|
||||
"rl.setHistory",
|
||||
"rl.history",
|
||||
"rl.numProp",
|
||||
"rl.terminal",
|
||||
"rl.line",
|
||||
"rl.isTerminal",
|
||||
"rl.isPromises",
|
||||
"rl.cursor",
|
||||
"rl.seq",
|
||||
"rl.moves",
|
||||
"rl.cursorPos",
|
||||
"rl.keypressStdin",
|
||||
"rl.keypressStream",
|
||||
"stdin.onKeypress",
|
||||
"rl.uiOpen",
|
||||
"rl.uiOpenStream",
|
||||
"rl.uiCursorTo",
|
||||
"rl.uiMoveCursor",
|
||||
"rl.uiClearLine",
|
||||
"rl.uiClearScreenDown",
|
||||
"rl.uiRollback",
|
||||
"rl.uiCommit",
|
||||
]);
|
||||
|
||||
/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the
|
||||
* allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so
|
||||
* divergence-57 binaries keep their exact link line. */
|
||||
const HTTP2_LEGACY_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"http2.streamNoop",
|
||||
"http2.streamUndefCall",
|
||||
]);
|
||||
|
||||
/** Names that reach the extended process-stream surface (the
|
||||
* SCR_STDIO_EXTENDED events unit): stdin.setEncoding and 'resize'. */
|
||||
const STDIO_EXTENDED_NAMES: ReadonlySet<string> = new Set(["setEncoding", "resize"]);
|
||||
|
||||
function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures {
|
||||
const features: RuntimeFeatures = {
|
||||
regex: false,
|
||||
copying: false,
|
||||
legacyTextDecoder: false,
|
||||
fileHandle: false,
|
||||
fetch: false,
|
||||
// Subclasses of the runtime EventTarget/Event bases tear down and box
|
||||
// through the EventTarget unit even when no event.* libCall survives.
|
||||
eventTarget: (mod.classes ?? []).some(
|
||||
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
|
||||
),
|
||||
processEvents: false,
|
||||
warningReplace: false,
|
||||
stdioExtended: false,
|
||||
emitter:
|
||||
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
|
||||
stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
|
||||
zlib: false,
|
||||
dc: false,
|
||||
assert: false,
|
||||
dynInvoke: mod.workers === true,
|
||||
dynAsync:
|
||||
/** The libCall nodes the rules below test. */
|
||||
type LibCallNode = IrExpr & { kind: "libCall" };
|
||||
|
||||
/** What makes a program need one optional runtime unit. The feature is set
|
||||
* when any condition holds: `initial` from the module's declarations, a
|
||||
* libCall named in `fns` or starting with one of `prefixes`, a libCall
|
||||
* `libCall` accepts, another expression node `expr` accepts, or a type
|
||||
* slot of one of the `types` kinds. A libCall or type that needs several
|
||||
* units appears in each unit's rule. */
|
||||
interface RuntimeFeatureRule {
|
||||
initial?: (mod: IrModule) => boolean;
|
||||
fns?: readonly IrLibFn[];
|
||||
prefixes?: readonly string[];
|
||||
libCall?: (node: LibCallNode) => boolean;
|
||||
expr?: (node: IrExpr, tables: () => TypeTables) => boolean;
|
||||
types?: readonly IrType["kind"][];
|
||||
}
|
||||
|
||||
/** Gives every rule in the table one type. */
|
||||
const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r;
|
||||
|
||||
/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */
|
||||
const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."];
|
||||
|
||||
/** One rule per runtime feature, sorted by name. A new feature is an
|
||||
* entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan
|
||||
* and its early exit derive from this table. */
|
||||
const RUNTIME_FEATURE_RULES = {
|
||||
assert: rule({ prefixes: ["assert."] }),
|
||||
/** The program enters AsyncLocalStorage stores (store.run, enterWith,
|
||||
* a diagnostics_channel store binding) or reads the test runner's
|
||||
* context (getTestContext): its timer, immediate, tick and
|
||||
* queueMicrotask scheduling goes through the runtime's *_als entry
|
||||
* points, which carry the active stores into each callback. */
|
||||
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
|
||||
bigint: rule({ fns: ["process.hrtimeValue"], prefixes: ["bigint."], types: ["bigint"] }),
|
||||
childProcess: rule({
|
||||
fns: [...CHILD_PROCESS_LIB_FNS],
|
||||
prefixes: CHILD_PROCESS_LIB_PREFIXES,
|
||||
types: ["child", "childStream", "childWriter", "spawnRes"],
|
||||
}),
|
||||
/** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
|
||||
* process-warning unit, and unknown output encodings render through the
|
||||
* inspect unit. */
|
||||
cipher: rule({ fns: ["crypto.cipherNew"] }),
|
||||
copying: rule({
|
||||
expr: (node, _tables) =>
|
||||
(node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) ||
|
||||
(node.kind === "bytesIntrinsic" && COPYING_BYTES_METHODS.has(node.method)),
|
||||
}),
|
||||
dc: rule({ prefixes: ["dc."] }),
|
||||
/** dns.lookup lives in the dgram unit. */
|
||||
dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }),
|
||||
dynAsync: rule({
|
||||
initial: (mod) =>
|
||||
mod.workers === true ||
|
||||
mod.functions.some(
|
||||
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
|
||||
),
|
||||
dynTier: false,
|
||||
// The legacy url functions and crypto.createCipheriv emit their
|
||||
// deprecation warnings through the dyn-async unit.
|
||||
fns: [
|
||||
...DYN_ASYNC_LIB_FNS,
|
||||
...READABLE_DYN_ASYNC_LIB_FNS,
|
||||
...LEGACY_URL_LIB_FNS,
|
||||
"crypto.cipherNew",
|
||||
],
|
||||
prefixes: READABLE_DYN_ASYNC_PREFIXES,
|
||||
expr: (node, tables) =>
|
||||
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
|
||||
// Rejection reasons cross as dyn values (allSettled's records, any's
|
||||
// AggregateError list).
|
||||
(node.kind === "intrinsic" &&
|
||||
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
|
||||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
|
||||
}),
|
||||
dynInvoke: rule({
|
||||
initial: (mod) => mod.workers === true,
|
||||
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS],
|
||||
expr: (node, _tables) => node.kind === "dynInvoke",
|
||||
}),
|
||||
/** The program has dynamic-tier jsval operations (js.* builtins, the
|
||||
* jsval pass): links the scr_js_* units (two-tier design I3). */
|
||||
dynTier: rule({ prefixes: ["js."] }),
|
||||
/** The stream unit emits through the emitter unit, also when the
|
||||
* program constructs no stream (a pipeline of functions). */
|
||||
emitter: rule({
|
||||
initial: (mod) =>
|
||||
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
|
||||
fns: [...STREAM_LIB_FNS],
|
||||
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
|
||||
}),
|
||||
/** Subclasses of the runtime EventTarget/Event bases tear down and box
|
||||
* through the EventTarget unit even when no event.* libCall survives. */
|
||||
eventTarget: rule({
|
||||
initial: (mod) =>
|
||||
(mod.classes ?? []).some(
|
||||
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
|
||||
),
|
||||
fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS],
|
||||
prefixes: ["event."],
|
||||
}),
|
||||
fetch: rule({ prefixes: ["fetch."] }),
|
||||
fileHandle: rule({ types: ["fileHandle"] }),
|
||||
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
|
||||
http: rule({
|
||||
prefixes: ["http.", "tls.", "https.", "http2."],
|
||||
types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"],
|
||||
}),
|
||||
/** The real h2 surface (scr_http2.c); the legacy http2.* libCalls of the
|
||||
* allowHTTP1 slice are served by scr_http.c and scr_tls.c. */
|
||||
http2: rule({
|
||||
libCall: (node) => node.fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(node.fn),
|
||||
types: ["http2Session", "http2Stream"],
|
||||
}),
|
||||
/** scr_test.c renders thrown strings with the inspect unit's string
|
||||
* formatting; scr_util_callable.c wraps the inspect unit; readline's
|
||||
* terminal unit prints completion errors through it when there is a
|
||||
* completer. */
|
||||
inspect: rule({
|
||||
fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS],
|
||||
prefixes: ["insp.", "console.", "test."],
|
||||
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
|
||||
}),
|
||||
legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }),
|
||||
/** The program calls mock.timers members: its timer calls go through
|
||||
* scr_test.c's mock-aware entry points. */
|
||||
mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }),
|
||||
net: rule({
|
||||
prefixes: NET_PREFIXES,
|
||||
types: [
|
||||
"netServer",
|
||||
"netSocket",
|
||||
"http2Session",
|
||||
"http2Stream",
|
||||
"httpReq",
|
||||
"httpRes",
|
||||
"httpClientReq",
|
||||
"secureCtx",
|
||||
],
|
||||
}),
|
||||
nodeTest: rule({ prefixes: ["test."], types: ["testCtx"] }),
|
||||
/** scr_util_callable.c reads options through the parseArgs unit's
|
||||
* descriptors. */
|
||||
parseArgs: rule({ fns: [...PARSE_ARGS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS] }),
|
||||
processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }),
|
||||
/** 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. */
|
||||
regex: rule({
|
||||
fns: [
|
||||
...REGEX_LIB_FNS,
|
||||
...LEGACY_URL_LIB_FNS,
|
||||
...UTIL_CALLABLE_LIB_FNS,
|
||||
"rl.terminal",
|
||||
"str.localeCase",
|
||||
],
|
||||
expr: (node, _tables) =>
|
||||
node.kind === "regexLit" ||
|
||||
node.kind === "regexIntrinsic" ||
|
||||
(node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)),
|
||||
types: ["regex"],
|
||||
}),
|
||||
searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }),
|
||||
/** 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
|
||||
* by name (dynamic member calls and event-name strings). */
|
||||
stdioExtended: rule({
|
||||
libCall: (node) =>
|
||||
node.fn === "stdin.onData" &&
|
||||
node.args[0]?.type.kind === "func" &&
|
||||
node.args[0].type.params[0]?.kind === "string",
|
||||
expr: (node, _tables) =>
|
||||
(node.kind === "dynInvoke" && STDIO_EXTENDED_NAMES.has(node.method)) ||
|
||||
(node.kind === "strLit" && STDIO_EXTENDED_NAMES.has(node.value)),
|
||||
}),
|
||||
stream: rule({
|
||||
initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
|
||||
fns: [...STREAM_LIB_FNS],
|
||||
prefixes: STREAM_LIB_PREFIXES,
|
||||
}),
|
||||
/** A Dirent row converted to dyn carries Node's Symbol(type) key
|
||||
* (scr_dyn_dirent_finish, defined in scr_symbol.c). */
|
||||
symbol: rule({
|
||||
initial: (mod) => (mod.records ?? []).some(isDirentShape),
|
||||
fns: [...SYMBOL_LIB_FNS],
|
||||
prefixes: ["sym.", "disposable."],
|
||||
types: ["symbol"],
|
||||
}),
|
||||
tls: rule({ prefixes: ["tls.", "https.", "http2."], types: ["secureCtx"] }),
|
||||
tlsCa: rule({ prefixes: ["tlsca."] }),
|
||||
/** process.emitWarning is assigned (process.setEmitWarning). */
|
||||
warningReplace: rule({ fns: ["process.setEmitWarning"] }),
|
||||
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
|
||||
* scr_webcrypto.c. */
|
||||
webcrypto: rule({ prefixes: ["webcrypto."] }),
|
||||
zlib: rule({ prefixes: ["zlib."] }),
|
||||
};
|
||||
|
||||
type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES;
|
||||
|
||||
export type RuntimeFeatures = Record<RuntimeFeatureName, boolean>;
|
||||
|
||||
const RUNTIME_FEATURE_NAMES: RuntimeFeatureName[] = [];
|
||||
for (const name in RUNTIME_FEATURE_RULES) RUNTIME_FEATURE_NAMES.push(name as RuntimeFeatureName);
|
||||
|
||||
/** Every feature off, in the table's order. TypeScript rejects this
|
||||
* literal when it misses a feature or names one without a rule. */
|
||||
function noRuntimeFeatures(): RuntimeFeatures {
|
||||
return {
|
||||
assert: false,
|
||||
asyncContext: false,
|
||||
inspect: false,
|
||||
childProcess: false,
|
||||
net: false,
|
||||
// A Dirent row converted to dyn carries Node's Symbol(type) key
|
||||
// (scr_dyn_dirent_finish, defined in scr_symbol.c).
|
||||
symbol: (mod.records ?? []).some(isDirentShape),
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
parseArgs: false,
|
||||
fsWatch: false,
|
||||
nodeTest: false,
|
||||
mockTimers: false,
|
||||
childProcess: false,
|
||||
cipher: false,
|
||||
copying: false,
|
||||
dc: false,
|
||||
dgram: false,
|
||||
dynAsync: false,
|
||||
dynInvoke: false,
|
||||
dynTier: false,
|
||||
emitter: false,
|
||||
eventTarget: false,
|
||||
fetch: false,
|
||||
fileHandle: false,
|
||||
fsWatch: false,
|
||||
http: false,
|
||||
http2: false,
|
||||
inspect: false,
|
||||
legacyTextDecoder: false,
|
||||
mockTimers: false,
|
||||
net: false,
|
||||
nodeTest: false,
|
||||
parseArgs: false,
|
||||
processEvents: false,
|
||||
qs: false,
|
||||
regex: false,
|
||||
searchParams: false,
|
||||
stdioExtended: false,
|
||||
stream: false,
|
||||
symbol: false,
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
cipher: false,
|
||||
warningReplace: false,
|
||||
webcrypto: false,
|
||||
zlib: false,
|
||||
};
|
||||
}
|
||||
|
||||
const ruleOf = (name: RuntimeFeatureName): RuntimeFeatureRule => RUNTIME_FEATURE_RULES[name];
|
||||
|
||||
/** The rules indexed for the scan: features by libCall name and by type
|
||||
* kind, and the prefix and predicate conditions in rule order. */
|
||||
const FEATURES_BY_FN = new Map<string, RuntimeFeatureName[]>();
|
||||
const FEATURES_BY_TYPE = new Map<string, RuntimeFeatureName[]>();
|
||||
const FEATURE_PREFIXES: { prefix: string; name: RuntimeFeatureName }[] = [];
|
||||
const FEATURE_LIBCALL_TESTS: RuntimeFeatureName[] = [];
|
||||
const FEATURE_EXPR_TESTS: RuntimeFeatureName[] = [];
|
||||
for (const name of RUNTIME_FEATURE_NAMES) {
|
||||
const rule = ruleOf(name);
|
||||
for (const fn of rule.fns ?? []) {
|
||||
const names = FEATURES_BY_FN.get(fn);
|
||||
if (names) names.push(name);
|
||||
else FEATURES_BY_FN.set(fn, [name]);
|
||||
}
|
||||
for (const kind of rule.types ?? []) {
|
||||
const names = FEATURES_BY_TYPE.get(kind);
|
||||
if (names) names.push(name);
|
||||
else FEATURES_BY_TYPE.set(kind, [name]);
|
||||
}
|
||||
for (const prefix of rule.prefixes ?? []) FEATURE_PREFIXES.push({ prefix, name });
|
||||
if (rule.libCall) FEATURE_LIBCALL_TESTS.push(name);
|
||||
if (rule.expr) FEATURE_EXPR_TESTS.push(name);
|
||||
}
|
||||
|
||||
function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures {
|
||||
const features = noRuntimeFeatures();
|
||||
for (const name of RUNTIME_FEATURE_NAMES) {
|
||||
const initial = ruleOf(name).initial;
|
||||
if (initial && initial(mod)) features[name] = true;
|
||||
}
|
||||
const keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
|
||||
let tables: TypeTables | undefined;
|
||||
const typeTables = (): TypeTables =>
|
||||
@@ -458,207 +710,19 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
if (node.kind === "libCall") {
|
||||
const fn = node.fn;
|
||||
if (fn.startsWith("js.")) features.dynTier = true;
|
||||
if (REGEX_LIB_FNS.has(fn)) features.regex = true;
|
||||
if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true;
|
||||
if (fn.startsWith("fetch.")) features.fetch = true;
|
||||
if (fn === "process.setEmitWarning") features.warningReplace = true;
|
||||
// A typed string 'data' listener takes setEncoding-decoded chunks.
|
||||
if (
|
||||
fn === "stdin.onData" &&
|
||||
node.args[0]?.type.kind === "func" &&
|
||||
node.args[0].type.params[0]?.kind === "string"
|
||||
)
|
||||
features.stdioExtended = true;
|
||||
if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true;
|
||||
if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true;
|
||||
if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true;
|
||||
// With a completer, readline's terminal unit prints completion errors
|
||||
// through the inspect unit.
|
||||
if (fn === "rl.terminal") {
|
||||
features.regex = true;
|
||||
if (!isDynUndefined(node.args[3]!)) features.inspect = true;
|
||||
}
|
||||
if (fn.startsWith("emitter.")) features.emitter = true;
|
||||
if (STREAM_LIB_FNS.has(fn) || startsWithAny(fn, STREAM_LIB_PREFIXES)) {
|
||||
// the stream unit emits through the emitter unit, also when the
|
||||
// program constructs no stream (a pipeline of functions)
|
||||
features.stream = true;
|
||||
features.emitter = true;
|
||||
}
|
||||
if (fn.startsWith("zlib.")) features.zlib = true;
|
||||
if (READABLE_DYN_ASYNC_LIB_FNS.has(fn) || startsWithAny(fn, READABLE_DYN_ASYNC_PREFIXES))
|
||||
features.dynAsync = true;
|
||||
if (fn.startsWith("dc.")) features.dc = true;
|
||||
if (fn.startsWith("assert.")) features.assert = true;
|
||||
if (DYN_INVOKE_LIB_FNS.has(fn)) features.dynInvoke = true;
|
||||
if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true;
|
||||
if (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true;
|
||||
if (
|
||||
fn.startsWith("insp.") ||
|
||||
fn.startsWith("console.") ||
|
||||
fn === "global.native" ||
|
||||
fn === "util.styleText"
|
||||
)
|
||||
features.inspect = true;
|
||||
if (CHILD_PROCESS_LIB_FNS.has(fn) || startsWithAny(fn, CHILD_PROCESS_LIB_PREFIXES))
|
||||
features.childProcess = true;
|
||||
if (fn.startsWith("net.")) features.net = true;
|
||||
if (fn.startsWith("http.")) {
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
}
|
||||
if (fn.startsWith("tls.") || fn.startsWith("https.") || fn.startsWith("http2.")) {
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
features.tls = true;
|
||||
}
|
||||
if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true;
|
||||
if (fn.startsWith("sym.") || fn.startsWith("disposable.") || SYMBOL_LIB_FNS.has(fn))
|
||||
features.symbol = true;
|
||||
if (fn === "process.hrtimeValue") features.bigint = true;
|
||||
if (fn.startsWith("bigint.")) features.bigint = true;
|
||||
if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true;
|
||||
if (QS_LIB_FNS.has(fn)) features.qs = true;
|
||||
// scr_url_legacy.c lowercases and normalizes hostnames (the regex
|
||||
// unit's libunicode), parses and stringifies queries, and emits its
|
||||
// deprecation warnings through the dyn-async unit.
|
||||
if (LEGACY_URL_LIB_FNS.has(fn)) {
|
||||
features.regex = true;
|
||||
features.qs = true;
|
||||
features.dynAsync = true;
|
||||
}
|
||||
if (PARSE_ARGS_LIB_FNS.has(fn)) features.parseArgs = true;
|
||||
if (fn === "util.styleText") features.processEvents = true;
|
||||
// scr_util_callable.c: dyn wrappers over the descriptor, inspect,
|
||||
// regex (NODE_DEBUG matching) and terminal-color units.
|
||||
if (UTIL_CALLABLE_LIB_FNS.has(fn)) {
|
||||
features.parseArgs = true;
|
||||
features.inspect = true;
|
||||
features.dynInvoke = true;
|
||||
features.regex = true;
|
||||
}
|
||||
if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true;
|
||||
// scr_test.c renders thrown strings in the failing-tests section with
|
||||
// the inspect unit's string formatting.
|
||||
if (fn.startsWith("test.")) {
|
||||
features.nodeTest = true;
|
||||
features.inspect = true;
|
||||
if (fn.startsWith("test.mockTimers") || fn.startsWith("test.ctxMockTimers")) {
|
||||
features.mockTimers = true;
|
||||
}
|
||||
}
|
||||
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
|
||||
if (fn.startsWith("tlsca.")) features.tlsCa = true;
|
||||
// Locale case mapping runs the default conversion of the regex unit.
|
||||
if (fn === "str.localeCase") features.regex = true;
|
||||
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
|
||||
// process-warning unit, and unknown output encodings render through
|
||||
// the inspect unit.
|
||||
if (fn === "crypto.cipherNew") {
|
||||
features.cipher = true;
|
||||
features.dynAsync = true;
|
||||
features.inspect = true;
|
||||
}
|
||||
// crypto.subtle and CryptoKey: scr_webcrypto.c.
|
||||
if (fn.startsWith("webcrypto.")) features.webcrypto = true;
|
||||
for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true;
|
||||
for (const { prefix, name } of FEATURE_PREFIXES)
|
||||
if (!features[name] && fn.startsWith(prefix)) features[name] = true;
|
||||
for (const name of FEATURE_LIBCALL_TESTS)
|
||||
if (!features[name] && ruleOf(name).libCall!(node)) features[name] = true;
|
||||
} else {
|
||||
switch (node.kind) {
|
||||
case "regexLit":
|
||||
case "regexIntrinsic":
|
||||
features.regex = true;
|
||||
break;
|
||||
case "strIntrinsic":
|
||||
if (CASE_MAPPING_STR_METHODS.has(node.method)) features.regex = true;
|
||||
break;
|
||||
case "arrIntrinsic":
|
||||
if (COPYING_ARR_METHODS.has(node.method)) features.copying = true;
|
||||
break;
|
||||
case "bytesIntrinsic":
|
||||
if (COPYING_BYTES_METHODS.has(node.method)) features.copying = true;
|
||||
break;
|
||||
case "dynInvoke":
|
||||
features.dynInvoke = true;
|
||||
if (STDIO_EXTENDED_NAMES.has(node.method)) features.stdioExtended = true;
|
||||
break;
|
||||
// The process stream object's setEncoding and 'resize' members are
|
||||
// reached by name (dynamic member calls and event-name strings).
|
||||
case "strLit":
|
||||
if (STDIO_EXTENDED_NAMES.has(node.value)) features.stdioExtended = true;
|
||||
break;
|
||||
case "awaitExpr":
|
||||
if (node.type.kind === "dyn") features.dynAsync = true;
|
||||
break;
|
||||
case "intrinsic":
|
||||
// Rejection reasons cross as dyn values (allSettled's records,
|
||||
// any's AggregateError list).
|
||||
if (node.name === "promise.allSettled" || node.name === "promise.any")
|
||||
features.dynAsync = true;
|
||||
break;
|
||||
case "dynFrom":
|
||||
if (!features.dynAsync && boxesPromise(node.value.type, typeTables(), new Set()))
|
||||
features.dynAsync = true;
|
||||
break;
|
||||
}
|
||||
for (const name of FEATURE_EXPR_TESTS)
|
||||
if (!features[name] && ruleOf(name).expr!(node, typeTables)) features[name] = true;
|
||||
}
|
||||
return keepGoing();
|
||||
};
|
||||
const type = (node: IrType): boolean => {
|
||||
switch (node.kind) {
|
||||
case "regex":
|
||||
features.regex = true;
|
||||
break;
|
||||
case "fileHandle":
|
||||
features.fileHandle = true;
|
||||
break;
|
||||
case "procStream":
|
||||
features.processEvents = true;
|
||||
break;
|
||||
case "child":
|
||||
case "childStream":
|
||||
case "childWriter":
|
||||
case "spawnRes":
|
||||
features.childProcess = true;
|
||||
break;
|
||||
case "netServer":
|
||||
case "netSocket":
|
||||
features.net = true;
|
||||
break;
|
||||
case "http2Session":
|
||||
case "http2Stream":
|
||||
features.net = true;
|
||||
features.http2 = true;
|
||||
break;
|
||||
case "httpReq":
|
||||
case "httpRes":
|
||||
case "httpClientReq":
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
break;
|
||||
case "secureCtx":
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
features.tls = true;
|
||||
break;
|
||||
case "symbol":
|
||||
features.symbol = true;
|
||||
break;
|
||||
case "bigint":
|
||||
features.bigint = true;
|
||||
break;
|
||||
case "searchParams":
|
||||
features.searchParams = true;
|
||||
break;
|
||||
case "fsWatcher":
|
||||
features.fsWatch = true;
|
||||
break;
|
||||
case "testCtx":
|
||||
features.nodeTest = true;
|
||||
break;
|
||||
case "dgramSocket":
|
||||
features.dgram = true;
|
||||
break;
|
||||
}
|
||||
for (const name of FEATURES_BY_TYPE.get(node.kind) ?? []) features[name] = true;
|
||||
return keepGoing();
|
||||
};
|
||||
if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
|
||||
@@ -702,64 +766,6 @@ export function moduleUsesFetch(mod: IrModule): boolean {
|
||||
return scanRuntimeFeatures(mod, "fetch").fetch;
|
||||
}
|
||||
|
||||
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
|
||||
* process signal/exit listeners and the piped-stdin surface. */
|
||||
const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.stdio",
|
||||
"process.onSignal",
|
||||
"process.offSignal",
|
||||
"process.onExit",
|
||||
"process.offExit",
|
||||
"stdin.onData",
|
||||
"stdin.onEnd",
|
||||
"stdin.onError",
|
||||
"stdin.nextChunk",
|
||||
// node:readline rides the stdin unit (scr_readline.c links beside
|
||||
// scr_events.c under the same gate).
|
||||
"rl.create",
|
||||
"rl.createSignal",
|
||||
"rl.question",
|
||||
"rl.questionAsync",
|
||||
"rl.questionAsyncSignal",
|
||||
"rl.close",
|
||||
"rl.pause",
|
||||
"rl.resume",
|
||||
"rl.prompt",
|
||||
"rl.promptWith",
|
||||
"rl.setPrompt",
|
||||
"rl.getPrompt",
|
||||
"rl.write",
|
||||
"rl.writeKey",
|
||||
"rl.on",
|
||||
"rl.iterOpen",
|
||||
"rl.iterNext",
|
||||
"rl.iterReturn",
|
||||
"rl.checkOptions",
|
||||
"rl.checkCompleter",
|
||||
"rl.setOptions",
|
||||
"rl.setHistory",
|
||||
"rl.history",
|
||||
"rl.numProp",
|
||||
"rl.terminal",
|
||||
"rl.line",
|
||||
"rl.isTerminal",
|
||||
"rl.cursor",
|
||||
"rl.seq",
|
||||
"rl.moves",
|
||||
"rl.cursorPos",
|
||||
"rl.keypressStdin",
|
||||
"rl.keypressStream",
|
||||
"stdin.onKeypress",
|
||||
"rl.uiOpen",
|
||||
"rl.uiOpenStream",
|
||||
"rl.uiCursorTo",
|
||||
"rl.uiMoveCursor",
|
||||
"rl.uiClearLine",
|
||||
"rl.uiClearScreenDown",
|
||||
"rl.uiRollback",
|
||||
"rl.uiCommit",
|
||||
]);
|
||||
|
||||
/** True when the module uses the process-events surface — the link switch
|
||||
* that pulls scr_events.c into the binary and has the emitted main call
|
||||
* scr_events_install (native-toolchain.ts + emitter; the scr_regex/scr_fetch/scr_zlib
|
||||
@@ -949,14 +955,6 @@ export function moduleUsesHttpServer(mod: IrModule): boolean {
|
||||
return scanRuntimeFeatures(mod, "http").http;
|
||||
}
|
||||
|
||||
/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the
|
||||
* allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so
|
||||
* divergence-57 binaries keep their exact link line. */
|
||||
const HTTP2_LEGACY_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"http2.streamNoop",
|
||||
"http2.streamUndefCall",
|
||||
]);
|
||||
|
||||
/** True when the module uses the REAL h2 surface (scr_http2.c): any core
|
||||
* http2.* libCall, or an h2 handle type left behind by a fenced statement
|
||||
* (its emitted release call needs the unit — the moduleUsesNet story). */
|
||||
|
||||
@@ -42,14 +42,15 @@ export function unshareNodes(mod: IrModule): IrModule {
|
||||
}
|
||||
if (affected.size === 0) return mod;
|
||||
// Rebuilding a function copies every node that has children; a repeated
|
||||
// childless node reads back as a fresh object at each of its sites there.
|
||||
// childless node gets its own shallow copy at each of its sites there.
|
||||
// Both kinds of copy keep sharing the type and location objects.
|
||||
const expr = (node: IrExpr): IrExpr => {
|
||||
const copy = mapExprChildren(node, expr, stmt);
|
||||
return copy !== node || !repeated.has(node) ? copy : reparseExpr(node);
|
||||
return copy !== node || !repeated.has(node) ? copy : copyChildlessExpr(node);
|
||||
};
|
||||
const stmt = (node: IrStmt): IrStmt => {
|
||||
const copy = mapStmtChildren(node, expr, stmt);
|
||||
return copy !== node || !repeated.has(node) ? copy : reparseStmt(node);
|
||||
return copy !== node || !repeated.has(node) ? copy : copyChildlessStmt(node);
|
||||
};
|
||||
return {
|
||||
...mod,
|
||||
@@ -58,3 +59,66 @@ export function unshareNodes(mod: IrModule): IrModule {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** A shallow copy of a node that mapExprChildren returns unchanged. Each
|
||||
* case spreads one node kind, so the copy is a plain record copy of that
|
||||
* kind. Any other kind is copied through the encoding. */
|
||||
function copyChildlessExpr(node: IrExpr): IrExpr {
|
||||
switch (node.kind) {
|
||||
case "varRef":
|
||||
return { ...node };
|
||||
case "strLit":
|
||||
return { ...node };
|
||||
case "numLit":
|
||||
return { ...node };
|
||||
case "boolLit":
|
||||
return { ...node };
|
||||
case "unitLit":
|
||||
return { ...node };
|
||||
case "closure":
|
||||
return { ...node };
|
||||
case "moduleNsRef":
|
||||
return { ...node };
|
||||
case "incDec":
|
||||
return { ...node };
|
||||
case "chainRecv":
|
||||
return { ...node };
|
||||
case "regexLit":
|
||||
return { ...node };
|
||||
case "templateStrings":
|
||||
return { ...node };
|
||||
case "setNew":
|
||||
return { ...node };
|
||||
case "mapNew":
|
||||
return { ...node };
|
||||
case "dynObjLit":
|
||||
return { ...node };
|
||||
case "selfRef":
|
||||
return { ...node };
|
||||
case "promiseWithResolvers":
|
||||
return { ...node };
|
||||
case "classRef":
|
||||
return { ...node };
|
||||
case "fieldAbsent":
|
||||
return { ...node };
|
||||
default:
|
||||
return reparseExpr(node);
|
||||
}
|
||||
}
|
||||
|
||||
/** A shallow copy of a statement that mapStmtChildren returns unchanged,
|
||||
* one case per kind as in copyChildlessExpr. */
|
||||
function copyChildlessStmt(node: IrStmt): IrStmt {
|
||||
switch (node.kind) {
|
||||
case "break":
|
||||
return { ...node };
|
||||
case "continue":
|
||||
return { ...node };
|
||||
case "runtimeFence":
|
||||
return { ...node };
|
||||
case "rethrow":
|
||||
return { ...node };
|
||||
default:
|
||||
return reparseStmt(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
import {
|
||||
F64,
|
||||
VOID,
|
||||
bytesOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "../src/ir/ir.js";
|
||||
import { validateModule } from "../src/ir/validate.js";
|
||||
|
||||
const loc = { file: "bytes-field-borrow.ts", start: 0, end: 0 };
|
||||
const units = bytesOf("u16");
|
||||
const scanner = { kind: "object" as const, className: "Scanner" };
|
||||
const self = (): IrExpr => ({ kind: "varRef", localId: "this", type: scanner, loc });
|
||||
const chars = (): IrExpr => ({
|
||||
kind: "fieldGet",
|
||||
obj: self(),
|
||||
className: "Scanner",
|
||||
field: "chars",
|
||||
type: units,
|
||||
loc,
|
||||
});
|
||||
const local = (): IrExpr => ({ kind: "varRef", localId: "c", type: units, loc });
|
||||
const read = (index: number): IrExpr => ({
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: local(),
|
||||
args: [{ kind: "numLit", value: index, type: F64, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
|
||||
/** `const c = this.chars; ...between; return c[0] + c[1]`. */
|
||||
function method(name: string, between: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "this", name: "this", type: scanner }],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
{ id: "this", name: "this", type: scanner, mutable: false },
|
||||
{ id: "c", name: "c", type: units, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{ kind: "varDecl", localId: "c", init: chars(), loc },
|
||||
...between,
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "bin", op: "+", left: read(0), right: read(1), type: F64 },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
} as IrFunction;
|
||||
}
|
||||
|
||||
test("a typed-array local read from a field the rest of its block never writes borrows it", () => {
|
||||
const replace: IrStmt = {
|
||||
kind: "fieldSet",
|
||||
obj: self(),
|
||||
className: "Scanner",
|
||||
field: "chars",
|
||||
value: {
|
||||
kind: "bytesNew",
|
||||
source: { kind: "numLit", value: 4, type: F64, loc },
|
||||
type: units,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
const mod: IrModule = {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [
|
||||
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
method("%Scanner.peek", []),
|
||||
method("%Scanner.swap", [replace]),
|
||||
],
|
||||
classes: [{ name: "Scanner", fields: [{ name: "chars", type: units }], methods: [], loc }],
|
||||
};
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const body = (name: string): string =>
|
||||
new RegExp(`^define [^\\n]*@sc_bf__x25_Scanner_${name}\\b[^]*?^}`, "m").exec(llvm)?.[0] ?? "";
|
||||
expect(body("peek")).not.toBe("");
|
||||
// The field keeps the array alive while the borrowed local reads it.
|
||||
expect(body("peek")).not.toContain("bytes_retain");
|
||||
expect(body("peek")).not.toContain("bytes_release");
|
||||
// Replacing the field could free the array the local holds: it keeps a reference.
|
||||
expect(body("swap")).toContain("call ptr @scr_bytes_retain_v(");
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze } from "../src/index.js";
|
||||
|
||||
function refusals(file: string, source: string): string[] {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-local-class-private-"));
|
||||
try {
|
||||
const entry = join(dir, file);
|
||||
writeFileSync(entry, source);
|
||||
const { coverage } = analyze(entry);
|
||||
return [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])].map((d) => d.message);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Function-local JavaScript classes compile private fields used only on
|
||||
// `this` when the class is a named declaration: each evaluation's prototype
|
||||
// methods check the receiver's evaluation against that binding. Private
|
||||
// methods and accessors, class expressions, private names on other objects,
|
||||
// and TypeScript classes keep the refusal.
|
||||
test.each([
|
||||
[
|
||||
"a private method",
|
||||
"main.js",
|
||||
"function make() { class Box { #read() { return 1; } read() { return this.#read(); } } return new Box(); } console.log(make().read());",
|
||||
],
|
||||
[
|
||||
"a private accessor",
|
||||
"main.js",
|
||||
"function make() { class Box { get #value() { return 1; } read() { return this.#value; } } return new Box(); } console.log(make().read());",
|
||||
],
|
||||
[
|
||||
"a class expression",
|
||||
"main.js",
|
||||
"function make() { const Box = class { #value = 1; read() { return this.#value; } }; return new Box(); } console.log(make().read());",
|
||||
],
|
||||
[
|
||||
"a private name on another object",
|
||||
"main.js",
|
||||
"function make() { class Box { #value = 1; has(other) { return #value in other; } } return new Box(); } console.log(make().has({}));",
|
||||
],
|
||||
[
|
||||
"a TypeScript class",
|
||||
"main.ts",
|
||||
"function make() { class Box { #value = 1; read(): number { return this.#value; } } return new Box(); } console.log(make().read());",
|
||||
],
|
||||
] as const)("function-local private names keep the refusal with %s", (_name, file, source) => {
|
||||
expect(refusals(file, source)).toContainEqual(
|
||||
expect.stringContaining("function-local classes with private brands"),
|
||||
);
|
||||
});
|
||||
|
||||
test("function-local private fields used on this compile", () => {
|
||||
expect(
|
||||
refusals(
|
||||
"main.js",
|
||||
"function make() { class Box { #value = 1; read() { return this.#value; } has() { return #value in this; } } return new Box(); } const box = make(); console.log(box.read(), box.has());",
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5467-typed-array-loop-versions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5468-typed-array-loop-aliasing.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5469-typed-array-loop-initializer-rebinding.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5607-string-char-compare.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5608-string-char-switch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5609-string-switch-optional.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5730-thread-slots-widened-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5750-string-from-char-codes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5751-string-length-cache.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5775-md5-lengths.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5876-readline-dispose-instanceof.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5877-readline-question-query.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5891-typed-array-field-borrows.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5983-runtime-constructor.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6058-class-view-asserted-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6061-type-only-class-fields/holder.ts",
|
||||
"<repo>/tests/corpus/6061-type-only-class-fields/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6063-union-class-field-checks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6342-throw-builtin-identity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6345-inspect-dyn-holes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6346-inspect-dyn-dataview.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6347-dataview-methods-js.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6348-errno-code-own-field.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6356-format-integer-dyn.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6407-dyn-bytes-in.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6408-dyn-bytes-in-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6416-tls-session-reused.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6417-object-assign-class-target.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6418-nullish-assign-index-record.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6430-npm-constructor-function-class/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6431-cjs-exports-rebinding/lib.cjs",
|
||||
"<repo>/tests/corpus/6431-cjs-exports-rebinding/extended.cjs",
|
||||
"<repo>/tests/corpus/6431-cjs-exports-rebinding/main.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6460-in-union-optional-slots.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6461-predicate-intersection-narrowing.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6462-assertion-attached-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/cjs-class-hierarchy-npm/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user