mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Merge main
This commit is contained in:
@@ -524,7 +524,7 @@ jobs:
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (tsgo and tsc-ts give the same IR)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
@@ -13,7 +13,6 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-properties.mjs",
|
||||
"args": [
|
||||
"1000000"
|
||||
]
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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/index-cursors.ts",
|
||||
"args": [
|
||||
"20000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Arrays of indices walked by loop-carried cursors: each step reads the
|
||||
// next index from the array and relinks the visited entries, so the
|
||||
// cursor and every stored value are small integers read back from the
|
||||
// array itself.
|
||||
const N = 4096;
|
||||
|
||||
function relink(rounds: number): number {
|
||||
const next: number[] = [];
|
||||
for (let i = 0; i < N; i++) next.push(i + 1 < N ? i + 1 : 0);
|
||||
let at = next[0]!;
|
||||
let total = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let s = 0; s < N; s++) {
|
||||
const t = next[at]!;
|
||||
next[at] = next[t]!;
|
||||
next[t] = at;
|
||||
at = t;
|
||||
total = (total + at) | 0;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "1");
|
||||
console.log("relink", relink(rounds));
|
||||
@@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ
|
||||
scriptc compiles a package automatically when it:
|
||||
|
||||
- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
|
||||
- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names.
|
||||
- ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript.
|
||||
|
||||
The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.
|
||||
|
||||
|
||||
@@ -505,8 +505,12 @@ export function emitDenseArrayState(
|
||||
indexExpr: IrExpr | undefined,
|
||||
kind: "state" | "has",
|
||||
width: number,
|
||||
/** The index is a dense array loop counter its entry test proved below
|
||||
* the hole-free prefix (provenDenseIndex): the slot is a present value. */
|
||||
proven = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
if (proven) return kind === "state" ? f64Lit(SCR_ARR_VALUE) : "true";
|
||||
const fn = kind === "state" ? "scr_arr_state" : "scr_arr_has";
|
||||
const ty = kind === "state" ? "double" : "i1";
|
||||
host.declare(`declare ${kind === "state" ? "double" : "zeroext i1"} @${fn}(ptr, double)`);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
matchDenseArrayLoop,
|
||||
pairedDenseShape,
|
||||
type DenseLoopShape,
|
||||
} from "./dense-array-loops.js";
|
||||
|
||||
@@ -42,6 +43,8 @@ const locals = new Map<string, IrLocal>([
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: true }],
|
||||
["c", { id: "c", name: "c", type: ARR, mutable: false }],
|
||||
["j", { id: "j", name: "j", type: F64, mutable: true }],
|
||||
["t", { id: "t", name: "t", type: F64, mutable: false }],
|
||||
]);
|
||||
|
||||
/** for (let i = 0; i < a.length; i++) body */
|
||||
@@ -193,3 +196,100 @@ test("a loop to another invariant limit covers each array whose length it keeps"
|
||||
expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]);
|
||||
expect(matchLimited(resizing)).toBeNull();
|
||||
});
|
||||
|
||||
test("a two-counter loop covers the arrays indexed at either counter", () => {
|
||||
// for (let i = 1, j = k - 1; i < j; i++, j--) { t = a[i]; a[i] = a[j]; a[j] = t; }
|
||||
const paired: IntegerPairedForLoop = {
|
||||
low: { localId: "i", start: num(1), step: 1, startGuarded: false },
|
||||
high: { localId: "j", start: ref("k"), step: -1, startGuarded: true },
|
||||
inclusive: false,
|
||||
guarded: true,
|
||||
range: { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
|
||||
};
|
||||
const pairedShape = pairedDenseShape(paired);
|
||||
const matchPaired = (body: IrStmt[]) =>
|
||||
matchDenseArrayLoop(loop(body), pairedShape, locals, new Set(), new Set(["a", "b"]));
|
||||
const swap: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: "t", init: read("a"), loc },
|
||||
write("a", read("a", ref("j"))),
|
||||
write("a", ref("t"), ref("j")),
|
||||
];
|
||||
const dense = matchPaired(swap);
|
||||
expect([...dense!.arrays.keys()]).toEqual(["a"]);
|
||||
expect(dense!.pair).toEqual({ high: "j", lowStart: num(1), highStart: ref("k") });
|
||||
expect(dense!.range).toEqual({ min: 0, max: 2147483646 });
|
||||
// Stores at any other index may extend the array.
|
||||
expect(matchPaired([...swap, write("a", num(0), ref("k"))])).toBeNull();
|
||||
});
|
||||
|
||||
test("a present-undefined store is covered only where it stores a covered read's missing arm", () => {
|
||||
// const v = b[i] (as number | undefined);
|
||||
// if (v is undefined) c[i] = undefined; else c[i] = v is number ? v : throw;
|
||||
const unionType = { kind: "union" as const, unionId: "u" };
|
||||
const v: IrExpr = { kind: "varRef", localId: "v", type: unionType, loc };
|
||||
const isTag = (tag: number): IrExpr => ({
|
||||
kind: "unionIsTag",
|
||||
unionId: "u",
|
||||
tag,
|
||||
negated: false,
|
||||
value: v,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const reader = (array: string, index = ref("i")): IrExpr => ({
|
||||
kind: "call",
|
||||
callee: "%arr.idxOr.f64",
|
||||
args: [arr(array), index],
|
||||
type: unionType,
|
||||
loc,
|
||||
});
|
||||
const undefinedStore: IrStmt = { kind: "arraySetUndefined", arr: arr("c"), index: ref("i"), loc };
|
||||
const guarded = (array: string): IrStmt[] => [
|
||||
{ kind: "varDecl", localId: "v", init: reader(array), loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: isTag(1),
|
||||
then: [undefinedStore],
|
||||
else_: [
|
||||
write("c", {
|
||||
kind: "ternary",
|
||||
cond: isTag(0),
|
||||
then: { kind: "unionNarrow", unionId: "u", tag: 0, value: v, type: F64, loc },
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [num(1), { kind: "strLit", value: "", type: { kind: "string" }, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
],
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const withV = new Map([
|
||||
...locals,
|
||||
["v", { id: "v", name: "v", type: unionType, mutable: false }],
|
||||
]);
|
||||
const matchStores = (body: IrStmt[], initialized: string[]) =>
|
||||
matchDenseArrayLoop(loop(body), shape, withV, new Set(), new Set(initialized));
|
||||
const dense = matchStores(guarded("b"), ["a", "b", "c"]);
|
||||
expect([...dense!.arrays.keys()].sort()).toEqual(["b", "c"]);
|
||||
// A read of an array the entry test does not cover may miss in the fast
|
||||
// copy, and its undefined store would run there.
|
||||
expect(matchStores(guarded("b"), ["a", "c"])).toBeNull();
|
||||
// A present undefined stored on its own leaves out its array and every
|
||||
// array the other name may hold.
|
||||
const alone: IrStmt[] = [{ kind: "exprStmt", expr: read("b"), loc }, undefinedStore];
|
||||
expect(matchStores(alone, ["a", "b", "c"])).toBeNull();
|
||||
const separate = matchDenseArrayLoop(
|
||||
loop(alone),
|
||||
shape,
|
||||
withV,
|
||||
new Set(["c"]),
|
||||
new Set(["a", "b", "c"]),
|
||||
);
|
||||
expect([...separate!.arrays.keys()]).toEqual(["b"]);
|
||||
});
|
||||
|
||||
@@ -20,16 +20,24 @@
|
||||
* A counted loop with another invariant limit, such as `j < n` after
|
||||
* `const n = a.length` or `j < 5`, is covered the same way: the test bounds
|
||||
* the loop's exclusive limit by 2^31 - 1 and by each indexed array's
|
||||
* `filled`, and the loop must provably never change any of their lengths. */
|
||||
* `filled`, and the loop must provably never change any of their lengths.
|
||||
*
|
||||
* A two-counter loop closing in on itself, such as
|
||||
* `for (let i = 1, j = k - 1; i < j; i++, j--)`, indexes only between its
|
||||
* low counter's start and its high counter's: the low counter only rises
|
||||
* and stays below the high one, which only falls. Its test bounds the low
|
||||
* start below by 0 and the high start by 2^31 - 2 and below each indexed
|
||||
* array's `filled`, both starts being exact integers. */
|
||||
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import { preservesArrayLength } from "../../ir/array-length-stability.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
|
||||
import { emitCountedLoopLimit } from "./counted-loops.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
@@ -47,6 +55,17 @@ export interface DenseArrayLoop {
|
||||
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
|
||||
/** The counter's range on every pass of the fast copy. */
|
||||
range: IntegerRange;
|
||||
/** A two-counter loop's high counter and both starts, when the counter
|
||||
* is its low one (`length` and `limit` are then null). */
|
||||
pair: DensePair | null;
|
||||
}
|
||||
|
||||
/** The high counter of a two-counter dense loop, and the starts its entry
|
||||
* test bounds. */
|
||||
export interface DensePair {
|
||||
high: string;
|
||||
lowStart: IrExpr;
|
||||
highStart: IrExpr;
|
||||
}
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
@@ -62,6 +81,8 @@ export interface DenseLoopShape {
|
||||
/** The counter's proven range on every pass of the ordinary loop; the
|
||||
* fast copy keeps its minimum, which must be at least 0. */
|
||||
range: IntegerRange;
|
||||
/** A two-counter loop's high counter and starts. */
|
||||
pair?: DensePair;
|
||||
}
|
||||
|
||||
/** `length.length - c` (c an integer literal, 0 when absent). */
|
||||
@@ -107,6 +128,124 @@ export function countedDenseShape(
|
||||
return { counter: counted.localId, length: start.receiver, range: counted.range };
|
||||
}
|
||||
|
||||
/** The shape of a two-counter loop whose low counter rises and high
|
||||
* counter falls: both stay between the two starts, which the entry test
|
||||
* bounds, so the fast copy's counters lie in [0, 2^31 - 2]. */
|
||||
export function pairedDenseShape(loop: IntegerPairedForLoop): DenseLoopShape {
|
||||
return {
|
||||
counter: loop.low.localId,
|
||||
length: null,
|
||||
range: { min: 0, max: INT32_MAX - 1 },
|
||||
pair: { high: loop.high.localId, lowStart: loop.low.start, highStart: loop.high.start },
|
||||
};
|
||||
}
|
||||
|
||||
interface ElementRead {
|
||||
arr: IrExpr;
|
||||
index: IrExpr;
|
||||
}
|
||||
|
||||
/** The array and index of a call of an element-reader helper
|
||||
* (`%arr.idxOr.*`, ir/int32-arrays.ts isElementReader): `a[i]` as
|
||||
* `number | undefined`. */
|
||||
function elementReaderCall(e: IrExpr): ElementRead | null {
|
||||
if (e.kind !== "call" || !e.callee.startsWith("%arr.idxOr.") || e.args.length !== 2) return null;
|
||||
return { arr: e.args[0]!, index: e.args[1]! };
|
||||
}
|
||||
|
||||
/** The array and index of an element read as `T | undefined`: an
|
||||
* element-reader call, or its body inlined for an int32 array
|
||||
* (`arrayState(a, i) === 1 ? wrap(a[i]) : wrap(undefined)`). */
|
||||
function elementRead(e: IrExpr): ElementRead | null {
|
||||
const call = elementReaderCall(e);
|
||||
if (call) return call;
|
||||
if (e.kind !== "ternary") return null;
|
||||
const cond = e.cond;
|
||||
if (cond.kind !== "bin" || cond.op !== "===") return null;
|
||||
const state = cond.left,
|
||||
present = cond.right,
|
||||
wrapped = e.then,
|
||||
missing = e.else_;
|
||||
if (state.kind !== "arrayState" || present.kind !== "numLit" || present.value !== 1) return null;
|
||||
if (wrapped.kind !== "unionWrap" || missing.kind !== "unionWrap") return null;
|
||||
const value = wrapped.value;
|
||||
if (value.kind !== "arrayGet" || missing.value.kind !== "unitLit") return null;
|
||||
const same = (x: IrExpr, y: IrExpr): boolean =>
|
||||
x.kind === "varRef" && y.kind === "varRef" && x.localId === y.localId;
|
||||
return same(state.arr, value.arr) && same(state.index, value.index)
|
||||
? { arr: state.arr, index: state.index }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The present-undefined stores of `a[i] = b[j]!` as lowering spells it,
|
||||
* with the element read each one stores the missing arm of:
|
||||
*
|
||||
* const v = %arr.idxOr(b, j);
|
||||
* if (v is undefined) a[i] = undefined; else a[i] = v is number ? v : throw;
|
||||
*
|
||||
* The read's local is declared once in the same list as the `if`, and only
|
||||
* read after it. */
|
||||
function missingElementStores(body: IrStmt[]): Map<IrStmt, ElementRead> {
|
||||
const result = new Map<IrStmt, ElementRead>();
|
||||
const writes = new Map<string, number>();
|
||||
const count = (id: string): void => void writes.set(id, (writes.get(id) ?? 0) + 1);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") count(e.localId);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") count(s.localId);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
body.every(stmt);
|
||||
const list = (stmts: IrStmt[]): void => {
|
||||
const reads = new Map<string, ElementRead>();
|
||||
for (const s of stmts) {
|
||||
if (s.kind === "varDecl" && s.init && writes.get(s.localId) === 1) {
|
||||
const read = elementRead(s.init);
|
||||
if (read) reads.set(s.localId, read);
|
||||
}
|
||||
if (s.kind === "if") {
|
||||
const store = missingArmStore(s, reads);
|
||||
if (store) result.set(store.stmt, store.read);
|
||||
}
|
||||
// Nested loops keep a loop out of the fast copy (matchDenseArrayLoop),
|
||||
// and other statement lists leave their stores unmatched.
|
||||
if (s.kind === "block") list(s.body);
|
||||
if (s.kind === "if") {
|
||||
list(s.then);
|
||||
if (s.else_) list(s.else_);
|
||||
}
|
||||
}
|
||||
};
|
||||
list(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The present-undefined store of `if (v is undefined) a[i] = undefined;
|
||||
* else a[i] = v is number ? v : throw;`, with the read `v` holds. */
|
||||
function missingArmStore(
|
||||
s: IrStmt & { kind: "if" },
|
||||
reads: ReadonlyMap<string, ElementRead>,
|
||||
): { stmt: IrStmt; read: ElementRead } | null {
|
||||
const cond = s.cond;
|
||||
if (cond.kind !== "unionIsTag" || cond.negated || cond.value.kind !== "varRef") return null;
|
||||
const local = cond.value.localId;
|
||||
const read = reads.get(local);
|
||||
if (!read || s.then.length !== 1 || s.else_?.length !== 1) return null;
|
||||
const store = s.then[0]!,
|
||||
other = s.else_[0]!;
|
||||
if (store.kind !== "arraySetUndefined" || other.kind !== "arraySet") return null;
|
||||
const value = other.value;
|
||||
if (value.kind !== "ternary") return null;
|
||||
const test = value.cond,
|
||||
narrowed = value.then;
|
||||
if (test.kind !== "unionIsTag" || test.negated || test.tag === cond.tag) return null;
|
||||
if (test.value.kind !== "varRef" || test.value.localId !== local) return null;
|
||||
if (narrowed.kind !== "unionNarrow" || narrowed.tag !== test.tag) return null;
|
||||
return narrowed.type.kind === "f64" ? { stmt: store, read } : null;
|
||||
}
|
||||
|
||||
/** Match the fast copy of a loop of the given shape. `initialized` holds the
|
||||
* bindings definitely assigned when the loop starts; `unaliased` the local
|
||||
* arrays no other binding holds. */
|
||||
@@ -118,6 +257,8 @@ export function matchDenseArrayLoop(
|
||||
initialized: ReadonlySet<string>,
|
||||
): DenseArrayLoop | null {
|
||||
const { counter, length } = shape;
|
||||
const high = shape.pair?.high ?? null;
|
||||
const isCounter = (id: string): boolean => id === counter || id === high;
|
||||
if (shape.range.min < 0 || !loop.cond) return null;
|
||||
let nodes = 0,
|
||||
nested = false;
|
||||
@@ -126,13 +267,21 @@ export function matchDenseArrayLoop(
|
||||
// element write.
|
||||
const disturbed = new Set<string>();
|
||||
const access = (arr: IrExpr, index: IrExpr): void => {
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter)
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && isCounter(index.localId))
|
||||
candidates.set(arr.localId, arr);
|
||||
};
|
||||
const missing = missingElementStores(loop.body);
|
||||
// The element reads behind the guarded present-undefined stores.
|
||||
const guards: ElementRead[] = [];
|
||||
// The arrays of the other present-undefined stores. The fast copy runs
|
||||
// them, and the slot each one writes leaves the hole-free prefix.
|
||||
const undefinedStores: IrExpr[] = [];
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId);
|
||||
if (e.kind === "arrayGet") access(e.arr, e.index);
|
||||
const reader = elementReaderCall(e);
|
||||
if (reader) access(reader.arr, reader.index);
|
||||
if (e.kind === "arrIntrinsic") {
|
||||
if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]);
|
||||
else if (e.method !== "length" && e.receiver.kind === "varRef")
|
||||
@@ -147,9 +296,21 @@ export function matchDenseArrayLoop(
|
||||
if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId);
|
||||
if (s.kind === "arraySet") {
|
||||
access(s.arr, s.index);
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter))
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || !isCounter(s.index.localId)))
|
||||
disturbed.add(s.arr.localId);
|
||||
}
|
||||
if (s.kind === "arraySetUndefined") {
|
||||
// A present undefined at a counter only where it stores a missing
|
||||
// element read (`a[i] = b[j]!`): the fast copy reads a present value
|
||||
// there, so the store never runs in it.
|
||||
const read = missing.get(s);
|
||||
if (read === undefined || s.index.kind !== "varRef" || !isCounter(s.index.localId)) {
|
||||
undefinedStores.push(s.arr);
|
||||
} else {
|
||||
access(s.arr, s.index);
|
||||
guards.push(read);
|
||||
}
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
|
||||
@@ -164,6 +325,15 @@ export function matchDenseArrayLoop(
|
||||
)
|
||||
)
|
||||
return null;
|
||||
// A store through another name, such as `const b = a` or a field, lands
|
||||
// in `a` unless one of the two bindings is a local array no other binding
|
||||
// holds.
|
||||
const mayStoreUndefined = (id: string): boolean =>
|
||||
undefinedStores.some((arr) =>
|
||||
arr.kind === "varRef"
|
||||
? arr.localId === id || (!unaliased.has(id) && !unaliased.has(arr.localId))
|
||||
: !unaliased.has(id),
|
||||
);
|
||||
const arrays = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
for (const [id, ref] of candidates) {
|
||||
const local = locals.get(id);
|
||||
@@ -173,6 +343,7 @@ export function matchDenseArrayLoop(
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
disturbed.has(id) ||
|
||||
mayStoreUndefined(id) ||
|
||||
(id !== length?.localId && !initialized.has(id))
|
||||
)
|
||||
continue;
|
||||
@@ -180,18 +351,33 @@ export function matchDenseArrayLoop(
|
||||
// once: the loop must not change any of their lengths.
|
||||
if (
|
||||
length === null &&
|
||||
!preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased)
|
||||
!preservesArrayLength(
|
||||
nodesOfLoop,
|
||||
{ receiver: id, inRange: counter, inRangeHigh: high },
|
||||
locals,
|
||||
unaliased,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
arrays.set(id, ref);
|
||||
}
|
||||
if (arrays.size === 0) return null;
|
||||
// Each guarded store's read must be one the fast copy proves present.
|
||||
for (const { arr, index } of guards)
|
||||
if (
|
||||
arr.kind !== "varRef" ||
|
||||
!arrays.has(arr.localId) ||
|
||||
index.kind !== "varRef" ||
|
||||
!isCounter(index.localId)
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
counter,
|
||||
length,
|
||||
limit: length === null ? (shape.limit ?? null) : null,
|
||||
arrays,
|
||||
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
|
||||
pair: shape.pair ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,6 +393,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
|
||||
host.markMemoryPointer(ptr, "array:header");
|
||||
return ptr;
|
||||
};
|
||||
if (loop.pair !== null) return emitPairedDenseGuard(host, loop, loop.pair, header);
|
||||
if (loop.length === null) {
|
||||
// A limit at or below the start runs no pass, and passes the test.
|
||||
const limit = emitCountedLoopLimit(host, loop.limit!);
|
||||
@@ -254,3 +441,54 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
/** A two-counter loop's entry test: both starts are exact integers, the
|
||||
* low one at least 0 and the high one at most 2^31 - 2 and below every
|
||||
* indexed array's hole-free prefix bound. The starts are stable values
|
||||
* (matchIntegerPairedForLoop), so evaluating them here again is unobservable. */
|
||||
function emitPairedDenseGuard(
|
||||
host: LlvmEmitterContext,
|
||||
loop: DenseArrayLoop,
|
||||
pair: DensePair,
|
||||
header: (array: string, field: number) => string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const integer = (expr: IrExpr, min: number, max: number): { value: string; ok: string } => {
|
||||
const start = host.emitExpr(expr);
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
value = B.tmp(),
|
||||
back = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
inRange = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(min)}`);
|
||||
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(max)}`);
|
||||
B.line(`${inRange} = and i1 ${low}, ${high}`);
|
||||
B.line(`${raw} = fptosi double ${start.name} to i64`);
|
||||
B.line(`${value} = freeze i64 ${raw}`);
|
||||
B.line(`${back} = sitofp i64 ${value} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${back}, ${start.name}`);
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
return { value, ok };
|
||||
};
|
||||
const low = integer(pair.lowStart, 0, INT32_MAX - 1);
|
||||
const high = integer(pair.highStart, -1, INT32_MAX - 1);
|
||||
let valid = B.tmp();
|
||||
B.line(`${valid} = and i1 ${low.ok}, ${high.ok}`);
|
||||
for (const ref of loop.arrays.values()) {
|
||||
const array = host.emitStableReceiver(ref, []);
|
||||
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
|
||||
filled = B.tmp(),
|
||||
wide = B.tmp(),
|
||||
covered = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
B.line(`${wide} = zext i32 ${filled} to i64`);
|
||||
B.line(`${covered} = icmp slt i64 ${high.value}, ${wide}`);
|
||||
B.line(`${both} = and i1 ${valid}, ${covered}`);
|
||||
valid = both;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,12 @@ import {
|
||||
matchIntegerPairedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
|
||||
import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
emitDenseLoopGuard,
|
||||
matchDenseArrayLoop,
|
||||
pairedDenseShape,
|
||||
} from "./dense-array-loops.js";
|
||||
import {
|
||||
emitCountedLoopLimit,
|
||||
emitCountedLoopSafety,
|
||||
@@ -966,7 +971,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
: pairedLoop
|
||||
? pairedDenseShape(pairedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
denseShape && !window
|
||||
? matchDenseArrayLoop(
|
||||
@@ -1144,11 +1151,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`${start} = fptosi double ${value.name} to i64`);
|
||||
B.line(`store i64 ${start}, ptr ${slot}`);
|
||||
pairedSlots.push(slot);
|
||||
const dense = LOOPS.of(emitter).denseLoop;
|
||||
emitter.integerLoopBindings.set(counter.localId, {
|
||||
slot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: pairedLoop.range,
|
||||
range:
|
||||
dense?.pair && dense.counter === pairedLoop.low.localId
|
||||
? dense.range
|
||||
: pairedLoop.range,
|
||||
});
|
||||
}
|
||||
} else if (s.init) {
|
||||
|
||||
@@ -499,6 +499,27 @@ test("class brand probes compare the program's key constant before the key text"
|
||||
const named = work(probe("dyn.typedRefIs"), dyn);
|
||||
expect(named).toContain("; ->v.typed_ref.type_key");
|
||||
expect(named).toContain("@scr_dyn_typed_ref_is_key");
|
||||
// Another key pointer is rejected on its length and last eight bytes
|
||||
// ("Example" plus the prefix's ':') before the out-of-line comparison.
|
||||
expect(named).toContain("; ->v.typed_ref.type_key_len");
|
||||
expect(named).toContain("and i32 %t");
|
||||
expect(named).toMatch(/icmp eq i32 %t\d+, 14\b/);
|
||||
const tail = Buffer.from("object:Example").readBigInt64LE(6);
|
||||
expect(named).toContain(`icmp eq i64 %t`);
|
||||
expect(named).toContain(`, ${tail}`);
|
||||
const shortKey: IrExpr = { kind: "strLit", value: "o:E", type: STRING, loc: receiverLoc };
|
||||
const short = work(
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [ref(dyn), shortKey],
|
||||
type: BOOL,
|
||||
loc: receiverLoc,
|
||||
},
|
||||
dyn,
|
||||
);
|
||||
expect(short).toMatch(/icmp eq i32 %t\d+, 3\b/);
|
||||
expect(short).not.toContain("load i64");
|
||||
// The key-pointer test answers false for capsules boxed elsewhere.
|
||||
const exact = work(probe("dyn.typedRefKeyIs"), dyn);
|
||||
expect(exact).toContain("; ->v.typed_ref.type_key");
|
||||
|
||||
@@ -68,7 +68,7 @@ import {
|
||||
type StringSplit,
|
||||
} from "./split-loops.js";
|
||||
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries } from "./record-results.js";
|
||||
import { addRecordResultEntries, resultEntryName } from "./record-results.js";
|
||||
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
|
||||
import {
|
||||
type ChunkResult,
|
||||
@@ -82,6 +82,7 @@ import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
fillAllocationLength,
|
||||
int32ArrayKey,
|
||||
selectInt32Arrays,
|
||||
} from "../../ir/int32-arrays.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
@@ -510,6 +511,15 @@ function provenInt32Arrays(mod: IrModule): ReadonlySet<string> {
|
||||
return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]);
|
||||
}
|
||||
|
||||
/** The functions whose calls the backend may emit as calls of their
|
||||
* result-slot entries (record-results.ts). */
|
||||
function resultEntryTargets(mod: IrModule): ReadonlySet<string> {
|
||||
const names = new Set(mod.functions.map((fn) => fn.name));
|
||||
const targets = new Set<string>();
|
||||
for (const name of names) if (names.has(resultEntryName(name))) targets.add(name);
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** The emitter for `input` after the module passes every emission runs. */
|
||||
function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
@@ -545,6 +555,7 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
),
|
||||
),
|
||||
provenInt32Arrays,
|
||||
resultEntryTargets,
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
@@ -781,7 +792,7 @@ export class LlEmitter {
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return true;
|
||||
const key = `${fn.name}\0${s.localId}`;
|
||||
const key = int32ArrayKey(fn.name, s.localId);
|
||||
const checked = checkedInt32Arrays.has(key);
|
||||
if (!checked && !int32Arrays.has(key)) return true;
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
@@ -799,15 +810,16 @@ export class LlEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether `arr` is one of the current function's int32 arrays
|
||||
* (ir/int32-arrays.ts, kept by the int32 slot analysis). */
|
||||
/** Whether `arr` reads one of the int32 arrays (ir/int32-arrays.ts, kept
|
||||
* by the int32 slot analysis): a local or parameter of the current
|
||||
* function, or a module global. */
|
||||
int32Array(arr: IrExpr): boolean {
|
||||
if (arr.kind !== "varRef") return false;
|
||||
if (this.int32KindArray(arr)) return true;
|
||||
if (this.int32Slots.int32Arrays.size === 0) return false;
|
||||
return (
|
||||
this.int32ArrayTemps.has(arr.localId) ||
|
||||
this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`)
|
||||
this.int32Slots.int32Arrays.has(int32ArrayKey(this.currentFunctionName, arr.localId))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -974,8 +986,8 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
* prefix: a counter of a dense array loop's fast copy indexing one of the
|
||||
* arrays its entry test covered. */
|
||||
provenDenseIndex(arr: IrExpr, index: IrExpr): boolean {
|
||||
const loop = LOOPS.of(this).denseLoop;
|
||||
return (
|
||||
@@ -983,8 +995,8 @@ export class LlEmitter {
|
||||
arr.kind === "varRef" &&
|
||||
loop.arrays.has(arr.localId) &&
|
||||
index.kind === "varRef" &&
|
||||
index.localId === loop.counter &&
|
||||
this.integerLoopBindings.has(loop.counter)
|
||||
(index.localId === loop.counter || index.localId === loop.pair?.high) &&
|
||||
this.integerLoopBindings.has(index.localId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import { emitCallArrayRead } from "./local-array-reads.js";
|
||||
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
|
||||
import { emitStackMapRead } from "./map-read-lifetimes.js";
|
||||
@@ -166,6 +167,9 @@ export function emitCallExpr(
|
||||
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
|
||||
// An inline union field read copies its tag and bits.
|
||||
if (host.inlineFieldGet(a)) return host.emitReadReceiver(a);
|
||||
// A scalar keyed record read writes its tag and bits.
|
||||
if (a.kind === "recordKeyGet" && scalarRecordKeyGet(host, a))
|
||||
return { name: emitStackRecordKeyGet(host, a), type: a.type };
|
||||
}
|
||||
if (inputs[index]) return host.emitReadReceiver(a);
|
||||
}
|
||||
|
||||
@@ -1076,9 +1076,10 @@ export function emitContainerExpr(
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
|
||||
const proven = host.provenDenseIndex(e.arr, e.index);
|
||||
const t = host.checkedInt32Array(e.arr)
|
||||
? host.emitByElementKind(arr.name, "double", (kind) =>
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)),
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind), proven),
|
||||
)!
|
||||
: emitDenseArrayState(
|
||||
host,
|
||||
@@ -1087,6 +1088,7 @@ export function emitContainerExpr(
|
||||
e.index,
|
||||
"state",
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
proven,
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, llvmCommentText } from "./common.js";
|
||||
import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { regexLiteralTemplate } from "./expr-primitives.js";
|
||||
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
|
||||
import { preservesRegexInputs } from "./reference-effects.js";
|
||||
|
||||
/** Widen one tagged read. The source is borrowed unless the overflow map
|
||||
@@ -365,6 +366,114 @@ export function emitRecordKeyGet(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
/** A keyed read whose result is a scalar union (`r[k]` on a
|
||||
* `Record<string, number>`, at `number | undefined`) and whose every hit
|
||||
* is a number or boolean arm of it, or an inline field of the same union.
|
||||
* Projections (`??`, `!`, tag tests) read it as a private stack box, so
|
||||
* the read allocates nothing. */
|
||||
export function scalarRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false;
|
||||
const resultType = e.type;
|
||||
const def = host.unionsById.get(resultType.unionId);
|
||||
if (!isScalarUnionDef(def) || undefinedArmTag(resultType, host.unionsById) < 0) return false;
|
||||
const isArm = (t: IrType): boolean =>
|
||||
(t.kind === "f64" || t.kind === "bool") && def!.arms.some((a) => typeEquals(a, t));
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
if (e.overflowOnly !== true) {
|
||||
for (const f of shape.fields) {
|
||||
const inline = host.recordFieldInline(e.shapeId, f.name);
|
||||
if (!isArm(f.type) && !(inline && typeEquals(f.type, resultType))) return false;
|
||||
}
|
||||
}
|
||||
return shape.indexValue === undefined || isArm(shape.indexValue);
|
||||
}
|
||||
|
||||
/** A scalarRecordKeyGet read as a private stack box: the tag and bits of
|
||||
* the declared field or overflow entry the key names, or the undefined arm
|
||||
* for a miss. The box must never reach an RC entry point. */
|
||||
export function emitStackRecordKeyGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): string {
|
||||
const B = host.B;
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: stack keyed read of a non-union");
|
||||
const resultType = e.type;
|
||||
const def = host.unionsById.get(resultType.unionId)!;
|
||||
const armTag = (t: IrType): number => def.arms.findIndex((a) => typeEquals(a, t));
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const key = host.emitExpr(e.key);
|
||||
const box = B.slot();
|
||||
B.entryAllocas.push(`${box} = alloca %ScrUnion`);
|
||||
const tagPtr = B.tmp(),
|
||||
bitsPtr = B.tmp();
|
||||
B.line(`store ${host.sizeType} -1, ptr ${box}`);
|
||||
B.line(`${tagPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 1`);
|
||||
B.line(`${bitsPtr} = getelementptr inbounds %ScrUnion, ptr ${box}, i64 0, i32 5`);
|
||||
const join = B.newLabel("rks.j");
|
||||
const storeScalar = (t: IrType, value: string): void => {
|
||||
const bits = B.tmp();
|
||||
if (t.kind === "f64") B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
else B.line(`${bits} = zext i1 ${value} to i64`);
|
||||
B.line(`store i32 ${armTag(t)}, ptr ${tagPtr}`);
|
||||
B.line(`store i64 ${bits}, ptr ${bitsPtr}`);
|
||||
};
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
for (const f of e.overflowOnly === true ? [] : shape.fields) {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
const lh = B.newLabel("rks.h");
|
||||
const ln = B.newLabel("rks.n");
|
||||
B.condBr(hit, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, f.name);
|
||||
if (inline) {
|
||||
const { tag, bits } = loadInlineUnion(B, ptr);
|
||||
const present = presentBits(B, tag, bits, undefinedArmTag(type, host.unionsById));
|
||||
B.line(`store i32 ${tag}, ptr ${tagPtr}`);
|
||||
B.line(`store i64 ${present}, ptr ${bitsPtr}`);
|
||||
} else {
|
||||
storeScalar(type, host.loadRecordField(ptr, type, false));
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const iv = shape.indexValue;
|
||||
if (iv) {
|
||||
const ovf = host.recordOvfPtr(obj.name, e.shapeId);
|
||||
const outTy = iv.kind === "f64" ? "double" : "i8";
|
||||
const outSlot = B.slot();
|
||||
B.entryAllocas.push(`${outSlot} = alloca ${outTy}`);
|
||||
B.line(`store ${outTy} ${iv.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`);
|
||||
const getter = `scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}`;
|
||||
host.declare(`declare zeroext i1 @${getter}(ptr, ptr, ptr)`);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = call zeroext i1 @${getter}(ptr ${ovf}, ptr ${key.name}, ptr ${outSlot})`);
|
||||
const lh = B.newLabel("rks.h");
|
||||
const ln = B.newLabel("rks.n");
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = load ${outTy}, ptr ${outSlot}`);
|
||||
let value = raw;
|
||||
if (iv.kind === "bool") {
|
||||
value = B.tmp();
|
||||
B.line(`${value} = trunc i8 ${raw} to i1`);
|
||||
}
|
||||
storeScalar(iv, value);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
// A miss is the undefined arm.
|
||||
B.line(`store i32 ${undefinedArmTag(resultType, host.unionsById)}, ptr ${tagPtr}`);
|
||||
B.line(`store i64 0, ptr ${bitsPtr}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
return box;
|
||||
}
|
||||
|
||||
export function keyedRecordReadInto(
|
||||
host: LlvmEmitterContext,
|
||||
slot: string,
|
||||
|
||||
@@ -25,6 +25,9 @@ import { rawBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
|
||||
/** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */
|
||||
const TYPE_KEY_LEN_MASK = (1 << 27) - 1;
|
||||
|
||||
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "assert.shapeStr" || e.fn === "assert.shapeRe") {
|
||||
@@ -281,6 +284,38 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
B.line(`store i1 true, ptr ${slot}`);
|
||||
B.condBr(same, done, compare);
|
||||
B.startBlock(compare);
|
||||
// A capsule with another key pointer usually belongs to another
|
||||
// class: reject it on its key length and last eight bytes before
|
||||
// the out-of-line name comparison, so a class table's chain of
|
||||
// probes costs a few instructions per class it skips.
|
||||
B.line(`store i1 false, ptr ${slot}`);
|
||||
const bytes = Buffer.from(literal, "utf8");
|
||||
const lenSlot = B.tmp(),
|
||||
lenWord = B.tmp(),
|
||||
len = B.tmp(),
|
||||
lenEq = B.tmp();
|
||||
B.line(
|
||||
`${lenSlot} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(88, 52)} ; ->v.typed_ref.type_key_len`,
|
||||
);
|
||||
B.line(`${lenWord} = load i32, ptr ${lenSlot}`);
|
||||
B.line(`${len} = and i32 ${lenWord}, ${TYPE_KEY_LEN_MASK}`);
|
||||
B.line(`${lenEq} = icmp eq i32 ${len}, ${bytes.length}`);
|
||||
const full = B.newLabel("brand.full");
|
||||
if (bytes.length >= 8) {
|
||||
const tail = B.newLabel("brand.tail");
|
||||
B.condBr(lenEq, tail, done);
|
||||
B.startBlock(tail);
|
||||
const tailPtr = B.tmp(),
|
||||
word = B.tmp(),
|
||||
wordEq = B.tmp();
|
||||
B.line(`${tailPtr} = getelementptr inbounds i8, ptr ${keyPtr}, i64 ${bytes.length - 8}`);
|
||||
B.line(`${word} = load i64, ptr ${tailPtr}, align 1`);
|
||||
B.line(`${wordEq} = icmp eq i64 ${word}, ${bytes.readBigInt64LE(bytes.length - 8)}`);
|
||||
B.condBr(wordEq, full, done);
|
||||
} else {
|
||||
B.condBr(lenEq, full, done);
|
||||
}
|
||||
B.startBlock(full);
|
||||
}
|
||||
}
|
||||
if (e.fn === "dyn.typedRefIs") {
|
||||
|
||||
@@ -797,7 +797,33 @@ export function emitLocalArrayRead(
|
||||
);
|
||||
return null;
|
||||
});
|
||||
else if (inline)
|
||||
else if (
|
||||
inline &&
|
||||
loopIndex !== null &&
|
||||
read.element.kind === "f64" &&
|
||||
host.provenDenseIndex(read.array, read.index)
|
||||
) {
|
||||
// In a dense array loop's fast copy, the entry test proved the counter
|
||||
// below the array's hole-free prefix: the slot holds a present value,
|
||||
// and the read has no missing arm.
|
||||
const dataPtr = B.tmp(),
|
||||
data = B.tmp(),
|
||||
valuePtr = B.tmp(),
|
||||
value = B.tmp(),
|
||||
width = int32 ? "i32" : "i64";
|
||||
B.line(
|
||||
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
|
||||
);
|
||||
host.markMemoryPointer(dataPtr, "array:header");
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
|
||||
B.line(
|
||||
`${valuePtr} = getelementptr inbounds ${width}, ptr ${data}, ${host.sizeType} ${loopIndex}`,
|
||||
);
|
||||
host.markMemoryPointer(valuePtr, "array:elements");
|
||||
B.line(`${value} = load ${width}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
storeValue(value);
|
||||
B.br(join);
|
||||
} else if (inline)
|
||||
emitDenseReferenceArrayRead(
|
||||
host,
|
||||
array,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
|
||||
import { borrowableInputs } from "./borrowed-inputs.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js";
|
||||
|
||||
/** Read a receiver whose value is consumed immediately by a field or tag
|
||||
@@ -95,6 +96,8 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
|
||||
const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null;
|
||||
if (narrowed) return narrowed;
|
||||
if (canStackUnion(e, emitter.boxedUnionsById)) return emitStackUnion(emitter, e).value;
|
||||
if (e.kind === "recordKeyGet" && scalarRecordKeyGet(emitter, e))
|
||||
return { name: emitStackRecordKeyGet(emitter, e), type: e.type };
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = emitter.emitUnionProjection(e.value);
|
||||
emitUnionNarrowGuard(emitter, union.name, e.unionId, e.tag);
|
||||
@@ -410,6 +413,7 @@ export function isStackUnionSource(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
emitter.optionalArrayReads.get(e) !== null ||
|
||||
isScalarOptionalSource(e, emitter.unionSources) ||
|
||||
emitter.scalarResultCall(e) ||
|
||||
emitter.inlineFieldGet(e)
|
||||
emitter.inlineFieldGet(e) ||
|
||||
scalarRecordKeyGet(emitter, e)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,9 +77,9 @@ Options:
|
||||
--npm-static <pkg[,pkg…]|auto>
|
||||
compile the named npm packages' shipped JS statically as
|
||||
program modules (repeatable; "auto" opts in every eligible
|
||||
direct import: own .d.ts, unminified JS, no build-transform
|
||||
markers; "pkg..." also names every package pkg depends
|
||||
on). Packages that fail preflight are refused with a
|
||||
direct import: own .d.ts, no build-transform markers;
|
||||
"pkg..." also names every package pkg depends on).
|
||||
Packages that fail preflight are refused with a
|
||||
diagnostic. Eligible dependencies compile automatically.
|
||||
--provenance-sources
|
||||
EXPERIMENTAL: compile npm dependencies from their
|
||||
|
||||
@@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
REGEX,
|
||||
STRING,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import { numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
@@ -65,14 +66,14 @@ export function lowerStringReplacement(
|
||||
const search = argument(args[0], false);
|
||||
const replacement = argument(args[1], true);
|
||||
if (search.type.kind === "dyn" || replacement.type.kind === "dyn") {
|
||||
if (replacement.type.kind === "func") {
|
||||
lowerer.noLowering(`string .${method} with checked search values and callbacks`, call);
|
||||
}
|
||||
// Evaluate all inputs once before choosing the search protocol. Regexes
|
||||
// retain their native pattern; plain values use the string-search helper.
|
||||
// A typed callback receives the regex match's arguments or the plain
|
||||
// search's (match, offset, subject).
|
||||
const values = [receiver, search, replacement];
|
||||
const locals = values.map((value) => lowerer.declareHiddenLocal("%replaceArg", value.type));
|
||||
const refs = locals.map((local) => varRef(local.id, local.type, loc));
|
||||
const callback = replacement.type.kind === "func";
|
||||
const checkedReplacement =
|
||||
replacement.type.kind === "dyn"
|
||||
? lowerer.coerceInto(args[1] ?? call, refs[2]!, STRING)
|
||||
@@ -91,14 +92,27 @@ export function lowerStringReplacement(
|
||||
? {
|
||||
kind: "ternary",
|
||||
cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [refs[1]!], type: BOOL, loc },
|
||||
then: {
|
||||
kind: "regexIntrinsic",
|
||||
method,
|
||||
receiver: refs[0]!,
|
||||
args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
then: callback
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "regex.replaceCallback",
|
||||
args: [
|
||||
refs[0]!,
|
||||
lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX),
|
||||
lowerer.coerceToExpected(refs[2]!, DYN),
|
||||
{ kind: "boolLit", value: method === "replaceAll", type: BOOL, loc },
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "regexIntrinsic",
|
||||
method,
|
||||
receiver: refs[0]!,
|
||||
args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
else_: plain,
|
||||
type: STRING,
|
||||
loc,
|
||||
@@ -148,11 +162,17 @@ function lowerStringReplacementValues(
|
||||
if (callback.kind !== "func") {
|
||||
throw new InternalCompilerError(`string .${method} replacement of type ${callback.kind}`);
|
||||
}
|
||||
// A checked parameter receives the boxed argument (undefined after the
|
||||
// subject), and a checked, number or boolean result converts with
|
||||
// ToString.
|
||||
const full = [STRING, F64, STRING];
|
||||
const ret = callback.ret.kind;
|
||||
if (
|
||||
callback.ret.kind !== "string" ||
|
||||
callback.params.length > full.length ||
|
||||
!callback.params.every((type, index) => typeEquals(type, full[index]!))
|
||||
(ret !== "string" && ret !== "dyn" && ret !== "f64" && ret !== "bool") ||
|
||||
!callback.params.every(
|
||||
(type, index) =>
|
||||
type.kind === "dyn" || (index < full.length && typeEquals(type, full[index]!)),
|
||||
)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`string .${method} callback signature`,
|
||||
@@ -175,13 +195,21 @@ function lowerStringReplacementValues(
|
||||
const chunksType = arrayOf(STRING);
|
||||
const chunks = varRef("chunks.0", chunksType, loc);
|
||||
const builder = stringBuilder(loc, chunks);
|
||||
const value: IrExpr = {
|
||||
const result: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: replace,
|
||||
args: [needle, match, subject].slice(0, callback.params.length),
|
||||
type: STRING,
|
||||
args: callback.params.map((type, index) => {
|
||||
const arg = [needle, match, subject][index];
|
||||
if (arg === undefined) return dynUndefinedExpr(loc);
|
||||
return type.kind === "dyn" ? lowerer.coerceToExpected(arg, DYN) : arg;
|
||||
}),
|
||||
type: callback.ret,
|
||||
loc,
|
||||
};
|
||||
const value: IrExpr =
|
||||
ret === "dyn"
|
||||
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [result], type: STRING, loc }
|
||||
: lowerer.ensureString(result, call);
|
||||
const stop: IrStmt = { kind: "break", loc };
|
||||
const body: IrStmt[] = [
|
||||
builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }),
|
||||
|
||||
@@ -2841,6 +2841,24 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A JavaScript default that tsc types `any` (`e.title || null` in an
|
||||
// instance of a function with untyped parameters, where `e.title` is a
|
||||
// string): the checked domain, as for union operands above.
|
||||
if (
|
||||
target === null &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
||||
lowerer.dynConvertible(left.type) &&
|
||||
lowerer.dynConvertible(right.type)
|
||||
) {
|
||||
return {
|
||||
kind: "logical",
|
||||
op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||",
|
||||
left: lowerer.coerceInto(expr.left, left, DYN),
|
||||
right: lowerer.coerceInto(expr.right, right, DYN),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.unsupported("SC1042", expr);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -2,11 +2,19 @@ import * as ts from "../ts7/adapter.js";
|
||||
import { isClassOwnEnumerableFieldName } from "../../ir/ir.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
|
||||
/** Reflection that can remove a property or change its attributes. */
|
||||
const PROPERTY_REFLECTION = new Set(["deleteProperty", "freeze", "seal", "preventExtensions"]);
|
||||
/* Object.freeze, Object.seal and Object.preventExtensions do not appear
|
||||
* here: they never create or remove a property, and a class instance keeps
|
||||
* its integrity state outside the field layout. A statically typed call
|
||||
* sets the instance's CLASS_ATTRIBUTES word (class-attributes.ts), which
|
||||
* TypeScript classes use with the same fixed layouts; through an untyped
|
||||
* value the runtime refuses the operation on any class instance
|
||||
* (scr_dyn_class_integrity_fence). Lifting that fence must revisit this. */
|
||||
|
||||
/** `Object.freeze({ ... })` and the like apply to a new literal, which is
|
||||
* not a class instance. */
|
||||
/** Reflection that can remove any property. */
|
||||
const PROPERTY_REFLECTION = new Set(["deleteProperty"]);
|
||||
|
||||
/** `Reflect.deleteProperty({ ... }, key)` applies to a new literal, which
|
||||
* is not a class instance. */
|
||||
function freshLiteralArgument(callee: ts.PropertyAccessExpression): boolean {
|
||||
const call = callee.parent;
|
||||
if (!ts.isCallExpression(call) || call.expression !== callee) return false;
|
||||
@@ -141,12 +149,35 @@ export function constructionPrefix(
|
||||
return { created, sealed };
|
||||
}
|
||||
|
||||
/** Whether a class declares a getter or setter, which can intercept a
|
||||
* base constructor's field assignment. */
|
||||
function declaresAccessor(info: ClassInfo): boolean {
|
||||
return [...info.methods.keys()].some(
|
||||
(name) => name.startsWith("get:") || name.startsWith("set:"),
|
||||
);
|
||||
/** The names of the getters and setters among a class's own methods. */
|
||||
export function accessorNames(methods: Iterable<string>): Set<string> {
|
||||
const names = new Set<string>();
|
||||
for (const name of methods)
|
||||
if (name.startsWith("get:") || name.startsWith("set:")) names.add(name.slice(4));
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Whether one of `accessors` can intercept an assignment the base
|
||||
* constructions make: a base constructor's `this.name = value` runs a
|
||||
* subclass accessor of that name instead of creating the field. Only field
|
||||
* names can reach the created prefix; any other assignment already ends
|
||||
* it. */
|
||||
export function accessorInterceptsBase(accessors: ReadonlySet<string>, base: ClassInfo): boolean {
|
||||
for (const name of accessors) if (base.fields.has(name)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether an accessor that instances of `info` inherit or declare shares
|
||||
* a name with one of their fields. Such a field and accessor disagree on
|
||||
* what the instance's property is; any other accessor lives on the
|
||||
* prototype beside the fields and never touches the layout. */
|
||||
function accessorShadowsField(info: ClassInfo): boolean {
|
||||
for (let current: ClassInfo | null = info; current; current = current.base) {
|
||||
if (current.def.runtime) break;
|
||||
for (const name of accessorNames(current.methods.keys()))
|
||||
if (info.fields.has(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** `constructionPrefix` for a collected class and its bases. */
|
||||
@@ -156,7 +187,8 @@ export function construction(info: ClassInfo): Construction | null {
|
||||
if (info.base) {
|
||||
base = construction(info.base);
|
||||
if (!base) return null;
|
||||
if (declaresAccessor(info)) base = { created: base.created, sealed: false };
|
||||
if (accessorInterceptsBase(accessorNames(info.methods.keys()), info.base))
|
||||
base = { created: base.created, sealed: false };
|
||||
}
|
||||
return constructionPrefix(info.decl, base, (name) => info.fields.has(name));
|
||||
}
|
||||
@@ -169,7 +201,7 @@ export function construction(info: ClassInfo): Construction | null {
|
||||
export function fixedFieldFamily(root: ClassInfo, deleted: Set<string> | null): boolean {
|
||||
if (deleted === null) return false;
|
||||
const fixed = (info: ClassInfo): boolean => {
|
||||
if (declaresAccessor(info)) return false;
|
||||
if (accessorShadowsField(info)) return false;
|
||||
const built = construction(info);
|
||||
if (!built) return false;
|
||||
for (const field of info.fieldOrder) {
|
||||
|
||||
@@ -306,6 +306,22 @@ function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null
|
||||
);
|
||||
}
|
||||
|
||||
/** The `f` of a `(0, f)` callee (a numeric left operand, so dropping it
|
||||
* changes only the call's receiver), or null. */
|
||||
function receiverlessCalleeOf(callee: ts.Expression): ts.Expression | null {
|
||||
let e = callee;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (
|
||||
!ts.isBinaryExpression(e) ||
|
||||
e.operatorToken.kind !== ts.SyntaxKind.CommaToken ||
|
||||
!ts.isNumericLiteral(e.left)
|
||||
)
|
||||
return null;
|
||||
let right = e.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
return right;
|
||||
}
|
||||
|
||||
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr);
|
||||
@@ -329,6 +345,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const json = lowerJsonRequireCall(lowerer, expr);
|
||||
if (json) return json;
|
||||
|
||||
// `(0, ns.member)(...)` over a builtin module (tsc's and esbuild's call
|
||||
// without a receiver): Node's module functions do not read `this`, so it
|
||||
// is the member call.
|
||||
{
|
||||
const member = receiverlessCalleeOf(expr.expression);
|
||||
if (member !== null && ts.isPropertyAccessExpression(member)) {
|
||||
const builtin = lowerer.lowerNamespaceBuiltinCall(expr, member);
|
||||
if (builtin) return builtin;
|
||||
}
|
||||
}
|
||||
|
||||
// eval(...) and Function(...): Node's EvalError under
|
||||
// --disallow-code-generation-from-strings (code-generation.ts).
|
||||
{
|
||||
|
||||
@@ -175,7 +175,12 @@ import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./s
|
||||
import { lowerUrlNew } from "./lower-url.js";
|
||||
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { construction, constructionPrefix } from "./js-class-fields.js";
|
||||
import {
|
||||
accessorInterceptsBase,
|
||||
accessorNames,
|
||||
construction,
|
||||
constructionPrefix,
|
||||
} from "./js-class-fields.js";
|
||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
@@ -4112,7 +4117,7 @@ export function collectClassShapeInner(
|
||||
// A subclass field created before anything can observe the instance
|
||||
// keeps its type: super() cannot run code that reads it earlier.
|
||||
const baseConstruction =
|
||||
base && ![...methods.keys()].some((m) => m.startsWith("get:") || m.startsWith("set:"))
|
||||
base && !accessorInterceptsBase(accessorNames(methods.keys()), base)
|
||||
? construction(base)
|
||||
: null;
|
||||
const sealedPrefix = baseConstruction
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
*
|
||||
* A binding settles at the first statement of its hoisting scope (the
|
||||
* function body or the source file) that stores it unconditionally: a
|
||||
* direct `var x = e`, `x = e;`, or a loop declaring it (`for (var i = 0;
|
||||
* ...)`, `for (var k in o)`), whose own condition, update and body run
|
||||
* after the store. A read settles when it runs after that statement:
|
||||
* direct `var x = e`, `x = e;`, or a `for` loop declaring it (`for (var
|
||||
* i = 0; ...)`), whose own condition, update and body run after the store.
|
||||
* A `for (var k in o)` or `for (var v of xs)` loop stores the binding only
|
||||
* when it iterates, so it settles the reads in its body and no others. A
|
||||
* read settles when it runs after the settling statement:
|
||||
* - a read in the scope's own code, in a TypeScript file: tsc's
|
||||
* definite-assignment analysis rejects any earlier one;
|
||||
* - a read in the scope's own code, in a JavaScript file: its statement
|
||||
@@ -194,6 +196,18 @@ function referencesBySymbol(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Where a binding is first stored unconditionally: the scope statement's
|
||||
* index, and for a declarator of a `var` statement or a `for` initializer,
|
||||
* the list and the declarator's end. */
|
||||
interface Settle {
|
||||
index: number;
|
||||
list: ts.VariableDeclarationList | null;
|
||||
end: number;
|
||||
/** The first for-in/of loop declaring the binding before `index`, whose
|
||||
* body runs after its store, or -1. */
|
||||
loop: number;
|
||||
}
|
||||
|
||||
class ScopeAnalysis {
|
||||
private readonly js: boolean;
|
||||
/** Whether a function declaration's references all settle, by scope
|
||||
@@ -214,10 +228,13 @@ class ScopeAnalysis {
|
||||
}
|
||||
|
||||
/** The index of the scope statement that first stores `symbol`
|
||||
* unconditionally, or Infinity. */
|
||||
settleIndex(scope: Scope, symbol: ts.Symbol): number {
|
||||
* unconditionally (Infinity when none does), and, when that statement
|
||||
* declares it in a `var` statement or a `for` initializer, the end of the
|
||||
* storing declarator: later declarators of the list run after it. */
|
||||
settleIndex(scope: Scope, symbol: ts.Symbol): Settle {
|
||||
const statements = statementsOf(scope);
|
||||
const checker = this.lowerer.checker;
|
||||
let loopIndex = -1;
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
const s = statements[i]!;
|
||||
const list = ts.isVariableStatement(s)
|
||||
@@ -233,8 +250,10 @@ class ScopeAnalysis {
|
||||
if (
|
||||
(d.initializer !== undefined || loop) &&
|
||||
boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol)
|
||||
)
|
||||
return i;
|
||||
) {
|
||||
if (!loop) return { index: i, list, end: d.end, loop: loopIndex };
|
||||
if (loopIndex < 0) loopIndex = i;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
ts.isExpressionStatement(s) &&
|
||||
@@ -243,20 +262,21 @@ class ScopeAnalysis {
|
||||
ts.isIdentifier(s.expression.left) &&
|
||||
checker.getSymbolAtLocation(s.expression.left) === symbol
|
||||
) {
|
||||
return i;
|
||||
return { index: i, list: null, end: -1, loop: loopIndex };
|
||||
}
|
||||
}
|
||||
return Infinity;
|
||||
return { index: Infinity, list: null, end: -1, loop: loopIndex };
|
||||
}
|
||||
|
||||
/** Whether `ref` (inside `scope`, or in another module) runs only after
|
||||
* the statement at index `settle`. `read`: `ref` reads the binding
|
||||
* itself, so tsc vouches for one in the scope's own code. */
|
||||
settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean {
|
||||
* the store `settle` describes. `read`: `ref` reads the binding itself,
|
||||
* so tsc vouches for one in the scope's own code. */
|
||||
settles(scope: Scope, settle: Settle, ref: ts.Identifier, read = true): boolean {
|
||||
if (ts.getSourceFileOfNode(ref) !== this.sf) return true;
|
||||
const statements = statementsOf(scope);
|
||||
let deferred: ts.Node | null = null;
|
||||
let top: ts.Node | null = null;
|
||||
const container: ts.Node | undefined = ts.isSourceFile(scope) ? scope : scope.body;
|
||||
for (let n: ts.Node = ref; n.parent !== undefined && n !== scope; n = n.parent) {
|
||||
if (
|
||||
ts.isFunctionLike(n) ||
|
||||
@@ -265,18 +285,26 @@ class ScopeAnalysis {
|
||||
ts.isClassStaticBlockDeclaration(n)
|
||||
)
|
||||
deferred = n;
|
||||
if (n.parent === scope || (!ts.isSourceFile(scope) && n.parent === scope.body)) top = n;
|
||||
// The scope's own statement, not the function body that holds it.
|
||||
if (n.parent === container) top = n;
|
||||
}
|
||||
if (top === null) return false;
|
||||
if (deferred === null && read && !this.js) return true;
|
||||
if (deferred !== null && ts.isFunctionDeclaration(deferred) && deferred.name !== undefined)
|
||||
return this.functionSettles(scope, settle, deferred);
|
||||
const index = statements.indexOf(top as ts.Statement);
|
||||
return index > settle || (index === settle && settlesWithin(top, ref));
|
||||
return (
|
||||
index > settle.index ||
|
||||
(index === settle.index &&
|
||||
(settlesWithin(top, ref) ||
|
||||
// A later declarator of the storing list (`var a = {}, b = { ...a }`).
|
||||
(settle.list !== null && isInside(ref, settle.list) && ref.pos >= settle.end))) ||
|
||||
(index === settle.loop && settlesWithin(top, ref))
|
||||
);
|
||||
}
|
||||
|
||||
private functionSettles(scope: Scope, settle: number, fn: ts.FunctionDeclaration): boolean {
|
||||
const key = `${fn.pos}:${settle}`;
|
||||
private functionSettles(scope: Scope, settle: Settle, fn: ts.FunctionDeclaration): boolean {
|
||||
const key = `${fn.pos}:${settle.index}:${settle.end}:${settle.loop}`;
|
||||
const known = this.functionMemo.get(key);
|
||||
if (known !== undefined) return known;
|
||||
this.functionMemo.set(key, true);
|
||||
|
||||
@@ -276,13 +276,14 @@ export default require_f();
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("an identifier-minified bundle names its lost helper names", () => {
|
||||
// esbuild --minify output: the helper shapes survive, their names do not.
|
||||
const result = split(
|
||||
test("an identifier-minified bundle splits through its helper's shape", () => {
|
||||
// esbuild --minify output: the helper shape survives, its name does not.
|
||||
const answer = expectSameBehavior(
|
||||
`var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` +
|
||||
`var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`,
|
||||
`var x=c((t,m)=>{m.exports={v:1}});var y=c(t=>{t.w=x().v+1});` +
|
||||
`console.log(x().v,y().w,x()===x());`,
|
||||
);
|
||||
expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string });
|
||||
expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.x.cjs", "index.js.y.cjs"]);
|
||||
});
|
||||
|
||||
test("an ES-module bundle of CommonJS chunks names the chunk interop", () => {
|
||||
|
||||
@@ -525,7 +525,7 @@ export function splitBundledChunks(
|
||||
}
|
||||
}
|
||||
}
|
||||
const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS");
|
||||
let helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS");
|
||||
|
||||
// Every name a recognizer relies on is written nowhere and declared
|
||||
// once at the top level; `Object` itself is never rebound.
|
||||
@@ -552,18 +552,14 @@ export function splitBundledChunks(
|
||||
);
|
||||
};
|
||||
if (helperNames.length === 0) {
|
||||
// The helper's shape under another name: an identifier-minified bundle,
|
||||
// whose other interop helpers lost their names as well.
|
||||
for (const [name, t] of top) {
|
||||
// An identifier-minified bundle: the helper's shape under another name.
|
||||
helperNames = [...top].flatMap(([name, t]) => {
|
||||
const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined;
|
||||
if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null)
|
||||
return {
|
||||
degrade:
|
||||
"its shipped JS is an identifier-minified bundle whose bundler-interop helpers " +
|
||||
"(__commonJS and the rest) have lost the names the recognizers verify",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
return init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null
|
||||
? [name]
|
||||
: [];
|
||||
});
|
||||
if (helperNames.length === 0) return null;
|
||||
}
|
||||
const forms = new Map<string, HelperForm>();
|
||||
for (const name of helperNames) {
|
||||
|
||||
@@ -260,11 +260,21 @@ function definePropertyExportOf(
|
||||
value = getterReturnOf(p);
|
||||
}
|
||||
}
|
||||
const esModuleStamp =
|
||||
name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword;
|
||||
const esModuleStamp = name === "__esModule" && value !== null && isTrueLiteral(value);
|
||||
return { name, value, esModuleStamp };
|
||||
}
|
||||
|
||||
/** `true`, or `!0`, its minified spelling. */
|
||||
function isTrueLiteral(expr: ts.Expression): boolean {
|
||||
return (
|
||||
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
(ts.isPrefixUnaryExpression(expr) &&
|
||||
expr.operator === ts.SyntaxKind.ExclamationToken &&
|
||||
ts.isNumericLiteral(expr.operand) &&
|
||||
expr.operand.text === "0")
|
||||
);
|
||||
}
|
||||
|
||||
/** Space-pads [start, end) of `text` preserving newlines — the recognized
|
||||
* statement disappears while every original offset survives. */
|
||||
function padSpan(text: string, start: number, end: number): string {
|
||||
|
||||
@@ -614,27 +614,12 @@ export function clearProgramBuiltinAliasCache(): void {
|
||||
* but the bundler-emitted CJS export rewrite (npm-static-rewrite.ts)
|
||||
* types the getter-table shapes and the offender attribution degrades a
|
||||
* package whose surface still breaks its consumers — so those bundles are
|
||||
* worth ATTEMPTING: eligible when the .d.ts and unminified criteria hold,
|
||||
* gracefully per-package-degraded when the attempt fails. */
|
||||
* worth ATTEMPTING: eligible when the .d.ts criteria hold, gracefully
|
||||
* per-package-degraded when the attempt fails. Minified JS is attempted
|
||||
* too: a minified dist is ordinary JavaScript, and the bundle recognizers
|
||||
* know esbuild's __commonJS helper by its shape under any name. */
|
||||
const TRANSFORM_MARKERS = ["__webpack_require__"];
|
||||
|
||||
/** Unminified-JS heuristic over an entry source: at least two lines and
|
||||
* an average line length under 200 characters (minified dists are one
|
||||
* enormous line; readable code averages well under 100). */
|
||||
function looksUnminified(source: string): boolean {
|
||||
const lines = source.split("\n");
|
||||
if (lines.length < 2) return false;
|
||||
return source.length / lines.length <= 200;
|
||||
}
|
||||
|
||||
/** A whitespace/syntax-minified bundler dist whose interop helpers keep
|
||||
* their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers
|
||||
* verify those shapes and refuse the package precisely when they cannot,
|
||||
* so the minification heuristic need not refuse it up front. */
|
||||
function namedBundlerHelpers(source: string): boolean {
|
||||
return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source);
|
||||
}
|
||||
|
||||
function hasTransformMarkers(source: string): boolean {
|
||||
return TRANSFORM_MARKERS.some((m) => source.includes(m));
|
||||
}
|
||||
@@ -676,8 +661,6 @@ export function npmStaticIneligibleReason(
|
||||
if (jsEntry === null) return "no runtime JS entry resolves";
|
||||
const source = trackedReadFile(jsEntry);
|
||||
if (source === null) return `its runtime entry ${jsEntry} cannot be read`;
|
||||
if (!looksUnminified(source) && !namedBundlerHelpers(source))
|
||||
return "its shipped JS looks minified";
|
||||
if (hasTransformMarkers(source)) {
|
||||
return "its shipped JS carries build-transform markers (bundled/transpiled dist)";
|
||||
}
|
||||
|
||||
@@ -371,15 +371,60 @@ test("repeated cycle edges reuse a file's binding index", () => {
|
||||
|
||||
test.for([
|
||||
[
|
||||
// A shared instance built by a user constructor: the reads of it in
|
||||
// the partner's function can run early; the class cannot.
|
||||
// A shared instance built by a user constructor that calls the
|
||||
// partner's function: its read of the instance can run early.
|
||||
{
|
||||
"main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);',
|
||||
"a.ts":
|
||||
'import { f } from "./b.ts"; export class C { n = 1; constructor() { console.log(f()); } } export const shared = new C();',
|
||||
"b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }',
|
||||
},
|
||||
["shared"],
|
||||
],
|
||||
[
|
||||
// User code runs while the instance is built, but nothing it reaches
|
||||
// calls the partner's function before the declaration completed.
|
||||
{
|
||||
"main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);',
|
||||
"a.ts":
|
||||
'import { f } from "./b.ts"; export class C { n = 1; } export const shared = new C(); console.log(f());',
|
||||
"b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }',
|
||||
},
|
||||
["shared"],
|
||||
[],
|
||||
],
|
||||
[
|
||||
// The partner's top level runs first and calls a hoisted function of
|
||||
// the importer that reads a const declared later; a second function
|
||||
// reads another const but nothing calls it early.
|
||||
{
|
||||
"main.ts": 'import { late } from "./a.ts"; console.log(late());',
|
||||
"a.ts":
|
||||
'import { probe } from "./b.ts"; export function read(): string { return name; } export function late(): string { return other + probe; } export const name = "n"; export const other = "o";',
|
||||
"b.ts":
|
||||
'import { read } from "./a.ts"; function attempt(): string { try { return read(); } catch { return "tdz"; } } export const probe = attempt();',
|
||||
},
|
||||
["name"],
|
||||
],
|
||||
[
|
||||
// The partner's class calls a method through `this` in a static
|
||||
// initializer; the method reads a const of the importer declared later.
|
||||
{
|
||||
"main.ts": 'import { name } from "./a.ts"; console.log(name);',
|
||||
"a.ts": 'import { Probe } from "./b.ts"; export const name = "n"; console.log(Probe.value);',
|
||||
"b.ts":
|
||||
'import { name } from "./a.ts"; export class Probe { static read(): string { return name; } static value = this.read(); }',
|
||||
},
|
||||
["name"],
|
||||
],
|
||||
[
|
||||
// The same through a static block of a class stored by a const.
|
||||
{
|
||||
"main.ts": 'import { name } from "./a.ts"; console.log(name);',
|
||||
"a.ts": 'import { Box } from "./b.ts"; export const name = "n"; console.log(Box.value);',
|
||||
"b.ts":
|
||||
'import { name } from "./a.ts"; export const Box = class { static value = ""; static read(): string { return name; } static { this.value = this.read(); } };',
|
||||
},
|
||||
["name"],
|
||||
],
|
||||
[
|
||||
// The partner's top level reads the importer's const before it ran.
|
||||
|
||||
@@ -1441,8 +1441,20 @@ function requiresOf7(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `true`, or `!0`, its minified spelling. */
|
||||
function isTrueLiteral(expr: ts.Expression): boolean {
|
||||
return (
|
||||
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
(ts.isPrefixUnaryExpression(expr) &&
|
||||
expr.operator === ts.SyntaxKind.ExclamationToken &&
|
||||
ts.isNumericLiteral(expr.operand) &&
|
||||
expr.operand.text === "0")
|
||||
);
|
||||
}
|
||||
|
||||
/** The exact `Object.defineProperty(exports|module.exports, "__esModule",
|
||||
* { value: true })` interop stamp (shared by preflight and the no-op lowering). */
|
||||
* { value: true })` interop stamp (shared by preflight and the no-op lowering);
|
||||
* `{ value: !0 }` is the same stamp. */
|
||||
export function isEsModuleStamp(expr: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false;
|
||||
const callee = expr.expression;
|
||||
@@ -1476,7 +1488,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean {
|
||||
ts.isPropertyAssignment(p) &&
|
||||
ts.isIdentifier(p.name) &&
|
||||
p.name.text === "value" &&
|
||||
p.initializer.kind === ts.SyntaxKind.TrueKeyword
|
||||
isTrueLiteral(p.initializer)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3896,22 +3908,7 @@ function computeCycleEarlyBindings7(
|
||||
const evaluation = known
|
||||
? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!)
|
||||
: comp.slice();
|
||||
interface Candidate {
|
||||
symbol: ts.Symbol;
|
||||
name: ts.Identifier;
|
||||
kind: CycleEarlyBinding["kind"];
|
||||
module: ts.SourceFile;
|
||||
/** User code can run between the cycle's start and the end of the
|
||||
* declaration (always true when the order is unknown). */
|
||||
exposed: boolean;
|
||||
rank: number;
|
||||
start: number;
|
||||
end: number;
|
||||
reference: ts.Identifier | null;
|
||||
folded: ts.Identifier | null;
|
||||
localNames: Map<string, string>;
|
||||
}
|
||||
const candidates = new Map<ts.Symbol, Candidate>();
|
||||
const candidates = new Map<ts.Symbol, CycleCandidate7>();
|
||||
const names = new Set<string>();
|
||||
let dirty = !known;
|
||||
evaluation.forEach((m, rank) => {
|
||||
@@ -3948,6 +3945,7 @@ function computeCycleEarlyBindings7(
|
||||
start: ts.tokenStartOfNode(stmt),
|
||||
end: stmt.end,
|
||||
reference: null,
|
||||
deferredReference: null,
|
||||
folded: null,
|
||||
localNames: new Map(),
|
||||
});
|
||||
@@ -4016,15 +4014,27 @@ function computeCycleEarlyBindings7(
|
||||
ref.end <= c.end
|
||||
)
|
||||
continue;
|
||||
if (inDeferredPosition7(ref)) {
|
||||
if (c.exposed && c.deferredReference === null) c.deferredReference = ref;
|
||||
continue;
|
||||
}
|
||||
let early: boolean;
|
||||
if (inDeferredPosition7(ref)) early = c.exposed;
|
||||
else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
|
||||
if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
|
||||
else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false;
|
||||
else early = !known || rankOf.get(sf)! < c.rank;
|
||||
if (early) c.reference = ref;
|
||||
}
|
||||
}
|
||||
// With a static evaluation order, a deferred reference only runs early
|
||||
// when the code that runs before the declaration completes can reach
|
||||
// the function holding it.
|
||||
const reached =
|
||||
known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference)
|
||||
? cycleReachedDeferredReferences7(program, evaluation, candidates)
|
||||
: null;
|
||||
for (const c of candidates.values()) {
|
||||
if (c.reference === null && c.deferredReference !== null)
|
||||
c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null);
|
||||
if (c.reference === null) continue;
|
||||
const settled = new Set<ts.SourceFile>(order.filter((sf) => !rankOf.has(sf)));
|
||||
if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m);
|
||||
@@ -4043,6 +4053,299 @@ function computeCycleEarlyBindings7(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Collects into `units` the callables an initializer expression stores
|
||||
* (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a
|
||||
* stored node that run where it is evaluated. */
|
||||
function storedCallables7(
|
||||
checker: ts.TypeChecker,
|
||||
e: ts.Expression,
|
||||
units: ts.Node[],
|
||||
immediate: Map<ts.Node, ts.Node[]>,
|
||||
): void {
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
||||
units.push(e);
|
||||
} else if (ts.isClassExpression(e)) {
|
||||
units.push(e);
|
||||
immediate.set(e, classSyncParts7(e));
|
||||
} else if (ts.isObjectLiteralExpression(e)) {
|
||||
for (const p of e.properties.nodes) {
|
||||
if (
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isGetAccessorDeclaration(p) ||
|
||||
ts.isSetAccessorDeclaration(p)
|
||||
) {
|
||||
// A computed key evaluates where the literal does; a plain
|
||||
// member-chain key (`[Equal.symbol]`) runs no user code.
|
||||
if (!ts.isComputedPropertyName(p.name)) units.push(p);
|
||||
else if (memberChainKey7(p.name.expression)) {
|
||||
units.push(p);
|
||||
immediate.set(p, [p.name]);
|
||||
}
|
||||
} else if (ts.isPropertyAssignment(p))
|
||||
storedCallables7(checker, p.initializer, units, immediate);
|
||||
}
|
||||
} else if (ts.isArrayLiteralExpression(e)) {
|
||||
for (const el of e.elements.nodes)
|
||||
if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate);
|
||||
} else if (ts.isCallExpression(e)) {
|
||||
// `const f = wrap(callback)` where wrap only returns closures over
|
||||
// the callback: it runs when the stored result is called.
|
||||
e.arguments.nodes.forEach((arg, index) => {
|
||||
let fn: ts.Expression = arg;
|
||||
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
||||
if (
|
||||
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
|
||||
returnsOnlyClosuresOver7(checker, e.expression, index)
|
||||
)
|
||||
units.push(fn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** A binding of an import cycle and what the reference scan found. */
|
||||
interface CycleCandidate7 {
|
||||
symbol: ts.Symbol;
|
||||
name: ts.Identifier;
|
||||
kind: CycleEarlyBinding["kind"];
|
||||
module: ts.SourceFile;
|
||||
/** User code can run between the cycle's start and the end of the
|
||||
* declaration (always true when the order is unknown). */
|
||||
exposed: boolean;
|
||||
rank: number;
|
||||
start: number;
|
||||
end: number;
|
||||
/** A reference read at top level before the declaration ran. */
|
||||
reference: ts.Identifier | null;
|
||||
/** A deferred reference (function body, parameter default, instance
|
||||
* field) when user code may run before the declaration completes. */
|
||||
deferredReference: ts.Identifier | null;
|
||||
folded: ts.Identifier | null;
|
||||
localNames: Map<string, string>;
|
||||
}
|
||||
|
||||
/** For one import cycle with a static evaluation order: the first deferred
|
||||
* reference to each candidate that code running before the candidate's
|
||||
* declaration completes can reach, or null when the walk cannot be precise
|
||||
* (top-level await, a default-exported function or class declaration).
|
||||
*
|
||||
* The cycle's top-level code runs in evaluation order. A stored callable
|
||||
* (a function or class declaration, or a function, class or object-literal
|
||||
* method stored by a variable initializer) becomes reachable once
|
||||
* reachable code names its binding, through an import alias or a
|
||||
* namespace member too; every other function-like in reachable code counts
|
||||
* as reachable, since it may be handed to a callee. A namespace import of
|
||||
* a cycle member used other than for a member read makes every stored
|
||||
* callable reachable. moduleEarlyBindingsOf7 is the single-module form. */
|
||||
function cycleReachedDeferredReferences7(
|
||||
program: ts.Program,
|
||||
evaluation: readonly ts.SourceFile[],
|
||||
candidates: ReadonlyMap<ts.Symbol, CycleCandidate7>,
|
||||
): Map<ts.Symbol, ts.Identifier> | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const members = new Set(evaluation);
|
||||
for (const sf of evaluation) {
|
||||
if (hasTopLevelAwait7(sf)) return null;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
(ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&
|
||||
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
||||
ts.ModifierFlags.Default) !==
|
||||
0
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const stored = new Set<ts.Node>();
|
||||
const immediate = new Map<ts.Node, ts.Node[]>();
|
||||
const storedNames: ts.Identifier[] = [];
|
||||
const unitsOfName = new Map<ts.Identifier, ts.Node[]>();
|
||||
const store = (name: ts.Identifier, units: ts.Node[]): void => {
|
||||
if (units.length === 0) return;
|
||||
for (const u of units) stored.add(u);
|
||||
storedNames.push(name);
|
||||
unitsOfName.set(name, units);
|
||||
};
|
||||
for (const sf of evaluation) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
||||
ts.ModifierFlags.Ambient) !==
|
||||
0
|
||||
)
|
||||
continue;
|
||||
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
|
||||
store(stmt.name, [stmt]);
|
||||
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
|
||||
store(stmt.name, [stmt]);
|
||||
} else if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
if (ts.isIdentifier(d.name) && d.initializer !== undefined) {
|
||||
const units: ts.Node[] = [];
|
||||
storedCallables7(checker, d.initializer, units, immediate);
|
||||
store(d.name, units);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const unitsBySymbol = new Map<ts.Symbol, ts.Node[]>();
|
||||
const allUnits: ts.Node[] = [];
|
||||
for (const name of storedNames) {
|
||||
const symbol = checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) continue;
|
||||
const units = unitsOfName.get(name)!;
|
||||
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]);
|
||||
allUnits.push(...units);
|
||||
}
|
||||
// A named class expression's inner name (`class Impl { static x =
|
||||
// Impl.m(); }`) names the class inside its own body, as the binding it
|
||||
// is stored in names it outside.
|
||||
const innerNames: ts.Identifier[] = [];
|
||||
for (const unit of allUnits) {
|
||||
if (!ts.isClassExpression(unit) || unit.name === undefined) continue;
|
||||
const symbol = checker.getSymbolAtLocation(unit.name);
|
||||
if (symbol === undefined) continue;
|
||||
innerNames.push(unit.name);
|
||||
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]);
|
||||
}
|
||||
// The identifier texts worth resolving: declared names, and the local
|
||||
// names of imports (named, default and namespace) in cycle members.
|
||||
const names = new Set<string>();
|
||||
for (const c of candidates.values()) names.add(c.name.text);
|
||||
for (const name of storedNames) names.add(name.text);
|
||||
for (const name of innerNames) names.add(name.text);
|
||||
for (const sf of evaluation) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue;
|
||||
const clause = stmt.importClause;
|
||||
if (clause.name !== undefined) names.add(clause.name.text);
|
||||
const bindings = clause.namedBindings;
|
||||
if (bindings === undefined) continue;
|
||||
if (ts.isNamedImports(bindings))
|
||||
for (const element of bindings.elements.nodes) names.add(element.name.text);
|
||||
else names.add(bindings.name.text);
|
||||
}
|
||||
}
|
||||
const idents: ts.Identifier[] = [];
|
||||
for (const sf of evaluation) {
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (ts.isImportDeclaration(node)) return "skip";
|
||||
if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
checker.prefetchSymbolNodesExact(idents);
|
||||
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
|
||||
const parent = id.parent;
|
||||
let symbol = checker.getSymbolAtLocation(id);
|
||||
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
|
||||
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
|
||||
}
|
||||
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = checker.getAliasedSymbol(symbol);
|
||||
}
|
||||
return symbol;
|
||||
};
|
||||
const isMemberNamespace = (symbol: ts.Symbol): boolean =>
|
||||
(symbol.flags & ts.SymbolFlags.ValueModule) !== 0 &&
|
||||
symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d));
|
||||
const declared = new Set<ts.Symbol>();
|
||||
const reachedUnits = new Set<ts.Node>();
|
||||
const out = new Map<ts.Symbol, ts.Identifier>();
|
||||
const work: ts.Node[] = [];
|
||||
const reach = (unit: ts.Node): void => {
|
||||
if (reachedUnits.has(unit)) return;
|
||||
reachedUnits.add(unit);
|
||||
work.push(unit);
|
||||
};
|
||||
const visit = (root: ts.Node): void => {
|
||||
ts.walkPreorder(root, (node) => {
|
||||
if (node !== root && stored.has(node)) {
|
||||
for (const part of immediate.get(node) ?? []) visit(part);
|
||||
return "skip";
|
||||
}
|
||||
if (ts.isImportDeclaration(node)) return "skip";
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
// `this` in a static initializer or block names the class, so it
|
||||
// reaches the class's methods as naming the class does.
|
||||
const owner = staticThisClass7(node);
|
||||
if (owner !== undefined && stored.has(owner)) reach(owner);
|
||||
return undefined;
|
||||
}
|
||||
if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
|
||||
if (inTypePosition7(node)) return undefined;
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined;
|
||||
if (parent !== undefined && declaredName7(parent) === node) return undefined;
|
||||
const symbol = symbolOf(node);
|
||||
if (symbol === undefined) return undefined;
|
||||
if (isMemberNamespace(symbol)) {
|
||||
// `ns.member` resolves through the member's own identifier; any
|
||||
// other use can reach every export.
|
||||
if (
|
||||
!(
|
||||
parent !== undefined &&
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.expression === node
|
||||
)
|
||||
)
|
||||
for (const unit of allUnits) reach(unit);
|
||||
return undefined;
|
||||
}
|
||||
const c = candidates.get(symbol);
|
||||
if (
|
||||
c !== undefined &&
|
||||
node !== c.name &&
|
||||
!declared.has(symbol) &&
|
||||
!out.has(symbol) &&
|
||||
inDeferredPosition7(node) &&
|
||||
!(
|
||||
c.kind === "class" &&
|
||||
ts.getSourceFileOfNode(node) === c.module &&
|
||||
ts.tokenStartOfNode(node) >= c.start &&
|
||||
node.end <= c.end
|
||||
)
|
||||
)
|
||||
out.set(symbol, node);
|
||||
for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit);
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
const drain = (): void => {
|
||||
while (work.length > 0) visit(work.pop()!);
|
||||
};
|
||||
const declare = (name: ts.BindingName): void => {
|
||||
for (const id of boundNames7(name)) {
|
||||
const symbol = checker.getSymbolAtLocation(id);
|
||||
if (symbol !== undefined) declared.add(symbol);
|
||||
}
|
||||
};
|
||||
for (const sf of evaluation) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
for (const part of classSyncParts7(stmt)) visit(part);
|
||||
drain();
|
||||
if (stmt.name !== undefined) declare(stmt.name);
|
||||
} else if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
visit(d);
|
||||
drain();
|
||||
declare(d.name);
|
||||
}
|
||||
} else if (!ts.isFunctionDeclaration(stmt)) visit(stmt);
|
||||
drain();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Top-level let/const and class bindings, in modules outside the
|
||||
* exactly-initialized cycles, that code running before their declaration
|
||||
* completes can read.
|
||||
@@ -4107,51 +4410,8 @@ function moduleEarlyBindingsOf7(
|
||||
storedBy.set(name.text, list);
|
||||
};
|
||||
const checker = program.getTypeChecker();
|
||||
const storedIn = (e: ts.Expression, units: ts.Node[]): void => {
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
||||
units.push(e);
|
||||
} else if (ts.isClassExpression(e)) {
|
||||
units.push(e);
|
||||
immediate.set(e, classSyncParts7(e));
|
||||
} else if (ts.isObjectLiteralExpression(e)) {
|
||||
for (const p of e.properties.nodes) {
|
||||
if (
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isGetAccessorDeclaration(p) ||
|
||||
ts.isSetAccessorDeclaration(p)
|
||||
) {
|
||||
// A computed key evaluates where the literal does; a plain
|
||||
// member-chain key (`[Equal.symbol]`) runs no user code.
|
||||
if (!ts.isComputedPropertyName(p.name)) units.push(p);
|
||||
else if (memberChainKey7(p.name.expression)) {
|
||||
units.push(p);
|
||||
immediate.set(p, [p.name]);
|
||||
}
|
||||
} else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units);
|
||||
}
|
||||
} else if (ts.isArrayLiteralExpression(e)) {
|
||||
for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units);
|
||||
} else if (ts.isCallExpression(e)) {
|
||||
// `const f = wrap(callback)` where wrap only returns closures over
|
||||
// the callback: it runs when the stored result is called.
|
||||
e.arguments.nodes.forEach((arg, index) => {
|
||||
let fn: ts.Expression = arg;
|
||||
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
||||
if (
|
||||
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
|
||||
returnsOnlyClosuresOver7(checker, e.expression, index)
|
||||
)
|
||||
units.push(fn);
|
||||
});
|
||||
}
|
||||
};
|
||||
const storedIn = (e: ts.Expression, units: ts.Node[]): void =>
|
||||
storedCallables7(checker, e, units, immediate);
|
||||
sf.statements.nodes.forEach((stmt, index) => {
|
||||
if (ambient(stmt)) return;
|
||||
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
|
||||
@@ -4404,6 +4664,40 @@ function returnsOnlyClosuresOver7(
|
||||
* expressions, decorators, computed member keys, static field
|
||||
* initializers and static blocks. Methods and instance fields wait for a
|
||||
* call or a construction. */
|
||||
/** The class whose static field initializer or static block evaluates `this`
|
||||
* (through arrow functions, which keep the enclosing `this`), or undefined
|
||||
* when another function or a class's instance side binds it. */
|
||||
function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined {
|
||||
let child = node;
|
||||
for (let n = node.parent; n !== undefined; child = n, n = n.parent) {
|
||||
if (ts.isArrowFunction(n)) continue;
|
||||
if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration;
|
||||
if (ts.isPropertyDeclaration(n)) {
|
||||
if (n.initializer !== child) return undefined;
|
||||
return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0
|
||||
? (n.parent as ts.ClassLikeDeclaration)
|
||||
: undefined;
|
||||
}
|
||||
if (ts.isComputedPropertyName(n)) {
|
||||
// A computed key evaluates in the scope around the class.
|
||||
const member = n.parent;
|
||||
const cls = member?.parent;
|
||||
if (cls === undefined) return undefined;
|
||||
child = cls;
|
||||
n = cls;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
ts.isFunctionLike(n) ||
|
||||
ts.isClassDeclaration(n) ||
|
||||
ts.isClassExpression(n) ||
|
||||
ts.isSourceFile(n)
|
||||
)
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] {
|
||||
const decorators = (n: ts.Node): ts.Node[] =>
|
||||
((n as { modifiers?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []).filter(
|
||||
|
||||
@@ -193,3 +193,34 @@ test("number and boolean writes to class fields run no user code", () => {
|
||||
// A reference stored into a field releases the old one.
|
||||
expect(preserves([fieldSet(arr("b"))], "i", new Set(), scope)).toBe(false);
|
||||
});
|
||||
|
||||
test("optional element reads, their assertion errors and present-undefined stores at an in-range index keep the length", () => {
|
||||
const optional = (receiver: string, index: IrExpr): IrExpr => ({
|
||||
kind: "call",
|
||||
callee: "%arr.idxOr.f64",
|
||||
args: [arr(receiver), index],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const undefinedAt = (receiver: string, index: IrExpr): IrStmt => ({
|
||||
kind: "arraySetUndefined",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
loc,
|
||||
});
|
||||
const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [num(1)], type: F64, loc };
|
||||
expect(preserves([stmt(optional("a", ref("i"))), stmt(thrown)])).toBe(true);
|
||||
expect(preserves([undefinedAt("a", ref("i"))])).toBe(true);
|
||||
// Past the counter, a present undefined extends the array.
|
||||
expect(preserves([undefinedAt("a", plus(ref("i"), num(1)))])).toBe(false);
|
||||
// The high counter of a two-counter loop is in range as well.
|
||||
const paired = (body: IrStmt[]): boolean =>
|
||||
preservesArrayLength(
|
||||
[cond, ...body],
|
||||
{ receiver: "a", inRange: "i", inRangeHigh: "j" },
|
||||
locals,
|
||||
new Set(),
|
||||
);
|
||||
expect(paired([undefinedAt("a", ref("j")), set("a", ref("j"), num(0))])).toBe(true);
|
||||
expect(preserves([set("a", ref("j"), num(0))])).toBe(false);
|
||||
});
|
||||
|
||||
@@ -162,6 +162,10 @@ export interface ArrayLengthLoop {
|
||||
* long as the length itself has not changed). An element write at this
|
||||
* index replaces an existing element or sets a non-index property. */
|
||||
inRange: string | null;
|
||||
/** A second such binding: the high counter of a two-counter loop
|
||||
* (`for (let i = lo, j = hi; i < j; i++, j--)`), whose values stay
|
||||
* between the low counter's start and its own. */
|
||||
inRangeHigh?: string | null;
|
||||
}
|
||||
|
||||
/** Whether running `nodes` (a loop's condition, body and update) can never
|
||||
@@ -191,7 +195,10 @@ export function preservesArrayLength(
|
||||
// escaping.
|
||||
const receiver = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && e.localId === loop.receiver) || expr(e);
|
||||
const written = (id: string): boolean => id === loop.receiver || id === loop.inRange;
|
||||
const inRange = (id: string): boolean =>
|
||||
(loop.inRange !== null && id === loop.inRange) ||
|
||||
(loop.inRangeHigh != null && id === loop.inRangeHigh);
|
||||
const written = (id: string): boolean => id === loop.receiver || inRange(id);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
@@ -224,12 +231,21 @@ export function preservesArrayLength(
|
||||
case "call":
|
||||
// A checked union extraction (`x!`, coercions/unions.ts): the
|
||||
// asserted arm's payload or a TypeError, and no user code.
|
||||
return e.callee.startsWith("%union.narrow.") && e.args.every(expr);
|
||||
if (e.callee.startsWith("%union.narrow.")) return e.args.every(expr);
|
||||
// An element read as `T | undefined` (int32-arrays.ts isElementReader).
|
||||
return (
|
||||
e.callee.startsWith("%arr.idxOr.") &&
|
||||
e.args.length === 2 &&
|
||||
receiver(e.args[0]!) &&
|
||||
expr(e.args[1]!)
|
||||
);
|
||||
case "mapIntrinsic":
|
||||
return READING_MAP_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr);
|
||||
case "setIntrinsic":
|
||||
return READING_SET_METHODS.has(e.method) && expr(e.receiver) && e.args.every(expr);
|
||||
case "libCall":
|
||||
// The TypeError of an assertion past a union arm leaves the loop.
|
||||
if (e.fn === "error.nodeThrow") return e.args.every(expr);
|
||||
return (
|
||||
isBorrowSafeMath(e.fn) &&
|
||||
e.args.every((arg) => arg.type.kind === "f64") &&
|
||||
@@ -254,14 +270,18 @@ export function preservesArrayLength(
|
||||
break;
|
||||
case "arraySet":
|
||||
return (
|
||||
(separate(s.arr) ||
|
||||
(s.index.kind === "varRef" &&
|
||||
loop.inRange !== null &&
|
||||
s.index.localId === loop.inRange)) &&
|
||||
(separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) &&
|
||||
receiver(s.arr) &&
|
||||
expr(s.index) &&
|
||||
expr(s.value)
|
||||
);
|
||||
case "arraySetUndefined":
|
||||
// A present undefined replacing an element below the length.
|
||||
return (
|
||||
(separate(s.arr) || (s.index.kind === "varRef" && inRange(s.index.localId))) &&
|
||||
receiver(s.arr) &&
|
||||
expr(s.index)
|
||||
);
|
||||
case "return":
|
||||
return s.value === null || expr(s.value);
|
||||
case "fieldSet":
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
|
||||
import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
|
||||
type SwitchCase = Extract<IrStmt, { kind: "switch" }>["cases"][number];
|
||||
@@ -6,22 +7,29 @@ import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
mapExprChildren,
|
||||
mapStmtChildren,
|
||||
transformExpr,
|
||||
transformStmt,
|
||||
transformStmtList,
|
||||
type IrTransform,
|
||||
} from "./traverse.js";
|
||||
|
||||
/** Local number arrays the backend may store as int32 slots.
|
||||
/** Number arrays the backend may store as int32 slots.
|
||||
*
|
||||
* A candidate is a local `number[]` of a synchronous function that no
|
||||
* other binding can hold and only compiled element operations reach:
|
||||
* every write of the local stores a fresh array (an array literal, or
|
||||
* `new Array(n).fill(v)`), and every read of it is the receiver of an
|
||||
* element read, write, state test, `length`, `push` or `pop` (a discarded
|
||||
* `pop` included), or the array of an element-reader helper call. The
|
||||
* array never reaches a runtime function that reads its slots, a library
|
||||
* call, a closure, a return or untyped code.
|
||||
* A candidate is a `number[]` binding that only compiled element
|
||||
* operations reach: a local of a synchronous function, a module global, or
|
||||
* a parameter. Every write of a local or a global stores a fresh array (an
|
||||
* array literal, or `new Array(n).fill(v)`); a parameter is never written.
|
||||
* Every read of a candidate is the receiver of an element read, write,
|
||||
* state test, `length`, `push` or `pop` (a discarded `pop` included), the
|
||||
* array of an element-reader helper call, or the argument of a direct call
|
||||
* whose parameter at that position is a candidate itself. A parameter is a
|
||||
* candidate only when its function is reached by direct calls alone and
|
||||
* every call passes a candidate there, so every binding that can hold one
|
||||
* of these arrays is a candidate: the array never reaches a runtime
|
||||
* function that reads its slots, a library call, a closure, a return or
|
||||
* untyped code.
|
||||
*
|
||||
* Candidates are only proposed here. The int32 slot analysis
|
||||
* (int32-slots.ts) keeps the ones whose every stored element is a proven
|
||||
@@ -33,6 +41,18 @@ import {
|
||||
* calls are inlined first (rewriteCandidates), so no shared helper ever
|
||||
* receives an int32 array. */
|
||||
|
||||
/** The candidate key of the binding `localId` as `fnName` reads it:
|
||||
* `function\0local` for a local or parameter, `\0global` for a module
|
||||
* global (`%g.` ids), which every function reads alike. */
|
||||
export function int32ArrayKey(fnName: string, localId: string): string {
|
||||
return localId.startsWith("%g.") ? `\0${localId}` : `${fnName}\0${localId}`;
|
||||
}
|
||||
|
||||
/** The int32 element slot (int32-slots.ts) of a candidate key. */
|
||||
export function int32ArraySlot(key: string): string {
|
||||
return key.startsWith("\0") ? `g:${key.slice(1)}` : `l:${key}`;
|
||||
}
|
||||
|
||||
/** Array methods a candidate may be the receiver of: the runtime paths
|
||||
* behind them read and write int32 slots through the double-valued slot
|
||||
* accessors. */
|
||||
@@ -202,10 +222,70 @@ function writesLocal(e: IrExpr, id: string): boolean {
|
||||
return !expr(e);
|
||||
}
|
||||
|
||||
/** The candidate locals of `fn` (see the module comment). */
|
||||
export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string>): Set<string> {
|
||||
/** Expression kinds an index can be built from without running any code. */
|
||||
const PURE_INDEX_KINDS: ReadonlySet<string> = new Set([
|
||||
"varRef",
|
||||
"numLit",
|
||||
"boolLit",
|
||||
"bin",
|
||||
"unary",
|
||||
"ternary",
|
||||
"logical",
|
||||
"arrayGet",
|
||||
"arrayState",
|
||||
]);
|
||||
|
||||
/** Whether evaluating an index runs no code (no call, accessor or
|
||||
* conversion): plain reads, literals and their operators only. */
|
||||
function pureIndex(e: IrExpr): boolean {
|
||||
return PURE_INDEX_KINDS.has(e.kind) && everyExprChild(e, pureIndex, () => false);
|
||||
}
|
||||
|
||||
/** The parameters and globals a module's functions share as candidates,
|
||||
* still assumed to qualify. */
|
||||
export interface SharedCandidates {
|
||||
/** Candidate parameters, as `function\0local` keys. */
|
||||
params: ReadonlySet<string>;
|
||||
/** Candidate globals, as `%g.` ids. */
|
||||
globals: ReadonlySet<string>;
|
||||
/** Whether the parameter at `index` of `callee` is a candidate. */
|
||||
paramAt: (callee: string, index: number) => boolean;
|
||||
}
|
||||
|
||||
const NO_SHARED: SharedCandidates = {
|
||||
params: new Set(),
|
||||
globals: new Set(),
|
||||
paramAt: () => false,
|
||||
};
|
||||
|
||||
/** The candidates of `fn` (see the module comment): its own locals, its
|
||||
* parameters in `shared.params`, and the globals of `shared.globals` that
|
||||
* it reads only in candidate positions. `written` receives the globals it
|
||||
* assigns fresh arrays. */
|
||||
export function int32ArrayCandidates(
|
||||
fn: IrFunction,
|
||||
readers: ReadonlySet<string>,
|
||||
shared: SharedCandidates = NO_SHARED,
|
||||
written: Set<string> = new Set(),
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
if (fn.async || fn.generator) {
|
||||
// Globals read here leave the candidates.
|
||||
for (const id of shared.globals) result.add(id);
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if ((e.kind === "varRef" || e.kind === "assignExpr") && result.has(e.localId))
|
||||
result.delete(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if ((s.kind === "varDecl" || s.kind === "assign") && result.has(s.localId))
|
||||
result.delete(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
const captured = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
@@ -217,11 +297,12 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
!local.boxed &&
|
||||
!local.tdz &&
|
||||
!captured.has(local.id) &&
|
||||
!params.has(local.id)
|
||||
(!params.has(local.id) || shared.params.has(`${fn.name}\0${local.id}`))
|
||||
)
|
||||
result.add(local.id);
|
||||
for (const id of shared.globals) result.add(id);
|
||||
if (result.size === 0) return result;
|
||||
const written = new Set<string>();
|
||||
const allocated = new Set<string>();
|
||||
// A receiver position: the candidate is read there without escaping.
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind !== "varRef") expr(e);
|
||||
@@ -255,13 +336,28 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
return true;
|
||||
}
|
||||
case "call": {
|
||||
if (!readers.has(e.callee) || e.args.length !== 2) break;
|
||||
const [array, index] = e.args;
|
||||
// Inlining evaluates the index before the array read.
|
||||
if (array!.kind === "varRef" && writesLocal(index!, array!.localId))
|
||||
result.delete(array!.localId);
|
||||
receiver(array!);
|
||||
expr(index!);
|
||||
if (readers.has(e.callee) && e.args.length === 2) {
|
||||
const [array, index] = e.args;
|
||||
// Inlining evaluates the index before the array read: an index
|
||||
// that rebinds the array keeps it out, and so does one that can run
|
||||
// code when the array is a module global (a call can reassign it).
|
||||
if (
|
||||
array!.kind === "varRef" &&
|
||||
(writesLocal(index!, array!.localId) ||
|
||||
(array!.localId.startsWith("%g.") && !pureIndex(index!)))
|
||||
)
|
||||
result.delete(array!.localId);
|
||||
receiver(array!);
|
||||
expr(index!);
|
||||
return true;
|
||||
}
|
||||
// An argument at a candidate parameter position passes the array
|
||||
// to another candidate binding (moduleInt32ArrayCandidates checks
|
||||
// every such call).
|
||||
e.args.forEach((arg, index) => {
|
||||
if (shared.paramAt(e.callee, index)) receiver(arg);
|
||||
else expr(arg);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +368,8 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (value && int32ArrayAllocation(value)) written.add(s.localId);
|
||||
if (params.has(s.localId)) result.delete(s.localId);
|
||||
else if (value && int32ArrayAllocation(value)) allocated.add(s.localId);
|
||||
else if (value) result.delete(s.localId);
|
||||
if (value) expr(value);
|
||||
return true;
|
||||
@@ -297,10 +394,228 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
for (const id of result) if (!written.has(id)) result.delete(id);
|
||||
for (const id of result) {
|
||||
if (shared.globals.has(id)) {
|
||||
if (allocated.has(id)) written.add(id);
|
||||
} else if (!params.has(id) && !allocated.has(id)) result.delete(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The members of `ids` (none when absent) that are in `live`. */
|
||||
function liveIn(ids: ReadonlySet<string> | undefined, live: ReadonlySet<string>): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (ids) for (const id of ids) if (live.has(id)) result.add(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The candidates of every function of `mod` that has any, only those
|
||||
* whose keys are in `keep` when given. Parameters and globals are shared:
|
||||
* starting from every one that may qualify, each pass drops the ones a
|
||||
* function reads outside candidate positions, the parameters some call
|
||||
* passes anything but a candidate, and the globals never assigned a fresh
|
||||
* array, until a pass drops nothing. Functions in `redirected` keep their
|
||||
* parameters: some of their calls reach another function. */
|
||||
export function moduleInt32ArrayCandidates(
|
||||
mod: IrModule,
|
||||
readers: ReadonlySet<string>,
|
||||
keep: ReadonlySet<string> | null = null,
|
||||
redirected: ReadonlySet<string> = new Set(),
|
||||
): Map<string, Set<string>> {
|
||||
const kept = (key: string): boolean => keep === null || keep.has(key);
|
||||
const arrayParams = new Map<string, number[]>();
|
||||
for (const fn of mod.functions) {
|
||||
// Class methods, constructors and runtime helpers (`%` names) keep
|
||||
// their parameters.
|
||||
if (fn.name.startsWith("%") || fn.async || fn.generator || redirected.has(fn.name)) continue;
|
||||
const indexes: number[] = [];
|
||||
fn.params.forEach((p, i) => {
|
||||
if (p.type.kind === "array" && p.type.elem.kind === "f64") indexes.push(i);
|
||||
});
|
||||
if (indexes.length > 0) arrayParams.set(fn.name, indexes);
|
||||
}
|
||||
let globals = new Set<string>();
|
||||
for (const g of mod.globals ?? [])
|
||||
if (
|
||||
g.type.kind === "array" &&
|
||||
g.type.elem.kind === "f64" &&
|
||||
g.initFlag === undefined &&
|
||||
kept(`\0${g.id}`)
|
||||
)
|
||||
globals.add(g.id);
|
||||
const result = new Map<string, Set<string>>();
|
||||
if (arrayParams.size === 0 && globals.size === 0) {
|
||||
for (const fn of mod.functions) {
|
||||
const candidates = int32ArrayCandidates(fn, readers);
|
||||
for (const id of candidates) if (!kept(`${fn.name}\0${id}`)) candidates.delete(id);
|
||||
if (candidates.size > 0) result.set(fn.name, candidates);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// One walk: the functions reached other than by direct calls, the direct
|
||||
// calls of functions with number[] parameters, the globals each function
|
||||
// reads or writes, and whether a module namespace object exists (its
|
||||
// member reads can reach any global by name).
|
||||
const reached = new Set(moduleFunctionReferences(mod));
|
||||
const calls: { caller: string; call: IrExpr & { kind: "call" } }[] = [];
|
||||
const globalsIn = new Map<string, Set<string>>();
|
||||
let namespaces = false;
|
||||
for (const fn of mod.functions) {
|
||||
const used = new Set<string>();
|
||||
const note = (id: string): void => {
|
||||
if (globals.has(id)) used.add(id);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
const name = exprFunctionReference(e);
|
||||
if (name !== undefined) reached.add(name);
|
||||
if (e.kind === "moduleNsRef") namespaces = true;
|
||||
if (e.kind === "call" && arrayParams.has(e.callee))
|
||||
calls.push({ caller: fn.name, call: e });
|
||||
if (e.kind === "varRef" || e.kind === "assignExpr") note(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") note(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (used.size > 0) globalsIn.set(fn.name, used);
|
||||
}
|
||||
if (namespaces) {
|
||||
globals = new Set();
|
||||
globalsIn.clear();
|
||||
}
|
||||
const paramIds = new Map<string, string[]>();
|
||||
let params = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
const indexes = arrayParams.get(fn.name);
|
||||
if (!indexes || reached.has(fn.name)) continue;
|
||||
paramIds.set(
|
||||
fn.name,
|
||||
fn.params.map((p) => p.localId),
|
||||
);
|
||||
for (const i of indexes) {
|
||||
const key = `${fn.name}\0${fn.params[i]!.localId}`;
|
||||
if (kept(key)) params.add(key);
|
||||
}
|
||||
}
|
||||
for (;;) {
|
||||
const current = params;
|
||||
const written = new Set<string>();
|
||||
const liveGlobals = new Set(globals);
|
||||
result.clear();
|
||||
for (const fn of mod.functions) {
|
||||
const shared: SharedCandidates = {
|
||||
params: current,
|
||||
globals: liveIn(globalsIn.get(fn.name), globals),
|
||||
paramAt: (callee, index) => {
|
||||
const id = paramIds.get(callee)?.[index];
|
||||
return id !== undefined && current.has(`${callee}\0${id}`);
|
||||
},
|
||||
};
|
||||
const candidates = int32ArrayCandidates(fn, readers, shared, written);
|
||||
for (const id of shared.globals) if (!candidates.has(id)) liveGlobals.delete(id);
|
||||
for (const id of candidates)
|
||||
if (!shared.globals.has(id) && !kept(`${fn.name}\0${id}`)) candidates.delete(id);
|
||||
if (candidates.size > 0) result.set(fn.name, candidates);
|
||||
}
|
||||
for (const id of liveGlobals) if (!written.has(id)) liveGlobals.delete(id);
|
||||
const live = new Set(
|
||||
[...current].filter((key) => {
|
||||
const cut = key.indexOf("\0");
|
||||
return result.get(key.slice(0, cut))?.has(key.slice(cut + 1)) === true;
|
||||
}),
|
||||
);
|
||||
// A parameter needs a candidate at that position of every call; a call
|
||||
// with fewer arguments passes undefined.
|
||||
for (const { caller, call } of calls) {
|
||||
const ids = paramIds.get(call.callee);
|
||||
if (!ids) continue;
|
||||
ids.forEach((id, i) => {
|
||||
const key = `${call.callee}\0${id}`;
|
||||
if (!live.has(key)) return;
|
||||
const arg = call.args[i];
|
||||
const ok =
|
||||
arg?.kind === "varRef" &&
|
||||
(arg.localId.startsWith("%g.")
|
||||
? liveGlobals.has(arg.localId)
|
||||
: result.get(caller)?.has(arg.localId) === true);
|
||||
if (!ok) live.delete(key);
|
||||
});
|
||||
}
|
||||
if (live.size === current.size && liveGlobals.size === globals.size) break;
|
||||
params = live;
|
||||
globals = liveGlobals;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The candidates one array can reach together: a direct call's argument
|
||||
* and the parameter it binds hold the same array, so their element facts
|
||||
* stand or fall together. `group` maps each of `keys` to its group's keys
|
||||
* (itself alone for most), and `globalReaders` each candidate global's
|
||||
* key to the functions that read or write it. */
|
||||
export function int32ArrayGroups(
|
||||
mod: IrModule,
|
||||
keys: ReadonlySet<string>,
|
||||
): { group: Map<string, string[]>; globalReaders: Map<string, Set<string>> } {
|
||||
const parent = new Map<string, string>();
|
||||
const root = (key: string): string => {
|
||||
let r = key;
|
||||
while (parent.has(r)) r = parent.get(r)!;
|
||||
if (r !== key) parent.set(key, r);
|
||||
return r;
|
||||
};
|
||||
const globalReaders = new Map<string, Set<string>>();
|
||||
const params = new Map(mod.functions.map((fn) => [fn.name, fn.params]));
|
||||
const paramKeys = new Set(
|
||||
mod.functions.flatMap((fn) => fn.params.map((p) => `${fn.name}\0${p.localId}`)),
|
||||
);
|
||||
if ([...keys].some((key) => key.startsWith("\0") || paramKeys.has(key))) {
|
||||
for (const fn of mod.functions) {
|
||||
const touch = (id: string): void => {
|
||||
const key = int32ArrayKey(fn.name, id);
|
||||
if (!key.startsWith("\0") || !keys.has(key)) return;
|
||||
const set = globalReaders.get(key) ?? new Set<string>();
|
||||
set.add(fn.name);
|
||||
globalReaders.set(key, set);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "varRef" || e.kind === "assignExpr") touch(e.localId);
|
||||
if (e.kind === "call")
|
||||
e.args.forEach((arg, i) => {
|
||||
const param = params.get(e.callee)?.[i];
|
||||
if (arg.kind !== "varRef" || !param) return;
|
||||
const a = int32ArrayKey(fn.name, arg.localId),
|
||||
p = `${e.callee}\0${param.localId}`;
|
||||
if (!keys.has(a) || !keys.has(p)) return;
|
||||
const ra = root(a),
|
||||
rp = root(p);
|
||||
if (ra !== rp) parent.set(ra, rp);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" || s.kind === "assign") touch(s.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const members = new Map<string, string[]>();
|
||||
for (const key of keys) {
|
||||
const r = root(key);
|
||||
const list = members.get(r);
|
||||
if (list) list.push(key);
|
||||
else members.set(r, [key]);
|
||||
}
|
||||
const group = new Map<string, string[]>();
|
||||
for (const key of keys) group.set(key, members.get(root(key))!);
|
||||
return { group, globalReaders };
|
||||
}
|
||||
|
||||
/** The element values stored into each candidate: element writes, pushed
|
||||
* values, literal elements and fill values. */
|
||||
export function int32ArrayStores(
|
||||
@@ -341,16 +656,34 @@ export function int32ArrayStores(
|
||||
return stores;
|
||||
}
|
||||
|
||||
const REPEATED_INDEX_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]);
|
||||
|
||||
/** An index the inlined read may evaluate at each of its uses: a plain read,
|
||||
* a literal, or integer arithmetic over them. Repeating it keeps the forms
|
||||
* the backend derives from its operands at every use, such as the exact i64
|
||||
* of an int32 product, where a bound local holds only the double. */
|
||||
function repeatableIndex(e: IrExpr): boolean {
|
||||
if (e.kind === "varRef" || e.kind === "numLit") return true;
|
||||
if (e.kind === "unary") return e.op === "-" && repeatableIndex(e.operand);
|
||||
return (
|
||||
e.kind === "bin" &&
|
||||
REPEATED_INDEX_OPS.has(e.op) &&
|
||||
repeatableIndex(e.left) &&
|
||||
repeatableIndex(e.right)
|
||||
);
|
||||
}
|
||||
|
||||
/** Rewrite a function's candidates for the backend. A discarded pop uses
|
||||
* the candidate in place of its hidden local. An element-reader helper call
|
||||
* whose array is a candidate is inlined: the helper's returned ternary, with
|
||||
* the candidate for its array and the index for its index, so the read means
|
||||
* the same. An index that is not a plain read or literal is bound to a fresh
|
||||
* local first: in its own declaration just before the statement when the
|
||||
* call is the whole value of a declaration or assignment (the form lowering
|
||||
* gives `a[i]!`, and the one the backend reads without a box), otherwise in a
|
||||
* sequence around the ternary. Selection keeps out indexes that write the
|
||||
* array, so evaluating the index before the array read is unobservable. */
|
||||
* the same. An index that is not repeatable (repeatableIndex) is bound to a
|
||||
* fresh local first: in its own declaration just before the statement when
|
||||
* the call is the whole value of a declaration or assignment (the form
|
||||
* lowering gives `a[i]!`, and the one the backend reads without a box),
|
||||
* otherwise in a sequence around the ternary. Selection keeps out indexes
|
||||
* that write the array, so evaluating the index before the array read is
|
||||
* unobservable. */
|
||||
function rewriteCandidates(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
@@ -374,14 +707,13 @@ function rewriteCandidates(
|
||||
candidates.has(e.args[0]!.localId)
|
||||
? e
|
||||
: null;
|
||||
const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit";
|
||||
// The statements of a list, with complex read indexes declared first.
|
||||
const hoist = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return [s];
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const call = readerCall(value);
|
||||
if (!call || simple(call.args[1]!)) return [s];
|
||||
if (!call || repeatableIndex(call.args[1]!)) return [s];
|
||||
const { decl, ref } = bind(call.args[1]!);
|
||||
const read: IrExpr = { ...call, args: [call.args[0]!, ref] };
|
||||
if (s.kind === "varDecl") return [decl, { ...s, init: read }];
|
||||
@@ -444,21 +776,23 @@ function rewriteCandidates(
|
||||
if (ret.kind !== "return" || !ret.value) return e;
|
||||
let index = call.args[1]!;
|
||||
const stmts: IrStmt[] = [];
|
||||
if (!simple(index)) {
|
||||
if (!repeatableIndex(index)) {
|
||||
const { decl, ref } = bind(index);
|
||||
stmts.push(decl);
|
||||
index = ref;
|
||||
}
|
||||
const operand = index;
|
||||
const body = transformExpr(reparseExpr(ret.value), {
|
||||
expr: (x) => {
|
||||
if (x.kind !== "varRef") return x;
|
||||
if (x.localId === arrayParam!.localId) return reparseExpr(array);
|
||||
if (x.localId === indexParam!.localId) return reparseExpr(operand);
|
||||
return x;
|
||||
},
|
||||
stmt: (x) => x,
|
||||
});
|
||||
// The arguments' copies are not searched again: an index may read a
|
||||
// local that has the id of one of the helper's parameters.
|
||||
const substituteExpr = (x: IrExpr): IrExpr => {
|
||||
if (x.kind !== "varRef") return mapExprChildren(x, substituteExpr, substituteStmt);
|
||||
if (x.localId === arrayParam!.localId) return reparseExpr(array);
|
||||
if (x.localId === indexParam!.localId) return reparseExpr(operand);
|
||||
return x;
|
||||
};
|
||||
const substituteStmt = (x: IrStmt): IrStmt =>
|
||||
mapStmtChildren(x, substituteExpr, substituteStmt);
|
||||
const body = substituteExpr(reparseExpr(ret.value));
|
||||
// The ternary is a fresh copy (reparseExpr): it takes the call's location.
|
||||
const typed = body;
|
||||
typed.loc = e.loc;
|
||||
@@ -471,40 +805,45 @@ function rewriteCandidates(
|
||||
return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) };
|
||||
}
|
||||
|
||||
/** Propose the module's int32 array candidates (`function\0local` keys in
|
||||
/** Propose the module's int32 array candidates (int32ArrayKey keys in
|
||||
* `int32ArrayCandidates`), only those in `keep` when given, and rewrite
|
||||
* their functions. */
|
||||
export function markInt32ArrayCandidates(
|
||||
mod: IrModule,
|
||||
keep: ReadonlySet<string> | null = null,
|
||||
redirected: ReadonlySet<string> = new Set(),
|
||||
): IrModule {
|
||||
if (mod.lib !== undefined) return mod;
|
||||
const readers = new Map<string, IrFunction>();
|
||||
for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn);
|
||||
const names = new Set(readers.keys());
|
||||
const keys: string[] = [];
|
||||
const all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected);
|
||||
if (all.size === 0) return mod;
|
||||
const keys = new Set<string>();
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const candidates = int32ArrayCandidates(fn, names);
|
||||
for (const id of candidates)
|
||||
if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`);
|
||||
else candidates.delete(id);
|
||||
return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers);
|
||||
const candidates = all.get(fn.name);
|
||||
if (!candidates) return fn;
|
||||
for (const id of candidates) keys.add(int32ArrayKey(fn.name, id));
|
||||
return rewriteCandidates(fn, candidates, readers);
|
||||
});
|
||||
return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys };
|
||||
return { ...mod, functions, int32ArrayCandidates: [...keys] };
|
||||
}
|
||||
|
||||
/** The module the backend emits: rewritten for exactly the candidates the
|
||||
* int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a
|
||||
* module without int32 arrays is `mod` itself. A dropped candidate's
|
||||
* rewrite is undone by marking again without it, until every candidate
|
||||
* marked is kept. */
|
||||
* marked is kept. `redirected` names the functions some of whose calls the
|
||||
* backend emits as calls of another function (a result-slot entry): their
|
||||
* parameters keep doubles. */
|
||||
export function selectInt32Arrays(
|
||||
mod: IrModule,
|
||||
prove: (marked: IrModule) => ReadonlySet<string>,
|
||||
redirected: (mod: IrModule) => ReadonlySet<string> = () => new Set(),
|
||||
): IrModule {
|
||||
let keep: ReadonlySet<string> | null = null;
|
||||
const others = redirected(mod);
|
||||
for (let round = 0; round < 4; round++) {
|
||||
const marked = markInt32ArrayCandidates(mod, keep);
|
||||
const marked = markInt32ArrayCandidates(mod, keep, others);
|
||||
if (marked === mod) return mod;
|
||||
const kept = prove(marked);
|
||||
const proposed = marked.int32ArrayCandidates ?? [];
|
||||
|
||||
@@ -20,7 +20,13 @@ import {
|
||||
type IrNodeKind,
|
||||
} from "./node-index.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
|
||||
import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js";
|
||||
import {
|
||||
checkableInt32Allocations,
|
||||
int32ArrayGroups,
|
||||
int32ArrayKey,
|
||||
int32ArraySlot,
|
||||
int32ArrayStores,
|
||||
} from "./int32-arrays.js";
|
||||
import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js";
|
||||
|
||||
/** The storage slot an array-valued expression reads, as an
|
||||
@@ -257,9 +263,10 @@ export class Int32Slots {
|
||||
/** Whether the module has int32 array candidates (the narrowedReads
|
||||
* range fact). */
|
||||
narrowedReads = false;
|
||||
/** Local number arrays (`function\0local`) stored as int32 slots: the
|
||||
* module's int32 array candidates whose every stored element the
|
||||
* fixpoint proved an int32. */
|
||||
/** Number arrays stored as int32 slots (int32ArrayKey keys: locals and
|
||||
* parameters as `function\0local`, globals as `\0global`): the module's
|
||||
* int32 array candidates whose every stored element, through every
|
||||
* binding that can hold the same array, the fixpoint proved an int32. */
|
||||
readonly int32Arrays = new Set<string>();
|
||||
/** Local number arrays that may hold int32 slots with checked stores: the
|
||||
* candidates whose element stores the fixpoint proved exact integers but
|
||||
@@ -849,17 +856,29 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key);
|
||||
for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key);
|
||||
// Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32
|
||||
// elements until a store the fixpoint cannot prove drops one.
|
||||
// elements until a store the fixpoint cannot prove drops one, with every
|
||||
// candidate the same array can reach (its group).
|
||||
const arrayCandidates = new Map<string, Set<string>>();
|
||||
slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0;
|
||||
for (const key of mod.int32ArrayCandidates ?? []) {
|
||||
const globalArrays = new Set<string>();
|
||||
const arrayKeys = new Set(mod.int32ArrayCandidates ?? []);
|
||||
slots.narrowedReads = arrayKeys.size > 0;
|
||||
for (const key of arrayKeys) {
|
||||
const cut = key.indexOf("\0");
|
||||
const fnName = key.slice(0, cut);
|
||||
const set = arrayCandidates.get(fnName) ?? new Set<string>();
|
||||
set.add(key.slice(cut + 1));
|
||||
arrayCandidates.set(fnName, set);
|
||||
slots.addInt32Elements(`l:${key}`);
|
||||
if (cut === 0) globalArrays.add(key.slice(1));
|
||||
else {
|
||||
const fnName = key.slice(0, cut);
|
||||
const set = arrayCandidates.get(fnName) ?? new Set<string>();
|
||||
set.add(key.slice(cut + 1));
|
||||
arrayCandidates.set(fnName, set);
|
||||
}
|
||||
slots.addInt32Elements(int32ArraySlot(key));
|
||||
}
|
||||
const arrayGroups = arrayKeys.size > 0 ? int32ArrayGroups(mod, arrayKeys) : null;
|
||||
// Functions that read or write a candidate global.
|
||||
const globalArrayFunctions = new Set<string>();
|
||||
if (arrayGroups)
|
||||
for (const names of arrayGroups.globalReaders.values())
|
||||
for (const name of names) globalArrayFunctions.add(name);
|
||||
|
||||
/* ── candidate fields ── */
|
||||
const children = new Map<string, string[]>();
|
||||
@@ -1141,6 +1160,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
const candidateWrites = nodes?.scan(CANDIDATE_WRITE_KINDS);
|
||||
const hasCandidateWrite = (fn: IrFunction): boolean => {
|
||||
if (arrayCandidates.has(fn.name)) return true;
|
||||
if (globalArrayFunctions.has(fn.name)) return true;
|
||||
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
|
||||
const returns = slots.hasReturn(fn.name);
|
||||
let found = false;
|
||||
@@ -1211,25 +1231,49 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
// (checkableInt32Allocations), it keeps int32 slots with checked stores
|
||||
// (checkedInt32Arrays), which widen it to doubles in place at the first
|
||||
// store that is not an int32.
|
||||
const arrays = arrayCandidates.get(fn.name);
|
||||
if (arrays) {
|
||||
let dropped = false;
|
||||
const locals = arrayCandidates.get(fn.name);
|
||||
let arrays = locals;
|
||||
if (globalArrayFunctions.has(fn.name)) {
|
||||
const both = new Set<string>();
|
||||
if (locals) for (const id of locals) both.add(id);
|
||||
for (const id of globalArrays) both.add(id);
|
||||
arrays = both;
|
||||
}
|
||||
if (arrays && arrayGroups) {
|
||||
const requeue = (name: string): void => {
|
||||
if (queued.has(name)) return;
|
||||
queued.add(name);
|
||||
queue.push(name);
|
||||
};
|
||||
const params = new Set(fn.params.map((param) => param.localId));
|
||||
for (const [id, values] of int32ArrayStores(fn, arrays)) {
|
||||
const key = `l:${fn.name}\0${id}`;
|
||||
if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue;
|
||||
slots.dropInt32Elements(key);
|
||||
const key = int32ArrayKey(fn.name, id);
|
||||
if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue;
|
||||
// The array's group drops together: every function reading a
|
||||
// member is analyzed again without its facts.
|
||||
const group = arrayGroups.group.get(key) ?? [key];
|
||||
for (const member of group) {
|
||||
slots.dropInt32Elements(int32ArraySlot(member));
|
||||
if (member.startsWith("\0")) {
|
||||
const names = arrayGroups.globalReaders.get(member);
|
||||
if (names) for (const name of names) requeue(name);
|
||||
} else requeue(member.slice(0, member.indexOf("\0")));
|
||||
}
|
||||
// Checked stores serve a lone local: its allocations are the only
|
||||
// arrays it holds.
|
||||
const checkable = (value: IrExpr): boolean => {
|
||||
const range = ranges.get(value);
|
||||
return int32(value) || range != null;
|
||||
};
|
||||
if (values.every(checkable) && checkableInt32Allocations(fn, id))
|
||||
slots.checkedInt32Arrays.add(`${fn.name}\0${id}`);
|
||||
else reason(`array:${fn.name}\0${id}`, "an element store is not int32");
|
||||
dropped = true;
|
||||
}
|
||||
if (dropped && !queued.has(fn.name)) {
|
||||
queued.add(fn.name);
|
||||
queue.push(fn.name);
|
||||
if (
|
||||
group.length === 1 &&
|
||||
!key.startsWith("\0") &&
|
||||
!params.has(id) &&
|
||||
values.every(checkable) &&
|
||||
checkableInt32Allocations(fn, id)
|
||||
)
|
||||
slots.checkedInt32Arrays.add(key);
|
||||
else for (const member of group) reason(`array:${member}`, "an element store is not int32");
|
||||
}
|
||||
}
|
||||
// An int32 written into a slot still assumed non-negative: widen the
|
||||
@@ -1300,10 +1344,8 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
}
|
||||
for (const proven of provenByFunction.values())
|
||||
for (const value of proven) slots.provenWrites.add(value);
|
||||
for (const [fnName, ids] of arrayCandidates)
|
||||
for (const id of ids)
|
||||
if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`))
|
||||
slots.int32Arrays.add(`${fnName}\0${id}`);
|
||||
for (const key of arrayKeys)
|
||||
if (slots.hasInt32ElementSlot(int32ArraySlot(key))) slots.int32Arrays.add(key);
|
||||
const ranged = rangedParameters(
|
||||
slots,
|
||||
functions,
|
||||
|
||||
@@ -440,6 +440,37 @@ test("int32-closed locals keep the int32 range across loops and switches", () =>
|
||||
expect(switched.get(afterSwitch)).toEqual(INT32);
|
||||
});
|
||||
|
||||
test("loop-carried locals written from Int32[] element reads keep the int32 range", () => {
|
||||
const arrType = { kind: "array", elem: F64 } as const;
|
||||
const loop = (localId: string): { after: IrExpr; f: IrFunction } => {
|
||||
const after = ref();
|
||||
// `x = a[x]!` in a loop: a cursor that follows indices stored in the array.
|
||||
const read: IrExpr = {
|
||||
loc,
|
||||
kind: "arrayGet",
|
||||
arr: { loc, kind: "varRef", localId, type: arrType },
|
||||
index: ref(),
|
||||
type: F64,
|
||||
};
|
||||
const f = fn([
|
||||
assign(num(0)),
|
||||
{
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("<", ref("unknown"), num(1)),
|
||||
body: [assign(read)],
|
||||
},
|
||||
observe(after),
|
||||
]);
|
||||
return { after, f };
|
||||
};
|
||||
const elements = { elements: (arr: IrExpr) => arr.kind === "varRef" && arr.localId === "ints" };
|
||||
const ints = loop("ints");
|
||||
expect(analyzeIntegerRanges(ints.f, elements).get(ints.after)).toEqual(INT32);
|
||||
const floats = loop("floats");
|
||||
expect(analyzeIntegerRanges(floats.f, elements).get(floats.after) ?? null).toBeNull();
|
||||
});
|
||||
|
||||
test("uint32-closed locals keep the uint32 range across loop back edges", () => {
|
||||
// for (let i = 0; i < n; i++) { v = unknown >>> 0; acc = (acc + v) >>> 0; ... }
|
||||
const loop = (start: IrExpr | null, writes: IrExpr[]) => {
|
||||
|
||||
@@ -480,6 +480,7 @@ export function analyzeIntegerRangeFacts(
|
||||
return boxedRange(e.localId);
|
||||
}
|
||||
case "fieldGet":
|
||||
case "arrayGet":
|
||||
case "call":
|
||||
return slotRange(e);
|
||||
default:
|
||||
|
||||
@@ -126,140 +126,6 @@ test("property helpers whose class branches only test capsules are cache sites",
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
});
|
||||
|
||||
test("bags converted between dyn and jsval are still the receiver", () => {
|
||||
// A program that declares a class returns its bags as jsvals, and the
|
||||
// helpers read them through js.toDyn.
|
||||
const toDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.toDyn",
|
||||
args: [value],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const fromDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [value],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
});
|
||||
const jsvalBag = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.js",
|
||||
["v"],
|
||||
[capsuleBranch("v"), ret(fromDyn(ref("v")))],
|
||||
);
|
||||
const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]);
|
||||
const getter = getHelper(
|
||||
"%dyn.class.property.0",
|
||||
"x",
|
||||
[capsuleBranch("p.0")],
|
||||
call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))),
|
||||
);
|
||||
const setter = setHelper("%dyn.class.property.1", "y");
|
||||
const write = setter.body[0] as Extract<IrStmt, { kind: "exprStmt" }>;
|
||||
const keySet = write.expr as Extract<IrExpr, { kind: "libCall" }>;
|
||||
keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0")));
|
||||
// A converted value that is not the receiver stays a helper call.
|
||||
const foreign = getHelper(
|
||||
"%dyn.class.property.2",
|
||||
"x",
|
||||
[],
|
||||
toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)),
|
||||
);
|
||||
const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
expect(sites.site("%dyn.class.property.2")).toBeNull();
|
||||
});
|
||||
|
||||
test("a bag helper may first rebind its parameter to a bag of it", () => {
|
||||
// Programs with subclasses normalize the value before the class branches.
|
||||
const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]);
|
||||
const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc });
|
||||
const normalized = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.normalized",
|
||||
["v"],
|
||||
[rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))],
|
||||
);
|
||||
const replaced = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty.replaced",
|
||||
["v"],
|
||||
[rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))],
|
||||
);
|
||||
const sites = sitesOf(
|
||||
normalize,
|
||||
normalized,
|
||||
replaced,
|
||||
getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))),
|
||||
getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))),
|
||||
// The property helper itself normalizes its receiver first, too.
|
||||
getHelper("%dyn.class.property.2", "x", [
|
||||
{ kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc },
|
||||
capsuleBranch("p.0"),
|
||||
]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" });
|
||||
});
|
||||
|
||||
test("keyed helpers may test capsules between the key's coercion and the read", () => {
|
||||
const read = (object: IrExpr, key: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
});
|
||||
const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc },
|
||||
then: [ret(read(object, "p.key"))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const keyed = (name: string, between: IrStmt[]): IrFunction =>
|
||||
helper(
|
||||
name,
|
||||
["p.0", "p.key"],
|
||||
[
|
||||
capsuleBranch("p.0"),
|
||||
guard("symbol", "p.key", call(identityBag.name, ref("p.0"))),
|
||||
guard("nullish", "p.0", ref("p.0")),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "key.string",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [ref("p.key")],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
...between,
|
||||
ret(read(call(identityBag.name, ref("p.0")), "key.string")),
|
||||
],
|
||||
["key.string"],
|
||||
);
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc },
|
||||
then: [ret(ref("p.0"))],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
const sites = sitesOf(
|
||||
keyed("%dyn.class.computed.0", []),
|
||||
keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]),
|
||||
keyed("%dyn.class.computed.2", [errorBranch]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" });
|
||||
expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" });
|
||||
expect(sites.site("%dyn.class.computed.2")).toBeNull();
|
||||
});
|
||||
|
||||
test("any other prefix or tail keeps the helper call", () => {
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
|
||||
@@ -62,11 +62,7 @@ export class JsIcSites {
|
||||
const receiver = fn.params[0]?.localId;
|
||||
if (receiver === undefined) return null;
|
||||
let i = 0;
|
||||
while (
|
||||
i < fn.body.length &&
|
||||
(this.capsuleBranch(fn.body[i]!, receiver) || this.rebinding(fn.body[i]!, receiver))
|
||||
)
|
||||
i++;
|
||||
while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++;
|
||||
const tail = fn.body.slice(i);
|
||||
if (fn.params.length === 1) return this.getTail(tail, receiver);
|
||||
if (fn.params.length !== 2) return null;
|
||||
@@ -113,14 +109,10 @@ export class JsIcSites {
|
||||
/** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key);
|
||||
* if (receiver == null) return dynKeyGet(receiver, key);
|
||||
* const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read
|
||||
* possibly under js.fromDyn. Capsule branches may precede the final read:
|
||||
* a program that declares classes looks the string key up in each class's
|
||||
* key table there. */
|
||||
* possibly under js.fromDyn. */
|
||||
private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null {
|
||||
const [symbol, nullish, decl] = tail;
|
||||
const ret = tail[tail.length - 1];
|
||||
if (tail.length < 4) return null;
|
||||
if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null;
|
||||
const [symbol, nullish, decl, ret] = tail;
|
||||
if (tail.length !== 4) return null;
|
||||
if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null;
|
||||
if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key))
|
||||
return null;
|
||||
@@ -167,12 +159,9 @@ export class JsIcSites {
|
||||
}
|
||||
|
||||
/** Whether `e` is the receiver itself for every ordinary object or array:
|
||||
* the receiver, or a chain of property-bag helpers applied to it. A
|
||||
* conversion between the dyn and jsval forms keeps the object it holds. */
|
||||
* the receiver, or a chain of property-bag helpers applied to it. */
|
||||
private bagOf(e: IrExpr, receiver: string): boolean {
|
||||
if (isVar(e, receiver)) return true;
|
||||
if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn"))
|
||||
return e.args.length === 1 && this.bagOf(e.args[0]!, receiver);
|
||||
return (
|
||||
e.kind === "call" &&
|
||||
e.args.length === 1 &&
|
||||
@@ -193,13 +182,6 @@ export class JsIcSites {
|
||||
return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param);
|
||||
}
|
||||
|
||||
/** `param = <bag of param>`, such as the subclass normalization
|
||||
* `value = %dyn.class.normalize(value)`, which leaves an ordinary object
|
||||
* or array in `param`. */
|
||||
private rebinding(s: IrStmt, param: string): boolean {
|
||||
return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param);
|
||||
}
|
||||
|
||||
/** A property-bag helper that returns its argument unless a capsule
|
||||
* branch is taken. */
|
||||
private identityHelper(name: string): boolean {
|
||||
@@ -222,10 +204,7 @@ export class JsIcSites {
|
||||
identity =
|
||||
fn.body
|
||||
.slice(0, -1)
|
||||
.every(
|
||||
(s) =>
|
||||
this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param),
|
||||
) &&
|
||||
.every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) &&
|
||||
last?.kind === "return" &&
|
||||
last.value !== null &&
|
||||
this.bagOf(last.value, param);
|
||||
|
||||
@@ -77,6 +77,19 @@ function literalCounter(n: number): number {
|
||||
for (let i = 0; i < n; i++) a[0] = a[0]! + 1;
|
||||
return a[0]!;
|
||||
}
|
||||
function walk(n: number, steps: number): number {
|
||||
const next: number[] = [];
|
||||
for (let i = 0; i < n; i++) next.push(i + 1 < n ? i + 1 : 0);
|
||||
let at = next[0]!;
|
||||
let total = 0;
|
||||
for (let s = 0; s < steps; s++) {
|
||||
const t = next[at]!;
|
||||
next[at] = at;
|
||||
at = t;
|
||||
total = (total + at) | 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
function joined(n: number): string {
|
||||
const a = [n | 0, 2];
|
||||
return a.join(",");
|
||||
@@ -91,6 +104,7 @@ console.log(
|
||||
shortCounter(),
|
||||
literalCounter(n),
|
||||
joined(n),
|
||||
walk(n | 0, 5),
|
||||
);
|
||||
`;
|
||||
|
||||
@@ -119,6 +133,16 @@ test("arrays storing only int32 elements get 4-byte slots", async () => {
|
||||
expect(counts).not.toContain("popArray");
|
||||
});
|
||||
|
||||
test("an array that stores a cursor read back from its own elements gets 4-byte slots", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const walk = functions(llvm, "walk");
|
||||
expect(walk).not.toBe("");
|
||||
// `at` is assigned `next[at]!` in the loop and stored back into `next`:
|
||||
// it holds an int32 whenever the array's elements are int32s.
|
||||
expect(walk).toMatch(/@scr_arr_new_small\(i32 7\)/);
|
||||
expect(walk).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/);
|
||||
});
|
||||
|
||||
test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
for (const name of ["fraction", "negativeZero", "joined"]) {
|
||||
@@ -196,3 +220,42 @@ test("only the candidates the proof keeps are rewritten", async () => {
|
||||
for (const fn of selected.functions)
|
||||
if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name));
|
||||
});
|
||||
|
||||
const sharedSource = `
|
||||
const data: number[] = [];
|
||||
for (let i = 0; i < 64; i++) data.push((i * 7) & 255);
|
||||
function fnv(values: number[]): number {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < values.length; i++) h = Math.imul(h ^ values[i]!, 16777619) >>> 0;
|
||||
return h;
|
||||
}
|
||||
function mean(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s += values[i]!;
|
||||
return s / values.length;
|
||||
}
|
||||
function first(values: number[]): number {
|
||||
return values[0]!;
|
||||
}
|
||||
const ints = [1, 2, 3];
|
||||
const halves = [0.5, 1.5];
|
||||
const pick: (values: number[]) => number = first;
|
||||
console.log(fnv(data), mean(ints), mean(halves), pick([4]));
|
||||
`;
|
||||
|
||||
test("arrays passed to helpers and held in module globals get 4-byte slots", async () => {
|
||||
const mod = await lowerIr(sharedSource);
|
||||
const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? [];
|
||||
// A global and the parameter every call passes it to; a function used as
|
||||
// a value keeps its parameter.
|
||||
expect(proposed).toEqual(expect.arrayContaining(["\0%g.e.data", "fnv\0values.0"]));
|
||||
expect(proposed.some((key) => key.startsWith("first\0"))).toBe(false);
|
||||
const llvm = await emitLlvm(sharedSource);
|
||||
// The helper reads int32 slots; `mean`, also called with fractions,
|
||||
// reads doubles.
|
||||
expect(functions(llvm, "fnv")).toMatch(
|
||||
/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/,
|
||||
);
|
||||
expect(functions(llvm, "mean")).not.toMatch(/getelementptr inbounds i32, ptr/);
|
||||
expect(llvm).toMatch(/@scr_arr_new(_small)?\(i32 7/);
|
||||
});
|
||||
|
||||
@@ -101,6 +101,51 @@ delete loose.label;
|
||||
expect(layout(deleted, "Shape").tracks).toBe(true);
|
||||
});
|
||||
|
||||
test("freezing values other than class instances keeps JavaScript class layouts fixed", async () => {
|
||||
const module = await lower(`${SHAPES}
|
||||
/** @param {{ value: any }} data */
|
||||
function settle(data) {
|
||||
data.value = Object.freeze(data.value);
|
||||
Object.seal(data);
|
||||
return Object.preventExtensions(data.value);
|
||||
}
|
||||
settle({ value: { sides: 1 } });
|
||||
const square = new Square();
|
||||
Object.freeze(square);
|
||||
`);
|
||||
expect(layout(module, "Shape").tracks).toBe(false);
|
||||
expect(layout(module, "Square").tracks).toBe(false);
|
||||
});
|
||||
|
||||
test("accessors that share no field's name keep JavaScript class layouts fixed", async () => {
|
||||
const module = await lower(`
|
||||
class Path {
|
||||
constructor(key) {
|
||||
this.cache = [];
|
||||
this.key = key;
|
||||
}
|
||||
get path() {
|
||||
if (!this.cache.length) this.cache.push(this.key);
|
||||
return this.cache;
|
||||
}
|
||||
}
|
||||
class Named extends Path {
|
||||
constructor(key) {
|
||||
super(key);
|
||||
this.label = "named";
|
||||
}
|
||||
/** @param {string} value */
|
||||
set label2(value) {
|
||||
this.label = value;
|
||||
}
|
||||
}
|
||||
console.log(new Named("k").path.length);
|
||||
`);
|
||||
expect(layout(module, "Path")).toEqual({ fields: "cache:dyn key:dyn", tracks: false });
|
||||
expect(layout(module, "Named").tracks).toBe(false);
|
||||
expect(layout(module, "Named").fields).toBe("cache:dyn key:dyn label:string");
|
||||
});
|
||||
|
||||
test("JavaScript subclass fields keep their types when the base constructor only assigns fields", async () => {
|
||||
const module = await lower(`
|
||||
class Base {
|
||||
|
||||
@@ -64,50 +64,6 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n);
|
||||
}
|
||||
});
|
||||
|
||||
test("property sites keep their caches in programs that declare classes and subclasses", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-"));
|
||||
try {
|
||||
const entry = join(dir, "main.js");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
// Declared classes give every property helper class branches and
|
||||
// property-bag lookups, and a subclass adds a normalization step; plain
|
||||
// objects still read through the caches.
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
class Point {
|
||||
constructor(x) { this.x = x; }
|
||||
}
|
||||
class Tagged extends Point {
|
||||
constructor(x, tag) { super(x); this.tag = tag; }
|
||||
}
|
||||
function dyn(v) { return v; }
|
||||
function read(o) { return o.x; }
|
||||
function write(o, v) { o.x = v; }
|
||||
function at(a, i) { return a[i]; }
|
||||
var o = dyn({ x: 1 });
|
||||
var p = dyn(new Point(4));
|
||||
var t = dyn(new Tagged(6, "t"));
|
||||
write(o, 3);
|
||||
write(p, 5);
|
||||
write(t, 7);
|
||||
console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag"));
|
||||
`,
|
||||
);
|
||||
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"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
expect(body(llvm, "read")).toContain("@scr_js_load_probe(");
|
||||
expect(body(llvm, "write")).toContain("@scr_js_store_probe(");
|
||||
expect(body(llvm, "at")).toContain("@scr_js_keyed_probe(");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("typed programs have no cache sites and keep the ordinary runtime", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-"));
|
||||
try {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7264-array-dense-paired-loops.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/util.ts",
|
||||
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/core.ts",
|
||||
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts",
|
||||
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts",
|
||||
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7382-cycle-static-this-tdz/beta.ts",
|
||||
"<repo>/tests/corpus/7382-cycle-static-this-tdz/alpha.ts",
|
||||
"<repo>/tests/corpus/7382-cycle-static-this-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts",
|
||||
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts",
|
||||
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7390-record-key-read-presence.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7543-var-list-declarator-order.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7545-string-regex-checked-arguments.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs",
|
||||
"<repo>/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs",
|
||||
"<repo>/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs",
|
||||
"<repo>/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7553-var-for-in-of-zero-iterations.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7580-int32-arrays-across-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7790-js-class-integrity-layouts.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7791-js-class-accessor-layouts.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-brand-probe-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-headers-copy-cache.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/string-last-index-of-ascii.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -540,6 +540,10 @@ _Static_assert(offsetof(ScrDyn, null_proto) == sizeof(size_t) + sizeof(ScrDynKin
|
||||
"dyn-to-record conversions read null_proto at byte 13 (9 on wasm32)");
|
||||
_Static_assert(offsetof(ScrDyn, v.typed_ref.type_key) == 16 + 3 * sizeof(void *),
|
||||
"typed_ref.type_key offset");
|
||||
/* Emitted brand probes read type_key_len as the low 27 bits of the 32-bit
|
||||
* word after `casts` (native 88, wasm32 52). */
|
||||
_Static_assert(offsetof(ScrDyn, v.typed_ref.casts) == 16 + 8 * sizeof(void *),
|
||||
"typed_ref.casts offset");
|
||||
/* The region links ride in payload space the capsule variant already
|
||||
* reserves, so dyn nodes keep their size (and allocation size class). */
|
||||
_Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref) &&
|
||||
|
||||
+163
-124
@@ -172,7 +172,14 @@ struct SfStream {
|
||||
struct SfHeaders {
|
||||
size_t rc;
|
||||
ScrArr *pairs;
|
||||
/* Fetch's "sort and combine" list of `pairs` as flat name/value pairs,
|
||||
* built on the first iteration step and dropped by every mutation. */
|
||||
ScrArr *sorted;
|
||||
bool immutable;
|
||||
/* `pairs` is in the form sf_add_converted_headers builds: lowercase valid
|
||||
* names, trimmed valid UTF-8 values, one combined pair per name except
|
||||
* set-cookie. Headers built from wire lines are not (duplicates stay). */
|
||||
bool canonical;
|
||||
};
|
||||
|
||||
struct SfResponse {
|
||||
@@ -2164,6 +2171,24 @@ static ScrDyn *sf_controller_get(void *ptr, const char *key, size_t len) {
|
||||
|
||||
static bool sf_header_name_ok(const char *s, size_t len);
|
||||
|
||||
/* A borrowed element of a runtime-built header pair array. These arrays are
|
||||
* only ever appended to or rewritten whole, so they are dense. */
|
||||
static inline ScrStr *sf_pair_at(ScrArr *pairs, size_t i) {
|
||||
if (i < pairs->filled) return (ScrStr *)(uintptr_t)scr_arr_dense_get(pairs, i);
|
||||
return (ScrStr *)scr_arr_peek_ref(pairs, (double)i);
|
||||
}
|
||||
|
||||
static ScrStr *sf_header_name_lower(const char *data, size_t len);
|
||||
|
||||
/* `name` in lowercase, +1: the same string when it has no uppercase byte. */
|
||||
static ScrStr *sf_header_name_lower_str(ScrStr *name) {
|
||||
for (size_t i = 0; i < name->len; i++) {
|
||||
char c = name->data[i];
|
||||
if (c >= 'A' && c <= 'Z') return sf_header_name_lower(name->data, name->len);
|
||||
}
|
||||
return scr_str_retain(name);
|
||||
}
|
||||
|
||||
static ScrStr *sf_header_name_lower(const char *data, size_t len) {
|
||||
char *lower = malloc(len ? len : 1);
|
||||
if (!lower) sf_oom();
|
||||
@@ -2232,13 +2257,8 @@ static SfHeaders *sf_headers_new_response(ScrArr *raw_pairs) {
|
||||
size_t n = (size_t)scr_arr_len(raw_pairs);
|
||||
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *raw_name = scr_arr_get_ref(raw_pairs, (double)i);
|
||||
scr_arr_push_ref(
|
||||
pairs, sf_header_name_lower(raw_name->data, raw_name->len));
|
||||
scr_str_release(raw_name);
|
||||
ScrStr *raw = scr_arr_get_ref(raw_pairs, (double)(i + 1));
|
||||
scr_arr_push_ref(pairs, sf_latin1_to_utf8(raw));
|
||||
scr_str_release(raw);
|
||||
scr_arr_push_ref(pairs, sf_header_name_lower_str(sf_pair_at(raw_pairs, i)));
|
||||
scr_arr_push_ref(pairs, sf_latin1_to_utf8(sf_pair_at(raw_pairs, i + 1)));
|
||||
}
|
||||
scr_arr_release(raw_pairs);
|
||||
return sf_headers_new(pairs, true);
|
||||
@@ -2252,9 +2272,17 @@ static SfHeaders *sf_headers_retain(SfHeaders *h) {
|
||||
static void sf_headers_release(SfHeaders *h) {
|
||||
if (!h || --h->rc > 0) return;
|
||||
scr_arr_release(h->pairs);
|
||||
if (h->sorted) scr_arr_release(h->sorted);
|
||||
free(h);
|
||||
}
|
||||
|
||||
/* Every change to h->pairs goes through here first. */
|
||||
static void sf_headers_changed(SfHeaders *h) {
|
||||
if (!h->sorted) return;
|
||||
scr_arr_release(h->sorted);
|
||||
h->sorted = NULL;
|
||||
}
|
||||
|
||||
static void *sf_headers_retain_v(void *p) {
|
||||
return sf_headers_retain((SfHeaders *)p);
|
||||
}
|
||||
@@ -2294,7 +2322,7 @@ static ScrStr *sf_headers_name(const ScrDyn *value, const char *method) {
|
||||
scr_str_release(raw);
|
||||
return NULL;
|
||||
}
|
||||
ScrStr *out = sf_header_name_lower(raw->data, raw->len);
|
||||
ScrStr *out = sf_header_name_lower_str(raw);
|
||||
scr_str_release(raw);
|
||||
return out;
|
||||
}
|
||||
@@ -2305,40 +2333,36 @@ static bool sf_headers_key_eq(const ScrStr *left, const ScrStr *right) {
|
||||
}
|
||||
|
||||
static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) {
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
ScrArr *pairs = h->pairs;
|
||||
size_t n = (size_t)scr_arr_len(pairs);
|
||||
size_t count = 0;
|
||||
size_t total = 0;
|
||||
ScrStr *first = NULL;
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
||||
if (sf_headers_key_eq(key, name)) {
|
||||
if (count > 0) total += 2;
|
||||
total += value->len;
|
||||
count++;
|
||||
}
|
||||
scr_str_release(key);
|
||||
scr_str_release(value);
|
||||
if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue;
|
||||
ScrStr *value = sf_pair_at(pairs, i + 1);
|
||||
if (count > 0) total += 2;
|
||||
else first = value;
|
||||
total += value->len;
|
||||
count++;
|
||||
}
|
||||
if (count == 0) return NULL;
|
||||
if (count == 1) return scr_str_retain(first);
|
||||
|
||||
char *joined = malloc(total > 0 ? total : 1);
|
||||
if (!joined) sf_oom();
|
||||
size_t at = 0;
|
||||
size_t emitted = 0;
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
||||
if (sf_headers_key_eq(key, name)) {
|
||||
if (emitted > 0) {
|
||||
joined[at++] = ',';
|
||||
joined[at++] = ' ';
|
||||
}
|
||||
memcpy(joined + at, value->data, value->len);
|
||||
at += value->len;
|
||||
emitted++;
|
||||
if (!sf_headers_key_eq(sf_pair_at(pairs, i), name)) continue;
|
||||
ScrStr *value = sf_pair_at(pairs, i + 1);
|
||||
if (emitted > 0) {
|
||||
joined[at++] = ',';
|
||||
joined[at++] = ' ';
|
||||
}
|
||||
scr_str_release(key);
|
||||
scr_str_release(value);
|
||||
memcpy(joined + at, value->data, value->len);
|
||||
at += value->len;
|
||||
emitted++;
|
||||
}
|
||||
ScrStr *out = scr_str_new(joined, total);
|
||||
free(joined);
|
||||
@@ -2348,17 +2372,17 @@ static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) {
|
||||
static void sf_headers_delete_name(SfHeaders *h, const ScrStr *name) {
|
||||
ScrArr *old = h->pairs;
|
||||
size_t n = (size_t)scr_arr_len(old);
|
||||
bool present = false;
|
||||
for (size_t i = 0; i + 1 < n && !present; i += 2)
|
||||
present = sf_headers_key_eq(sf_pair_at(old, i), name);
|
||||
if (!present) return;
|
||||
sf_headers_changed(h);
|
||||
ScrArr *next = scr_arr_new(SCR_ELEM_STR, n);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(old, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(old, (double)(i + 1));
|
||||
if (sf_headers_key_eq(key, name)) {
|
||||
scr_str_release(key);
|
||||
scr_str_release(value);
|
||||
continue;
|
||||
}
|
||||
scr_arr_push_ref(next, key);
|
||||
scr_arr_push_ref(next, value);
|
||||
ScrStr *key = sf_pair_at(old, i);
|
||||
if (sf_headers_key_eq(key, name)) continue;
|
||||
scr_arr_push_ref(next, scr_str_retain(key));
|
||||
scr_arr_push_ref(next, scr_str_retain(sf_pair_at(old, i + 1)));
|
||||
}
|
||||
h->pairs = next;
|
||||
scr_arr_release(old);
|
||||
@@ -2399,15 +2423,13 @@ static bool sf_headers_convert_pair(
|
||||
* Fetch-specific semicolon separator for Cookie. */
|
||||
static void sf_headers_append_pair(
|
||||
SfHeaders *h, ScrStr *name, ScrStr *value) {
|
||||
sf_headers_changed(h);
|
||||
if (!(name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0)) {
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
||||
bool match = sf_headers_key_eq(key, name);
|
||||
scr_str_release(key);
|
||||
if (!match) continue;
|
||||
if (!sf_headers_key_eq(sf_pair_at(h->pairs, i), name)) continue;
|
||||
|
||||
ScrStr *previous = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
||||
ScrStr *previous = sf_pair_at(h->pairs, i + 1);
|
||||
const char *separator =
|
||||
name->len == 6 && memcmp(name->data, "cookie", 6) == 0
|
||||
? "; "
|
||||
@@ -2420,7 +2442,10 @@ static void sf_headers_append_pair(
|
||||
memcpy(joined_data + previous->len + 2, value->data, value->len);
|
||||
ScrStr *joined = scr_str_new(joined_data, joined_len);
|
||||
free(joined_data);
|
||||
scr_str_release(previous);
|
||||
/* Appending an empty value leaves the separator's space at the end of
|
||||
* the combined value, which a copy trims again: the pairs are no
|
||||
* longer in their canonical, trimmed form. */
|
||||
if (value->len == 0) h->canonical = false;
|
||||
scr_str_release(name);
|
||||
scr_str_release(value);
|
||||
scr_arr_set_ref(h->pairs, (double)(i + 1), joined);
|
||||
@@ -2447,7 +2472,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
|
||||
if (capacity && !names) sf_oom();
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
||||
ScrStr *key = sf_pair_at(h->pairs, i);
|
||||
bool seen = false;
|
||||
for (size_t j = 0; j < count; j++) {
|
||||
if (sf_headers_key_eq(names[j], key)) {
|
||||
@@ -2455,8 +2480,7 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (seen) scr_str_release(key);
|
||||
else names[count++] = key;
|
||||
if (!seen) names[count++] = key;
|
||||
}
|
||||
if (count > 1) {
|
||||
qsort(names, count, sizeof *names, &sf_headers_name_cmp);
|
||||
@@ -2469,44 +2493,44 @@ static bool sf_headers_is_set_cookie(const ScrStr *name) {
|
||||
return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0;
|
||||
}
|
||||
|
||||
/* The `index`th entry of Fetch's "sort and combine" list: names in
|
||||
* byte order, each with its combined value, except that every set-cookie
|
||||
* value is its own entry (in insertion order), as Node's Headers
|
||||
* iterator yields them. False past the end. Outputs are +1. */
|
||||
/* Fetch's "sort and combine" list as flat name/value pairs: names in byte
|
||||
* order, each with its combined value, except that every set-cookie value
|
||||
* is its own entry (in insertion order), as Node's Headers iterator yields
|
||||
* them. Cached until the next mutation; borrowed. */
|
||||
static ScrArr *sf_headers_sorted(SfHeaders *h) {
|
||||
if (h->sorted) return h->sorted;
|
||||
size_t count = 0;
|
||||
ScrStr **names = sf_headers_sorted_names(h, &count); /* borrowed names */
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
ScrArr *sorted = scr_arr_new(SCR_ELEM_STR, n);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (!sf_headers_is_set_cookie(names[i])) {
|
||||
scr_arr_push_ref(sorted, scr_str_retain(names[i]));
|
||||
scr_arr_push_ref(sorted, sf_headers_get_value(h, names[i]));
|
||||
continue;
|
||||
}
|
||||
for (size_t j = 0; j + 1 < n; j += 2) {
|
||||
if (!sf_headers_is_set_cookie(sf_pair_at(h->pairs, j))) continue;
|
||||
scr_arr_push_ref(sorted, scr_str_retain(names[i]));
|
||||
scr_arr_push_ref(sorted, scr_str_retain(sf_pair_at(h->pairs, j + 1)));
|
||||
}
|
||||
}
|
||||
free(names);
|
||||
h->sorted = sorted;
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/* The `index`th entry of the sort-and-combine list (sf_headers_sorted).
|
||||
* False past the end. Outputs are +1. */
|
||||
static bool sf_headers_sorted_entry(SfHeaders *h, size_t index,
|
||||
ScrStr **name_out,
|
||||
ScrStr **value_out) {
|
||||
size_t count = 0;
|
||||
ScrStr **names = sf_headers_sorted_names(h, &count);
|
||||
bool found = false;
|
||||
size_t at = 0;
|
||||
for (size_t i = 0; i < count && !found; i++) {
|
||||
if (!sf_headers_is_set_cookie(names[i])) {
|
||||
if (at == index) {
|
||||
*name_out = scr_str_retain(names[i]);
|
||||
*value_out = sf_headers_get_value(h, names[i]);
|
||||
found = true;
|
||||
}
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
for (size_t j = 0; j + 1 < n && !found; j += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)j);
|
||||
bool match = sf_headers_is_set_cookie(key);
|
||||
scr_str_release(key);
|
||||
if (!match) continue;
|
||||
if (at == index) {
|
||||
*name_out = scr_str_retain(names[i]);
|
||||
*value_out = scr_arr_get_ref(h->pairs, (double)(j + 1));
|
||||
found = true;
|
||||
}
|
||||
at++;
|
||||
}
|
||||
}
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
return found;
|
||||
ScrArr *sorted = sf_headers_sorted(h);
|
||||
size_t entries = (size_t)scr_arr_len(sorted) / 2;
|
||||
if (index >= entries) return false;
|
||||
*name_out = scr_str_retain(sf_pair_at(sorted, 2 * index));
|
||||
*value_out = scr_str_retain(sf_pair_at(sorted, 2 * index + 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* One step of a live Headers iterator (entries, keys or values): the
|
||||
@@ -2570,14 +2594,8 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
||||
if (key->len == 10 &&
|
||||
memcmp(key->data, "set-cookie", 10) == 0) {
|
||||
scr_dyn_arr_push(out, scr_dyn_new_str(value));
|
||||
}
|
||||
scr_str_release(key);
|
||||
scr_str_release(value);
|
||||
if (sf_headers_is_set_cookie(sf_pair_at(h->pairs, i)))
|
||||
scr_dyn_arr_push(out, scr_dyn_new_str(sf_pair_at(h->pairs, i + 1)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -3323,14 +3341,13 @@ static bool sf_pairs_have(ScrArr *pairs, const char *name) {
|
||||
size_t n = (size_t)scr_arr_len(pairs);
|
||||
size_t want = strlen(name);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
||||
ScrStr *key = sf_pair_at(pairs, i);
|
||||
bool same = key->len == want;
|
||||
for (size_t j = 0; j < want && same; j++) {
|
||||
char c = key->data[j];
|
||||
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||||
same = c == name[j];
|
||||
}
|
||||
scr_str_release(key);
|
||||
if (same) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -3430,6 +3447,21 @@ static bool sf_request_header_ok(
|
||||
!sf_header_name_ci(name, name_len, "expect");
|
||||
}
|
||||
|
||||
/* Whether every value in `pairs` is already trimmed (sf_trim_header_value
|
||||
* would keep it): the form a copy can take unchanged (SfHeaders.canonical).
|
||||
* Combining duplicate names with an empty value leaves the separator's
|
||||
* space at the end of the combined value, which a copy trims again. */
|
||||
static bool sf_header_values_trimmed(ScrArr *pairs) {
|
||||
size_t n = (size_t)scr_arr_len(pairs);
|
||||
for (size_t i = 1; i < n; i += 2) {
|
||||
const ScrStr *value = sf_pair_at(pairs, i);
|
||||
if (value->len == 0) continue;
|
||||
char first = value->data[0], last = value->data[value->len - 1];
|
||||
if (first == ' ' || first == '\t' || last == ' ' || last == '\t') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static ScrStr *sf_trim_header_value(const ScrStr *value) {
|
||||
size_t start = 0;
|
||||
size_t end = value->len;
|
||||
@@ -3441,11 +3473,14 @@ static ScrStr *sf_trim_header_value(const ScrStr *value) {
|
||||
(value->data[end - 1] == ' ' || value->data[end - 1] == '\t')) {
|
||||
end--;
|
||||
}
|
||||
if (start == 0 && end == value->len) return scr_str_retain((ScrStr *)value);
|
||||
return scr_str_new(value->data + start, end - start);
|
||||
}
|
||||
|
||||
static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
|
||||
static bool sf_push_header(ScrArr *pairs, ScrStr *name_str,
|
||||
ScrStr *value, bool request_guard) {
|
||||
const char *name = name_str->data;
|
||||
size_t name_len = name_str->len;
|
||||
if (!sf_header_name_ok(name, name_len)) {
|
||||
ScrStr *display = scr_str_new(name, name_len);
|
||||
sf_headers_invalid("append", display, "name");
|
||||
@@ -3471,7 +3506,7 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
|
||||
if (!sf_header_name_ci(name, name_len, "set-cookie")) {
|
||||
size_t n = (size_t)scr_arr_len(pairs);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(pairs, (double)i);
|
||||
ScrStr *key = sf_pair_at(pairs, i);
|
||||
bool match = key->len == name_len;
|
||||
for (size_t j = 0; j < name_len && match; j++) {
|
||||
unsigned char a = (unsigned char)key->data[j];
|
||||
@@ -3480,11 +3515,9 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
|
||||
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
|
||||
match = a == b;
|
||||
}
|
||||
scr_str_release(key);
|
||||
if (!match) continue;
|
||||
|
||||
ScrStr *previous =
|
||||
scr_arr_get_ref(pairs, (double)(i + 1));
|
||||
ScrStr *previous = sf_pair_at(pairs, i + 1);
|
||||
const char *separator =
|
||||
sf_header_name_ci(name, name_len, "cookie") ? "; " : ", ";
|
||||
size_t separator_len = 2;
|
||||
@@ -3498,7 +3531,6 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
|
||||
normalized->data, normalized->len);
|
||||
ScrStr *joined = scr_str_new(joined_data, joined_len);
|
||||
free(joined_data);
|
||||
scr_str_release(previous);
|
||||
scr_str_release(normalized);
|
||||
if (request_guard && !sf_request_header_ok(name, name_len, joined)) {
|
||||
scr_str_release(joined);
|
||||
@@ -3517,8 +3549,8 @@ static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
|
||||
scr_arr_push_ref(
|
||||
pairs,
|
||||
request_guard
|
||||
? scr_str_new(name, name_len)
|
||||
: sf_header_name_lower(name, name_len));
|
||||
? scr_str_retain(name_str)
|
||||
: sf_header_name_lower_str(name_str));
|
||||
scr_arr_push_ref(pairs, normalized);
|
||||
return true;
|
||||
}
|
||||
@@ -3545,10 +3577,13 @@ static void sf_headers_pair_length_error(size_t len) {
|
||||
* leaves header-name/value syntax validation for initialization. Response
|
||||
* construction converts this snapshot before status/statusText and only
|
||||
* validates it after body extraction, matching Undici's two phases. */
|
||||
static ScrArr *sf_headers_convert_init(const ScrDyn *headers) {
|
||||
/* *canonical_out reports that the result copies a canonical Headers
|
||||
* object's pairs (SfHeaders.canonical). */
|
||||
static ScrArr *sf_headers_convert_init(const ScrDyn *headers, bool *canonical_out) {
|
||||
*canonical_out = false;
|
||||
if (headers && headers->kind == SCR_DYN_TYPED_REF) {
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(headers);
|
||||
ScrArr *converted = sf_headers_convert_init(materialized);
|
||||
ScrArr *converted = sf_headers_convert_init(materialized, canonical_out);
|
||||
scr_dyn_release(materialized);
|
||||
return converted;
|
||||
}
|
||||
@@ -3639,13 +3674,9 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers) {
|
||||
headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS) {
|
||||
SfHeaders *source = (SfHeaders *)headers->v.handle.ptr;
|
||||
size_t n = (size_t)scr_arr_len(source->pairs);
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *name = scr_arr_get_ref(source->pairs, (double)i);
|
||||
ScrStr *value =
|
||||
scr_arr_get_ref(source->pairs, (double)(i + 1));
|
||||
scr_arr_push_ref(converted, name);
|
||||
scr_arr_push_ref(converted, value);
|
||||
}
|
||||
for (size_t i = 0; i < n - n % 2; i++)
|
||||
scr_arr_push_ref(converted, scr_str_retain(sf_pair_at(source->pairs, i)));
|
||||
*canonical_out = source->canonical;
|
||||
return converted;
|
||||
}
|
||||
/* Functions are WebIDL objects and an ordinary function has no
|
||||
@@ -3660,25 +3691,29 @@ fail:
|
||||
}
|
||||
|
||||
static bool sf_add_converted_headers(
|
||||
ScrArr *pairs, ScrArr *converted, bool request_guard) {
|
||||
ScrArr *pairs, ScrArr *converted, bool request_guard, bool canonical) {
|
||||
size_t n = (size_t)scr_arr_len(converted);
|
||||
/* Pushing canonical pairs into an empty list one by one through
|
||||
* sf_push_header would reproduce them exactly. */
|
||||
if (canonical && !request_guard && scr_arr_len(pairs) == 0) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
scr_arr_push_ref(pairs, scr_str_retain(sf_pair_at(converted, i)));
|
||||
return true;
|
||||
}
|
||||
for (size_t i = 0; i + 1 < n; i += 2) {
|
||||
ScrStr *name = scr_arr_get_ref(converted, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(converted, (double)(i + 1));
|
||||
bool ok = sf_push_header(
|
||||
pairs, name->data, name->len, value, request_guard);
|
||||
scr_str_release(name);
|
||||
scr_str_release(value);
|
||||
if (!ok) return false;
|
||||
if (!sf_push_header(pairs, sf_pair_at(converted, i),
|
||||
sf_pair_at(converted, i + 1), request_guard))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sf_add_headers(
|
||||
ScrArr *pairs, const ScrDyn *headers, bool request_guard) {
|
||||
ScrArr *converted = sf_headers_convert_init(headers);
|
||||
bool canonical = false;
|
||||
ScrArr *converted = sf_headers_convert_init(headers, &canonical);
|
||||
if (!converted) return false;
|
||||
bool ok = sf_add_converted_headers(pairs, converted, request_guard);
|
||||
bool ok = sf_add_converted_headers(pairs, converted, request_guard, canonical);
|
||||
scr_arr_release(converted);
|
||||
return ok;
|
||||
}
|
||||
@@ -4858,6 +4893,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
|
||||
SfJsonSerialize serialize) {
|
||||
ScrArr *header_pairs = NULL;
|
||||
ScrArr *converted_headers = NULL;
|
||||
bool converted_canonical = false;
|
||||
ScrBytes *body_bytes = NULL;
|
||||
ScrStr *status_text = NULL;
|
||||
SfStream *stream = NULL;
|
||||
@@ -4922,7 +4958,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
|
||||
* later response-initialization step, after body extraction. */
|
||||
ScrDyn *init_headers = sf_response_init_get(init, "headers", 7);
|
||||
if (light_ok) light_form = sf_light_header_form(init_headers, json_body, &light_record);
|
||||
converted_headers = sf_headers_convert_init(init_headers);
|
||||
converted_headers = sf_headers_convert_init(init_headers, &converted_canonical);
|
||||
scr_dyn_release(init_headers);
|
||||
if (!converted_headers) goto fail;
|
||||
|
||||
@@ -5017,7 +5053,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
|
||||
|
||||
header_pairs = scr_arr_new(SCR_ELEM_STR, 8);
|
||||
if (!sf_add_converted_headers(
|
||||
header_pairs, converted_headers, false)) goto fail;
|
||||
header_pairs, converted_headers, false, converted_canonical)) goto fail;
|
||||
scr_arr_release(converted_headers);
|
||||
converted_headers = NULL;
|
||||
|
||||
@@ -5048,6 +5084,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
|
||||
response->rc = 1;
|
||||
response->body = stream;
|
||||
response->headers = sf_headers_new(header_pairs, false);
|
||||
response->headers->canonical = sf_header_values_trimmed(header_pairs);
|
||||
response->url = scr_str_new("", 0);
|
||||
response->status_text = status_text;
|
||||
response->status = status;
|
||||
@@ -5163,13 +5200,15 @@ static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, Sc
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_headers_new(ScrDyn *init) {
|
||||
ScrArr *converted = sf_headers_convert_init(init);
|
||||
bool canonical = false;
|
||||
ScrArr *converted = sf_headers_convert_init(init, &canonical);
|
||||
if (!converted) return NULL;
|
||||
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 8);
|
||||
bool ok = sf_add_converted_headers(pairs, converted, false);
|
||||
bool ok = sf_add_converted_headers(pairs, converted, false, canonical);
|
||||
scr_arr_release(converted);
|
||||
if (!ok) { scr_arr_release(pairs); return NULL; }
|
||||
SfHeaders *headers = sf_headers_new(pairs, false);
|
||||
headers->canonical = sf_header_values_trimmed(pairs);
|
||||
ScrDyn *result = scr_dyn_new_handle(headers, SCR_DYNH_FETCH_HEADERS);
|
||||
sf_headers_release(headers);
|
||||
return result;
|
||||
|
||||
@@ -1928,6 +1928,22 @@ static __attribute__((noinline)) double scr_str_unit_search(const ScrStr *s, con
|
||||
/* Search backwards from a clamped UTF-16 position. A low-surrogate position
|
||||
* maps to its scalar's first byte, so that scalar remains searchable. */
|
||||
double scr_str_last_index_of_from(ScrStr *s, ScrStr *needle, double position) {
|
||||
/* A proven-ASCII receiver: byte offsets are UTF-16 indexes, and a needle
|
||||
* with other bytes (including lone surrogate halves) is never found. */
|
||||
if (s->ascii) {
|
||||
size_t start = isnan(position) || position >= (double)s->len ? s->len
|
||||
: position <= 0 ? 0 : (size_t)trunc(position);
|
||||
if (needle->len == 0) return (double)start;
|
||||
if (needle->len > s->len) return -1.0;
|
||||
size_t max_start = s->len - needle->len;
|
||||
if (start < max_start) max_start = start;
|
||||
char first = needle->data[0];
|
||||
for (size_t i = max_start + 1; i-- > 0;) {
|
||||
if (s->data[i] == first && memcmp(s->data + i, needle->data, needle->len) == 0)
|
||||
return (double)i;
|
||||
}
|
||||
return -1.0;
|
||||
}
|
||||
ScrSidx *e = scr_sidx_for(s);
|
||||
size_t len16 = scr_sidx_len(s, e);
|
||||
size_t start16 = isnan(position) || position >= (double)len16 ? len16
|
||||
|
||||
@@ -394,8 +394,8 @@ export function dynamicTierSymbols(names) {
|
||||
* not yet traced to its typed root. bench/thread-slots,
|
||||
* bench/channel-messages and bench/worker-messages, added later, link them
|
||||
* through worker messages:
|
||||
* node:worker_threads values are dyn objects. bench/dynamic-methods and
|
||||
* bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
|
||||
* node:worker_threads values are dyn objects. bench/dynamic-methods is
|
||||
* untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
|
||||
* tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also
|
||||
* fails when a listed workload stops linking them, so remove a workload here
|
||||
* when a leak fix makes its binary clean. */
|
||||
@@ -410,7 +410,6 @@ export const DYNAMIC_WORKLOADS = new Set([
|
||||
"bench/thread-slots",
|
||||
"bench/channel-messages",
|
||||
"bench/dynamic-methods",
|
||||
"bench/dynamic-properties",
|
||||
"bench/worker-messages",
|
||||
"bench/typed-array-scan-workers",
|
||||
"bench/log-summary",
|
||||
|
||||
@@ -11,7 +11,7 @@ function tail(path: string): string {
|
||||
}
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const path = "tmp-1006-" + tail(process.argv[1]) + ".json";
|
||||
const path = "tmp-1006-" + tail(process.argv[1]) + "-" + process.pid + ".json";
|
||||
|
||||
type Config = {
|
||||
name: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ function tail(path: string): string {
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
|
||||
const scratch = "tmp-1354-" + tail(process.argv[1]);
|
||||
const scratch = "tmp-1354-" + tail(process.argv[1]) + "-" + process.pid;
|
||||
if (existsSync(scratch)) {
|
||||
if (existsSync(j(scratch, "f.txt"))) {
|
||||
rmSync(j(scratch, "f.txt"));
|
||||
|
||||
@@ -15,7 +15,7 @@ function tail(path: string): string {
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const scratch = "tmp-1357-" + tail(process.argv[1]);
|
||||
const scratch = "tmp-1357-" + tail(process.argv[1]) + "-" + process.pid;
|
||||
if (existsSync(scratch)) {
|
||||
if (existsSync(scratch + "/a.txt")) {
|
||||
rmSync(scratch + "/a.txt");
|
||||
|
||||
@@ -14,7 +14,7 @@ async function main(): Promise<void> {
|
||||
console.log(timeout.hasRef(), immediate.hasRef());
|
||||
}
|
||||
|
||||
const path = "/tmp/scriptc-disposable-file.txt";
|
||||
const path = `/tmp/scriptc-disposable-file-${process.pid}.txt`;
|
||||
await writeFile(path, "resource");
|
||||
{
|
||||
await using handle = await open(path, "r");
|
||||
|
||||
@@ -8,7 +8,7 @@ function tail(path) {
|
||||
while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1;
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const dir = `/tmp/scr-dirent-dyn-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`;
|
||||
const dir = `/tmp/scr-dirent-dyn-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}-${process.pid}`;
|
||||
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
fs.mkdirSync(`${dir}/sub`, { recursive: true });
|
||||
fs.writeFileSync(`${dir}/a.txt`, "x");
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Two-counter loops that index number arrays at both counters
|
||||
// (`for (let i = lo, j = hi; i < j; i++, j--)`), and element copies through
|
||||
// `a[i]!` reads, both inside and outside the ranges a single entry test can
|
||||
// cover: filled prefixes, holes, present undefined, reads past the end,
|
||||
// fractional and negative starts, int32 arrays and long checked arrays.
|
||||
|
||||
function reversePrefix(a: number[], end: number): void {
|
||||
for (let i = 0, j = end; i < j; i++, j--) {
|
||||
const t = a[i]!;
|
||||
a[i] = a[j]!;
|
||||
a[j] = t;
|
||||
}
|
||||
}
|
||||
|
||||
function show(label: string, a: number[]): void {
|
||||
console.log(label, a.length, JSON.stringify(a), a);
|
||||
}
|
||||
|
||||
// The whole prefix is present: the entry test passes.
|
||||
const plain: number[] = [];
|
||||
for (let k = 0; k < 8; k++) plain[k] = k * 3;
|
||||
reversePrefix(plain, 5);
|
||||
show("plain", plain);
|
||||
reversePrefix(plain, 7);
|
||||
show("whole", plain);
|
||||
reversePrefix(plain, 0);
|
||||
show("empty range", plain);
|
||||
|
||||
// The high start is past the end: the reads there miss, the stores extend.
|
||||
const short = [1, 2, 3];
|
||||
reversePrefix(short, 5);
|
||||
show("past the end", short);
|
||||
|
||||
// A hole inside the range.
|
||||
const holey: number[] = [10, 20];
|
||||
holey[3] = 40;
|
||||
holey[4] = 50;
|
||||
reversePrefix(holey, 4);
|
||||
show("hole", holey);
|
||||
|
||||
// A present undefined inside the range, copied by a mirror loop.
|
||||
const loose: number[] = [7, 8, 9, 10];
|
||||
(loose as (number | undefined)[])[2] = undefined;
|
||||
for (let i = 0, j = 3; i < j; i++, j--) loose[i] = loose[j]!;
|
||||
show("undefined", loose);
|
||||
|
||||
// Fractional and negative starts index non-element properties.
|
||||
const fractional = [1, 2, 3, 4];
|
||||
reversePrefix(fractional, 2.5);
|
||||
show("fractional", fractional);
|
||||
const negative = [5, 6, 7];
|
||||
for (let i = -1, j = 2; i < j; i++, j--) negative[i] = negative[j]!;
|
||||
show("negative", negative);
|
||||
|
||||
// A local int32 array (every stored element is a small integer).
|
||||
function int32Flips(start: number): void {
|
||||
const counts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
let flips = 0;
|
||||
for (let first = counts[start]!; first !== 0; first = counts[0]!) {
|
||||
for (let i = 0, j = first; i < j; i++, j--) {
|
||||
const t = counts[i]!;
|
||||
counts[i] = counts[j]!;
|
||||
counts[j] = t;
|
||||
}
|
||||
flips++;
|
||||
if (flips > 20) break;
|
||||
}
|
||||
console.log("int32", flips, counts.join(","));
|
||||
}
|
||||
int32Flips(3);
|
||||
int32Flips(9);
|
||||
|
||||
// A local long array with checked int32 stores, widened by a fraction.
|
||||
function longChecked(n: number, fraction: number): void {
|
||||
const long = new Array<number>(n).fill(0);
|
||||
for (let k = 0; k < long.length; k++) long[k] = k;
|
||||
for (let i = 0, j = n - 1; i < j; i++, j--) {
|
||||
const t = long[i]!;
|
||||
long[i] = long[j]!;
|
||||
long[j] = t;
|
||||
}
|
||||
console.log("long", long[0], long[1], long[n - 2], long[n - 1]);
|
||||
long[10] = fraction;
|
||||
for (let i = 0, j = 20; i < j; i++, j--) {
|
||||
const t = long[i]!;
|
||||
long[i] = long[j]!;
|
||||
long[j] = t;
|
||||
}
|
||||
console.log("widened", long.slice(0, 12).join(","), long[20]);
|
||||
}
|
||||
longChecked(5000, 0.5);
|
||||
|
||||
// Element copies through `a[i]!`: the source is shorter than the range.
|
||||
function copy(to: number[], from: number[], n: number): void {
|
||||
for (let i = 0; i < n; i++) to[i] = from[i]!;
|
||||
}
|
||||
const into = [0, 0, 0, 0, 0, 0];
|
||||
copy(into, [1, 2, 3, 4, 5, 6], 6);
|
||||
show("copy", into);
|
||||
copy(into, [9, 8], 4);
|
||||
show("short source", into);
|
||||
const gappy: number[] = [1];
|
||||
gappy[2] = 3;
|
||||
gappy[3] = 4;
|
||||
copy(into, gappy, 4);
|
||||
show("gappy source", into);
|
||||
|
||||
// Two names for one array.
|
||||
function aliased(n: number): void {
|
||||
const same: number[] = [];
|
||||
for (let k = 0; k < n; k++) same[k] = k + 1;
|
||||
const alias = same;
|
||||
for (let i = 0, j = n - 1; i < j; i++, j--) {
|
||||
const t = alias[i]!;
|
||||
same[i] = alias[j]!;
|
||||
same[j] = t;
|
||||
}
|
||||
show("alias", same);
|
||||
}
|
||||
aliased(5);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { describe, kind, mark } from "./util.ts";
|
||||
|
||||
export const version = 3;
|
||||
export const marked = mark({});
|
||||
console.log("core body", kind(), describe());
|
||||
@@ -0,0 +1,10 @@
|
||||
// A cycle member runs user code before it declares a constant that only
|
||||
// functions read, one of them as a computed key and one under `typeof`
|
||||
// (both resolved from the constant's literal type). Nothing calls those
|
||||
// functions before the declaration ran, so they read the constant
|
||||
// normally once the other member's top level calls them.
|
||||
import { marked } from "./core.ts";
|
||||
import { describe, kind, mark } from "./util.ts";
|
||||
|
||||
console.log(JSON.stringify(marked), kind());
|
||||
console.log(JSON.stringify(mark({ seen: false })), describe());
|
||||
@@ -0,0 +1,23 @@
|
||||
import { version } from "./core.ts";
|
||||
|
||||
export function makeCounter(): () => number {
|
||||
let n = 0;
|
||||
return () => ++n;
|
||||
}
|
||||
const next = makeCounter();
|
||||
console.log("util body", next(), next());
|
||||
|
||||
export const TAG = "~tagged";
|
||||
|
||||
export function mark(box: Record<string, boolean>): Record<string, boolean> {
|
||||
box[TAG] = true;
|
||||
return box;
|
||||
}
|
||||
|
||||
export function kind(): string {
|
||||
return typeof TAG;
|
||||
}
|
||||
|
||||
export function describe(): string {
|
||||
return `core v${version}`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { probe } from "./beta.ts";
|
||||
|
||||
export function report(): string {
|
||||
return `alpha ${NAME}`;
|
||||
}
|
||||
|
||||
export function late(): string {
|
||||
return `${OTHER} after ${probe}`;
|
||||
}
|
||||
|
||||
export const NAME = "ready";
|
||||
export const OTHER = "other";
|
||||
console.log("alpha body", report());
|
||||
@@ -0,0 +1,12 @@
|
||||
import { report } from "./alpha.ts";
|
||||
|
||||
function attempt(): string {
|
||||
try {
|
||||
return report();
|
||||
} catch (error) {
|
||||
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
|
||||
}
|
||||
}
|
||||
|
||||
export const probe = attempt();
|
||||
console.log("beta body", probe);
|
||||
@@ -0,0 +1,7 @@
|
||||
// The first cycle member to run calls a function of the other member
|
||||
// before that member declared the constant the function reads: Node's
|
||||
// temporal-dead-zone ReferenceError. A second function of the same member
|
||||
// reads another constant but runs only after the cycle finished.
|
||||
import { late, report } from "./alpha.ts";
|
||||
|
||||
console.log(report(), late());
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Probe, Lazy } from "./beta.ts";
|
||||
|
||||
export { Probe };
|
||||
export const label = "ready";
|
||||
export const other = "other";
|
||||
|
||||
export function later(): string {
|
||||
return `${Lazy.describe()} ${other}`;
|
||||
}
|
||||
|
||||
console.log("alpha body", Probe.result, Lazy.value);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { label, other } from "./alpha.ts";
|
||||
|
||||
function guard(read: () => string): string {
|
||||
try {
|
||||
return read();
|
||||
} catch (error) {
|
||||
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
|
||||
}
|
||||
}
|
||||
|
||||
export class Probe {
|
||||
static helper(): string {
|
||||
return guard(() => `label ${label}`);
|
||||
}
|
||||
static result = this.helper();
|
||||
}
|
||||
|
||||
export class Lazy {
|
||||
static read(): string {
|
||||
return guard(() => `other ${other}`);
|
||||
}
|
||||
static describe(): string {
|
||||
return `lazy ${other}`;
|
||||
}
|
||||
static value = [0].map(() => this.read()).join("");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// The first cycle member to run builds classes whose static initializers
|
||||
// call a method through `this`, directly and from an arrow function; both
|
||||
// methods read constants of the other member before it declared them:
|
||||
// Node's temporal-dead-zone ReferenceError. A third method reads a constant
|
||||
// only after the cycle finished.
|
||||
import { label, Probe, later } from "./alpha.ts";
|
||||
|
||||
console.log(label, Probe.result, later());
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Probe, Lazy } from "./beta.ts";
|
||||
|
||||
export { Probe };
|
||||
export const label = "ready";
|
||||
export const other = "other";
|
||||
|
||||
export function later(): string {
|
||||
return `${Lazy.describe()} ${other}`;
|
||||
}
|
||||
|
||||
console.log("alpha body", Probe.result, Lazy.value);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { label, other } from "./alpha.ts";
|
||||
|
||||
function guard(read: () => string): string {
|
||||
try {
|
||||
return read();
|
||||
} catch (error) {
|
||||
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
|
||||
}
|
||||
}
|
||||
|
||||
export const Probe = class ProbeImpl {
|
||||
static helper(): string {
|
||||
return guard(() => `label ${label}`);
|
||||
}
|
||||
static result = ProbeImpl.helper();
|
||||
};
|
||||
|
||||
export const Lazy = class LazyImpl {
|
||||
static read(): string {
|
||||
return guard(() => `other ${other}`);
|
||||
}
|
||||
static describe(): string {
|
||||
return `lazy ${other}`;
|
||||
}
|
||||
static value = [0].map(() => LazyImpl.read()).join("");
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// The first cycle member to run builds classes from named class
|
||||
// expressions whose static initializers call a method through the inner
|
||||
// name, directly and from an arrow function; both methods read constants
|
||||
// of the other member before it declared them: Node's temporal-dead-zone
|
||||
// ReferenceError. A third method reads a constant only after the cycle
|
||||
// finished.
|
||||
import { label, Probe, later } from "./alpha.ts";
|
||||
|
||||
console.log(label, Probe.result, later());
|
||||
@@ -0,0 +1,49 @@
|
||||
// Keyed reads of string-indexed records whose values are numbers or
|
||||
// booleans, consumed by `??`, `!`, comparisons with undefined and typeof:
|
||||
// declared fields, an optional declared field, index-signature entries and
|
||||
// missing keys. A stored NaN is a value, so `??` keeps it; -0 survives the
|
||||
// read; a missing key is undefined.
|
||||
interface Scores {
|
||||
base: number;
|
||||
bonus?: number;
|
||||
[name: string]: number | undefined;
|
||||
}
|
||||
|
||||
const scores: Scores = { base: 10 };
|
||||
scores["alice"] = 3;
|
||||
scores["nan"] = NaN;
|
||||
scores["negzero"] = -0;
|
||||
|
||||
const keys = ["base", "bonus", "alice", "nan", "negzero", "missing"];
|
||||
for (const key of keys) {
|
||||
const read = scores[key];
|
||||
console.log(
|
||||
key,
|
||||
scores[key] ?? "default",
|
||||
scores[key] === undefined,
|
||||
typeof scores[key],
|
||||
Object.is(scores[key] ?? 1, -0),
|
||||
(scores[key] ?? 0) + 1,
|
||||
read,
|
||||
);
|
||||
}
|
||||
scores.bonus = 5;
|
||||
console.log("bonus", scores["bonus"] ?? "default", scores["bonus"]! * 2);
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
for (const word of "a b a c b a d".split(" ")) counts[word] = (counts[word] ?? 0) + 1;
|
||||
let total = 0;
|
||||
for (const word in counts) total += counts[word]!;
|
||||
console.log(JSON.stringify(counts), total, counts["zzz"] ?? -1);
|
||||
|
||||
const flags: Record<string, boolean> = { on: true, off: false };
|
||||
for (const key of ["on", "off", "unset"]) {
|
||||
console.log(key, flags[key] ?? "unset", flags[key] === undefined, typeof flags[key]);
|
||||
}
|
||||
let seen = 0;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const key = "k" + (i % 7);
|
||||
flags[key] = !(flags[key] ?? false);
|
||||
if (flags[key]!) seen++;
|
||||
}
|
||||
console.log(seen, Object.keys(flags).length);
|
||||
@@ -0,0 +1,66 @@
|
||||
// A `var` read in a later declarator of the statement that stores it runs
|
||||
// after the store; a read in the same or an earlier declarator sees undefined.
|
||||
var base = { a: 1, b: "x" },
|
||||
more = { ...base, c: 2 },
|
||||
keys = Object.keys({ ...more }).join(","),
|
||||
first = base.a + 1;
|
||||
console.log(JSON.stringify(more), keys, first);
|
||||
|
||||
function describeBase() {
|
||||
return base.b + more.c;
|
||||
}
|
||||
var rules = { space: /\s+/, word: /\w+/ },
|
||||
merged = { ...rules, digit: /\d/ },
|
||||
described = describeBase();
|
||||
console.log(Object.keys(merged).join(","), merged.word.test("ab"), described);
|
||||
|
||||
var table = { one: 1, two: 2 },
|
||||
lookup = function (key) {
|
||||
return table[key];
|
||||
},
|
||||
arrow = (key) => ({ ...table })[key];
|
||||
console.log(lookup("one"), arrow("two"));
|
||||
|
||||
var readLater = function () {
|
||||
return later;
|
||||
},
|
||||
early = readLater(),
|
||||
later = { k: 1 },
|
||||
after = readLater().k;
|
||||
console.log(early, after);
|
||||
|
||||
var self = self ? 1 : 2;
|
||||
console.log(self);
|
||||
|
||||
for (var i = 0, j = i + 10; i < 2; i++) console.log(i, j);
|
||||
|
||||
function nested() {
|
||||
var inner = [1, 2, 3],
|
||||
copy = [...inner, 4],
|
||||
total = copy.reduce((sum, n) => sum + n, 0);
|
||||
return { copy, total };
|
||||
}
|
||||
console.log(JSON.stringify(nested()));
|
||||
|
||||
function earlyInFunction() {
|
||||
show("before");
|
||||
var point = { n: 1 },
|
||||
next = { m: point.n + 1 };
|
||||
show("after");
|
||||
function show(when) {
|
||||
console.log(when, typeof point, point === undefined ? "none" : point.n, next === undefined ? "none" : next.m);
|
||||
}
|
||||
var check = () => console.log("check", items === undefined);
|
||||
check();
|
||||
var items = [1];
|
||||
check();
|
||||
}
|
||||
earlyInFunction();
|
||||
|
||||
function loopLists() {
|
||||
for (var k = 0, sum = k; k < 3; k++) sum += k;
|
||||
var doubled = sum * 2,
|
||||
again = doubled + 1;
|
||||
return [sum, doubled, again];
|
||||
}
|
||||
console.log(JSON.stringify(loopLists()));
|
||||
@@ -0,0 +1,89 @@
|
||||
// String search, match and replace with untyped pattern values: a RegExp
|
||||
// is used as is, and any other value is converted as Node converts it.
|
||||
const rules = JSON.parse('{"other":{}}');
|
||||
rules.other.space = /\s+/g;
|
||||
rules.other.word = /o+/;
|
||||
rules.other.text = "l";
|
||||
rules.other.dot = ".";
|
||||
rules.other.none = undefined;
|
||||
rules.other.nul = null;
|
||||
rules.other.one = /(l)(o)/;
|
||||
rules.other.many = /l+/g;
|
||||
const t = " hello world";
|
||||
|
||||
console.log(
|
||||
t.search(rules.other.word),
|
||||
t.search(rules.other.text),
|
||||
t.search(rules.other.none),
|
||||
t.search(rules.other.nul),
|
||||
t.search(rules.other.space),
|
||||
"a.b".search(rules.other.dot),
|
||||
);
|
||||
console.log(rules.other.space.lastIndex);
|
||||
|
||||
let conversions = 0;
|
||||
rules.other.obj = {
|
||||
toString() {
|
||||
conversions++;
|
||||
return "w";
|
||||
},
|
||||
};
|
||||
console.log(t.search(rules.other.obj), t.match(rules.other.obj)[0], conversions);
|
||||
|
||||
const all = t.match(rules.other.many);
|
||||
console.log(JSON.stringify(all), rules.other.many.lastIndex);
|
||||
const pair = t.match(rules.other.one);
|
||||
console.log(pair[0], pair[1], pair[2], pair.index, pair.length, pair.input === t);
|
||||
const literal = t.match(rules.other.dot);
|
||||
console.log(literal[0], literal.index, literal.length);
|
||||
console.log(JSON.stringify(t.match(rules.other.none)), t.match(rules.other.zz));
|
||||
rules.other.zz = "zz";
|
||||
console.log(t.match(rules.other.zz));
|
||||
function first(text, re) {
|
||||
const m = text.match(re);
|
||||
return m ? m[0] : "none";
|
||||
}
|
||||
console.log(first("abc", rules.other.dot), first("", rules.other.dot));
|
||||
|
||||
console.log(t.replace(rules.other.space, (m) => "[" + m.length + "]"));
|
||||
console.log(t.replace(rules.other.word, (m, offset) => m.toUpperCase() + offset));
|
||||
console.log(t.replace(rules.other.text, (m, offset, subject) => "<" + m + offset + subject.length + ">"));
|
||||
console.log(t.replaceAll(rules.other.text, (m) => m.toUpperCase()));
|
||||
console.log(t.replaceAll(rules.other.space, () => "_"));
|
||||
console.log(t.replace(rules.other.one, (m, p1, p2, offset) => p1 + "|" + p2 + "|" + offset));
|
||||
console.log(t.replace(rules.other.text, (m, offset, subject, extra) => typeof extra));
|
||||
console.log(t.replace(rules.other.text, (m) => m.length));
|
||||
try {
|
||||
t.replaceAll(rules.other.word, (m) => m);
|
||||
} catch (e) {
|
||||
console.log(e.constructor.name, e.message);
|
||||
}
|
||||
|
||||
function tabs(text, width) {
|
||||
let column = 0;
|
||||
let out = "";
|
||||
for (const ch of text) {
|
||||
if (ch === "\t") {
|
||||
const pad = 4 - (column % 4);
|
||||
out += " ".repeat(pad);
|
||||
column += pad;
|
||||
} else {
|
||||
out += ch;
|
||||
column++;
|
||||
}
|
||||
}
|
||||
return out + width;
|
||||
}
|
||||
class Lexer {
|
||||
constructor(table) {
|
||||
this.rules = table;
|
||||
}
|
||||
indent(line, width) {
|
||||
return line.replace(this.rules.leading, (w) => tabs(w, width));
|
||||
}
|
||||
firstText(line) {
|
||||
return line.search(this.rules.nonSpace);
|
||||
}
|
||||
}
|
||||
const lexer = new Lexer({ leading: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/ });
|
||||
console.log(JSON.stringify(lexer.indent("\tabc", 2)), lexer.firstText(" x"));
|
||||
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: !0 });
|
||||
exports.label = exports.describe = void 0;
|
||||
const node_util_1 = require("node:util");
|
||||
const node_path_1 = require("node:path");
|
||||
function describe(name, size) {
|
||||
return (0, node_util_1.format)("%s holds %d (%s)", name, size, (0, node_path_1.basename)("/a/b/c.txt"));
|
||||
}
|
||||
exports.describe = describe;
|
||||
exports.label = "lib";
|
||||
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
// tsc and esbuild output: the __esModule stamp spelled { value: !0 }, and
|
||||
// calls of builtin module functions without a receiver, (0, ns.member)(...).
|
||||
const lib = require("./lib.cjs");
|
||||
const dc = require("node:diagnostics_channel");
|
||||
const util = require("node:util");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
console.log(lib.label, lib.describe("cache", 3));
|
||||
const metrics = (0, dc.channel)("cache:metrics");
|
||||
console.log(metrics.name, metrics.hasSubscribers);
|
||||
const seen = [];
|
||||
dc.subscribe("cache:metrics", (message) => seen.push(message.key));
|
||||
metrics.publish({ key: "a" });
|
||||
console.log(seen.join(","), metrics.hasSubscribers);
|
||||
const tracing = (0, dc.tracingChannel)("cache");
|
||||
console.log(tracing.hasSubscribers);
|
||||
console.log((0, util.inspect)({ a: [1, 2] }), (0, util.isDeepStrictEqual)([1], [1]));
|
||||
console.log((0, path.join)("a", "b"), typeof (0, os.platform)());
|
||||
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
// Minifiers spell `true` as `!0`: Object.defineProperty(exports,
|
||||
// "__esModule", { value: !0 }) is the same interop stamp as { value: true }.
|
||||
const stamped = require("./stamped.cjs");
|
||||
const moduleStamped = require("./module-stamped.cjs");
|
||||
console.log(stamped.answer, stamped.twice(4), moduleStamped.label);
|
||||
console.log(typeof stamped.twice, moduleStamped.label.length);
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(module.exports, "__esModule", { value: !0 });
|
||||
module.exports.label = "module";
|
||||
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: !0 });
|
||||
exports.answer = void 0;
|
||||
exports.answer = 42;
|
||||
exports.twice = (n) => n * 2;
|
||||
@@ -0,0 +1,18 @@
|
||||
// A for-in or for-of loop stores its var binding only when it iterates:
|
||||
// after a loop that ran zero times, closures read undefined.
|
||||
function f(o) {
|
||||
for (var key in o) {}
|
||||
const read = () => typeof key;
|
||||
const same = () => key === undefined;
|
||||
return [read(), same()];
|
||||
}
|
||||
console.log(JSON.stringify(f({})), JSON.stringify(f({ a: 1 })));
|
||||
function g(xs) {
|
||||
for (var x of xs) {}
|
||||
const show = () => [typeof x, String(x)];
|
||||
return show();
|
||||
}
|
||||
console.log(JSON.stringify(g([])), JSON.stringify(g([4])));
|
||||
for (var top in {}) {}
|
||||
const readTop = () => typeof top;
|
||||
console.log(readTop());
|
||||
@@ -0,0 +1,166 @@
|
||||
// Number arrays of proven int32s that reach other functions: passed to
|
||||
// helpers called directly, along chains of helpers and recursion, and held
|
||||
// in module globals that several functions read. A helper that also
|
||||
// receives an array of other numbers or one that is not a fresh array,
|
||||
// stores a fraction through its parameter, or escapes as a value keeps
|
||||
// every array it can reach in doubles, as does a global an async function
|
||||
// reads. Each case uses its own arrays and helpers.
|
||||
|
||||
const data: number[] = [];
|
||||
let seed = 7;
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
seed = (Math.imul(seed, 48271) + 11) & 0x7fffffff;
|
||||
data.push(seed % 1000);
|
||||
}
|
||||
|
||||
function fnv(values: number[]): number {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
h ^= values[i]! & 0xff;
|
||||
h = Math.imul(h, 16777619) >>> 0;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function histogram(values: number[], buckets: number): number[] {
|
||||
const counts: number[] = new Array(buckets).fill(0);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const b = values[i]! % buckets;
|
||||
counts[b] = counts[b]! + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
// A helper that writes through its parameter and calls another helper.
|
||||
function scale(values: number[], factor: number): number {
|
||||
for (let i = 0; i < values.length; i++) values[i] = (values[i]! * factor) & 0xffff;
|
||||
return sum(values);
|
||||
}
|
||||
|
||||
function sum(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s = (s + values[i]!) | 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Recursion passes the array on, and the elements index another array.
|
||||
function chase(next: number[], at: number, steps: number): number {
|
||||
if (steps === 0) return at;
|
||||
return chase(next, next[at]!, steps - 1);
|
||||
}
|
||||
|
||||
function pushAll(target: number[], count: number): void {
|
||||
for (let i = 0; i < count; i++) target.push((i * 3) & 0xffff);
|
||||
}
|
||||
|
||||
// `n` is a power of two: the masked elements are indexes of `perm`.
|
||||
function locals(n: number): string {
|
||||
const perm: number[] = [];
|
||||
for (let i = 0; i < n; i++) perm.push((i * 37 + 11) & (n - 1));
|
||||
const copy: number[] = [];
|
||||
pushAll(copy, n);
|
||||
const s1 = scale(copy, 5);
|
||||
return `locals ${chase(perm, 0, 1000)} ${s1} ${copy[n - 1]} ${copy.length} ${sum(perm)}`;
|
||||
}
|
||||
|
||||
// Read only through the element reader (`a[i] ?? 0`) and states.
|
||||
function readSparse(values: number[], at: number): string {
|
||||
return `${values[at] ?? -1} ${at in values} ${values.length}`;
|
||||
}
|
||||
|
||||
// The same helper receives an array of other numbers too: the arrays it
|
||||
// can reach keep doubles.
|
||||
function mean(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s += values[i]!;
|
||||
return s / values.length;
|
||||
}
|
||||
|
||||
// A store through the parameter that is not an int32.
|
||||
function halve(values: number[]): void {
|
||||
for (let i = 0; i < values.length; i++) values[i] = values[i]! / 2;
|
||||
}
|
||||
|
||||
// A function used as a value keeps its parameters.
|
||||
function first(values: number[]): number {
|
||||
return values[0]!;
|
||||
}
|
||||
|
||||
let reassigned: number[] = [1, 2, 3];
|
||||
function regrow(n: number): number {
|
||||
reassigned = [];
|
||||
for (let i = 0; i < n; i++) reassigned.push(i & 15);
|
||||
let s = 0;
|
||||
for (let i = 0; i < reassigned.length; i++) s += reassigned[i]!;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Not a fresh array: the result of a call.
|
||||
function total(values: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < values.length; i++) s += values[i]!;
|
||||
return s;
|
||||
}
|
||||
|
||||
const awaited: number[] = [4, 5, 6];
|
||||
async function later(): Promise<number> {
|
||||
return awaited[0]! + awaited.length;
|
||||
}
|
||||
|
||||
// A helper whose record result its callers only project: the backend may
|
||||
// call its result-slot entry instead, so its parameter keeps doubles.
|
||||
interface Span {
|
||||
lo: number;
|
||||
hi: number;
|
||||
}
|
||||
function spanAt(values: number[], i: number): Span {
|
||||
return { lo: values[i]!, hi: values[i + 1]! };
|
||||
}
|
||||
const spans: number[] = [5, 9, 2, 7, 11];
|
||||
function spanTotal(): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const span = spanAt(spans, i);
|
||||
total += span.hi * 10 - span.lo;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const ints: number[] = [3, 4, 5];
|
||||
const fractions: number[] = [0.5, 1.5, 2.5];
|
||||
const halved: number[] = [8, 6, 4];
|
||||
halve(halved);
|
||||
const pick: (values: number[]) => number = first;
|
||||
const viaValue: number[] = [9, 8, 7];
|
||||
|
||||
// An index that calls a function reassigning the global: the read uses the
|
||||
// array the global held before the index ran, as in Node.
|
||||
let swapped: number[] = [1, 2, 3];
|
||||
function swapIn(): number {
|
||||
swapped = [];
|
||||
for (let i = 0; i < 4; i++) swapped.push(i + 10);
|
||||
return 1;
|
||||
}
|
||||
function fillSwapped(n: number): number {
|
||||
swapped = [];
|
||||
for (let i = 0; i < n; i++) swapped.push(i & 15);
|
||||
let s = 0;
|
||||
for (let i = 0; i < swapped.length; i++) s += swapped[i]!;
|
||||
return s;
|
||||
}
|
||||
function readSwapped(): number | undefined {
|
||||
return swapped[swapIn()];
|
||||
}
|
||||
|
||||
console.log(`fnv ${fnv(data)} ${fnv(data)}`);
|
||||
const h = histogram(data, 10);
|
||||
console.log(`hist ${h.length} ${h[0]} ${h[9]} ${total(h)}`);
|
||||
console.log(locals(128));
|
||||
console.log(readSparse(data, 5), readSparse(data, 5000));
|
||||
console.log(`mean ${mean(ints)} ${mean(fractions)}`);
|
||||
console.log(`halved ${halved[0]} ${halved[2]} ${halved.length}`);
|
||||
console.log(`value ${pick(viaValue)} ${viaValue.length}`);
|
||||
console.log(`regrow ${regrow(40)} ${reassigned.length} ${reassigned[39]} ${regrow(5)}`);
|
||||
console.log(`span ${spanTotal()}`);
|
||||
console.log(`swap ${fillSwapped(6)} ${readSwapped()} ${swapped[1]}`);
|
||||
void later().then((v) => console.log(`later ${v}`));
|
||||
@@ -0,0 +1,139 @@
|
||||
"use strict";
|
||||
// Object.freeze, Object.seal and Object.preventExtensions next to JavaScript
|
||||
// classes whose fields all exist from construction. The integrity state of
|
||||
// a class instance lives beside its fields, so classes keep fixed layouts
|
||||
// whether or not a program freezes values; frozen instances still refuse
|
||||
// writes and additions as in Node.
|
||||
|
||||
class Point {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
norm() {
|
||||
return Math.abs(this.x) + Math.abs(this.y);
|
||||
}
|
||||
}
|
||||
|
||||
class Point3 extends Point {
|
||||
constructor(x, y, z) {
|
||||
super(x, y);
|
||||
this.z = z;
|
||||
}
|
||||
norm() {
|
||||
return super.norm() + Math.abs(this.z);
|
||||
}
|
||||
}
|
||||
|
||||
class Status {
|
||||
constructor() {
|
||||
this.value = "valid";
|
||||
}
|
||||
dirty() {
|
||||
if (this.value === "valid") this.value = "dirty";
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {any} target @param {string} key @param {unknown} value */
|
||||
function put(target, key, value) {
|
||||
target[key] = value;
|
||||
}
|
||||
|
||||
/** @param {any} value */
|
||||
function describe(value) {
|
||||
const keys = Object.keys(value);
|
||||
const parts = [];
|
||||
for (const key in value) parts.push(`${key}=${String(value[key])}`);
|
||||
return `${keys.join(",")} | ${parts.join(" ")} | ${JSON.stringify(value)}`;
|
||||
}
|
||||
|
||||
/** @param {() => void} fn */
|
||||
function attempt(label, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(label, "ok");
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof TypeError ? "TypeError" : "other", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// The zod shape: freeze whatever value a result carries. Here it is always
|
||||
// plain data, never a class instance.
|
||||
/** @param {{ status: string, value: any }} data */
|
||||
function freezeResult(data) {
|
||||
if (data.status === "valid") data.value = Object.freeze(data.value);
|
||||
return data;
|
||||
}
|
||||
|
||||
const results = [
|
||||
freezeResult({ status: "valid", value: { a: 1, list: [1, 2] } }),
|
||||
freezeResult({ status: "valid", value: [3, 4] }),
|
||||
freezeResult({ status: "aborted", value: { b: 2 } }),
|
||||
freezeResult({ status: "valid", value: "text" }),
|
||||
];
|
||||
for (const result of results) console.log(result.status, Object.isFrozen(result.value), JSON.stringify(result.value));
|
||||
/** @param {() => void} fn */
|
||||
function throws(fn) {
|
||||
try {
|
||||
fn();
|
||||
return false;
|
||||
} catch (e) {
|
||||
return e instanceof TypeError;
|
||||
}
|
||||
}
|
||||
console.log(throws(() => put(results[0].value, "a", 9)), throws(() => put(results[2].value, "b", 3)));
|
||||
console.log(JSON.stringify(results[2].value));
|
||||
|
||||
// Class instances that only ever travel through untyped code.
|
||||
const status = new Status();
|
||||
status.dirty();
|
||||
const p = new Point(1, -2);
|
||||
const q = new Point3(3, 4, -5);
|
||||
put(p, "tag", "p");
|
||||
put(q, "w", 6);
|
||||
console.log(describe(status), status.value);
|
||||
console.log(describe(p), p.norm());
|
||||
console.log(describe(q), q.norm());
|
||||
console.log(Object.isFrozen(p), Object.isExtensible(q), Object.isSealed(status));
|
||||
|
||||
// Statically typed freeze, seal and preventExtensions of instances.
|
||||
const frozen = new Point(5, 6);
|
||||
Object.freeze(frozen);
|
||||
attempt("frozen field write", () => {
|
||||
/** @type {Point} */ (frozen).x = 50;
|
||||
});
|
||||
attempt("frozen field write untyped", () => put(frozen, "y", 60));
|
||||
attempt("frozen addition", () => put(frozen, "extra", 1));
|
||||
console.log(describe(frozen), Object.isFrozen(frozen), Object.isSealed(frozen), Object.isExtensible(frozen));
|
||||
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(frozen, "x")));
|
||||
|
||||
const sealed = new Point3(1, 2, 3);
|
||||
put(sealed, "before", true);
|
||||
Object.seal(sealed);
|
||||
attempt("sealed field write", () => {
|
||||
sealed.z = 30;
|
||||
});
|
||||
attempt("sealed added write", () => put(sealed, "before", false));
|
||||
attempt("sealed addition", () => put(sealed, "after", 1));
|
||||
console.log(describe(sealed), Object.isFrozen(sealed), Object.isSealed(sealed), Object.isExtensible(sealed));
|
||||
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(sealed, "z")));
|
||||
|
||||
const closed = new Status();
|
||||
Object.preventExtensions(closed);
|
||||
closed.dirty();
|
||||
attempt("closed addition", () => put(closed, "more", 1));
|
||||
console.log(describe(closed), Object.isFrozen(closed), Object.isExtensible(closed));
|
||||
|
||||
// A subclass instance frozen through its base type.
|
||||
/** @type {Point} */
|
||||
const asBase = new Point3(7, 8, 9);
|
||||
Object.freeze(asBase);
|
||||
attempt("frozen subclass field", () => put(asBase, "z", 90));
|
||||
console.log(describe(asBase), asBase.norm(), Object.isFrozen(asBase));
|
||||
|
||||
// Fresh instances of the same classes stay writable.
|
||||
const fresh = new Point3(0, 0, 0);
|
||||
fresh.x = 1;
|
||||
put(fresh, "y", 2);
|
||||
put(fresh, "added", 3);
|
||||
console.log(describe(fresh), fresh.norm(), Object.isFrozen(fresh));
|
||||
@@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
// Getters and setters on JavaScript classes. An accessor whose name no
|
||||
// field uses lives on the prototype beside the fields; an accessor that
|
||||
// shares a field's name decides what an assignment does. Both behave as in
|
||||
// Node, for direct use and for instances passed to untyped code.
|
||||
|
||||
class LazyPath {
|
||||
constructor(parent, value, path, key) {
|
||||
this._cachedPath = [];
|
||||
this.parent = parent;
|
||||
this.data = value;
|
||||
this._path = path;
|
||||
this._key = key;
|
||||
}
|
||||
get path() {
|
||||
if (!this._cachedPath.length) {
|
||||
if (Array.isArray(this._key)) this._cachedPath.push(...this._path, ...this._key);
|
||||
else this._cachedPath.push(...this._path, this._key);
|
||||
}
|
||||
return this._cachedPath;
|
||||
}
|
||||
}
|
||||
|
||||
class Temperature {
|
||||
constructor(celsius) {
|
||||
this.celsius = celsius;
|
||||
}
|
||||
get fahrenheit() {
|
||||
return this.celsius * 1.8 + 32;
|
||||
}
|
||||
set fahrenheit(value) {
|
||||
this.celsius = (value - 32) / 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
class Labeled extends Temperature {
|
||||
constructor(celsius, label) {
|
||||
super(celsius);
|
||||
this.label = label;
|
||||
}
|
||||
get description() {
|
||||
return `${this.label}: ${this.celsius.toFixed(1)}C`;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {any} target @param {string} key @param {unknown} value */
|
||||
function put(target, key, value) {
|
||||
target[key] = value;
|
||||
}
|
||||
|
||||
/** @param {any} value */
|
||||
function describe(value) {
|
||||
const parts = [];
|
||||
for (const key in value) parts.push(key);
|
||||
return `${Object.keys(value).join(",")} | ${parts.join(",")} | ${JSON.stringify(value)}`;
|
||||
}
|
||||
|
||||
/** @param {any} value @param {string} key */
|
||||
function probe(value, key) {
|
||||
return `${key}: in=${key in value} own=${Object.prototype.hasOwnProperty.call(value, key)} value=${JSON.stringify(value[key])}`;
|
||||
}
|
||||
|
||||
const root = { common: true };
|
||||
const paths = [];
|
||||
for (let i = 0; i < 3; i++) paths.push(new LazyPath(root, { n: i }, ["items"], i));
|
||||
paths.push(new LazyPath(root, null, ["a"], ["b", "c"]));
|
||||
for (const item of paths) console.log(item.path.join("."), item.data && item.data.n, item.parent === root);
|
||||
console.log(describe(paths[0]));
|
||||
console.log(probe(paths[1], "path"), probe(paths[1], "_key"));
|
||||
put(paths[2], "extra", 1);
|
||||
console.log(describe(paths[2]), paths[2].path.length);
|
||||
|
||||
const t = new Temperature(100);
|
||||
console.log(t.fahrenheit, describe(t));
|
||||
t.fahrenheit = 32;
|
||||
console.log(t.celsius, t.fahrenheit);
|
||||
put(t, "fahrenheit", 212);
|
||||
console.log(t.celsius, probe(t, "fahrenheit"), probe(t, "celsius"));
|
||||
put(t, "note", "added");
|
||||
console.log(describe(t));
|
||||
|
||||
const l = new Labeled(20, "room");
|
||||
console.log(l.description, l.fahrenheit, describe(l));
|
||||
l.fahrenheit = 50;
|
||||
console.log(l.description, probe(l, "description"));
|
||||
@@ -17,7 +17,7 @@ function tail(p: string): string {
|
||||
return p.slice(i + 1);
|
||||
}
|
||||
|
||||
const scratch = "tmp-957-" + tail(process.argv[1]);
|
||||
const scratch = "tmp-957-" + tail(process.argv[1]) + "-" + process.pid;
|
||||
if (fs.existsSync(scratch)) {
|
||||
fs.rmSync(scratch + "/a.txt");
|
||||
fs.rmdirSync(scratch);
|
||||
|
||||
@@ -32,7 +32,7 @@ function freshDir(dir: string): void {
|
||||
}
|
||||
mkdirSync(dir);
|
||||
}
|
||||
const scratch = "tmp-992-" + tail(process.argv[1]);
|
||||
const scratch = "tmp-992-" + tail(process.argv[1]) + "-" + process.pid;
|
||||
freshDir(scratch);
|
||||
|
||||
const file = scratch + "/notes.txt";
|
||||
|
||||
@@ -26,7 +26,7 @@ function freshDir(dir: string): void {
|
||||
}
|
||||
mkdirSync(dir);
|
||||
}
|
||||
const scratch = "tmp-993-" + tail(process.argv[1]);
|
||||
const scratch = "tmp-993-" + tail(process.argv[1]) + "-" + process.pid;
|
||||
freshDir(scratch);
|
||||
|
||||
console.log(readdirSync(scratch).length); // fresh dir is empty
|
||||
|
||||
@@ -30,7 +30,7 @@ function freshDir(dir: string): void {
|
||||
}
|
||||
mkdirSync(dir);
|
||||
}
|
||||
const scratch = "tmp-994-" + tail(process.argv[1]);
|
||||
const scratch = "tmp-994-" + tail(process.argv[1]) + "-" + process.pid;
|
||||
freshDir(scratch);
|
||||
|
||||
let caught = 0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user