Merge main

This commit is contained in:
Chris Tate
2026-10-23 11:56:40 -05:00
136 changed files with 3512 additions and 699 deletions
+1 -1
View File
@@ -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
-1
View File
@@ -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));
+1 -1
View File
@@ -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");
+21 -9
View File
@@ -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)
);
}
+3 -3
View File
@@ -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 {
+4 -21
View File
@@ -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)";
}
+48 -3
View File
@@ -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.
+359 -65
View File
@@ -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":
+391 -52
View File
@@ -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 ?? [];
+72 -30
View File
@@ -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:
-134
View File
@@ -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",
+6 -27
View File
@@ -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 {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7264-array-dense-paired-loops.ts"
],
"diags": []
}
@@ -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": []
}
@@ -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": []
}
@@ -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": []
}
@@ -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": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7390-record-key-read-presence.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7543-var-list-declarator-order.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7545-string-regex-checked-arguments.js"
],
"diags": []
}
@@ -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": []
}
@@ -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": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7553-var-for-in-of-zero-iterations.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7580-int32-arrays-across-calls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7790-js-class-integrity-layouts.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7791-js-class-accessor-layouts.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/class-brand-probe-keys.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/fetch-headers-copy-cache.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/string-last-index-of-ascii.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts"
],
"diags": []
}
+4
View File
@@ -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
View File
@@ -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;
+16
View File
@@ -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
+2 -3
View File
@@ -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",
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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"));
+1 -1
View File
@@ -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"));
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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