Merge the restacked base into ni/i2-handles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	packages/compiler/src/executable/executable-cache.ts
#	packages/compiler/src/index.ts
This commit is contained in:
Malte Ubl
2026-10-27 05:41:01 -07:00
391 changed files with 24294 additions and 3372 deletions
+4 -2
View File
@@ -10,12 +10,14 @@ process.stdout.write("native write 1\n");
console.error("native error 1");
callSync("./stdio.cjs", "big", 200000);
process.stdout.write(`${"n".repeat(200000)}\n`);
callSync("./stdio.cjs", "later", 5);
// The island timer (50 ms) and the native timer (100 ms) are due well
// after this script and the immediates finish, on a loaded host too.
callSync("./stdio.cjs", "later", 50);
setTimeout(() => {
console.log("native timer log");
callSync("./stdio.cjs", "write", "2");
console.error("native timer error");
}, 10);
}, 100);
setImmediate(() => console.log("native immediate log"));
process.on("exit", () => console.log("native exit listener log"));
console.log("native log 2");
@@ -0,0 +1,81 @@
// Non-null assertions on array element reads whose index is out of bounds
// or names a hole, used as operands of operators that convert them. `!`
// performs no runtime check, so the read is undefined, as in Node: NaN in
// arithmetic, false in comparisons, "undefined" in strings.
const values = [3, 1, 4, 1, 5];
const words = ["alpha", "beta"];
const flags = [true, false];
const holey: number[] = [];
holey[3] = 7;
function show(v: unknown): string {
return Object.is(v, -0) ? "-0" : String(v);
}
function line(label: string, parts: unknown[]): void {
console.log(label.padEnd(10), parts.map(show).join(" "));
}
for (const k of [0, 4, 5, -1, 1.5, 100]) {
line(`k=${k}`, [
values[k]! + 1,
values[k]! * 2,
-values[k]!,
values[k]! | 0,
values[k]! < 3,
values[k]! >= 1,
values[k]! === 1,
values[k]! !== 1,
`${values[k]!}`,
"v=" + values[k]!,
Math.max(values[k]!, 0),
words[k]! + "!",
`<${words[k]!}>`,
words[k]! < "b",
!flags[k]!,
]);
}
line("holes", [holey[0]! + 1, holey[3]! + 1, `${holey[1]!}`, holey[2]! === undefined, holey[9]! > 0]);
// `+` with an untyped partner concatenates or adds when it runs.
const untypedText: any = "x";
const untypedNumber: any = 5;
for (const k of [1, 9]) {
line(`untyped ${k}`, [
values[k]! + untypedText,
untypedText + values[k]!,
values[k]! + untypedNumber,
words[k]! + untypedNumber,
words[k]! + untypedText,
]);
}
let sum = 0;
let text = "";
for (let i = 0; i <= values.length; i++) {
sum += values[i]!;
text += words[i % 3]!;
text += ",";
}
line("compound", [sum, text]);
// A counter array: an index past the end reads NaN, and the store widens
// the array to doubles.
function counter(n: number, m: number): number[] {
const a = new Array(n).fill(0) as number[];
for (let i = 0; i < 6000; i++) a[i % m] = a[i % m]! + 1;
return [a[0]!, a[m - 1]!, a.length, a[n + 1]! + 0];
}
line("counter", counter(5000, 5000));
line("past", counter(5000, 5003));
line("zero", counter(4, 0));
const grid = [
[1, 2],
[3, 4],
];
let total = 0;
for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) total += grid[r % 2]![c]! * 1;
line("grid", [total]);
@@ -0,0 +1,142 @@
// Member reads and writes through non-null asserted array element reads.
// `!` performs no runtime check: a missing element is undefined, so the
// member access throws Node's TypeError, which the program can catch.
class Cell {
count = 0;
label: string;
constructor(label: string) {
this.label = label;
}
bump(): number {
return ++this.count;
}
}
function cell(label: string): Cell {
return new Cell(label);
}
const cells = [cell("a"), cell("b")];
const words = ["alpha", "beta"];
const grid = [
[1, 2],
[3, 4],
];
const maps = [new Map<string, number>([["x", 1]])];
const holey: Cell[] = [];
holey[2] = cell("z");
let reads = 0;
function at(n: number): number {
reads++;
return n;
}
function attempt(label: string, f: () => unknown): void {
try {
console.log(label.padEnd(12), "ok", f());
} catch (e) {
const error = e as Error;
console.log(label.padEnd(12), error instanceof TypeError, error.message);
}
}
for (const k of [0, 1, 2, -1]) {
attempt(`label ${k}`, () => cells[k]!.label);
attempt(`bump ${k}`, () => cells[k]!.bump());
attempt(`set ${k}`, () => (cells[k]!.label = "set"));
attempt(`count++ ${k}`, () => cells[k]!.count++);
attempt(`length ${k}`, () => words[k]!.length);
attempt(`upper ${k}`, () => words[k]!.toUpperCase());
attempt(`grid ${k}`, () => grid[k]![0]);
attempt(`row ${k}`, () => {
const j = 1;
return grid[k]![j];
});
attempt(`push ${k}`, () => grid[k]!.push(9));
attempt(`map ${k}`, () => maps[k]!.get("x"));
}
attempt("hole", () => holey[0]!.label);
attempt("hole value", () => holey[2]!.label);
attempt("order", () => cells[at(7)]!.count + at(1));
console.log("reads", reads);
let total = 0;
try {
for (let i = 0; i < 4; i++) total += grid[i]![0]!;
} catch (e) {
console.log("loop", (e as Error).message, total);
}
console.log(cells.map((c) => `${c.label}:${c.count}`).join(" "));
// A write through a missing element evaluates the key and the value before
// it throws; an error the value throws is the one that propagates.
const events: string[] = [];
const note = (event: string, value: number): number => {
events.push(event);
return value;
};
attempt("write order", () => {
cells[note("index", 7)]!.count = note("value", 1);
});
attempt("write key order", () => {
grid[note("row", 9)]![note("column", 0)] = note("cell", 5);
});
attempt("write literal key", () => {
grid[9]![0] = note("cell 0", 5);
});
attempt("value throws", () => {
cells[7]!.count = (() => {
throw new Error("from the value");
})();
});
attempt("present write", () => {
cells[0]!.count = note("present", 41) + 1;
return cells[0]!.count;
});
console.log("events", events.join(","));
// The array is read before an index that replaces it.
let current = [new Cell("first")];
const replace = (): number => {
current = [];
return 0;
};
attempt("replaced read", () => current[replace()]!.label);
current = [new Cell("second")];
attempt("replaced write", () => {
current[replace()]!.label = "written";
});
let names = ["kept"];
attempt("replaced string", () => `${names[(names = [], 0)]!}`);
console.log(current.length, names.length);
// The element is read before the key and the value: a value that replaces,
// removes or adds it writes to, or misses, the element the target read, and
// a key the value reassigns is the one read before it.
const slots = [new Cell("p"), new Cell("q")];
const original = slots[0]!;
attempt("value replaces", () => {
slots[0]!.label = ((slots[0] = new Cell("r")), "set");
return `${original.label} ${slots[0]!.label}`;
});
const tail = slots[1]!;
attempt("value removes", () => {
slots[1]!.label = (slots.pop(), "gone");
return `${tail.label} ${slots.length}`;
});
attempt("value adds", () => {
slots[1]!.label = (slots.push(new Cell("s")), "added");
});
const table: number[][] = [[1, 2]];
let column = 0;
attempt("value moves key", () => {
table[0]![column] = ((column = 1), 5);
return table[0]!.join(",");
});
column = 0;
attempt("value moves missing key", () => {
table[4]![column] = ((column = 1), 5);
});
console.log(slots.length, column);
@@ -0,0 +1,69 @@
// Unbounded recursion through each kind of call cycle raises Node's
// RangeError, and the program recovers and recurses again afterwards.
import { Even, Odd, Tree } from "./shapes.ts";
function probe(name: string, run: (n: number) => number): void {
try {
console.log(name, "returned", run(10_000_000));
} catch (e) {
console.log(name, e instanceof RangeError, (e as Error).message);
}
console.log(name, "after", run(100));
}
function down(n: number): number {
return n === 0 ? 0 : 1 + down(n - 1);
}
probe("direct", down);
function ping(n: number): number {
return n === 0 ? 0 : 1 + pong(n - 1);
}
function pong(n: number): number {
return n === 0 ? 0 : 2 + ping(n - 1);
}
probe("mutual", ping);
function first(n: number): number {
return n === 0 ? 0 : 1 + second(n - 1);
}
function second(n: number): number {
return n === 0 ? 0 : 1 + third(n - 1);
}
function third(n: number): number {
return n === 0 ? 0 : 1 + fourth(n - 1);
}
function fourth(n: number): number {
return n === 0 ? 0 : 1 + fifth(n - 1);
}
function fifth(n: number): number {
return n === 0 ? 0 : 1 + first(n - 1);
}
probe("ring", first);
function viaValue(n: number, again: (m: number) => number): number {
return n === 0 ? 0 : 1 + again(n - 1);
}
const callback = (m: number): number => viaValue(m, callback);
probe("closure", callback);
function viaMap(n: number): number {
return n === 0 ? 0 : 1 + [n - 1].map(viaMap)[0]!;
}
probe("library callback", viaMap);
const even = new Even();
const odd = new Odd();
even.next = odd;
odd.next = even;
probe("virtual", (n) => even.depth(n));
probe("constructor", (n) => {
let tree: Tree | null = new Tree(n);
let count = 0;
while (tree !== null) {
count++;
tree = tree.child;
}
return count;
});
@@ -0,0 +1,24 @@
// Classes in their own module, so their names carry the module prefix.
export abstract class Link {
next: Link | null = null;
abstract depth(n: number): number;
}
export class Even extends Link {
depth(n: number): number {
return n === 0 ? 0 : 1 + this.next!.depth(n - 1);
}
}
export class Odd extends Link {
depth(n: number): number {
return n === 0 ? 0 : 2 + this.next!.depth(n - 1);
}
}
export class Tree {
child: Tree | null;
constructor(n: number) {
this.child = n === 0 ? null : new Tree(n - 1);
}
}
@@ -0,0 +1,162 @@
// Untyped for-of and for await loops over generator objects: values,
// completion values, early exits, errors, other iterables through the same
// loop, and the microtask order of each step.
function* numbers(n) {
for (let i = 0; i < n; i++) yield i;
return "done";
}
function* mixed() {
yield "a";
yield { k: 1 };
yield undefined;
yield [1, 2];
yield Promise.resolve(7);
return 99;
}
async function* asyncNumbers(n) {
for (let i = 0; i < n; i++) yield i * 10;
return "async done";
}
async function* asyncMixed() {
yield "x";
yield Promise.resolve(8);
yield null;
}
function collect(it) {
const out = [];
for (const x of it) out.push(x);
return out;
}
async function collectAsync(it) {
const out = [];
for await (const x of it) out.push(x);
return out;
}
const show = (v) => JSON.stringify(v, (k, x) => (x instanceof Promise ? "promise" : x === undefined ? "undef" : x));
async function main() {
// The same loops over generators, arrays, Maps, strings and custom iterators.
console.log("sync", show(collect(numbers(4))), show(collect(mixed())));
console.log("sync other", show(collect([1, 2])), show(collect(new Map([[1, 2]]))), show(collect("hé")));
const custom = {
[Symbol.iterator]() {
let i = 0;
return { next: () => ({ value: i * i, done: i++ > 2 }) };
},
};
console.log("sync custom", show(collect(custom)));
console.log("async", show(await collectAsync(asyncNumbers(3))), show(await collectAsync(asyncMixed())));
console.log("async over sync", show(await collectAsync(mixed())), show(await collectAsync(numbers(3))));
console.log("async other", show(await collectAsync([Promise.resolve(1), 2])), show(await collectAsync(custom)));
const asyncCustom = {
[Symbol.asyncIterator]() {
let i = 0;
return { next: async () => ({ value: `c${i}`, done: i++ > 1 }) };
},
};
console.log("async custom", show(await collectAsync(asyncCustom)));
// Symbol.asyncIterator returning a sync generator: each result is awaited,
// its value is not, so a yielded promise reaches the loop as a promise.
const viaAsyncIterator = { [Symbol.asyncIterator]: () => mixed() };
const seen = [];
for await (const x of viaAsyncIterator) seen.push(x instanceof Promise ? `promise ${await x}` : x);
console.log("async iterator sync", show(seen));
// A generator that has started keeps its position; one that finished stays done.
const started = numbers(5);
started.next();
console.log("started", show(collect(started)), show(started.next()));
// Early exits close the generator: finally blocks run once.
function* guarded(label) {
try {
yield 1;
yield 2;
} finally {
console.log(label, "finally");
}
}
const pick = (v) => v;
for (const x of pick(guarded("break"))) {
console.log("break at", x);
break;
}
try {
for (const x of pick(guarded("throw"))) throw new Error(`body ${x}`);
} catch (e) {
console.log("caught", e.message);
}
const g = pick(guarded("after"));
for (const x of g) {
if (x === 1) break;
}
console.log("after break", show(g.next()));
async function* aguarded(label) {
try {
yield 1;
yield 2;
} finally {
console.log(label, "finally");
}
}
for await (const x of pick(aguarded("abreak"))) {
console.log("abreak at", x);
break;
}
for await (const x of pick(guarded("sync abreak"))) {
console.log("sync abreak at", x);
break;
}
// Errors from the generator body reach the loop.
function* failing() {
yield 1;
throw new TypeError("generator failed");
}
async function* afailing() {
yield 1;
throw new RangeError("async generator failed");
}
try {
for (const x of pick(failing())) console.log("failing got", x);
} catch (e) {
console.log("caught", e.name, e.message);
}
try {
for await (const x of pick(afailing())) console.log("afailing got", x);
} catch (e) {
console.log("caught", e.name, e.message);
}
// Microtask order: each step against a promise chain ticking alongside.
const ticker = (label, n) => {
let p = Promise.resolve();
for (let i = 0; i < n; i++) p = p.then(() => console.log(label, "tick", i));
return p;
};
const consume = async (label, it) => {
for await (const x of it) console.log(label, "value", x);
console.log(label, "end");
};
await Promise.all([consume("sync", pick(numbers(3))), ticker("sync", 10)]);
await Promise.all([consume("async", pick(asyncNumbers(3))), ticker("async", 10)]);
await Promise.all([consume("promises", pick(mixed())), ticker("promises", 14)]);
// Stages chained through untyped parameters.
async function* double(src) {
for await (const x of src) yield x * 2;
}
async function* keepOdd(src) {
for await (const x of src) if (x % 4 !== 0) yield x;
}
let sum = 0;
for await (const x of keepOdd(double(asyncNumbers(50)))) sum += x;
console.log("stages", sum);
let syncSum = 0;
function* inc(src) {
for (const x of src) yield x + 1;
}
for (const x of inc(inc(numbers(100)))) syncSum += x;
console.log("sync stages", syncSum);
}
main();
@@ -0,0 +1,82 @@
// Untyped stages chained through generator objects: a sync generator source
// consumed with for await, async generator transforms, an async function
// consumer, and the same stages over arrays and custom iterables.
function run(source, transform, destination) {
return destination(transform(source()));
}
const N = 2000;
const total = await run(
function* () {
for (let i = 0; i < N; i++) yield i;
},
async function* (src) {
for await (const n of src) yield n * 2;
},
async function (src) {
let sum = 0;
for await (const n of src) sum += n;
return sum;
},
);
console.log("total", total);
async function* map(it, f) {
for await (const x of it) yield await f(x);
}
async function* take(it, n) {
if (n <= 0) return;
for await (const x of it) {
yield x;
if (--n === 0) return;
}
}
async function* source(n) {
try {
for (let i = 0; i < n; i++) yield i;
} finally {
console.log("source closed");
}
}
async function toArray(it) {
const out = [];
for await (const x of it) out.push(x);
return out;
}
const half = async (x) => x / 2;
console.log(await toArray(take(map(source(10), half), 3)));
console.log(await toArray(map([1, 2, 3], half)));
console.log(await toArray(map(new Set(["a", "b"]), async (s) => s.toUpperCase())));
const words = {
*[Symbol.iterator]() {
yield "x";
yield "y";
},
};
console.log(await toArray(map(words, async (s) => s + s)));
// A stage that switches iterables between calls of the same loop.
function* letters() {
yield "p";
yield "q";
}
async function* numbersAsync() {
yield 1;
yield 2;
}
const sources = [letters(), numbersAsync(), ["z"], letters()];
for (const s of sources) console.log(await toArray(s));
// Sync stages: generator to generator through untyped parameters.
function* inc(src) {
for (const x of src) yield x + 1;
}
function* evens(src) {
for (const x of src) if (x % 2 === 0) yield x;
}
function* range(n) {
for (let i = 0; i < n; i++) yield i;
return "range done";
}
let s = 0;
for (const x of evens(inc(range(1000)))) s += x;
console.log("sync", s, [...inc([10, 20])], Array.from(evens(range(7))));
@@ -0,0 +1,172 @@
// Async generators whose bodies suspend only at their own awaits and yields:
// requests queued while the body awaits, return() and throw() at a yield and
// during an await, finally blocks that await, yield* into another async
// generator, awaits of plain values and of settled and pending promises, a
// deep chain of stages, and the microtask order around them.
const log = [];
const tick = (label) => Promise.resolve().then(() => log.push(label));
const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 1));
async function* counter(n) {
try {
for (let i = 0; i < n; i++) {
const v = await (i % 2 === 0 ? i : Promise.resolve(i));
log.push(`yield ${v}`);
yield v;
}
return "done";
} finally {
log.push("counter finally");
await later(0);
log.push("counter finally after await");
}
}
// Requests queued before the body settles the first one.
{
const it = counter(3);
const requests = [];
for (let i = 0; i < 5; i++) requests.push(it.next());
const results = [];
for (const request of requests) results.push(await request);
console.log(JSON.stringify(results));
console.log(log.splice(0).join(" | "));
}
// return() at a yield runs the finally block, which awaits.
{
const it = counter(5);
console.log(JSON.stringify(await it.next()));
const ret = it.return("early");
tick("tick after return()");
console.log(JSON.stringify(await ret));
console.log(JSON.stringify(await it.next()));
console.log(log.splice(0).join(" | "));
}
// return() queued while the body awaits a pending promise.
async function* slow() {
try {
const v = await later(7);
yield v;
yield v + 1;
return "slow done";
} finally {
log.push("slow finally");
}
}
{
const it = slow();
const first = it.next();
const ret = it.return("stopped");
console.log(JSON.stringify(await first), JSON.stringify(await ret));
console.log(log.splice(0).join(" | "));
}
// throw() at a yield, caught by the body, which keeps yielding.
async function* catcher() {
for (let i = 0; ; i++) {
try {
yield i;
} catch (e) {
log.push(`caught ${e}`);
await null;
yield `recovered ${i}`;
return;
}
}
}
{
const it = catcher();
console.log(JSON.stringify(await it.next()));
console.log(JSON.stringify(await it.throw("boom")));
console.log(JSON.stringify(await it.next()));
console.log(log.splice(0).join(" | "));
}
// A body that throws after an await rejects its request and completes.
async function* failing() {
yield 1;
await later(0);
throw new Error("failed after await");
}
{
const it = failing();
console.log(JSON.stringify(await it.next()));
try {
await it.next();
} catch (e) {
console.log("rejected:", e.message);
}
console.log(JSON.stringify(await it.next()));
}
// yield* into another async generator, and break out of for await.
async function* inner() {
try {
yield "a";
yield await later("b");
yield "c";
} finally {
log.push("inner finally");
}
}
async function* outer() {
yield "start";
const r = yield* inner();
log.push(`inner returned ${r}`);
yield "end";
}
{
const seen = [];
for await (const x of outer()) {
seen.push(x);
if (x === "b") break;
}
console.log(seen.join(","));
console.log(log.splice(0).join(" | "));
}
// Stages chained through untyped values, interleaved with a promise chain.
async function* source(n) {
for (let i = 0; i < n; i++) yield i;
}
async function* map(it, f) {
for await (const x of it) yield await f(x);
}
async function* filter(it, p) {
for await (const x of it) if (p(x)) yield x;
}
{
const order = [];
const chain = (async () => {
for await (const x of filter(map(source(6), async (x) => x * 3), (v) => v % 2 === 0)) order.push(`item ${x}`);
})();
Promise.resolve()
.then(() => order.push("p1"))
.then(() => order.push("p2"))
.then(() => order.push("p3"));
await chain;
console.log(order.join(" "));
}
// A deep chain of pass-through stages.
async function* pass(it) {
for await (const x of it) yield x;
}
function stack(depth) {
return depth === 0 ? source(4) : pass(stack(depth - 1));
}
{
let sum = 0;
for await (const x of stack(500)) sum += x;
console.log("deep sum", sum);
}
// Generator objects keep their tag and protocol.
{
const g = counter(1);
console.log(Object.prototype.toString.call(g));
console.log(JSON.stringify(await g.next()), JSON.stringify(await g.next()));
log.splice(0);
}
@@ -0,0 +1,99 @@
// Typed async generators whose bodies suspend only at their own awaits and
// yields: number, string and object channels, values passed into next(),
// return() and throw() while the body awaits, finally blocks that await,
// yield* delegation, and an async function consuming them.
const log: string[] = [];
const later = <T>(value: T): Promise<T> => new Promise((resolve) => setTimeout(() => resolve(value), 1));
async function* numbers(n: number): AsyncGenerator<number, string, void> {
try {
for (let i = 0; i < n; i++) {
yield i % 2 === 0 ? i : await later(i);
}
return "numbers done";
} finally {
await later(0);
log.push("numbers finally");
}
}
async function* accumulate(): AsyncGenerator<number, number, number> {
let total = 0;
for (;;) {
const v = yield total;
if (v < 0) return total;
total += await Promise.resolve(v);
}
}
interface Point {
x: number;
y: number;
}
async function* points(n: number): AsyncGenerator<Point> {
for (let i = 0; i < n; i++) {
await null;
yield { x: i, y: i * i };
}
}
async function* labels(): AsyncGenerator<string, void, undefined> {
yield "first";
yield* (async function* () {
yield "inner one";
yield await later("inner two");
})();
yield "last";
}
async function sum(it: AsyncGenerator<number, string, void>): Promise<number> {
let s = 0;
for await (const v of it) s += v;
return s;
}
async function main(): Promise<void> {
console.log("sum", await sum(numbers(6)));
console.log(log.splice(0).join(" | "));
const acc = accumulate();
console.log(JSON.stringify(await acc.next(0)));
for (const v of [1, 2, 3]) console.log(JSON.stringify(await acc.next(v)));
console.log(JSON.stringify(await acc.next(-1)));
console.log(JSON.stringify(await acc.next(5)));
for await (const p of points(3)) console.log(p.x, p.y);
const words: string[] = [];
for await (const w of labels()) words.push(w);
console.log(words.join(", "));
const it = numbers(10);
console.log(JSON.stringify(await it.next()));
const pending = it.next();
const ret = it.return("returned");
console.log(JSON.stringify(await pending), JSON.stringify(await ret));
console.log(log.splice(0).join(" | "));
const thrower = numbers(4);
await thrower.next();
try {
await thrower.throw(new Error("thrown in"));
} catch (e) {
console.log("rejected:", (e as Error).message);
}
console.log(JSON.stringify(await thrower.next()));
console.log(log.splice(0).join(" | "));
const order: string[] = [];
const run = (async () => {
for await (const v of numbers(3)) order.push(`v${v}`);
})();
void Promise.resolve()
.then(() => order.push("m1"))
.then(() => order.push("m2"));
await run;
console.log(order.join(" "));
log.splice(0);
}
await main();
@@ -0,0 +1,96 @@
// Compressed zlib bytes over the vendored zlib, the copy Node bundles: deflate,
// raw deflate and gzip output match Node's for every level, strategy, memLevel,
// windowBits and dictionary, for text, binary and a 1 MiB input that slides the
// window, and for streams that flush and change parameters between writes.
import { createHash } from "node:crypto";
import {
constants,
createDeflate,
createGzip,
deflateRawSync,
deflateSync,
gunzipSync,
gzipSync,
inflateRawSync,
inflateSync,
type Deflate,
type Gzip,
type ZlibOptions,
} from "node:zlib";
const digest = (bytes: Buffer): string =>
`${bytes.length} ${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`;
const words = ["stream", "pipeline", "émetteur", "π", "buffer", "deflate", "\u0000", "window"];
const parts: string[] = [];
for (let i = 0; i < 3000; i++) parts.push(`${words[i % words.length]}-${(i * 7919) % 1000}`);
const text = Buffer.from(parts.join(" "), "utf8");
let seed = 0x2545f491;
const binary = Buffer.alloc(20000);
for (let i = 0; i < binary.length; i++) {
seed = (seed * 1103515245 + 12345) >>> 0;
binary[i] = i % 3 === 0 ? seed >>> 24 : (i >> 4) & 0xff;
}
const large = Buffer.alloc(1 << 20);
for (let i = 0; i < large.length; i++) large[i] = text[(i * 31) % text.length]! ^ ((i >> 12) & 7);
for (const level of [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) {
const deflated = deflateSync(text, { level });
console.log("deflate", level, digest(deflated), inflateSync(deflated).equals(text));
console.log("raw", level, digest(deflateRawSync(binary, { level })));
console.log("gzip", level, digest(gzipSync(text, { level })));
}
const variants: [string, ZlibOptions][] = [
["filtered", { strategy: constants.Z_FILTERED }],
["huffman only", { strategy: constants.Z_HUFFMAN_ONLY }],
["rle", { strategy: constants.Z_RLE }],
["fixed", { strategy: constants.Z_FIXED }],
["memLevel 1", { memLevel: 1 }],
["memLevel 9", { memLevel: 9 }],
["windowBits 9", { windowBits: 9 }],
["windowBits 12", { windowBits: 12 }],
["level 9 memLevel 9", { level: 9, memLevel: 9 }],
["dictionary", { dictionary: Buffer.from("stream pipeline buffer deflate window") }],
];
for (const [label, options] of variants) {
const deflated = deflateSync(text, options);
console.log(label, digest(deflated), digest(deflateRawSync(binary, options)));
console.log(label, "inflates", inflateSync(deflated, options).equals(text));
}
for (const level of [1, 6, 9]) {
const gz = gzipSync(large, { level });
console.log("1 MiB gzip", level, digest(gz), gunzipSync(gz).equals(large));
const raw = deflateRawSync(large, { level, windowBits: 10 });
console.log("1 MiB raw windowBits 10", level, digest(raw), inflateRawSync(raw, { windowBits: 10 }).equals(large));
}
function collect(stream: Deflate | Gzip, label: string, done: () => void): void {
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
stream.on("end", () => {
console.log(label, digest(Buffer.concat(chunks)));
done();
});
}
const flushed = createDeflate({ level: 6 });
collect(flushed, "stream sync flush", () => {
const changed = createGzip({ level: 1 });
collect(changed, "stream params", () => console.log("done"));
changed.write(text.subarray(0, 8000));
changed.flush(constants.Z_FULL_FLUSH, () => {
changed.params(9, constants.Z_DEFAULT_STRATEGY, () => {
changed.write(text.subarray(8000));
changed.end(binary);
});
});
});
flushed.write(text.subarray(0, 5000));
flushed.flush(constants.Z_SYNC_FLUSH, () => {
flushed.write(binary);
flushed.end(text.subarray(5000));
});
+69
View File
@@ -0,0 +1,69 @@
// Inflation over the vendored zlib's chunked inflate: inputs fed in pieces of
// 1 to 4096 bytes, output chunks of 64 bytes to 64 KiB, raw windowBits 9 to 15,
// concatenated gzip members, back-references across a 1 MiB window, and a
// truncated input. The output and the error match Node's.
const zlib = require("node:zlib");
const { createHash } = require("node:crypto");
const digest = (bytes) => `${bytes.length} ${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`;
const parts = [];
for (let i = 0; i < 20000; i++) parts.push(`row ${i % 977} ${"abcdefgh".slice(i % 8)} ${(i * 2654435761) % 4093}`);
const text = Buffer.from(parts.join("\n"));
const large = Buffer.alloc(1 << 20);
for (let i = 0; i < large.length; i++) large[i] = text[(i * 13) % text.length] ^ ((i >> 15) & 3);
const gz = zlib.gzipSync(large, { level: 6 });
for (const chunkSize of [64, 1024, 16384, 65536]) {
console.log("gunzip chunkSize", chunkSize, digest(zlib.gunzipSync(gz, { chunkSize })));
}
for (const windowBits of [9, 10, 12, 15]) {
const raw = zlib.deflateRawSync(text, { windowBits, level: 9 });
console.log("raw windowBits", windowBits, digest(zlib.inflateRawSync(raw, { windowBits })));
}
const members = Buffer.concat([zlib.gzipSync(text.subarray(0, 1000)), zlib.gzipSync(text.subarray(1000, 5000))]);
console.log("members", digest(zlib.gunzipSync(members)));
try {
zlib.inflateSync(zlib.deflateSync(text).subarray(0, 2000));
} catch (e) {
console.log("truncated", e.code, e.errno, e.message);
}
function feed(input, pieceSize, outSize, next) {
const inflate = zlib.createGunzip({ chunkSize: outSize });
const hash = createHash("sha256");
let length = 0;
inflate.on("data", (chunk) => {
hash.update(chunk);
length += chunk.length;
});
inflate.on("end", () => {
console.log("stream piece", pieceSize, "chunk", outSize, length, hash.digest("hex").slice(0, 16));
next();
});
let offset = 0;
const pump = () => {
while (offset < input.length) {
const piece = input.subarray(offset, offset + pieceSize);
offset += pieceSize;
if (!inflate.write(piece)) {
inflate.once("drain", pump);
return;
}
}
inflate.end();
};
pump();
}
const head = zlib.gzipSync(large.subarray(0, 1 << 16), { level: 6 });
const runs = [
[head, 1, 64],
[gz, 7, 1024],
[gz, 333, 4096],
[gz, 4096, 65536],
];
const step = (i) => {
if (i < runs.length) feed(runs[i][0], runs[i][1], runs[i][2], () => step(i + 1));
};
step(0);
@@ -0,0 +1,38 @@
// indexOf with a start index on ASCII receivers: integral, fractional,
// negative, -0, NaN, Infinity and out-of-range starts; empty needles, which
// are found at the clamped start; needles longer than the rest; needles with
// non-ASCII characters and lone surrogates, which an ASCII receiver never
// contains; and receivers built at run time (slices, concatenations, readline
// style splits) next to non-ASCII ones. The results match Node's.
const line = `10.1.2.3 - - [24/Oct/2026:10:00:00 +0000] "GET /api/items/7?page=3 HTTP/1.1" 200 5120 "agent"`;
const starts = [0, 1, 2.7, -1, -0, NaN, Infinity, -Infinity, line.length - 1, line.length, line.length + 5, 1e21];
const needles = ["", '"', " ", "/", "api", "HTTP/1.1", line, line + "x", "é", "\ud83d", "\udca9", "💩"];
for (const needle of needles) {
const found = starts.map((start) => line.indexOf(needle, start));
console.log(JSON.stringify(needle), found.join(" "));
}
const quote = line.indexOf('"');
const space = line.indexOf(" ", quote);
const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1));
const close = line.indexOf('"', space);
console.log(quote, space, path, close, line.slice(close + 2, close + 5));
const built = ["alpha", "beta", "gamma"].join("/") + "/" + String(42);
for (let i = 0; i <= built.length + 1; i++) {
console.log(i, built.indexOf("/", i), built.indexOf("a", i), built.indexOf("", i), built.indexOf("42", i));
}
const mixed = "café/naïve/ascii/end";
const ascii = mixed.slice(mixed.indexOf("ascii"));
console.log(ascii, ascii.indexOf("/", 1), ascii.indexOf("end", 3), mixed.indexOf("/", 5), mixed.indexOf("ascii", 6));
let count = 0;
let at = 0;
const csv = Array.from({ length: 200 }, (_, i) => `${i},${i * 3},row${i}`).join(",");
while ((at = csv.indexOf(",", at)) !== -1) {
count++;
at += 1.5;
}
console.log("commas", count);
@@ -0,0 +1,62 @@
// Untyped reads and writes of class instances at property sites: base and
// subclass instances, subclass instances returned where the base class is
// the inferred type, a subclass that redeclares an inherited field, a
// getter, fields set after construction and fields never set.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Tagged extends Point {
constructor(x, y, tag) {
super(x, y);
this.tag = tag;
}
}
class Shifted extends Point {
label = "s";
x = 100;
}
class Gauge {
get x() {
return 9;
}
}
class Late {
constructor(set) {
if (set) this.x = "late";
}
}
function make(i) {
if (i % 3 === 0) return new Point(i, -i);
if (i % 3 === 1) return new Tagged(i, i * 2, "t" + i);
return new Shifted(i, 0);
}
function readX(o) {
return o.x;
}
function readY(o) {
return o.y;
}
function writeX(o, v) {
o.x = v;
}
const values = [];
for (let i = 0; i < 9; i++) values.push(make(i));
values.push(new Gauge(), new Late(true), new Late(false), { x: "plain", y: 1 }, { y: 2, x: "swapped" });
let sum = 0;
for (let round = 0; round < 3; round++) {
for (const v of values) {
const x = readX(v);
sum += typeof x === "number" ? x : x === undefined ? 1000 : x.length;
const y = readY(v);
if (typeof y === "number") sum += y;
}
for (let i = 0; i < 9; i++) writeX(values[i], round * 10 + i);
}
console.log(sum);
console.log(values.map((v) => String(readX(v))).join(" "));
console.log(values.map((v) => String(readY(v))).join(" "));
writeX(values[1], "changed");
console.log(readX(values[1]), readX(values[2]), values[2].label, values[1].tag);
@@ -0,0 +1,32 @@
// Untyped property reads of class instances that typed code passes as their
// base class: subclass instances in a Point[] read their inherited field and
// their own field through an untyped module.
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const reader: {
readX(o: unknown): unknown;
readTag(o: unknown): unknown;
sum(list: unknown[]): number;
} = require("./reader.cjs");
class Point {
x: any;
constructor(x: any) {
this.x = x;
}
}
class Tagged extends Point {
tag: any;
constructor(x: any, tag: any) {
super(x);
this.tag = tag;
}
}
function make(i: number): Point {
return i % 2 === 0 ? new Point(i) : new Tagged(i * 10, "t" + i);
}
const list: Point[] = [];
for (let i = 0; i < 6; i++) list.push(make(i));
console.log(list.map((p) => String(reader.readX(p))).join(" "));
console.log(list.map((p) => String(reader.readTag(p))).join(" "));
console.log(reader.sum(list));
@@ -0,0 +1 @@
{ "type": "module" }
@@ -0,0 +1,11 @@
exports.readX = function (o) {
return o.x;
};
exports.readTag = function (o) {
return o.tag;
};
exports.sum = function (list) {
let s = 0;
for (let i = 0; i < list.length; i++) s += list[i].x;
return s;
};
+63
View File
@@ -0,0 +1,63 @@
// Builders whose groups hold instances of a class that extends Array,
// beside a builder whose group takes an arena. An Array subclass's
// instance is an array, so a group that allocates one keeps its objects on
// the heap; the results and their teardown match Node either way.
class Row extends Array<number> {
sum(): number {
let s = 0;
for (const v of this) s += v;
return s;
}
}
class Branch {
row: Row;
left: Branch | null;
right: Branch | null;
constructor(row: Row, left: Branch | null, right: Branch | null) {
this.row = row;
this.left = left;
this.right = right;
}
total(): number {
return this.row.sum() + (this.left ? this.left.total() : 0) + (this.right ? this.right.total() : 0);
}
}
class Pair {
left: Pair | null;
right: Pair | null;
constructor(left: Pair | null, right: Pair | null) {
this.left = left;
this.right = right;
}
count(): number {
return 1 + (this.left ? this.left.count() : 0) + (this.right ? this.right.count() : 0);
}
}
function row(n: number): Row {
const r = new Row();
for (let j = 0; j <= n; j++) r.push(j);
return r;
}
function branches(depth: number): Branch {
return depth === 0
? new Branch(row(depth), null, null)
: new Branch(row(depth), branches(depth - 1), branches(depth - 1));
}
function pairs(depth: number): Pair {
return depth === 0 ? new Pair(null, null) : new Pair(pairs(depth - 1), pairs(depth - 1));
}
let checks = 0;
for (let round = 0; round < 20; round++) {
const b = branches(6);
const p = pairs(8);
const r = row(round);
checks += b.total() + p.count() + r.sum() + r.length;
if (round === 0) console.log(b.total(), b.row.length, b.row instanceof Row, p.count(), r.sum());
}
console.log(checks);
+49
View File
@@ -0,0 +1,49 @@
// Groups with arrays: child lists that grow past their first storage,
// back references to parents and siblings, and lists of lists.
class Node {
parent: Node | undefined = undefined;
children: Node[] = [];
sibling: Node | undefined = undefined;
weight: number;
constructor(weight: number) {
this.weight = weight;
}
}
function wide(depth: number, fan: number, parent: Node | undefined, weight: number): Node {
const node = new Node(weight);
node.parent = parent;
if (depth > 0) {
for (let i = 0; i < fan; i++) node.children.push(wide(depth - 1, fan, node, weight * 3 + i));
for (let i = 0; i < fan; i++) node.children[i]!.sibling = node.children[(i + 1) % fan]!;
}
return node;
}
function weigh(node: Node): number {
let total = node.weight;
for (const child of node.children) total += weigh(child);
return total % 1000003;
}
let w = 0;
for (let round = 0; round < 30; round++) w = (w + weigh(wide(2 + (round % 3), 3 + (round % 20), undefined, round))) % 1000003;
console.log("weights", w);
function grid(rows: number, cols: number): number[][] {
const out: number[][] = [];
for (let r = 0; r < rows; r++) {
const row: number[] = [];
for (let c = 0; c < cols; c++) row.push(r * cols + c);
out.push(row);
}
return out;
}
let cells = 0;
for (let k = 1; k < 40; k++) {
const g = grid(k, 70);
cells += g.length + g[k - 1]![69]! + g[0]!.length;
}
console.log("cells", cells);
const t = wide(2, 4, undefined, 1);
console.log(t.children.length, t.children[1]!.parent === t, t.children[3]!.sibling === t.children[0], t.children[2]!.children[3]!.weight);
+30
View File
@@ -0,0 +1,30 @@
// A group too large for one arena: past its byte cap the rest of the
// group goes to the heap, and both parts are released together.
interface Cell {
index: number;
label: string;
next: Cell | null;
}
function list(n: number): Cell {
let head: Cell = { index: 0, label: "c0", next: null };
const first = head;
for (let i = 1; i < n; i++) {
const cell: Cell = { index: i, label: i % 1000 === 0 ? `c${i}` : "c", next: null };
head.next = cell;
head = cell;
}
return first;
}
function walk(c: Cell | null): number {
let total = 0;
let labels = 0;
for (let at = c; at !== null; at = at.next) {
total += at.index;
if (at.label.length > 1) labels++;
}
return total + labels;
}
for (let round = 0; round < 3; round++) console.log("cells", walk(list(900000 + round)));
+72
View File
@@ -0,0 +1,72 @@
// Values built in a group that leave it: handed to callbacks that keep
// them, stored by a method into a module list, kept from a getter, pushed
// into a caller's list, or captured by a returned closure. None of those
// groups takes an arena; the one whose values stay in the frame does.
class Node {
kids: Node[] = [];
label: string;
constructor(label: string) {
this.label = label;
}
get first(): Node {
return this.kids[0]!;
}
register(): void {
registry.push(this);
}
}
const registry: Node[] = [];
const saved: Node[] = [];
const held = new Map<number, Node>();
function build(depth: number, label: string): Node {
const n = new Node(label);
for (let i = 0; depth > 0 && i < 3; i++) n.kids.push(build(depth - 1, `${label}.${i}`));
return n;
}
function each(depth: number, cb: (n: Node) => void): number {
const t = build(depth, "e");
cb(t);
return t.kids.length;
}
function visit(n: Node, cb: (x: Node) => void): void {
cb(n);
for (const k of n.kids) visit(k, cb);
}
function forward(depth: number, cb: (x: Node) => void): number {
const t = build(depth, "f");
visit(t, cb);
return t.kids.length;
}
function buildInto(sink: Node[], depth: number): number {
const n = build(depth, "s");
sink.push(n.kids[0]!);
return n.kids.length;
}
function closureOf(depth: number): () => string {
const n = build(depth, "c");
return () => n.kids[1]!.label;
}
let total = 0;
for (let r = 0; r < 8; r++) {
total += each(2, (n) => saved.push(n.kids[1]!));
total += forward(2, (x) => {
if (x.label.endsWith(".2")) saved.push(x);
});
const t = build(2, `t${r}`);
t.kids.forEach((k) => {
if (k.label.endsWith(".0")) saved.push(k);
});
const u = build(2, `u${r}`);
for (const k of u.kids) if (k.label.endsWith(".1")) saved.push(k);
const v = build(2, `v${r}`);
held.set(r, v.first);
const a = build(2, `a${r}`);
if (r % 3 === 0) a.kids[0]!.register();
total += buildInto(saved, 2);
const w = build(2, `w${r}`);
total += w.kids.length + w.first.kids.length;
}
console.log(total, saved.length, saved.map((n) => n.label + "/" + n.kids.length).slice(0, 10).join(","));
const f = closureOf(2);
console.log(f(), registry.map((n) => n.label + n.kids.length).join(","));
console.log([...held.values()].map((n) => n.label + n.kids[0]!.label).join(","));
+80
View File
@@ -0,0 +1,80 @@
// Groups built through virtual calls and sorted with callbacks, nested
// groups inside a group, and temporaries the group does not keep.
abstract class Shape {
abstract area(): number;
abstract scaled(k: number): Shape;
}
class Square extends Shape {
side: number;
constructor(side: number) {
super();
this.side = side;
}
area(): number {
return this.side * this.side;
}
scaled(k: number): Shape {
return new Square(this.side * k);
}
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
area(): number {
return Math.round(Math.PI * this.radius * this.radius * 100) / 100;
}
scaled(k: number): Shape {
return new Circle(this.radius * k);
}
}
interface Box {
label: string;
shapes: Shape[];
}
function measure(s: Shape): { area: number } {
return { area: s.area() };
}
function boxes(n: number): Box[] {
const out: Box[] = [];
for (let i = 0; i < n; i++) {
const shapes: Shape[] = [];
for (let j = 0; j < 4; j++) {
const base: Shape = j % 2 === 0 ? new Square(j + i) : new Circle(j + 1);
shapes.push(base.scaled(2));
}
// Temporaries the group does not keep.
let total = 0;
for (const s of shapes) total += measure(s).area;
shapes.sort((a, b) => a.area() - b.area());
out.push({ label: `box${i}:${Math.round(total)}`, shapes });
}
return out;
}
function summarize(n: number): string {
const list = boxes(n);
let largest = "";
let best = -1;
for (const b of list) {
const top = b.shapes[b.shapes.length - 1]!.area();
if (top > best) {
best = top;
largest = b.label;
}
}
return `${largest}@${best}`;
}
let out = "";
for (let round = 1; round < 30; round++) {
const all = boxes(round);
out = `${all.length} ${all[0]!.label} ${all[round - 1]!.shapes[0]!.area()} ${summarize(round)}`;
if (round % 7 === 0) console.log(out);
}
console.log(out);
+49
View File
@@ -0,0 +1,49 @@
// Groups whose members refer to objects outside them: a long-lived
// registry, strings built along the way, and a parameter's object. The
// arena's drop releases those references; the outside objects stay usable.
class Category {
members = 0;
name: string;
constructor(name: string) {
this.name = name;
}
}
class Member {
next: Member | undefined = undefined;
category: Category;
label: string;
rank: number;
constructor(category: Category, label: string, rank: number) {
this.category = category;
this.label = label;
this.rank = rank;
category.members++;
}
}
function ring(categories: Category[], size: number, seed: number): Member {
const first = new Member(categories[seed % categories.length]!, `m${seed}`, seed);
let last = first;
for (let i = 1; i < size; i++) {
const member = new Member(categories[(seed + i * 7) % categories.length]!, `m${seed}-${i}`, seed + i);
last.next = member;
last = member;
}
last.next = first;
return first;
}
const categories: Category[] = [];
for (let i = 0; i < 5; i++) categories.push(new Category(`cat-${i}`));
let ranks = 0;
let labels = "";
for (let round = 0; round < 200; round++) {
const r = ring(categories, 30, round);
ranks += r.next!.rank + r.next!.next!.rank;
if (round % 50 === 0) labels += `${r.label}:${r.category.name}:${r.next!.label} `;
}
console.log("ranks", ranks);
console.log("labels", labels.trim());
for (const c of categories) console.log(c.name, c.members);
+33
View File
@@ -0,0 +1,33 @@
// Exceptions inside call regions: the region's arena is dropped as the
// exception leaves the call, and the caller's own objects are untouched.
interface Entry {
key: string;
value: number;
next: Entry | null;
}
function tally(keys: string[], limit: number): number {
const entries: Entry[] = [];
for (const k of keys) {
entries.push({ key: k, value: k.length, next: entries.length > 0 ? entries[entries.length - 1]! : null });
if (entries.length > limit) throw new RangeError(`too many entries: ${entries.length}`);
}
let sum = 0;
for (const e of entries) sum += e.value + (e.next === null ? 0 : e.next.value);
return sum;
}
const kept: Entry[] = [{ key: "outside", value: 1, next: null }];
const keys = "alpha beta gamma delta epsilon zeta eta theta iota kappa".split(" ");
let total = 0;
let errors = 0;
for (let round = 0; round < 40; round++) {
try {
total += tally(keys, 3 + (round % 10));
} catch (e) {
errors++;
if (round < 6) console.log((e as Error).name, (e as Error).message);
}
kept.push({ key: `k${round}`, value: round, next: kept[kept.length - 1]! });
}
console.log("total", total, "errors", errors, "kept", kept.length, kept[kept.length - 1]!.next!.key);
+44
View File
@@ -0,0 +1,44 @@
// Call regions: calls of functions that build what they need and return
// a number, so everything they allocate is dead when they return.
interface Point {
x: number;
y: number;
}
interface Segment {
from: Point;
to: Point;
}
function perimeter(n: number, scale: number): number {
const points: Point[] = [];
for (let i = 0; i < n; i++) points.push({ x: Math.cos(i) * scale, y: Math.sin(i) * scale });
const segments: Segment[] = [];
for (let i = 0; i < n; i++) segments.push({ from: points[i]!, to: points[(i + 1) % n]! });
let total = 0;
for (const s of segments) total += Math.hypot(s.to.x - s.from.x, s.to.y - s.from.y);
return Math.round(total * 1000) / 1000;
}
let sum = 0;
for (let k = 3; k < 300; k++) sum += perimeter(k, k / 10);
console.log("perimeters", sum.toFixed(3));
function histogram(words: string[]): number {
const buckets: { word: string; count: number }[] = [];
for (const w of words) {
let found = false;
for (const b of buckets)
if (b.word === w) {
b.count++;
found = true;
}
if (!found) buckets.push({ word: w, count: 1 });
}
let best = 0;
for (const b of buckets) best = Math.max(best, b.count * 100 + b.word.length);
return best;
}
const words = "the quick brown fox jumps over the lazy dog the end".split(" ");
let h = 0;
for (let i = 0; i < 200; i++) h += histogram(words);
console.log("histogram", h);
+36
View File
@@ -0,0 +1,36 @@
// A thrown value that holds an object built in a group: the group cannot
// take an arena, because the value outlives it in the catch handler.
class Node {
label: string;
weight: number;
constructor(label: string, weight: number) {
this.label = label;
this.weight = weight;
}
}
class BuildError {
node: Node;
constructor(node: Node) {
this.node = node;
}
}
function build(n: number, failAt: number): Node[] {
const out: Node[] = [];
for (let i = 0; i < n; i++) {
const node = new Node(`n${i}`, i);
out.push(node);
if (i === failAt) throw new BuildError(node);
}
return out;
}
let total = 0;
const saved: string[] = [];
for (let r = 0; r < 20; r++) {
try {
total += build(50, r % 4 === 0 ? 30 : -1).length;
} catch (e) {
const b = e as BuildError;
saved.push(b.node.label + ":" + b.node.weight);
}
}
console.log(total, saved.join(","));
+40
View File
@@ -0,0 +1,40 @@
// A builder that throws part way: the arena of the group it was building
// is dropped as the exception leaves, and building goes on afterwards.
interface Item {
id: number;
tags: string[];
next: Item | null;
}
function chain(n: number, failAt: number): Item {
let head: Item = { id: 0, tags: ["head"], next: null };
const first = head;
for (let i = 1; i < n; i++) {
if (i === failAt) throw new Error(`failed at ${i} of ${n}`);
const item: Item = { id: i, tags: [`t${i}`, `u${i % 3}`], next: null };
head.next = item;
head = item;
}
return first;
}
function length(item: Item | null): number {
let n = 0;
for (let at = item; at !== null; at = at.next) n++;
return n;
}
let built = 0;
let failed = 0;
for (let round = 0; round < 60; round++) {
try {
const c = chain(20 + (round % 7), round % 5 === 0 ? 10 + (round % 3) : -1);
built += length(c) + c.next!.tags.length;
} catch (e) {
failed++;
if (round < 20) console.log("caught", (e as Error).message);
}
}
console.log("built", built, "failed", failed);
const last = chain(5, -1);
console.log(last.id, last.next!.id, last.next!.tags.join("/"), length(last));
+70
View File
@@ -0,0 +1,70 @@
// Recursive builders whose results their callers only read: each tree is a
// group that lives in an arena until the statement (or the loop body that
// holds it) ends.
class TreeNode {
left: TreeNode | null;
right: TreeNode | null;
value: number;
constructor(left: TreeNode | null, right: TreeNode | null, value: number) {
this.left = left;
this.right = right;
this.value = value;
}
sum(): number {
return this.value + (this.left === null ? 0 : this.left.sum()) + (this.right === null ? 0 : this.right.sum());
}
depth(): number {
return 1 + Math.max(this.left === null ? 0 : this.left.depth(), this.right === null ? 0 : this.right.depth());
}
}
function build(depth: number, value: number): TreeNode {
return depth === 0
? new TreeNode(null, null, value)
: new TreeNode(build(depth - 1, value * 2), build(depth - 1, value * 2 + 1), value);
}
let total = 0;
for (let d = 0; d < 12; d++) total += build(d, 1).sum();
console.log("sums", total);
for (let d = 2; d <= 4; d++) {
const t = build(d, 1);
console.log("tree", d, t.sum(), t.depth(), t.left!.value, t.right!.right!.value);
}
function deepest(n: number): number {
let best = 0;
for (let i = 0; i < n; i++) {
const t = build(i % 6, i);
best = Math.max(best, t.depth() * 1000 + (t.sum() % 1000));
}
return best;
}
console.log("deepest", deepest(40));
// Record-literal trees: a root call whose result its caller only reads
// builds the whole tree, its root node included, in the group's arena.
interface Node2 {
left: Node2 | null;
right: Node2 | null;
value: number;
}
function buildRecord(depth: number, value: number): Node2 {
return depth === 0
? { left: null, right: null, value }
: { left: buildRecord(depth - 1, value * 2), right: buildRecord(depth - 1, value * 2 + 1), value };
}
function sumRecord(node: Node2): number {
return node.value + (node.left === null ? 0 : sumRecord(node.left)) + (node.right === null ? 0 : sumRecord(node.right));
}
let recordTotal = 0;
for (let d = 0; d < 12; d++) recordTotal += sumRecord(buildRecord(d, 1));
console.log("record sums", recordTotal);
for (let d = 1; d <= 3; d++) {
const t = buildRecord(d, 1);
console.log("record tree", d, sumRecord(t), t.value, t.left!.value, t.right!.value);
}
+75
View File
@@ -0,0 +1,75 @@
// Arena groups in worker threads: each worker builds and reads trees of
// classes and record literals, groups that live in the worker's own arena
// until the statement or loop body that holds them ends. The main thread
// builds its own groups meanwhile and keeps one tree for the whole run.
import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads";
class Pair {
left: Pair | null;
right: Pair | null;
label: string;
constructor(left: Pair | null, right: Pair | null, label: string) {
this.left = left;
this.right = right;
this.label = label;
}
}
interface Leaf {
left: Leaf | null;
right: Leaf | null;
weight: number;
}
function pairs(depth: number, label: string): Pair {
return depth === 0 ? new Pair(null, null, label) : new Pair(pairs(depth - 1, label + "l"), pairs(depth - 1, label + "r"), label);
}
function leaves(depth: number, weight: number): Leaf {
return depth === 0
? { left: null, right: null, weight }
: { left: leaves(depth - 1, weight + 1), right: leaves(depth - 1, weight * 2), weight };
}
function countPairs(p: Pair): number {
return 1 + (p.left === null ? 0 : countPairs(p.left)) + (p.right === null ? 0 : countPairs(p.right));
}
function rightmost(p: Pair): string {
return p.right === null ? p.label : rightmost(p.right);
}
function weigh(l: Leaf): number {
return l.weight + (l.left === null ? 0 : weigh(l.left)) + (l.right === null ? 0 : weigh(l.right));
}
function task(depth: number, rounds: number): string {
let nodes = 0;
let total = 0;
for (let i = 0; i < rounds; i++) {
nodes += countPairs(pairs(depth, "p"));
total += weigh(leaves(depth, i));
}
const kept = pairs(depth, "k");
return `depth ${depth}: ${nodes} pairs, weight ${total}, rightmost ${rightmost(kept)}, kept ${countPairs(kept)}`;
}
if (isMainThread) {
const longLived = leaves(10, 1);
const depths = [3, 5, 7, 9];
const lines: string[] = new Array(depths.length).fill("");
let pending = depths.length;
depths.forEach((depth, index) => {
const worker = new Worker(new URL(import.meta.url), { workerData: depth });
worker.on("message", (line: string) => {
lines[index] = line;
if (--pending === 0) {
for (const l of lines) console.log(l);
console.log("main", task(6, 20));
console.log("long lived", weigh(longLived));
}
});
});
} else {
parentPort!.postMessage(task(workerData as number, 50));
}
@@ -0,0 +1,60 @@
// A class that extends Array whose elements can lead back to it only
// through a field of plain array type (TreeNode.kids holds a Kids, typed as
// an array of TreeNode): its instances carry the collector header that
// arrays of its element type carry, so the collector can visit them through
// that field. Forest's elements are Kids instances, which carry one too.
class Kids extends Array<TreeNode> {
label = "kids";
total(): number {
let sum = 0;
for (const kid of this) sum += kid.size();
return sum;
}
}
class TreeNode {
kids = new Kids();
value: number;
constructor(value: number) {
this.value = value;
}
size(): number {
return 1 + this.kids.total();
}
}
class Forest extends Array<Kids> {
name = "forest";
}
function grow(depth: number): TreeNode {
const node = new TreeNode(depth);
if (depth > 0) for (let i = 0; i < 3; i++) node.kids.push(grow(depth - 1));
return node;
}
// Garbage cycles through the field of array type make the collector run
// while the trees are alive.
function churn(rounds: number): number {
let made = 0;
for (let i = 0; i < rounds; i++) {
const node = new TreeNode(i);
node.kids.push(node);
made++;
}
return made;
}
let checks = 0;
for (let round = 0; round < 20; round++) {
checks += grow(4).size();
churn(500);
}
console.log(checks);
const forest = new Forest();
for (let i = 0; i < 4; i++) forest.push(grow(2).kids);
churn(5000);
let leaves = 0;
for (const kids of forest) leaves += kids.total();
console.log(forest.name, forest.length, leaves, forest[0]!.label, forest[0] instanceof Kids);
@@ -0,0 +1,37 @@
// Instances of classes that extend Array read back from an array of them
// (an element read is `Row | undefined`) still answer instanceof for their
// own class and Array, false for another Array subclass, and plain arrays
// answer false.
class Row extends Array<number> {
sum(): number {
let s = 0;
for (const v of this) s += v;
return s;
}
}
class Column extends Array<number> {}
function rows(n: number): Row[] {
const out: Row[] = [];
for (let i = 0; i < n; i++) {
const row = new Row();
for (let j = 0; j <= i; j++) row.push(j);
out.push(row);
}
return out;
}
const lines: number[][] = [new Row(), new Column(), [1, 2]];
const table = rows(6);
console.log(table.length, table[5]!.sum(), table[5] instanceof Row, table[5] instanceof Column);
console.log(table[4] instanceof Row, table[4] instanceof Array);
console.log(table.at(-1) instanceof Row, table.at(10) instanceof Row);
const plain: number[][] = [[1, 2]];
console.log(plain[0] instanceof Row, plain[0] instanceof Array);
const kinds = lines.map((r) => (r instanceof Column ? "column" : r instanceof Row ? "row" : "plain"));
console.log(kinds.join(","));
let checks = 0;
for (let round = 0; round < 20; round++)
checks += rows(6).reduce((s, r) => s + (r instanceof Row ? r.sum() : 0), 0);
console.log(checks);
@@ -0,0 +1,22 @@
// React's development warnings discard a logical whose right operand is a
// conditional of console.error calls.
function warnIfMissingRender(instance, Component) {
instance.render ||
(Component.prototype && typeof Component.prototype.render === "function"
? console.error("%s(...): No `render` method found on the returned component.", "App")
: console.error("%s(...): No `render` method found, and its prototype has none.", "App"));
}
warnIfMissingRender({ render: null }, { prototype: { render() {} } });
warnIfMissingRender({ render: null }, { prototype: {} });
warnIfMissingRender({ render() {} }, { prototype: {} });
let warned = 0;
function warn(message) {
warned++;
console.log("warn:", message);
}
for (const props of [{ key: 1 }, {}, { key: "a", ref: null }]) {
"key" in props && (typeof props.key === "string" ? warn("string key") : warn("number key"));
props.ref === undefined || (props.ref === null ? warn("null ref") : warn("ref"));
}
console.log("warned", warned);
@@ -0,0 +1,69 @@
// A logical expression whose result is discarded and whose right operand is
// a conditional of void calls (`skip || (flag ? warnA() : warnB())`) runs as
// a branch: the left operand decides whether the conditional runs, and the
// conditional runs one arm.
const order: string[] = [];
function check(label: string, value: boolean): boolean {
order.push(label);
return value;
}
function emit(label: string): void {
order.push(label);
}
for (const left of [false, true]) {
for (const flag of [false, true]) {
check("and", left) && (check("flag", flag) ? emit("and:yes") : emit("and:no"));
check("or", left) || (check("flag", flag) ? emit("or:yes") : emit("or:no"));
left || (flag && (check("inner", !flag) ? emit("nested:yes") : emit("nested:no")));
left || (order.push("comma"), flag ? emit("comma:yes") : emit("comma:no"));
!left && (flag ? (left ? emit("deep:a") : emit("deep:b")) : emit("deep:c"));
}
}
console.log(order.join(" "));
order.length = 0;
// A for loop's initializer and update discard their results too.
let steps = 0;
for (
check("init", false) || (check("init flag", true) ? emit("init:yes") : emit("init:no"));
steps < 2;
check("update", true) && (steps > 1 ? emit("update:last") : emit("update:next"))
) {
steps++;
}
console.log(steps, order.join(" "));
order.length = 0;
// The left operand narrows the conditional.
function describe(value: string | number): void {
typeof value === "string" && (value.length > 2 ? emit(value.toUpperCase()) : emit(value));
typeof value === "number" || (value === "" ? emit("empty") : emit(`text:${value}`));
}
describe("abc");
describe("ab");
describe("");
describe(7);
console.log(order.join(" "));
order.length = 0;
// A generic callback: the conditional's type is T, void for the first two
// calls and number for the last.
function choose<T>(skip: boolean, flag: boolean, a: () => T, b: () => T): void {
skip || (flag ? a() : b());
}
choose(false, true, () => emit("generic:a"), () => emit("generic:b"));
choose(false, false, () => emit("generic:a"), () => emit("generic:b"));
choose(true, true, () => emit("generic:skipped"), () => emit("generic:skipped"));
choose(false, true, () => order.push("number:a"), () => order.push("number:b"));
console.log(order.join(" "));
order.length = 0;
// A throw from the chosen arm propagates.
try {
choose(false, false, () => emit("unused"), () => {
throw new Error("from b");
});
} catch (error) {
console.log((error as Error).message);
}
@@ -0,0 +1,35 @@
// A frozen native record or array seen through an untyped reference keeps
// its frozen state: strict-mode writes throw Node's TypeErrors and leave
// the value unchanged, and Object.isFrozen answers true through any name.
export {};
function write(target: unknown, key: string, value: unknown): void {
try {
(target as Record<string, unknown>)[key] = value;
console.log("wrote", key);
} catch (e) {
console.log("threw", (e as Error).message);
}
}
const literal = Object.freeze({ a: 1, b: "x" });
function make(n: number) {
return Object.freeze({ a: n, tags: ["t"] });
}
const made = make(2);
write(literal, "a", 5);
write(literal, "z", 5);
write(made, "a", 7);
write(made, "tags", []);
made.tags.push("u");
console.log(literal.a, made.a, JSON.stringify(literal), JSON.stringify(made));
console.log(Object.isFrozen(literal), Object.isFrozen(made), Object.isFrozen(made.tags));
const list = Object.freeze([1, 2]);
write(list, "0", 9);
write(list, "5", 9);
console.log(list[0], list.length, Object.isFrozen(list));
const open = { a: 1 };
write(open, "a", 3);
console.log(open.a);
@@ -0,0 +1,43 @@
// Sloppy-mode code ignores writes that strict mode refuses: assignments to
// frozen objects and arrays and to non-extensible objects leave them
// unchanged without an error, while strict functions still throw.
function write(target, key, value) {
target[key] = value;
console.log("after", key, JSON.stringify(target));
}
function strictWrite(target, key, value) {
"use strict";
try {
target[key] = value;
console.log("wrote", key);
} catch (e) {
console.log("threw", e.message);
}
}
const frozen = Object.freeze({ a: 1, b: "x" });
write(frozen, "a", 5);
write(frozen, "z", 5);
strictWrite(frozen, "a", 5);
const closed = Object.preventExtensions(JSON.parse('{"a":1}'));
write(closed, "a", 2);
write(closed, "b", 2);
strictWrite(closed, "b", 2);
const list = Object.freeze([1, 2]);
write(list, "0", 9);
strictWrite(list, "0", 9);
const holder = {};
Object.defineProperty(holder, "fixed", { value: 1, writable: false, enumerable: true });
write(holder, "fixed", 2);
strictWrite(holder, "fixed", 2);
const withSetter = {
/** @param {number} x */
set v(x) {
throw new Error("setter " + x);
},
};
try {
write(withSetter, "v", 1);
} catch (e) {
console.log("setter threw", e.message);
}
console.log(frozen.a, closed.b, list[0], holder.fixed);
+40
View File
@@ -0,0 +1,40 @@
// A recursive function over a number array runs as an integer clone.
// sumFrom only receives arrays that hold int32s, which keep 4-byte int32
// slots, and the clone reads them at that width, near their end too.
// sumWide receives an array that also holds a fraction, with 8-byte slots.
function sumFrom(xs: number[], i: number): number {
if (i >= xs.length) return 0;
return xs[i]! + sumFrom(xs, i + 1);
}
function masked(n: number, start: number): number {
const a: number[] = [];
for (let i = 0; i < n; i++) a.push(i & 7);
return sumFrom(a, start);
}
function halves(n: number, start: number): number {
const a: number[] = [];
for (let i = 0; i < n; i++) a.push(i < n / 2 ? 0 : 3);
return sumFrom(a, start);
}
function sumWide(xs: number[], i: number): number {
if (i >= xs.length) return 0;
return xs[i]! + sumWide(xs, i + 1);
}
function widened(n: number, start: number): number {
const a: number[] = [];
for (let i = 0; i < n; i++) a.push(i % 5);
const before = sumWide(a, start);
a.push(0.5);
return before * 10 + sumWide(a, start);
}
const { n } = JSON.parse('{"n": 1000}') as { n: number };
for (const start of [0, 1, n / 2, n - 9, n - 2, n - 1, n])
console.log(start, masked(n | 0, start | 0), halves(n | 0, start | 0));
for (const size of [63, 64, 65])
console.log(size, halves(size, size - 24), halves(size, size - 1), masked(size, size - 3));
console.log(widened(40, 0), widened(40, 37));
@@ -0,0 +1,96 @@
// worker.terminate() stops a worker that spends its time in short runs of
// inner loops: counted and paired loops of a few iterations inside loops
// without a condition, runs at the length where an inner loop starts to
// poll on its own, and a short inner loop whose body calls a slow function.
// Each terminate resolves promptly.
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
function flips(n: number): number {
const p = new Int32Array(n),
q = new Int32Array(n),
count = new Int32Array(n);
for (let i = 0; i < n; i++) p[i] = i;
let total = 0;
for (let round = 0; ; round++) {
for (let i = 0; i < n; i++) q[i] = p[i]!;
let first = q[0]!;
for (;;) {
if (first === 0) break;
for (let i = 0, j = first; i < j; i++, j--) {
const t = q[i]!;
q[i] = q[j]!;
q[j] = t;
}
total++;
first = q[0]!;
}
// The next permutation: rotate the first k + 1 elements for the
// smallest k whose count has not run out.
for (let k = 1; ; k++) {
if (k === n) {
for (let i = 0; i < n; i++) p[i] = i;
count.fill(0);
break;
}
const head = p[0]!;
for (let i = 0; i < k; i++) p[i] = p[i + 1]!;
p[k] = head;
if (++count[k]! <= k) break;
count[k] = 0;
}
if (round === 2000) parentPort!.postMessage("ready");
}
return total;
}
function thresholds(values: number[]): number {
let acc = 0;
for (let round = 0; ; round++) {
for (const run of [63, 64, 65]) {
for (let i = 0; i < run; i++) acc = (acc + values[i]! * round) % 1000003;
for (let i = 0, j = run; i < j; i++, j--) acc = (acc + values[i]! + values[j]!) % 1000003;
}
if (round === 2000) parentPort!.postMessage("ready");
}
return acc;
}
function slow(seed: number): number {
let acc = seed;
for (let i = 0; i < 20000; i++) acc = (acc * 31 + i) % 1000003;
return acc;
}
function slowBodies(): number {
let acc = 0;
for (let round = 0; ; round++) {
for (let i = 0; i < 4; i++) acc = (acc + slow(acc + i)) % 1000003;
if (round === 50) parentPort!.postMessage("ready");
}
return acc;
}
if (isMainThread) {
for (const kind of ["flips", "thresholds", "slow"]) {
await new Promise<void>((resolve) => {
const worker = new Worker(new URL(import.meta.url), { workerData: kind });
worker.once("message", () => {
const asked = Date.now();
void worker.terminate().then((code: number) => {
console.log(kind, "terminate", code, Date.now() - asked < 1000 ? "prompt" : "late");
});
});
worker.once("exit", (code: number) => {
console.log(kind, "exit", code);
resolve();
});
});
}
} else {
let result = 0;
if (workerData === "flips") result = flips(9);
else if (workerData === "thresholds")
result = thresholds(Array.from({ length: 70 }, (_, i) => i));
else result = slowBodies();
console.log("unexpected result", result);
}
@@ -0,0 +1,84 @@
// Member reads through non-null asserted element reads at computed indexes:
// `xs[xs.length - 1]!`, `xs[i + 1]!`, `xs[-i]!` and indexes read from
// fields. A missing element (past the end, before the start, a fraction,
// NaN, or an empty array) is undefined, so the member access throws Node's
// TypeError; a present one reads the member.
interface Gene {
c: number;
sc: number;
}
class Cell {
label: string;
constructor(label: string) {
this.label = label;
}
shout(): string {
return this.label.toUpperCase();
}
}
function attempt(label: string, f: () => unknown): void {
try {
console.log(label.padEnd(16), "ok", f());
} catch (e) {
const error = e as Error;
console.log(label.padEnd(16), error instanceof TypeError, error.message);
}
}
// The fallback of a cumulative search, as fasta writes it.
function pick(genes: Gene[], rand: number): number {
for (const gene of genes) {
if (rand < gene.c) return gene.sc;
}
return genes[genes.length - 1]!.sc;
}
const genes: Gene[] = [
{ c: 0.25, sc: 97 },
{ c: 0.5, sc: 99 },
{ c: 0.75, sc: 103 },
];
const codes: number[] = [];
for (let k = 0; k < 8; k++) codes.push(pick(genes, k / 8));
console.log("codes", codes.join(","));
attempt("fallback", () => pick(genes, 2));
attempt("empty fallback", () => pick([], 0.5));
const cells = [new Cell("a"), new Cell("b"), new Cell("c")];
const none: Cell[] = [];
const holey: Cell[] = [];
holey[3] = new Cell("d");
const box = { at: 1, size: 3 };
for (const i of [0, 1, 2, 3, -1, 0.5, NaN]) {
attempt(`last - ${i}`, () => cells[cells.length - 1 - i]!.label);
attempt(`next ${i}`, () => cells[i + 1]!.label);
attempt(`neg ${i}`, () => cells[-i]!.label);
attempt(`double ${i}`, () => cells[i * 2]!.shout());
}
attempt("empty last", () => none[none.length - 1]!.label);
attempt("empty method", () => none[none.length - 1]!.shout());
attempt("hole last", () => holey[holey.length - 1]!.label);
attempt("hole before", () => holey[holey.length - 2]!.label);
attempt("field index", () => cells[box.at - 1]!.label);
attempt("field past", () => cells[box.size + box.at]!.label);
attempt("other length", () => cells[holey.length - 2]!.label);
const rows = [
[1, 2],
[3, 4],
];
attempt("row last", () => rows[rows.length - 1]![0]);
attempt("row past", () => rows[rows.length]![1]);
attempt("row length", () => rows[rows.length - 2]!.length);
// The same read in a loop that runs past the end.
let total = 0;
try {
for (let i = 0; i < 5; i++) total += cells[cells.length - 1 - i]!.label.length;
} catch (e) {
console.log("loop", (e as Error).message, total);
}
+62
View File
@@ -0,0 +1,62 @@
// Writes through an untyped view of a native record store into the record
// itself: typed reads see them, the view keeps answering from the record
// (no snapshot), and typed writes show through the view again.
type Point = { x: number; y: number; label: string; tag?: string };
const p: Point = { x: 1, y: 2, label: "a" };
const d = p as unknown as Record<string, unknown>;
d.x = 10;
d.label = "b";
console.log(p.x, p.label, JSON.stringify(d), Object.keys(d).join(","));
p.y = 7;
console.log(d.y, JSON.stringify(p), "y" in d);
d.tag = "t";
console.log(p.tag, JSON.stringify(d), Object.keys(p).join(","));
d.tag = "u";
console.log(p.tag, d.tag);
d.label = "c";
console.log(p.label, JSON.stringify(Object.entries(d)));
// Nested values written through the view are the record's own.
type Nested = { inner: { v: number }; list: number[]; flag: boolean };
const n: Nested = { inner: { v: 1 }, list: [1], flag: false };
const dn = n as unknown as { inner: unknown; list: unknown; flag: unknown };
const fresh = { v: 2 };
dn.inner = fresh;
fresh.v = 3;
console.log(n.inner.v, JSON.stringify(dn));
dn.list = [4, 5];
n.list.push(6);
console.log(n.list.length, n.list[1], JSON.stringify(dn.list));
dn.flag = true;
console.log(n.flag, dn.flag);
// A counter updated through the view in a loop.
const acc: { total: number; count: number } = { total: 0, count: 0 };
const da = acc as unknown as { total: number; count: number };
for (let i = 0; i < 100000; i++) {
da.total = da.total + i;
da.count += 1;
}
console.log(acc.total, acc.count);
// Object.assign onto the view.
type Cfg = { host: string; port: number; debug: boolean; tags?: string[] };
const c: Cfg = { host: "a", port: 1, debug: false, tags: ["x"] };
const dc = c as unknown as object;
Object.assign(dc, { port: 2, debug: true });
console.log(c.port, c.debug, JSON.stringify(dc));
Object.assign(dc, { host: "z", extra: 1 });
console.log(c.host, JSON.stringify(dc), Object.keys(dc).length);
c.port = 3;
console.log(JSON.stringify(dc));
// Writes through a function that takes the record as unknown.
function bump(value: unknown, key: string): void {
const object = value as Record<string, number>;
object[key] = object[key]! + 1;
}
const counts = { a: 1, b: 2 };
bump(counts, "a");
bump(counts, "b");
bump(counts, "b");
console.log(counts.a, counts.b, JSON.stringify(counts));
+52
View File
@@ -0,0 +1,52 @@
// A record value converted from an untyped value into a narrower record
// type whose fields begin the wider record's storage is the same object:
// a write through either name shows through the other, untyped views of
// the narrower name see every property, and identity holds.
type Wide = { a: number; b: string; c: boolean; d: number };
type Narrow = { a: number; b: string };
type Head = { a: number };
function make(): unknown {
return { a: 1, b: "x", c: true, d: 4 } satisfies Wide;
}
const src = make();
const w = src as Wide;
const n = src as Narrow;
n.a = 10;
console.log(w.a, (src as Head).a);
w.b = "y";
console.log(n.b);
console.log(JSON.stringify(src), JSON.stringify(n));
const n2 = src as Narrow;
console.log(n2 === n, n2.a);
(src as Record<string, unknown>).a = 30;
console.log(w.a, n.a, (src as Head).a);
console.log(Object.keys(n).join(","), JSON.stringify({ n }), (n as unknown) === src);
console.log(n);
// A native record boxed and narrowed in a function.
function narrow(value: unknown): Narrow {
return value as Narrow;
}
const wide: Wide = { a: 1, b: "p", c: false, d: 2 };
const seen = narrow(wide);
seen.b = "q";
wide.a = 5;
console.log(wide.b, seen.a, JSON.stringify(seen));
// Narrowing to a leading field of a record whose other fields are scalars.
type Sample = { a: number; b: number; c: boolean };
const items: Sample[] = [
{ a: 1, b: 10, c: true },
{ a: 2, b: 20, c: false },
];
function asHead(value: unknown): Head {
return value as Head;
}
const heads = items.map((item) => asHead(item));
for (const head of heads) head.a *= 10;
items[0]!.a += 1;
console.log(JSON.stringify(items), JSON.stringify(heads), heads[0]!.a);
const last = items[items.length - 1]!;
const second = asHead(last);
second.a = 7;
console.log(last.a, (second as unknown) === (last as unknown), Object.keys(second).join(","));
+50
View File
@@ -0,0 +1,50 @@
// charCodeAt and codePointAt on short non-ASCII strings: characters of each
// UTF-8 length, astral pairs, lone surrogates, out-of-range and non-integer
// indexes, strings at the 16-byte boundary, and a long receiver scanned in
// between.
const samples = [
"é", "ß", "日", "語", "😀", "🚀", "a", "",
"café", "naïve", "straße", "日本語", "テキスト", "한국어",
"a😀b", "😀😀", "xéy日z",
"\ud800", "\udc00", "a\ud800b", "\ud83d", "\ude00x",
"0123456789abcdé", "0123456789abcdeé", "0123456789abcdefé", "ééééééé", "éééééééé", "ééééééééé",
];
function codes(s: string): string {
const out: string[] = [];
for (let i = -1; i <= s.length + 1; i++) out.push(String(s.charCodeAt(i)));
out.push(String(s.charCodeAt(0.5)), String(s.charCodeAt(NaN)), String(s.charCodeAt(1.9)));
return out.join(",");
}
function points(s: string): string {
const out: string[] = [];
for (let i = -1; i <= s.length + 1; i++) out.push(String(s.codePointAt(i)));
return out.join(",");
}
for (const s of samples) {
console.log(JSON.stringify(s), s.length, codes(s));
console.log(" points", points(s));
}
// A long receiver scanned while short ones are read.
let long = "";
for (let i = 0; i < 200; i++) long += i % 3 === 0 ? "é" : i % 3 === 1 ? "日" : "x";
let sum = 0;
for (let i = 0; i < long.length; i++) {
sum += long.charCodeAt(i);
sum += samples[i % samples.length]!.charCodeAt(0) || 0;
}
console.log("long", long.length, sum);
// Characters of a for-of loop classified by their code points.
let letters = 0;
let others = 0;
for (const c of "the café, 日本 😀 naïve!") {
const code = c.codePointAt(0)!;
if ((code >= 0x61 && code <= 0x7a) || code >= 0xc0) letters++;
else others++;
}
console.log("letters", letters, "others", others);
+25
View File
@@ -0,0 +1,25 @@
// Spreading strings into arrays: code points of each UTF-8 length, astral
// pairs, lone surrogates, the empty string, and the fresh array each spread
// returns.
const samples = ["", "a", "abc", "é", "café", "日本語", "😀", "a😀b", "🚀🚀", "\ud800", "a\udc00b", "xé日😀"];
for (const s of samples) {
const chars = [...s];
console.log(JSON.stringify(s), chars.length, JSON.stringify(chars), chars.join("|"));
}
// Each spread is a new array: changing one leaves the others and the string.
const text = "héllo 😀";
const first = [...text];
const second = [...text];
first.reverse();
first.push("!");
console.log(first.join(""), second.join(""), first === second, text);
// The spread result is an ordinary array.
const letters = [..."naïve"];
letters.sort();
console.log(letters, letters.length, Array.isArray(letters));
console.log([..."abc"].map((c) => c.toUpperCase()).join(""));
console.log([..."日本"].concat([..."語"]).join("-"));
@@ -0,0 +1,68 @@
// Loops whose own typed-array accesses run once per pass around nested
// loops that read typed arrays: row stores past the end, reads at NaN,
// negative and fractional rows, rows that read a typed array the nested
// loop writes, and a nested loop that reads the row's own result.
function rowSums(u: Float64Array, out: Float64Array, start: number, end: number): void {
for (let i = start; i < end; i++) {
let t = out[i] ?? -1;
for (let j = 0; j < u.length; j++) t += u[j]! * (i + 1);
out[i] = t;
}
}
function scaled(u: Float64Array, scale: Float64Array, out: Float64Array, rows: number): void {
for (let i = 0; i < rows; i++) {
const s = scale[i * 2] ?? 0.5;
for (let j = 0; j < u.length; j++) out[j] = out[j]! + u[j]! * s;
out[i] = out[i]! - 1;
}
}
function chained(v: Int32Array, rows: number): number {
let total = 0;
for (let i = 0; i < rows; i++) {
let k = 0;
while (k < v.length) {
v[k] = v[k]! + (v[i] ?? 7);
k++;
}
total += v[i + 1] ?? -3;
}
return total;
}
const show = (label: string, v: Float64Array | Int32Array): void => {
const parts: string[] = [];
for (let k = 0; k < v.length; k++) parts.push(Object.is(v[k], -0) ? "-0" : String(v[k]));
console.log(label, parts.join(" "));
};
const u = new Float64Array([0.5, -1.25, 3, 7.75]);
for (const [start, end] of [
[0, 4],
[0, 7],
[2, 9],
[-2, 3],
[0.5, 3.5],
[NaN, 3],
[3, 1],
]) {
const out = new Float64Array(5).fill(2);
rowSums(u, out, start!, end!);
show(`rows ${start} ${end}`, out);
}
for (const rows of [0, 1, 3, 6]) {
const out = new Float64Array(4).fill(1);
scaled(u, new Float64Array([2, -1, 0.25, 4, 8]), out, rows);
show(`scaled ${rows}`, out);
const same = new Float64Array([1, 2, 3, 4]);
scaled(same, same, same, rows);
show(`aliased ${rows}`, same);
}
for (const rows of [0, 2, 4, 9]) {
const v = new Int32Array([1, -2, 3, 2147483647]);
const total = chained(v, rows);
show(`chained ${rows} ${total}`, v);
}
@@ -0,0 +1,52 @@
// Typed array reads and writes at `base + c`, `c + base` and `base - c`,
// where `base` is a number that may be an integer, a fraction, negative,
// -0, NaN, infinite or past the length.
function readGroup(values: Float64Array, at: number): number[] {
return [values[at + 0]!, values[at + 1]!, values[2 + at]!, values[at - 1]!, values[at + 7]!];
}
function writeGroup(values: Int32Array, at: number, value: number): void {
values[at + 0] = value;
values[1 + at] = value + 1;
values[at - 2] = value - 2;
}
function sumWindow(values: Uint8Array, at: number): number {
return (values[at] ?? 0) + (values[at + 1] ?? 0) + (values[at + 2] ?? 0) + (values[at + 3] ?? 0);
}
const floats = new Float64Array(16);
for (let i = 0; i < floats.length; i++) floats[i] = i * 1.5;
const bases = [0, 1, 4, 8.5, -0, -1, -0.5, 15, 16, 2 ** 31, 2 ** 52, 2 ** 53, -(2 ** 53), NaN, Infinity, -Infinity];
for (const base of bases) console.log(String(base), JSON.stringify(readGroup(floats, base)));
const ints = new Int32Array(8);
for (const base of [2, 3.25, -0, 7, 9, NaN, 2 ** 52 + 1]) {
writeGroup(ints, base, 10);
console.log(String(base), ints.join(","));
}
const bytes = new Uint8Array([5, 4, 3, 2, 1, 250, 251, 252]);
let total = 0;
for (let i = 0; i < 8; i++) total += sumWindow(bytes, i);
console.log(total, sumWindow(bytes, 6), sumWindow(bytes, 4.5));
// A base with a fraction too small to survive the sum indexes like the sum.
for (const base of [1e-20, -1e-20, 2 ** -60, 3 + 2 ** -50, 0.5, -0.75]) {
console.log(String(base), JSON.stringify(readGroup(floats, base)));
writeGroup(ints, base, 20);
console.log(String(base), ints.join(","));
}
// A right-hand side that changes the base: the index is the one evaluated
// before it.
function rebase(values: Int32Array, at: number): number {
values[at + 1] = ((at = 5), 31);
values[2 + at] = at++;
values[at - 1] += ((at = 0), 4);
Atomics.add(values, at + 3, ((at = 6), 2));
return at;
}
const targets = new Int32Array(10);
console.log(rebase(targets, 2), targets.join(","));
@@ -0,0 +1,50 @@
// `??` in untyped JavaScript where the left is an optional number, boolean or
// string and the default has the same type: Map reads, find(), a function that
// may return undefined, and defaults of another type, null or an object.
const words = ["alpha", "beta", "gamma", "beta", "delta", "beta", "alpha"];
const counts = new Map();
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1);
for (const [word, n] of counts) console.log(word, n);
const odd = new Map();
odd.set("nan", NaN);
odd.set("zero", -0);
odd.set("one", 1);
for (const key of ["nan", "zero", "one", "missing"]) {
const value = odd.get(key) ?? 7;
console.log(key, value, Object.is(value, -0), Number.isNaN(value), typeof value);
}
const flags = new Map();
flags.set("on", true);
flags.set("off", false);
for (const key of ["on", "off", "missing"]) console.log(key, flags.get(key) ?? true);
const names = new Map();
names.set("a", "");
names.set("b", "bee");
for (const key of ["a", "b", "c"]) console.log(JSON.stringify(names.get(key) ?? "none"));
const values = [3, 8, 13, 21];
console.log(values.find((v) => v > 10) ?? -1, values.find((v) => v > 100) ?? -1);
function lookup(table, key) {
return key in table ? table[key] : undefined;
}
const table = { x: 1.5, y: 0 };
console.log(lookup(table, "x") ?? 0, lookup(table, "y") ?? 9, lookup(table, "z") ?? 9);
// Defaults of another type keep the left's value or take the default as is.
console.log(counts.get("beta") ?? "none", counts.get("omega") ?? "none");
console.log(counts.get("omega") ?? null, counts.get("beta") ?? null);
const fallback = { n: 0 };
console.log(counts.get("omega") ?? fallback, counts.get("alpha") ?? fallback);
// The result is an ordinary value in later arithmetic and comparisons.
let total = 0;
for (const word of ["alpha", "beta", "omega"]) total += (counts.get(word) ?? 0) * 10;
console.log(total, (counts.get("omega") ?? 0) === 0, (odd.get("nan") ?? 0) === (odd.get("nan") ?? 0));
// A default of the same type that can itself be undefined keeps its value.
const spare = [4, 5];
console.log(counts.get("omega") ?? spare[9], counts.get("beta") ?? spare[9], counts.get("omega") ?? spare[1]);
+160
View File
@@ -0,0 +1,160 @@
// While loops whose remainder reads locals copied from module globals,
// parameters and fields. Entry values cover integers inside and outside
// [-2^48, 2^48], fractions, -0, NaN and infinities.
let column = 0;
let lines = 0;
// A switch with more cases than a small leaf function has statements.
function complement(code: number): number {
switch (code) {
case 65:
return 84;
case 67:
return 71;
case 71:
return 67;
case 84:
return 65;
case 97:
return 116;
case 99:
return 103;
case 103:
return 99;
case 116:
return 97;
case 85:
return 65;
case 117:
return 97;
case 82:
return 89;
case 89:
return 82;
case 75:
return 77;
case 77:
return 75;
}
return 78;
}
// Copies bytes in reverse, complemented, with a newline after every 8
// columns counted from the module-level column and line counters.
function wrap(input: Uint8Array, out: Uint8Array): number {
const base = column;
let breaks = lines;
let count = 0;
let o = 0;
let i = 0;
const len = input.length;
let last = 1;
let negativeZeros = 0;
while (i < len) {
out[o] = complement(input[len - 1 - i]!);
i++;
o++;
count++;
last = (count + base - breaks) % 8;
if (Object.is(last, -0)) negativeZeros++;
if (last === 0) {
out[o] = 10;
o++;
count++;
breaks++;
}
}
column = base + count;
lines = breaks;
console.log(` last ${Object.is(last, -0) ? "-0" : last}, ${negativeZeros} of them -0`);
return o;
}
function text(out: Uint8Array, n: number): string {
let s = "";
for (let i = 0; i < n; i++) s += out[i] === 10 ? "|" : String.fromCharCode(out[i]!);
return s;
}
const input = new Uint8Array(29);
const bases = "ACGTTGCAAGCTTCGA";
for (let i = 0; i < input.length; i++) input[i] = bases.charCodeAt((i * 7) % bases.length);
const out = new Uint8Array(64);
const starts: [number, number][] = [
[0, 0],
[5, 2],
[-13, 0],
[-16, 0],
[2 ** 48, 3],
[-(2 ** 48), 0],
[2 ** 48 + 1, 0],
[2 ** 50, 1],
[2 ** 53 - 5, 0],
[2 ** 53, 0],
[2 ** 53 + 2, 1],
[0.5, 0],
[3, 0.25],
[3, 2 ** 49],
[-0, 0],
[0, -0],
[NaN, 0],
[Infinity, 0],
[-Infinity, 0],
[1e300, 0],
];
for (const [c, l] of starts) {
column = c;
lines = l;
console.log(`start column ${c} lines ${l}`);
const n = wrap(input, out);
console.log(` ${text(out, n)}`);
console.log(` column ${column} lines ${lines}`);
}
// Two calls in a row: the second starts from the first one's counters.
column = 3;
lines = 0;
wrap(input, out);
const n = wrap(input, out);
console.log(`again ${text(out, n)} column ${column} lines ${lines}`);
// A parameter and a field as the loop's entry values.
function ring(values: Int32Array, offset: number, size: number): number {
let sum = 0;
let i = 0;
while (i < values.length) {
sum += values[(i + offset) % size]!;
i++;
}
return sum;
}
const values = new Int32Array([3, 1, 4, 1, 5, 9, 2, 6]);
for (const offset of [0, 3, 7, 2 ** 40, 2 ** 49, 1.5, -0, -2]) {
const sum = ring(values, offset, values.length);
console.log(`ring ${offset} ${Number.isNaN(sum) ? "NaN" : sum}`);
}
class Cursor {
position: number;
constructor(position: number) {
this.position = position;
}
stripes(bytes: Uint8Array): string {
const start = this.position;
let i = 0;
let marks = "";
while (i < bytes.length) {
const r = (start + i) % 3;
marks += Object.is(r, -0) ? "z" : String(r);
i++;
}
this.position = start + i;
return marks;
}
}
for (const position of [0, 4, -5, 2 ** 48 + 7, 0.75, 2 ** 60]) {
const cursor = new Cursor(position);
console.log(`stripes ${position} ${cursor.stripes(input.subarray(0, 7))} ${cursor.position}`);
}
+1
View File
@@ -166,6 +166,7 @@ test("every job on a macOS runner is in the macos lane", () => {
);
expect(macosJobs.map(([name]) => name).sort()).toEqual([
"llvm_artifacts_macos_arm64",
"llvm_artifacts_macos_arm64_contracts",
"llvm_artifacts_macos_x64",
"mobile_runtime_packs_ios",
"native_macos",
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3800 }
@@ -0,0 +1 @@
{ "plain": 3585, "san": 5768 }
@@ -0,0 +1 @@
{ "plain": 2780, "san": 53539 }
@@ -0,0 +1 @@
{ "plain": 5000, "san": 3200 }
@@ -0,0 +1 @@
{ "plain": 9000, "san": 13300 }
@@ -0,0 +1 @@
{ "plain": 3900, "san": 5000 }
@@ -0,0 +1 @@
{ "plain": 3100 }
@@ -0,0 +1 @@
{ "plain": 3600, "san": 18500 }
@@ -0,0 +1 @@
{ "plain": 3200, "san": 4800 }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3300 }
@@ -0,0 +1 @@
{ "plain": 4136, "san": 4906 }
@@ -0,0 +1 @@
{ "plain": 3559, "san": 5110 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 3474, "san": 38229 }
@@ -0,0 +1 @@
{ "plain": 4906, "san": 6969 }
@@ -0,0 +1 @@
{ "plain": 3900, "san": 6300 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 4800, "san": 4600 }
@@ -0,0 +1 @@
{ "plain": 4500, "san": 4300 }
@@ -0,0 +1 @@
{ "plain": 6500, "san": 8800 }
@@ -0,0 +1 @@
{ "plain": 2100, "san": 2500 }
@@ -0,0 +1 @@
{ "plain": 3711, "san": 5463 }
@@ -0,0 +1 @@
{ "plain": 3600, "san": 3000 }
+113
View File
@@ -0,0 +1,113 @@
/* The heap census (backend/llvm/heap-census.ts): a program built with
* SCRIPTC_HEAP_CENSUS counts class instances and the stores, lengths and
* accesses of array-typed fields, and writes them at exit, on a normal exit
* and on process.exit alike. */
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const exec = promisify(execFile);
const sanitize = process.env["SCRIPTC_SAN"] === "1";
const PROGRAM = `
class Node {
kids: Node[];
tags: string[] | undefined;
constructor(kids: Node[]) {
this.kids = kids;
if (kids.length > 1) this.tags = ["a", "b"];
else this.tags = undefined;
}
}
let total = 0;
const roots: Node[] = [];
for (let i = 0; i < 100; i++) {
const leaves: Node[] = [];
for (let j = 0; j < i % 6; j++) leaves.push(new Node([]));
roots.push(new Node(leaves));
}
for (const r of roots) total += r.kids.length + (r.tags?.length ?? 0);
console.log(total);
`;
async function census(source: string): Promise<{ stdout: string; counts: Map<string, number> }> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-heap-census-"));
const saved = process.env["SCRIPTC_HEAP_CENSUS"];
try {
const entry = join(dir, "main.ts");
await writeFile(entry, source);
process.env["SCRIPTC_HEAP_CENSUS"] = "1";
const result = await compile(entry, {
backend: "llvm",
sanitize,
outDir: dir,
outPath: join(dir, "program"),
});
if (saved === undefined) delete process.env["SCRIPTC_HEAP_CENSUS"];
else process.env["SCRIPTC_HEAP_CENSUS"] = saved;
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
if (!result.ok) throw new Error("compile failed");
const out = join(dir, "census.tsv");
const { stdout } = await exec(result.binaryPath, [], {
env: { ...process.env, SCRIPTC_HEAP_CENSUS: out },
timeout: 120_000,
});
const counts = new Map<string, number>();
for (const line of (await readFile(out, "utf8")).trimEnd().split("\n")) {
const [kind, cls, field, count] = line.split("\t");
counts.set(`${kind} ${cls}${field ? "." + field : ""}`, Number(count));
}
return { stdout, counts };
} finally {
if (saved === undefined) delete process.env["SCRIPTC_HEAP_CENSUS"];
else process.env["SCRIPTC_HEAP_CENSUS"] = saved;
await rm(dir, { recursive: true, force: true });
}
}
const EXPECTED: [string, number][] = [
// 100 roots and 246 leaves (i % 6 children each).
["new Node", 346],
["store Node.kids", 346],
["elements Node.kids", 246],
["len0 Node.kids", 263],
["len1 Node.kids", 17],
["len2 Node.kids", 17],
["len3-4 Node.kids", 33],
["len5-8 Node.kids", 16],
["len9-16 Node.kids", 0],
// Literals count where the store itself writes one.
["literal Node.kids", 0],
["store Node.tags", 346],
["literal Node.tags", 66],
// An undefined tags field counts as length 0.
["len0 Node.tags", 280],
["len2 Node.tags", 66],
["elements Node.tags", 132],
];
test.skipIf(process.platform !== "linux")(
"the census counts instances and array field stores at a normal exit",
async () => {
const { stdout, counts } = await census(PROGRAM);
expect(stdout).toBe("378\n");
for (const [key, count] of EXPECTED) expect([key, counts.get(key)]).toEqual([key, count]);
// Every store also computes the field's address.
expect(counts.get("access Node.kids")).toBeGreaterThanOrEqual(346 + 100);
},
240_000,
);
test.skipIf(process.platform !== "linux")(
"the census is written on process.exit too",
async () => {
const { stdout, counts } = await census(PROGRAM + "process.exit(0);\n");
expect(stdout).toBe("378\n");
expect(counts.get("new Node")).toBe(346);
},
240_000,
);
+60
View File
@@ -0,0 +1,60 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import ts from "typescript";
import { expect, test } from "vitest";
import { generateIrFields } from "../../scripts/generate-ir-fields.mjs";
const compilerDir = join(import.meta.dirname, "../../packages/compiler");
const irPath = join(compilerDir, "src/ir/ir.ts");
const generatedPath = join(compilerDir, "src/ir/fields.generated.ts");
const irText = readFileSync(irPath, "utf8");
const arrayGet = '{ kind: "arrayGet"; arr: IrExpr; index: IrExpr; type: IrType; loc: SrcLoc }';
/** ir.ts with `field` added to the arrayGet node. */
function withArrayGetField(field: string): string {
expect(irText).toContain(arrayGet);
return irText.replace(arrayGet, arrayGet.replace("loc: SrcLoc }", `loc: SrcLoc; ${field} }`));
}
/** The type errors of `generated` as fields.generated.ts, with `ir` as ir.ts,
* under the compiler package's options. */
function generatedErrors(ir: string, generated: string): string[] {
const parsed = ts.getParsedCommandLineOfConfigFile(
join(compilerDir, "tsconfig.json"),
{ noEmit: true, incremental: false },
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
)!;
const host = ts.createCompilerHost(parsed.options);
const readSourceFile = host.getSourceFile;
host.getSourceFile = (file, languageVersion, ...rest) =>
file === irPath
? ts.createSourceFile(file, ir, languageVersion)
: file === generatedPath
? ts.createSourceFile(file, generated, languageVersion)
: readSourceFile.call(host, file, languageVersion, ...rest);
const program = ts.createProgram([generatedPath], parsed.options, host);
return ts
.getPreEmitDiagnostics(program, program.getSourceFile(generatedPath))
.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
}
test("a nested record of scalar fields passes the checks once the readers are regenerated", () => {
const ir = withArrayGetField("counts?: { hits: number; misses: number | undefined }");
expect(generatedErrors(ir, generateIrFields(ir))).toEqual([]);
});
test("the record's own fields are checked: an added field or a widened scalar fails with its name", () => {
const generated = generateIrFields(
withArrayGetField("counts?: { hits: number; misses: number | undefined }"),
);
const added = generatedErrors(
withArrayGetField("counts?: { hits: number; misses: number | undefined; label: string }"),
generated,
);
expect(added).toEqual([`Type '"label"' does not satisfy the constraint 'never'.`]);
const widened = generatedErrors(
withArrayGetField("counts?: { hits: number | string; misses: number | undefined }"),
generated,
);
expect(widened).toEqual([`Type 'string' does not satisfy the constraint 'never'.`]);
});
+7 -2
View File
@@ -142,6 +142,9 @@ nativeToolchainTest(
"scr_url_",
"scr_json_parse",
"scr_date_",
// The generator runtime installs its tag reader with its first
// generator; a load-time constructor would retain it everywhere.
"scr_gen_tag",
]) {
expect(helloSymbols, `hello retains ${family}`).not.toContain(family);
}
@@ -159,8 +162,10 @@ nativeToolchainTest(
// Symbol absence is the primary reachability contract. Keep a deliberately
// roomy, platform-specific hello-world ceiling too: it catches losing
// section GC without pinning an exact linker/SDK byte count. The canonical
// Linux build is about 84KB and current Mach-O builds are about 70KB. Most
// section GC without pinning an exact linker/SDK byte count. The Linux
// build is about 65KB with lld (CI's pinned LLVM provides it) and about
// 94KB with GNU ld, which keeps a unit's string literals whole once any of
// them is reachable; current Mach-O builds are about 70KB. Most
// of Linux hello's runtime code is the cycle collector, which its exit
// path reaches (scr_lib_cleanup releases an array). ELF segments are page
// aligned, so the Linux size moves in 4 KiB steps whenever .text crosses
+23
View File
@@ -5,6 +5,7 @@ import {
sandboxBootstrapCommand,
sandboxEffectWorkerAllocation,
sandboxImageConfig,
sandboxNativeReference,
sandboxRunnerConfig,
sandboxTestSourceConfig,
sandboxTestWorkerAllocation,
@@ -97,6 +98,10 @@ test("the managed-image bootstrap pins the custom image's Node, pnpm, and LLVM",
expect(bootstrap).toContain("libclang-rt-22-dev");
expect(bootstrap).toContain("llvm-22-dev");
expect(bootstrap).toContain("ninja-build");
// CI links with ld.lld; without it Sandbox binaries link with GNU ld.
expect(bootstrap).toContain("lld-22");
const dockerfile = readFileSync(new URL("../../Dockerfile.sandbox", import.meta.url), "utf8");
expect(dockerfile).toContain("lld-22");
});
test("a custom image does not select the Sandbox team or project", () => {
@@ -154,6 +159,24 @@ test("ambiguous or malformed snapshot sources fail before creating workers", ()
);
});
test("a native reference checkout needs a prepared source and an absolute path", () => {
const snapshot = { SCRIPTC_SANDBOX_SNAPSHOT: "snap_ABC123" };
expect(sandboxNativeReference(snapshot)).toBeUndefined();
expect(
sandboxNativeReference({
...snapshot,
SCRIPTC_SANDBOX_NATIVE_REFERENCE: " /vercel/sandbox/prev/ ",
}),
).toBe("/vercel/sandbox/prev");
expect(() =>
sandboxNativeReference({ SCRIPTC_SANDBOX_NATIVE_REFERENCE: "/vercel/sandbox/prev" }),
).toThrow("needs SCRIPTC_SANDBOX_SNAPSHOT");
for (const path of ["prev", "/a/../b", "/a b", "/a;rm"])
expect(() =>
sandboxNativeReference({ ...snapshot, SCRIPTC_SANDBOX_NATIVE_REFERENCE: path }),
).toThrow("absolute path");
});
test("OIDC authentication wins and supplies its own Sandbox scope", () => {
const config = sandboxVercelConfig({
VERCEL_OIDC_TOKEN: "header.payload.signature",
+3 -1
View File
@@ -59,7 +59,9 @@ test("the production layout and metadata stage lowers statically", async () => {
});
for (const backend of ["llvm"] as const) {
test(`native LLVM layout and metadata emission (${backend})`, async () => {
// The sanitized dev build of the stage takes 170 to 300 s of CI time beside
// its shard's other tests.
test(`native LLVM layout and metadata emission (${backend})`, { timeout: 600_000 }, async () => {
const dir = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-emission-"),
);
@@ -18,6 +18,9 @@ test("the complete production frontend lowers with zero rejected statements", as
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
[
// The lowering holds about 3 GB of heap; Node's default limit is lower
// on hosts with less memory.
"--max-old-space-size=8192",
"--import",
"tsx",
"--input-type=module",
+2
View File
@@ -316,6 +316,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
// Weak-collection hashes mix 32-bit pointers in 64 bits.
"4591-weak-keys-union-records.ts",
"7742-weak-collections-many-keys.ts",
// zlib's portable build (CPU_NO_SIMD) compresses to Node's bytes.
"8405-zlib-compressed-bytes.ts",
])("the LLVM target runs %s byte-identically to Node", async (fixture) => {
const entry = join(repoRoot, "tests/corpus", fixture);
const outDir = await mkdtemp("/tmp/scriptc-wasi-");
+108
View File
@@ -1,14 +1,20 @@
import { spawnSync } from "node:child_process";
import { expect, test } from "vitest";
import {
cachedComparison,
cachegrindCounts,
compareArtifact,
compareCounts,
compareExecutables,
definedFunctionNames,
dynamicTierSymbols,
mergeReports,
nmOutput,
programInputsHash,
programLines,
programVerdict,
recordedBaseline,
timingVerdict,
} from "../../scripts/zero-cost-compare.mjs";
const ir = (functions: object[], globals: object[]) =>
@@ -191,3 +197,105 @@ test("the zero-cost gate fails differing programs unless they match the signatur
expect(verdict({}, false, failed, failed, true)).toEqual({ verdict: "both-fail", fails: true });
expect(verdict({}, true, ok, failed)).toEqual({ verdict: "build-error", fails: true });
});
test("a recorded baseline settles identical programs and shared build errors", () => {
const hashes = { ir: "a", llvm: "b", exe: "c" };
const entry = { ok: true, ms: 12, hashes, exeBytes: 100 };
expect(cachedComparison(entry, { ok: true }, { ...hashes })).toEqual({
baseline: { ok: true, ms: 12 },
artifacts: {
ir: { identical: true },
llvm: { identical: true },
exe: { identical: true, bytes: { baseline: 100, candidate: 100 } },
},
});
// Any other hash, a missing artifact or no record builds the baseline.
expect(cachedComparison(entry, { ok: true }, { ...hashes, exe: "d" })).toBeNull();
expect(
cachedComparison({ ...entry, hashes: { ...hashes, ir: null } }, { ok: true }, hashes),
).toBeNull();
expect(cachedComparison(undefined, { ok: true }, hashes)).toBeNull();
expect(cachedComparison(entry, { ok: false, error: "SC1090" }, {})).toBeNull();
const failed = { ok: false, ms: 3, error: "SC1090" };
expect(cachedComparison(failed, { ok: false, error: "SC1090" }, {})).toEqual({
baseline: { ok: false, error: "SC1090", ms: 3, cached: true },
artifacts: {},
});
expect(cachedComparison(failed, { ok: false, error: "SC1091" }, {})).toBeNull();
expect(cachedComparison(failed, { ok: true }, hashes)).toBeNull();
});
test("a recorded baseline applies only to the program inputs it was recorded for", () => {
const files: Record<string, string> = {
"/p/tests/corpus/a/main.ts": 'import { f } from "./lib.ts";',
"/p/tests/corpus/a/lib.ts": "export const f = 1;",
};
const read = (file: string) => Buffer.from(files[file]!);
const paths = Object.keys(files);
const inputs = programInputsHash("/p", paths, "release", read);
expect(programInputsHash("/p", [...paths].reverse(), "release", read)).toBe(inputs);
expect(programInputsHash("/p", paths, "speed", read)).not.toBe(inputs);
expect(programInputsHash("/q", paths, "release", read)).not.toBe(inputs);
// An edit to an import changes the hash as an edit to the entry does.
files["/p/tests/corpus/a/lib.ts"] = "export const f = 2;";
const edited = programInputsHash("/p", paths, "release", read);
expect(edited).not.toBe(inputs);
const entry = { inputs, ok: false, ms: 3, error: "SC1090" };
const cache = { programs: { "corpus/a/main.ts": entry } };
expect(recordedBaseline(cache, "corpus/a/main.ts", inputs)).toBe(entry);
// A record of the old sources must not settle the new sources as both-fail or identical.
expect(recordedBaseline(cache, "corpus/a/main.ts", edited)).toBeUndefined();
expect(recordedBaseline(cache, "corpus/b/main.ts", inputs)).toBeUndefined();
expect(recordedBaseline(null, "corpus/a/main.ts", inputs)).toBeUndefined();
});
test("merged gate shards judge compile time and comparisons over every shard", () => {
const trees = { baseline: { revision: "a" }, candidate: { revision: "b" } };
const row = (id: string, ratio: number) => ({
id,
baselineMs: [100, 100, 100],
candidateMs: [100 * ratio, 100 * ratio, 100 * ratio],
ratios: [ratio, ratio, ratio],
});
const shard = (
programs: object[],
rows: object[],
failures: string[] = [],
globals: string[] = [],
) => ({
schema: 1,
trees,
programs,
failures: [...failures, ...globals],
globalFailures: globals,
timing: { runs: 3, maxGrowth: 0.02, rows, groups: {} },
elapsedMs: 10,
});
const identical = (id: string) => ({ id, verdict: "identical", artifacts: {} });
const fast = shard([identical("bench/a")], [row("bench/a", 1.0)]);
// Alone, this shard's bench is slower; with the other shard it is not.
const slow = shard(
[
{
id: "corpus/x.ts",
verdict: "differs",
artifacts: { exe: { identical: false, first: "resized f" } },
},
],
[row("bench/b", 1.03)],
["corpus/x.ts: differs"],
["bench compile time grew +3.00% (CI low +3.00%)"],
);
const { report, timingLines } = mergeReports([fast, slow]);
expect(report.counts).toEqual({ identical: 1, differs: 1 });
expect(report.failures).toEqual(["corpus/x.ts: differs"]);
expect(report.verdict).toBe("fail");
expect(timingLines).toEqual([timingVerdict("bench", report.timing.rows, 0.02).line]);
expect(timingVerdict("bench", report.timing.rows, 0.02).fails).toBe(false);
expect(programLines(report.programs[1])).toEqual(["DIFFERS corpus/x.ts exe: resized f"]);
expect(mergeReports([fast]).report.verdict).toBe("pass");
expect(mergeReports([shard([], [])]).report.failures).toEqual([
"no program built with both checkouts, so nothing was compared",
]);
expect(() => mergeReports([fast, fast])).toThrow(/more than one gate report/);
});