mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -10,9 +10,11 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `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 |
|
||||
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `date-math` | cpu | schedules built with Date setters (local and UTC), weekday and month-end rules through the getters, Dates sorted by time value |
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
@@ -29,6 +31,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
|
||||
| `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 |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/date-math.ts",
|
||||
"args": [
|
||||
"40"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Calendar arithmetic on Dates: schedules built by adjusting Dates with the
|
||||
// UTC and local setters, weekday and month-end rules read back through the
|
||||
// getters, Dates compared and sorted by time value, and ISO text for a
|
||||
// sample. Argument: the number of rounds.
|
||||
process.env.TZ = "America/New_York";
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "40");
|
||||
let seed = 2026;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
/** The next business day at 09:00 local time on or after `from`. */
|
||||
function nextBusinessDay(from: Date): Date {
|
||||
const day = new Date(from.getTime());
|
||||
day.setHours(9, 0, 0, 0);
|
||||
if (day < from) day.setDate(day.getDate() + 1);
|
||||
while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1);
|
||||
return day;
|
||||
}
|
||||
|
||||
/** The last day of `date`'s month, in UTC. */
|
||||
function monthEnd(date: Date): number {
|
||||
const end = new Date(date.getTime());
|
||||
end.setUTCDate(1);
|
||||
end.setUTCMonth(end.getUTCMonth() + 1);
|
||||
end.setUTCDate(0);
|
||||
return end.getUTCDate();
|
||||
}
|
||||
|
||||
let checksum = 0;
|
||||
let sample = "";
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const due: Date[] = [];
|
||||
const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12));
|
||||
for (let i = 0; i < 4000; i++) {
|
||||
cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000));
|
||||
const next = nextBusinessDay(cursor);
|
||||
checksum += next.getDate() + monthEnd(next) + next.getHours();
|
||||
if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15);
|
||||
due.push(next);
|
||||
}
|
||||
due.sort((a, b) => +a - +b);
|
||||
let previous = due[0]!;
|
||||
for (const date of due) {
|
||||
checksum += (+date - +previous) / 3600000;
|
||||
previous = date;
|
||||
}
|
||||
sample = due[due.length >> 1]!.toISOString();
|
||||
}
|
||||
console.log(Math.round(checksum), sample);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-properties.mjs",
|
||||
"args": [
|
||||
"1000000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Untyped JavaScript reading and writing properties of values it never
|
||||
// declares types for: records built by object literals in two key orders,
|
||||
// nested records, property updates, and instances of a class and its
|
||||
// subclass read and written the same way. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "1000000");
|
||||
class Cursor {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
step(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class Marker extends Cursor {
|
||||
constructor(x, y, label) {
|
||||
super(x, y);
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
function makeItem(i) {
|
||||
if (i % 3 === 0)
|
||||
return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } };
|
||||
return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } };
|
||||
}
|
||||
function cost(item) {
|
||||
return item.price * item.qty + item.meta.weight;
|
||||
}
|
||||
function restock(item, round) {
|
||||
item.qty = (item.qty + round) % 7;
|
||||
item.meta.tag = (item.meta.tag + 1) % 7;
|
||||
}
|
||||
function distance(a, b) {
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
}
|
||||
const items = [];
|
||||
for (let i = 0; i < 1024; i++) items.push(makeItem(i));
|
||||
const cursors = [];
|
||||
for (let i = 0; i < 64; i++)
|
||||
cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i));
|
||||
const origin = { x: 0, y: 0 };
|
||||
let checksum = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
const item = items[r & 1023];
|
||||
checksum += cost(item);
|
||||
restock(item, r & 3);
|
||||
checksum += item.meta.tag;
|
||||
const cursor = cursors[r & 63];
|
||||
checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin);
|
||||
if (cursor.x > 100 || cursor.x < -100) cursor.x = 0;
|
||||
if (cursor.y > 100 || cursor.y < -100) cursor.y = 0;
|
||||
}
|
||||
console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/throw-chains.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Exceptions that cross many frames: throws from helpers below a recursion,
|
||||
// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a
|
||||
// callback), with each frame holding an array across its call, and a
|
||||
// backtracking parser that uses exceptions for control flow.
|
||||
class ParseError extends Error {
|
||||
readonly at: number;
|
||||
constructor(at: number, message: string) {
|
||||
super(message);
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
/** A thrown value that is not an Error (no stack to capture). */
|
||||
class Stop {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const held: number[][] = [];
|
||||
function fail(n: number, plain: boolean): number {
|
||||
if (plain) throw new Stop(n);
|
||||
throw new RangeError("chain " + n);
|
||||
}
|
||||
function check(n: number, plain: boolean): number {
|
||||
return (n & 3) === 0 ? fail(n, plain) : n;
|
||||
}
|
||||
function validate(n: number, plain: boolean): number {
|
||||
return check(n, plain) + (n & 1);
|
||||
}
|
||||
function bottom(n: number, plain: boolean): number {
|
||||
const box = [n, -1];
|
||||
const r = validate(n, plain);
|
||||
held[0] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function walk(n: number, depth: number, plain: boolean): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
|
||||
function chains(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
for (const depth of [2, 5, 20])
|
||||
for (const plain of [false, true]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += walk(i, depth - 2, plain);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof Stop ? error.code & 7 : 1;
|
||||
}
|
||||
}
|
||||
out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// The same chains through a callback: each level visits the next with a
|
||||
// closure, as tree visitors do.
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, false) : each(n, depth - 1);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function visits(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (const depth of [2, 5, 20]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += visit(i, depth - 2, each);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof RangeError ? 1 : 0;
|
||||
}
|
||||
}
|
||||
out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// expr := term (("+" | "-") term)*; term := atom ("*" atom)*;
|
||||
// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name.
|
||||
// A call and a name start alike: the parser tries the call first and
|
||||
// backtracks when it fails.
|
||||
let text = "";
|
||||
let pos = 0;
|
||||
function peek(): string {
|
||||
return pos < text.length ? text[pos]! : "";
|
||||
}
|
||||
function expect(c: string): void {
|
||||
if (peek() !== c) throw new ParseError(pos, `expected ${c}`);
|
||||
pos++;
|
||||
}
|
||||
function name(): string {
|
||||
const start = pos;
|
||||
while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++;
|
||||
if (pos === start) throw new ParseError(pos, "expected a name");
|
||||
return text.slice(start, pos);
|
||||
}
|
||||
function call(): number {
|
||||
const callee = name();
|
||||
expect("(");
|
||||
let value = expr() + callee.length;
|
||||
while (peek() === ",") {
|
||||
pos++;
|
||||
value += expr();
|
||||
}
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
function atom(): number {
|
||||
const c = peek();
|
||||
if (c >= "0" && c <= "9") {
|
||||
let value = 0;
|
||||
while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48);
|
||||
return value;
|
||||
}
|
||||
if (c === "(") {
|
||||
pos++;
|
||||
const value = expr();
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
const start = pos;
|
||||
try {
|
||||
return call();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
pos = start;
|
||||
return name().length;
|
||||
}
|
||||
}
|
||||
function term(): number {
|
||||
let value = atom();
|
||||
while (peek() === "*") {
|
||||
pos++;
|
||||
value = (value * atom()) % 1000003;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function expr(): number {
|
||||
let value = term();
|
||||
while (peek() === "+" || peek() === "-") {
|
||||
const op = text[pos++];
|
||||
value = op === "+" ? value + term() : value - term();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
let seed = 4242;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function source(depth: number): string {
|
||||
const r = random();
|
||||
if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100));
|
||||
if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8));
|
||||
if (r < 0.6) return `(${source(depth + 1)})`;
|
||||
if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`;
|
||||
return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`;
|
||||
}
|
||||
|
||||
function parse(count: number): string {
|
||||
let total = 0;
|
||||
let failed = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Every tenth input lacks its last character, which fails deep inside it.
|
||||
const full = source(0);
|
||||
text = i % 10 === 0 ? full.slice(0, -1) : full;
|
||||
pos = 0;
|
||||
try {
|
||||
total = (total + expr()) % 1000003;
|
||||
if (pos !== text.length) throw new ParseError(pos, "trailing input");
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
return `parsed ${count}: total ${total} failed ${failed}`;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
console.log(chains(Math.floor(60000 * scale)));
|
||||
console.log(visits(Math.floor(60000 * scale)));
|
||||
console.log(parse(Math.floor(40000 * scale)));
|
||||
@@ -58,7 +58,7 @@ The LLVM helper divides an executable's LLVM IR into partitions by its size. Up
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. A function's partition follows from its name, and a small internal function goes with the one function that calls it, so an edit leaves the other functions in their partitions.
|
||||
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. In programs with 32 partitions or more, functions from the same source file are placed in the same partition, so that their calls to each other stay inside it; a file larger than 1 MiB of LLVM text or than an average partition is divided into parts by function name. Other functions, and the functions of smaller programs, are placed by their name, and a small internal function goes with the one function that calls it. An edit leaves almost every other function in its partition.
|
||||
|
||||
These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`.
|
||||
Values whose type can be cyclic, such as recursive records, also read as `undefined`.
|
||||
`assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
|
||||
The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
|
||||
`throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
|
||||
`assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
|
||||
`assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
|
||||
Checked-dynamic values compared deeply have no cycle detection.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported.
|
||||
Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Unions of a Date with other non-nullish types are unsupported.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
- **Date:** Spread constructor arguments and locale formatters are unsupported.
|
||||
Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
|
||||
A Date converted to `unknown` is the same Date object.
|
||||
See [Dates and timezones](#dates-and-timezones) for timezone data.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
|
||||
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
|
||||
The inherited methods and accessors run with the inheriting object as `this`.
|
||||
Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
|
||||
`C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
|
||||
Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
|
||||
Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
|
||||
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
|
||||
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
|
||||
`instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
@@ -36,6 +37,9 @@ struct Symbol {
|
||||
std::string PromotedHeader;
|
||||
std::vector<unsigned> Refs;
|
||||
std::vector<unsigned> Metadata;
|
||||
// The source-module group the emitter wrote before this definition, or
|
||||
// empty.
|
||||
StringRef Group;
|
||||
unsigned Partition = NoPartition;
|
||||
};
|
||||
|
||||
@@ -70,6 +74,83 @@ uint32_t stableHash(StringRef Name) {
|
||||
return Hash;
|
||||
}
|
||||
|
||||
// Places the functions whose root carries a source-module group by group: a
|
||||
// module's functions call each other most, so keeping them together leaves
|
||||
// fewer calls for the thin link to import across partitions. A group larger
|
||||
// than the mean partition, or than MaxChunkBytes, is divided into a power of
|
||||
// two of chunks by the hash of each root's name, so the division changes only
|
||||
// when the group's size doubles. Chunks are placed largest first, each in the
|
||||
// lighter of two partitions chosen by hashes of its key, after the functions
|
||||
// placed by name. The placement depends only on the module text and the
|
||||
// partition count. Modules with fewer than MinGroupedPartitions partitions
|
||||
// keep the placement by name: with few partitions per core, the balance of
|
||||
// the largest partitions decides the build time more than the imports.
|
||||
void placeGroups(std::vector<Symbol> &Symbols, ArrayRef<unsigned> Roots,
|
||||
unsigned Count) {
|
||||
constexpr unsigned MinGroupedPartitions = 32;
|
||||
if (Count < MinGroupedPartitions)
|
||||
return;
|
||||
auto Bytes = [](const Symbol &S) -> uint64_t {
|
||||
return S.Header.size() + S.Body.size();
|
||||
};
|
||||
std::vector<uint64_t> Load(Count, 0);
|
||||
uint64_t Total = 0;
|
||||
StringMap<uint64_t> GroupBytes;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
Total += Bytes(S);
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (Group.empty())
|
||||
Load[S.Partition] += Bytes(S);
|
||||
else
|
||||
GroupBytes[Group] += Bytes(S);
|
||||
}
|
||||
if (GroupBytes.empty())
|
||||
return;
|
||||
// A chunk is at most MaxChunkBytes, so the code a partition optimizes
|
||||
// together, and the inlining within it, does not grow when a program has
|
||||
// fewer partitions.
|
||||
constexpr uint64_t MaxChunkBytes = 1024 * 1024;
|
||||
uint64_t Chunk =
|
||||
std::min(MaxChunkBytes, std::max<uint64_t>(1, Total / Count));
|
||||
std::vector<std::string> Keys(Symbols.size());
|
||||
StringMap<uint64_t> KeyBytes;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (S.Kind != SymbolKind::Function || Group.empty())
|
||||
continue;
|
||||
uint64_t Chunks = 1;
|
||||
while (Chunks * Chunk < GroupBytes[Group])
|
||||
Chunks *= 2;
|
||||
Keys[I] = Chunks == 1
|
||||
? Group.str()
|
||||
: (Group + "#" +
|
||||
Twine(stableHash(Symbols[Roots[I]].Name) % Chunks))
|
||||
.str();
|
||||
KeyBytes[Keys[I]] += Bytes(S);
|
||||
}
|
||||
std::vector<std::pair<StringRef, uint64_t>> Order;
|
||||
for (const auto &Entry : KeyBytes)
|
||||
Order.push_back({Entry.getKey(), Entry.getValue()});
|
||||
llvm::sort(Order, [](const auto &A, const auto &B) {
|
||||
return A.second != B.second ? A.second > B.second : A.first < B.first;
|
||||
});
|
||||
StringMap<unsigned> Where;
|
||||
for (const auto &[Key, Size] : Order) {
|
||||
unsigned First = stableHash(Key) % Count;
|
||||
unsigned Second = stableHash((Key + "/2").str()) % Count;
|
||||
unsigned Chosen = Load[Second] < Load[First] ? Second : First;
|
||||
Load[Chosen] += Size;
|
||||
Where[Key] = Chosen;
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (!Keys[I].empty())
|
||||
Symbols[I].Partition = Where[Keys[I]];
|
||||
}
|
||||
|
||||
bool startsWithLinkage(StringRef Rest) {
|
||||
for (StringRef Linkage :
|
||||
{"linkonce", "weak", "common", "appending", "extern_weak",
|
||||
@@ -326,6 +407,11 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
return true;
|
||||
};
|
||||
|
||||
// The emitter writes "; scriptc-group <key>" before the definitions of each
|
||||
// source function; the group applies until the next such line, and an
|
||||
// empty key ends it.
|
||||
constexpr StringRef GroupLine = "; scriptc-group";
|
||||
StringRef Group;
|
||||
size_t Pos = 0;
|
||||
while (Pos < Text.size()) {
|
||||
size_t End = Text.find('\n', Pos);
|
||||
@@ -335,10 +421,15 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
size_t Next = End + 1;
|
||||
if (Line.empty() || Line.trim().empty()) {
|
||||
// Blank separator.
|
||||
} else if (Line.starts_with(GroupLine) &&
|
||||
(Line.size() == GroupLine.size() ||
|
||||
Line[GroupLine.size()] == ' ')) {
|
||||
Group = Line.drop_front(GroupLine.size()).trim();
|
||||
} else if (Line.starts_with("define ")) {
|
||||
Symbol S;
|
||||
S.Kind = SymbolKind::Function;
|
||||
S.Header = Line;
|
||||
S.Group = Group;
|
||||
if (!parseFunctionHeader(Line, S))
|
||||
return std::nullopt;
|
||||
// The emitter indents every body line, so the first line that is
|
||||
@@ -466,6 +557,7 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
Referrer[Ref] = I;
|
||||
}
|
||||
}
|
||||
std::vector<unsigned> Roots(Symbols.size());
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
if (Symbols[I].Kind != SymbolKind::Function)
|
||||
continue;
|
||||
@@ -479,8 +571,10 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
break;
|
||||
Root = Parent;
|
||||
}
|
||||
Roots[I] = Root;
|
||||
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
|
||||
}
|
||||
placeGroups(Symbols, Roots, Count);
|
||||
for (const Symbol &S : Symbols) {
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
|
||||
@@ -20,11 +20,15 @@ struct ProgramPartitions {
|
||||
};
|
||||
|
||||
// Divides the LLVM assembly scriptc emits into Count modules without parsing
|
||||
// it, so the partitions can be parsed concurrently. Functions are placed by a
|
||||
// stable hash of their name; an internal function referenced by exactly one
|
||||
// other function stays with that function unless its body is too large to
|
||||
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
|
||||
// emitter's narrow top-level grammar; callers then split a parsed module.
|
||||
// it, so the partitions can be parsed concurrently. In modules of 32 or
|
||||
// more partitions, functions under a "; scriptc-group <key>" line are placed
|
||||
// with the other functions of that source-module group, which is divided
|
||||
// into chunks when it is larger than the mean partition or 1 MiB; other
|
||||
// functions are placed by a stable hash of their name. An internal function referenced by exactly one other function stays
|
||||
// with that function unless its body is too large to inline, and a global
|
||||
// stays with the first function that references it. Returns nullopt for any
|
||||
// construct outside the emitter's narrow top-level grammar; callers then
|
||||
// split a parsed module.
|
||||
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
|
||||
unsigned Count);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -21,6 +21,7 @@ import {
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { lldLinkerArgs } from "./lld.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
installDarwinDebugSymbols,
|
||||
@@ -240,8 +241,11 @@ export async function linkNativeExecutable(
|
||||
try {
|
||||
const staged = await stageRuntimePackArtifacts(plan.runtimePack);
|
||||
stagedRoot = staged.root;
|
||||
// Linux links with lld when the default driver finds it.
|
||||
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
|
||||
const args = [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...plan.systemLibraries.map((name) => `-l${name}`),
|
||||
@@ -276,7 +280,15 @@ export async function linkNativeExecutable(
|
||||
...(await snapshotDependencies([...toolchain, ...additionalDependencyPaths])),
|
||||
])
|
||||
.catch(() => null);
|
||||
await execFileAsync(linker, args);
|
||||
await execFileAsync(linker, args).catch(async (error: unknown) => {
|
||||
// A failed link with lld runs again with the driver's default linker,
|
||||
// whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
await execFileAsync(
|
||||
linker,
|
||||
args.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
});
|
||||
const output = await stat(privateOut);
|
||||
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
|
||||
if (plan.darwinDebugSymbols) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { driverFindsLld, lldLinkerArgs } from "./lld.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function tool(path: string): string {
|
||||
writeFileSync(path, "#!/bin/sh\n");
|
||||
chmodSync(path, 0o755);
|
||||
return path;
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-lld-"));
|
||||
directories.push(root);
|
||||
const llvm = join(root, "llvm", "bin");
|
||||
const usr = join(root, "usr", "bin");
|
||||
const other = join(root, "other");
|
||||
for (const directory of [llvm, usr, other]) mkdirSync(directory, { recursive: true });
|
||||
return { root, llvm, usr, other };
|
||||
}
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a driver finds ld.lld beside itself, beside its real executable, or on PATH",
|
||||
() => {
|
||||
const f = fixture();
|
||||
const clang = tool(join(f.llvm, "clang"));
|
||||
// Without ld.lld anywhere.
|
||||
expect(driverFindsLld(clang, { PATH: f.other })).toBe(false);
|
||||
// Beside the driver.
|
||||
tool(join(f.llvm, "ld.lld"));
|
||||
expect(driverFindsLld(clang, { PATH: f.other })).toBe(true);
|
||||
// A PATH entry that links to the driver: clang looks beside its real executable.
|
||||
symlinkSync(clang, join(f.usr, "clang"));
|
||||
expect(driverFindsLld("clang", { PATH: f.usr })).toBe(true);
|
||||
// A driver elsewhere finds ld.lld on PATH, and not otherwise.
|
||||
const gcc = tool(join(f.other, "gcc"));
|
||||
expect(driverFindsLld(gcc, { PATH: f.other })).toBe(false);
|
||||
expect(driverFindsLld(gcc, { PATH: [f.other, f.llvm].join(":") })).toBe(true);
|
||||
expect(driverFindsLld("missing-driver", { PATH: f.llvm })).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"Linux links with the default driver use lld when the driver finds it",
|
||||
() => {
|
||||
const f = fixture();
|
||||
const clang = tool(join(f.llvm, "clang"));
|
||||
tool(join(f.llvm, "ld.lld"));
|
||||
const env = { PATH: f.other };
|
||||
expect(lldLinkerArgs("linux", clang, env)).toEqual(["-fuse-ld=lld"]);
|
||||
expect(lldLinkerArgs("darwin", clang, env)).toEqual([]);
|
||||
expect(lldLinkerArgs("windows", clang, env)).toEqual([]);
|
||||
expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
function isFile(path: string): boolean {
|
||||
try {
|
||||
return statSync(path).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The executable `command` names: a path, or the first match on PATH. */
|
||||
function commandFile(command: string, env: NodeJS.ProcessEnv): string | null {
|
||||
if (command.includes("/")) return isFile(command) ? resolve(command) : null;
|
||||
for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) {
|
||||
const candidate = join(directory || ".", command);
|
||||
if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in
|
||||
* the directory of the driver's executable (where clang looks first) or on
|
||||
* PATH. Linking still falls back to the driver's default linker when a link
|
||||
* with lld fails.
|
||||
*/
|
||||
export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const file = commandFile(driver, env);
|
||||
if (file === null) return false;
|
||||
try {
|
||||
for (const directory of new Set([dirname(file), dirname(realpathSync(file))]))
|
||||
if (isFile(join(directory, "ld.lld"))) return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return commandFile("ld.lld", env) !== null;
|
||||
}
|
||||
|
||||
/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */
|
||||
export function lldLinkerArgs(
|
||||
platform: string,
|
||||
driver: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env)
|
||||
? ["-fuse-ld=lld"]
|
||||
: [];
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
* code after it); here the lines are DROPPED instead, and emitStmts skips
|
||||
* whole statements once the current block is terminated, so no dropped
|
||||
* definition can leak into a later block. */
|
||||
import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export class BlockBuilder {
|
||||
private readonly blocks: { label: string; lines: string[]; term: string | null }[] = [];
|
||||
@@ -86,7 +87,18 @@ export class BlockBuilder {
|
||||
|
||||
terminate(s: string): void {
|
||||
if (this.cur.term === null) {
|
||||
if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue);
|
||||
if (
|
||||
this.returnEpilogue &&
|
||||
(s.startsWith("ret ") ||
|
||||
s.startsWith("call void @scr_unwind_raise()") ||
|
||||
s === UNWIND_RESUME)
|
||||
)
|
||||
this.line(this.returnEpilogue);
|
||||
if (s === UNWIND_RESUME) {
|
||||
const exception = this.tmp();
|
||||
this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`);
|
||||
s = `resume { ptr, i32 } ${exception}`;
|
||||
}
|
||||
this.cur.term = ` ${this.withDebugLocation(s)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +580,9 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () =>
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m));
|
||||
expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/);
|
||||
const fast = body.slice(body.lastIndexOf("bytes.version.fast"));
|
||||
// The byte copy, up to the units copy that follows it.
|
||||
const units = body.search(/^bytes\.version\.fast\.units\d+:/m);
|
||||
const fast = body.slice(body.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units);
|
||||
expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
|
||||
expect(fast).not.toContain("@sc_str_char_code_at_int");
|
||||
// The checked copy keeps the inline helper and its fallback.
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
import {
|
||||
emitStringAsciiFlag,
|
||||
emitStringByteLength,
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
stringScanType,
|
||||
} from "./string-reads.js";
|
||||
import { unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -73,6 +79,14 @@ export interface BytesLoopVersion {
|
||||
receivers: { ref: VarRef; global: boolean; string: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
ownedReceivers: number[];
|
||||
/** The string reads among `bounds`. */
|
||||
stringReads: ReadonlySet<Node>;
|
||||
/** The version's one string receiver, when the loop may also run over
|
||||
* its UTF-16 units if it is not ASCII (string-unit-versions.ts). */
|
||||
unitsReceiver: number | null;
|
||||
/** The units receiver's `length` reads in the loop. Both fast copies
|
||||
* read the length the guard took (its UTF-16 length) in their place. */
|
||||
unitLengths: ReadonlySet<IrExpr>;
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** While loops: accesses at a counter below a loop-invariant bound. */
|
||||
@@ -114,6 +128,9 @@ export interface BytesLoopFacts {
|
||||
/** String bindings held as a slice of another string (string-slices.ts):
|
||||
* they have no string header for the guard to read. */
|
||||
scalarStrings: ReadonlySet<string>;
|
||||
/** The functions a loop over a string's UTF-16 units may call; absent
|
||||
* or null where emitted code does not read the runtime's scan receiver. */
|
||||
unitSafeFunctions?: ReadonlySet<string> | null;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
@@ -670,6 +687,19 @@ export function matchBytesLoopVersion(
|
||||
// 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 stringReads = new Set<Node>();
|
||||
for (const group of [...fixedKept, ...affineKept])
|
||||
if (receivers[group.check.receiver]!.string)
|
||||
for (const node of group.nodes) stringReads.add(node);
|
||||
for (const group of boundedKept)
|
||||
if (receivers[group.check.receiver]!.string)
|
||||
for (const { node } of group.nodes) stringReads.add(node);
|
||||
const strings = order.filter((index) => receivers[index]!.string);
|
||||
const unitLengths =
|
||||
facts.unitSafeFunctions != null && strings.length === 1
|
||||
? unitSafeLoop(loop, receivers[strings[0]!]!.ref.localId, facts.unitSafeFunctions)
|
||||
: null;
|
||||
const unitsReceiver = unitLengths !== null ? renumber.get(strings[0]!)! : null;
|
||||
const needsBound =
|
||||
affineKept.length > 0 &&
|
||||
counted !== null &&
|
||||
@@ -679,6 +709,9 @@ export function matchBytesLoopVersion(
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
stringReads,
|
||||
unitsReceiver,
|
||||
unitLengths: unitLengths ?? new Set<IrExpr>(),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
@@ -1018,13 +1051,22 @@ function findWhileCounterAccesses(
|
||||
/** Branches to `fast` when every access the version proves is in bounds for
|
||||
* every iteration and every view it proves thread-owned has no shared
|
||||
* storage, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set). */
|
||||
* integer form is not established, or a module receiver not yet set).
|
||||
*
|
||||
* With `unitsFast`, the version's units receiver may instead be a string
|
||||
* that is not ASCII: the guard has the runtime publish it as the scan
|
||||
* receiver (one read), tests the same accesses against its UTF-16 length,
|
||||
* and branches to `unitsFast`. It returns the units pointer that copy
|
||||
* reads, which stays valid while the loop runs (string-unit-versions.ts),
|
||||
* and for a version with a units receiver, the receiver's UTF-16 length
|
||||
* on both fast paths (an i64). */
|
||||
export function emitBytesLoopVersionGuard(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
fast: string,
|
||||
slow: string,
|
||||
): void {
|
||||
unitsFast?: string,
|
||||
): { units: string | null; length: string | null } | null {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
if (cond === "true") return;
|
||||
@@ -1043,19 +1085,36 @@ export function emitBytesLoopVersionGuard(
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
const len = version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver);
|
||||
const unitsIndex = unitsFast !== undefined ? version.unitsReceiver : null;
|
||||
const units: { read: { pointer: string; taken: string; published: string } | null } = {
|
||||
read: null,
|
||||
};
|
||||
const widen = (len: string): string => {
|
||||
if (host.sizeType === "i64") return len;
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
|
||||
return wide;
|
||||
};
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
if (index === unitsIndex) {
|
||||
const read = emitUnitsLength(host, receiver);
|
||||
units.read = read;
|
||||
return read.length;
|
||||
}
|
||||
return widen(
|
||||
version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver),
|
||||
);
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
// The published count is the receiver's UTF-16 length only while the
|
||||
// runtime publishes the receiver itself.
|
||||
if (units.read) conditions.push(units.read.published);
|
||||
// A string's byte length is its UTF-16 length when it is ASCII.
|
||||
version.receivers.forEach(({ string }, index) => {
|
||||
if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
if (string && index !== unitsIndex)
|
||||
conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
});
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
@@ -1158,7 +1217,7 @@ export function emitBytesLoopVersionGuard(
|
||||
// Unreachable: every term has a proven range. Stay checked.
|
||||
B.br(slow);
|
||||
B.startBlock(B.newLabel("bytes.version.dead"));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const wide = widenInteger(host, integer);
|
||||
const sum = B.tmp();
|
||||
@@ -1215,8 +1274,70 @@ export function emitBytesLoopVersionGuard(
|
||||
valid = all;
|
||||
}
|
||||
}
|
||||
if (valid === "true") B.br(fast);
|
||||
else B.condBr(valid, fast, slow);
|
||||
const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null;
|
||||
const taken = units.read;
|
||||
const target = taken ? B.newLabel("bytes.version.units") : fast;
|
||||
if (valid === "true") B.br(target);
|
||||
else B.condBr(valid, target, slow);
|
||||
if (!taken) return { units: null, length };
|
||||
B.startBlock(target);
|
||||
B.condBr(taken.taken, unitsFast!, fast);
|
||||
return { units: taken.pointer, length };
|
||||
}
|
||||
|
||||
/** The length of a version's units receiver `s` in UTF-16 units: its byte
|
||||
* length when it is ASCII; otherwise, after one runtime read publishes it
|
||||
* as the scan receiver, the published unit count (0 when the runtime does
|
||||
* not hold its units). Also the units pointer, whether the loop is to
|
||||
* read it, and whether the published receiver is `s`. */
|
||||
function emitUnitsLength(
|
||||
host: LlvmEmitterContext,
|
||||
s: string,
|
||||
): { length: string; pointer: string; taken: string; published: string } {
|
||||
const B = host.B;
|
||||
const sz = host.sizeType;
|
||||
const type = stringScanType(sz);
|
||||
const ascii = emitStringAsciiFlag(host, s);
|
||||
const bytes = emitStringByteLength(host, s);
|
||||
const from = B.currentLabel();
|
||||
const publish = B.newLabel("bytes.version.publish"),
|
||||
join = B.newLabel("bytes.version.length");
|
||||
B.condBr(ascii, join, publish);
|
||||
B.startBlock(publish);
|
||||
host.declare(STRING_CHAR_CODE_AT_DECL);
|
||||
const read = B.tmp(),
|
||||
receiver = B.tmp(),
|
||||
same = B.tmp(),
|
||||
pointerSlot = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
B.line(`${read} = call double @scr_str_char_code_at(ptr ${s}, double 0.0)`);
|
||||
B.line(`${receiver} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${same} = icmp eq ptr ${receiver}, ${s}`);
|
||||
B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`);
|
||||
B.line(`${pointer} = load ptr, ptr ${pointerSlot}`);
|
||||
B.line(`${countSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 2`);
|
||||
B.line(`${count} = load ${sz}, ptr ${countSlot}`);
|
||||
B.line(`${published} = select i1 ${same}, ${sz} ${count}, ${sz} 0`);
|
||||
const end = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const length = B.tmp(),
|
||||
units = B.tmp(),
|
||||
taken = B.tmp();
|
||||
B.line(`${length} = phi ${sz} [ ${bytes}, %${from} ], [ ${published}, %${end} ]`);
|
||||
B.line(`${units} = phi ptr [ null, %${from} ], [ ${pointer}, %${end} ]`);
|
||||
B.line(`${taken} = phi i1 [ false, %${from} ], [ true, %${end} ]`);
|
||||
const exact = B.tmp();
|
||||
B.line(`${exact} = phi i1 [ true, %${from} ], [ ${same}, %${end} ]`);
|
||||
let wide = length;
|
||||
if (sz !== "i64") {
|
||||
wide = B.tmp();
|
||||
B.line(`${wide} = zext ${sz} ${length} to i64`);
|
||||
}
|
||||
return { length: wide, pointer: units, taken, published: exact };
|
||||
}
|
||||
|
||||
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
|
||||
|
||||
@@ -50,8 +50,8 @@ export function emitCountedLoopLimit(
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
* the integer form's finite range and an unproven start is an exact integer
|
||||
* within the loop's start bound. Returns `true` when nothing needs testing. */
|
||||
export function emitCountedLoopSafety(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
@@ -65,7 +65,11 @@ export function emitCountedLoopSafety(
|
||||
`${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`,
|
||||
);
|
||||
}
|
||||
if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start));
|
||||
if (loop.startGuarded) {
|
||||
const start = host.emitExpr(loop.start);
|
||||
const bound = loop.startBound;
|
||||
safe = andSafe(host, safe, emitExactIntegerTest(host, start.name, -bound, bound).ok);
|
||||
}
|
||||
for (const follower of loop.followers) {
|
||||
if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start));
|
||||
if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower));
|
||||
|
||||
@@ -122,6 +122,7 @@ const counted = (
|
||||
guarded: false,
|
||||
limitGuarded: false,
|
||||
startGuarded: false,
|
||||
startBound: Number.MAX_SAFE_INTEGER,
|
||||
guardLimit: 0,
|
||||
followers: [],
|
||||
...fields,
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
recordTextCodecClass,
|
||||
recordHandleCodecClass,
|
||||
textCodecRecordClass,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
@@ -98,14 +98,16 @@ export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
|
||||
/** The handle tags of TextEncoder and TextDecoder records boxed as dyn
|
||||
* (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */
|
||||
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = {
|
||||
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder" | "StringDecoder", number> = {
|
||||
TextDecoder: 76,
|
||||
TextEncoder: 77,
|
||||
StringDecoder: 78,
|
||||
};
|
||||
|
||||
/** The decoder record fields scr_text_decoder_to_dyn reads, in its
|
||||
* parameter order. */
|
||||
const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const;
|
||||
const STRING_DECODER_FIELDS = ["%enc", "%pending"] as const;
|
||||
|
||||
/** Handle kinds whose runtime unit boxes them through its own function,
|
||||
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
||||
@@ -1825,11 +1827,18 @@ export class LlDyn {
|
||||
const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id));
|
||||
if (codec !== null) {
|
||||
// The record the codec's handle box holds (+1), or the path TypeError.
|
||||
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
|
||||
const checked = B.tmp();
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
if (codec === "StringDecoder") {
|
||||
host.declare(`declare ptr @scr_string_decoder_from_dyn(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_string_decoder_from_dyn(ptr %d, ptr %path, ptr ${want})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`,
|
||||
@@ -2917,14 +2926,14 @@ export class LlDyn {
|
||||
return matched;
|
||||
}
|
||||
|
||||
/** A TextEncoder or TextDecoder record boxes as its handle (+1), which
|
||||
* holds the record by reference. A decoder passes the byte offsets of the
|
||||
* fields its boxed decode reads. */
|
||||
/** A TextEncoder, TextDecoder or StringDecoder record boxes as its
|
||||
* handle (+1), which holds the record by reference. The decoders pass
|
||||
* the byte offsets of the fields their boxed methods read. */
|
||||
private textCodecToDyn(
|
||||
B: BlockBuilder,
|
||||
t: IrType & { kind: "record" },
|
||||
shape: IrRecordShape,
|
||||
codec: "TextEncoder" | "TextDecoder",
|
||||
codec: "TextEncoder" | "TextDecoder" | "StringDecoder",
|
||||
): void {
|
||||
const host = this.host;
|
||||
const rc = vAdapters(host, t);
|
||||
@@ -2939,10 +2948,12 @@ export class LlDyn {
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const start = B.tmp();
|
||||
B.line(`${start} = ptrtoint ptr %v to ${this.S}`);
|
||||
const offsets = TEXT_DECODER_FIELDS.map((field) => {
|
||||
const fields: readonly string[] =
|
||||
codec === "StringDecoder" ? STRING_DECODER_FIELDS : TEXT_DECODER_FIELDS;
|
||||
const offsets = fields.map((field) => {
|
||||
const index = shape.fields.findIndex((f) => f.name === field);
|
||||
if (index < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`);
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${codec} record lacks ${field}`);
|
||||
const p = B.tmp();
|
||||
const at = B.tmp();
|
||||
const offset = B.tmp();
|
||||
@@ -2953,11 +2964,11 @@ export class LlDyn {
|
||||
B.line(`${offset} = sub ${this.S} ${at}, ${start}`);
|
||||
return `${this.S} ${offset}`;
|
||||
});
|
||||
host.declare(
|
||||
`declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`,
|
||||
);
|
||||
const fn =
|
||||
codec === "StringDecoder" ? "scr_string_decoder_to_dyn" : "scr_text_decoder_to_dyn";
|
||||
host.declare(`declare ptr @${fn}(ptr, ptr, ptr, ${fields.map(() => this.S).join(", ")})`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
|
||||
`${r} = call ptr @${fn}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
@@ -3137,7 +3148,7 @@ export class LlDyn {
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
if (!shape)
|
||||
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
||||
const codec = recordTextCodecClass(shape);
|
||||
const codec = recordHandleCodecClass(shape);
|
||||
if (codec !== null) {
|
||||
this.textCodecToDyn(B, t, shape, codec);
|
||||
break;
|
||||
|
||||
@@ -56,11 +56,14 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
|
||||
import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name);
|
||||
emitter.usesLandingPads = false;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
@@ -136,6 +139,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.loopCopiesEnabled = true;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
@@ -159,6 +163,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.optionalArrayReads,
|
||||
emitter.referenceWrites,
|
||||
emitter.fnByName,
|
||||
emitter.nodeIndex,
|
||||
)
|
||||
: new Set();
|
||||
emitter.localArrayReads = findLocalArrayReads(
|
||||
@@ -324,7 +329,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
|
||||
const paramIds = new Set(fn.params.map((p) => p.localId));
|
||||
if (emitter.debug === null) {
|
||||
for (const id of findIntegerViews(numericFn)) {
|
||||
for (const id of findIntegerViews(numericFn, emitter.nodeIndex)) {
|
||||
// An i32 slot is its own view.
|
||||
if (emitter.int32Locals.has(id)) continue;
|
||||
const slot = B.slot();
|
||||
@@ -595,6 +600,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
B.terminate(cleanup.terminator);
|
||||
}
|
||||
|
||||
if (emitter.usesLandingPads && emitter.currentUnwindAbi)
|
||||
B.entryAllocas.push(`${UNWIND_SLOT} = alloca { ptr, i32 }`);
|
||||
if (emitter.logArgSlots > 0) {
|
||||
B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`);
|
||||
}
|
||||
@@ -635,7 +642,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const debug = emitter.debugScope === null ? "" : ` !dbg ${emitter.debugScope}`;
|
||||
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
|
||||
emitter.privateGlobalSlots.clear();
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
// Landing pads (emitDirectCall) need the personality.
|
||||
const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi);
|
||||
if (emitter.integerClones.members.has(fn.name)) {
|
||||
body += "\n" + emitIntegerClone(fn, emitter, emitter.stackChecks?.has(fn.name) === true);
|
||||
@@ -1307,8 +1317,10 @@ export function emitArgPackAndTrampolinePrologue(
|
||||
* code. */
|
||||
export function parkingFunctions(emitter: LlEmitter): Set<string> | null {
|
||||
if (emitter.wasi || emitter.mod.workers === true) return null;
|
||||
for (const decl of emitter.decls) if (/@scr_test_sub\b/.test(decl)) return null;
|
||||
for (const text of emitter.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null;
|
||||
const testSub = (text: string): boolean =>
|
||||
text.includes("@scr_test_sub") && /@scr_test_sub\b/.test(text);
|
||||
for (const decl of emitter.decls) if (testSub(decl)) return null;
|
||||
for (const text of emitter.fnDefText.values()) if (testSub(text)) return null;
|
||||
// Reverse mention edges, then propagate "may park" to every mentioner.
|
||||
const mentionedBy = new Map<string, Set<string>>();
|
||||
const parks = new Set<string>();
|
||||
|
||||
@@ -61,10 +61,11 @@ import { hasLoneSurrogate, wtf8Bytes } from "../literals.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
|
||||
import { literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
|
||||
@@ -206,6 +207,26 @@ function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean)
|
||||
];
|
||||
}
|
||||
|
||||
/** The group line written before each function's text. Functions from one
|
||||
* source file share a group, which the LLVM helper keeps in the same
|
||||
* partitions so that their calls to each other stay inside a partition
|
||||
* (native/llvm-codegen/src/partition.h). A group is numbered by the first
|
||||
* function of its file in the module's function order, which does not depend
|
||||
* on the host. Functions without a source file get no group. */
|
||||
function partitionGroupLines(functions: readonly IrFunction[]): string[] {
|
||||
const groups = new Map<string, number>();
|
||||
return functions.map((fn) => {
|
||||
const file = fn.loc.file;
|
||||
if (file === "") return PARTITION_GROUP_END;
|
||||
let group = groups.get(file);
|
||||
if (group === undefined) groups.set(file, (group = groups.size));
|
||||
return `${PARTITION_GROUP_END} ${group}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** Ends the group of the definitions that follow. */
|
||||
const PARTITION_GROUP_END = "; scriptc-group";
|
||||
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
@@ -915,9 +936,13 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
for (const line of wrappers) out.push(line);
|
||||
for (const line of asyncDefs) out.push(line);
|
||||
for (const line of emitter.resolveThunkDefs) out.push(line);
|
||||
out.push(fnDefs[0] ?? "");
|
||||
for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!);
|
||||
out.push("");
|
||||
const groups = partitionGroupLines(emitter.mod.functions);
|
||||
for (let i = 0; i < fnDefs.length; i++) {
|
||||
if (i > 0) out.push("");
|
||||
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
|
||||
}
|
||||
if (fnDefs.length === 0) out.push("");
|
||||
out.push(PARTITION_GROUP_END, "");
|
||||
// WebAssembly modules translated into this program, and the registry.
|
||||
for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
|
||||
out.push(line);
|
||||
@@ -1590,9 +1615,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// A scan's receiver reads inline too (ScrStrScan): from ordinary global
|
||||
// storage, or, in worker executables, from the thread's static TLS block,
|
||||
// one thread-pointer-relative load (see executableTls).
|
||||
const scanInline =
|
||||
emitter.executableTls ||
|
||||
(emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true);
|
||||
const scanInline = stringScanInline(emitter);
|
||||
let scanDeclared = false;
|
||||
for (const helper of STRING_READ_HELPERS) {
|
||||
if (!emitter.decls.has(helper.decl)) continue;
|
||||
|
||||
@@ -994,7 +994,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
LOOPS.of(emitter).denseLoop = dense;
|
||||
@@ -1018,7 +1018,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).byteWindowsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.integerRanges = window.ranges;
|
||||
@@ -1072,7 +1072,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(safe, fast, slow);
|
||||
B.startBlock(slow);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
B.br(countedJoin);
|
||||
B.startBlock(fast);
|
||||
@@ -2049,7 +2049,7 @@ function emitCallbackVersionedLoop(
|
||||
emitter.callbackVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId);
|
||||
@@ -2062,6 +2062,20 @@ function emitCallbackVersionedLoop(
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
/** The copy of a versioned loop that runs when its guard fails. Its loops
|
||||
* get no speculative or int32 kind copies, which would emit each loop again
|
||||
* for a path that rarely runs. Proven integer loops, which emit one copy,
|
||||
* are kept. */
|
||||
function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
|
||||
const copies = emitter.loopCopiesEnabled;
|
||||
emitter.loopCopiesEnabled = false;
|
||||
try {
|
||||
emitter.emitStmt(s);
|
||||
} finally {
|
||||
emitter.loopCopiesEnabled = copies;
|
||||
}
|
||||
}
|
||||
|
||||
/** Entries in a row whose fast copy bailed before a speculative loop site
|
||||
* stops trying it. */
|
||||
const SPECULATION_BAILS = 8;
|
||||
@@ -2082,28 +2096,56 @@ function emitVersionedLoop(
|
||||
const fast = B.newLabel("bytes.version.fast"),
|
||||
slow = B.newLabel("bytes.version.slow"),
|
||||
done = B.newLabel("bytes.version.done");
|
||||
emitBytesLoopVersionGuard(emitter, version, fast, slow);
|
||||
const unitsFast =
|
||||
version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast);
|
||||
const units = guard?.units ?? null;
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
originalRanges = emitter.integerRanges;
|
||||
originalRanges = emitter.integerRanges,
|
||||
originalUnitReads = emitter.stringUnitReads,
|
||||
originalLengthReads = emitter.stringLengthReads;
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
|
||||
if (version.ranges.size > 0)
|
||||
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
|
||||
if (version.counted?.guarded || version.paired?.loop.guarded)
|
||||
LOOPS.of(emitter).provenCountedGuards.add(s);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
// Both fast copies read the string's length the guard took.
|
||||
const length = guard?.length;
|
||||
if (length != null && version.unitLengths.size > 0)
|
||||
emitter.stringLengthReads = new Map([
|
||||
...originalLengthReads,
|
||||
...[...version.unitLengths].map((node) => [node, length] as const),
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
if (unitsFast !== undefined && units !== null) {
|
||||
// The string reads take the receiver's UTF-16 units.
|
||||
B.startBlock(unitsFast);
|
||||
emitter.bytesBounds = new Set([
|
||||
...originalBounds,
|
||||
...[...version.bounds].filter((node) => !version.stringReads.has(node)),
|
||||
]);
|
||||
emitter.stringUnitReads = new Map([
|
||||
...originalUnitReads,
|
||||
...[...version.stringReads].map((node) => [node, units] as const),
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
}
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.stringUnitReads = originalUnitReads;
|
||||
emitter.stringLengthReads = originalLengthReads;
|
||||
LOOPS.of(emitter).provenCountedGuards.delete(s);
|
||||
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
|
||||
}
|
||||
@@ -2120,6 +2162,7 @@ export function emitLoopCopies(
|
||||
const plan =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.speculation === null &&
|
||||
emitter.speculationResume === null
|
||||
? planSpeculativeLoop(
|
||||
@@ -2147,6 +2190,7 @@ export function emitLoopCopies(
|
||||
const kinds =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
|
||||
emitter.int32Kinds === null &&
|
||||
emitter.speculation === null &&
|
||||
@@ -2437,7 +2481,7 @@ function planProvenLoop(
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" }>,
|
||||
): ProvenLoopPlan | null {
|
||||
if (
|
||||
loop.kind !== "for" ||
|
||||
loop.kind === "doWhile" ||
|
||||
!emitter.speculativeLoopsEnabled ||
|
||||
emitter.speculation !== null ||
|
||||
emitter.speculationResume !== null ||
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
} from "./emit-journal.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import { inlineExpressionFunctions } from "../../ir/expression-inlining.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
fillAllocationLength,
|
||||
@@ -97,13 +98,20 @@ import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import {
|
||||
analyzeInt32Slots,
|
||||
dynamicFieldStores,
|
||||
type DynamicFieldStores,
|
||||
type Int32Slots,
|
||||
} from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js";
|
||||
import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js";
|
||||
import { hoistFinalFieldReads } from "../../ir/field-values.js";
|
||||
import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js";
|
||||
import { unitSafeFunctions } from "./string-unit-versions.js";
|
||||
import { stringScanInline } from "./string-reads.js";
|
||||
import {
|
||||
findInt32Abis,
|
||||
INT32_ABI_SUFFIX,
|
||||
@@ -123,6 +131,16 @@ import {
|
||||
type FfiCallbackAdapter,
|
||||
} from "../ffi-callbacks.js";
|
||||
import { computeMayThrow } from "../may-throw.js";
|
||||
import {
|
||||
UNWIND_PERSONALITY,
|
||||
UNWIND_RAISE,
|
||||
UNWIND_RAISE_DECLARATION,
|
||||
UNWIND_RESUME,
|
||||
UNWIND_SLOT,
|
||||
UnwindAbiViolation,
|
||||
bridgePlainUnwindCalls,
|
||||
unwindAbiFunctions,
|
||||
} from "./unwind-abi.js";
|
||||
import {
|
||||
mangleAsyncInlineAwaitCall,
|
||||
mangleAsyncSpawn,
|
||||
@@ -171,7 +189,7 @@ import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
|
||||
import { IrNodeIndex } from "../../ir/node-index.js";
|
||||
import { eachNodeOfKinds, IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import { lastUseRead } from "./local-extents.js";
|
||||
import { findReboundParameters } from "./rebound-parameters.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
@@ -434,6 +452,12 @@ export interface ForkPrimitives {
|
||||
export interface LlvmTargetOptions {
|
||||
/** C ABI target; omitted direct emission uses the host ABI. */
|
||||
targetTriple?: string;
|
||||
/** Linux (glibc and musl) executables: functions reached only by direct
|
||||
* calls propagate exceptions by unwinding the native stack (unwind-abi.ts). */
|
||||
unwindAbi?: boolean;
|
||||
/** Functions kept on the pending-flag ABI under `unwindAbi`: the ones an
|
||||
* earlier emission found breaking its rules (UnwindAbiViolation). */
|
||||
unwindExcluded?: ReadonlySet<string>;
|
||||
/** Emit the function bodies in this many contiguous ranges, each by its
|
||||
* own emitter, and merge them in function order (emit-journal.ts). The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
@@ -506,14 +530,24 @@ function versionParameters(mod: IrModule): IrModule {
|
||||
return versionLoopBoundParameters(mod, mangleFunction);
|
||||
}
|
||||
|
||||
/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and
|
||||
* checked), under the inputs LlEmitter gives it (selectInt32Arrays). */
|
||||
function provenInt32Arrays(mod: IrModule): ReadonlySet<string> {
|
||||
const slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores: dynamicFieldStores(mod),
|
||||
initializerBindings: findInitializerBindings(mod),
|
||||
});
|
||||
return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]);
|
||||
/** The int32 slot analysis of one module and the inputs it read: the
|
||||
* constructor's own analyses of that module, which LlEmitter takes over
|
||||
* instead of repeating them when it emits the same module. */
|
||||
export interface Int32SlotAnalysis {
|
||||
readonly mod: IrModule;
|
||||
readonly nodeIndex: IrNodeIndex;
|
||||
readonly initializerBindings: ReturnType<typeof findInitializerBindings>;
|
||||
readonly dynamicStores: DynamicFieldStores;
|
||||
readonly slots: Int32Slots;
|
||||
}
|
||||
|
||||
/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. */
|
||||
function analyzeModuleInt32Slots(mod: IrModule): Int32SlotAnalysis {
|
||||
const nodeIndex = new IrNodeIndex(mod);
|
||||
const initializerBindings = findInitializerBindings(mod, nodeIndex);
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex);
|
||||
const slots = analyzeInt32Slots(mod, { dynamicStores, initializerBindings, nodes: nodeIndex });
|
||||
return { mod, nodeIndex, initializerBindings, dynamicStores, slots };
|
||||
}
|
||||
|
||||
/** The functions whose calls the backend may emit as calls of their
|
||||
@@ -530,8 +564,16 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
// Each round of selectInt32Arrays analyzes the module it marks; the
|
||||
// emitter of the module it returns reuses that round's analysis.
|
||||
const rounds: { last: Int32SlotAnalysis | null } = { last: null };
|
||||
const proven = (marked: IrModule): ReadonlySet<string> => {
|
||||
const analysis = analyzeModuleInt32Slots(marked);
|
||||
rounds.last = analysis;
|
||||
return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]);
|
||||
};
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
const emitted =
|
||||
options.debugSources === undefined
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
@@ -539,13 +581,15 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -559,24 +603,44 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
),
|
||||
),
|
||||
),
|
||||
provenInt32Arrays,
|
||||
proven,
|
||||
resultEntryTargets,
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
);
|
||||
: mod;
|
||||
const prepared = rounds.last;
|
||||
return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
|
||||
}
|
||||
|
||||
/** Emits the module's parts, emitting it again with the functions an
|
||||
* UnwindAbiViolation names on the pending-flag ABI. */
|
||||
function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] {
|
||||
for (let excluded = options.unwindExcluded ?? new Set<string>(); ;) {
|
||||
const emitter = moduleEmitter(input, { ...options, unwindExcluded: excluded });
|
||||
try {
|
||||
return emitter.emitParts();
|
||||
} catch (error) {
|
||||
if (!(error instanceof UnwindAbiViolation)) throw error;
|
||||
const names = new Set(excluded);
|
||||
for (const name of emitter.unwindAbi)
|
||||
for (const symbol of [mangleFunction(name), mangleBorrowedFunction(name)])
|
||||
if (error.symbols.has(symbol) || error.symbols.has(symbol + INT32_ABI_SUFFIX))
|
||||
names.add(name);
|
||||
if (names.size === excluded.size) throw error;
|
||||
excluded = names;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
return moduleEmitter(input, options).emit();
|
||||
return emitModuleText(input, options).join("\n");
|
||||
}
|
||||
|
||||
/** emitLlvmModule's text as parts that concatenate to it, without the copy
|
||||
* that joining them makes: a large program's text is 150 MB. */
|
||||
export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] {
|
||||
return moduleEmitter(input, options)
|
||||
.emitParts()
|
||||
.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
return emitModuleText(input, options).flatMap((part, index) =>
|
||||
index === 0 ? [part] : ["\n", part],
|
||||
);
|
||||
}
|
||||
|
||||
/** Keep large translation units below the host's single-string limit. */
|
||||
@@ -586,7 +650,7 @@ export function emitLlvmModuleSource(
|
||||
): string | readonly string[] {
|
||||
// IR validation, contracts and the frontend's disposal run after lowering without checks.
|
||||
relieveHeapPressure(true);
|
||||
const parts = moduleEmitter(input, options).emitParts();
|
||||
const parts = emitModuleText(input, options);
|
||||
const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
|
||||
if (length <= 256 * 1024 * 1024) return parts.join("\n");
|
||||
return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
@@ -598,6 +662,21 @@ export function llStrBytes(text: string): string {
|
||||
|
||||
const NO_BORROWED: ReadonlySet<number> = new Set();
|
||||
|
||||
/** The nodes that write a binding by its id (numberGlobalWriters). */
|
||||
const GLOBAL_WRITE_KINDS: readonly IrNodeKind[] = ["assign", "varDecl", "assignExpr", "incDec"];
|
||||
/** Runtime library calls (readsLoopMetrics). */
|
||||
const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"];
|
||||
|
||||
/** [A-Za-z0-9_] */
|
||||
function isIdentifierByte(code: number): boolean {
|
||||
return (
|
||||
(code >= 48 && code <= 57) ||
|
||||
(code >= 65 && code <= 90) ||
|
||||
(code >= 97 && code <= 122) ||
|
||||
code === 95
|
||||
);
|
||||
}
|
||||
|
||||
/** Runtime entry points (and await-call entries) that may park the running
|
||||
* execution context, as they appear in emitted function text. */
|
||||
export const SUSPENDS =
|
||||
@@ -1215,6 +1294,10 @@ export class LlEmitter {
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
* checks are emitted only after calls that can actually raise. */
|
||||
readonly mayThrow: Set<string>;
|
||||
/** The functions that propagate exceptions by unwinding (unwind-abi.ts). */
|
||||
readonly unwindAbi: Set<string>;
|
||||
/** Whether the function being emitted is one of unwindAbi. */
|
||||
currentUnwindAbi = false;
|
||||
readonly indirectMayThrow: boolean;
|
||||
/** Worker programs: the functions that poll for termination on entry
|
||||
* (workerTerminationPolls); null outside worker programs. */
|
||||
@@ -1423,6 +1506,9 @@ export class LlEmitter {
|
||||
integerTempsEnabled = false;
|
||||
/** Speculative integer loops (speculative-integers.ts) run in this function. */
|
||||
speculativeLoopsEnabled = false;
|
||||
/** False inside the copy of a versioned loop that runs when its guard
|
||||
* fails: loops there get no speculative or int32 kind copies. */
|
||||
loopCopiesEnabled = true;
|
||||
/** The fast copy of a speculative integer loop being emitted: its
|
||||
* integer-computed writes and where each resumes in the ordinary copy. */
|
||||
speculation: {
|
||||
@@ -1477,6 +1563,12 @@ export class LlEmitter {
|
||||
* (one-unit tests and switches, charAtUnitRead), with the charAt each
|
||||
* stands for: a loop version's proof for the charAt covers its read. */
|
||||
readonly charAtUnitReads = new Map<IrExpr, IrExpr>();
|
||||
/** String reads a loop version proved in bounds of a string's UTF-16
|
||||
* units, with the units pointer to read (string-unit-versions.ts). */
|
||||
stringUnitReads: ReadonlyMap<IrExpr | IrStmt, string> = new Map();
|
||||
/** `length` reads of a loop version's string, with the i64 UTF-16
|
||||
* length its guard took (bytes-loop-versions.ts). */
|
||||
stringLengthReads: ReadonlyMap<IrExpr, string> = new Map();
|
||||
/** Worker programs: typed-array accesses whose views an enclosing version
|
||||
* guard proved thread-owned. */
|
||||
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
@@ -1562,6 +1654,7 @@ export class LlEmitter {
|
||||
readonly mod: IrModule,
|
||||
options: LlvmTargetOptions,
|
||||
journal: EmitJournal | null = null,
|
||||
int32Analysis: Int32SlotAnalysis | null = null,
|
||||
) {
|
||||
this.targetOptions = options;
|
||||
this.journal = journal;
|
||||
@@ -1572,7 +1665,7 @@ export class LlEmitter {
|
||||
options.debugSources === undefined);
|
||||
// One walk of every body, which the analyses below read instead of
|
||||
// walking the bodies themselves.
|
||||
this.nodeIndex = new IrNodeIndex(mod);
|
||||
this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod);
|
||||
this.stackTraces = observesStacks(mod, this.nodeIndex);
|
||||
this.nullableUnions = new NullableUnions(
|
||||
mod,
|
||||
@@ -1591,14 +1684,18 @@ export class LlEmitter {
|
||||
this.nullableUnions.armTypes(),
|
||||
);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings = findInitializerBindings(mod, this.nodeIndex);
|
||||
this.initializerBindings =
|
||||
int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
this.int32Slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
const dynamicStores =
|
||||
int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
this.int32Slots =
|
||||
int32Analysis?.slots ??
|
||||
analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.liveViewsReadOnly =
|
||||
mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0;
|
||||
this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map();
|
||||
@@ -1668,6 +1765,16 @@ export class LlEmitter {
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.mayThrow = mt.fns;
|
||||
this.unwindAbi =
|
||||
options.unwindAbi === true &&
|
||||
!this.wasi &&
|
||||
mod.lib === undefined &&
|
||||
options.debugSources === undefined &&
|
||||
(options.targetTriple
|
||||
? /-linux-(?:gnu|musl)/.test(options.targetTriple)
|
||||
: process.platform === "linux")
|
||||
? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded)
|
||||
: new Set();
|
||||
this.workerEntryPolls = mt.workerEntryPolls ?? null;
|
||||
this.stackChecks = mt.stackChecks ?? null;
|
||||
// A stackless call's eager prefix borrows its caller's stack; the
|
||||
@@ -1803,10 +1910,13 @@ export class LlEmitter {
|
||||
: undefined,
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
// Integer clones and their dispatchers call each other through their
|
||||
// own lowering, which keeps plain calls and flag tests.
|
||||
for (const name of this.integerClones.members) this.unwindAbi.delete(name);
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName, this.nodeIndex);
|
||||
const traced = computeTraced(mod, dynamicStores, cycleMutability);
|
||||
this.tracedShapes = traced.shapes;
|
||||
this.tracedUnions = traced.unions;
|
||||
@@ -2082,7 +2192,8 @@ export class LlEmitter {
|
||||
return this.emitParts().join("\n");
|
||||
}
|
||||
emitParts(): string[] {
|
||||
return emitParts(this);
|
||||
const parts = emitParts(this);
|
||||
return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
@@ -2235,9 +2346,23 @@ export class LlEmitter {
|
||||
this.mangledFunctions.set(mangleBorrowedFunction(fn.name), fn.name);
|
||||
}
|
||||
}
|
||||
for (const match of text.matchAll(/@(sc_b?f_[A-Za-z0-9_]+)/g)) {
|
||||
const callee = this.mangledFunctions.get(match[1]!);
|
||||
// The names `@(sc_b?f_[A-Za-z0-9_]+)` matches, found with indexOf.
|
||||
for (let at = text.indexOf("@sc_"); at >= 0;) {
|
||||
let start = at + 4;
|
||||
if (text.charCodeAt(start) === 98 /* b */) start++;
|
||||
if (text.charCodeAt(start) !== 102 /* f */ || text.charCodeAt(start + 1) !== 95 /* _ */) {
|
||||
at = text.indexOf("@sc_", at + 1);
|
||||
continue;
|
||||
}
|
||||
let end = start + 2;
|
||||
while (end < text.length && isIdentifierByte(text.charCodeAt(end))) end++;
|
||||
if (end === start + 2) {
|
||||
at = text.indexOf("@sc_", at + 1);
|
||||
continue;
|
||||
}
|
||||
const callee = this.mangledFunctions.get(text.slice(at + 1, end));
|
||||
if (callee !== undefined) yield callee;
|
||||
at = text.indexOf("@sc_", end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3178,6 +3303,13 @@ export class LlEmitter {
|
||||
terminator = `br label %${target.label}`;
|
||||
} else if (this.currentCoro !== null) {
|
||||
terminator = `br label %${this.currentCoro.finalLabel}`;
|
||||
} else if (this.currentUnwindAbi && this.inLandingPad) {
|
||||
// The unwind that entered the landing pad goes on to the caller's.
|
||||
terminator = UNWIND_RESUME;
|
||||
} else if (this.currentUnwindAbi) {
|
||||
// The caller's invoke takes the exception at its landing pad.
|
||||
this.declare(UNWIND_RAISE_DECLARATION);
|
||||
terminator = UNWIND_RAISE;
|
||||
} else {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
terminator =
|
||||
@@ -3208,6 +3340,7 @@ export class LlEmitter {
|
||||
passThrough !== undefined &&
|
||||
!target &&
|
||||
this.currentCoro === null &&
|
||||
!this.currentUnwindAbi &&
|
||||
!this.currentInt32Result
|
||||
) {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
@@ -3394,6 +3527,46 @@ export class LlEmitter {
|
||||
B.startBlock(B.newLabel("exc.dead"));
|
||||
}
|
||||
|
||||
/** Emits `line`, a `call` of `callee`, and reports whether the call
|
||||
* needs no pending check after it: a call of an unwinding-ABI function
|
||||
* becomes an `invoke` whose landing pad runs this frame's unwind (the
|
||||
* same releases and handler as emitPendingCheck's unwind path), and the
|
||||
* normal path continues in a fresh block. Coroutine bodies keep plain
|
||||
* calls (unwindAbiFunctions excludes their callees). */
|
||||
emitDirectCall(line: string, callee: string): boolean {
|
||||
const B = this.B;
|
||||
if (!this.unwindAbi.has(callee) || this.currentCoro !== null || B.isTerminated()) {
|
||||
B.line(line);
|
||||
return false;
|
||||
}
|
||||
const ok = B.newLabel("inv.ok");
|
||||
const pad = B.newLabel("inv.lp");
|
||||
this.usesLandingPads = true;
|
||||
// A trailing comment goes after the invoke's labels.
|
||||
const comment = line.indexOf(" ; ");
|
||||
const text = comment < 0 ? line : line.slice(0, comment);
|
||||
B.terminate(
|
||||
`${text.replace(/^((?:%[\w.]+ = )?)call /, "$1invoke ")} to label %${ok} unwind label %${pad}`,
|
||||
);
|
||||
B.startBlock(pad);
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
if (this.currentUnwindAbi) B.line(`store { ptr, i32 } ${exception}, ptr ${UNWIND_SLOT}`);
|
||||
this.inLandingPad = true;
|
||||
this.emitUnwind();
|
||||
this.inLandingPad = false;
|
||||
B.startBlock(ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Whether emitUnwind runs for a landing pad, whose unwind goes on with
|
||||
* `resume` instead of raising again. */
|
||||
private inLandingPad = false;
|
||||
|
||||
/** Whether the function being emitted has a landing pad (its definition
|
||||
* needs the personality). */
|
||||
usesLandingPads = false;
|
||||
|
||||
/** A recursive function's entry check: below the running stack's guard,
|
||||
* throw Node's RangeError and unwind like any other pending exception. */
|
||||
emitStackCheck(): void {
|
||||
@@ -3569,15 +3742,14 @@ export class LlEmitter {
|
||||
set.add(fn.name);
|
||||
writers.set(localId, set);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind === "assign" || s.kind === "varDecl") note(s.localId);
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") note(e.localId);
|
||||
return true;
|
||||
},
|
||||
eachNodeOfKinds(fn, this.nodeIndex.scan(GLOBAL_WRITE_KINDS), (node) => {
|
||||
if (
|
||||
node.kind === "assign" ||
|
||||
node.kind === "varDecl" ||
|
||||
node.kind === "assignExpr" ||
|
||||
node.kind === "incDec"
|
||||
)
|
||||
note(node.localId);
|
||||
});
|
||||
}
|
||||
this.globalWriters = writers;
|
||||
@@ -4311,11 +4483,21 @@ export class LlEmitter {
|
||||
workers: this.mod.workers === true,
|
||||
owned: this.bytesOwned,
|
||||
scalarStrings: new Set(this.stringSlices.keys()),
|
||||
unitSafeFunctions: this.unitSafeFunctions(),
|
||||
},
|
||||
paired,
|
||||
);
|
||||
}
|
||||
|
||||
private unitSafe: ReadonlySet<string> | null | undefined;
|
||||
/** The functions a loop over a string's UTF-16 units may call, or null
|
||||
* where emitted reads do not use the runtime's scan receiver. */
|
||||
private unitSafeFunctions(): ReadonlySet<string> | null {
|
||||
if (this.unitSafe === undefined)
|
||||
this.unitSafe = stringScanInline(this) ? unitSafeFunctions(this.mod) : null;
|
||||
return this.unitSafe;
|
||||
}
|
||||
|
||||
/** The alias tag of a whole-slot load of a binding (module globals only). */
|
||||
slotAlias(id: string): string {
|
||||
const binding = this.binding(id);
|
||||
@@ -5158,7 +5340,22 @@ export class LlEmitter {
|
||||
if (fn.captures !== undefined) params.unshift("ptr %sc_env");
|
||||
const ret = this.llType(fn.returnType);
|
||||
const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`;
|
||||
B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// The owned entry of an unwinding-ABI body releases the borrowed
|
||||
// parameters on the unwind too, then resumes it to its caller.
|
||||
const unwinding = this.unwindAbi.has(fn.name) && kept.size === 0;
|
||||
if (unwinding) {
|
||||
const ok = B.newLabel("adapt.ok");
|
||||
const pad = B.newLabel("adapt.lp");
|
||||
B.terminate(
|
||||
`${ret === "void" ? "" : "%result = "}invoke ${call.slice(5)} to label %${ok} unwind label %${pad}`,
|
||||
);
|
||||
B.startBlock(pad);
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type);
|
||||
B.terminate(`resume { ptr, i32 } ${exception}`);
|
||||
B.startBlock(ok);
|
||||
} else B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// A borrowed result is reachable from the parameters: own it first. A
|
||||
// throwing body returns a null dummy, which needs no owner (and string
|
||||
// or array retains do not accept NULL).
|
||||
@@ -5184,7 +5381,9 @@ export class LlEmitter {
|
||||
B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
|
||||
const symbol =
|
||||
kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
|
||||
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
|
||||
if (unwinding) this.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
const personality = unwinding ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS}${personality} {\n${B.render()}\n}`;
|
||||
}
|
||||
canBorrowCallArgument(value: IrExpr): boolean {
|
||||
return canBorrowCallArgument(this, value);
|
||||
@@ -5357,13 +5556,21 @@ export class LlEmitter {
|
||||
private loopMetricsRead: boolean | undefined;
|
||||
readsLoopMetrics(): boolean {
|
||||
if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
|
||||
const visitor = {
|
||||
expr: (e: IrExpr): boolean =>
|
||||
!(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
|
||||
stmt: (): boolean => true,
|
||||
};
|
||||
this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
|
||||
return this.loopMetricsRead;
|
||||
let found = false;
|
||||
const scan = this.nodeIndex.scan(LIB_CALL_KINDS);
|
||||
for (const fn of this.mod.functions) {
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (
|
||||
node.kind === "libCall" &&
|
||||
(node.fn === "perf.loopStart" || node.fn === "perf.idleTime")
|
||||
)
|
||||
found = true;
|
||||
return !found;
|
||||
});
|
||||
if (found) break;
|
||||
}
|
||||
this.loopMetricsRead = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
private lastUseMovesAllowed(): boolean {
|
||||
@@ -5731,6 +5938,10 @@ export class LlEmitter {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.kind === "dynInvoke" && e.calleeNameValue === undefined) {
|
||||
emitDynamicExpr(this, e, true);
|
||||
return;
|
||||
}
|
||||
this.emitExpr(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -245,38 +245,46 @@ export function emitCallExpr(
|
||||
? INTEGER_DISPATCH_SUFFIX
|
||||
: "";
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`;
|
||||
// A call of an unwinding-ABI callee is an invoke (emitDirectCall):
|
||||
// its landing pad unwinds, so no pending check follows it.
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(`call void ${target}(${argList})`, e.callee);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (abi?.union) {
|
||||
B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
if (stackResult) return host.stackScalarUnion(t, e.type);
|
||||
return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type });
|
||||
}
|
||||
if (abi?.result) {
|
||||
B.line(`${t} = call i32 ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(`${t} = call i32 ${target}(${argList})`, e.callee);
|
||||
const out = host.int32Result(t, e.type);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return out;
|
||||
}
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
let out: LlValue;
|
||||
if (borrowed && host.borrowedReturns.has(e.callee)) {
|
||||
// A borrowed-return body hands back +0 (a null dummy when it threw):
|
||||
// test the exception first, then own the result before the
|
||||
// argument snapshots that keep it reachable are released.
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
out = host.own({ name: host.retainFrame(t, e.type), type: e.type });
|
||||
} else {
|
||||
out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
}
|
||||
if (borrowed) {
|
||||
host.toFrame(out);
|
||||
@@ -1378,10 +1386,11 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call
|
||||
)
|
||||
.join(", ");
|
||||
const t = host.B.tmp();
|
||||
host.B.line(
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} @${mangleBorrowedFunction(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}(${argList}) ; borrowed result`,
|
||||
e.callee,
|
||||
);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
emitAsciiChar,
|
||||
emitAsciiCharCode,
|
||||
emitUnitCharCode,
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
@@ -322,6 +323,15 @@ export function charAtUnitRead(
|
||||
return read;
|
||||
}
|
||||
|
||||
/** The UTF-16 units an enclosing loop version proved the index of `e`
|
||||
* below the count of (string-unit-versions.ts). */
|
||||
function provenUnitsRead(host: LlvmEmitterContext, e: IrExpr): string | null {
|
||||
const units = host.stringUnitReads.get(e);
|
||||
if (units !== undefined) return units;
|
||||
const origin = host.charAtUnitReads.get(e);
|
||||
return origin !== undefined ? (host.stringUnitReads.get(origin) ?? null) : null;
|
||||
}
|
||||
|
||||
/** An enclosing loop version proved the string of `e` ASCII and its index
|
||||
* below the length (bytes-loop-versions.ts). */
|
||||
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
@@ -364,9 +374,14 @@ export function emitStrIntrinsic(
|
||||
const method = e.method;
|
||||
switch (method) {
|
||||
case "length": {
|
||||
const t = B.tmp();
|
||||
const length = host.stringLengthReads.get(e);
|
||||
if (length !== undefined) {
|
||||
B.line(`${t} = sitofp i64 ${length} to double`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
// sc_str_utf16_len (string-reads.ts) answers proven-ASCII strings inline.
|
||||
host.declare(STRING_UTF16_LEN_DECL);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call double @sc_str_utf16_len(ptr ${r.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -376,6 +391,8 @@ export function emitStrIntrinsic(
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
if (index) {
|
||||
const wide = widenInteger(host, index);
|
||||
const units = provenUnitsRead(host, e);
|
||||
if (units !== null) return { name: emitUnitCharCode(host, units, wide), type: e.type };
|
||||
if (provenStringRead(host, e))
|
||||
return { name: emitAsciiCharCode(host, r.name, wide), type: e.type };
|
||||
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
|
||||
@@ -1461,6 +1478,7 @@ export function emitMapLikeIntrinsic(
|
||||
: emitStringMapGetScalar(host, r.name, stringKey!);
|
||||
found = scalar.found;
|
||||
if (stringKey !== null) rememberMapEntry(host, e, r.name, stringKey, scalar.entry);
|
||||
else rememberMapEntry(host, e, r.name, numberKey!, scalar.entry, "double");
|
||||
readHit = () => {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
@@ -1532,7 +1550,13 @@ export function emitMapLikeIntrinsic(
|
||||
const vAcc = elemAccess(receiverType.value);
|
||||
if (vAcc === "ref") host.toHeap(v);
|
||||
const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name);
|
||||
if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
if (
|
||||
(kAcc === "str" || kAcc === "f64") &&
|
||||
vAcc !== "ref" &&
|
||||
emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
) {
|
||||
// Written through the read's entry, or inserted.
|
||||
} else if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
const bits = B.tmp();
|
||||
B.line(
|
||||
vAcc === "f64"
|
||||
@@ -1540,12 +1564,7 @@ export function emitMapLikeIntrinsic(
|
||||
: `${bits} = zext i1 ${v.name} to i64`,
|
||||
);
|
||||
emitNumberMapSetScalar(host, r.name, k.name, bits, ordinary);
|
||||
} else if (
|
||||
kAcc !== "str" ||
|
||||
vAcc === "ref" ||
|
||||
!emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
)
|
||||
ordinary();
|
||||
} else ordinary();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "add": {
|
||||
|
||||
@@ -30,6 +30,9 @@ export interface WideInteger {
|
||||
name: string;
|
||||
below: number;
|
||||
bound: number;
|
||||
/** The double is the integer rounded once: the result of one operation on
|
||||
* operands that always equal their doubles. */
|
||||
single?: true;
|
||||
}
|
||||
|
||||
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { BYTES_ELEM_NUM, closureIdentityEqual } from "./common.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
@@ -57,6 +58,7 @@ export function emitDynamicExpr(
|
||||
| "caughtNarrow"
|
||||
| "caughtToDyn"
|
||||
>,
|
||||
discarded = false,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
@@ -87,7 +89,7 @@ export function emitDynamicExpr(
|
||||
// Bytes already box their shared mutable storage directly. A second
|
||||
// capsule would split identity between checked and native views.
|
||||
const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className);
|
||||
// A TextEncoder or TextDecoder record boxes as its own handle, which
|
||||
// A TextEncoder, TextDecoder or StringDecoder record boxes as its own handle, which
|
||||
// keeps the record's identity (toDynHelper).
|
||||
const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null;
|
||||
if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) {
|
||||
@@ -257,6 +259,18 @@ export function emitDynamicExpr(
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
// A statement-position push drops the new length, so its entry
|
||||
// need not box it.
|
||||
if (discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id") {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_invoke_discard_id(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
B.line(
|
||||
`call void @scr_dyn_invoke_discard_id(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${host.cstr(e.calleeName)})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: VOID };
|
||||
}
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
|
||||
@@ -13,13 +13,14 @@ import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import {
|
||||
alwaysExactInteger,
|
||||
exactCompareOperands,
|
||||
exactInteger,
|
||||
int32Number,
|
||||
integerNumber,
|
||||
narrowInteger,
|
||||
wideArithmetic,
|
||||
widenInteger,
|
||||
type LlInteger,
|
||||
} from "./integer-values.js";
|
||||
import {
|
||||
bitwiseIdentityOperand,
|
||||
@@ -45,7 +46,7 @@ import {
|
||||
slotWidth,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
|
||||
import { emitWrappingSum, integerOrNaNRange, markWrappingOperands } from "./wrapping-sums.js";
|
||||
import { emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
|
||||
|
||||
/** The signed integer predicates of the number compares. */
|
||||
@@ -58,6 +59,63 @@ const INTEGER_COMPARES: Record<string, string> = {
|
||||
"!==": "ne",
|
||||
};
|
||||
|
||||
const UNSIGNED_COMPARES: Record<string, string> = {
|
||||
"<": "ult",
|
||||
"<=": "ule",
|
||||
">": "ugt",
|
||||
">=": "uge",
|
||||
"===": "eq",
|
||||
"!==": "ne",
|
||||
};
|
||||
|
||||
/** A number compare of operands that are each an exact integer or an
|
||||
* integer-or-NaN value (integerOrNaNRange: integer typed-array elements,
|
||||
* `charCodeAt` results, sums of those), all within int32 or all within
|
||||
* uint32: their 32-bit views compared, and NaN comparing false (unequal for
|
||||
* `!==`). The 32-bit view is the value's ToUint32, which holds its bits
|
||||
* whenever it is not NaN. Null when an operand has no such view. */
|
||||
function emitIntegerOrNaNCompare(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"bin">,
|
||||
l: LlValue,
|
||||
li: LlInteger | null,
|
||||
r: LlValue,
|
||||
ri: LlInteger | null,
|
||||
): string | null {
|
||||
const lr = li?.range ?? integerOrNaNRange(host, e.left),
|
||||
rr = ri?.range ?? integerOrNaNRange(host, e.right);
|
||||
if (!lr || !rr) return null;
|
||||
const signed = withinInt32(lr) && withinInt32(rr);
|
||||
if (!signed && !(withinUint32(lr) && withinUint32(rr))) return null;
|
||||
const bits = (value: LlValue, integer: LlInteger | null): string | undefined =>
|
||||
integer ? narrowInteger(host, integer) : value.uint32;
|
||||
const left = bits(l, li),
|
||||
right = bits(r, ri);
|
||||
if (left === undefined || right === undefined) return null;
|
||||
const B = host.B;
|
||||
const compare = B.tmp();
|
||||
B.line(
|
||||
`${compare} = icmp ${(signed ? INTEGER_COMPARES : UNSIGNED_COMPARES)[e.op]} i32 ${left}, ${right}`,
|
||||
);
|
||||
let result = compare;
|
||||
for (const [value, integer] of [
|
||||
[l, li],
|
||||
[r, ri],
|
||||
] as const) {
|
||||
if (integer) continue;
|
||||
const ordered = B.tmp(),
|
||||
combined = B.tmp();
|
||||
B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`);
|
||||
if (e.op === "!==") {
|
||||
const unordered = B.tmp();
|
||||
B.line(`${unordered} = xor i1 ${ordered}, true`);
|
||||
B.line(`${combined} = or i1 ${result}, ${unordered}`);
|
||||
} else B.line(`${combined} = and i1 ${result}, ${ordered}`);
|
||||
result = combined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function emitLiteralExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">,
|
||||
@@ -363,14 +421,16 @@ export function emitOperatorExpr(
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
// An operand with only a wide view, such as `i + 1` of a speculated
|
||||
// counter: compare the integers when both always equal the doubles.
|
||||
const left = alwaysExactInteger(host, l, li),
|
||||
right = left === null ? null : alwaysExactInteger(host, r, ri);
|
||||
if (left !== null && right !== null) {
|
||||
B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`);
|
||||
// An operand with only a wide view, such as `i + 1` or `2 * i + 1`
|
||||
// of a speculated counter: compare the integers when that gives the
|
||||
// doubles' result.
|
||||
const operands = exactCompareOperands(host, l, li, r, ri);
|
||||
if (operands) {
|
||||
B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${operands[0]}, ${operands[1]}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
const nanCoded = emitIntegerOrNaNCompare(host, e, l, li, r, ri);
|
||||
if (nanCoded) return { name: nanCoded, type: e.type };
|
||||
B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
|
||||
}
|
||||
} else if (bit[e.op] !== undefined) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { streamRooted, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import {
|
||||
type IrRecordShape,
|
||||
type IrType,
|
||||
@@ -843,8 +843,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
host.declare(`declare ptr @scr_error_dyn_fields(ptr)`);
|
||||
B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`);
|
||||
} else if (
|
||||
meta.def.runtime &&
|
||||
RUNTIME_STREAM_CLASSES.has(t.className) &&
|
||||
((meta.def.runtime && RUNTIME_STREAM_CLASSES.has(t.className)) || streamRooted(meta)) &&
|
||||
!meta.def.instancePrototypeHelper &&
|
||||
!meta.def.prototypeDataHelper
|
||||
) {
|
||||
|
||||
@@ -111,13 +111,17 @@ export function wideArithmetic(
|
||||
rv = view(r, ri);
|
||||
if (!lv || !rv) return null;
|
||||
let below: number, bound: number;
|
||||
let single = false;
|
||||
if (op === "*") {
|
||||
if (lv.below !== Infinity || rv.below !== Infinity) return null;
|
||||
below = EXACT_DOUBLES;
|
||||
bound = productBound(lv.bound, rv.bound);
|
||||
// Scaling by a power of two never rounds a double that stays finite.
|
||||
below = powerOfTwo(li) || powerOfTwo(ri) ? Infinity : EXACT_DOUBLES;
|
||||
single = true;
|
||||
} else if (lv.below === Infinity && rv.below === Infinity) {
|
||||
below = EXACT_DOUBLES;
|
||||
bound = sumBound(lv.bound, rv.bound);
|
||||
single = true;
|
||||
} else if (lv.below === Infinity || rv.below === Infinity) {
|
||||
const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
|
||||
below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
|
||||
@@ -132,7 +136,50 @@ export function wideArithmetic(
|
||||
host.B.line(
|
||||
`${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
|
||||
);
|
||||
return { name, below, bound };
|
||||
return single ? { name, below, bound, single } : { name, below, bound };
|
||||
}
|
||||
|
||||
/** Whether an exact integer is the constant 2^k or -2^k. */
|
||||
function powerOfTwo(integer: LlInteger | null): boolean {
|
||||
if (integer === null || integer.range.min !== integer.range.max) return false;
|
||||
const value = Math.abs(integer.range.min);
|
||||
// Math.log2 can round to an integer for a value that is not a power of
|
||||
// two (2^52 - 1), so the candidate power is compared with the value.
|
||||
return value >= 1 && value <= 2 ** 52 && 2 ** Math.round(Math.log2(value)) === value;
|
||||
}
|
||||
|
||||
/** The i64 operands of a number compare that gives the doubles' result:
|
||||
* both equal their doubles wherever they can be, or one is an exact integer
|
||||
* of magnitude at most 2^53 - 1 and the other's double is its integer
|
||||
* rounded once. Rounding is monotonic, and b - 1, b and b + 1 are doubles
|
||||
* for a safe integer b, so an integer rounded once compares with b as the
|
||||
* integer does. Null for any other pair. */
|
||||
export function exactCompareOperands(
|
||||
host: LlvmEmitterContext,
|
||||
l: LlValue,
|
||||
li: LlInteger | null,
|
||||
r: LlValue,
|
||||
ri: LlInteger | null,
|
||||
): [string, string] | null {
|
||||
const describe = (value: LlValue, integer: LlInteger | null) => {
|
||||
if (integer) return { exact: true, single: true, bound: magnitude(integer.range) };
|
||||
const wide = value.wideInteger;
|
||||
if (!wide) return null;
|
||||
return { exact: wide.bound <= wide.below, single: wide.single === true, bound: wide.bound };
|
||||
};
|
||||
const a = describe(l, li),
|
||||
b = describe(r, ri);
|
||||
if (!a || !b) return null;
|
||||
const safe = (d: { exact: boolean; bound: number }): boolean =>
|
||||
d.exact && d.bound <= Number.MAX_SAFE_INTEGER;
|
||||
const ok =
|
||||
(a.exact && b.exact) ||
|
||||
(safe(a) && (b.exact || b.single)) ||
|
||||
(safe(b) && (a.exact || a.single));
|
||||
if (!ok) return null;
|
||||
const operand = (value: LlValue, integer: LlInteger | null): string =>
|
||||
integer ? widenInteger(host, integer) : value.wideInteger!.name;
|
||||
return [operand(l, li), operand(r, ri)];
|
||||
}
|
||||
|
||||
/** An i64 index for a consumer that compares it, unsigned, with a length or
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const LOOP_KINDS: readonly IrNodeKind[] = ["for", "forOf", "while", "doWhile"];
|
||||
|
||||
/** Keep a computed ToUint32 view beside an ordinary number when integer
|
||||
* consumers in loops can reuse it. The number remains authoritative for floating
|
||||
@@ -14,9 +17,16 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
* compare converts the stored number back, and LLVM moves that number's
|
||||
* conversion into the join of a NaN-coded typed-array read (`phi [sitofp x,
|
||||
* 0.0]`), where nothing folds it back to the integer. */
|
||||
export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
export function findIntegerViews(fn: IrFunction, nodes?: IrNodeIndex): Set<string> {
|
||||
const wanted = new Set<string>();
|
||||
if (fn.async || fn.generator) return wanted;
|
||||
// Every view is wanted inside a loop: a body without one wants none.
|
||||
let loops = false;
|
||||
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
|
||||
if (statement && LOOP_KINDS.includes(node.kind)) loops = true;
|
||||
return !loops;
|
||||
});
|
||||
if (!loops) return wanted;
|
||||
const initialized = new Set(fn.params.map((p) => p.localId));
|
||||
const excluded = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const CLOSURE_KINDS: readonly IrNodeKind[] = ["closure"];
|
||||
|
||||
/** The local a node rebinds. Writes through the bound reference, such as a
|
||||
* field store or an array push, leave the binding unchanged. Catch bindings
|
||||
@@ -37,7 +40,11 @@ export class LazyCaptures {
|
||||
private readonly pending = new Set<IrFunction>();
|
||||
private sites: Map<string, { owner: string; captures: string[] }[]> | null = null;
|
||||
|
||||
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking the bodies it holds. */
|
||||
private readonly nodes?: IrNodeIndex,
|
||||
) {}
|
||||
|
||||
/** The captures of closure body `fnName` that hold one value for the
|
||||
* closure's whole life: at every creation site the captured binding is
|
||||
@@ -103,16 +110,15 @@ export class LazyCaptures {
|
||||
private creationSites(): Map<string, { owner: string; captures: string[] }[]> {
|
||||
if (this.sites !== null) return this.sites;
|
||||
const sites = new Map<string, { owner: string; captures: string[] }[]>();
|
||||
const scan = this.nodes?.scan(CLOSURE_KINDS);
|
||||
for (const fn of this.functions.values()) {
|
||||
const visit = (node: IrStmt | IrExpr): boolean => {
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (node.kind === "closure") {
|
||||
let list = sites.get(node.fnName);
|
||||
if (!list) sites.set(node.fnName, (list = []));
|
||||
list.push({ owner: fn.name, captures: node.captures });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
everyStmtList(fn.body, { stmt: visit, expr: visit });
|
||||
});
|
||||
}
|
||||
this.sites = sites;
|
||||
return sites;
|
||||
|
||||
@@ -726,6 +726,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
|
||||
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
}
|
||||
// A setter's write: the time value into the Date object (ScrDate.ms).
|
||||
if (e.fn === "date.store") {
|
||||
const date = emitBorrowedInput(host, e.args[0]!).name;
|
||||
const ms = host.emitExpr(e.args[1]!).name;
|
||||
const field = B.tmp();
|
||||
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
|
||||
B.line(`store double ${ms}, ptr ${field}`);
|
||||
return { name: ms, type: e.type };
|
||||
}
|
||||
// queueMicrotask's checked-dynamic form carries the AsyncLocalStorage
|
||||
// context like the static one (lib-process.ts).
|
||||
const sym =
|
||||
|
||||
@@ -119,6 +119,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
B.line(`call void @scr_child_on_error(ptr ${child.name}, ptr ${cb.name}, ptr @${adapter})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "child.hasChannel") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_ipc_present(ptr)`);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call i1 @scr_child_ipc_present(ptr ${child.name})`);
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
if (e.fn === "child.connected") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_ipc_connected(ptr)`);
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap<string, number | null> = new Ma
|
||||
|
||||
export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
|
||||
"assert.throwsValidate": "scr_assert_throws_validate",
|
||||
"insp.jsonDyn": "scr_util_format_j",
|
||||
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
|
||||
"intl.segmenterNew": "scr_intl_segmenter_new",
|
||||
@@ -82,6 +83,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"util.isDeepStrictEqual": "scr_util_is_deep_strict_equal",
|
||||
"util.styleText": "scr_util_style_text",
|
||||
"util.inherits": "scr_util_inherits",
|
||||
"util.inheritsPrototype": "scr_util_inherits_prototype",
|
||||
"util.deprecate": "scr_util_deprecate",
|
||||
"util.callbackify": "scr_util_callbackify",
|
||||
"util.promisifyDyn": "scr_util_promisify",
|
||||
@@ -334,6 +336,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getTime": "scr_date_get_time",
|
||||
"date.valueOf": "scr_date_get_time",
|
||||
"date.fromTime": "scr_date_get_time",
|
||||
"date.setFields": "scr_date_set_fields",
|
||||
"date.timeClip": "scr_date_new_ms",
|
||||
"date.parseGetTime": "scr_date_parse",
|
||||
"date.utc": "scr_date_utc",
|
||||
"date.newLocal": "scr_date_new_local",
|
||||
@@ -399,6 +403,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.eqBool": "scr_assert_eq_bool",
|
||||
"assert.eqSym": "scr_assert_eq_sym",
|
||||
"assert.eqDyn": "scr_assert_eq_dyn",
|
||||
"assert.messageValue": "scr_assert_message_value",
|
||||
"assert.methodValue": "scr_assert_method_value",
|
||||
"assert.messageClear": "scr_assert_message_clear",
|
||||
"assert.looseResult": "scr_assert_loose_result",
|
||||
"assert.deepResult": "scr_assert_deep_result",
|
||||
"assert.looseDeepResult": "scr_assert_loose_deep_result",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { F64, isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { OptionalArrayReads } from "./local-array-reads.js";
|
||||
@@ -57,6 +58,8 @@ export function dynHoleRead(value: IrExpr): (IrExpr & { kind: "arrayGet" }) | nu
|
||||
return same(cond.left.arr, then.arr) && same(cond.left.index, then.index) ? then : null;
|
||||
}
|
||||
|
||||
const LOOP_KINDS: readonly IrNodeKind[] = ["forOf", "for", "while", "doWhile"];
|
||||
|
||||
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
|
||||
* Initialization and unrelated work elsewhere in the function may mutate.
|
||||
* The array remains owned by its unchanged local or global binding; the
|
||||
@@ -72,6 +75,8 @@ export function findLoopArrayBorrows(
|
||||
optionalReads?: OptionalArrayReads,
|
||||
writes?: ReferenceWrites | null,
|
||||
functions?: ReadonlyMap<string, IrFunction>,
|
||||
/** The module's node index, read instead of walking for the loops. */
|
||||
nodes?: IrNodeIndex,
|
||||
): ReadonlySet<IrStmt> {
|
||||
const result = new Set<IrStmt>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -88,9 +93,10 @@ export function findLoopArrayBorrows(
|
||||
(!local.boxed && !local.tdz && (bindings?.has(local.id) || parameters.has(local.id)))
|
||||
);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (loop) => {
|
||||
eachNodeOfKinds(fn, nodes?.scan(LOOP_KINDS), (node, statement) => {
|
||||
if (!statement) return true;
|
||||
const loop = node as IrStmt;
|
||||
{
|
||||
if (loop.kind === "forOf") {
|
||||
const local = locals.get(loop.localId);
|
||||
if (
|
||||
@@ -187,7 +193,7 @@ export function findLoopArrayBorrows(
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -137,12 +137,20 @@ test("a read followed by a write of the same map pairs, and the write reuses the
|
||||
}
|
||||
});
|
||||
|
||||
test("number keys keep the write-after-read lookup instead of pairing", () => {
|
||||
// findReadNumberMapWrites stores such writes into the found entry through
|
||||
// a read-only probe that LLVM merges with the read's.
|
||||
const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(F64))], F64, F64);
|
||||
expect(pairs(mod)).toBe(0);
|
||||
expect(body(mod)).not.toContain("mapfuse.hit");
|
||||
test("number keys pair, and the write compares the key's bits with the read's", () => {
|
||||
for (const value of [F64, BOOL]) {
|
||||
const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(value))], F64, value);
|
||||
expect(pairs(mod)).toBe(1);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("mapfuse.hit");
|
||||
expect(ir).toContain("alloca double ; fused map read: key");
|
||||
expect(ir).toMatch(/icmp eq i64 %t\d+, %t\d+/);
|
||||
// The miss still inserts through the runtime.
|
||||
expect(ir).toMatch(/call void @scr_map_set_f64_(?:f64|bool)\(/);
|
||||
expect(ir).toContain("@sc_map_entry_f64(");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("the single-statement form pairs an unconditional nested read", () => {
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* key match by identity implies the SameValueZero match the set would find.
|
||||
*
|
||||
* Only scalar values (number, boolean) fuse: overwriting them releases
|
||||
* nothing, exactly like the runtime's own overwrite. Only string-keyed
|
||||
* typed maps fuse here: a number-key write after a read of the same key
|
||||
* already probes through the read-only entry lookup that LLVM merges with
|
||||
* the read's (findReadNumberMapWrites, map-read-lifetimes.ts).
|
||||
* nothing, exactly like the runtime's own overwrite. String-keyed and
|
||||
* number-keyed typed maps fuse. A number key matches when its bits are the
|
||||
* read's: such keys are SameValueZero-equal, and a key that is equal with
|
||||
* other bits (-0 for 0) takes the ordinary set.
|
||||
*/
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
@@ -30,7 +30,7 @@ type MapOp = Extract<IrExpr, { kind: "mapIntrinsic" }>;
|
||||
/** One fused read/write pair. The slots exist once the read emits through
|
||||
* an entry path; a write whose read took another path stays ordinary. */
|
||||
export interface MapEntryPair {
|
||||
slots: { map: string; key: string; entry: string } | null;
|
||||
slots: { map: string; key: string; entry: string; keyType: "ptr" | "double" } | null;
|
||||
}
|
||||
|
||||
export interface MapEntryFusion {
|
||||
@@ -38,13 +38,14 @@ export interface MapEntryFusion {
|
||||
writes: Map<IrExpr, MapEntryPair>;
|
||||
}
|
||||
|
||||
/** A typed string-keyed Map read/write with a scalar value. */
|
||||
/** A typed string- or number-keyed Map read/write with a scalar value. */
|
||||
function fusableOp(e: IrExpr, method: "get" | "set"): e is MapOp {
|
||||
if (e.kind !== "mapIntrinsic" || e.method !== method) return false;
|
||||
const receiver = e.receiver.type;
|
||||
if (receiver.kind !== "map") return false;
|
||||
if (receiver.kind !== "map" || receiver.key.kind === "union") return false;
|
||||
if (receiver.value.kind !== "f64" && receiver.value.kind !== "bool") return false;
|
||||
if (mapKeyAccess(receiver.key) !== "str") return false;
|
||||
const access = mapKeyAccess(receiver.key);
|
||||
if (access !== "str" && access !== "f64") return false;
|
||||
return method === "get" ? borrowsMapReadInputs(e) : borrowsMapMutationReceiver(e);
|
||||
}
|
||||
|
||||
@@ -219,23 +220,25 @@ export function findMapEntryFusions(fn: IrFunction): MapEntryFusion {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** After a fused read resolved `entry` for the string `key` in `map`: park
|
||||
* all three for the paired write. */
|
||||
/** After a fused read resolved `entry` for `key` in `map`: park all three
|
||||
* for the paired write. `keyType` is the key's LLVM type: a string's ptr or
|
||||
* a number's double. */
|
||||
export function rememberMapEntry(
|
||||
host: LlvmEmitterContext,
|
||||
read: IrExpr,
|
||||
map: string,
|
||||
key: string,
|
||||
entry: string,
|
||||
keyType: "ptr" | "double" = "ptr",
|
||||
): void {
|
||||
const pair = host.mapEntryFusion?.reads.get(read);
|
||||
if (!pair) return;
|
||||
const B = host.B;
|
||||
if (pair.slots === null) {
|
||||
const slots = { map: B.slot(), key: B.slot(), entry: B.slot() };
|
||||
const slots = { map: B.slot(), key: B.slot(), entry: B.slot(), keyType };
|
||||
B.entryAllocas.push(
|
||||
`${slots.map} = alloca ptr ; fused map read: map`,
|
||||
`${slots.key} = alloca ptr ; fused map read: key`,
|
||||
`${slots.key} = alloca ${keyType} ; fused map read: key`,
|
||||
`${slots.entry} = alloca ptr ; fused map read: entry`,
|
||||
`store ptr null, ptr ${slots.entry}`,
|
||||
);
|
||||
@@ -243,7 +246,7 @@ export function rememberMapEntry(
|
||||
}
|
||||
const slots = pair.slots;
|
||||
B.line(`store ptr ${map}, ptr ${slots.map}`);
|
||||
B.line(`store ptr ${key}, ptr ${slots.key}`);
|
||||
B.line(`store ${slots.keyType} ${key}, ptr ${slots.key}`);
|
||||
B.line(`store ptr ${entry}, ptr ${slots.entry}`);
|
||||
}
|
||||
|
||||
@@ -270,13 +273,20 @@ export function emitFusedMapSet(
|
||||
both = B.tmp(),
|
||||
reuse = B.tmp();
|
||||
B.line(`${parkedMap} = load ptr, ptr ${slots.map}`);
|
||||
B.line(`${parkedKey} = load ptr, ptr ${slots.key}`);
|
||||
B.line(`${parkedKey} = load ${slots.keyType}, ptr ${slots.key}`);
|
||||
B.line(`${entry} = load ptr, ptr ${slots.entry}`);
|
||||
// The entry is valid only between its read and this write.
|
||||
B.line(`store ptr null, ptr ${slots.entry}`);
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
B.line(`${sameMap} = icmp eq ptr ${parkedMap}, ${map}`);
|
||||
B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`);
|
||||
if (slots.keyType === "ptr") B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`);
|
||||
else {
|
||||
const parkedBits = B.tmp(),
|
||||
keyBits = B.tmp();
|
||||
B.line(`${parkedBits} = bitcast double ${parkedKey} to i64`);
|
||||
B.line(`${keyBits} = bitcast double ${key} to i64`);
|
||||
B.line(`${sameKey} = icmp eq i64 ${parkedBits}, ${keyBits}`);
|
||||
}
|
||||
B.line(`${both} = and i1 ${sameMap}, ${sameKey}`);
|
||||
B.line(`${reuse} = select i1 ${found}, i1 ${both}, i1 false`);
|
||||
const hit = B.newLabel("mapfuse.hit"),
|
||||
|
||||
@@ -314,6 +314,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
// Booleans are stored as 0/1, already the widened union payload.
|
||||
const scalar = emitNumberMapGetScalar(host, receiver.name, number);
|
||||
found = scalar.found;
|
||||
rememberMapEntry(host, read.expr, receiver.name, number, scalar.entry, "double");
|
||||
B.line(`store i64 ${scalar.bits}, ptr ${payload}`);
|
||||
}
|
||||
} else if (string !== null && !isRefCounted(read.value)) {
|
||||
|
||||
@@ -110,10 +110,9 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe(
|
||||
"@sc_rc_release_union",
|
||||
);
|
||||
// Array retains inline; array releases stay calls.
|
||||
const array = { kind: "array", elem: { kind: "string" } } as never;
|
||||
expect(retainSym(host, array)).toBe("@sc_rc_retain_arr");
|
||||
expect(releaseSym(host, array)).toBe("@scr_arr_release");
|
||||
expect(releaseSym(host, array)).toBe("@sc_rc_release_arr");
|
||||
expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn");
|
||||
expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn");
|
||||
// Unmirrored families and caught snapshots keep their runtime entry points.
|
||||
@@ -125,6 +124,7 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(declarations).toContain("declare void @scr_str_release(ptr)");
|
||||
expect(declarations).toContain("declare void @scr_cyc_on_release(ptr)");
|
||||
expect(inlineRcDecls(host).sort()).toEqual([
|
||||
"declare void @scr_arr_release(ptr)",
|
||||
"declare void @scr_cyc_on_release(ptr)",
|
||||
"declare void @scr_dyn_release(ptr)",
|
||||
"declare void @scr_dyn_release_survivor(ptr)",
|
||||
@@ -280,6 +280,22 @@ describe("inline release budgets", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("each family's budget applies to its own sites in a function that has both", () => {
|
||||
const strings = body("mixed", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str");
|
||||
// One function: string sites over their budget, dyn sites within theirs.
|
||||
const mixed = strings.replace(
|
||||
" ret void",
|
||||
` call void @sc_rc_release_dyn(ptr %v)\n call void @sc_rc_release_other(ptr %v)\n ret void`,
|
||||
);
|
||||
const other = body("other", 2, "str");
|
||||
const limited = limitInlineReleases([mixed, other], "release").join("\n");
|
||||
const out = limited.slice(0, limited.indexOf("@other("));
|
||||
expect(out).not.toContain("@sc_rc_release_str(");
|
||||
expect(out).toContain("call void @sc_rc_release_dyn(ptr %v)");
|
||||
expect(out).toContain("call void @sc_rc_release_other(ptr %v)");
|
||||
expect(limited).toContain(other);
|
||||
});
|
||||
|
||||
test("a function over the string budget calls the runtime string release at every site", () => {
|
||||
const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str");
|
||||
const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str");
|
||||
|
||||
@@ -550,17 +550,16 @@ interface InlineRcFamily {
|
||||
* scr_dyn_release_survivor for kinds whose candidacy is per value,
|
||||
* including arrays and objects of intact documents (ScrDynRegion).
|
||||
*
|
||||
* Releases stay plain calls for:
|
||||
* - arr: array releases are the most numerous release sites and the
|
||||
* least hot (at most 1% self time across the benchmark suite), and
|
||||
* inlining them roughly doubled the code growth for no measured speedup.
|
||||
* Array retains are inlined. */
|
||||
* The array release inlines the decrement of an array that stays alive
|
||||
* and is not headered (`a->cyc`): those are most array releases in
|
||||
* type-checker workloads, where scr_arr_release's own prologue and checks
|
||||
* took about half of its time. */
|
||||
export const INLINE_RC_FAMILIES: Readonly<Record<string, InlineRcFamily>> = {
|
||||
str: { cycle: "none", retainNull: false, release: "scr_str_release" },
|
||||
bytes: { cycle: "none", retainNull: false, release: "scr_bytes_release" },
|
||||
regex: { cycle: "none", retainNull: false, release: "scr_regex_release" },
|
||||
bigint: { cycle: "none", retainNull: true, release: "scr_bigint_release" },
|
||||
arr: { cycle: "arr", retainNull: false, release: null },
|
||||
arr: { cycle: "arr", retainNull: false, release: "scr_arr_release" },
|
||||
map: { cycle: "map", retainNull: false, release: "scr_map_release" },
|
||||
union: { cycle: "always", retainNull: false, release: "scr_union_release" },
|
||||
closure: { cycle: "always", retainNull: false, release: "scr_closure_release" },
|
||||
@@ -692,34 +691,32 @@ export function limitInlineReleases(
|
||||
parts: readonly string[],
|
||||
mode: InlineRcMode | undefined,
|
||||
): string[] {
|
||||
const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION);
|
||||
return mode === "release"
|
||||
? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION)
|
||||
: dyn;
|
||||
}
|
||||
|
||||
function limitInlineReleaseFamily(
|
||||
parts: readonly string[],
|
||||
family: "dyn" | "str",
|
||||
max: number,
|
||||
): string[] {
|
||||
const call = `call void @${inlineRcName(family, "release")}(`;
|
||||
const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`;
|
||||
const families: readonly ("dyn" | "str")[] = mode === "release" ? ["dyn", "str"] : ["dyn"];
|
||||
const limits = families.map((family) =>
|
||||
family === "dyn" ? INLINE_DYN_RELEASES_PER_FUNCTION : INLINE_STR_RELEASES_PER_FUNCTION,
|
||||
);
|
||||
const calls = families.map((family) => `${inlineRcName(family, "release")}(`);
|
||||
const runtimes = families.map((family) => `call void @${INLINE_RC_FAMILIES[family]!.release}(`);
|
||||
const head = "call void @";
|
||||
const prefix = `${head}${inlineRcName("", "release")}`;
|
||||
// The function each release site belongs to: the last `define` line
|
||||
// before it, counted over the joined text.
|
||||
// before it, counted over the joined text. One walk over each part finds
|
||||
// the sites of every family.
|
||||
const sites = (
|
||||
part: string,
|
||||
each: (offset: number, fn: number) => void,
|
||||
each: (offset: number, family: number, fn: number) => void,
|
||||
first: number,
|
||||
): number => {
|
||||
let fn = first;
|
||||
let define = part.startsWith("define ") ? 0 : part.indexOf("\ndefine ");
|
||||
for (let at = part.indexOf(call); at >= 0; at = part.indexOf(call, at + call.length)) {
|
||||
for (let at = part.indexOf(prefix); at >= 0; at = part.indexOf(prefix, at + prefix.length)) {
|
||||
const family = calls.findIndex((call) => part.startsWith(call, at + head.length));
|
||||
if (family < 0) continue;
|
||||
while (define >= 0 && define < at) {
|
||||
fn++;
|
||||
define = part.indexOf("\ndefine ", define + 1);
|
||||
}
|
||||
each(at, fn);
|
||||
each(at, family, fn);
|
||||
}
|
||||
while (define >= 0) {
|
||||
fn++;
|
||||
@@ -727,29 +724,47 @@ function limitInlineReleaseFamily(
|
||||
}
|
||||
return fn;
|
||||
};
|
||||
const counts = new Map<number, number>();
|
||||
const counts = families.map(() => new Map<number, number>());
|
||||
// The functions each part's sites can belong to.
|
||||
const spans: number[] = [];
|
||||
let fn = 0;
|
||||
for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn);
|
||||
const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f));
|
||||
if (over.size === 0) return [...parts];
|
||||
fn = 0;
|
||||
return parts.map((part) => {
|
||||
const cuts: number[] = [];
|
||||
for (const part of parts) {
|
||||
spans.push(fn);
|
||||
fn = sites(
|
||||
part,
|
||||
(at, f) => {
|
||||
if (over.has(f)) cuts.push(at);
|
||||
},
|
||||
(_, family, f) => counts[family]!.set(f, (counts[family]!.get(f) ?? 0) + 1),
|
||||
fn,
|
||||
);
|
||||
spans.push(fn);
|
||||
}
|
||||
const over = counts.map(
|
||||
(count, family) => new Set([...count].filter(([, n]) => n > limits[family]!).map(([f]) => f)),
|
||||
);
|
||||
if (over.every((set) => set.size === 0)) return [...parts];
|
||||
const anyOver = new Set(over.flatMap((set) => [...set]));
|
||||
const touches = (from: number, to: number): boolean => {
|
||||
for (let f = from; f <= to; f++) if (anyOver.has(f)) return true;
|
||||
return false;
|
||||
};
|
||||
return parts.map((part, index) => {
|
||||
const from = spans[2 * index]!;
|
||||
if (!touches(from, spans[2 * index + 1]!)) return part;
|
||||
const cuts: { at: number; family: number }[] = [];
|
||||
sites(
|
||||
part,
|
||||
(at, family, f) => {
|
||||
if (over[family]!.has(f)) cuts.push({ at, family });
|
||||
},
|
||||
from,
|
||||
);
|
||||
if (cuts.length === 0) return part;
|
||||
let out = "";
|
||||
let from = 0;
|
||||
for (const at of cuts) {
|
||||
out += part.slice(from, at) + runtime;
|
||||
from = at + call.length;
|
||||
let next = 0;
|
||||
for (const cut of cuts) {
|
||||
out += part.slice(next, cut.at) + runtimes[cut.family]!;
|
||||
next = cut.at + head.length + calls[cut.family]!.length;
|
||||
}
|
||||
return out + part.slice(from);
|
||||
return out + part.slice(next);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,19 @@ export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: st
|
||||
return code;
|
||||
}
|
||||
|
||||
/** Unit `index` (an i64, below their count) of a string's UTF-16 `units`
|
||||
* (ScrStrScan): one 16-bit load, as a double. */
|
||||
export function emitUnitCharCode(host: LlvmEmitterContext, units: string, index: string): string {
|
||||
const B = host.B;
|
||||
const pointer = B.tmp(),
|
||||
unit = B.tmp(),
|
||||
code = B.tmp();
|
||||
B.line(`${pointer} = getelementptr inbounds i16, ptr ${units}, i64 ${index}`);
|
||||
B.line(`${unit} = load i16, ptr ${pointer}`);
|
||||
B.line(`${code} = uitofp i16 ${unit} to double`);
|
||||
return code;
|
||||
}
|
||||
|
||||
/** The one-character string of unit `index` (an i64) of a proven-ASCII
|
||||
* string `s` whose length covers it: the immortal `@sc_ascii_chars` entry
|
||||
* for its byte, as the inline charAt helpers answer. */
|
||||
@@ -114,6 +127,15 @@ function loadLength(sz: string): string[] {
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether emitted reads use the runtime's scan receiver: from ordinary
|
||||
* global storage, or, in worker executables, from the thread's static TLS
|
||||
* block (executableTls). */
|
||||
export function stringScanInline(host: LlvmEmitterContext): boolean {
|
||||
return (
|
||||
host.executableTls || (host.mod.workers !== true && host.mod.lib?.threadInstances !== true)
|
||||
);
|
||||
}
|
||||
|
||||
/** The runtime's ScrStrScan record and the declaration naming it. */
|
||||
export function stringScanType(sz: string): string {
|
||||
return `{ ptr, ptr, ${sz} }`;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import type { IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { unitSafeFunctions, unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
async function lower(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-versions-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const loopOf = (mod: IrModule, name: string): IrStmt & { kind: "for" } =>
|
||||
mod.functions.find((f) => f.name === name)!.body.find((s) => s.kind === "for") as IrStmt & {
|
||||
kind: "for";
|
||||
};
|
||||
|
||||
test("loops over a string's units call only functions that read no string", async () => {
|
||||
const mod = await lower(`
|
||||
function lower(c: number): boolean {
|
||||
return c >= 97 && c <= 122;
|
||||
}
|
||||
function word(c: number): boolean {
|
||||
return lower(c) || (c >= 48 && c <= 57) || Math.abs(c - 200) < 50;
|
||||
}
|
||||
function codeOf(t: string, i: number): number {
|
||||
return t.charCodeAt(i);
|
||||
}
|
||||
function viaCodeOf(t: string): number {
|
||||
return codeOf(t, 0);
|
||||
}
|
||||
function words(s: string): number {
|
||||
let n = 0;
|
||||
const starts = [0];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (word(s.charCodeAt(i))) n++;
|
||||
else starts.push(i);
|
||||
}
|
||||
return n + starts.length;
|
||||
}
|
||||
function pairs(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + codeOf(t, i);
|
||||
return n;
|
||||
}
|
||||
function both(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + t.charCodeAt(i);
|
||||
return n;
|
||||
}
|
||||
function concatenates(s: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) out += "-";
|
||||
return out;
|
||||
}
|
||||
console.log(words("ab c"), pairs("ab", "cd"), both("ab", "cd"), concatenates("a b"), viaCodeOf("x"));
|
||||
`);
|
||||
const safe = unitSafeFunctions(mod);
|
||||
expect(safe.has("lower")).toBe(true);
|
||||
expect(safe.has("word")).toBe(true);
|
||||
// A string read, directly or through a call, makes a function unsafe.
|
||||
expect(safe.has("codeOf")).toBe(false);
|
||||
expect(safe.has("viaCodeOf")).toBe(false);
|
||||
|
||||
const param = (fn: string, index = 0): string =>
|
||||
mod.functions.find((f) => f.name === fn)!.params[index]!.localId;
|
||||
// A unit-safe loop answers its receiver's length reads: here the
|
||||
// condition's `s.length`.
|
||||
const lengths = unitSafeLoop(loopOf(mod, "words"), param("words"), safe);
|
||||
expect(lengths?.size).toBe(1);
|
||||
expect([...lengths!].every((e) => e.kind === "strIntrinsic" && e.method === "length")).toBe(true);
|
||||
expect(unitSafeLoop(loopOf(mod, "pairs"), param("pairs"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "both"), param("both"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "concatenates"), param("concatenates"), safe)).toBeNull();
|
||||
});
|
||||
|
||||
test("loops that sort with a comparator or copy an untyped source get no units copy", async () => {
|
||||
// Either can run program code that indexes other strings, which evicts
|
||||
// the receiver's units while the loop reads them.
|
||||
const mod = await lower(`
|
||||
function sorts(s: string, values: Float64Array, compare: (a: number, b: number) => number): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
n += s.charCodeAt(i);
|
||||
if (i === 3) values.sort(compare);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function copies(s: string, values: Float64Array, source: any): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
n += s.charCodeAt(i);
|
||||
if (i === 3) values.set(source);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function sortsNumbers(s: string, values: Float64Array): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
n += s.charCodeAt(i);
|
||||
if (i === 3) values.sort();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
const values = new Float64Array([2, 1]);
|
||||
console.log(sorts("abcdef", values, (a, b) => a - b), copies("abcdef", values, [4, 3]), sortsNumbers("abcdef", values));
|
||||
`);
|
||||
const safe = unitSafeFunctions(mod);
|
||||
const param = (fn: string): string =>
|
||||
mod.functions.find((f) => f.name === fn)!.params[0]!.localId;
|
||||
expect(unitSafeLoop(loopOf(mod, "sorts"), param("sorts"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "copies"), param("copies"), safe)).toBeNull();
|
||||
// A numeric sort calls no program code.
|
||||
expect(unitSafeLoop(loopOf(mod, "sortsNumbers"), param("sortsNumbers"), safe)).not.toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { IrBytesIntrinsicMethod, IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
|
||||
/* Loop versions over the UTF-16 units of a string that is not ASCII.
|
||||
*
|
||||
* The runtime publishes the receiver of an indexed scan with its UTF-16
|
||||
* units (ScrStrScan, string-reads.ts), and drops them when the string is
|
||||
* released or appended to in place, or when its index entry is evicted:
|
||||
* the runtime indexes another non-ASCII string. A loop version's string
|
||||
* receiver is a binding the loop does not write, which keeps it alive and
|
||||
* unappended for the whole loop. Its fast copy over units therefore only
|
||||
* needs a body that indexes no other string: the operations below, and
|
||||
* calls of functions made of the same operations without string reads
|
||||
* (`unitSafeFunctions`). */
|
||||
|
||||
const ARRAY_METHODS = new Set(["push", "pop", "length"]);
|
||||
|
||||
/* The typed-array and Buffer methods a units copy may run: those that
|
||||
* call no program code and read no checked-dynamic value or string. A
|
||||
* comparator sort (sortCompare) calls back into the program, and a copy
|
||||
* from a checked-dynamic source (setFromDyn) reads its elements through
|
||||
* getters; either can index another string, which evicts the receiver's
|
||||
* units while the loop reads them. */
|
||||
const UNIT_SAFE_BYTES_METHODS: ReadonlySet<IrBytesIntrinsicMethod> = new Set([
|
||||
"buffer",
|
||||
"length",
|
||||
"byteLength",
|
||||
"byteOffset",
|
||||
"get",
|
||||
"slice",
|
||||
"subarray",
|
||||
"copyWithin",
|
||||
"sortNumeric",
|
||||
"toReversed",
|
||||
"with",
|
||||
"toArray",
|
||||
"setFrom",
|
||||
"readNum",
|
||||
"writeNum",
|
||||
"readNumVar",
|
||||
"writeNumVar",
|
||||
"equals",
|
||||
"compareBuf",
|
||||
"indexOfNum",
|
||||
"lastIndexOfNum",
|
||||
"includesNum",
|
||||
"indexOfElem",
|
||||
"lastIndexOfElem",
|
||||
"includesElem",
|
||||
"fill",
|
||||
"fillNum",
|
||||
"fillElem",
|
||||
"copy",
|
||||
"swap16",
|
||||
"swap32",
|
||||
"swap64",
|
||||
"dataViewNew",
|
||||
"dvGetUint8",
|
||||
"dvGetInt8",
|
||||
"dvGetUint16",
|
||||
"dvGetInt16",
|
||||
"dvGetUint32",
|
||||
"dvGetInt32",
|
||||
"dvGetFloat16",
|
||||
"dvGetFloat32",
|
||||
"dvGetFloat64",
|
||||
"dvGetBigUint64Number",
|
||||
"dvGetBigInt64Number",
|
||||
"dvSetUint8",
|
||||
"dvSetInt8",
|
||||
"dvSetUint16",
|
||||
"dvSetInt16",
|
||||
"dvSetUint32",
|
||||
"dvSetInt32",
|
||||
"dvSetFloat16",
|
||||
"dvSetFloat32",
|
||||
"dvSetFloat64",
|
||||
]);
|
||||
|
||||
/** Whether `e` (or statement `s`) uses only operations that index no
|
||||
* string: numbers, booleans, locals, records and class fields, typed
|
||||
* arrays, number arrays and Math. `reads` admits the string reads a
|
||||
* version covers; `calls` the callees. */
|
||||
function unitSafe(
|
||||
node: { e: IrExpr } | { s: IrStmt },
|
||||
reads: (e: IrExpr & { kind: "strIntrinsic" }) => boolean,
|
||||
calls: (callee: string) => boolean,
|
||||
): boolean {
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "numLit":
|
||||
case "boolLit":
|
||||
case "bin":
|
||||
case "unary":
|
||||
case "logical":
|
||||
case "ternary":
|
||||
case "toBool":
|
||||
case "seqExpr":
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
case "recordGet":
|
||||
case "fieldGet":
|
||||
break;
|
||||
case "bytesIntrinsic":
|
||||
if (!UNIT_SAFE_BYTES_METHODS.has(e.method)) return false;
|
||||
break;
|
||||
case "arrayGet":
|
||||
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Growing or shrinking an array of numbers allocates; it reads no
|
||||
// string.
|
||||
const array = e.receiver.type;
|
||||
if (
|
||||
!ARRAY_METHODS.has(e.method) ||
|
||||
array.kind !== "array" ||
|
||||
(array.elem.kind !== "f64" && array.elem.kind !== "bool")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "libCall":
|
||||
if (!e.fn.startsWith("math.")) return false;
|
||||
break;
|
||||
case "call":
|
||||
if (!calls(e.callee)) return false;
|
||||
break;
|
||||
case "strIntrinsic":
|
||||
if (!reads(e)) return false;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
case "exprStmt":
|
||||
case "if":
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "break":
|
||||
case "continue":
|
||||
case "return":
|
||||
case "recordSet":
|
||||
case "fieldSet":
|
||||
case "bytesSet":
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
return "e" in node ? expr(node.e) : stmt(node.s);
|
||||
}
|
||||
|
||||
/** The module's functions a units version may call: synchronous
|
||||
* functions without captures whose bodies use only unit-safe operations,
|
||||
* read no string, and call only such functions. */
|
||||
export function unitSafeFunctions(mod: IrModule): ReadonlySet<string> {
|
||||
const candidates = new Map(
|
||||
mod.functions
|
||||
.filter((fn) => !fn.async && !fn.generator && !(fn.captures?.length ?? 0))
|
||||
.map((fn) => [fn.name, fn]),
|
||||
);
|
||||
const safe = new Set(candidates.keys());
|
||||
for (let settled = false; !settled;) {
|
||||
settled = true;
|
||||
for (const name of safe) {
|
||||
const fn = candidates.get(name)!;
|
||||
const ok = fn.body.every((s) =>
|
||||
unitSafe(
|
||||
{ s },
|
||||
() => false,
|
||||
(callee) => safe.has(callee),
|
||||
),
|
||||
);
|
||||
if (!ok) {
|
||||
safe.delete(name);
|
||||
settled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
/** The `length` reads of the binding `receiver` in a loop whose every
|
||||
* part is unit-safe, reading only charCodeAt and length of `receiver` and
|
||||
* calling only `safe` functions; null for any other loop. */
|
||||
export function unitSafeLoop(
|
||||
loop: IrStmt & { kind: "for" | "while" },
|
||||
receiver: string,
|
||||
safe: ReadonlySet<string>,
|
||||
): ReadonlySet<IrExpr> | null {
|
||||
const lengths = new Set<IrExpr>();
|
||||
const reads = (e: IrExpr & { kind: "strIntrinsic" }): boolean => {
|
||||
if (e.receiver.kind !== "varRef" || e.receiver.localId !== receiver) return false;
|
||||
if (e.method === "length") lengths.add(e);
|
||||
return e.method === "charCodeAt" || e.method === "length";
|
||||
};
|
||||
const calls = (callee: string): boolean => safe.has(callee);
|
||||
return unitSafe({ s: loop }, reads, calls) ? lengths : null;
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { UnwindAbiViolation, bridgePlainUnwindCalls } from "./unwind-abi.js";
|
||||
|
||||
async function moduleFor(source: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unwind-abi-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The definition of the source function `name` (its i32 or borrowing entry when it has one). */
|
||||
function body(ir: string, name: string): string {
|
||||
return new RegExp(
|
||||
`^define internal [^\\n]*@sc_b?f_[^\\n(]*${name}(?:\\.i32)?\\([^]*?^}`,
|
||||
"m",
|
||||
).exec(ir)![0];
|
||||
}
|
||||
|
||||
const SOURCE = `
|
||||
function leaf(n: number): number {
|
||||
if (n === 3) throw new RangeError("three");
|
||||
return n * 2;
|
||||
}
|
||||
function first(n: number): number {
|
||||
return leaf(n) + 1;
|
||||
}
|
||||
function second(n: number): number {
|
||||
return first(n) * 3;
|
||||
}
|
||||
function third(n: number): number {
|
||||
return second(n) - 2;
|
||||
}
|
||||
function mid(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = third(n);
|
||||
parts.push("b");
|
||||
return r + parts.length;
|
||||
}
|
||||
class Counter {
|
||||
total = 0;
|
||||
add(n: number): void { this.total += mid(n); }
|
||||
}
|
||||
function outer(n: number): string {
|
||||
try { return "ok " + mid(n); } catch (e) { return "caught " + (e as Error).message; }
|
||||
}
|
||||
const counter = new Counter();
|
||||
for (let i = 0; i < 5; i++) { console.log(outer(i)); try { counter.add(i); } catch { console.log("add", i); } }
|
||||
const f = (x: number) => mid(x);
|
||||
console.log(f(1), counter.total);
|
||||
`;
|
||||
|
||||
const LINUX = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const;
|
||||
|
||||
test("directly called functions raise, and their callers invoke them without a pending check", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX);
|
||||
// leaf throws: the exception starts on the pending flag, and the three
|
||||
// calls above it test the flag too; mid raises it on.
|
||||
for (const name of ["leaf", "first", "second", "third"])
|
||||
expect(body(ir, name)).not.toContain("@scr_unwind_raise");
|
||||
const mid = body(ir, "mid");
|
||||
expect(mid).toMatch(/= call double @sc_b?f_[^(]*third\(/);
|
||||
expect(mid).toContain("call void @scr_unwind_raise()");
|
||||
// outer invokes mid; the landing pad takes the exception to its handler.
|
||||
const outer = body(ir, "outer");
|
||||
expect(outer).toMatch(
|
||||
/= invoke double @sc_b?f_[^(]*mid[^\n]* to label %inv\.ok\d+ unwind label %inv\.lp\d+/,
|
||||
);
|
||||
expect(outer).toMatch(/inv\.lp\d+:\n\s+%t\d+ = landingpad \{ ptr, i32 \} cleanup/);
|
||||
expect(outer).toContain("personality ptr @__gcc_personality_v0");
|
||||
expect(ir).toContain("declare i32 @__gcc_personality_v0(...)");
|
||||
expect(ir).toContain("declare void @scr_unwind_raise() cold");
|
||||
});
|
||||
|
||||
test("a method of a class outside any hierarchy is on the unwinding ABI; closures keep the flag", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX);
|
||||
// Counter.add invokes mid and resumes the unwind from its landing pad; the
|
||||
// closure over mid is a pointer target and returns with the flag.
|
||||
const add = body(ir, "Counter_add");
|
||||
expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/);
|
||||
expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/);
|
||||
expect(add).not.toContain("@scr_unwind_raise");
|
||||
const closures = ir
|
||||
.split(/\n(?=define )/)
|
||||
.filter((def) => /^define internal [^\n]*@sc_b?f_[^\n(]*fn_/.test(def));
|
||||
expect(closures.length).toBeGreaterThan(0);
|
||||
for (const def of closures) expect(def).not.toContain("call void @scr_unwind_raise()");
|
||||
});
|
||||
|
||||
test("recursive functions keep the pending flag, also when they recurse through a callback", async () => {
|
||||
const source = `
|
||||
function leaf(n: number): number {
|
||||
if (n < 0) throw new RangeError("negative");
|
||||
return n;
|
||||
}
|
||||
function one(n: number): number { return leaf(n) + 1; }
|
||||
function two(n: number): number { return one(n) + 1; }
|
||||
function three(n: number): number { return two(n) + 1; }
|
||||
function four(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = three(n);
|
||||
return r + parts.length;
|
||||
}
|
||||
function walk(n: number, depth: number): number {
|
||||
const parts: string[] = ["w" + n];
|
||||
const r = depth === 0 ? four(n) : walk(n, depth - 1);
|
||||
return r + parts.length;
|
||||
}
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const parts: string[] = ["v" + n];
|
||||
const r = depth === 0 ? four(n) : each(n, depth - 1);
|
||||
return r + parts.length;
|
||||
}
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (let i = -1; i < 2; i++) {
|
||||
try { console.log(walk(i, 3), visit(i, 3, each)); } catch (e) { console.log((e as Error).message); }
|
||||
}
|
||||
`;
|
||||
const ir = emitLlvmModule(await moduleFor(source), LINUX);
|
||||
expect(body(ir, "four")).toContain("call void @scr_unwind_raise()");
|
||||
for (const name of ["walk", "visit"]) {
|
||||
expect(body(ir, name)).not.toContain("@scr_unwind_raise");
|
||||
expect(body(ir, name)).toMatch(/invoke double @sc_b?f_[^(]*four/);
|
||||
}
|
||||
});
|
||||
|
||||
test("without the option, and off Linux, emission keeps the pending-flag ABI", async () => {
|
||||
const mod = await moduleFor(SOURCE);
|
||||
for (const options of [{}, { targetTriple: "arm64-apple-macosx14.0.0", unwindAbi: true }]) {
|
||||
const ir = emitLlvmModule(mod, options);
|
||||
expect(ir).not.toContain("invoke ");
|
||||
expect(ir).not.toContain("@scr_unwind_raise");
|
||||
expect(ir).not.toContain("personality");
|
||||
}
|
||||
});
|
||||
|
||||
test("bridgePlainUnwindCalls keeps the pending-flag contract at plain calls of raising functions", () => {
|
||||
const raising = [
|
||||
"define internal ptr @sc_bf_raises(ptr %p) #0 { ; raises",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = [
|
||||
"define internal ptr @sc_f_adapter(ptr %p0) #0 {",
|
||||
"entry:",
|
||||
" %result = call ptr @sc_bf_raises(ptr %p0) ; borrowed result",
|
||||
" br label %next",
|
||||
"next:",
|
||||
" %v = phi ptr [ %result, %entry ]",
|
||||
" ret ptr %v",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([`${raising}\n${caller}`]);
|
||||
expect(out).toContain(
|
||||
"%result.ub = invoke ptr @sc_bf_raises(ptr %p0) to label %ub.n.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("%result = phi ptr [ %result.ub, %ub.n.0 ], [ null, %ub.lp.0 ]");
|
||||
// The split block's successor now comes from its last part.
|
||||
expect(out).toContain("%v = phi ptr [ %result, %ub.ok.0 ]");
|
||||
expect(out).toMatch(
|
||||
/define internal ptr @sc_f_adapter\(ptr %p0\) #0 personality ptr @__gcc_personality_v0 \{/,
|
||||
);
|
||||
expect(out).toContain("declare i32 @__gcc_personality_v0(...)");
|
||||
});
|
||||
|
||||
test("an invoked wrapper of a raising function resumes the unwind; its address must not escape", () => {
|
||||
const raising = [
|
||||
"define internal double @sc_bf_body.i32(i32 %a) #0 {",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const wrapper = [
|
||||
"define internal double @sc_bf_body(double %a) #0 {",
|
||||
"entry:",
|
||||
" %i = fptosi double %a to i32",
|
||||
" %r = call double @sc_bf_body.i32(i32 %i)",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = [
|
||||
"define internal double @sc_bf_caller(double %a) #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
" %r = invoke double @sc_bf_body(double %a) to label %ok unwind label %lp",
|
||||
"lp:",
|
||||
" %x = landingpad { ptr, i32 } cleanup",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"ok:",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([[raising, wrapper, caller].join("\n")]);
|
||||
expect(out).toContain(
|
||||
"%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("resume { ptr, i32 } %ub.v.0");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
]),
|
||||
).toThrow(UnwindAbiViolation);
|
||||
try {
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
]);
|
||||
} catch (error) {
|
||||
expect([...(error as UnwindAbiViolation).symbols]).toEqual(["sc_bf_body.i32"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("an invoked function that never tests the pending flag needs no raise", () => {
|
||||
const raising = [
|
||||
"define internal void @sc_bf_raises() #0 {",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const leaf = [
|
||||
"define internal double @sc_bf_leaf() #0 {",
|
||||
"entry:",
|
||||
" ret double 1.0",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = (callee: string) =>
|
||||
[
|
||||
"define internal double @sc_bf_caller() #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
` %r = invoke double @${callee}() to label %ok unwind label %lp`,
|
||||
"lp:",
|
||||
" %x = landingpad { ptr, i32 } cleanup",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"ok:",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([[raising, leaf, caller("sc_bf_leaf")].join("\n")]),
|
||||
).not.toThrow();
|
||||
const testing = leaf.replace(
|
||||
" ret double 1.0",
|
||||
" %f = load i32, ptr @scr_exc_flag\n ret double 1.0",
|
||||
);
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([[raising, testing, caller("sc_bf_leaf")].join("\n")]),
|
||||
).toThrow(UnwindAbiViolation);
|
||||
});
|
||||
|
||||
test("calls that cannot unwind are marked nounwind", () => {
|
||||
const raising = [
|
||||
"define internal void @sc_bf_raises(ptr %b) #0 {",
|
||||
"entry:",
|
||||
" %n = call i64 @scr_bytes_length(ptr %b) ; length",
|
||||
" call void @scr_bytes_release(ptr %b), !dbg !4",
|
||||
" %r = call ptr %b(ptr %b)",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([raising]);
|
||||
expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length");
|
||||
expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4");
|
||||
expect(out).toContain("%r = call ptr %b(ptr %b) nounwind");
|
||||
expect(out).toContain(" call void @scr_unwind_raise()\n");
|
||||
});
|
||||
@@ -0,0 +1,491 @@
|
||||
/* The functions that propagate exceptions by unwinding instead of the
|
||||
* pending flag (see unwindAbiFunctions). */
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
|
||||
/** How many calls above a `throw` functions keep the pending flag. */
|
||||
const THROW_LEVELS = 3;
|
||||
/** The call graph's node for calls through values. */
|
||||
const ANY_FUNCTION = " any function";
|
||||
|
||||
/** The nodes of `edges` on a cycle (a recursion), by Tarjan's algorithm
|
||||
* without recursion of its own: a large module's call chains are deeper
|
||||
* than the host stack. */
|
||||
function recursiveFunctions(edges: ReadonlyMap<string, readonly string[]>): Set<string> {
|
||||
const index = new Map<string, number>();
|
||||
const low = new Map<string, number>();
|
||||
const stack: string[] = [];
|
||||
const onStack = new Set<string>();
|
||||
const result = new Set<string>();
|
||||
let counter = 0;
|
||||
const enter = (name: string): { name: string; next: number } => {
|
||||
index.set(name, counter);
|
||||
low.set(name, counter++);
|
||||
stack.push(name);
|
||||
onStack.add(name);
|
||||
return { name, next: 0 };
|
||||
};
|
||||
for (const root of edges.keys()) {
|
||||
if (index.has(root)) continue;
|
||||
const work = [enter(root)];
|
||||
while (work.length > 0) {
|
||||
const top = work[work.length - 1]!;
|
||||
const out = edges.get(top.name) ?? [];
|
||||
if (top.next < out.length) {
|
||||
const callee = out[top.next++]!;
|
||||
if (!index.has(callee)) work.push(enter(callee));
|
||||
else if (onStack.has(callee))
|
||||
low.set(top.name, Math.min(low.get(top.name)!, index.get(callee)!));
|
||||
continue;
|
||||
}
|
||||
work.pop();
|
||||
const parent = work[work.length - 1];
|
||||
if (parent !== undefined)
|
||||
low.set(parent.name, Math.min(low.get(parent.name)!, low.get(top.name)!));
|
||||
if (low.get(top.name) !== index.get(top.name)) continue;
|
||||
const members: string[] = [];
|
||||
let member: string;
|
||||
do {
|
||||
member = stack.pop()!;
|
||||
onStack.delete(member);
|
||||
members.push(member);
|
||||
} while (member !== top.name);
|
||||
if (members.length > 1 || out.includes(top.name)) for (const m of members) result.add(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The may-throw functions whose exceptions reach their callers by
|
||||
* unwinding the native stack (scr_unwind_raise) instead of returning with
|
||||
* the pending flag set. Their callers call them with `invoke`, so the call
|
||||
* needs no flag test; the landing pad runs the frame's unwind.
|
||||
*
|
||||
* A function qualifies only when every way it can be reached is a direct
|
||||
* `call` from emitted code that can lower it as an `invoke`: it is not
|
||||
* async or a generator, not a constructor, not a method a virtual call can
|
||||
* reach or one a vtable holds (an overridden method), not a closure
|
||||
* target, not the entry or a library export, not named by any node other
|
||||
* than a `call`'s callee, and not called from an async or generator body
|
||||
* (coroutine bodies keep plain calls).
|
||||
*
|
||||
* A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a
|
||||
* few nanoseconds for a flag test, so the functions an exception is likely to
|
||||
* cross many of keep the flag: those with a `throw` or rethrow of their own
|
||||
* and those up to THROW_LEVELS calls above one (a handler near the throw
|
||||
* takes it without a native unwind), and the recursive ones (deep stacks
|
||||
* are recursions). */
|
||||
export function unwindAbiFunctions(
|
||||
mod: IrModule,
|
||||
mayThrow: ReadonlySet<string>,
|
||||
excluded: ReadonlySet<string> = new Set(),
|
||||
): Set<string> {
|
||||
const names = new Set(mod.functions.map((fn) => fn.name));
|
||||
const blocked = new Set<string>(excluded);
|
||||
blocked.add(mod.entry);
|
||||
// The runtime calls the error property readers through the pointers the
|
||||
// module installs (emit-module.ts).
|
||||
blocked.add("%error.name.read");
|
||||
blocked.add("%error.message.read");
|
||||
for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName);
|
||||
// Methods a virtual call can reach, and the overridden methods of class
|
||||
// hierarchies (whose vtables list them), are called through pointers; the
|
||||
// others are called directly, like functions. A virtual call reaches its listed
|
||||
// targets, or else the method in its receiver class's hierarchy; a
|
||||
// receiver that is no class of the module (an interface) may reach any
|
||||
// class's method of that name.
|
||||
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
|
||||
const anyClass = new Set<string>();
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e: IrExpr) => {
|
||||
if (e.kind !== "virtualCall") return true;
|
||||
if (e.targets !== undefined) for (const target of e.targets) blocked.add(target);
|
||||
else if (classes.has(e.className)) blocked.add(`%${e.className}.${e.method}`);
|
||||
else anyClass.add(e.method);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
// A hierarchy's vtables hold the methods declared by more than one of its
|
||||
// classes (an override and what it overrides).
|
||||
const rootOf = (name: string): string => {
|
||||
let cls = classes.get(name);
|
||||
while (cls?.base !== undefined && classes.has(cls.base)) cls = classes.get(cls.base);
|
||||
return cls?.name ?? name;
|
||||
};
|
||||
const declarers = new Map<string, number>();
|
||||
for (const cls of mod.classes ?? [])
|
||||
for (const m of cls.methods ?? []) {
|
||||
const key = `${rootOf(cls.name)}\0${m}`;
|
||||
declarers.set(key, (declarers.get(key) ?? 0) + 1);
|
||||
}
|
||||
for (const cls of mod.classes ?? []) {
|
||||
blocked.add(`%${cls.name}.constructor`);
|
||||
blocked.add(`%${cls.name}.construct`);
|
||||
const root = rootOf(cls.name);
|
||||
for (const m of cls.methods ?? [])
|
||||
if (anyClass.has(m) || declarers.get(`${root}\0${m}`)! > 1) blocked.add(`%${cls.name}.${m}`);
|
||||
}
|
||||
// Any function a node names other than as a call's callee may be reached
|
||||
// through a pointer.
|
||||
const referenced = new Set<string>();
|
||||
const noteNames = (node: object, skip: string | undefined): void => {
|
||||
for (const key in node) {
|
||||
if (key === skip) continue;
|
||||
const value = (node as Record<string, unknown>)[key];
|
||||
if (typeof value === "string" && names.has(value)) referenced.add(value);
|
||||
}
|
||||
};
|
||||
// The call graph, for the functions near a throw and the recursive ones.
|
||||
// Calls through values (closures, class values, dynamic calls, callbacks
|
||||
// passed to the runtime) reach ANY_FUNCTION, which reaches every function
|
||||
// a value can hold.
|
||||
const throws = new Set<string>();
|
||||
const callers = new Map<string, string[]>();
|
||||
const callees = new Map<string, string[]>();
|
||||
const edge = (from: string, to: string): void => {
|
||||
let list = callers.get(to);
|
||||
if (list === undefined) callers.set(to, (list = []));
|
||||
list.push(from);
|
||||
let out = callees.get(from);
|
||||
if (out === undefined) callees.set(from, (out = []));
|
||||
out.push(to);
|
||||
};
|
||||
const methodsNamed = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? [])
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodsNamed.get(m);
|
||||
if (list === undefined) methodsNamed.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
for (const fn of mod.functions) {
|
||||
const coroutine = fn.async === true || fn.generator !== undefined;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e: IrExpr) => {
|
||||
const kind = e.kind as string;
|
||||
if (kind === "throw" || kind === "rethrow") throws.add(fn.name);
|
||||
if (e.kind === "call") {
|
||||
edge(fn.name, e.callee);
|
||||
// The entry's callees (the module initializers) run once: on the
|
||||
// flag they keep landing pads out of the top-level code inlined
|
||||
// into the entry.
|
||||
if (coroutine || fn.name === mod.entry) blocked.add(e.callee);
|
||||
noteNames(e, "callee");
|
||||
return true;
|
||||
}
|
||||
noteNames(e, undefined);
|
||||
if (e.kind === "virtualCall")
|
||||
for (const target of e.targets ?? methodsNamed.get(e.method) ?? []) edge(fn.name, target);
|
||||
else if (e.kind === "new") {
|
||||
edge(fn.name, `%${e.className}.constructor`);
|
||||
edge(fn.name, `%${e.className}.construct`);
|
||||
} else if (kind === "callValue" || kind === "dynCall" || kind === "newValue")
|
||||
edge(fn.name, ANY_FUNCTION);
|
||||
else if ("args" in e) {
|
||||
const args: unknown = e.args;
|
||||
if (Array.isArray(args))
|
||||
for (const arg of args as IrExpr[])
|
||||
if (arg.type.kind === "func") edge(fn.name, ANY_FUNCTION);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
const kind = s.kind as string;
|
||||
if (kind === "throw" || kind === "rethrow") throws.add(fn.name);
|
||||
noteNames(s, undefined);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const name of referenced) {
|
||||
blocked.add(name);
|
||||
edge(ANY_FUNCTION, name);
|
||||
}
|
||||
// A throw starts on the pending flag, and so do the functions up to
|
||||
// THROW_LEVELS calls above it. The union narrowing helpers' throws (a
|
||||
// failed narrowing) don't count for their callers.
|
||||
for (const name of throws) blocked.add(name);
|
||||
let level = [...throws].filter((name) => !name.startsWith("%union."));
|
||||
const near = new Set(level);
|
||||
for (let i = 0; i < THROW_LEVELS; i++) {
|
||||
const next: string[] = [];
|
||||
for (const name of level)
|
||||
for (const caller of callers.get(name) ?? [])
|
||||
if (!near.has(caller)) {
|
||||
near.add(caller);
|
||||
next.push(caller);
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
for (const name of near) blocked.add(name);
|
||||
for (const name of recursiveFunctions(callees)) blocked.add(name);
|
||||
const result = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
if (!mayThrow.has(fn.name) || blocked.has(fn.name)) continue;
|
||||
if (fn.async === true || fn.generator !== undefined) continue;
|
||||
result.add(fn.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** What an unwinding-ABI function's unwind ends with when no handler of
|
||||
* its own takes the exception. */
|
||||
export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable";
|
||||
/** What the unwind of a landing pad ends with when no handler of the
|
||||
* function takes the exception: the unwind goes on (`resume`) with the
|
||||
* exception the landing pad stored in UNWIND_SLOT. The block builder
|
||||
* expands it (BlockBuilder.terminate). */
|
||||
export const UNWIND_RESUME = "resume { ptr, i32 } <unwind slot>";
|
||||
/** The function's slot for the exception its landing pads take. */
|
||||
export const UNWIND_SLOT = "%unw.exc";
|
||||
/** scr_unwind_raise's declaration. It is not `noreturn`: sanitized builds
|
||||
* would call the sanitizer's no-return hook before it, which fails on fiber
|
||||
* stacks; scr_unwind_raise clears the unwound frames' shadow itself. */
|
||||
export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold";
|
||||
|
||||
/** The personality of functions with landing pads: the landing pads are
|
||||
* cleanups that scr_unwind_raise's forced unwind runs. */
|
||||
export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0";
|
||||
|
||||
/** An unwind leaving the function: a raise, or a landing pad's `resume`. */
|
||||
const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /;
|
||||
/** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */
|
||||
const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/;
|
||||
|
||||
/** Functions emission found breaking the unwinding ABI's rules
|
||||
* (bridgePlainUnwindCalls), by symbol: the caller emits the module again
|
||||
* with them on the pending-flag ABI. */
|
||||
export class UnwindAbiViolation extends Error {
|
||||
readonly symbols: ReadonlySet<string>;
|
||||
constructor(symbols: ReadonlySet<string>) {
|
||||
super(`unwinding ABI: ${[...symbols].slice(0, 3).join(", ")} must keep the pending flag`);
|
||||
this.symbols = symbols;
|
||||
}
|
||||
}
|
||||
const DEFINE_NAME = /^define [^@\n]*@([\w.$%-]+)\(/;
|
||||
const PLAIN_CALL = /^(\s*)(%[\w.$-]+) = call (.*?)@([\w.$%-]+)\(|^(\s*)call (.*?)@([\w.$%-]+)\(/;
|
||||
const RETURN_ATTRIBUTES =
|
||||
/\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g;
|
||||
|
||||
/** A call instruction's line. */
|
||||
const CALL_LINE = /^\s*(?:%[\w.$-]+ = )?(?:(?:tail|musttail|notail) )?call /;
|
||||
/** A call's callee (a function or a register) and the parenthesis that
|
||||
* opens its arguments. */
|
||||
const CALLEE = /[@%][\w.$%-]+\(/;
|
||||
|
||||
/** The index of the parenthesis that closes the argument list opened at `open`. */
|
||||
function argumentsEnd(line: string, open: number): number {
|
||||
let depth = 0;
|
||||
let close = open;
|
||||
for (; close < line.length; close++) {
|
||||
const c = line.charAt(close);
|
||||
if (c === "(") depth++;
|
||||
else if (c === ")" && --depth === 0) break;
|
||||
}
|
||||
return close;
|
||||
}
|
||||
|
||||
/** A call that cannot unwind, marked so: only scr_unwind_raise and the
|
||||
* raising functions propagate an unwind (runtime code and every function
|
||||
* reached through a pointer return normally), so every other call is
|
||||
* `nounwind`, and the inliner keeps it a call where it inlines a raising
|
||||
* function at an invoke. */
|
||||
function markNounwind(line: string): string {
|
||||
const call = line.indexOf("call ");
|
||||
const callee = CALLEE.exec(line.slice(call));
|
||||
if (callee === null || callee[0] === "@scr_unwind_raise(") return line;
|
||||
const close = argumentsEnd(line, call + callee.index + callee[0].length - 1);
|
||||
return close >= line.length
|
||||
? line
|
||||
: `${line.slice(0, close + 1)} nounwind${line.slice(close + 1)}`;
|
||||
}
|
||||
|
||||
/** The value a bridged call yields on its landing pad: the dummy a
|
||||
* pending-flag callee returns with an exception pending. */
|
||||
function dummyValue(type: string): string {
|
||||
if (type === "ptr") return "null";
|
||||
if (type === "double" || type === "float") return "0.0";
|
||||
if (/^i\d+$/.test(type)) return "0";
|
||||
return "zeroinitializer";
|
||||
}
|
||||
|
||||
/** Rewrites every remaining plain call of an unwinding-ABI function (one
|
||||
* that raises, scr_unwind_raise, or resumes an unwind) in `parts`, the module's text, into an
|
||||
* invoke whose landing pad continues after the call with the exception
|
||||
* pending and the call's dummy result: the pending-flag contract those
|
||||
* call sites were emitted for, whichever lowering emitted them. Calls of
|
||||
* these functions that emitDirectCall lowered are invokes already. */
|
||||
export function bridgePlainUnwindCalls(parts: string[]): string[] {
|
||||
const raisers = new Set<string>();
|
||||
const split = parts.map((part) => part.split(/\n(?=define )/));
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
if (m !== null && UNWINDS.test(chunk)) raisers.add(m[1]!);
|
||||
}
|
||||
if (raisers.size === 0) return parts;
|
||||
// An invoke's callee must raise or never test the pending flag: one that
|
||||
// returned with the flag pending would pass the exception by its invoking
|
||||
// caller unseen. A wrapper an invoke reaches (an int32 or borrowed-ABI
|
||||
// entry forwarding to the body) propagates its callee's unwind instead
|
||||
// (`resume`), so it raises too. A raising function must be reached only by
|
||||
// calls: a pointer to it (a vtable or table entry, a runtime callback)
|
||||
// could be called without a landing pad. Functions that break either rule
|
||||
// go back to the pending-flag ABI (UnwindAbiViolation).
|
||||
const violations = new Set<string>();
|
||||
const invoked = new Set<string>();
|
||||
const testsFlag = new Set<string>();
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
if (m !== null && PENDING_TEST.test(chunk)) testsFlag.add(m[1]!);
|
||||
for (const c of chunk.matchAll(/ invoke [^@\n]*@([\w.$%-]+)\(/g)) invoked.add(c[1]!);
|
||||
}
|
||||
const resumers = new Set<string>();
|
||||
for (let grew = true; grew;) {
|
||||
grew = false;
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
const name = m?.[1];
|
||||
if (name === undefined || raisers.has(name) || !invoked.has(name)) continue;
|
||||
if (![...chunk.matchAll(/ call [^@\n]*@([\w.$%-]+)\(/g)].some((c) => raisers.has(c[1]!)))
|
||||
continue;
|
||||
if (testsFlag.has(name)) continue;
|
||||
raisers.add(name);
|
||||
resumers.add(name);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
for (const name of invoked) if (!raisers.has(name) && testsFlag.has(name)) violations.add(name);
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks)
|
||||
for (const line of chunk.split("\n")) {
|
||||
if (line.startsWith("define ")) continue;
|
||||
for (const m of line.matchAll(/@([\w.$%-]+)/g)) {
|
||||
if (!raisers.has(m[1]!)) continue;
|
||||
const at = m.index!;
|
||||
if (
|
||||
/(?:call|invoke) [^@]*$/.test(line.slice(0, at)) &&
|
||||
line.charAt(at + m[0].length) === "("
|
||||
)
|
||||
continue;
|
||||
violations.add(m[1]!);
|
||||
}
|
||||
}
|
||||
if (violations.size > 0) throw new UnwindAbiViolation(violations);
|
||||
let personality = false;
|
||||
let counter = 0;
|
||||
const out = split.map((chunks) =>
|
||||
chunks
|
||||
.map((chunk) => {
|
||||
if (!DEFINE_NAME.test(chunk) || !chunk.includes("call ")) return chunk;
|
||||
const lines = chunk.split("\n");
|
||||
const resume = resumers.has(DEFINE_NAME.exec(chunk)![1]!);
|
||||
const result: string[] = [];
|
||||
// The label a block's terminator now follows, for phi operands.
|
||||
const moved = new Map<string, string>();
|
||||
let block = "";
|
||||
let changed = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
const label = /^([\w.$-]+):/.exec(line);
|
||||
if (label !== null) block = label[1]!;
|
||||
const m = PLAIN_CALL.exec(line);
|
||||
const callee = m === null ? undefined : (m[4] ?? m[7]);
|
||||
if (m === null || callee === undefined || !raisers.has(callee)) {
|
||||
const marked = CALL_LINE.test(line) ? markNounwind(line) : line;
|
||||
changed ||= marked !== line;
|
||||
result.push(marked);
|
||||
continue;
|
||||
}
|
||||
// The call's text from its callee to the end of the argument list.
|
||||
const close = argumentsEnd(line, line.indexOf(`@${callee}(`) + callee.length + 1);
|
||||
const head = line.slice(line.indexOf("call ") + 5, close + 1);
|
||||
const tail = line.slice(close + 1);
|
||||
const meta = /,\s*!/.exec(tail);
|
||||
const attrs = (
|
||||
meta === null ? tail.replace(/;.*$/, "") : tail.slice(0, meta.index)
|
||||
).trimEnd();
|
||||
const dbg = meta === null ? "" : tail.slice(meta.index).replace(/\s;.*$/, "");
|
||||
const n = counter++;
|
||||
if (block === "") {
|
||||
result.splice(1, 0, "entry.ub:");
|
||||
block = "entry.ub";
|
||||
}
|
||||
const indent = m[1] ?? m[5] ?? " ";
|
||||
const ok = `ub.ok.${n}`;
|
||||
const pad = `ub.lp.${n}`;
|
||||
if (resume) {
|
||||
// The wrapper holds nothing to release: the unwind goes on to
|
||||
// its caller's landing pad.
|
||||
const assign = m[2] !== undefined ? `${m[2]} = ` : "";
|
||||
result.push(
|
||||
`${indent}${assign}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}resume { ptr, i32 } %ub.v.${n}`,
|
||||
);
|
||||
result.push(`${ok}:`);
|
||||
} else if (m[2] !== undefined) {
|
||||
const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim();
|
||||
const normal = `ub.n.${n}`;
|
||||
result.push(
|
||||
`${indent}${m[2]}.ub = invoke ${head}${attrs} to label %${normal} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(`${normal}:`, `${indent}br label %${ok}`);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}br label %${ok}`,
|
||||
);
|
||||
result.push(
|
||||
`${ok}:`,
|
||||
`${indent}${m[2]} = phi ${type} [ ${m[2]}.ub, %${normal} ], [ ${dummyValue(type)}, %${pad} ]`,
|
||||
);
|
||||
} else {
|
||||
result.push(
|
||||
`${indent}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}br label %${ok}`,
|
||||
);
|
||||
result.push(`${ok}:`);
|
||||
}
|
||||
moved.set(block, ok);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return chunk;
|
||||
if (moved.size === 0) return result.join("\n");
|
||||
personality = true;
|
||||
const header = result[0]!;
|
||||
const brace = header.lastIndexOf(" {");
|
||||
if (!header.includes("personality") && brace >= 0) {
|
||||
// Before the function's metadata attachments (`!dbg`), if any.
|
||||
const dbg = header.slice(0, brace).search(/\s!dbg !\d+$/);
|
||||
const at = dbg >= 0 ? dbg : brace;
|
||||
result[0] = `${header.slice(0, at)} ${UNWIND_PERSONALITY}${header.slice(at)}`;
|
||||
}
|
||||
// A split block's successors now come from its last part.
|
||||
if (moved.size > 0)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const line = result[i]!;
|
||||
if (!line.includes(" phi ")) continue;
|
||||
result[i] = line.replace(/, %([\w.$-]+) \]/g, (all, name: string) => {
|
||||
const to = moved.get(name);
|
||||
return to === undefined ? all : `, %${to} ]`;
|
||||
});
|
||||
}
|
||||
return result.join("\n");
|
||||
})
|
||||
.join("\n"),
|
||||
);
|
||||
if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)")))
|
||||
out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`;
|
||||
return out;
|
||||
}
|
||||
@@ -4,13 +4,15 @@ import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-rang
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { LlInteger } from "./integer-values.js";
|
||||
|
||||
/** Marks the `+` and `-` operands whose number only reaches ToInt32/ToUint32
|
||||
* consumers (`| 0`, the other bitwise operators, `Math.imul`, `Math.clz32`),
|
||||
* directly or through further sums, in the emitter's `wrappingSums`. Called
|
||||
* by the consumer before its operands emit. */
|
||||
/** Marks the `+`, `-` and `*` operands whose number only reaches
|
||||
* ToInt32/ToUint32 consumers (`| 0`, the other bitwise operators,
|
||||
* `Math.imul`, `Math.clz32`), directly or through further sums and
|
||||
* products, in the emitter's `wrappingSums`. Called by the consumer before
|
||||
* its operands emit. */
|
||||
export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrExpr[]): void {
|
||||
for (const operand of operands) {
|
||||
if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-")) continue;
|
||||
if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-" && operand.op !== "*"))
|
||||
continue;
|
||||
if (operand.type.kind !== "f64" || host.wrappingSums.has(operand)) continue;
|
||||
host.wrappingSums.add(operand);
|
||||
markWrappingOperands(host, operand.left, operand.right);
|
||||
@@ -128,12 +130,15 @@ function wrappingOperand(
|
||||
return { low, valid };
|
||||
}
|
||||
|
||||
/** ToUint32 of `left op right` (`+` or `-`) in 32-bit arithmetic when the
|
||||
* expression only reaches ToInt32/ToUint32 consumers and both operands are
|
||||
* exact integers or NaN with an exact result. Both conversions are
|
||||
* arithmetic mod 2^32, so the wrapping i32 sum of the operands' low halves
|
||||
* is the converted exact sum; a NaN operand makes the sum NaN, which
|
||||
* converts to 0. Returns undefined, emitting nothing, for any other sum. */
|
||||
/** ToUint32 of `left op right` (`+`, `-` or `*`) in 32-bit arithmetic when
|
||||
* the expression only reaches ToInt32/ToUint32 consumers and both operands
|
||||
* are exact integers or NaN with an exact result. Both conversions are
|
||||
* arithmetic mod 2^32, so the wrapping i32 sum or product of the operands'
|
||||
* low halves is the converted exact result; a NaN operand makes the result
|
||||
* NaN, which converts to 0. A product is exact when the operands' magnitude
|
||||
* bounds multiply to at most 2^53; it may be -0 (`0 * -1`), which converts
|
||||
* to 0 like the integer 0. Returns undefined, emitting nothing, for any
|
||||
* other expression. */
|
||||
export function emitWrappingSum(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "bin" },
|
||||
@@ -142,15 +147,23 @@ export function emitWrappingSum(
|
||||
li: LlInteger | null,
|
||||
ri: LlInteger | null,
|
||||
): string | undefined {
|
||||
if ((e.op !== "+" && e.op !== "-") || !host.wrappingSums.has(e)) return undefined;
|
||||
if ((e.op !== "+" && e.op !== "-" && e.op !== "*") || !host.wrappingSums.has(e)) return undefined;
|
||||
const leftRange = li ? li.range : integerOrNaNRange(host, e.left);
|
||||
const rightRange = leftRange && (ri ? ri.range : integerOrNaNRange(host, e.right));
|
||||
if (!rightRange || !integerArithmeticRange(e.op, leftRange!, rightRange)) return undefined;
|
||||
if (!rightRange) return undefined;
|
||||
if (
|
||||
e.op === "*"
|
||||
? !exactProduct(leftRange!, rightRange)
|
||||
: !integerArithmeticRange(e.op, leftRange!, rightRange)
|
||||
)
|
||||
return undefined;
|
||||
const B = host.B;
|
||||
const left = wrappingOperand(host, l, li, leftRange!);
|
||||
const right = wrappingOperand(host, r, ri, rightRange);
|
||||
const sum = B.tmp();
|
||||
B.line(`${sum} = ${e.op === "+" ? "add" : "sub"} i32 ${left.low}, ${right.low}`);
|
||||
B.line(
|
||||
`${sum} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} i32 ${left.low}, ${right.low}`,
|
||||
);
|
||||
let valid = left.valid ?? right.valid;
|
||||
if (valid === null) return sum;
|
||||
if (left.valid && right.valid) {
|
||||
@@ -162,6 +175,17 @@ export function emitWrappingSum(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether every product of a value in `left` and one in `right` is an
|
||||
* integer of magnitude below 2^53, which the double product holds exactly.
|
||||
* The bound's own product is computed in a double: a bound of at least
|
||||
* 2^53 never rounds below 2^53, while 2^53 + 1 (3 * 3002399751580331)
|
||||
* rounds to 2^53, so the test is strict. */
|
||||
function exactProduct(left: IntegerRange, right: IntegerRange): boolean {
|
||||
const magnitude = (range: IntegerRange): number =>
|
||||
Math.max(Math.abs(range.min), Math.abs(range.max));
|
||||
return magnitude(left) * magnitude(right) < 2 ** 53;
|
||||
}
|
||||
|
||||
/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of
|
||||
* those integers: locals declared once with an initializer, neither parameters nor captured or
|
||||
* boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once
|
||||
|
||||
@@ -93,7 +93,7 @@ export function computeMayThrow(
|
||||
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
|
||||
const constructorClasses = new Map<string, string>();
|
||||
const classValueCallers = new Map<string, Set<string>>();
|
||||
const tdzGlobals = (mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id);
|
||||
const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id));
|
||||
for (const cls of mod.classes ?? []) {
|
||||
constructorClasses.set(`%${cls.name}.constructor`, cls.name);
|
||||
for (const m of cls.methods ?? []) {
|
||||
@@ -113,11 +113,10 @@ export function computeMayThrow(
|
||||
if (explain) f.calleeLocs = new Map();
|
||||
// TDZ reads and non-initializing writes can throw ReferenceError.
|
||||
// Capture locals carry the same flag as the declaring binding.
|
||||
const tdzIds = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]);
|
||||
const mutableTdzIds = new Set([
|
||||
...tdzGlobals,
|
||||
...fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id),
|
||||
]);
|
||||
// Every TDZ global counts for both reads and writes; the module's set is
|
||||
// shared by every function.
|
||||
const tdzLocals = new Set(fn.locals.filter((l) => l.tdz).map((l) => l.id));
|
||||
const mutableTdzLocals = new Set(fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id));
|
||||
// Traverse typed executable nodes without copying the IR into unknown.
|
||||
const visit = (rec: IrExpr | IrStmt): boolean => {
|
||||
const threw = f.throws;
|
||||
@@ -133,13 +132,17 @@ export function computeMayThrow(
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
if (tdzIds.has(rec.localId)) f.throws = true;
|
||||
if (tdzGlobals.has(rec.localId) || tdzLocals.has(rec.localId)) f.throws = true;
|
||||
break;
|
||||
case "assign":
|
||||
// A declaration is allowed to fill an empty box. Legacy const
|
||||
// TDZ stores also initialize; only mutable subsequent stores
|
||||
// introduce the new write-side exception edge.
|
||||
if (rec.initializes !== true && mutableTdzIds.has(rec.localId)) f.throws = true;
|
||||
if (
|
||||
rec.initializes !== true &&
|
||||
(tdzGlobals.has(rec.localId) || mutableTdzLocals.has(rec.localId))
|
||||
)
|
||||
f.throws = true;
|
||||
break;
|
||||
case "dynCheck":
|
||||
case "caughtCheck":
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
cacheTargetIdentity,
|
||||
isZigDriver,
|
||||
isMuslTarget,
|
||||
type CcDriver,
|
||||
} from "./driver.js";
|
||||
import {
|
||||
resolveProgramShardMergeIdentity,
|
||||
@@ -821,6 +822,7 @@ export async function compileCInternal(
|
||||
// generated/native input that references inflate symbols. Cross
|
||||
// and targetless Zig builds use vendored zlib objects in the input section above.
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
// glibc keeps libm separate from libc. This must trail the generated
|
||||
// program and every native FFI input because GNU ld resolves archives
|
||||
// from left to right.
|
||||
@@ -1068,6 +1070,7 @@ export async function compileCInternal(
|
||||
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
...driver.linkArgs,
|
||||
...executableLinkFlags,
|
||||
];
|
||||
@@ -1080,6 +1083,7 @@ export async function compileCInternal(
|
||||
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
...driver.linkArgs,
|
||||
...executableLinkFlags,
|
||||
];
|
||||
@@ -1689,3 +1693,11 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Linux runtimes raise through the native unwinder (scr_unwind_raise):
|
||||
* Zig's libunwind, or the toolchain's libgcc_eh. Both are static archives,
|
||||
* so programs load no shared library for them. */
|
||||
function unwinderLinkArgs(driver: CcDriver): string[] {
|
||||
if (targetPlatform(driver) !== "linux") return [];
|
||||
return isZigDriver(driver) ? ["-lunwind"] : ["-lgcc_eh"];
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_object.c",
|
||||
"scr_union.c",
|
||||
"scr_exception.c",
|
||||
"scr_unwind.c",
|
||||
"scr_error.c",
|
||||
"scr_console.c",
|
||||
"scr_lib.c",
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { nativeCodegenDiag } from "../diagnostics/diagnostic.js";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import { RuntimePackError } from "./runtime-pack-core.js";
|
||||
import { selectNativeRuntimePack } from "./runtime-pack-native.js";
|
||||
import { WASM32_WASI_TARGET } from "./targets.js";
|
||||
import { RuntimePackError, type RuntimePackManifest } from "./runtime-pack-core.js";
|
||||
import {
|
||||
runtimePackManifest,
|
||||
selectNativeRuntimePack,
|
||||
stageNativeRuntimeSelection,
|
||||
type NativeRuntimeSelection,
|
||||
type RuntimePackStore,
|
||||
} from "./runtime-pack-native.js";
|
||||
import { MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js";
|
||||
|
||||
vi.mock("node:crypto", async (original) => {
|
||||
const crypto = await original<typeof import("node:crypto")>();
|
||||
return { ...crypto, createHash: vi.fn(crypto.createHash) };
|
||||
});
|
||||
vi.mock("node:fs", async (original) => {
|
||||
const fs = await original<typeof import("node:fs")>();
|
||||
return { ...fs, readFileSync: vi.fn(fs.readFileSync) };
|
||||
});
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterEach(async () =>
|
||||
@@ -45,3 +72,171 @@ test("a missing native helper keeps the reinstall hint", () => {
|
||||
"reinstall scriptc with optional dependencies enabled for this host",
|
||||
);
|
||||
});
|
||||
|
||||
/** A runtime pack with one runtime object and a license, and a store that
|
||||
* keeps entries in memory. */
|
||||
async function packFixture() {
|
||||
const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-store-"));
|
||||
dirs.push(root);
|
||||
const target = MACOS_ARM64_TARGET;
|
||||
const version = compilerReleaseVersion();
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ name: target.runtimePackPackage, version }),
|
||||
);
|
||||
mkdirSync(join(root, "artifacts"));
|
||||
const object = join(root, "artifacts", "base.o");
|
||||
writeFileSync(object, "base object");
|
||||
writeFileSync(join(root, "license.txt"), "license");
|
||||
const unit = {
|
||||
source: "scr_base.c",
|
||||
predicate: true,
|
||||
variants: [
|
||||
{
|
||||
id: "default",
|
||||
when: {},
|
||||
defines: [],
|
||||
path: "artifacts/base.o",
|
||||
sha256: createHash("sha256").update("base object").digest("hex"),
|
||||
size: Buffer.byteLength("base object"),
|
||||
},
|
||||
],
|
||||
};
|
||||
const manifest: RuntimePackManifest = {
|
||||
schema: "scriptc.runtime-pack.v1",
|
||||
format: 1,
|
||||
package: target.runtimePackPackage,
|
||||
version,
|
||||
target: {
|
||||
name: target.name,
|
||||
llvm_triple: target.llvmTriple,
|
||||
architecture: target.architecture,
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
compiler: { command: "clang", identity: "fixture clang", target: target.llvmTriple },
|
||||
macros: {
|
||||
executable: [],
|
||||
excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT"],
|
||||
sanitizer: "external-toolchain-required",
|
||||
},
|
||||
flavors: {
|
||||
release: { optimization: "-O2", runtime_units: [unit] },
|
||||
dev: { optimization: "-O0", runtime_units: [unit] },
|
||||
},
|
||||
archives: [],
|
||||
system_libraries: [{ name: "System", predicate: true }],
|
||||
licenses: [{ path: "license.txt", license: "fixture" }],
|
||||
};
|
||||
const manifestText = JSON.stringify(manifest);
|
||||
writeFileSync(join(root, "runtime-pack.json"), manifestText);
|
||||
const entries = new Map<string, string>();
|
||||
const store: RuntimePackStore = {
|
||||
read: (key) => entries.get(key) ?? null,
|
||||
write: (key, text) => void entries.set(key, text),
|
||||
};
|
||||
const select = () =>
|
||||
selectNativeRuntimePack(
|
||||
root,
|
||||
target,
|
||||
version,
|
||||
{} as NativeLinkFeatures,
|
||||
"release",
|
||||
"executable",
|
||||
store,
|
||||
);
|
||||
let stages = 0;
|
||||
const stage = (selection: NativeRuntimeSelection) => {
|
||||
const directory = join(root, `stage-${stages++}`);
|
||||
mkdirSync(directory);
|
||||
return stageNativeRuntimeSelection(selection, directory, store);
|
||||
};
|
||||
return { root, object, manifestText, entries, select, stage };
|
||||
}
|
||||
|
||||
test("a stored selection answers while the pack's files keep their identities", async () => {
|
||||
const f = await packFixture();
|
||||
const first = f.select();
|
||||
expect(first.selected.systemLibraries).toEqual(["System"]);
|
||||
expect(f.entries.size).toBe(1);
|
||||
// The next selection reads the stored answer instead of the manifest.
|
||||
const [key, text] = [...f.entries][0]!;
|
||||
const stored = JSON.parse(text);
|
||||
stored.selected.systemLibraries = ["Stored"];
|
||||
f.entries.set(key, JSON.stringify(stored));
|
||||
const second = f.select();
|
||||
expect(second.selected.systemLibraries).toEqual(["Stored"]);
|
||||
expect(second.licenses).toEqual(first.licenses);
|
||||
expect(second.manifest).toBeUndefined();
|
||||
expect(runtimePackManifest(second).flavors.release).toEqual(first.manifest!.flavors.release);
|
||||
// Writing the manifest again, with the same text, gives it a new identity.
|
||||
writeFileSync(join(f.root, "runtime-pack.json"), f.manifestText);
|
||||
expect(f.select().selected.systemLibraries).toEqual(["System"]);
|
||||
});
|
||||
|
||||
test.each(["replaced", "rewritten in place"])(
|
||||
"staging hashes an artifact again when it is %s, even at the same size and mtime",
|
||||
async (change) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const before = statSync(f.object);
|
||||
const changed = change === "replaced" ? f.object + ".new" : f.object;
|
||||
writeFileSync(changed, "x".repeat(before.size));
|
||||
utimesSync(changed, before.atime, before.mtime);
|
||||
if (changed !== f.object) renameSync(changed, f.object);
|
||||
expect(() => f.stage(selection)).toThrow(
|
||||
"runtime pack artifact hash mismatch: artifacts/base.o",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("staging does not hash an unchanged artifact again", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
const hashes = () => vi.mocked(createHash).mock.calls.length;
|
||||
let start = hashes();
|
||||
f.stage(selection);
|
||||
const firstStage = hashes() - start;
|
||||
start = hashes();
|
||||
const staged = f.stage(selection);
|
||||
// Only the store key is hashed; the staged copy has the verified bytes.
|
||||
expect(hashes() - start).toBe(firstStage - 1);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"staging reads an artifact's bytes from the file whose identity it checks",
|
||||
async () => {
|
||||
const f = await packFixture();
|
||||
// The artifact path is a symbolic link to the verified file.
|
||||
const real = f.object + ".real";
|
||||
renameSync(f.object, real);
|
||||
symlinkSync(real, f.object);
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
// During the next read the link names another file of the same size,
|
||||
// and it names the verified file again before the read ends.
|
||||
const other = f.object + ".other";
|
||||
writeFileSync(other, "x".repeat(statSync(real).size));
|
||||
const read = vi.mocked(readFileSync);
|
||||
const original = read.getMockImplementation()!;
|
||||
read.mockImplementation(((source: Parameters<typeof readFileSync>[0], ...rest: unknown[]) => {
|
||||
if (typeof source === "string" && !source.endsWith("base.o"))
|
||||
return (original as (...args: unknown[]) => unknown)(source, ...rest);
|
||||
read.mockImplementation(original);
|
||||
unlinkSync(f.object);
|
||||
symlinkSync(other, f.object);
|
||||
try {
|
||||
return (original as (...args: unknown[]) => unknown)(source, ...rest);
|
||||
} finally {
|
||||
unlinkSync(f.object);
|
||||
symlinkSync(real, f.object);
|
||||
}
|
||||
}) as typeof readFileSync);
|
||||
const staged = f.stage(selection);
|
||||
expect(read.getMockImplementation()).toBe(original);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
/** Synchronous runtime-pack host for the statically compiled driver. Each
|
||||
* selected artifact is hashed and copied from the same read into private
|
||||
* storage. This host has no persistent cache or mutable installed link inputs. */
|
||||
* selected artifact is verified and copied from the same read into private
|
||||
* storage. With a store, a pack's selections and the identities of the
|
||||
* artifacts it verified are kept between builds: an artifact whose file
|
||||
* identity is unchanged since it was hashed is not hashed again. */
|
||||
import type { NativeOptimization } from "./optimization.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
constants,
|
||||
existsSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
writeSync,
|
||||
type Stats,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
@@ -34,13 +39,66 @@ export interface NativeRuntimePack {
|
||||
|
||||
export interface NativeRuntimeSelection {
|
||||
root: string;
|
||||
manifest: RuntimePackManifest;
|
||||
/** The parsed manifest; a selection read from a store has none until
|
||||
* runtimePackManifest parses it. */
|
||||
manifest?: RuntimePackManifest;
|
||||
licenses: RuntimePackManifest["licenses"];
|
||||
packageText: string;
|
||||
manifestText: string;
|
||||
selected: RuntimePackArtifacts;
|
||||
flavor: NativeOptimization;
|
||||
}
|
||||
|
||||
/** Text entries kept between builds (the native compiler's cache). */
|
||||
export interface RuntimePackStore {
|
||||
read(key: string): string | null;
|
||||
write(key: string, text: string): void;
|
||||
}
|
||||
|
||||
interface FileIdentity {
|
||||
dev: number;
|
||||
ino: number;
|
||||
size: number;
|
||||
mtime: number;
|
||||
ctime: number;
|
||||
}
|
||||
|
||||
function fileIdentity(path: string): FileIdentity {
|
||||
return statsIdentity(statSync(path));
|
||||
}
|
||||
|
||||
function statsIdentity(info: Stats): FileIdentity {
|
||||
return {
|
||||
dev: info.dev,
|
||||
ino: info.ino,
|
||||
size: info.size,
|
||||
mtime: info.mtimeMs,
|
||||
ctime: info.ctimeMs,
|
||||
};
|
||||
}
|
||||
|
||||
function sameIdentity(a: FileIdentity, b: FileIdentity): boolean {
|
||||
return (
|
||||
a.dev === b.dev &&
|
||||
a.ino === b.ino &&
|
||||
a.size === b.size &&
|
||||
a.mtime === b.mtime &&
|
||||
a.ctime === b.ctime
|
||||
);
|
||||
}
|
||||
|
||||
function storeKey(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
/** A stored selection: what selecting from the pack's files, with these
|
||||
* identities, returned. */
|
||||
interface StoredSelection {
|
||||
schema: 1;
|
||||
licenses: RuntimePackManifest["licenses"];
|
||||
selected: RuntimePackArtifacts;
|
||||
}
|
||||
|
||||
export function selectNativeRuntimePack(
|
||||
root: string,
|
||||
target: NativeTargetSpec,
|
||||
@@ -48,38 +106,204 @@ export function selectNativeRuntimePack(
|
||||
features: NativeLinkFeatures,
|
||||
flavor: NativeOptimization,
|
||||
mode: RuntimePackMode = "executable",
|
||||
store?: RuntimePackStore,
|
||||
): NativeRuntimeSelection {
|
||||
if (!existsSync(join(root, "package.json"))) {
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
if (!existsSync(packagePath)) {
|
||||
throw new RuntimePackError(
|
||||
`runtime pack for ${target.name} is not installed; install ${target.runtimePackPackage}@${compilerVersion} in your project or set SCRIPTC_RUNTIME_PACK to its directory`,
|
||||
"missing",
|
||||
"scriptc includes the runtime pack for its own platform only; other targets need their runtime pack package in the project",
|
||||
);
|
||||
}
|
||||
const packageText = readFileSync(join(root, "package.json"), "utf8");
|
||||
const manifestText = readFileSync(join(root, "runtime-pack.json"), "utf8");
|
||||
// The pack's files by identity, with everything else selection reads.
|
||||
let key: string | null = null;
|
||||
let identities: FileIdentity[] = [];
|
||||
if (store !== undefined) {
|
||||
try {
|
||||
identities = [fileIdentity(packagePath), fileIdentity(manifestPath)];
|
||||
key = storeKey(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
root,
|
||||
identities,
|
||||
target: target.name,
|
||||
compilerVersion,
|
||||
features,
|
||||
flavor,
|
||||
mode,
|
||||
}),
|
||||
);
|
||||
const stored = readStoredSelection(store.read(key));
|
||||
if (stored !== null) {
|
||||
const packageText = readFileSync(packagePath, "utf8");
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
// The texts read are the ones with the stored identities.
|
||||
if (
|
||||
sameIdentity(fileIdentity(packagePath), identities[0]!) &&
|
||||
sameIdentity(fileIdentity(manifestPath), identities[1]!)
|
||||
) {
|
||||
return {
|
||||
root,
|
||||
licenses: stored.licenses,
|
||||
packageText,
|
||||
manifestText,
|
||||
selected: stored.selected,
|
||||
flavor,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
const packageText = readFileSync(packagePath, "utf8");
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
const identity = JSON.parse(packageText) as { name?: string; version?: string };
|
||||
const manifest = parseRuntimePackManifest(JSON.parse(manifestText));
|
||||
validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion);
|
||||
const selected = selectRuntimePackArtifacts(manifest, features, flavor, process.env, mode);
|
||||
return { root, manifest, packageText, manifestText, selected, flavor };
|
||||
if (store !== undefined && key !== null) {
|
||||
try {
|
||||
// Stored only when the files selected from still have the identities
|
||||
// in the key.
|
||||
if (
|
||||
sameIdentity(fileIdentity(packagePath), identities[0]!) &&
|
||||
sameIdentity(fileIdentity(manifestPath), identities[1]!)
|
||||
) {
|
||||
const entry: StoredSelection = { schema: 1, licenses: manifest.licenses, selected };
|
||||
store.write(key, JSON.stringify(entry));
|
||||
}
|
||||
} catch {
|
||||
/* A failed write is a later miss. */
|
||||
}
|
||||
}
|
||||
return {
|
||||
root,
|
||||
manifest,
|
||||
licenses: manifest.licenses,
|
||||
packageText,
|
||||
manifestText,
|
||||
selected,
|
||||
flavor,
|
||||
};
|
||||
}
|
||||
|
||||
function stageArtifact(root: string, stage: string, artifact: RuntimePackArtifact): string {
|
||||
/** The selection's parsed manifest. */
|
||||
export function runtimePackManifest(selection: NativeRuntimeSelection): RuntimePackManifest {
|
||||
return (selection.manifest ??= parseRuntimePackManifest(JSON.parse(selection.manifestText)));
|
||||
}
|
||||
|
||||
function readStoredSelection(text: string | null): StoredSelection | null {
|
||||
if (text === null) return null;
|
||||
const value: unknown = JSON.parse(text);
|
||||
if (value === null || typeof value !== "object") return null;
|
||||
const entry = value as Partial<StoredSelection>;
|
||||
if (
|
||||
entry.schema !== 1 ||
|
||||
!Array.isArray(entry.licenses) ||
|
||||
entry.selected === undefined ||
|
||||
entry.selected === null ||
|
||||
typeof entry.selected !== "object" ||
|
||||
!Array.isArray(entry.selected.runtime) ||
|
||||
!Array.isArray(entry.selected.archives) ||
|
||||
!Array.isArray(entry.selected.systemLibraries)
|
||||
)
|
||||
return null;
|
||||
return entry as StoredSelection;
|
||||
}
|
||||
|
||||
/** An artifact path's identity when its bytes last matched the manifest's
|
||||
* size and digest. */
|
||||
interface VerifiedArtifact {
|
||||
path: string;
|
||||
sha256: string;
|
||||
dev: number;
|
||||
ino: number;
|
||||
size: number;
|
||||
mtime: number;
|
||||
ctime: number;
|
||||
}
|
||||
|
||||
interface StoredVerification {
|
||||
schema: number;
|
||||
artifacts: VerifiedArtifact[];
|
||||
}
|
||||
|
||||
function readVerified(
|
||||
store: RuntimePackStore | undefined,
|
||||
key: string,
|
||||
): Map<string, VerifiedArtifact> {
|
||||
const verified = new Map<string, VerifiedArtifact>();
|
||||
try {
|
||||
const text = store?.read(key) ?? null;
|
||||
if (text === null) return verified;
|
||||
const stored = JSON.parse(text) as StoredVerification;
|
||||
if (stored.schema === 1)
|
||||
for (const artifact of stored.artifacts) verified.set(artifact.path, artifact);
|
||||
} catch {
|
||||
/* A malformed entry verifies everything again. */
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
function stageArtifact(
|
||||
root: string,
|
||||
stage: string,
|
||||
artifact: RuntimePackArtifact,
|
||||
verified: Map<string, VerifiedArtifact>,
|
||||
newlyVerified: VerifiedArtifact[],
|
||||
): string {
|
||||
const source = join(root, artifact.path);
|
||||
const destination = join(stage, artifact.path);
|
||||
let bytes: Buffer;
|
||||
let before: FileIdentity;
|
||||
let after: FileIdentity;
|
||||
try {
|
||||
bytes = readFileSync(source);
|
||||
// The identities are those of the open file the bytes come from, also
|
||||
// when the path is a symbolic link that changes during the read.
|
||||
const input = openSync(source, constants.O_RDONLY);
|
||||
try {
|
||||
before = statsIdentity(fstatSync(input));
|
||||
bytes = readFileSync(input);
|
||||
after = statsIdentity(fstatSync(input));
|
||||
} finally {
|
||||
closeSync(input);
|
||||
}
|
||||
} catch {
|
||||
throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid");
|
||||
}
|
||||
// The bytes are those last hashed when the file kept the identity it had
|
||||
// then for the whole read.
|
||||
const known = verified.get(artifact.path);
|
||||
const unchanged = sameIdentity(before, after);
|
||||
const trusted =
|
||||
known !== undefined &&
|
||||
known.sha256 === artifact.sha256 &&
|
||||
known.size === artifact.size &&
|
||||
unchanged &&
|
||||
known.dev === before.dev &&
|
||||
known.ino === before.ino &&
|
||||
known.size === before.size &&
|
||||
known.mtime === before.mtime &&
|
||||
known.ctime === before.ctime;
|
||||
if (
|
||||
bytes.length !== artifact.size ||
|
||||
createHash("sha256").update(bytes).digest("hex") !== artifact.sha256
|
||||
(!trusted && createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
||||
) {
|
||||
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
||||
}
|
||||
if (!trusted && unchanged)
|
||||
newlyVerified.push({
|
||||
path: artifact.path,
|
||||
sha256: artifact.sha256,
|
||||
dev: before.dev,
|
||||
ino: before.ino,
|
||||
size: before.size,
|
||||
mtime: before.mtime,
|
||||
ctime: before.ctime,
|
||||
});
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
const fd = openSync(
|
||||
destination,
|
||||
@@ -123,15 +347,19 @@ export function stageNativeRuntimePack(
|
||||
export function stageNativeRuntimeSelection(
|
||||
selection: NativeRuntimeSelection,
|
||||
stageRoot: string,
|
||||
store?: RuntimePackStore,
|
||||
): NativeRuntimePack {
|
||||
const { root, manifest, selected, packageText, manifestText } = selection;
|
||||
const { root, licenses, selected, packageText, manifestText } = selection;
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
const runtimeObjects = selected.runtime.map((artifact) =>
|
||||
stageArtifact(root, stageRoot, artifact),
|
||||
);
|
||||
const archives = selected.archives.map((artifact) => stageArtifact(root, stageRoot, artifact));
|
||||
for (const license of manifest.licenses) {
|
||||
const key = storeKey(JSON.stringify({ schema: 1, verified: root }));
|
||||
const verified = readVerified(store, key);
|
||||
const newlyVerified: VerifiedArtifact[] = [];
|
||||
const stage = (artifact: RuntimePackArtifact): string =>
|
||||
stageArtifact(root, stageRoot, artifact, verified, newlyVerified);
|
||||
const runtimeObjects = selected.runtime.map(stage);
|
||||
const archives = selected.archives.map(stage);
|
||||
for (const license of licenses) {
|
||||
try {
|
||||
readFileSync(join(root, license.path));
|
||||
} catch {
|
||||
@@ -144,5 +372,14 @@ export function stageNativeRuntimeSelection(
|
||||
) {
|
||||
throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid");
|
||||
}
|
||||
if (store !== undefined && newlyVerified.length > 0) {
|
||||
for (const artifact of newlyVerified) verified.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...verified.values()] };
|
||||
try {
|
||||
store.write(key, JSON.stringify(entry));
|
||||
} catch {
|
||||
/* A failed write only means hashing again next time. */
|
||||
}
|
||||
}
|
||||
return { runtimeObjects, archives, systemLibraries: selected.systemLibraries };
|
||||
}
|
||||
|
||||
@@ -622,7 +622,7 @@ export function unsupportedTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnosti
|
||||
code: "SC2001",
|
||||
message:
|
||||
`values of type '${typeText}' cannot be compiled yet ` +
|
||||
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')`,
|
||||
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')`,
|
||||
loc,
|
||||
milestone: "M2",
|
||||
};
|
||||
|
||||
@@ -270,3 +270,49 @@ test("synchronous tracking restores parents after throws and propagates unstable
|
||||
trackedFileExists(join(dir, "outside.ts"));
|
||||
expect(parent.snapshot()).toEqual(before);
|
||||
});
|
||||
|
||||
test("a settled file is checked by its identity, and any write invalidates it", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
await writeFile(source, "export const answer = 1;\n");
|
||||
const written = fs.statSync(source);
|
||||
// The file was last changed long enough before the read.
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(Date.now() + 5000);
|
||||
const tracker = new FrontendInputTracker();
|
||||
try {
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
const snapshot = tracker.snapshot();
|
||||
const probe = snapshot.probes.find((entry) => entry.op === "file");
|
||||
expect(probe?.op === "file" && probe.identity !== undefined).toBe(true);
|
||||
expect(validFrontendInputSnapshot(snapshot)).toBe(true);
|
||||
const reads = vi.mocked(fs.readFileSync);
|
||||
reads.mockClear();
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(0);
|
||||
// A same-size rewrite that restores the modification time still changes
|
||||
// the change time.
|
||||
await writeFile(source, "export const answer = 2;\n");
|
||||
fs.utimesSync(source, written.atime, written.mtime);
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(false);
|
||||
});
|
||||
|
||||
test("a file changed shortly before the read is hashed on every check", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
await writeFile(source, "export const answer = 1;\n");
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
const snapshot = tracker.snapshot();
|
||||
const probe = snapshot.probes.find((entry) => entry.op === "file");
|
||||
expect(probe?.op === "file" && probe.identity).toBeUndefined();
|
||||
const reads = vi.mocked(fs.readFileSync);
|
||||
reads.mockClear();
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@ import { observeSourceText } from "./ts7/ast-node.js";
|
||||
* behind an old resolution answer.
|
||||
*/
|
||||
export type FrontendInputProbe =
|
||||
| { op: "file"; path: string; digest: string }
|
||||
/** `identity` (fileIdentity) lets a later check skip hashing the file. */
|
||||
| { op: "file"; path: string; digest: string; identity?: string }
|
||||
| { op: "read-error"; path: string }
|
||||
| { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" }
|
||||
| { op: "entries"; path: string; files: string[]; directories: string[] }
|
||||
@@ -53,6 +54,36 @@ function frontendSourceDigest(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
/** How long a file must have been unchanged before a read for its identity
|
||||
* to stand for its content: a later write then gets a newer change time
|
||||
* even on file systems whose timestamps tick once a second or every two. */
|
||||
const IDENTITY_SETTLE_MS = 2000;
|
||||
|
||||
/** The device, inode, size, modification time and change time of `path`. A
|
||||
* write changes the change time, which no tool can set back, so an equal
|
||||
* identity means the content is the one hashed with it. */
|
||||
function fileIdentity(path: string): string | null {
|
||||
try {
|
||||
const info = statSync(path);
|
||||
return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The text of `path` with its identity when the identity was the same before
|
||||
* and after the read and the file had been unchanged for IDENTITY_SETTLE_MS. */
|
||||
function readWithIdentity(path: string): { text: string; identity: string | null } {
|
||||
const readAt = Date.now();
|
||||
const before = statSync(path);
|
||||
const text = readFileSync(path, "utf8");
|
||||
const after = fileIdentity(path);
|
||||
const identity = `${before.dev}:${before.ino}:${before.size}:${before.mtimeMs}:${before.ctimeMs}`;
|
||||
const settled =
|
||||
before.mtimeMs <= readAt - IDENTITY_SETTLE_MS && before.ctimeMs <= readAt - IDENTITY_SETTLE_MS;
|
||||
return { text, identity: settled && after === identity ? identity : null };
|
||||
}
|
||||
|
||||
function pathKind(path: string): Extract<FrontendInputProbe, { op: "kind" }>["kind"] {
|
||||
try {
|
||||
const info = statSync(path);
|
||||
@@ -199,8 +230,11 @@ export function trackedReadFile(path: string): string | null {
|
||||
const virtual = virtualSources.get(path);
|
||||
if (virtual !== undefined) return virtual;
|
||||
try {
|
||||
const text = readFileSync(path, "utf8");
|
||||
record({ op: "file", path, digest: frontendSourceDigest(text) });
|
||||
const { text, identity } = readWithIdentity(path);
|
||||
const digest = frontendSourceDigest(text);
|
||||
record(
|
||||
identity === null ? { op: "file", path, digest } : { op: "file", path, digest, identity },
|
||||
);
|
||||
return text;
|
||||
} catch {
|
||||
record({ op: "read-error", path });
|
||||
@@ -337,6 +371,9 @@ export function frontendInputsStillMatch(
|
||||
}
|
||||
switch (probe.op) {
|
||||
case "file": {
|
||||
// An unchanged identity stands for the content hashed with it.
|
||||
if (probe.identity !== undefined && fileIdentity(probe.path) === probe.identity)
|
||||
return true;
|
||||
try {
|
||||
return frontendSourceDigest(readFileSync(probe.path, "utf8")) === probe.digest;
|
||||
} catch {
|
||||
@@ -443,6 +480,11 @@ export function frontendInputsSemanticallyMatch(
|
||||
probes.push(probe);
|
||||
continue;
|
||||
}
|
||||
if (probe.identity !== undefined && fileIdentity(probe.path) === probe.identity) {
|
||||
currentSources.set(probe.path, previous);
|
||||
probes.push(probe);
|
||||
continue;
|
||||
}
|
||||
let current: string;
|
||||
try {
|
||||
current = readFileSync(probe.path, "utf8");
|
||||
@@ -460,7 +502,7 @@ export function frontendInputsSemanticallyMatch(
|
||||
if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current))
|
||||
return null;
|
||||
changed.push({ path: probe.path, previous, current });
|
||||
probes.push({ ...probe, digest: currentDigest });
|
||||
probes.push({ op: "file", path: probe.path, digest: currentDigest });
|
||||
}
|
||||
const adjusted: FrontendInputSnapshot = { ...snapshot, probes };
|
||||
if (!frontendInputsStillMatch(adjusted, exclusions)) return null;
|
||||
@@ -488,7 +530,11 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front
|
||||
if (typeof value.path !== "string" || typeof value.op !== "string") return false;
|
||||
switch (value.op) {
|
||||
case "file":
|
||||
return typeof value.digest === "string" && /^[0-9a-f]{64}$/.test(value.digest);
|
||||
return (
|
||||
typeof value.digest === "string" &&
|
||||
/^[0-9a-f]{64}$/.test(value.digest) &&
|
||||
(value.identity === undefined || typeof value.identity === "string")
|
||||
);
|
||||
case "read-error":
|
||||
return true;
|
||||
case "kind":
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DYN,
|
||||
DYN_HANDLE_KINDS,
|
||||
F64,
|
||||
STRING,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
@@ -285,6 +286,43 @@ export function lowerAbstractEquality(
|
||||
};
|
||||
return negated ? { kind: "unary", op: "!", operand: result, type: BOOL, loc } : result;
|
||||
}
|
||||
// Two Dates are equal when they are the same object, as with ===.
|
||||
if (left.type.kind === "date" && right.type.kind === "date")
|
||||
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
// A Date and a string or number compare through the Date's default
|
||||
// primitive, its toString() text.
|
||||
if (left.type.kind === "date" || right.type.kind === "date") {
|
||||
const otherType = left.type.kind === "date" ? right.type : left.type;
|
||||
if (otherType.kind === "string" || otherType.kind === "f64") {
|
||||
// Both operands evaluate before the Date converts.
|
||||
const l = lowerer.declareHiddenLocal("%looseA", left.type);
|
||||
const r = lowerer.declareHiddenLocal("%looseB", right.type);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: l.id, init: left, loc },
|
||||
{ kind: "varDecl", localId: r.id, init: right, loc },
|
||||
];
|
||||
const primitive = (value: IrExpr): IrExpr => {
|
||||
if (value.type.kind !== "date") return value;
|
||||
const text: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "date.toString",
|
||||
args: [value],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return otherType.kind === "string"
|
||||
? text
|
||||
: { kind: "libCall", fn: "num.fromString", args: [text], type: F64, loc };
|
||||
};
|
||||
const a = primitive(varRef(l.id, left.type, loc));
|
||||
const b = primitive(varRef(r.id, right.type, loc));
|
||||
const result: IrExpr =
|
||||
otherType.kind === "string"
|
||||
? { kind: "strEq", negated, left: a, right: b, type: BOOL, loc }
|
||||
: { kind: "bin", op: negated ? "!==" : "===", left: a, right: b, type: BOOL, loc };
|
||||
return { kind: "seqExpr", stmts, result, type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
if (!supportsPair(lowerer, left.type, right.type)) return null;
|
||||
if (left.kind === "strLit" && right.kind === "strLit") {
|
||||
return { kind: "boolLit", value: (left.value === right.value) !== negated, type: BOOL, loc };
|
||||
|
||||
@@ -2,7 +2,13 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { type ClassInfo } from "../lower-classes.js";
|
||||
import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js";
|
||||
import {
|
||||
DATE_METHOD_HINT,
|
||||
dateJsonType,
|
||||
dateSetterTime,
|
||||
dateValueMethod,
|
||||
isDateSetter,
|
||||
} from "./dates.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -248,6 +254,22 @@ export function lowerInheritedDateMethodCall(
|
||||
type: result.type,
|
||||
loc,
|
||||
});
|
||||
if (isDateSetter(name) && name !== "setTime") {
|
||||
// The field setters change the Date the class's time slot holds once
|
||||
// the arguments are evaluated: an argument that calls setTime on the
|
||||
// instance gives the slot a new Date.
|
||||
const date = lowerer.declareHiddenLocal("%dateTime", DATE_T);
|
||||
const current = varRef(date.id, DATE_T, loc);
|
||||
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc };
|
||||
const next = dateSetterTime(lowerer, call, name, stmts, time);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: date.id,
|
||||
init: timeOf(lowerer, value, root, loc),
|
||||
loc,
|
||||
});
|
||||
return done({ kind: "libCall", fn: "date.store", args: [current, next], type: F64, loc });
|
||||
}
|
||||
if (name === "setTime") {
|
||||
// Every argument is evaluated before setTime converts the first one.
|
||||
let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc };
|
||||
|
||||
@@ -2,12 +2,15 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { lowerToNumberArguments } from "../lower-exprs.js";
|
||||
import { programDeclaresDateClass } from "./date-classes.js";
|
||||
import {
|
||||
BOOL,
|
||||
DATE_T,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
NULL_T,
|
||||
@@ -36,6 +39,32 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
|
||||
getTimezoneOffset: "date.getTimezoneOffset",
|
||||
};
|
||||
|
||||
/** The setters that change calendar fields: the field code
|
||||
* scr_date_set_fields takes (scr_number.h SCR_DATE_SET_*; 8 marks the UTC
|
||||
* forms) and the most arguments each takes. */
|
||||
const DATE_FIELD_SETTERS: ReadonlyMap<string, { code: number; max: number }> = new Map([
|
||||
["setMilliseconds", { code: 0, max: 1 }],
|
||||
["setSeconds", { code: 1, max: 2 }],
|
||||
["setMinutes", { code: 2, max: 3 }],
|
||||
["setHours", { code: 3, max: 4 }],
|
||||
["setDate", { code: 4, max: 1 }],
|
||||
["setMonth", { code: 5, max: 2 }],
|
||||
["setFullYear", { code: 6, max: 3 }],
|
||||
["setUTCMilliseconds", { code: 8, max: 1 }],
|
||||
["setUTCSeconds", { code: 9, max: 2 }],
|
||||
["setUTCMinutes", { code: 10, max: 3 }],
|
||||
["setUTCHours", { code: 11, max: 4 }],
|
||||
["setUTCDate", { code: 12, max: 1 }],
|
||||
["setUTCMonth", { code: 13, max: 2 }],
|
||||
["setUTCFullYear", { code: 14, max: 3 }],
|
||||
]);
|
||||
|
||||
/** Whether `name` is a Date setter scriptc lowers: setTime or a field
|
||||
* setter. */
|
||||
export function isDateSetter(name: string): boolean {
|
||||
return name === "setTime" || DATE_FIELD_SETTERS.has(name);
|
||||
}
|
||||
|
||||
const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
|
||||
["toString", "date.toString"],
|
||||
["toDateString", "date.toDateString"],
|
||||
@@ -43,11 +72,11 @@ const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
|
||||
]);
|
||||
|
||||
export const DATE_METHOD_HINT =
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering";
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering";
|
||||
|
||||
/** The Date slice: statics plus read-only Date values backed by one
|
||||
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
|
||||
* identity and mutating methods remain fenced. */
|
||||
/** Date statics and methods on Date objects (a TimeClip'd millisecond
|
||||
* time value with identity): getters, text forms and setters.
|
||||
* Construction lives in lowerNew. */
|
||||
export function lowerDateCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -67,16 +96,18 @@ export function lowerDateCall(
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
// A Date a JavaScript file constructs is a mutable object; setTime on
|
||||
// it takes the dynamic method call. So does every Date method on a
|
||||
// A Date a JavaScript file constructs is a checked-dynamic value; a
|
||||
// setter on it takes the dynamic method call, which changes the Date the
|
||||
// handle holds. So does every Date method on a
|
||||
// checked-dynamic receiver when the program declares a class that extends
|
||||
// Date: the value can be an instance, whose class may declare the method.
|
||||
if (
|
||||
(name === "setTime" || programDeclaresDateClass(lowerer)) &&
|
||||
(isDateSetter(name) || programDeclaresDateClass(lowerer)) &&
|
||||
tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn"
|
||||
)
|
||||
return null;
|
||||
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
|
||||
if (isDateSetter(name)) return lowerDateSetter(lowerer, call, access, name);
|
||||
if (call.arguments.nodes.length !== 0) {
|
||||
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
|
||||
}
|
||||
@@ -253,3 +284,151 @@ export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean {
|
||||
lowerer.isStdlibMember(access)
|
||||
);
|
||||
}
|
||||
|
||||
/** `d.setTime(t)` and the field setters (`d.setHours(h, m?, s?, ms?)` ...)
|
||||
* on a Date object: the receiver, then the arguments in order, then the
|
||||
* new time value (scr_date_set_fields over the time value the Date holds
|
||||
* once the arguments have run) stored into the Date and answered. Missing
|
||||
* optional arguments keep their fields; arguments past a setter's last
|
||||
* one are evaluated and ignored. A Date local that holds its time value
|
||||
* (date-values.ts) takes the new time value itself. */
|
||||
function lowerDateSetter(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
name: string,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const nodes = call.arguments.nodes;
|
||||
if (nodes.some((node) => ts.isSpreadElement(node)))
|
||||
lowerer.noLowering(`Date.prototype.${name} with spread arguments`, call, DATE_METHOD_HINT);
|
||||
const timeLocal = timeValueLocalOf(lowerer, access.expression);
|
||||
const stmts: IrStmt[] = [];
|
||||
let receiver: IrExpr | null = null;
|
||||
if (timeLocal === null) {
|
||||
// A checked-dynamic receiver takes the dynamic method call
|
||||
// (lowerDateCall), so the value here is a Date object.
|
||||
const value = lowerer.lowerExpr(access.expression);
|
||||
if (value.type.kind !== "date")
|
||||
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
|
||||
const date = lowerer.declareHiddenLocal("%date", DATE_T);
|
||||
stmts.push({ kind: "varDecl", localId: date.id, init: value, loc });
|
||||
receiver = { kind: "varRef", localId: date.id, type: DATE_T, loc };
|
||||
}
|
||||
const time: IrExpr =
|
||||
receiver === null
|
||||
? { kind: "varRef", localId: timeLocal!.id, type: F64, loc }
|
||||
: { kind: "libCall", fn: "date.getTime", args: [receiver], type: F64, loc };
|
||||
const next = dateSetterTime(lowerer, call, name, stmts, time);
|
||||
if (receiver === null) {
|
||||
stmts.push({ kind: "assign", localId: timeLocal!.id, value: next, loc });
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "varRef", localId: timeLocal!.id, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "libCall", fn: "date.store", args: [receiver, next], type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The new time value of setter `name` called with `call`'s arguments on a
|
||||
* Date whose time value `time` reads: the arguments evaluate into
|
||||
* temporaries appended to `stmts` (ToNumber of each, an undefined one is
|
||||
* NaN), and the result reads `time` after them. A missing first argument
|
||||
* is NaN; missing optional arguments keep their fields; arguments past a
|
||||
* setter's last one are evaluated and ignored. */
|
||||
export function dateSetterTime(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
stmts: IrStmt[],
|
||||
time: IrExpr,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const converted = lowerToNumberArguments(
|
||||
lowerer,
|
||||
call.arguments.nodes as readonly ts.Expression[],
|
||||
);
|
||||
stmts.push(...converted.stmts);
|
||||
const args: IrExpr[] = converted.values.map((value) => {
|
||||
if (value.kind === "numLit") return value;
|
||||
const arg = lowerer.declareHiddenLocal("%dateArg", F64);
|
||||
stmts.push({ kind: "varDecl", localId: arg.id, init: value, loc });
|
||||
return { kind: "varRef", localId: arg.id, type: F64, loc };
|
||||
});
|
||||
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
if (name === "setTime")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "date.timeClip",
|
||||
args: [args[0] ?? number(Number.NaN)],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const setter = DATE_FIELD_SETTERS.get(name)!;
|
||||
// The first argument is required: a missing one is undefined, so NaN.
|
||||
const used = args.length === 0 ? [number(Number.NaN)] : args.slice(0, setter.max);
|
||||
const operands = [...used];
|
||||
while (operands.length < 4) operands.push(number(0));
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "date.setFields",
|
||||
args: [time, number(setter.code), number(used.length), ...operands],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The local a Date binding that holds its time value lowers to, for an
|
||||
* identifier `node` that names one (date-values.ts); null otherwise. */
|
||||
function timeValueLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal | null {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (!ts.isIdentifier(node)) return null;
|
||||
const local = lowerer.resolveLocal(node);
|
||||
return local !== undefined && local !== null && lowerer.timeValueDateLocals.has(local)
|
||||
? local
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `d[Symbol.toPrimitive](hint)` on a Date with a literal hint: "number"
|
||||
* answers the time value, "string" and "default" the toString() text (a
|
||||
* Date's default hint is a string). Null for other calls. */
|
||||
export function lowerDateToPrimitiveCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.ElementAccessExpression,
|
||||
): IrExpr | null {
|
||||
const key = access.argumentExpression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
key.name.text !== "toPrimitive" ||
|
||||
!ts.isIdentifier(key.expression) ||
|
||||
!lowerer.isStdlibGlobal(key.expression, "Symbol") ||
|
||||
lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date"
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(call);
|
||||
const hint = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null;
|
||||
const text = hint !== null && ts.isStringLiteralLike(hint) ? hint.text : null;
|
||||
if (text !== "number" && text !== "string" && text !== "default")
|
||||
return lowerer.noLowering(
|
||||
'Date.prototype[Symbol.toPrimitive] without a "number", "string" or "default" literal hint',
|
||||
call,
|
||||
);
|
||||
const raw = lowerer.lowerExpr(access.expression);
|
||||
const date: IrExpr =
|
||||
raw.type.kind === "dyn"
|
||||
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: DATE_T, loc }
|
||||
: raw;
|
||||
return text === "number"
|
||||
? { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc }
|
||||
: { kind: "libCall", fn: "date.toString", args: [date], type: STRING, loc };
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
import {
|
||||
classPrototypeData,
|
||||
hasClassPrototypeData,
|
||||
prototypeReflectedFlag,
|
||||
observedPrototypeMethod,
|
||||
reflectClassPrototype,
|
||||
} from "./class-prototypes.js";
|
||||
@@ -4179,6 +4180,103 @@ export class ClassDynamicDispatch {
|
||||
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** `%dyn.class.bagHasOwn`: whether an instance's property bag holds `key`
|
||||
* as its own property. False before the instance has a bag. */
|
||||
private readBagHasOwn(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const name = "%dyn.class.bagHasOwn";
|
||||
let helper = lowerer.liftedFnNamed(name);
|
||||
if (!helper) {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
helper = {
|
||||
name,
|
||||
params: [
|
||||
{ localId: "p.0", name: "bag", type: DYN },
|
||||
{ localId: "p.1", name: "key", type: STRING },
|
||||
],
|
||||
returnType: BOOL,
|
||||
locals: [
|
||||
{ id: "p.0", name: "bag", type: DYN, mutable: false },
|
||||
{ id: "p.1", name: "key", type: STRING, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
|
||||
then: [
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.hasOwn",
|
||||
args: [value, varRef("p.1", STRING, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push(helper);
|
||||
}
|
||||
this.generated.add(helper);
|
||||
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** The `constructor` an instance of `info` inherits when it has no own
|
||||
* one. The class's prototype view answers its own `constructor` (a
|
||||
* replacement, or the class once reflection installed it). A view
|
||||
* without one answers the class itself unless reflection had populated it
|
||||
* (prototypeReflectedFlag, set when the view initializes): then the
|
||||
* program deleted the property, and the read continues to the base
|
||||
* class's prototype and past the last one to Object.prototype, as in
|
||||
* Node. A prototype's own `constructor` is read with the instance
|
||||
* `receiver` as `this`, so an accessor's getter runs on the instance. */
|
||||
private inheritedConstructor(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
key: IrExpr,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN);
|
||||
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null;
|
||||
if (!prototype) return cls;
|
||||
const base = info.base;
|
||||
const read = (): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectGet",
|
||||
args: [prototype, lowerer.coerceToExpected(key, DYN), receiver],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
// Past the last class prototype, the read continues to Object.prototype's data property.
|
||||
const next: IrExpr =
|
||||
base && !base.builtinError && base.decl && !base.generic && !base.localClass
|
||||
? this.inheritedConstructor(lowerer, base, key, receiver, loc)
|
||||
: { kind: "dynKeyGet", value: prototype, key, type: DYN, loc };
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc },
|
||||
then: read(),
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: varRef(prototypeReflectedFlag(lowerer, info), BOOL, loc),
|
||||
then: next,
|
||||
else_: cls,
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The hints for which some boxed class declares a conversion method. A
|
||||
* string conversion calls toString first, so without a declared toString
|
||||
* or Symbol.toPrimitive it answers Object.prototype.toString's
|
||||
@@ -5937,15 +6035,19 @@ export class ClassDynamicDispatch {
|
||||
!info.classDecorators
|
||||
) {
|
||||
this.constructors.set(info.def.name, info);
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
// `constructor` is an own property of every class prototype, so an
|
||||
// ancestor's prototype answers for an instance of this class only
|
||||
// after the program deletes the class's own: the instance's own
|
||||
// property answers, then the class's prototype (inheritedConstructor).
|
||||
const answered = this.readBagHasOwn(lowerer, bag, key, loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
cond: answered,
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
@@ -5953,7 +6055,13 @@ export class ClassDynamicDispatch {
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: cls,
|
||||
else_: this.inheritedConstructor(
|
||||
lowerer,
|
||||
info,
|
||||
key,
|
||||
lowerer.coerceToExpected(receiver, DYN),
|
||||
loc,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import {
|
||||
classMemberNameOf,
|
||||
classValueRef,
|
||||
@@ -299,6 +299,23 @@ function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The global that is true once the class's prototype view has been
|
||||
* populated by reflection (reflectClassPrototype): the view then holds an
|
||||
* own `constructor` unless the program deleted it. A view that was never
|
||||
* reflected leaves `constructor` implicit. */
|
||||
export function prototypeReflectedFlag(lowerer: Lowerer, info: ClassInfo): string {
|
||||
if (info.prototypeReflectedFlag) return info.prototypeReflectedFlag;
|
||||
const id = `%g.%prototype.data.${info.def.name}.reflected`;
|
||||
lowerer.globalsList.push({
|
||||
id,
|
||||
name: `${info.def.jsName ?? info.def.name}.prototype.reflected`,
|
||||
type: BOOL,
|
||||
mutable: true,
|
||||
});
|
||||
info.prototypeReflectedFlag = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Populate the shared prototype view before exposing it to reflection.
|
||||
* Methods and accessors are non-enumerable and retain declaration identity. */
|
||||
export function reflectClassPrototype(
|
||||
@@ -399,6 +416,12 @@ export function reflectClassPrototype(
|
||||
{ key: "value", value: classValueRef(lowerer, info, info.decl) },
|
||||
writable,
|
||||
]),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: prototypeReflectedFlag(lowerer, info),
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
lowerer.fnStack.push(context);
|
||||
@@ -503,6 +526,69 @@ export function isErrorPrototypeValue(
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a bare `C.prototype` read becomes the prototype of another object
|
||||
* or is only compared or inspected: the argument of `Object.create` or
|
||||
* `Object.getPrototypeOf` / `Reflect.getPrototypeOf`, the prototype argument
|
||||
* of `Object.setPrototypeOf` / `Reflect.setPrototypeOf`, or an operand of
|
||||
* `===` / `!==`. In these positions the complete view of the
|
||||
* prototype (reflectClassPrototype) answers. Objects that inherit from it
|
||||
* run the class's methods with themselves as the receiver, which the
|
||||
* methods' receiver-independent lowering (reflectedClassMethodValue)
|
||||
* supports. Other positions, such as `Object.setPrototypeOf(C.prototype, X)`,
|
||||
* which would relink the class, keep the diagnostic. */
|
||||
function prototypeViewPosition(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
node = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) {
|
||||
const args = parent.arguments.nodes;
|
||||
const objectMethod = lowerer.stdlibGlobalMember(parent.expression, "Object");
|
||||
const reflectMethod = lowerer.stdlibGlobalMember(parent.expression, "Reflect");
|
||||
if (
|
||||
objectMethod === "create" ||
|
||||
objectMethod === "getPrototypeOf" ||
|
||||
reflectMethod === "getPrototypeOf"
|
||||
)
|
||||
return args[0] === node;
|
||||
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
|
||||
return args[1] === node;
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)
|
||||
);
|
||||
}
|
||||
|
||||
/** A module-level JavaScript class named directly (not a class value that
|
||||
* may hold a subclass at run time): its prototype has a complete view whose
|
||||
* methods accept any receiver. */
|
||||
function viewableClass(lowerer: Lowerer, info: ClassInfo, receiver: ts.Expression): boolean {
|
||||
return (
|
||||
info.decl !== null &&
|
||||
info.localClass === undefined &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
|
||||
exactClassOfReceiver(lowerer, receiver) === info
|
||||
);
|
||||
}
|
||||
|
||||
/** The complete prototype view of the class `node` names, when it is a
|
||||
* viewable class (util.inherits's superCtor); null otherwise. */
|
||||
export function classPrototypeView(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const info = exactClassOfReceiver(lowerer, node);
|
||||
if (!info || !viewableClass(lowerer, info, node)) return null;
|
||||
return reflectClassPrototype(lowerer, info, loc, classValueRef(lowerer, info, node));
|
||||
}
|
||||
|
||||
/** Named data and method slots have native descriptors. Bare reflection and
|
||||
* accessor replacement still require a complete view of the prototype. */
|
||||
export function lowerClassPrototypeData(
|
||||
@@ -566,6 +652,12 @@ export function lowerClassPrototypeData(
|
||||
});
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), receiver);
|
||||
}
|
||||
if (
|
||||
name === null &&
|
||||
prototypeViewPosition(lowerer, expr) &&
|
||||
viewableClass(lowerer, info, expr.expression)
|
||||
)
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr));
|
||||
if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { isDateSetter } from "./builtins/dates.js";
|
||||
|
||||
/** Date methods whose receiver is only read for its time value: a Date
|
||||
* local used as their receiver can store the time value instead of a Date
|
||||
@@ -41,12 +42,15 @@ const RELATIONAL = new Set([
|
||||
|
||||
/** True when a local `const` (or a `let` that is never assigned again)
|
||||
* initialized with `new Date(...)` can keep the Date's time value instead
|
||||
* of a Date object: no use observes the Date's identity. Every use reads the time value as
|
||||
* a Date method receiver, a String() or template operand, a relational or
|
||||
* string `+` operand, or `new Date(d)`'s argument, inside the declaring
|
||||
* function, or inside the block or loop that scopes a module-level
|
||||
* binding (no closure captures it). Top-level module bindings keep the
|
||||
* object. Other uses box the time value into a new Date at the use. */
|
||||
* of a Date object: no use observes the Date's identity. Every use reads
|
||||
* the time value as a Date method receiver (a setter replaces it; the
|
||||
* arguments of a method that reads it call no setter on it), a
|
||||
* String(), Number() or template operand, a unary + or - operand, a
|
||||
* relational or string `+` operand, or `new Date(d)`'s argument, inside
|
||||
* the declaring function, or inside the block or loop that scopes a
|
||||
* module-level binding (no closure captures it). Top-level module bindings
|
||||
* keep the object. Other uses box the time value into a new Date at the
|
||||
* use. */
|
||||
export function isTimeValueDateDeclaration(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
@@ -94,17 +98,21 @@ function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): bo
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (!symbol) return false;
|
||||
let ok = true;
|
||||
let sets = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (!ok) return;
|
||||
if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) {
|
||||
if (lowerer.checker.getSymbolAtLocation(node) === symbol) {
|
||||
if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false;
|
||||
const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node, symbol) : null;
|
||||
if (use === null) ok = false;
|
||||
else if (use === "set") sets = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(scope);
|
||||
if (ok && sets) lowerer.timeValueDateWrites.add(decl);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -164,29 +172,75 @@ function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A use of the binding that reads only its time value. */
|
||||
function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
/** How a use of the binding uses its time value: "read" for a use that
|
||||
* reads only the time value, "set" for a setter call that replaces it, or
|
||||
* null for any other use. */
|
||||
function timeValueUse(
|
||||
lowerer: Lowerer,
|
||||
id: ts.Identifier,
|
||||
symbol: ts.Symbol,
|
||||
): "read" | "set" | null {
|
||||
let node: ts.Node = id;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (parent === undefined) return null;
|
||||
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
|
||||
const call = parent.parent;
|
||||
return (
|
||||
!parent.questionDotToken &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === parent &&
|
||||
!call.questionDotToken &&
|
||||
TIME_VALUE_METHODS.has(parent.name.text) &&
|
||||
lowerer.isStdlibMember(parent)
|
||||
);
|
||||
if (
|
||||
parent.questionDotToken ||
|
||||
!ts.isCallExpression(call) ||
|
||||
call.expression !== parent ||
|
||||
call.questionDotToken ||
|
||||
!lowerer.isStdlibMember(parent)
|
||||
)
|
||||
return null;
|
||||
if (isDateSetter(parent.name.text)) return "set";
|
||||
// The method reads the time value after its arguments are evaluated:
|
||||
// a setter there (`d.toJSON(d.setTime(0))`) changes what it reads.
|
||||
if (call.arguments.nodes.some((arg) => callsSetterOf(lowerer, arg, symbol))) return null;
|
||||
return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null;
|
||||
}
|
||||
return readsTimeValue(lowerer, node, parent, symbol) ? "read" : null;
|
||||
}
|
||||
|
||||
/** True when `node` calls a setter on the binding `symbol` names. */
|
||||
function callsSetterOf(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean {
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
isDateSetter(node.expression.name.text)
|
||||
) {
|
||||
let receiver = node.expression.expression;
|
||||
while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression;
|
||||
if (ts.isIdentifier(receiver) && lowerer.checker.getSymbolAtLocation(receiver) === symbol)
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
ts.forEachChild(node, (child) => callsSetterOf(lowerer, child, symbol) || undefined) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
/** A use of the binding, other than a method call, that reads only its
|
||||
* time value. */
|
||||
function readsTimeValue(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
parent: ts.Node,
|
||||
symbol: ts.Symbol,
|
||||
): boolean {
|
||||
if (ts.isTemplateSpan(parent)) return parent.expression === node;
|
||||
// Unary + and -, the number hint.
|
||||
if (ts.isPrefixUnaryExpression(parent))
|
||||
return (
|
||||
parent.operator === ts.SyntaxKind.PlusToken || parent.operator === ts.SyntaxKind.MinusToken
|
||||
);
|
||||
if (ts.isCallExpression(parent)) {
|
||||
return (
|
||||
parent.arguments.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === node &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "String")
|
||||
(lowerer.isStdlibGlobal(parent.expression, "String") ||
|
||||
lowerer.isStdlibGlobal(parent.expression, "Number"))
|
||||
);
|
||||
}
|
||||
if (ts.isNewExpression(parent)) {
|
||||
@@ -198,6 +252,9 @@ function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
// A left operand converts after the right one is evaluated: a setter
|
||||
// there changes the time value the comparison or concatenation reads.
|
||||
if (parent.left === node && callsSetterOf(lowerer, parent.right, symbol)) return false;
|
||||
const other = parent.left === node ? parent.right : parent.left;
|
||||
const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind;
|
||||
if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date";
|
||||
|
||||
@@ -1606,6 +1606,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A Date against an untyped value: the same Date object, whose
|
||||
// checked-dynamic handle the untyped value would hold.
|
||||
if (dynSide.type.kind === "dyn" && scalarSide.type.kind === "date") {
|
||||
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: scalarSide === left ? boxed : left,
|
||||
right: scalarSide === right ? boxed : right,
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
dynSide.type.kind === "dyn" &&
|
||||
(scalarSide.type.kind === "f64" ||
|
||||
@@ -2030,6 +2043,28 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
if (left.type.kind === "string" || right.type.kind === "string") {
|
||||
// A Date on the left converts after the right operand is
|
||||
// evaluated, which can run its setters.
|
||||
if (left.type.kind === "date" && !isSafeToRepeat(right)) {
|
||||
const date = lowerer.declareHiddenLocal("%plus", left.type);
|
||||
const other = lowerer.declareHiddenLocal("%plus", right.type);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: date.id, init: left, loc },
|
||||
{ kind: "varDecl", localId: other.id, init: right, loc },
|
||||
],
|
||||
result: {
|
||||
kind: "strConcat",
|
||||
left: concatOperand(lowerer, varRef(date.id, left.type, loc), expr.left),
|
||||
right: concatOperand(lowerer, varRef(other.id, right.type, loc), expr.right),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "strConcat",
|
||||
left: concatOperand(lowerer, left, expr.left),
|
||||
|
||||
@@ -117,6 +117,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../.
|
||||
import { typedArrayLacksMember } from "../builtins/typed-array-members.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { DATE_METHOD_HINT } from "../builtins/dates.js";
|
||||
import {
|
||||
cjsClassExprWholeExportOf,
|
||||
cjsExportAssignmentOf,
|
||||
@@ -2062,6 +2063,9 @@ export function lowerIntrinsicProperty(
|
||||
);
|
||||
}
|
||||
if (kind === "date") {
|
||||
// A Date held as a checked-dynamic value (a Date a JavaScript file
|
||||
// constructs) answers its method values at run time.
|
||||
if (tryLowerExpression(lowerer, expr.expression)?.type.kind === "dyn") return null;
|
||||
const methods = new Set([
|
||||
"getTime",
|
||||
"valueOf",
|
||||
@@ -2090,7 +2094,7 @@ export function lowerIntrinsicProperty(
|
||||
lowerer.noLowering(
|
||||
`Date.prototype.${name}`,
|
||||
expr,
|
||||
"getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering",
|
||||
DATE_METHOD_HINT,
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ const NO_CLASS_INSTANCE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"nullT",
|
||||
]);
|
||||
|
||||
const PRIMITIVE_WRAPPERS = ["Boolean", "Number", "String", "BigInt", "Symbol"] as const;
|
||||
|
||||
/** `x instanceof C` for a program-declared class C. When x's static
|
||||
* class and C are both in extends-hierarchies the test is dynamic — the
|
||||
* O(1) preorder-interval check against the vtable (`instanceOf` node).
|
||||
@@ -190,6 +192,20 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// The primitive wrapper constructors: a compiled program never creates a
|
||||
// boxed primitive (`new Boolean(...)` and friends have no lowering), and
|
||||
// a primitive is never an instance, so the test is false for every value
|
||||
// (typebox's IsBoolean/IsNumber/IsString guards). The operand still runs.
|
||||
if (PRIMITIVE_WRAPPERS.some((name) => lowerer.isStdlibGlobal(expr.right, name))) {
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: value, loc }],
|
||||
result: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.right, "FinalizationRegistry")) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return { kind: "libCall", fn: "finalizationRegistry.is", args: [value], type: BOOL, loc };
|
||||
@@ -291,6 +307,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (!target) {
|
||||
const date = lowerer.isStdlibGlobal(expr.right, "Date");
|
||||
const url = lowerer.isStdlibGlobal(expr.right, "URL");
|
||||
// A catch binding holds a thrown Date as its checked-dynamic handle.
|
||||
const caughtDate = date ? lowerer.caughtLocalOf(expr.left) : null;
|
||||
if (caughtDate)
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.nativeDateIs",
|
||||
args: [{ kind: "caughtToDyn", value: varRef(caughtDate.id, CAUGHT, loc), type: DYN, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if ((date || url) && !lowerer.caughtLocalOf(expr.left)) {
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
if (value.type.kind === "dyn")
|
||||
@@ -1905,6 +1931,30 @@ export function lowerInExpression(
|
||||
};
|
||||
}
|
||||
}
|
||||
// A child process has `send`, `disconnect` and `channel` only when it was
|
||||
// created with an IPC channel (fork, or an "ipc" stdio entry), and keeps
|
||||
// them after the channel closes: the runtime answers from the child.
|
||||
if (recv.type.kind === "child" && CHILD_CHANNEL_MEMBERS.has(key))
|
||||
return { kind: "libCall", fn: "child.hasChannel", args: [recv], type: BOOL, loc };
|
||||
// A native Node handle (an http.Server, a socket, a child process...):
|
||||
// a member Node's declarations give the class is on its prototype, so
|
||||
// `"closeAllConnections" in server` answers true. Undeclared names can
|
||||
// still be real (internal `_`-prefixed fields), so they keep the fence.
|
||||
if (
|
||||
NATIVE_HANDLE_KINDS.has(recv.type.kind) &&
|
||||
nodeDeclaredMember(lowerer, lowerer.typeOf(expr.right), key)
|
||||
) {
|
||||
const answer: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
return isSafeToDiscard(recv)
|
||||
? answer
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: recv, loc }],
|
||||
result: answer,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (recv.type.kind !== "record") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2181,24 +2231,43 @@ function lowerRuntimeKeyIn(
|
||||
return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** Standard-library constructors the untyped instanceof helper has no
|
||||
* instance test for: an untyped `x instanceof Object` always refuses. */
|
||||
const DYN_INSTANCEOF_REFUSED: ReadonlySet<string> = new Set(["Object", "Function"]);
|
||||
|
||||
/** The SC1090 message `x instanceof C` throws, with the site's position,
|
||||
* when `C` is a runtime constructor with no instance test. A right-hand
|
||||
* side that names one of DYN_INSTANCEOF_REFUSED always reaches that
|
||||
* refusal, so the site is listed with the runtime fences. */
|
||||
* when `C` is a runtime constructor with no instance test. */
|
||||
function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string {
|
||||
const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor");
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr.right);
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
lowerer.isStdlibSymbol(symbol) &&
|
||||
DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol))
|
||||
)
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const sf = ts.getSourceFileOfNode(expr);
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1;
|
||||
return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`;
|
||||
}
|
||||
|
||||
/** IR kinds of native Node handles whose members come from Node's own
|
||||
* declarations. */
|
||||
const NATIVE_HANDLE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"netServer",
|
||||
"netSocket",
|
||||
"httpReq",
|
||||
"httpRes",
|
||||
"httpClientReq",
|
||||
"child",
|
||||
"dgramSocket",
|
||||
"fsWatcher",
|
||||
"http2Session",
|
||||
"http2Stream",
|
||||
]);
|
||||
|
||||
/** Child process members that exist only on a child with an IPC channel. */
|
||||
const CHILD_CHANNEL_MEMBERS: ReadonlySet<string> = new Set(["send", "disconnect", "channel"]);
|
||||
|
||||
/** Whether every constituent of `t` declares a required member `key` in
|
||||
* a declaration file (Node's @types/node). */
|
||||
function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean {
|
||||
const parts = t.isUnionType() ? ts.constituentTypes(t) : [t];
|
||||
return parts.every((part) => {
|
||||
const prop = lowerer.checker.getPropertyOfType(part, key);
|
||||
if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) !== 0) return false;
|
||||
return (
|
||||
prop.declarations.length > 0 &&
|
||||
prop.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { isTemporalGlobalRoot } from "../lower-namespaces.js";
|
||||
import { isF64HandleTyped } from "../f64-handles.js";
|
||||
|
||||
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
@@ -281,13 +282,18 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
promise: "object",
|
||||
bytes: "object",
|
||||
regex: "object",
|
||||
date: "object",
|
||||
generator: "object",
|
||||
classval: "function",
|
||||
moduleNs: "object",
|
||||
undefinedT: "undefined",
|
||||
nullT: "object",
|
||||
};
|
||||
const folded = FOLD[operand.type.kind];
|
||||
// An f64 handle (a readline Interface, a Timeout) is an object.
|
||||
const folded =
|
||||
operand.type.kind === "f64" && isF64HandleTyped(lowerer, expr.expression)
|
||||
? "object"
|
||||
: FOLD[operand.type.kind];
|
||||
if (folded) {
|
||||
const result: IrExpr = { kind: "strLit", value: folded, type: STRING, loc };
|
||||
return isSafeToDiscard(operand)
|
||||
|
||||
@@ -120,6 +120,11 @@ function coercedUnion(lowerer: Lowerer, type: IrType): boolean {
|
||||
return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm));
|
||||
}
|
||||
|
||||
/** ToNumber of a Date: its time value (valueOf, the number hint). */
|
||||
function dateTimeValue(date: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc };
|
||||
}
|
||||
|
||||
/** ToNumber of a boolean: 1 for true, 0 for false. */
|
||||
function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
@@ -145,23 +150,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
const operand =
|
||||
raw.type.kind === "bool"
|
||||
? booleanToNumber(raw, loc)
|
||||
: coercedUnion(lowerer, raw.type)
|
||||
? ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [{ kind: "dynFrom", value: raw, type: DYN, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
} satisfies IrExpr)
|
||||
: raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
: raw.type.kind === "date"
|
||||
? dateTimeValue(raw, loc)
|
||||
: coercedUnion(lowerer, raw.type)
|
||||
? ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [raw],
|
||||
args: [{ kind: "dynFrom", value: raw, type: DYN, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
} satisfies IrExpr)
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
: raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
? ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [raw],
|
||||
type: F64,
|
||||
loc,
|
||||
} satisfies IrExpr)
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]);
|
||||
if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc };
|
||||
return { kind: "unary", op: "-", operand, type: F64, loc };
|
||||
@@ -185,6 +192,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
|
||||
}
|
||||
if (raw.type.kind === "bool") return booleanToNumber(raw, loc);
|
||||
// A Date's number hint answers its time value.
|
||||
if (raw.type.kind === "date") return dateTimeValue(raw, loc);
|
||||
const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus");
|
||||
if (classNumber) return classNumber;
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Whether `node`'s value is an f64 handle: an object whose class
|
||||
* type-mapper.ts represents as an f64 id into a runtime registry (a
|
||||
* readline Interface, a Timeout or Immediate, an AsyncLocalStorage, a
|
||||
* diagnostics_channel Channel). Its checker type is an object type that
|
||||
* maps to f64; the Number wrapper interface maps to f64 for primitives. */
|
||||
export function isF64HandleTyped(lowerer: Lowerer, node: ts.Node): boolean {
|
||||
const type = lowerer.checker.getTypeAtLocation(node);
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol !== undefined && ts.symbolName(symbol) === "Number") return false;
|
||||
return lowerer.mapTypeOf(type)?.kind === "f64";
|
||||
}
|
||||
@@ -51,11 +51,15 @@ import {
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
VOID,
|
||||
isDynTypedRefType,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { boolLit, countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { everyExpr } from "../../ir/traverse.js";
|
||||
import { checkedClassInstanceOf } from "./class-construction.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
|
||||
/** node:assert/strict binds the loose NAMES to the strict comparisons —
|
||||
* Node's own aliasing (`strict.equal === assert.strictEqual`). */
|
||||
@@ -73,6 +77,63 @@ function canonicalAssertMember(module: string, member: string): string {
|
||||
return member;
|
||||
}
|
||||
|
||||
/** The assert methods that are function values (scr_assert_method_value). */
|
||||
const ASSERT_FUNCTION_VALUES: ReadonlySet<string> = new Set([
|
||||
"ok",
|
||||
"fail",
|
||||
"equal",
|
||||
"notEqual",
|
||||
"strictEqual",
|
||||
"notStrictEqual",
|
||||
"deepEqual",
|
||||
"notDeepEqual",
|
||||
"deepStrictEqual",
|
||||
"notDeepStrictEqual",
|
||||
"partialDeepStrictEqual",
|
||||
"ifError",
|
||||
]);
|
||||
|
||||
/** The assert method a `const` binding holds (`const ok = assert.ok`,
|
||||
* `const eq = strictEqual`), or null. */
|
||||
export function assertFunctionAliasOf(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): { module: string; member: string } | null {
|
||||
const decl = lowerer.checker.getSymbolAtLocation(ident)?.valueDeclaration;
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0 ||
|
||||
!decl.initializer
|
||||
)
|
||||
return null;
|
||||
let init = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
const bi = ts.isPropertyAccessExpression(init)
|
||||
? lowerer.builtinMemberOf(init)
|
||||
: ts.isIdentifier(init)
|
||||
? lowerer.builtinImportOf(init)
|
||||
: null;
|
||||
if (!bi || (bi.module !== "assert" && bi.module !== "assert/strict")) return null;
|
||||
return ASSERT_FUNCTION_VALUES.has(canonicalAssertMember(bi.module, bi.member)) ? bi : null;
|
||||
}
|
||||
|
||||
/** An assert method read as a value (`emitter.on("x", assert.fail)`,
|
||||
* `const eq = assert.strictEqual`): an untyped function that asserts as
|
||||
* the method does, the same value for every read. Null for other
|
||||
* members. */
|
||||
export function assertFunctionValue(
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module !== "assert" && bi.module !== "assert/strict") return null;
|
||||
const member = canonicalAssertMember(bi.module, bi.member);
|
||||
if (!ASSERT_FUNCTION_VALUES.has(member)) return null;
|
||||
return { kind: "libCall", fn: "assert.methodValue", args: [strLit(member, loc)], type: DYN, loc };
|
||||
}
|
||||
|
||||
/* Node's AssertionError carries actual, expected and operator. Where the
|
||||
* verdict is computed in compiled code, the failure branch records them
|
||||
* (assert.record) before the throwing call, so a passing assertion boxes
|
||||
@@ -166,28 +227,156 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha
|
||||
);
|
||||
}
|
||||
|
||||
/** The message the runtime reads from a registered value
|
||||
* (SCR_ASSERT_VALUE_MESSAGE in scr_assert.h). */
|
||||
const VALUE_MESSAGE = "\u0001scriptc:assert-message-value";
|
||||
|
||||
/** A message argument that is not a string, met while one assertion
|
||||
* lowers (withValueMessage). `fail` marks assert.fail's message, which
|
||||
* Node uses for every value but null and undefined. */
|
||||
let valueMessage: { value: IrExpr; has: string; text: string; fail: boolean } | null = null;
|
||||
|
||||
/** The optional trailing message argument: Node accepts a string or an
|
||||
* Error (throwing the Error itself) — the STRING form lowers; a literal
|
||||
* `undefined` counts as omitted (Node's `message == null` test). Returns
|
||||
* the libCall's (msg, hasMsg) pair — a "" dummy when omitted, so the
|
||||
* runtime can distinguish an omitted message from an empty one. */
|
||||
* runtime can distinguish an omitted message from an empty one. Any other
|
||||
* value that crosses into the checked-dynamic tree (an untyped
|
||||
* `failures.join('')`, an Error) is registered with the runtime at the call
|
||||
* instead (withValueMessage). */
|
||||
function lowerMessageArg(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression | undefined,
|
||||
loc: SrcLoc,
|
||||
fail = false,
|
||||
): { msg: IrExpr; hasMsg: IrExpr } {
|
||||
if (!node || (ts.isIdentifier(node) && node.text === "undefined")) {
|
||||
return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: boolLit(false, loc) };
|
||||
}
|
||||
const msg = lowerer.lowerExpr(node);
|
||||
if (msg.type.kind !== "string") {
|
||||
if (msg.type.kind === "string") return { msg, hasMsg: boolLit(true, loc) };
|
||||
const value =
|
||||
valueMessage === null && (msg.type.kind === "dyn" || lowerer.dynConvertible(msg.type))
|
||||
? lowerer.coerceToExpected(msg, DYN)
|
||||
: null;
|
||||
if (value === null || value.type.kind !== "dyn") {
|
||||
lowerer.noLowering(
|
||||
`assert messages of type '${lowerer.fmt(msg.type)}'`,
|
||||
node,
|
||||
"only string messages lower (Node also accepts an Error to throw — construct and throw it directly)",
|
||||
);
|
||||
}
|
||||
return { msg, hasMsg: boolLit(true, loc) };
|
||||
const has = lowerer.declareHiddenLocal("assert.hasMessage", BOOL);
|
||||
const text = lowerer.declareHiddenLocal("assert.messageText", STRING);
|
||||
valueMessage = { value, has: has.id, text: text.id, fail };
|
||||
return { msg: varRef(text.id, STRING, loc), hasMsg: varRef(has.id, BOOL, loc) };
|
||||
}
|
||||
|
||||
/** Lowers one assertion; a message value met on the way is evaluated after
|
||||
* the assertion's other arguments, as Node evaluates them. A null or
|
||||
* undefined message is omitted (Node's `message == null` test); a truthy
|
||||
* one is registered with the runtime, which the failure reads; any other
|
||||
* value is the empty message, which Node's comparison diffs treat as no
|
||||
* message too (createErrDiff tests the message's truthiness). */
|
||||
function withValueMessage(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
lower: () => IrExpr | null,
|
||||
): IrExpr | null {
|
||||
const outer = valueMessage;
|
||||
valueMessage = null;
|
||||
try {
|
||||
const lowered = lower();
|
||||
const message = valueMessage as {
|
||||
value: IrExpr;
|
||||
has: string;
|
||||
text: string;
|
||||
fail: boolean;
|
||||
} | null;
|
||||
if (lowered === null || message === null) return lowered;
|
||||
// assert.ok's statements evaluate the value first; its call is last.
|
||||
const before = lowered.kind === "seqExpr" ? lowered.stmts : [];
|
||||
const call = lowered.kind === "seqExpr" ? lowered.result : lowered;
|
||||
if (call.kind !== "libCall") {
|
||||
lowerer.noLowering(
|
||||
"assert messages that are not strings in this assertion form",
|
||||
expr,
|
||||
"pass a string message",
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [...before];
|
||||
// An argument computed from the message (assert.fail's generated flag)
|
||||
// reads it after the declarations below; the others are evaluated first.
|
||||
const readsMessage = (arg: IrExpr): boolean =>
|
||||
!everyExpr(arg, {
|
||||
stmt: () => true,
|
||||
expr: (e) =>
|
||||
!(e.kind === "varRef" && (e.localId === message.has || e.localId === message.text)),
|
||||
});
|
||||
const args = call.args.map((arg, index) =>
|
||||
readsMessage(arg) ? arg : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc),
|
||||
);
|
||||
const local = lowerer.declareHiddenLocal("assert.message", DYN);
|
||||
const value = varRef(local.id, DYN, loc);
|
||||
// The runtime reports a registered value: a truthy one for the other
|
||||
// methods, any value but null and undefined for assert.fail (Node's
|
||||
// `actual == null` test), whose other values read "Failed".
|
||||
const registered: IrExpr = message.fail
|
||||
? varRef(message.has, BOOL, loc)
|
||||
: { kind: "dynTest", test: "truthy", value, type: BOOL, loc };
|
||||
stmts.push(
|
||||
{ kind: "varDecl", localId: local.id, init: message.value, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: message.has,
|
||||
init: { kind: "dynTest", test: "nullish", negated: true, value, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: message.text,
|
||||
init: {
|
||||
kind: "ternary",
|
||||
cond: registered,
|
||||
then: strLit(VALUE_MESSAGE, loc),
|
||||
else_: strLit(message.fail ? "Failed" : "", loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: registered,
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "assert.messageValue",
|
||||
args: [value, { kind: "dynTest", test: "error", value, type: BOOL, loc }],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: { ...call, args }, loc },
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "libCall", fn: "assert.messageClear", args: [], type: VOID, loc },
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
} finally {
|
||||
valueMessage = outer;
|
||||
}
|
||||
}
|
||||
|
||||
/** The module-function dispatch — the spoke's entry, called from both the
|
||||
@@ -201,6 +390,15 @@ export function lowerAssertModuleCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module !== "assert" && bi.module !== "assert/strict") return null;
|
||||
return withValueMessage(lowerer, expr, loc, () => lowerAssertMember(lowerer, expr, bi, loc));
|
||||
}
|
||||
|
||||
function lowerAssertMember(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
switch (canonicalAssertMember(bi.module, bi.member)) {
|
||||
case "ok":
|
||||
return lowerAssertOk(lowerer, expr, loc);
|
||||
@@ -880,7 +1078,7 @@ export function lowerAssertDirectCall(
|
||||
'use the default import: import assert from "node:assert")',
|
||||
);
|
||||
}
|
||||
return lowerAssertOk(lowerer, expr, loc);
|
||||
return withValueMessage(lowerer, expr, loc, () => lowerAssertOk(lowerer, expr, loc));
|
||||
}
|
||||
|
||||
/** assert(value[, message]) / assert.ok(value[, message]): the truthiness
|
||||
@@ -889,7 +1087,24 @@ export function lowerAssertDirectCall(
|
||||
* the same bytes at compile time). */
|
||||
function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression));
|
||||
if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) {
|
||||
if (expr.arguments.nodes.length === 0) {
|
||||
// Node's message for a call without arguments.
|
||||
const text = "No value argument passed to `assert.ok()`";
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [recordOnFailure(lowerer, boolLit(true, loc), null, boolLit(true, loc), "==", loc)],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "assert.ok",
|
||||
args: [boolLit(false, loc), strLit(text, loc), boolLit(true, loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (expr.arguments.nodes.length > 2) {
|
||||
lowerer.noLowering(
|
||||
`${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`,
|
||||
expr,
|
||||
@@ -953,22 +1168,26 @@ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc)
|
||||
generated = boolLit(true, loc);
|
||||
} else {
|
||||
// Node's AssertionError sets generatedMessage to !message, so an
|
||||
// empty custom message counts as generated.
|
||||
msg = stableValue(
|
||||
lowerer,
|
||||
stmts,
|
||||
lowerMessageArg(lowerer, msgNode, loc).msg,
|
||||
"%assertFailMessage",
|
||||
loc,
|
||||
);
|
||||
generated = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: msg,
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
// empty custom message counts as generated. A message that is not a
|
||||
// string is read from the hidden local that withValueMessage declares
|
||||
// after these statements, so it is used as is.
|
||||
const { msg: lowered, hasMsg } = lowerMessageArg(lowerer, msgNode, loc, true);
|
||||
if (lowered.kind === "varRef" && hasMsg.kind === "varRef") {
|
||||
// A value message: generated when Node reads "Failed" for null or
|
||||
// undefined; the runtime sets the flag for a registered value.
|
||||
msg = lowered;
|
||||
generated = { kind: "unary", op: "!", operand: hasMsg, type: BOOL, loc };
|
||||
} else {
|
||||
msg = stableValue(lowerer, stmts, lowered, "%assertFailMessage", loc);
|
||||
generated = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: msg,
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const falseLit: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
stmts.push(recordOnFailure(lowerer, boolLit(true, loc), null, null, "fail", loc));
|
||||
@@ -1056,9 +1275,22 @@ function lowerAssertEqual(
|
||||
// would answer false where Node can answer true (the value's own
|
||||
// earlier crossing) — a miscompare, never lowered. Bytes fence both
|
||||
// ways (the dyn copy cannot carry the Buffer/Uint8Array brand the
|
||||
// checker type knows); class instances and everything non-convertible
|
||||
// keep the fence too.
|
||||
if (a.type.kind === "dyn" || b.type.kind === "dyn") {
|
||||
// checker type knows); everything non-convertible keeps the fence too.
|
||||
// A class instance crosses as a reference to the instance itself, so
|
||||
// strict compares are honest reference equality, between two instances
|
||||
// too (assert.strictEqual(e, e.setMaxListeners(1))).
|
||||
const instanceType = (t: IrType): boolean =>
|
||||
isDynTypedRefType(t) && !lowerer.isSubclassOf(t.className, "%Error");
|
||||
const instance = (e: IrExpr): boolean =>
|
||||
!deep &&
|
||||
(instanceType(e.type) ||
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions
|
||||
.get(e.type.unionId)
|
||||
?.arms.every((arm) => instanceType(arm) || isUnitType(arm)) ??
|
||||
false))) &&
|
||||
lowerer.dynConvertible(e.type);
|
||||
if (a.type.kind === "dyn" || b.type.kind === "dyn" || (instance(a) && instance(b))) {
|
||||
const box = (e: IrExpr, node: ts.Expression): IrExpr => {
|
||||
if (e.type.kind === "dyn") return e;
|
||||
const k = e.type.kind;
|
||||
@@ -1067,6 +1299,18 @@ function lowerAssertEqual(
|
||||
k === "f64" ||
|
||||
k === "string" ||
|
||||
k === "bool" ||
|
||||
// A union of scalars and units crosses by value, like its arms.
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions
|
||||
.get(e.type.unionId)
|
||||
?.arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "string" ||
|
||||
arm.kind === "bool" ||
|
||||
isUnitType(arm),
|
||||
) ??
|
||||
false)) ||
|
||||
(k === "func" && lowerer.dynConvertible(e.type)) ||
|
||||
// Runtime handles cross by REFERENCE (identity preserved), so the
|
||||
// strict compare is honest: assert.strictEqual(this, server) —
|
||||
@@ -1082,6 +1326,9 @@ function lowerAssertEqual(
|
||||
// same). The deep walk compares the retained error data without
|
||||
// re-encoding and losing identity (SEMANTICS.md).
|
||||
(k === "object" && e.type.kind === "object" && e.type.className === "%Error") ||
|
||||
instance(e) ||
|
||||
// A symbol crosses as itself, so strict compares are identity.
|
||||
(!deep && k === "symbol") ||
|
||||
(deep && k !== "bytes" && k !== "object" && lowerer.dynConvertible(e.type)) ||
|
||||
// Under skipPrototype the prototype a class instance or Buffer
|
||||
// brand would carry is not compared.
|
||||
@@ -1470,17 +1717,29 @@ function classifyThrowsExpected(
|
||||
displayName: rec ? rec.lib : info.def.name.replace(/^%/, ""),
|
||||
};
|
||||
}
|
||||
if (
|
||||
(surface === "assert.doesNotThrow" || surface === "assert.doesNotReject") &&
|
||||
expectedT?.kind === "func"
|
||||
) {
|
||||
if (expectedT?.kind === "func") {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
// An untyped value there (a dynamic-tier function's result) is checked
|
||||
// at run time like any untyped expected value.
|
||||
if (value.type.kind === "jsval" || value.type.kind === "dyn") {
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
if (boxed.type.kind === "dyn") return { form: "errValue", value: boxed };
|
||||
}
|
||||
// A JavaScript function accepts any argument; a typed one must take
|
||||
// unknown values (the caught value need not be an Error).
|
||||
if (
|
||||
value.type.kind === "func" &&
|
||||
value.type.params.every((param) => param.kind === "dyn") &&
|
||||
(value.type.params.every((param) => param.kind === "dyn") ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node))) &&
|
||||
lowerer.dynConvertible(value.type)
|
||||
) {
|
||||
return { form: "predicate", value: lowerer.coerceToExpected(value, DYN) };
|
||||
// The boxed function carries its source name (Node's message names
|
||||
// a named validation function).
|
||||
const fnName = jsFuncNameOf(node);
|
||||
return {
|
||||
form: "predicate",
|
||||
value: { kind: "dynFrom", value, ...(fnName !== null ? { fnName } : {}), type: DYN, loc },
|
||||
};
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`${surface} with this validation callback`,
|
||||
@@ -1546,7 +1805,9 @@ function classifyThrowsExpected(
|
||||
// tracingChannel suite's shape): the value crosses as a dyn error and
|
||||
// the runtime compares per Node's expectsError key walk. A
|
||||
// checked-dynamic expected rides directly; a static %Error boxes.
|
||||
if (expectedT?.kind === "dyn") {
|
||||
// An untyped JavaScript value (a parameter) too: a function there is
|
||||
// checked at run time (assertThrowsHelper).
|
||||
if (expectedT?.kind === "dyn" || expectedT === null) {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "dyn") return { form: "errValue", value };
|
||||
}
|
||||
@@ -1943,6 +2204,46 @@ function assertThrowsHelper(
|
||||
return { e: strLit("", loc), has: false };
|
||||
})();
|
||||
|
||||
// expectedException's function branch: a function with a prototype that
|
||||
// the caught value is an instance of passes; anything else is called as
|
||||
// a validation function, whose result must be true.
|
||||
const validateStmts = (fn: IrExpr): IrStmt[] => {
|
||||
const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc });
|
||||
const prototype: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: fn,
|
||||
key: strLit("prototype", loc),
|
||||
optional: true,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
ifStmt(
|
||||
{ kind: "dynTest", test: "undefined", negated: true, value: prototype, type: BOOL, loc },
|
||||
[
|
||||
ifStmt(
|
||||
checkedClassInstanceOf(
|
||||
lowerer,
|
||||
actual(),
|
||||
fn,
|
||||
"assert.throws against this native constructor is not supported yet",
|
||||
loc,
|
||||
),
|
||||
[doReturn],
|
||||
),
|
||||
],
|
||||
),
|
||||
lib("assert.throwsValidate", [
|
||||
actual(),
|
||||
fn,
|
||||
strLit(mode === "rejects" ? "rejects" : "throws", loc),
|
||||
m(),
|
||||
hm(),
|
||||
]),
|
||||
doReturn,
|
||||
];
|
||||
};
|
||||
|
||||
let catchBody: IrStmt[];
|
||||
if (mode === "dnr" || mode === "dnt") {
|
||||
const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc });
|
||||
@@ -2116,7 +2417,13 @@ function assertThrowsHelper(
|
||||
// caught value crosses as a dyn value (identity-preserving) and
|
||||
// the runtime walks the expected's keys — a mismatch throws the
|
||||
// deep-equal AssertionError.
|
||||
// A function there is a validation function (or an error class
|
||||
// value) as in the predicate form.
|
||||
catchBody = [
|
||||
ifStmt(
|
||||
{ kind: "dynTest", test: "function", value: varRef("ev.0", DYN, loc), type: BOOL, loc },
|
||||
validateStmts(varRef("ev.0", DYN, loc)),
|
||||
),
|
||||
lib("assert.expectsErrDyn", [
|
||||
{ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc },
|
||||
varRef("ev.0", DYN, loc),
|
||||
@@ -2127,7 +2434,8 @@ function assertThrowsHelper(
|
||||
];
|
||||
break;
|
||||
case "predicate":
|
||||
throw new InternalCompilerError("assert validation callback only enters a no-error helper");
|
||||
catchBody = validateStmts(varRef("predicate.0", DYN, loc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
arrayClassView,
|
||||
lowerArrayClassStaticCall,
|
||||
} from "./builtins/array-classes.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import { lowerDateToPrimitiveCall } from "./builtins/dates.js";
|
||||
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerDynStreamMemberCall } from "./dyn-streams.js";
|
||||
@@ -1057,6 +1059,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
}
|
||||
const bi = lowerer.builtinImportOf(expr.expression);
|
||||
if (bi) return lowerBuiltinCall(lowerer, expr, bi, loc);
|
||||
// A constant bound to an assert method (`const ok = assert.ok`) called
|
||||
// directly is that assertion, with the message Node quotes the call for.
|
||||
{
|
||||
const alias = assertFunctionAliasOf(lowerer, expr.expression);
|
||||
const served = alias ? lowerer.lowerAssertModuleCall(expr, alias, loc) : null;
|
||||
if (served) return served;
|
||||
}
|
||||
// The assert module binding called DIRECTLY (`assert(x)` — a default
|
||||
// import or the CJS `const assert = require("assert")`): Node's
|
||||
// module object IS assert.ok; namespace-import bindings fence inside
|
||||
@@ -1822,6 +1831,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// The element spelling of an unsupported native Web method must fence
|
||||
// before lowering the callee into a checked-dynamic keyed read.
|
||||
if (ts.isElementAccessExpression(expr.expression)) {
|
||||
const datePrimitive = lowerDateToPrimitiveCall(lowerer, expr, expr.expression);
|
||||
if (datePrimitive) return datePrimitive;
|
||||
const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression);
|
||||
if (timersDispose) return timersDispose;
|
||||
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
|
||||
@@ -2462,6 +2473,9 @@ function lowerNumberConstructorValue(
|
||||
};
|
||||
const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc);
|
||||
if (optionalNumber) return optionalNumber;
|
||||
// A Date's number hint answers its time value.
|
||||
if (arg.type.kind === "date")
|
||||
return { kind: "libCall", fn: "date.valueOf", args: [arg], type: F64, loc };
|
||||
// A program class instance converts through its own methods.
|
||||
const classNumber = classToNumberConversion(lowerer, arg, argNode);
|
||||
if (classNumber) return classNumber;
|
||||
@@ -8709,7 +8723,12 @@ function lowerObjectStaticCall(
|
||||
(probed && isDynTypedRefType(probed.type)) ||
|
||||
(probed?.type.kind === "object" && lowerer.errorHierarchyClassOf(probed.type.className)) ||
|
||||
(probed && constructor && (constructor.callableBase || hasRuntimeStatics(constructor))) ||
|
||||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))
|
||||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) ||
|
||||
// null, undefined and unions of untyped JavaScript values: the same
|
||||
// probe, whose ToObject throws Node's TypeError on nullish.
|
||||
(probed &&
|
||||
(isUnitType(probed.type) || probed.type.kind === "union") &&
|
||||
lowerer.dynConvertible(probed.type))
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerer.coerceToExpected(probed, DYN);
|
||||
|
||||
@@ -489,6 +489,10 @@ export interface ClassInfo {
|
||||
localPrototypeData?: true;
|
||||
prototypeMethodValues?: Map<string, IrExpr>;
|
||||
prototypeReflectionReady?: true;
|
||||
/** The global that records at run time that the prototype view was
|
||||
* populated by reflection, so it holds its own `constructor` until the
|
||||
* program deletes it (prototypeReflectedFlag). */
|
||||
prototypeReflectedFlag?: string;
|
||||
prototypeObservedMethodsReady?: true;
|
||||
/** The globals holding each observed method's original prototype value
|
||||
* (set when the prototype data is built), by method name. A typed call
|
||||
@@ -10484,6 +10488,29 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc };
|
||||
return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc };
|
||||
}
|
||||
// A union (`Date | string | number`) or checked-dynamic value: the
|
||||
// runtime's constructor reads a Date's time value, parses a string
|
||||
// and converts anything else to a primitive and a number.
|
||||
if (
|
||||
(arg.type.kind === "union" || arg.type.kind === "dyn") &&
|
||||
lowerer.dynConvertible(arg.type)
|
||||
) {
|
||||
const value: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "date.nativeNewValue",
|
||||
args: [
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
elems: [lowerer.coerceToExpected(arg, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "date.checkedValue", args: [value], type: DATE_T, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`new Date of '${lowerer.fmt(arg.type)}' values`,
|
||||
args[0]!,
|
||||
|
||||
@@ -2377,6 +2377,7 @@ export function typeofAnswer(arm: IrType): string | null {
|
||||
case "set":
|
||||
case "generator":
|
||||
case "regex":
|
||||
case "date":
|
||||
case "bytes":
|
||||
case "url":
|
||||
case "searchParams":
|
||||
|
||||
@@ -183,13 +183,9 @@ export function lowerGenMethodCall(
|
||||
);
|
||||
}
|
||||
arg = lowerer.lowerExpr(argNode);
|
||||
if (arg.type.kind === "date") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
".throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead)",
|
||||
);
|
||||
}
|
||||
// A Date throws as its checked-dynamic handle, the same Date object
|
||||
// (the throw statement's rule).
|
||||
if (arg.type.kind === "date") arg = lowerer.coerceToExpected(arg, DYN);
|
||||
if (arg.type.kind === "void" || arg.type.kind === "caught" || isUnitType(arg.type)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import { classPrototypeView } from "./class-prototypes.js";
|
||||
import {
|
||||
dynUndefinedExpr,
|
||||
boolLit,
|
||||
@@ -3527,8 +3528,14 @@ function lowerUtilCallableCall(
|
||||
member === "inherits" && expr.arguments.nodes[1] !== undefined
|
||||
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
|
||||
: null;
|
||||
// A superCtor class whose prototype has a complete view (a module-level
|
||||
// JavaScript class named directly) passes that view to the runtime.
|
||||
const superView =
|
||||
member === "inherits" && builtinBase === null && expr.arguments.nodes[1] !== undefined
|
||||
? classPrototypeView(lowerer, expr.arguments.nodes[1], loc)
|
||||
: null;
|
||||
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
|
||||
if (index === 1 && builtinBase !== null) continue;
|
||||
if (index === 1 && (builtinBase !== null || superView !== null)) continue;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
|
||||
lowerer.noLowering(
|
||||
`util.${member} over a class constructor`,
|
||||
@@ -3560,7 +3567,9 @@ function lowerUtilCallableCall(
|
||||
for (const arg of expr.arguments.nodes.slice(arity))
|
||||
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc });
|
||||
const type = fn === "util.inherits" ? VOID : DYN;
|
||||
const result: IrExpr = { kind: "libCall", fn, args, type, loc };
|
||||
const result: IrExpr = superView
|
||||
? { kind: "libCall", fn: "util.inheritsPrototype", args: [...args, superView], type, loc }
|
||||
: { kind: "libCall", fn, args, type, loc };
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
}
|
||||
|
||||
|
||||
@@ -1630,16 +1630,22 @@ export function lowerStaticAbortControllerCall(
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
const access = call.expression;
|
||||
// An optional chain (`controller?.abort()`) lowers the call again once it
|
||||
// has narrowed the receiver (Lowerer.chainHandled).
|
||||
if (
|
||||
call.questionDotToken ||
|
||||
(call.questionDotToken && !lowerer.chainHandled.has(call)) ||
|
||||
call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) ||
|
||||
(!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) ||
|
||||
access.questionDotToken ||
|
||||
(access.questionDotToken && !lowerer.chainHandled.has(access)) ||
|
||||
staticResponseMemberName(lowerer, access) !== "abort"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
|
||||
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(
|
||||
access.questionDotToken
|
||||
? lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression))
|
||||
: lowerer.typeOf(access.expression),
|
||||
);
|
||||
const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol();
|
||||
if (
|
||||
symbol === undefined ||
|
||||
|
||||
@@ -1624,8 +1624,47 @@ const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"bytes",
|
||||
"func",
|
||||
"regex",
|
||||
"date",
|
||||
]);
|
||||
|
||||
/** A `return` at the top level of a CommonJS module (preflight admits it
|
||||
* there only): it ends the module's evaluation, and its value is
|
||||
* evaluated and discarded, as Node's module wrapper does. Top-level
|
||||
* bindings and export assignments after it would be left uninitialized
|
||||
* where Node leaves them in their temporal dead zone or absent, so a
|
||||
* module with any keeps a refusal. Null for a return inside a function. */
|
||||
function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null {
|
||||
let top: ts.Node = stmt;
|
||||
while (top.parent !== undefined && !ts.isSourceFile(top.parent)) {
|
||||
if (ts.isFunctionLike(top.parent) || ts.isClassStaticBlockDeclaration(top.parent)) return null;
|
||||
top = top.parent;
|
||||
}
|
||||
const sf = top.parent;
|
||||
if (sf === undefined || !ts.isSourceFile(sf)) return null;
|
||||
const stmts = sf.statements.nodes;
|
||||
const later = stmts.slice(stmts.indexOf(top as ts.Statement) + 1);
|
||||
if (
|
||||
later.some(
|
||||
(s) =>
|
||||
ts.isVariableStatement(s) || ts.isClassDeclaration(s) || cjsExportAssignmentOf(s) !== null,
|
||||
)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
stmt,
|
||||
"a top-level return in a CommonJS module that declares variables or classes, or assigns exports, after it",
|
||||
);
|
||||
}
|
||||
const loc = locOf(stmt);
|
||||
const ret: IrStmt = { kind: "return", value: null, loc };
|
||||
if (!stmt.expression) return ret;
|
||||
return {
|
||||
kind: "block",
|
||||
body: [{ kind: "exprStmt", expr: lowerer.lowerExpr(stmt.expression), loc }, ret],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
|
||||
if (ownPrototypeResetStatement(lowerer, stmt))
|
||||
return { kind: "block", body: [], loc: locOf(stmt) };
|
||||
@@ -1735,6 +1774,8 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
if (ts.isForOfStatement(stmt)) return lowerer.lowerForOf(stmt);
|
||||
if (ts.isLabeledStatement(stmt)) return lowerLabeled(lowerer, stmt);
|
||||
if (ts.isReturnStatement(stmt)) {
|
||||
const moduleReturn = lowerModuleLevelReturn(lowerer, stmt);
|
||||
if (moduleReturn) return moduleReturn;
|
||||
// Implicit-any instance RETURN INFERENCE (function-returns.ts
|
||||
// resolveInferredReturn): the value lowers BARE and the statement
|
||||
// records itself — the post-pass settles the instance's return type
|
||||
@@ -1828,9 +1869,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
return { kind: "rethrow", localId: caughtLocal.id, loc: locOf(stmt) };
|
||||
}
|
||||
// Exception-representable values move into the runtime's exception
|
||||
// cell. `throw f()` of a void call has no value; Date has numeric
|
||||
// storage internally, but cannot preserve its object kind through the
|
||||
// untyped catch boundary. Reject both rather than changing JS identity.
|
||||
// cell. `throw f()` of a void call has no value.
|
||||
let value = lowerer.lowerExpr(stmt.expression);
|
||||
if (value.type.kind === "void")
|
||||
lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression));
|
||||
@@ -1842,19 +1881,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
} else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) {
|
||||
value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) };
|
||||
} 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.
|
||||
// Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes,
|
||||
// Dates 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.coerceInto(stmt.expression, value, DYN);
|
||||
}
|
||||
if (value.type.kind === "date") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
stmt.expression,
|
||||
"throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead)",
|
||||
);
|
||||
}
|
||||
// A dyn throw rides the REF cell arm (scr_throw_ref over the checked-dynamic tree
|
||||
// node — identity preserved through catch bindings, caughtToDyn
|
||||
// passes it back by reference). The JS-lane traced-call shape:
|
||||
@@ -5450,7 +5482,12 @@ export function lowerVarDecl(
|
||||
if (isTimeValueDateDeclaration(lowerer, decl)) {
|
||||
const loc = locOf(decl);
|
||||
const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T);
|
||||
const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet);
|
||||
const local = lowerer.declareLocal(
|
||||
decl.name,
|
||||
decl.name.text,
|
||||
F64,
|
||||
isLet || lowerer.timeValueDateWrites.has(decl),
|
||||
);
|
||||
lowerer.timeValueDateLocals.add(local);
|
||||
const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc };
|
||||
return { kind: "varDecl", localId: local.id, init, loc };
|
||||
|
||||
@@ -239,7 +239,7 @@ test("optional union widening keeps refused and missing contracts current", () =
|
||||
unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"),
|
||||
};
|
||||
expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull();
|
||||
lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]);
|
||||
lowerer.unions.finalizeRecursive(recursive, [{ kind: "promise", inner: F64 }, STRING]);
|
||||
expect(lowerer.withUndefinedArmOf(pending)).toBeNull();
|
||||
expect(lowerer.withUndefinedArmOf(pending)).toBeNull();
|
||||
|
||||
|
||||
@@ -600,7 +600,11 @@ import {
|
||||
lowerTestMethodCall,
|
||||
lowerTestCtxProperty,
|
||||
} from "./lower-test.js";
|
||||
import { lowerAssertModuleCall, lowerAssertDirectCall } from "./lower-assert.js";
|
||||
import {
|
||||
assertFunctionValue,
|
||||
lowerAssertModuleCall,
|
||||
lowerAssertDirectCall,
|
||||
} from "./lower-assert.js";
|
||||
import type { AssertInstanceOptions } from "./assert-instances.js";
|
||||
import { lowerUtilModuleCall } from "./lower-inspect.js";
|
||||
import { lowerSystemErrorMapValue } from "./lower-system-errors.js";
|
||||
@@ -2369,6 +2373,9 @@ export class Lowerer {
|
||||
* Date's time value instead of a Date object (date-values.ts). */
|
||||
readonly timeValueDateDecisions = new Map<ts.Node, boolean>();
|
||||
readonly timeValueDateLocals = new Set<IrLocal>();
|
||||
/** Declarations of those locals that a setter call writes
|
||||
* (date-values.ts). */
|
||||
readonly timeValueDateWrites = new Set<ts.VariableDeclaration>();
|
||||
/** Record bindings whose flow class reaches storage outside the class:
|
||||
* they keep the declared layout, so an initializer from unchecked reads
|
||||
* converts once instead of the binding adopting the widened variant. */
|
||||
@@ -3917,7 +3924,8 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, is an owned runtime
|
||||
* handle (a Worker or MessagePort, a fetch Headers/Request/Response, or
|
||||
* handle (a Worker or MessagePort, a fetch Headers/Request/Response or
|
||||
* AbortController/AbortSignal, or
|
||||
* a node:crypto ECDH, DiffieHellman or DiffieHellmanGroup), or is a
|
||||
* WeakMap/WeakSet (native weak collections live in checked-dynamic
|
||||
* storage, as locals of those types do); `any` fields stay refused. */
|
||||
@@ -14024,6 +14032,8 @@ export class Lowerer {
|
||||
* deliberately narrower than table membership because many table rows
|
||||
* are only dispatch sentinels for call-site-specific lowering. */
|
||||
lowerBuiltinCallableValue(bi: { module: string; member: string }, loc: SrcLoc): IrExpr | null {
|
||||
const assertion = assertFunctionValue(bi, loc);
|
||||
if (assertion) return assertion;
|
||||
if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc);
|
||||
if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member))
|
||||
return lowerUtilTypeValue(this, bi.member, loc);
|
||||
|
||||
@@ -873,6 +873,12 @@ export const OBJECT_CALLABLE_VALUES: Record<string, BuiltinModuleFn | undefined>
|
||||
result: DYN,
|
||||
valueParams: exactValueParams(DYN, DYN),
|
||||
},
|
||||
hasOwn: {
|
||||
fn: "dyn.hasOwnComputed",
|
||||
params: [DYN, DYN],
|
||||
result: BOOL,
|
||||
valueParams: exactValueParams(DYN, DYN),
|
||||
},
|
||||
keys: { fn: "dyn.objKeys", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
|
||||
values: { fn: "dyn.objValues", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
|
||||
entries: { fn: "dyn.objEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
|
||||
@@ -2338,6 +2344,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
],
|
||||
note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.setters",
|
||||
kind: "stdlib",
|
||||
name: "Date.prototype setters",
|
||||
fns: ["date.setFields", "date.timeClip", "date.store"],
|
||||
note: "setTime and the local and UTC field setters, with their optional arguments",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.UTC",
|
||||
kind: "stdlib",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { npmStaticPackageOfPath } from "./npm-static.js";
|
||||
import { trackedReadFile } from "./input-tracker.js";
|
||||
import { npmPackageNameOf } from "./workspace-registry.js";
|
||||
import { typeOnlyNamedImport } from "./type-only-imports.js";
|
||||
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
|
||||
export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclaration): boolean {
|
||||
return program.analysis.prunedNpmReexports.has(stmt);
|
||||
@@ -117,6 +118,21 @@ function pureExternalPackage(
|
||||
|
||||
type Demand = Set<string> | null; // null requests the entire namespace
|
||||
|
||||
/** One module edge of a package file, resolved once per plan. */
|
||||
interface ModuleEdge {
|
||||
spec: string;
|
||||
dep: ts.SourceFile | null;
|
||||
}
|
||||
|
||||
/** A file's runtime module edges: its import declarations with the names
|
||||
* they request (null: the whole module), its `export … from` declarations,
|
||||
* and its `import()` and `require()` calls with one string argument. */
|
||||
interface ModuleEdges {
|
||||
imports: (ModuleEdge & { names: readonly string[] | null })[];
|
||||
reexports: (ModuleEdge & { stmt: ts.ExportDeclaration })[];
|
||||
calls: ModuleEdge[];
|
||||
}
|
||||
|
||||
/** Package files outside the demanded module graph are absent from the
|
||||
* executable program. All ordinary project files retain their preflight
|
||||
* behavior, including files not imported by the entry. */
|
||||
@@ -151,6 +167,70 @@ export function planNpmStaticReexports(
|
||||
}
|
||||
};
|
||||
|
||||
// The demand walk visits a file again whenever another of its names is
|
||||
// requested, and every purity check walks the files it reaches: both read
|
||||
// these edges instead of resolving the file's syntax each time.
|
||||
const edgeMemo = new Map<ts.SourceFile, ModuleEdges>();
|
||||
const edgesOf = (sf: ts.SourceFile): ModuleEdges => {
|
||||
const cached = edgeMemo.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const edges: ModuleEdges = { imports: [], reexports: [], calls: [] };
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (ts.isImportDeclaration(stmt)) {
|
||||
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
||||
const clause = stmt.importClause;
|
||||
if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue;
|
||||
const spec = stmt.moduleSpecifier.text;
|
||||
let names: string[] | null = null;
|
||||
if (
|
||||
clause !== undefined &&
|
||||
(clause.namedBindings === undefined ||
|
||||
!(
|
||||
ts.isNamespaceImport(clause.namedBindings) ||
|
||||
(ts.isNamedImports(clause.namedBindings) &&
|
||||
clause.namedBindings.elements.nodes.length === 0)
|
||||
)) &&
|
||||
// `import { type A }` still loads the module, like `import {}`.
|
||||
!typeOnlyNamedImport(stmt)
|
||||
) {
|
||||
names = [];
|
||||
if (clause.name !== undefined) names.push("default");
|
||||
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
||||
for (const element of clause.namedBindings.elements.nodes) {
|
||||
if (!element.isTypeOnly) names.push((element.propertyName ?? element.name).text);
|
||||
}
|
||||
}
|
||||
}
|
||||
edges.imports.push({ spec, dep: resolveEdge(sf, spec), names });
|
||||
} else if (ts.isExportDeclaration(stmt)) {
|
||||
// `export { type A } from` still loads the module under Node's
|
||||
// type stripping; only `export type … from` erases.
|
||||
if (
|
||||
stmt.isTypeOnly ||
|
||||
stmt.moduleSpecifier === undefined ||
|
||||
!ts.isStringLiteral(stmt.moduleSpecifier)
|
||||
)
|
||||
continue;
|
||||
const spec = stmt.moduleSpecifier.text;
|
||||
edges.reexports.push({ spec, dep: resolveEdge(sf, spec), stmt });
|
||||
}
|
||||
}
|
||||
for (const node of moduleSourceCandidates(program, sf).calls) {
|
||||
if (node.arguments.nodes.length !== 1) continue;
|
||||
const arg = node.arguments.nodes[0];
|
||||
if (arg === undefined || !ts.isStringLiteralLike(arg)) continue;
|
||||
const kind =
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
||||
? "import"
|
||||
: ts.isIdentifier(node.expression) && node.expression.text === "require"
|
||||
? "require"
|
||||
: null;
|
||||
if (kind !== null) edges.calls.push({ spec: arg.text, dep: resolveEdge(sf, arg.text, kind) });
|
||||
}
|
||||
edgeMemo.set(sf, edges);
|
||||
return edges;
|
||||
};
|
||||
|
||||
for (const sf of files) {
|
||||
if (npmStaticPackageOfPath(sf.fileName) === null) request(sf, null);
|
||||
}
|
||||
@@ -178,57 +258,65 @@ export function planNpmStaticReexports(
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// An edge this program does not resolve: the conservative
|
||||
// whole-dependency promise of the external package it names.
|
||||
const externalPure = (sf: ts.SourceFile, spec: string): boolean => {
|
||||
const resolved = resolveBareModule(sf.fileName, spec, "js-only");
|
||||
const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile);
|
||||
const root =
|
||||
pkg === null || resolved === null
|
||||
? null
|
||||
: packageRootJsonPath(resolved.typesFile, pkg, packageJsons);
|
||||
return root !== null && pureExternalPackage(root, packageJsons);
|
||||
};
|
||||
// Check the actual imported closure: metadata for one file never vouches
|
||||
// for its side-effectful descendants, even across a package or a cycle.
|
||||
const pureModuleTree = (root: ts.SourceFile): boolean => {
|
||||
const cached = pureMemo.get(root);
|
||||
if (cached !== undefined) return cached;
|
||||
const visited = new Set<ts.SourceFile>();
|
||||
const visit = (sf: ts.SourceFile): boolean => {
|
||||
if (visited.has(sf)) return true;
|
||||
if (pureMemo.get(sf) === false || !filePure(sf)) return false;
|
||||
visited.add(sf);
|
||||
const edges: { spec: string; kind?: "import" | "require" }[] = [];
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
ts.isImportDeclaration(stmt) &&
|
||||
stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier)
|
||||
)
|
||||
edges.push({ spec: stmt.moduleSpecifier.text });
|
||||
else if (
|
||||
ts.isExportDeclaration(stmt) &&
|
||||
!stmt.isTypeOnly &&
|
||||
stmt.moduleSpecifier &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier)
|
||||
)
|
||||
edges.push({ spec: stmt.moduleSpecifier.text });
|
||||
// A file is pure when every file it reaches is. The walk finds the
|
||||
// strongly connected components of the module graph (Tarjan): a finished
|
||||
// component is pure as a whole. On the first impure file the walk stops,
|
||||
// and every file still on its stack reaches that file, so each is impure.
|
||||
const order = new Map<ts.SourceFile, number>();
|
||||
const stack: ts.SourceFile[] = [];
|
||||
/** The lowest order the file's walk reaches (Infinity for a finished
|
||||
* component), or -1 once it reaches an impure file. */
|
||||
const visit = (sf: ts.SourceFile): number => {
|
||||
const known = pureMemo.get(sf);
|
||||
if (known !== undefined) return known ? Infinity : -1;
|
||||
const seen = order.get(sf);
|
||||
if (seen !== undefined) return seen;
|
||||
if (!filePure(sf)) {
|
||||
pureMemo.set(sf, false);
|
||||
return -1;
|
||||
}
|
||||
const own = order.size;
|
||||
order.set(sf, own);
|
||||
stack.push(sf);
|
||||
let low = own;
|
||||
const edges = edgesOf(sf);
|
||||
for (const list of [edges.imports, edges.reexports, edges.calls]) {
|
||||
for (const edge of list) {
|
||||
const reached =
|
||||
edge.dep !== null ? visit(edge.dep) : externalPure(sf, edge.spec) ? low : -1;
|
||||
if (reached < 0) return -1;
|
||||
if (reached < low) low = reached;
|
||||
}
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return;
|
||||
const arg = node.arguments.nodes[0];
|
||||
if (!arg || !ts.isStringLiteralLike(arg)) return;
|
||||
if (node.expression.kind === ts.SyntaxKind.ImportKeyword)
|
||||
edges.push({ spec: arg.text, kind: "import" });
|
||||
else if (ts.isIdentifier(node.expression) && node.expression.text === "require")
|
||||
edges.push({ spec: arg.text, kind: "require" });
|
||||
});
|
||||
return edges.every((edge) => {
|
||||
const dep = resolveEdge(sf, edge.spec, edge.kind);
|
||||
if (dep !== null) return visit(dep);
|
||||
const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only");
|
||||
const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile);
|
||||
const root =
|
||||
pkg === null || resolved === null
|
||||
? null
|
||||
: packageRootJsonPath(resolved.typesFile, pkg, packageJsons);
|
||||
return root !== null && pureExternalPackage(root, packageJsons);
|
||||
});
|
||||
};
|
||||
const pure = visit(root);
|
||||
if (pure) for (const sf of visited) pureMemo.set(sf, true);
|
||||
else pureMemo.set(root, false);
|
||||
return pure;
|
||||
}
|
||||
if (low === own) {
|
||||
for (;;) {
|
||||
const member = stack.pop()!;
|
||||
order.delete(member);
|
||||
pureMemo.set(member, true);
|
||||
if (member === sf) break;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
};
|
||||
const pureModuleTree = (root: ts.SourceFile): boolean => {
|
||||
if (visit(root) >= 0) return true;
|
||||
for (const sf of stack) pureMemo.set(sf, false);
|
||||
stack.length = 0;
|
||||
order.clear();
|
||||
return false;
|
||||
};
|
||||
const canPrune = (
|
||||
sf: ts.SourceFile,
|
||||
@@ -245,78 +333,32 @@ export function planNpmStaticReexports(
|
||||
return names !== undefined && names !== null && !names.has(stmt.exportClause.name.text);
|
||||
};
|
||||
|
||||
// A file is queued again only when its demand grows. Its imports and
|
||||
// calls request the same modules every time; only a namespace re-export
|
||||
// the new demand names stops being prunable.
|
||||
const walked = new Set<ts.SourceFile>();
|
||||
while (queue.length > 0) {
|
||||
const sf = queue.shift()!;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (ts.isImportDeclaration(stmt)) {
|
||||
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
||||
const clause = stmt.importClause;
|
||||
if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue;
|
||||
const dep = resolveEdge(sf, stmt.moduleSpecifier.text);
|
||||
if (
|
||||
clause === undefined ||
|
||||
(clause.namedBindings !== undefined &&
|
||||
(ts.isNamespaceImport(clause.namedBindings) ||
|
||||
(ts.isNamedImports(clause.namedBindings) &&
|
||||
clause.namedBindings.elements.nodes.length === 0))) ||
|
||||
// `import { type A }` still loads the module, like `import {}`.
|
||||
typeOnlyNamedImport(stmt)
|
||||
) {
|
||||
request(dep, null);
|
||||
} else {
|
||||
if (clause.name !== undefined) request(dep, "default");
|
||||
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
||||
for (const element of clause.namedBindings.elements.nodes) {
|
||||
if (!element.isTypeOnly) request(dep, (element.propertyName ?? element.name).text);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ts.isExportDeclaration(stmt)) {
|
||||
// `export { type A } from` still loads the module under Node's
|
||||
// type stripping; only `export type … from` erases.
|
||||
if (
|
||||
stmt.isTypeOnly ||
|
||||
stmt.moduleSpecifier === undefined ||
|
||||
!ts.isStringLiteral(stmt.moduleSpecifier)
|
||||
)
|
||||
continue;
|
||||
const dep = resolveEdge(sf, stmt.moduleSpecifier.text);
|
||||
if (canPrune(sf, stmt, dep)) continue;
|
||||
request(dep, null);
|
||||
const edges = edgesOf(sf);
|
||||
const first = !walked.has(sf);
|
||||
walked.add(sf);
|
||||
if (first) {
|
||||
for (const edge of edges.imports) {
|
||||
if (edge.names === null) request(edge.dep, null);
|
||||
else for (const name of edge.names) request(edge.dep, name);
|
||||
}
|
||||
}
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return undefined;
|
||||
const arg = node.arguments.nodes[0];
|
||||
if (arg === undefined || !ts.isStringLiteralLike(arg)) return undefined;
|
||||
if (
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
(ts.isIdentifier(node.expression) && node.expression.text === "require")
|
||||
) {
|
||||
request(
|
||||
resolveEdge(
|
||||
sf,
|
||||
arg.text,
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require",
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
for (const edge of edges.reexports) {
|
||||
if (canPrune(sf, edge.stmt, edge.dep)) continue;
|
||||
request(edge.dep, null);
|
||||
}
|
||||
if (first) for (const edge of edges.calls) request(edge.dep, null);
|
||||
}
|
||||
|
||||
const pruned = new Set<ts.ExportDeclaration>();
|
||||
for (const sf of demanded.keys()) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
!ts.isExportDeclaration(stmt) ||
|
||||
stmt.moduleSpecifier === undefined ||
|
||||
!ts.isStringLiteral(stmt.moduleSpecifier)
|
||||
)
|
||||
continue;
|
||||
if (canPrune(sf, stmt, resolveEdge(sf, stmt.moduleSpecifier.text))) pruned.add(stmt);
|
||||
}
|
||||
for (const edge of edgesOf(sf).reexports)
|
||||
if (canPrune(sf, edge.stmt, edge.dep)) pruned.add(edge.stmt);
|
||||
}
|
||||
program.analysis.prunedNpmReexports.clear();
|
||||
for (const stmt of pruned) program.analysis.prunedNpmReexports.add(stmt);
|
||||
|
||||
@@ -140,6 +140,7 @@ import {
|
||||
} from "./type-only-imports.js";
|
||||
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
import { planDeclProjection, type DeclProjection } from "./decl-projection.js";
|
||||
import { reportShapes } from "./shape-inference/index.js";
|
||||
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
|
||||
import {
|
||||
containsJsx,
|
||||
@@ -1260,6 +1261,10 @@ export function loadProgram(
|
||||
npmStaticPackages.length > 0,
|
||||
projector,
|
||||
);
|
||||
// Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES).
|
||||
const shapes = process.env["SCRIPTC_DEBUG_SHAPES"];
|
||||
if (shapes !== undefined && shapes !== "" && shapes !== "0" && npmStaticPackages.length > 0)
|
||||
reportShapes(load.program, shapes);
|
||||
return {
|
||||
...load,
|
||||
services,
|
||||
@@ -5358,6 +5363,16 @@ function preflight7(load: LoadResult): {
|
||||
return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text);
|
||||
};
|
||||
|
||||
// A `return` at the top level of a CommonJS module (TS1108): Node runs
|
||||
// the module as a function body, so the return ends the module's
|
||||
// evaluation. ES modules and TypeScript keep the error.
|
||||
const cjsTopLevelReturnSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
||||
if (d.code !== 1108 || d.fileName === undefined || !isJsSourceFileName(d.fileName))
|
||||
return false;
|
||||
const sf = p.getSourceFile(d.fileName);
|
||||
return sf !== undefined && isCjsJsFile7(sf, p);
|
||||
};
|
||||
|
||||
const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
||||
if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined)
|
||||
return false;
|
||||
@@ -5404,6 +5419,7 @@ function preflight7(load: LoadResult): {
|
||||
!isNpmStaticSubclassArgument(p, d) &&
|
||||
!workspaceImplicitAnySuppressed(p, d) &&
|
||||
!typeofTestedNameSuppressed(p, d) &&
|
||||
!cjsTopLevelReturnSuppressed(p, d) &&
|
||||
!jsdocTypeSuppressed(p, d, commentDup),
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
/* Static-object specialization, stage 1 (DESIGN-constant-objects.md):
|
||||
* constant object graphs.
|
||||
*
|
||||
* An object literal is a SINGLETON when the code that creates it runs at
|
||||
* most once: at module top level, or in a function every call path to which
|
||||
* starts at module evaluation through exactly one call site outside loops
|
||||
* (IIFEs, revealing modules, init helpers). A singleton's field is CONSTANT
|
||||
* when every store to it runs in such once-only code, no reflective or
|
||||
* computed write reaches the object, and the object never escapes to code
|
||||
* the analysis cannot see. A constant object has only constant fields and a
|
||||
* fixed key set; constant objects whose constant fields point at each other
|
||||
* form a constant graph (`ns`, `ns.util`, `ns.util.trim`).
|
||||
*
|
||||
* This stage computes the graphs and reports them, with the rule that
|
||||
* demoted every near miss, and the reads and calls a later stage could fold.
|
||||
* Nothing reaches code generation. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Atom } from "./domain.js";
|
||||
import type { FunctionInfo, Op, ShapeAnalysis } from "./analyze.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
|
||||
type Node = ts.Node;
|
||||
|
||||
export interface ConstantField {
|
||||
readonly key: string;
|
||||
/** Distinct atoms the field may hold. */
|
||||
readonly values: readonly Atom[];
|
||||
/** Stores (the literal's own member included). */
|
||||
readonly stores: number;
|
||||
}
|
||||
|
||||
/** An object the analysis tracks as one allocation: an object literal, a
|
||||
* function object (a namespace or an ES5 constructor), or a constructor's
|
||||
* default prototype object. */
|
||||
export interface Candidate {
|
||||
readonly atom: Atom;
|
||||
/** The syntax that creates it (the literal or the function). */
|
||||
readonly node: Node;
|
||||
readonly kind: "literal" | "function" | "prototype";
|
||||
/** Keys the creating syntax itself defines. */
|
||||
readonly keys: readonly string[];
|
||||
/** Whether the creating syntax is a plain data literal. */
|
||||
readonly plain: boolean;
|
||||
readonly inPackage: boolean;
|
||||
}
|
||||
|
||||
export interface ConstantObject {
|
||||
readonly site: Candidate;
|
||||
readonly fields: readonly ConstantField[];
|
||||
readonly frozen: boolean;
|
||||
}
|
||||
|
||||
export interface NearMiss {
|
||||
readonly site: Candidate;
|
||||
/** Null when the whole object was demoted. */
|
||||
readonly key: string | null;
|
||||
readonly rule:
|
||||
| "init-factory"
|
||||
| "allocated-repeatedly"
|
||||
| "escapes"
|
||||
| "reflective-write"
|
||||
| "computed-write"
|
||||
| "literal-form"
|
||||
| "written-after-init"
|
||||
| "lazy-cache";
|
||||
readonly detail: string;
|
||||
}
|
||||
|
||||
export interface ConstantGraphs {
|
||||
readonly objects: readonly ConstantObject[];
|
||||
readonly nearMisses: readonly NearMiss[];
|
||||
/** Reads `e.k` whose receiver is exactly one constant object and whose
|
||||
* field is constant, and the subset that are callees. */
|
||||
readonly foldableReads: number;
|
||||
readonly foldableCalls: number;
|
||||
/** Reads of a constant field through a receiver that may also be other
|
||||
* values. */
|
||||
readonly mixedReads: number;
|
||||
readonly once: (fn: Node) => boolean;
|
||||
}
|
||||
|
||||
function isLoop(n: Node): boolean {
|
||||
return (
|
||||
ts.isForStatement(n) ||
|
||||
ts.isForInStatement(n) ||
|
||||
ts.isForOfStatement(n) ||
|
||||
ts.isWhileStatement(n) ||
|
||||
ts.isDoStatement(n)
|
||||
);
|
||||
}
|
||||
|
||||
/** The function (or source file) whose body contains `n`. */
|
||||
function ownerFunction(n: Node): Node {
|
||||
for (let p = n.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isSourceFile(p)) return p;
|
||||
if (ts.isFunctionLike(p) && (p as { body?: Node }).body !== undefined) return p;
|
||||
}
|
||||
return n.getSourceFile();
|
||||
}
|
||||
|
||||
/** Whether `n` sits in a loop of its own function's body. */
|
||||
function inLoop(n: Node, owner: Node): boolean {
|
||||
for (let p = n.parent; p !== undefined && p !== owner; p = p.parent) if (isLoop(p)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function loc(n: Node): string {
|
||||
const sf = n.getSourceFile();
|
||||
const file = sf.fileName.split("/node_modules/").pop() ?? sf.fileName;
|
||||
return `${file.split("/").slice(-2).join("/")}:${sf.getLineAndCharacterOfPosition(ts.tokenStartOfNode(n)).line + 1}`;
|
||||
}
|
||||
|
||||
/** `x.k || (x.k = v)`, `x.k ??= v`, `x.k ||= v`, `if (!x.k) x.k = v`. */
|
||||
function isCacheStore(write: Node): boolean {
|
||||
let assign: Node | undefined = write.parent;
|
||||
while (assign !== undefined && ts.isParenthesizedExpression(assign)) assign = assign.parent;
|
||||
if (assign === undefined || !ts.isBinaryExpression(assign)) return false;
|
||||
const op = assign.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.BarBarEqualsToken || op === ts.SyntaxKind.QuestionQuestionEqualsToken)
|
||||
return true;
|
||||
if (op !== ts.SyntaxKind.EqualsToken) return false;
|
||||
const text = write.getText();
|
||||
let up: Node | undefined = assign.parent;
|
||||
while (up !== undefined && ts.isParenthesizedExpression(up)) up = up.parent;
|
||||
if (
|
||||
up !== undefined &&
|
||||
ts.isBinaryExpression(up) &&
|
||||
(up.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
||||
up.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) &&
|
||||
up.left.getText() === text
|
||||
)
|
||||
return true;
|
||||
for (let p: Node | undefined = assign.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isIfStatement(p)) {
|
||||
const cond = p.expression.getText().replace(/\s+/g, "");
|
||||
return cond === `!${text}` || cond === `${text}===undefined` || cond === `${text}==null`;
|
||||
}
|
||||
if (ts.isFunctionLike(p) || ts.isSourceFile(p)) break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const OBJECT_DEMOTING: ReadonlySet<Op["kind"]> = new Set([
|
||||
"computedWrite",
|
||||
"delete",
|
||||
"defineProperty",
|
||||
"setPrototypeOf",
|
||||
"assign",
|
||||
"proxy",
|
||||
]);
|
||||
|
||||
export function computeConstantGraphs(analysis: ShapeAnalysis): ConstantGraphs {
|
||||
const infosByNode = new Map<Node, FunctionInfo[]>();
|
||||
for (const info of analysis.functions.values()) {
|
||||
let l = infosByNode.get(info.node);
|
||||
if (l === undefined) infosByNode.set(info.node, (l = []));
|
||||
l.push(info);
|
||||
}
|
||||
const edgesTo = new Map<Node, (typeof analysis.calls)[number][]>();
|
||||
for (const e of analysis.calls) {
|
||||
let l = edgesTo.get(e.callee.node);
|
||||
if (l === undefined) edgesTo.set(e.callee.node, (l = []));
|
||||
l.push(e);
|
||||
}
|
||||
const escapedNodes = new Set<Node>([...analysis.escapedFunctions].map((f) => f.node));
|
||||
|
||||
const onceMemo = new Map<Node, boolean>();
|
||||
const once = (fn: Node): boolean => {
|
||||
if (ts.isSourceFile(fn)) return true;
|
||||
const known = onceMemo.get(fn);
|
||||
if (known !== undefined) return known;
|
||||
onceMemo.set(fn, false); // a cycle is not once-only
|
||||
let result = false;
|
||||
const edges = edgesTo.get(fn) ?? [];
|
||||
if (
|
||||
infosByNode.has(fn) &&
|
||||
!escapedNodes.has(fn) &&
|
||||
edges.length > 0 &&
|
||||
new Set(edges.map((e) => e.site)).size === 1
|
||||
) {
|
||||
result = edges.every(
|
||||
(e) =>
|
||||
!e.inLoop && e.caller !== null && !inLoop(e.site, e.caller.node) && once(e.caller.node),
|
||||
);
|
||||
}
|
||||
onceMemo.set(fn, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Whether every call of a function comes from once-only code outside
|
||||
// loops, through any number of call sites: an init factory, whose
|
||||
// allocations are one object per call site (schema builders called at
|
||||
// module initialization).
|
||||
const initOnlyMemo = new Map<Node, boolean>();
|
||||
const initOnly = (fn: Node): boolean => {
|
||||
if (ts.isSourceFile(fn)) return true;
|
||||
const known = initOnlyMemo.get(fn);
|
||||
if (known !== undefined) return known;
|
||||
initOnlyMemo.set(fn, false);
|
||||
const edges = edgesTo.get(fn) ?? [];
|
||||
const result =
|
||||
infosByNode.has(fn) &&
|
||||
!escapedNodes.has(fn) &&
|
||||
edges.length > 0 &&
|
||||
edges.every(
|
||||
(e) =>
|
||||
!e.inLoop &&
|
||||
e.caller !== null &&
|
||||
!inLoop(e.site, e.caller.node) &&
|
||||
(once(e.caller.node) || initOnly(e.caller.node)),
|
||||
);
|
||||
initOnlyMemo.set(fn, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const opsByAtom = new Map<Atom, Op[]>();
|
||||
for (const op of analysis.ops) {
|
||||
for (const a of analysis.nodes[op.recv]!.atoms) {
|
||||
let l = opsByAtom.get(a);
|
||||
if (l === undefined) opsByAtom.set(a, (l = []));
|
||||
l.push(op);
|
||||
}
|
||||
}
|
||||
|
||||
const objects: ConstantObject[] = [];
|
||||
const nearMisses: NearMiss[] = [];
|
||||
const constantKeys = new Map<Atom, Set<string>>();
|
||||
const candidates: Candidate[] = [];
|
||||
for (const site of analysis.sites) {
|
||||
if (site.kind !== "object") continue;
|
||||
candidates.push({
|
||||
atom: site.atom,
|
||||
node: site.node,
|
||||
kind: "literal",
|
||||
keys: site.keys ?? [],
|
||||
plain: !(
|
||||
site.hasAccessor ||
|
||||
site.hasComputedKey ||
|
||||
site.spreads.length > 0 ||
|
||||
site.keys === null
|
||||
),
|
||||
inPackage: site.inPackage,
|
||||
});
|
||||
}
|
||||
const inPackage = (n: Node): boolean =>
|
||||
npmStaticPackageOfPath(n.getSourceFile().fileName) !== null;
|
||||
for (const atom of analysis.atoms.atoms) {
|
||||
if (atom.kind !== "function" || atom.node === undefined) continue;
|
||||
const hasFields = (analysis.fields.get(atom)?.size ?? 0) > 0;
|
||||
const proto = analysis.protoOf.get(atom);
|
||||
if (!hasFields && proto === undefined) continue;
|
||||
candidates.push({
|
||||
atom,
|
||||
node: atom.node,
|
||||
kind: "function",
|
||||
keys: [],
|
||||
plain: true,
|
||||
inPackage: inPackage(atom.node),
|
||||
});
|
||||
if (proto !== undefined)
|
||||
candidates.push({
|
||||
atom: proto,
|
||||
node: atom.node,
|
||||
kind: "prototype",
|
||||
keys: ["constructor"],
|
||||
plain: true,
|
||||
inPackage: inPackage(atom.node),
|
||||
});
|
||||
}
|
||||
for (const site of candidates) {
|
||||
// A function object is created where its expression or declaration
|
||||
// runs; its prototype with it.
|
||||
const owner = ownerFunction(site.node);
|
||||
if (!once(owner) || inLoop(site.node, owner)) {
|
||||
const factory = !inLoop(site.node, owner) && initOnly(owner);
|
||||
if (site.inPackage || factory)
|
||||
nearMisses.push({
|
||||
site,
|
||||
key: null,
|
||||
rule: factory ? "init-factory" : "allocated-repeatedly",
|
||||
detail: factory
|
||||
? `one object per call of ${loc(owner)} (${new Set((edgesTo.get(owner) ?? []).map((e) => e.site)).size} call sites)`
|
||||
: `created in ${owner.kind === ts.SyntaxKind.SourceFile ? "a module loop" : loc(owner)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const escaped = analysis.escaped.get(site.atom);
|
||||
if (escaped !== undefined) {
|
||||
nearMisses.push({ site, key: null, rule: "escapes", detail: escaped });
|
||||
continue;
|
||||
}
|
||||
if (!site.plain) {
|
||||
nearMisses.push({
|
||||
site,
|
||||
key: null,
|
||||
rule: "literal-form",
|
||||
detail: "accessor, computed key or spread in the literal",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const ops = opsByAtom.get(site.atom) ?? [];
|
||||
const demoting = ops.find((op) => OBJECT_DEMOTING.has(op.kind));
|
||||
if (demoting !== undefined) {
|
||||
nearMisses.push({
|
||||
site,
|
||||
key: null,
|
||||
rule: demoting.kind === "computedWrite" ? "computed-write" : "reflective-write",
|
||||
detail: `${demoting.kind} at ${loc(demoting.node)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const frozen = ops.some((op) => op.kind === "freeze");
|
||||
const stores = new Map<string, number>();
|
||||
for (const k of site.keys) stores.set(k, 1);
|
||||
// A function's own data fields come only from stores.
|
||||
const demotedKeys = new Map<string, NearMiss>();
|
||||
for (const op of ops) {
|
||||
// A `length` store on an object (not an array) is an ordinary write.
|
||||
if ((op.kind !== "write" && op.kind !== "lengthWrite") || op.name === null) continue;
|
||||
stores.set(op.name, (stores.get(op.name) ?? 0) + 1);
|
||||
const fn = ownerFunction(op.node);
|
||||
if (once(fn) && !inLoop(op.node, fn)) continue;
|
||||
if (demotedKeys.has(op.name)) continue;
|
||||
demotedKeys.set(op.name, {
|
||||
site,
|
||||
key: op.name,
|
||||
rule: isCacheStore(op.node) ? "lazy-cache" : "written-after-init",
|
||||
detail: loc(op.node),
|
||||
});
|
||||
}
|
||||
for (const miss of demotedKeys.values()) nearMisses.push(miss);
|
||||
const fieldMap = analysis.fields.get(site.atom) ?? new Map<string, number>();
|
||||
const fields: ConstantField[] = [];
|
||||
const keys = new Set<string>();
|
||||
for (const [key, count] of stores) {
|
||||
if (demotedKeys.has(key)) continue;
|
||||
const id = fieldMap.get(key);
|
||||
const values = id === undefined ? [] : [...analysis.nodes[id]!.atoms];
|
||||
fields.push({ key, values, stores: count });
|
||||
keys.add(key);
|
||||
}
|
||||
constantKeys.set(site.atom, keys);
|
||||
if (demotedKeys.size === 0) objects.push({ site, fields, frozen });
|
||||
}
|
||||
|
||||
let foldableReads = 0,
|
||||
foldableCalls = 0,
|
||||
mixedReads = 0;
|
||||
for (const op of analysis.ops) {
|
||||
if (op.kind !== "read" || op.name === null) continue;
|
||||
const atoms = analysis.nodes[op.recv]!.atoms;
|
||||
const constant = [...atoms].filter((a) => constantKeys.get(a)?.has(op.name!) === true);
|
||||
if (constant.length === 0) continue;
|
||||
if (atoms.size === 1) {
|
||||
foldableReads++;
|
||||
const p = op.node.parent;
|
||||
if (p !== undefined && ts.isCallExpression(p) && p.expression === op.node) foldableCalls++;
|
||||
} else mixedReads++;
|
||||
}
|
||||
return { objects, nearMisses, foldableReads, foldableCalls, mixedReads, once };
|
||||
}
|
||||
|
||||
/** Report lines (SCRIPTC_DEBUG_SHAPES=const): per package, the constant
|
||||
* graphs with their roots, the near misses by rule, and the foldable reads. */
|
||||
export function constantGraphReport(analysis: ShapeAnalysis, graphs: ConstantGraphs): string[] {
|
||||
const lines: string[] = [];
|
||||
const pkgOf = (site: Candidate): string =>
|
||||
npmStaticPackageOfPath(site.node.getSourceFile().fileName) ?? "<user>";
|
||||
const byPkg = new Map<string, { objects: ConstantObject[]; misses: NearMiss[] }>();
|
||||
const entry = (pkg: string) => {
|
||||
let e = byPkg.get(pkg);
|
||||
if (e === undefined) byPkg.set(pkg, (e = { objects: [], misses: [] }));
|
||||
return e;
|
||||
};
|
||||
for (const o of graphs.objects) entry(pkgOf(o.site)).objects.push(o);
|
||||
for (const m of graphs.nearMisses) entry(pkgOf(m.site)).misses.push(m);
|
||||
const constantAtoms = new Set(graphs.objects.map((o) => o.site.atom));
|
||||
const referenced = new Set<Atom>();
|
||||
for (const o of graphs.objects)
|
||||
for (const f of o.fields)
|
||||
if (f.values.length === 1 && constantAtoms.has(f.values[0]!)) referenced.add(f.values[0]!);
|
||||
const describe = (o: ConstantObject, depth: number, seen: Set<Atom>): string => {
|
||||
const parts = o.fields.map((f) => {
|
||||
if (f.values.length !== 1)
|
||||
return `${f.key}: ${f.values.length === 0 ? "?" : f.values.map((a) => a.label).join("|")}`;
|
||||
const v = f.values[0]!;
|
||||
const child = graphs.objects.find((x) => x.site.atom === v);
|
||||
if (child !== undefined && depth < 3 && !seen.has(v)) {
|
||||
seen.add(v);
|
||||
return `${f.key}: ${describe(child, depth + 1, seen)}`;
|
||||
}
|
||||
return `${f.key}: ${v.kind === "function" && child === undefined ? "fn" : v.label}`;
|
||||
});
|
||||
const shown = parts.length > 8 ? [...parts.slice(0, 8), `… ${parts.length - 8} more`] : parts;
|
||||
return `{${shown.join(", ")}}${o.frozen ? " (frozen)" : ""}`;
|
||||
};
|
||||
lines.push(
|
||||
`analysis ${analysis.steps} steps${analysis.budgetExceeded ? " (BUDGET EXCEEDED: facts incomplete)" : ""}, ${analysis.files.length} files`,
|
||||
);
|
||||
lines.push(
|
||||
`constant objects ${graphs.objects.length}, near misses ${graphs.nearMisses.length}; foldable reads ${graphs.foldableReads} (calls ${graphs.foldableCalls}), mixed-receiver reads ${graphs.mixedReads}`,
|
||||
);
|
||||
for (const [pkg, e] of [...byPkg].sort((a, b) => a[0].localeCompare(b[0]))) {
|
||||
const fields = e.objects.reduce((n, o) => n + o.fields.length, 0);
|
||||
const rules = new Map<string, number>();
|
||||
for (const m of e.misses) rules.set(m.rule, (rules.get(m.rule) ?? 0) + 1);
|
||||
lines.push(
|
||||
`${pkg}: ${e.objects.length} constant objects (${fields} fields); near misses ${[...rules].map(([r, n]) => `${r} ${n}`).join(", ") || "none"}`,
|
||||
);
|
||||
for (const o of e.objects) {
|
||||
if (referenced.has(o.site.atom)) continue;
|
||||
lines.push(` graph ${loc(o.site.node)} ${describe(o, 0, new Set([o.site.atom]))}`);
|
||||
}
|
||||
for (const m of e.misses.filter((x) => x.rule !== "allocated-repeatedly").slice(0, 40)) {
|
||||
lines.push(
|
||||
` near miss ${loc(m.site.node)}${m.key === null ? "" : ` .${m.key}`}: ${m.rule} (${m.detail})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1:
|
||||
* the abstract domain of the allocation-site points-to analysis.
|
||||
*
|
||||
* An abstract value is a set of atoms. An atom names a set of runtime
|
||||
* values: a primitive kind, an allocation site (object or array literal, or
|
||||
* a value a builtin creates at one call site), the instances of one class,
|
||||
* one class value, one function (closure site), a builtin object, or ⊤ —
|
||||
* every value the analysis does not track. Value nodes hold atom sets and
|
||||
* flow into each other along inclusion edges; the solver (analyze.ts)
|
||||
* attaches load, store and call constraints that fire as atoms arrive. */
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
|
||||
export type AtomKind =
|
||||
| "prim"
|
||||
| "top"
|
||||
| "object"
|
||||
| "array"
|
||||
| "instance"
|
||||
| "class"
|
||||
| "function"
|
||||
| "builtin"
|
||||
| "promise"
|
||||
| "collection"
|
||||
| "dictionary";
|
||||
|
||||
export interface Atom {
|
||||
readonly id: number;
|
||||
readonly kind: AtomKind;
|
||||
/** Short display name, e.g. `string`, `ZodString`, `{status,value}@types.js:804`. */
|
||||
readonly label: string;
|
||||
/** The syntax that creates the atom: the literal, class or function
|
||||
* declaration, or the builtin call. Undefined for prims, ⊤ and builtins. */
|
||||
readonly node?: ts.Node | undefined;
|
||||
/** builtin atoms: the dotted path (`Object.keys`, `array.push`). */
|
||||
readonly path?: string | undefined;
|
||||
/** method atoms of builtin receivers: the receiver atom. */
|
||||
readonly receiver?: Atom | undefined;
|
||||
/** function atoms: the analysis context of the function's clone (a
|
||||
* method's receiver class), "" for the context-insensitive default. */
|
||||
readonly ctx?: string | undefined;
|
||||
}
|
||||
|
||||
export class AtomTable {
|
||||
readonly atoms: Atom[] = [];
|
||||
private readonly byKey = new Map<string, Atom>();
|
||||
|
||||
intern(key: string, make: (id: number) => Omit<Atom, "id">): Atom {
|
||||
let atom = this.byKey.get(key);
|
||||
if (atom === undefined) {
|
||||
const id = this.atoms.length;
|
||||
atom = { id, ...make(id) };
|
||||
this.atoms.push(atom);
|
||||
this.byKey.set(key, atom);
|
||||
}
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
|
||||
/** Object.prototype members: a read of one of these names on a plain
|
||||
* object resolves on the prototype, never to an absent own key. */
|
||||
export const OBJECT_PROTOTYPE_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"constructor",
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable",
|
||||
"toLocaleString",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"__defineGetter__",
|
||||
"__defineSetter__",
|
||||
"__lookupGetter__",
|
||||
"__lookupSetter__",
|
||||
"__proto__",
|
||||
]);
|
||||
|
||||
/** Global names the analysis models as builtin objects. Any other global is ⊤. */
|
||||
export const MODELED_GLOBALS: ReadonlySet<string> = new Set([
|
||||
"Object",
|
||||
"Array",
|
||||
"JSON",
|
||||
"Math",
|
||||
"Number",
|
||||
"String",
|
||||
"Boolean",
|
||||
"Symbol",
|
||||
"Promise",
|
||||
"Map",
|
||||
"Set",
|
||||
"WeakMap",
|
||||
"WeakSet",
|
||||
"Error",
|
||||
"TypeError",
|
||||
"RangeError",
|
||||
"SyntaxError",
|
||||
"Date",
|
||||
"RegExp",
|
||||
"console",
|
||||
"Reflect",
|
||||
"parseInt",
|
||||
"parseFloat",
|
||||
"isNaN",
|
||||
"isFinite",
|
||||
"encodeURIComponent",
|
||||
"decodeURIComponent",
|
||||
"encodeURI",
|
||||
"decodeURI",
|
||||
"BigInt",
|
||||
"Proxy",
|
||||
]);
|
||||
|
||||
/** Results of string (and number) methods, by name. Anything else is ⊤. */
|
||||
type MethodResult = "string" | "number" | "boolean" | "array";
|
||||
export const STRING_METHOD_RESULTS: ReadonlyMap<string, MethodResult> = new Map<
|
||||
string,
|
||||
MethodResult
|
||||
>([
|
||||
["at", "string"],
|
||||
["charAt", "string"],
|
||||
["charCodeAt", "number"],
|
||||
["codePointAt", "number"],
|
||||
["concat", "string"],
|
||||
["endsWith", "boolean"],
|
||||
["includes", "boolean"],
|
||||
["indexOf", "number"],
|
||||
["lastIndexOf", "number"],
|
||||
["localeCompare", "number"],
|
||||
["match", "array"],
|
||||
["matchAll", "array"],
|
||||
["normalize", "string"],
|
||||
["padEnd", "string"],
|
||||
["padStart", "string"],
|
||||
["repeat", "string"],
|
||||
["replace", "string"],
|
||||
["replaceAll", "string"],
|
||||
["search", "number"],
|
||||
["slice", "string"],
|
||||
["split", "array"],
|
||||
["startsWith", "boolean"],
|
||||
["substring", "string"],
|
||||
["substr", "string"],
|
||||
["toLowerCase", "string"],
|
||||
["toUpperCase", "string"],
|
||||
["toLocaleLowerCase", "string"],
|
||||
["toLocaleUpperCase", "string"],
|
||||
["toString", "string"],
|
||||
["trim", "string"],
|
||||
["trimEnd", "string"],
|
||||
["trimStart", "string"],
|
||||
["valueOf", "string"],
|
||||
["test", "boolean"],
|
||||
["toFixed", "string"],
|
||||
["toPrecision", "string"],
|
||||
["toExponential", "string"],
|
||||
]);
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Shape inference for untyped npm JavaScript
|
||||
* (results/design-shape-inference/DESIGN.md). Stage S1: report only. */
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import { analyzeShapes } from "./analyze.js";
|
||||
import { buildShapeReport } from "./report.js";
|
||||
import { computeConstantGraphs, constantGraphReport } from "./constant-graphs.js";
|
||||
|
||||
export { analyzeShapes } from "./analyze.js";
|
||||
export { buildShapeReport } from "./report.js";
|
||||
|
||||
/** SCRIPTC_DEBUG_SHAPES: `1` reports package code in hot functions,
|
||||
* `verbose` every package site, `all` user files too, any other value only
|
||||
* files whose path contains it. Writes to stderr; changes nothing else. */
|
||||
export function reportShapes(program: ts.Program, filter: string): void {
|
||||
const start = performance.now();
|
||||
const analysis = analyzeShapes(program, program.getTypeChecker());
|
||||
// `const`: the constant object graphs (DESIGN-constant-objects.md, stage 1).
|
||||
const lines =
|
||||
filter === "const"
|
||||
? constantGraphReport(analysis, computeConstantGraphs(analysis))
|
||||
: buildShapeReport(analysis, filter).lines;
|
||||
const ms = (performance.now() - start).toFixed(0);
|
||||
process.stderr.write(`[shapes] ${ms} ms\n${lines.map((l) => `[shapes] ${l}`).join("\n")}\n`);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/* Shape inference stage S1: the plan the analysis would hand the lowering,
|
||||
* as a report (DESIGN.md §4.5, §5.5). Nothing here changes compilation.
|
||||
*
|
||||
* Per allocation site of package code, the verdict:
|
||||
* P local — a native record would be exact: no reflection, no added
|
||||
* keys, no computed writes, and its objects never escape;
|
||||
* P escaping — exact as a record behind identity-keeping capsules: it
|
||||
* escapes, but the census leaves every later operation
|
||||
* answerable (fields untyped code may write become dyn);
|
||||
* G <reason> — keeps today's dynamic object, with the rule that decided.
|
||||
* Plus class field types, the census, and the hot functions' value types. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { OBJECT_PROTOTYPE_MEMBERS, type Atom } from "./domain.js";
|
||||
import { locLabel, type FunctionInfo, type LiteralSite, type ShapeAnalysis } from "./analyze.js";
|
||||
|
||||
export interface SiteVerdict {
|
||||
readonly site: LiteralSite;
|
||||
readonly label: string;
|
||||
readonly verdict: "P-local" | "P-escaping" | "G";
|
||||
readonly reasons: string[];
|
||||
/** P-escaping: fields untyped code may write (they become dyn slots). */
|
||||
readonly writableByTop: string[];
|
||||
readonly freezable: boolean;
|
||||
readonly brand: boolean;
|
||||
}
|
||||
|
||||
export interface ShapeReport {
|
||||
readonly sites: SiteVerdict[];
|
||||
readonly hot: Set<FunctionInfo>;
|
||||
readonly lines: string[];
|
||||
}
|
||||
|
||||
const MAX_ATOMS_SHOWN = 6;
|
||||
|
||||
function fieldKeys(fields: ReadonlyMap<string, number> | undefined): string[] {
|
||||
return fields === undefined ? [] : [...fields.keys()];
|
||||
}
|
||||
|
||||
function fieldEntries(fields: ReadonlyMap<string, number> | undefined): [string, number][] {
|
||||
return fields === undefined ? [] : [...fields.entries()];
|
||||
}
|
||||
|
||||
export function describeValue(analysis: ShapeAnalysis, node: number): string {
|
||||
const atoms = [...analysis.nodes[node]!.atoms];
|
||||
if (atoms.length === 0) return "∅";
|
||||
const labels = [...new Set(atoms.map((a) => a.label))];
|
||||
labels.sort((a, b) => (a === "⊤" ? 1 : b === "⊤" ? -1 : a.localeCompare(b)));
|
||||
const shown = labels.slice(0, MAX_ATOMS_SHOWN).join(" | ");
|
||||
if (labels.length <= MAX_ATOMS_SHOWN) return shown;
|
||||
return `${shown} | +${labels.length - MAX_ATOMS_SHOWN}${labels.includes("⊤") ? " | ⊤" : ""}`;
|
||||
}
|
||||
|
||||
function atomsOf(analysis: ShapeAnalysis, node: number): Set<Atom> {
|
||||
return analysis.nodes[node]!.atoms;
|
||||
}
|
||||
|
||||
/** Functions reached from call sites in loops (or loop callbacks), and
|
||||
* everything they call: the static hotness heuristic (DESIGN.md §8.1). */
|
||||
export function hotFunctions(analysis: ShapeAnalysis): Set<FunctionInfo> {
|
||||
const hot = new Set<FunctionInfo>();
|
||||
const callees = new Map<FunctionInfo | null, FunctionInfo[]>();
|
||||
const work: FunctionInfo[] = [];
|
||||
for (const c of analysis.calls) {
|
||||
let l = callees.get(c.caller);
|
||||
if (l === undefined) callees.set(c.caller, (l = []));
|
||||
l.push(c.callee);
|
||||
if (c.inLoop && !hot.has(c.callee)) {
|
||||
hot.add(c.callee);
|
||||
work.push(c.callee);
|
||||
}
|
||||
}
|
||||
while (work.length > 0) {
|
||||
const f = work.pop()!;
|
||||
for (const g of callees.get(f) ?? []) {
|
||||
if (!hot.has(g)) {
|
||||
hot.add(g);
|
||||
work.push(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hot;
|
||||
}
|
||||
|
||||
export function buildShapeReport(analysis: ShapeAnalysis, filter: string): ShapeReport {
|
||||
const lines: string[] = [];
|
||||
const all = filter === "all" || filter === "verbose";
|
||||
const verbose = filter === "verbose";
|
||||
const fileFilter = filter !== "1" && !all ? filter : null;
|
||||
const wanted = (n: ts.Node, inPackage: boolean): boolean => {
|
||||
if (fileFilter !== null) return n.getSourceFile().fileName.includes(fileFilter);
|
||||
return all || inPackage;
|
||||
};
|
||||
|
||||
// Census facts: operations whose receiver may be ⊤ reach every escaped atom.
|
||||
|
||||
// Index ops by the site atoms they can reach directly.
|
||||
const byAtom = new Map<Atom, typeof analysis.ops>();
|
||||
const topOps: typeof analysis.ops = [];
|
||||
for (const o of analysis.ops) {
|
||||
const atoms = atomsOf(analysis, o.recv);
|
||||
for (const a of atoms) {
|
||||
if (a.kind !== "object" && a.kind !== "array") continue;
|
||||
let l = byAtom.get(a);
|
||||
if (l === undefined) byAtom.set(a, (l = []));
|
||||
l.push(o);
|
||||
}
|
||||
if (atoms.has(analysis.top)) topOps.push(o);
|
||||
}
|
||||
|
||||
// Key-order groups: sites with the same key set and different orders need brands.
|
||||
const orders = new Map<string, Set<string>>();
|
||||
for (const s of analysis.sites) {
|
||||
if (s.kind !== "object" || s.keys === null) continue;
|
||||
const set = [...s.keys].sort().join(",");
|
||||
let o = orders.get(set);
|
||||
if (o === undefined) orders.set(set, (o = new Set()));
|
||||
o.add(s.keys.join(","));
|
||||
}
|
||||
|
||||
const verdicts: SiteVerdict[] = [];
|
||||
for (const s of analysis.sites) {
|
||||
if (!s.inPackage && !all && fileFilter === null) continue;
|
||||
const reasons: string[] = [];
|
||||
const escapedWhy = analysis.escaped.get(s.atom);
|
||||
const escapes = escapedWhy !== undefined;
|
||||
if (s.hasAccessor) reasons.push("accessor in literal");
|
||||
if (s.hasProto) reasons.push(s.kind === "array" ? "hole in literal" : "__proto__ in literal");
|
||||
if (s.hasComputedKey) reasons.push("computed key in literal");
|
||||
for (const sp of s.spreads)
|
||||
if (atomsOf(analysis, sp).has(analysis.top)) reasons.push("spread of ⊤");
|
||||
const direct = byAtom.get(s.atom) ?? [];
|
||||
const ops = escapes ? [...direct, ...topOps] : direct;
|
||||
const keys = new Set(s.keys ?? []);
|
||||
const fieldsOf = analysis.fields.get(s.atom);
|
||||
const knownKeys =
|
||||
s.keys !== null ? keys : new Set([...fieldKeys(fieldsOf)].filter((k) => k !== "*"));
|
||||
let freezable = false;
|
||||
const seen = new Set<string>();
|
||||
const note = (r: string): void => {
|
||||
if (!seen.has(r)) {
|
||||
seen.add(r);
|
||||
reasons.push(r);
|
||||
}
|
||||
};
|
||||
for (const o of ops) {
|
||||
const viaTop = !atomsOf(analysis, o.recv).has(s.atom);
|
||||
const where = `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`;
|
||||
switch (o.kind) {
|
||||
case "delete":
|
||||
case "defineProperty":
|
||||
case "setPrototypeOf":
|
||||
case "seal":
|
||||
case "proxy":
|
||||
note(`${o.kind} at ${where}`);
|
||||
break;
|
||||
case "assign":
|
||||
note(`Object.assign target at ${where}`);
|
||||
break;
|
||||
case "freeze":
|
||||
freezable = true;
|
||||
break;
|
||||
case "computedWrite":
|
||||
if (!viaTop)
|
||||
note(s.kind === "array" ? `index write at ${where}` : `computed write at ${where}`);
|
||||
break;
|
||||
case "lengthWrite":
|
||||
if (s.kind === "array") note(`length write at ${where}`);
|
||||
break;
|
||||
case "write":
|
||||
if (s.kind === "object" && !viaTop && o.name !== null && !knownKeys.has(o.name))
|
||||
note(`adds key '${o.name}' at ${where}`);
|
||||
break;
|
||||
case "read":
|
||||
if (
|
||||
s.kind === "object" &&
|
||||
!underPromiseGuard(o.node) &&
|
||||
escapes &&
|
||||
o.name !== null &&
|
||||
!viaTop &&
|
||||
!knownKeys.has(o.name) &&
|
||||
!OBJECT_PROTOTYPE_MEMBERS.has(o.name)
|
||||
)
|
||||
note(`reads absent key '${o.name}' at ${where} (escapes)`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
let brand = false;
|
||||
if (s.kind === "object" && s.keys !== null) {
|
||||
const o = orders.get([...s.keys].sort().join(","));
|
||||
if (o !== undefined && o.size > 1) brand = true;
|
||||
}
|
||||
const writableByTop: string[] = [];
|
||||
if (escapes) {
|
||||
if (analysis.topComputedWrites.length > 0) writableByTop.push("*");
|
||||
else for (const k of knownKeys) if (analysis.topWriteNames.has(k)) writableByTop.push(k);
|
||||
}
|
||||
const verdict: SiteVerdict["verdict"] =
|
||||
reasons.length > 0 ? "G" : escapes ? "P-escaping" : "P-local";
|
||||
const label = s.atom.label;
|
||||
if (escapes) reasons.push(`escapes: ${escapedWhy}`);
|
||||
verdicts.push({ site: s, label, verdict, reasons, writableByTop, freezable, brand });
|
||||
}
|
||||
|
||||
const hot = hotFunctions(analysis);
|
||||
const hotNodes = new Set<ts.Node>([...hot].map((f) => f.node));
|
||||
|
||||
// ---- print ----
|
||||
const pkgFns = [...analysis.functions.values()].filter((f) => f.inPackage);
|
||||
lines.push(
|
||||
`analyzed ${analysis.files.length} files (${analysis.packageFiles} package JS), ${analysis.functions.size} functions (${pkgFns.length} in packages), ${analysis.atoms.atoms.length} atoms, ${analysis.nodes.length} value nodes, ${analysis.steps} steps${analysis.budgetExceeded ? " — BUDGET EXCEEDED, results incomplete" : ""}`,
|
||||
);
|
||||
const topNames = [...analysis.topWriteNames.entries()].map(
|
||||
([n, node]) => `${n} (${locLabel(node)})`,
|
||||
);
|
||||
lines.push(
|
||||
`census: writes on ⊤ receivers: ${topNames.length === 0 ? "none" : topNames.slice(0, 20).join(", ")}${topNames.length > 20 ? ` +${topNames.length - 20}` : ""}`,
|
||||
);
|
||||
lines.push(
|
||||
`census: computed writes on ⊤ receivers: ${analysis.topComputedWrites.length === 0 ? "none" : analysis.topComputedWrites.slice(0, 8).map(locLabel).join(", ")}`,
|
||||
);
|
||||
const topReflect = topOps.filter(
|
||||
(o) =>
|
||||
o.kind !== "read" &&
|
||||
o.kind !== "computedRead" &&
|
||||
o.kind !== "write" &&
|
||||
o.kind !== "computedWrite",
|
||||
);
|
||||
lines.push(
|
||||
`census: reflection on ⊤ receivers: ${topReflect.length === 0 ? "none" : topReflect.map((o) => `${o.kind} (${locLabel(o.node)})`).join(", ")}`,
|
||||
);
|
||||
const escapedPkg = [...analysis.escaped.keys()].filter(
|
||||
(a) =>
|
||||
a.node !== undefined &&
|
||||
wanted(a.node, a.node.getSourceFile().fileName.includes("/node_modules/")),
|
||||
);
|
||||
lines.push(
|
||||
`escaped atoms: ${analysis.escaped.size} (${escapedPkg.length} shown below where relevant)`,
|
||||
);
|
||||
const firstEscapes = [...analysis.escaped.entries()]
|
||||
.filter(([, why]) => !why.startsWith("via "))
|
||||
.slice(0, 12);
|
||||
for (const [atom, why] of firstEscapes) lines.push(` escape root: ${atom.label}: ${why}`);
|
||||
|
||||
const counts = { "P-local": 0, "P-escaping": 0, G: 0 };
|
||||
const hotCounts = { "P-local": 0, "P-escaping": 0, G: 0 };
|
||||
const inHot = (n: ts.Node): boolean => {
|
||||
let p: ts.Node | undefined = n.parent;
|
||||
while (p !== undefined) {
|
||||
if (hotNodes.has(p)) return true;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const v of verdicts) {
|
||||
counts[v.verdict]++;
|
||||
if (inHot(v.site.node)) hotCounts[v.verdict]++;
|
||||
}
|
||||
lines.push(
|
||||
`sites: ${verdicts.length} (P local ${counts["P-local"]}, P escaping ${counts["P-escaping"]}, G ${counts.G}); in hot functions: P local ${hotCounts["P-local"]}, P escaping ${hotCounts["P-escaping"]}, G ${hotCounts.G}`,
|
||||
);
|
||||
for (const v of verdicts) {
|
||||
if (!wanted(v.site.node, v.site.inPackage)) continue;
|
||||
if (!verbose && !inHot(v.site.node)) continue;
|
||||
const extra: string[] = [];
|
||||
if (v.brand) extra.push("needs brand (key order)");
|
||||
if (v.freezable) extra.push("freezable");
|
||||
if (v.writableByTop.length > 0) extra.push(`⊤-writable: ${v.writableByTop.join(",")}`);
|
||||
const fields = analysis.fields.get(v.site.atom);
|
||||
const ftypes =
|
||||
v.site.kind === "array"
|
||||
? `elem: ${analysis.elements.has(v.site.atom) ? describeValue(analysis, analysis.elements.get(v.site.atom)!) : "∅"}`
|
||||
: fieldEntries(fields)
|
||||
.filter(([k]) => k !== "*")
|
||||
.map(([k, id]) => `${k}: ${describeValue(analysis, id)}`)
|
||||
.join("; ");
|
||||
lines.push(
|
||||
`site ${v.label}${inHot(v.site.node) ? " [hot]" : ""}: ${v.verdict}${v.reasons.length > 0 ? ` — ${v.reasons.join("; ")}` : ""}${extra.length > 0 ? ` [${extra.join("; ")}]` : ""}`,
|
||||
);
|
||||
if (ftypes.length > 0) lines.push(` ${ftypes}`);
|
||||
}
|
||||
|
||||
// Class fields.
|
||||
for (const atom of analysis.atoms.atoms) {
|
||||
if (atom.kind !== "instance" || atom.node === undefined) continue;
|
||||
const inPackage = atom.node.getSourceFile().fileName.includes("/node_modules/");
|
||||
if (!wanted(atom.node, inPackage)) continue;
|
||||
const fm = analysis.fields.get(atom);
|
||||
if (fm === undefined || fm.size === 0) continue;
|
||||
const esc = analysis.escaped.get(atom);
|
||||
const parts = [...fm.entries()].map(([k, id]) => {
|
||||
const w =
|
||||
esc !== undefined &&
|
||||
!k.startsWith("#") &&
|
||||
(analysis.topComputedWrites.length > 0 || analysis.topWriteNames.has(k))
|
||||
? " (⊤-writable)"
|
||||
: "";
|
||||
return `${k}: ${describeValue(analysis, id)}${w}`;
|
||||
});
|
||||
lines.push(
|
||||
`class ${atom.label}${esc !== undefined ? ` (escapes: ${esc})` : ""}: ${parts.join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Hot functions.
|
||||
const hotPkg = [...hot].filter((info) => wanted(info.node, info.inPackage));
|
||||
lines.push(`hot functions: ${hotPkg.length} (clones per receiver class counted separately)`);
|
||||
for (const info of hotPkg) {
|
||||
const n = info.node;
|
||||
const params = info.params.map((p, i) => `p${i}=${describeValue(analysis, p)}`).join(", ");
|
||||
const flags = [
|
||||
info.usesArguments ? "uses arguments" : "",
|
||||
analysis.escaped.size > 0 && [...analysis.escaped.keys()].some((a) => a.node === n)
|
||||
? "escapes"
|
||||
: "",
|
||||
].filter((x) => x !== "");
|
||||
lines.push(
|
||||
`fn ${info.label}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}: this=${describeValue(analysis, info.thisNode)}${params.length > 0 ? `, ${params}` : ""}, ret=${describeValue(analysis, info.ret)}`,
|
||||
);
|
||||
}
|
||||
const why = process.env["SCRIPTC_DEBUG_SHAPES_WHY"];
|
||||
if (why !== undefined) {
|
||||
for (const info of analysis.functions.values()) {
|
||||
if (!info.label.startsWith(why)) continue;
|
||||
for (const [what, node] of [
|
||||
["ret", info.ret],
|
||||
...info.params.map((p, i) => [`p${i}`, p] as const),
|
||||
] as const) {
|
||||
if (!analysis.nodes[node]!.atoms.has(analysis.top)) continue;
|
||||
lines.push(`why ⊤ in ${info.label} ${what}:`);
|
||||
let cur: number | string | undefined = node;
|
||||
for (let i = 0; i < 40 && typeof cur === "number"; i++) {
|
||||
lines.push(` ${analysis.nodeLabel.get(cur) ?? `node ${cur}`}`);
|
||||
cur = analysis.topCause.get(cur);
|
||||
}
|
||||
if (typeof cur === "string") lines.push(` seed: ${cur}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { sites: verdicts, hot, lines };
|
||||
}
|
||||
|
||||
/** A read inside the true arm of a promise test (`isAsync(x)`,
|
||||
* `x instanceof Promise`): flow-sensitive narrowing removes the record
|
||||
* arms there. Stage S1's locals are flow-insensitive, so the report
|
||||
* recognizes the guard syntactically. */
|
||||
function underPromiseGuard(node: ts.Node): boolean {
|
||||
let child: ts.Node = node;
|
||||
let p = node.parent;
|
||||
while (p !== undefined && !ts.isFunctionLike(p)) {
|
||||
let cond: ts.Expression | undefined;
|
||||
if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression;
|
||||
if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition;
|
||||
if (cond !== undefined && isPromiseTest(cond)) return true;
|
||||
child = p;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPromiseTest(e: ts.Expression): boolean {
|
||||
if (ts.isParenthesizedExpression(e)) return isPromiseTest(e.expression);
|
||||
if (ts.isBinaryExpression(e)) {
|
||||
if (e.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword)
|
||||
return ts.isIdentifier(e.right) && e.right.text === "Promise";
|
||||
if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return isPromiseTest(e.left) || isPromiseTest(e.right);
|
||||
return false;
|
||||
}
|
||||
if (ts.isCallExpression(e) && ts.isIdentifier(e.expression))
|
||||
return /^is(Async|Promise|Thenable)$/.test(e.expression.text);
|
||||
return false;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { tokenEquivalentSource } from "./token-equivalence.js";
|
||||
import { tokenEquivalentSource, tokenPositionEquivalentSource } from "./token-equivalence.js";
|
||||
|
||||
const equivalent = (previous: string, current: string, path = "/src/a.ts") =>
|
||||
tokenEquivalentSource(path, previous, current);
|
||||
const positionEquivalent = (previous: string, current: string, path = "/src/a.ts") =>
|
||||
tokenPositionEquivalentSource(path, previous, current);
|
||||
|
||||
test("whitespace and plain comments are trivia", () => {
|
||||
expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true);
|
||||
@@ -105,3 +107,50 @@ test("text the scanner cannot classify is never equivalent", () => {
|
||||
expect(equivalent("const a = <b/>; // a\n", "const a = <b/>; // b\n", "/src/a.tsx")).toBe(false);
|
||||
expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false);
|
||||
});
|
||||
|
||||
test("a / after a non-null assertion divides", () => {
|
||||
expect(
|
||||
equivalent("const q = data[i]! / 7; // ratio\n", "const q = data[i]! / 7; // the ratio\n"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
equivalent("const q = value! / 7; // ratio\n", "const q = value! / 7; // the ratio\n"),
|
||||
).toBe(true);
|
||||
expect(equivalent("const q = this! / 7; // ratio\n", "const q = this! / 7; // the ratio\n")).toBe(
|
||||
true,
|
||||
);
|
||||
// A prefix `!` negates a regular expression, whose `//` is code.
|
||||
expect(
|
||||
equivalent("const t = !/[//]/.test(s); c = 1;\n", "const t = !/[//]/.test(s); c = 2;\n"),
|
||||
).toBe(false);
|
||||
expect(equivalent("return !/[//]/.test(s); // one\n", "return !/[//]/.test(s); // two\n")).toBe(
|
||||
true,
|
||||
);
|
||||
// After a line break, `!` starts a new expression.
|
||||
expect(equivalent("a\n!/[//]/.test(s); c = 1;\n", "a\n!/[//]/.test(s); c = 2;\n")).toBe(false);
|
||||
// `f()!` and `if (a) !` read the same up to the `/`.
|
||||
expect(equivalent("const q = f()! / 2; // one\n", "const q = f()! / 2; // two\n")).toBe(false);
|
||||
});
|
||||
|
||||
test("position equivalence also keeps every token's line and column", () => {
|
||||
const source = "const a = 1; // one\nconst b = a; /* two */ f(b);\n";
|
||||
// A comment's text within its line, after the line's last token.
|
||||
expect(
|
||||
positionEquivalent(source, "const a = 1; // the first\nconst b = a; /* two */ f(b);\n"),
|
||||
).toBe(true);
|
||||
// Comments and whitespace after the last token, and trailing whitespace.
|
||||
expect(positionEquivalent(source, source + "// appended\n\n")).toBe(true);
|
||||
expect(
|
||||
positionEquivalent(source, "const a = 1; // one\nconst b = a; /* two */ f(b); \n"),
|
||||
).toBe(true);
|
||||
expect(positionEquivalent(source, source.replaceAll("\n", "\r\n"))).toBe(true);
|
||||
// A longer comment before a token moves its column; a new line moves the
|
||||
// lines below it. Both stay token-equivalent.
|
||||
const column = "const a = 1; // one\nconst b = a; /* the second */ f(b);\n";
|
||||
const line = "// header\nconst a = 1; // one\nconst b = a; /* two */ f(b);\n";
|
||||
expect(positionEquivalent(source, column)).toBe(false);
|
||||
expect(positionEquivalent(source, line)).toBe(false);
|
||||
expect(equivalent(source, column)).toBe(true);
|
||||
expect(equivalent(source, line)).toBe(true);
|
||||
// A token edit is never equivalent.
|
||||
expect(positionEquivalent(source, source.replace("1", "2"))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
* before it. After `)` it divides unless the parenthesis closes an `if`,
|
||||
* `while`, `for`, `for await`, or `with` header. After `}`, `++` or
|
||||
* `--` the scanner cannot tell which, so it gives up, as it does for
|
||||
* escaped identifiers, which can spell keywords.
|
||||
* escaped identifiers, which can spell keywords. After `!` in
|
||||
* TypeScript it divides when the `!` follows an operand on the same line
|
||||
* (a non-null assertion, `x! / 2`) and gives up when that operand is a
|
||||
* `)`, `}`, `++` or `--`.
|
||||
* - A `/` with a line break (or a comment that spans lines) since the
|
||||
* previous token gives up: whether automatic semicolon insertion ends the
|
||||
* statement before it decides how it reads.
|
||||
@@ -35,6 +38,8 @@ interface Token {
|
||||
text: string;
|
||||
comment: boolean;
|
||||
lineBreakBefore: boolean;
|
||||
/** The offset of the token's first code unit. */
|
||||
start: number;
|
||||
}
|
||||
|
||||
/** Keywords after which `/` starts a regular expression. */
|
||||
@@ -135,14 +140,20 @@ function scan(path: string, source: string): Token[] | null {
|
||||
// A line break, or a comment spanning lines, since the last token other
|
||||
// than a comment.
|
||||
let breakSinceCode = false;
|
||||
// The previous token and the one before it, and whether a `)` closed a
|
||||
// statement header.
|
||||
const last: { token: string | null; before: string | null } = { token: null, before: null };
|
||||
// The previous token and the one before it, whether a line break came
|
||||
// before the previous token, and whether a `)` closed a statement header.
|
||||
const last: { token: string | null; before: string | null; broke: boolean } = {
|
||||
token: null,
|
||||
before: null,
|
||||
broke: false,
|
||||
};
|
||||
let closedHeader = false;
|
||||
// Every token ends at `index` when it is pushed.
|
||||
const push = (text: string, comment = false): void => {
|
||||
tokens.push({ text, comment, lineBreakBefore });
|
||||
tokens.push({ text, comment, lineBreakBefore, start: index - text.length });
|
||||
lineBreakBefore = false;
|
||||
if (!comment) {
|
||||
last.broke = breakSinceCode;
|
||||
breakSinceCode = false;
|
||||
last.before = last.token;
|
||||
last.token = text;
|
||||
@@ -291,7 +302,19 @@ function scan(path: string, source: string): Token[] | null {
|
||||
else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`"))
|
||||
regex = false;
|
||||
else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false;
|
||||
else regex = true;
|
||||
else if (before === "!" && typeScript && !last.broke && last.before !== null) {
|
||||
// `x! / 2` divides: a `!` right after an operand on the same line is
|
||||
// a non-null assertion. `!/re/.test(s)` negates a regular expression.
|
||||
const operand = last.before;
|
||||
if (operand === ")" || operand === "}" || operand === "++" || operand === "--") return null;
|
||||
regex = !(
|
||||
operand === "]" ||
|
||||
operand.startsWith("'") ||
|
||||
operand.startsWith('"') ||
|
||||
operand.endsWith("`") ||
|
||||
(/^[A-Za-z0-9_$]/.test(operand) && !EXPRESSION_KEYWORDS.has(operand))
|
||||
);
|
||||
} else regex = true;
|
||||
if (regex) {
|
||||
const start = index;
|
||||
index++;
|
||||
@@ -373,9 +396,33 @@ function scan(path: string, source: string): Token[] | null {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** Whether `current` differs from `previous` only in whitespace and in
|
||||
* comments that cannot affect compilation. */
|
||||
export function tokenEquivalentSource(path: string, previous: string, current: string): boolean {
|
||||
/** The line and column of each token's start, with the line terminators
|
||||
* source locations count (backend/source-locations.ts). */
|
||||
function tokenPositions(source: string, tokens: readonly Token[]): number[] {
|
||||
const positions: number[] = [];
|
||||
let line = 0;
|
||||
let lineStart = 0;
|
||||
let offset = 0;
|
||||
for (const token of tokens) {
|
||||
for (; offset < token.start; offset++) {
|
||||
const code = source.charCodeAt(offset);
|
||||
if (code === 13 && source.charCodeAt(offset + 1) === 10) offset++;
|
||||
if (code === 10 || code === 13 || code === 0x2028 || code === 0x2029) {
|
||||
line++;
|
||||
lineStart = offset + 1;
|
||||
}
|
||||
}
|
||||
positions.push(line, token.start - lineStart);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
function equivalentSources(
|
||||
path: string,
|
||||
previous: string,
|
||||
current: string,
|
||||
positions: boolean,
|
||||
): boolean {
|
||||
if (previous === current) return true;
|
||||
const left = scan(path, previous);
|
||||
if (left === null) return false;
|
||||
@@ -387,5 +434,27 @@ export function tokenEquivalentSource(path: string, previous: string, current: s
|
||||
if (a.text !== b.text || a.comment !== b.comment) return false;
|
||||
if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false;
|
||||
}
|
||||
return true;
|
||||
if (!positions) return true;
|
||||
const before = tokenPositions(previous, left);
|
||||
const after = tokenPositions(current, right);
|
||||
return before.every((value, index) => value === after[index]);
|
||||
}
|
||||
|
||||
/** Whether `current` differs from `previous` only in whitespace and in
|
||||
* comments that cannot affect compilation. */
|
||||
export function tokenEquivalentSource(path: string, previous: string, current: string): boolean {
|
||||
return equivalentSources(path, previous, current, false);
|
||||
}
|
||||
|
||||
/** Whether `current` is token-equivalent to `previous` (tokenEquivalentSource)
|
||||
* and every token keeps its line and column: the edit leaves the source
|
||||
* positions debug information records unchanged (a comment's text within
|
||||
* its line, comments and whitespace after the last token, trailing
|
||||
* whitespace). */
|
||||
export function tokenPositionEquivalentSource(
|
||||
path: string,
|
||||
previous: string,
|
||||
current: string,
|
||||
): boolean {
|
||||
return equivalentSources(path, previous, current, true);
|
||||
}
|
||||
|
||||
@@ -219,3 +219,53 @@ test("a supplied file that changes is a different entry", () => {
|
||||
expect(store.entries.size).toBe(2);
|
||||
writeFileSync(fallback, fallbackText);
|
||||
});
|
||||
|
||||
/** The program's files, recording which ones were read. */
|
||||
function recording(texts: Record<string, string>): ((name: string) => SourceFile | undefined) & {
|
||||
read: string[];
|
||||
} {
|
||||
const inner = files(texts);
|
||||
const read: string[] = [];
|
||||
return Object.assign((name: string) => (read.push(name), inner(name)), { read });
|
||||
}
|
||||
|
||||
test("a program of modules stores its answer without reading the supplied files", () => {
|
||||
const store = memoryStore();
|
||||
setLibDiagnosticsStore(store, "checker");
|
||||
const sources = recording({ [main]: "export const n: number = 'x';\n" });
|
||||
reusedLibDiagnostics(
|
||||
sessionProgram({ semantic: new Map([[main, []]]) }),
|
||||
names,
|
||||
supplied,
|
||||
sources,
|
||||
);
|
||||
expect(sources.read).toEqual([main]);
|
||||
const [text] = [...store.entries.values()];
|
||||
expect(JSON.parse(text!).globals).toBeNull();
|
||||
// A later program of modules reuses it, again without reading them.
|
||||
const again = recording({ [main]: "export const n: number = 'x';\n" });
|
||||
const program = sessionProgram({ semantic: new Map([[main, []]]) });
|
||||
expect(reusedLibDiagnostics(program, names, supplied, again)).not.toBeNull();
|
||||
expect(program.asked).toEqual([`checked ${main}`]);
|
||||
expect(again.read).toEqual([main]);
|
||||
});
|
||||
|
||||
test("a script that uses an answer stored without global names collects and stores them", () => {
|
||||
const store = memoryStore();
|
||||
setLibDiagnosticsStore(store, "checker");
|
||||
const semantic = new Map([[main, []]]);
|
||||
reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile);
|
||||
const script = recording({ [main]: "const total = 1;\n" });
|
||||
const program = sessionProgram({ semantic });
|
||||
expect(reusedLibDiagnostics(program, names, supplied, script)).not.toBeNull();
|
||||
expect(program.asked).toEqual([`checked ${main}`]);
|
||||
expect(script.read).toEqual([main, lib, fallback]);
|
||||
const [text] = [...store.entries.values()];
|
||||
expect(new Set(JSON.parse(text!).globals)).toEqual(new Set(["NaN", "Array", "process"]));
|
||||
// A script that declares one of them runs the full check, and the entry keeps its names.
|
||||
const clash = sessionProgram({ semantic });
|
||||
reusedLibDiagnostics(clash, names, supplied, files({ [main]: "declare var NaN: number;\n" }));
|
||||
expect(clash.asked[0]).toBe("getGlobalDiagnostics");
|
||||
expect(store.entries.size).toBe(1);
|
||||
expect(JSON.parse([...store.entries.values()][0]!).globals).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -10,7 +10,14 @@
|
||||
* (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the
|
||||
* order the full check sorts them, by file name. An answer is stored only
|
||||
* when the full check reports no global diagnostics: the global diagnostics
|
||||
* of checking the other files are then the full check's. */
|
||||
* of checking the other files are then the full check's.
|
||||
*
|
||||
* The global names the supplied files declare matter only to programs with
|
||||
* scripts, and collecting them reads every supplied file's syntax tree,
|
||||
* which a program of modules does not otherwise read (about 4 MB at the
|
||||
* peak of a small build). An answer stored by such a program leaves them
|
||||
* out; the first program with scripts that uses the answer collects them
|
||||
* and stores them with it. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { statSync } from "node:fs";
|
||||
import type { Node, SourceFile } from "./ast-types.js";
|
||||
@@ -25,7 +32,7 @@ export interface LibDiagnosticsStore {
|
||||
write(key: string, text: string): void;
|
||||
}
|
||||
|
||||
const SCHEMA = "scriptc.lib-diagnostics.v1";
|
||||
const SCHEMA = "scriptc.lib-diagnostics.v2";
|
||||
|
||||
let configured: { store: LibDiagnosticsStore; checker: string } | null = null;
|
||||
|
||||
@@ -89,7 +96,14 @@ export function reusedLibDiagnostics(
|
||||
const key = entryKey(checker, options, libs);
|
||||
if (key === null) return null;
|
||||
const stored = readEntry(store, key, libs);
|
||||
if (stored !== null && !collides(stored.globals)) {
|
||||
// An answer stored without the supplied files' global names: a program
|
||||
// with scripts collects them and stores them with the answer.
|
||||
let globals = stored?.globals ?? null;
|
||||
if (stored !== null && globals === null && scripts.length > 0) {
|
||||
globals = suppliedGlobalNames(libs, sourceFile);
|
||||
if (globals !== null) writeEntry(store, key, globals, Object.fromEntries(stored.files));
|
||||
}
|
||||
if (stored !== null && (scripts.length === 0 || (globals !== null && !collides(globals)))) {
|
||||
const checked = program.checkedFileDiagnosticData(own);
|
||||
if (checked === null) return null;
|
||||
const byFile = new Map<string, Ts7DiagnosticData[]>(stored.files);
|
||||
@@ -104,20 +118,35 @@ export function reusedLibDiagnostics(
|
||||
const semantic = program.diagnosticData("getSemanticDiagnostics");
|
||||
// A server that cannot check only some files could never use the answer.
|
||||
if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) {
|
||||
const globals = suppliedGlobalNames(libs, sourceFile);
|
||||
if (globals !== null && !collides(globals)) {
|
||||
const names = scripts.length === 0 ? null : suppliedGlobalNames(libs, sourceFile);
|
||||
if (scripts.length === 0 || (names !== null && !collides(names))) {
|
||||
const files: Record<string, Ts7DiagnosticData[]> = {};
|
||||
for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name);
|
||||
try {
|
||||
store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files }));
|
||||
} catch {
|
||||
// Best effort: a failed write is a later miss.
|
||||
}
|
||||
writeEntry(store, key, names, files);
|
||||
}
|
||||
}
|
||||
return { semantic, global };
|
||||
}
|
||||
|
||||
/** Stores an answer: the supplied files' diagnostics, and their global names
|
||||
* when they were collected (null otherwise). Best effort: a failed write is
|
||||
* a later miss. */
|
||||
function writeEntry(
|
||||
store: LibDiagnosticsStore,
|
||||
key: string,
|
||||
globals: ReadonlySet<string> | null,
|
||||
files: Record<string, Ts7DiagnosticData[]>,
|
||||
): void {
|
||||
try {
|
||||
store.write(
|
||||
key,
|
||||
JSON.stringify({ schema: SCHEMA, globals: globals === null ? null : [...globals], files }),
|
||||
);
|
||||
} catch {
|
||||
// A failed write is a later miss.
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a file cannot add to or change the global declarations the
|
||||
* supplied files declare: a module with no `declare global`, no module
|
||||
* augmentation and no UMD `export as namespace` declaration. */
|
||||
@@ -276,7 +305,7 @@ function readEntry(
|
||||
store: LibDiagnosticsStore,
|
||||
key: string,
|
||||
libs: readonly string[],
|
||||
): { files: Map<string, Ts7DiagnosticData[]>; globals: Set<string> } | null {
|
||||
): { files: Map<string, Ts7DiagnosticData[]>; globals: Set<string> | null } | null {
|
||||
let text: string | null;
|
||||
try {
|
||||
text = store.read(key);
|
||||
@@ -290,7 +319,11 @@ function readEntry(
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals))
|
||||
if (
|
||||
entry.schema !== SCHEMA ||
|
||||
entry.files === undefined ||
|
||||
(entry.globals !== null && !Array.isArray(entry.globals))
|
||||
)
|
||||
return null;
|
||||
const files = new Map<string, Ts7DiagnosticData[]>();
|
||||
for (const name of libs) {
|
||||
@@ -298,7 +331,10 @@ function readEntry(
|
||||
if (!Array.isArray(list)) return null;
|
||||
files.set(name, list);
|
||||
}
|
||||
return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) };
|
||||
const globals = Array.isArray(entry.globals)
|
||||
? new Set(entry.globals.filter((name): name is string => typeof name === "string"))
|
||||
: null;
|
||||
return { files, globals };
|
||||
}
|
||||
|
||||
/** Each file's diagnostics (already in the checker's order) in the order the
|
||||
|
||||
@@ -132,6 +132,10 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
|
||||
// binding types as any and reached uses fence instead of passing unchecked
|
||||
2307,
|
||||
2792,
|
||||
// `delete` of a property the inference typed required (2790): JS deletes
|
||||
// any configurable property, and the lowering checks each delete's
|
||||
// receiver itself
|
||||
2790,
|
||||
// useUnknownInCatchVariables' annotation demand ("'e' is of type
|
||||
// 'unknown'", "Object is of type 'unknown'"): a JS catch clause CANNOT
|
||||
// annotate its variable, so the demand is unsatisfiable in source —
|
||||
|
||||
@@ -1747,9 +1747,17 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx):
|
||||
return { kind: "generator", async: true, ...channels };
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
const FETCH_HANDLE_NAMES: ReadonlySet<string> = new Set([
|
||||
"Headers",
|
||||
"Request",
|
||||
"Response",
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
]);
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response, AbortController,
|
||||
* AbortSignal), optionally in arrays or with null/undefined: checked-dynamic
|
||||
* handles wherever they are stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
@@ -1765,9 +1773,7 @@ export function isFetchHandleType(
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(ts.symbolName(symbol) !== "Headers" &&
|
||||
ts.symbolName(symbol) !== "Request" &&
|
||||
ts.symbolName(symbol) !== "Response") ||
|
||||
!FETCH_HANDLE_NAMES.has(ts.symbolName(symbol)) ||
|
||||
!symbol.declarations.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
@@ -7051,6 +7057,43 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A mapped type reached through a type alias whose arguments mention no
|
||||
* type parameter (`Record<never, number>`, `Catalog<typeof data, "image">`,
|
||||
* a module-level alias without arguments):
|
||||
* the checker computed its members from concrete inputs, so an empty
|
||||
* result really is the empty object type, unlike `Partial<T>` inside a
|
||||
* generic body. */
|
||||
function resolvedMappedAlias(t: ts.Type, ctx: TypeMapperCtx): boolean {
|
||||
if (!isMappedShape(t) || t.getAliasSymbol() === undefined) return false;
|
||||
const args = t.getAliasTypeArguments();
|
||||
const seen = new Set<ts.Type>();
|
||||
const concrete = (part: ts.Type): boolean => {
|
||||
if (part.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index)) return false;
|
||||
if (part.flags & (ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional)) return false;
|
||||
if (seen.has(part)) return true;
|
||||
seen.add(part);
|
||||
if (part.isUnionType() || part.isIntersectionType())
|
||||
return ts.constituentTypes(part).every(concrete);
|
||||
if (!part.getAliasTypeArguments().every(concrete)) return false;
|
||||
return (
|
||||
(part.flags & ts.TypeFlags.Object) === 0 ||
|
||||
((part as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) === 0 ||
|
||||
ctx.checker.getTypeArguments(part as ts.TypeReference).every(concrete)
|
||||
);
|
||||
};
|
||||
// An alias without type arguments is concrete when it is declared at
|
||||
// module level (a function body's alias may mention that function's type
|
||||
// parameters).
|
||||
if (args.length === 0)
|
||||
return t
|
||||
.getAliasSymbol()!
|
||||
.declarations.every(
|
||||
(d) =>
|
||||
ts.isTypeAliasDeclaration(d) && (ts.isSourceFile(d.parent) || ts.isModuleBlock(d.parent)),
|
||||
);
|
||||
return args.every(concrete);
|
||||
}
|
||||
|
||||
/** True for MAPPED-type results — `Partial<Config>`, `Record<"a", n>`,
|
||||
* whatever `Pick`/`Omit` reduce to. Their shape is computed by the checker,
|
||||
* not declared anywhere: the only declaration behind them is the utility
|
||||
@@ -8005,8 +8048,12 @@ function mapRecordTypeInner(
|
||||
// still unknown there) — interning `{}` would give a wrong type. The
|
||||
// degenerate empties (`Partial<{}>`, `Omit<C, keyof C>`) go with it.
|
||||
// An INDEX-SIGNATURE shape is exempt: `Record<string, T>` legitimately
|
||||
// has zero declared members — the signature is the shape.
|
||||
if (computed && props.length === 0 && !indexValue) return null;
|
||||
// has zero declared members — the signature is the shape. So is a mapped
|
||||
// alias instantiated over concrete arguments (`Record<never, T>`, a
|
||||
// catalog type whose key filter keeps no key): the checker resolved it,
|
||||
// and it is the empty object type below.
|
||||
if (computed && props.length === 0 && !indexValue && !resolvedMappedAlias(widened, ctx))
|
||||
return null;
|
||||
// An empty object type — `{}` (spelled, the checker's shared
|
||||
// intrinsic, or inferred from an empty object literal), `interface
|
||||
// Empty {}` — is tsc's TOP type over non-nullish values: every number,
|
||||
|
||||
@@ -875,6 +875,7 @@ async function prepareExecutableInput(
|
||||
pointerBits: buildPlatform === "wasi" ? 32 : 64,
|
||||
wasi: buildPlatform === "wasi",
|
||||
runtimeAbiMarker: useRuntimePack,
|
||||
unwindAbi: buildPlatform === "linux",
|
||||
objectAudit: opts.sanitize === true,
|
||||
lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true),
|
||||
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
|
||||
|
||||
@@ -591,6 +591,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.styleText",
|
||||
"util.toUSVString",
|
||||
"util.inherits",
|
||||
"util.inheritsPrototype",
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
@@ -976,6 +977,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"assert.noErrorPredicate",
|
||||
"assert.regexDynTest",
|
||||
"assert.expectsErrDyn",
|
||||
"assert.throwsValidate",
|
||||
"assert.ifErrorErr",
|
||||
"assert.ifErrorF64",
|
||||
"assert.ifErrorStr",
|
||||
|
||||
@@ -1146,6 +1146,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* arguments (scr_util_callable.c). The function results are native dyn
|
||||
* wrappers; call sites with a static function type check them back. */
|
||||
"util.inherits": { argTypes: [DYN, DYN], result: VOID },
|
||||
"util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
"util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
"util.callbackify": { argTypes: [DYN], result: DYN },
|
||||
/** util.promisify over an untyped function value: a native dyn function
|
||||
@@ -2608,6 +2609,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
result: CHILD_T,
|
||||
},
|
||||
"child.connected": { argTypes: [CHILD_T], result: BOOL },
|
||||
"child.hasChannel": { argTypes: [CHILD_T], result: BOOL },
|
||||
"child.send": { argTypes: [CHILD_T, STRING], result: BOOL },
|
||||
"child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL },
|
||||
"child.disconnect": { argTypes: [CHILD_T], result: VOID },
|
||||
@@ -4436,6 +4438,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
// The equality quartet over checked-dynamic operands (the frontend
|
||||
// boxes a static side into the checked-dynamic tree first).
|
||||
"assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID },
|
||||
/** A message argument that is not a string, registered for the next
|
||||
* failing assertion (scr_assert_message_value). */
|
||||
"assert.messageValue": { argTypes: [DYN, BOOL], result: VOID },
|
||||
/** An assert method as a function value, by canonical name. */
|
||||
"assert.methodValue": { argTypes: [STRING], result: DYN },
|
||||
"assert.messageClear": { argTypes: [], result: VOID },
|
||||
"assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID },
|
||||
// The throws(fn, {shape}) accumulator: begin/slot calls never throw;
|
||||
// shapeEnd throws the Comparison diff. The error slot is the
|
||||
@@ -4461,6 +4469,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* against the caught value's; a mismatch throws the deep-equal
|
||||
* AssertionError (scr_assert.c). MAY THROW by design. */
|
||||
"assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID },
|
||||
/** A validation function against the caught value (Node's expectedException). */
|
||||
"assert.throwsValidate": { argTypes: [DYN, DYN, STRING, STRING, BOOL], result: VOID },
|
||||
// assert.ifError's typed entries (always throw; unit args never
|
||||
// lower). The error slot is program-dependent — any %Error-hierarchy
|
||||
// class, root or subclass (the insp.error precedent, null slot).
|
||||
@@ -4899,6 +4909,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* lib-call argument passes the time value itself, and any other use
|
||||
* creates the Date. */
|
||||
"date.fromTime": { argTypes: [F64], result: DATE_T },
|
||||
/** Date.prototype's field setters over a time value
|
||||
* (scr_date_set_fields): time value, setter code, argument count and four
|
||||
* arguments; answers the new time value. */
|
||||
"date.setFields": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 },
|
||||
/** TimeClip of a number (setTime's argument). */
|
||||
"date.timeClip": { argTypes: [F64], result: F64 },
|
||||
/** Stores a time value into a Date object and answers it (a setter's
|
||||
* write). */
|
||||
"date.store": { argTypes: [DATE_T, F64], result: F64 },
|
||||
/** Date.prototype.toString / toDateString / toTimeString and String(date):
|
||||
* V8's local-time formats with the host zone's long name, "Invalid Date"
|
||||
* for an invalid date. Owned (+1) string; never throws. */
|
||||
|
||||
@@ -130,10 +130,7 @@ export function specializeCallbackCalls(
|
||||
const { written, closureWritten } = rebindings(mod);
|
||||
const copies = new Map<string, IrFunction>();
|
||||
const candidates = new Map<string, Candidate>();
|
||||
let totalNodes = 0;
|
||||
for (const fn of mod.functions) {
|
||||
const nodes = countNodes(fn);
|
||||
totalNodes += nodes;
|
||||
if (
|
||||
!(fn.ownsPrototype || fn.name.startsWith("%arr.")) ||
|
||||
fn.name === mod.entry ||
|
||||
@@ -142,10 +139,11 @@ export function specializeCallbackCalls(
|
||||
fn.captures !== undefined ||
|
||||
fn.classCaptures?.length ||
|
||||
fn.fastRead !== undefined ||
|
||||
fn.speculativeDispatch ||
|
||||
nodes > MAX_FUNCTION_NODES
|
||||
fn.speculativeDispatch
|
||||
)
|
||||
continue;
|
||||
const nodes = countNodes(fn);
|
||||
if (nodes > MAX_FUNCTION_NODES) continue;
|
||||
const rebound = written.get(fn.name)!;
|
||||
const slots: number[] = [];
|
||||
fn.params.forEach((param, index) => {
|
||||
@@ -165,19 +163,30 @@ export function specializeCallbackCalls(
|
||||
const definitions = new Map<string, Map<string, IrExpr>>();
|
||||
const globalIds = new Set(mod.globals?.map((global) => global.id));
|
||||
const globalWrites = new Map<string, IrExpr[]>();
|
||||
// The sum of countNodes over every function, counted by the walk below.
|
||||
let totalNodes = 0;
|
||||
for (const fn of mod.functions) {
|
||||
let nodes = 0;
|
||||
const values = new Map<string, IrExpr[]>();
|
||||
const counts = new Map<string, number>();
|
||||
const record = (id: string, value: IrExpr | null, weight: number): void => {
|
||||
counts.set(id, (counts.get(id) ?? 0) + weight);
|
||||
if (value) values.set(id, [...(values.get(id) ?? []), value]);
|
||||
if (value) {
|
||||
const list = values.get(id);
|
||||
if (list === undefined) values.set(id, [value]);
|
||||
else list.push(value);
|
||||
}
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (stmt) => {
|
||||
nodes++;
|
||||
if (stmt.kind === "varDecl") record(stmt.localId, stmt.init, stmt.init ? 1 : 0);
|
||||
if (stmt.kind === "assign") {
|
||||
if (globalIds.has(stmt.localId))
|
||||
globalWrites.set(stmt.localId, [...(globalWrites.get(stmt.localId) ?? []), stmt.value]);
|
||||
if (globalIds.has(stmt.localId)) {
|
||||
const list = globalWrites.get(stmt.localId);
|
||||
if (list === undefined) globalWrites.set(stmt.localId, [stmt.value]);
|
||||
else list.push(stmt.value);
|
||||
}
|
||||
record(stmt.localId, stmt.value, 1);
|
||||
}
|
||||
if (stmt.kind === "forOf") record(stmt.localId, null, 2);
|
||||
@@ -186,6 +195,7 @@ export function specializeCallbackCalls(
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
nodes++;
|
||||
if (expr.kind === "assignExpr" || expr.kind === "incDec") {
|
||||
record(expr.localId, null, 2);
|
||||
if (globalIds.has(expr.localId)) globalWrites.set(expr.localId, []);
|
||||
@@ -208,6 +218,7 @@ export function specializeCallbackCalls(
|
||||
)
|
||||
single.set(id, list[0]!);
|
||||
definitions.set(fn.name, single);
|
||||
totalNodes += Math.min(nodes, MAX_FUNCTION_NODES + 1);
|
||||
}
|
||||
const globalDefinitions = new Map<string, IrExpr>();
|
||||
for (const global of mod.globals ?? [])
|
||||
|
||||
@@ -538,7 +538,7 @@ export class CaptureFlow {
|
||||
facts.callees.add(e.callee.localId);
|
||||
break;
|
||||
}
|
||||
return everyExprChild(e, expr, (s) => stmt(s, false));
|
||||
return everyExprChild(e, expr, nested);
|
||||
};
|
||||
// `split` mirrors rewrite(): only statement lists reached through
|
||||
// ordinary nesting from the body are expanded. Switch cases share one
|
||||
@@ -599,8 +599,10 @@ export class CaptureFlow {
|
||||
list(s.body, split)
|
||||
);
|
||||
}
|
||||
return everyStmtChild(s, expr, (child) => stmt(child, false));
|
||||
return everyStmtChild(s, expr, nested);
|
||||
};
|
||||
// Statements nested in expressions and in other statements' heads.
|
||||
const nested = (s: IrStmt): boolean => stmt(s, false);
|
||||
list(fn.body, true);
|
||||
return facts;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user