mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
@@ -0,0 +1,85 @@
|
||||
// LifetimeRegion (@scriptc/experimental): freeing a region whose objects
|
||||
// own a cycle runs a limited cycle pass over what the region owned. That
|
||||
// pass also reaches a shared object the region pointed at, which is a
|
||||
// candidate of its own (it lost a reference) and stays referenced from
|
||||
// outside. The shared object must stay a valid candidate after the pass:
|
||||
// it dies afterwards, later passes run, and the program prints the same
|
||||
// as under Node.js.
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import { LifetimeRegion } from "@scriptc/experimental";
|
||||
|
||||
class Shared {
|
||||
readonly label: string;
|
||||
peers: Shared[] = [];
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
// Ordinary objects the region's instance owns: a cycle that points back
|
||||
// into the region and at the shared object.
|
||||
class Owned {
|
||||
holder: Holder | undefined = undefined;
|
||||
shared: Shared | undefined = undefined;
|
||||
partner: Owned | undefined = undefined;
|
||||
}
|
||||
|
||||
class Holder implements ProgramLifetime {
|
||||
owned: Owned | undefined = undefined;
|
||||
readonly id: number;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// Cyclic garbage, so the collector runs passes of its own.
|
||||
class Ring {
|
||||
next: Ring | undefined = undefined;
|
||||
readonly value: number;
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function churn(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = new Ring(i);
|
||||
const b = new Ring(i + 1);
|
||||
a.next = b;
|
||||
b.next = a;
|
||||
total += a.value + b.next.value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function round(k: number): string {
|
||||
let shared: Shared | undefined = new Shared(`shared ${k}`);
|
||||
// A second reference, dropped below: the shared object becomes a candidate.
|
||||
let keep: Shared[] = [shared];
|
||||
shared.peers.push(new Shared(`peer ${k}`));
|
||||
const region = new LifetimeRegion();
|
||||
region.run(() => {
|
||||
const holder = new Holder(k);
|
||||
const owned = new Owned();
|
||||
const partner = new Owned();
|
||||
owned.partner = partner;
|
||||
partner.partner = owned;
|
||||
owned.holder = holder;
|
||||
owned.shared = shared;
|
||||
holder.owned = owned;
|
||||
});
|
||||
keep = [];
|
||||
region.release();
|
||||
const label = shared.label + " / " + shared.peers[0]!.label + " / " + keep.length;
|
||||
// The shared object dies, and the passes that follow read the candidate buffers.
|
||||
shared = undefined;
|
||||
churn(2000);
|
||||
return label;
|
||||
}
|
||||
|
||||
for (let k = 0; k < 20; k++) {
|
||||
const label = round(k);
|
||||
if (k % 5 === 0) console.log(label);
|
||||
}
|
||||
console.log("churn", churn(10000));
|
||||
@@ -0,0 +1,87 @@
|
||||
// Locals assigned from integer typed-array reads and used as indices, loop
|
||||
// starts and loop bounds. A read past the end is undefined, written here as
|
||||
// NaN (`?? NaN`): an index of NaN reads undefined and writes nothing, and a
|
||||
// loop that starts at NaN never runs. Results must match Node.
|
||||
|
||||
// A permutation chain with in-place reversals, as fannkuch-redux does.
|
||||
function flips(perm: Int32Array): number {
|
||||
const pp = new Int32Array(perm.length);
|
||||
pp.set(perm);
|
||||
let count = 0;
|
||||
let p0 = pp[0]!;
|
||||
while (pp[p0] !== 0) {
|
||||
count++;
|
||||
for (let i = 1, j = p0 - 1; i < j; i++, j--) {
|
||||
const t = pp[i]!;
|
||||
pp[i] = pp[j]!;
|
||||
pp[j] = t;
|
||||
}
|
||||
const t = pp[p0]!;
|
||||
pp[p0] = p0;
|
||||
p0 = t;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
const perms = [
|
||||
[3, 1, 2, 0],
|
||||
[2, 0, 3, 1, 4],
|
||||
[4, 3, 2, 1, 0],
|
||||
[1, 2, 3, 4, 5, 0],
|
||||
];
|
||||
console.log("flips", perms.map((p) => flips(Int32Array.from(p))).join(","));
|
||||
|
||||
// Indices that leave the array: a value past the end, a negative value, and
|
||||
// one above 2^31 in a Uint32Array.
|
||||
const links = new Int32Array([2, 5, -1, 0, 1]);
|
||||
const seen: string[] = [];
|
||||
let at = links[0]!;
|
||||
for (let step = 0; step < 6; step++) {
|
||||
const next = links[at] ?? NaN;
|
||||
seen.push(`${at}->${next}`);
|
||||
links[at] = step;
|
||||
at = next;
|
||||
}
|
||||
console.log("links", seen.join(" "), links.join(","));
|
||||
|
||||
const wide = new Uint32Array([1, 0xfffffff0, 3, 0]);
|
||||
const hops: number[] = [];
|
||||
let w = wide[0]!;
|
||||
for (let step = 0; step < 4; step++) {
|
||||
const v = wide[w] ?? NaN;
|
||||
hops.push(v);
|
||||
w = v;
|
||||
}
|
||||
console.log("uint32", hops.join(","), w);
|
||||
|
||||
// A loop whose bounds come from reads, one of them past the end.
|
||||
const bounds = new Uint8Array([5, 2, 9]);
|
||||
const marks: number[] = [];
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const top = bounds[k] ?? NaN;
|
||||
let sum = 0;
|
||||
for (let i = 0, j = top - 1; i < j; i++, j--) sum += i * 10 + j;
|
||||
for (let i = top - 1; i >= 0; i -= 2) sum += i;
|
||||
marks.push(sum);
|
||||
}
|
||||
console.log("bounds", marks.join(","));
|
||||
|
||||
// Sums and products of read locals used as indices.
|
||||
const table = new Int16Array([3, -2, 7, 1, 0, 4, 6, 2]);
|
||||
const picks: string[] = [];
|
||||
for (let i = 0; i < table.length; i++) {
|
||||
const a = table[i]!;
|
||||
const b = table[(i + 1) % table.length]!;
|
||||
picks.push(`${table[a + b] ?? -100}/${table[a * 2] ?? -100}/${table[b - a] ?? -100}`);
|
||||
}
|
||||
console.log("sums", picks.join(" "));
|
||||
|
||||
// A float element as an index keeps its double meaning.
|
||||
const floats = new Float64Array([1.5, 2, 0]);
|
||||
const out = new Int32Array([10, 20, 30]);
|
||||
const read: string[] = [];
|
||||
for (let i = 0; i < floats.length; i++) {
|
||||
const f = floats[i]!;
|
||||
read.push(String(out[f] ?? -1));
|
||||
out[f] = i;
|
||||
}
|
||||
console.log("floats", read.join(","), out.join(","));
|
||||
@@ -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);
|
||||
@@ -7,6 +7,11 @@
|
||||
// untyped code reads the same properties, and a write whose send fails
|
||||
// after the peer reset the connection answers false. An echo client that
|
||||
// waits for 'drain' when write answers false moves every byte.
|
||||
//
|
||||
// The server picks each connection's handling from `mode` when it accepts
|
||||
// it. The lookup client's bytes fit in the kernel's buffers, so its 'drain'
|
||||
// can come before the server accepts that connection; the program waits for
|
||||
// the accept before it changes `mode`.
|
||||
import { connect, createServer, Socket } from "node:net";
|
||||
import type { LookupAddress, LookupAllOptions, LookupOneOptions } from "node:dns";
|
||||
|
||||
@@ -113,6 +118,15 @@ function afterReset(port: number): Promise<void> {
|
||||
|
||||
let paused: Socket | null = null;
|
||||
let mode = "echo";
|
||||
let accepted: (() => void) | null = null;
|
||||
|
||||
/** Resolves when the server accepts its next connection. */
|
||||
function nextConnection(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
accepted = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
const server = createServer((socket: Socket) => {
|
||||
if (mode === "echo") socket.pipe(socket);
|
||||
else if (mode === "drain") socket.resume();
|
||||
@@ -120,6 +134,11 @@ const server = createServer((socket: Socket) => {
|
||||
socket.pause();
|
||||
paused = socket;
|
||||
} else socket.destroy();
|
||||
if (accepted !== null) {
|
||||
const resolve = accepted;
|
||||
accepted = null;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
@@ -127,7 +146,9 @@ server.listen(0, "127.0.0.1", async () => {
|
||||
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
||||
await echo(port);
|
||||
mode = "drain";
|
||||
const lookupAccepted = nextConnection();
|
||||
await lookupPending(port);
|
||||
await lookupAccepted;
|
||||
mode = "pause";
|
||||
await largeWrite(port);
|
||||
mode = "end";
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// Option callbacks that keep their `callback` argument: a Writable whose
|
||||
// write() keeps every third callback and calls it later, a write that
|
||||
// calls its callback and keeps it (`cb(); keep(cb)`) and calls it again
|
||||
// after later writes (ERR_MULTIPLE_CALLBACK), and a Transform that calls
|
||||
// back synchronously, from setImmediate, and from a promise. Every write
|
||||
// and transform completes in Node's order. Every write and writev call
|
||||
// receives the same callback, as in Node (a Set, a WeakSet and a WeakMap
|
||||
// find it again); a transform call receives a new one each time.
|
||||
import { Transform, Writable } from "node:stream";
|
||||
|
||||
type Done = (error?: Error | null) => void;
|
||||
|
||||
const kept: Done[] = [];
|
||||
const log: string[] = [];
|
||||
|
||||
const deferred = new Writable({
|
||||
highWaterMark: 4,
|
||||
write(chunk: Buffer, _encoding: string, callback: Done) {
|
||||
const text = chunk.toString();
|
||||
if (Number(text) % 3 === 0) {
|
||||
kept.push(callback);
|
||||
log.push(`kept ${text}`);
|
||||
setImmediate(() => {
|
||||
const cb = kept.shift();
|
||||
log.push(`late ${text}`);
|
||||
if (cb) cb();
|
||||
});
|
||||
return;
|
||||
}
|
||||
log.push(`write ${text}`);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
deferred.write(String(i), () => log.push(`done ${i}`));
|
||||
}
|
||||
deferred.end(() => {
|
||||
console.log(log.join("\n"));
|
||||
twice();
|
||||
});
|
||||
|
||||
function twice(): void {
|
||||
let saved: Done | null = null;
|
||||
const w = new Writable({
|
||||
write(chunk: Buffer, _encoding: string, callback: Done) {
|
||||
const text = chunk.toString();
|
||||
console.log("twice write", text);
|
||||
callback();
|
||||
if (text === "a") saved = callback;
|
||||
},
|
||||
});
|
||||
w.on("error", (error: Error & { code?: string }) => {
|
||||
console.log("twice error", error.code);
|
||||
});
|
||||
w.write("a");
|
||||
w.write("b");
|
||||
w.write("c");
|
||||
setImmediate(() => {
|
||||
const cb = saved;
|
||||
if (cb) cb();
|
||||
setImmediate(transforms);
|
||||
});
|
||||
}
|
||||
|
||||
function transforms(): void {
|
||||
let n = 0;
|
||||
const t = new Transform({
|
||||
transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: Buffer) => void) {
|
||||
const k = n++;
|
||||
const out = Buffer.from(`[${chunk.toString()}]`);
|
||||
if (k % 3 === 0) callback(null, out);
|
||||
else if (k % 3 === 1) setImmediate(() => callback(null, out));
|
||||
else Promise.resolve().then(() => callback(null, out));
|
||||
},
|
||||
});
|
||||
const parts: string[] = [];
|
||||
t.on("data", (chunk: Buffer) => parts.push(chunk.toString()));
|
||||
t.on("end", () => {
|
||||
console.log("transform", parts.join(""));
|
||||
weakKeys();
|
||||
});
|
||||
for (let i = 0; i < 9; i++) t.write(String(i));
|
||||
t.end();
|
||||
}
|
||||
|
||||
function weakKeys(): void {
|
||||
const seen = new WeakSet<Done>();
|
||||
const notes = new WeakMap<Done, string>();
|
||||
const held = new Set<Done>();
|
||||
const w = new Writable({
|
||||
write(chunk: Buffer, _encoding: string, callback: Done) {
|
||||
const text = chunk.toString();
|
||||
console.log("weak write", text, seen.has(callback), notes.get(callback) ?? "none", held.has(callback));
|
||||
seen.add(callback);
|
||||
notes.set(callback, text);
|
||||
if (text === "q") held.add(callback);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
for (const text of ["p", "q", "r", "s"]) w.write(text);
|
||||
w.end(batches);
|
||||
}
|
||||
|
||||
function batches(): void {
|
||||
const seen = new WeakSet<Done>();
|
||||
let first: Done | null = null;
|
||||
const w = new Writable({
|
||||
writev(chunks: Array<{ chunk: Buffer }>, callback: Done) {
|
||||
const text = chunks.map((c) => c.chunk.toString()).join("");
|
||||
console.log("writev", text, seen.has(callback), first === callback);
|
||||
seen.add(callback);
|
||||
if (first === null) first = callback;
|
||||
setImmediate(() => callback());
|
||||
},
|
||||
});
|
||||
w.write("a");
|
||||
w.write("b");
|
||||
w.write("c");
|
||||
w.end(weakTransform);
|
||||
}
|
||||
|
||||
function weakTransform(): void {
|
||||
type TransformDone = (error?: Error | null, data?: Buffer) => void;
|
||||
const seen = new WeakSet<TransformDone>();
|
||||
const held = new Set<TransformDone>();
|
||||
const t = new Transform({
|
||||
transform(chunk: Buffer, _encoding: string, callback: TransformDone) {
|
||||
const text = chunk.toString();
|
||||
console.log("weak transform", text, seen.has(callback), held.has(callback));
|
||||
seen.add(callback);
|
||||
if (text === "x") held.add(callback);
|
||||
callback(null, chunk);
|
||||
},
|
||||
});
|
||||
t.resume();
|
||||
t.on("end", () => console.log("weak transform end"));
|
||||
for (const text of ["x", "y", "z"]) t.write(text);
|
||||
t.end();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Streams dropped before they end, after their option callbacks ran: a
|
||||
// Writable, an object-mode Transform read in part, and a Writable whose
|
||||
// destroy() never calls back. Each stream keeps the last `callback` it
|
||||
// passed, which holds the stream, so dropping one leaves a cycle; the
|
||||
// sanitized lane's exit audit checks that it is reclaimed.
|
||||
import { Readable, Transform, Writable } from "node:stream";
|
||||
|
||||
type Done = (error?: Error | null) => void;
|
||||
|
||||
let written = 0;
|
||||
function writeSome(n: number): void {
|
||||
const w = new Writable({
|
||||
write(_chunk: Buffer, _encoding: string, callback: Done) {
|
||||
written++;
|
||||
callback();
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < n; i++) w.write("x");
|
||||
}
|
||||
|
||||
let transformed = 0;
|
||||
function transformSome(): void {
|
||||
const t = new Transform({
|
||||
objectMode: true,
|
||||
transform(chunk: number, _encoding: string, callback: (error?: Error | null, data?: number) => void) {
|
||||
transformed++;
|
||||
callback(null, chunk * 2);
|
||||
},
|
||||
});
|
||||
Readable.from([1, 2, 3, 4, 5, 6]).pipe(t);
|
||||
t.once("data", (value: number) => console.log("first", value));
|
||||
}
|
||||
|
||||
function destroyNoCallback(): void {
|
||||
const w = new Writable({
|
||||
write(_chunk: Buffer, _encoding: string, callback: Done) {
|
||||
callback();
|
||||
},
|
||||
destroy(_error: Error | null, _callback: Done) {
|
||||
console.log("destroy called");
|
||||
},
|
||||
});
|
||||
w.write("y", () => {
|
||||
w.destroy();
|
||||
console.log("destroyed", w.destroyed, w.closed);
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < 20; i++) writeSome(5);
|
||||
transformSome();
|
||||
destroyNoCallback();
|
||||
setImmediate(() => {
|
||||
setImmediate(() => console.log("written", written, "transformed", transformed));
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
// A class that extends Array: construction (one number argument sets the
|
||||
// length, other arguments become the elements), length and index writes,
|
||||
// Array.isArray, instanceof, constructor, inherited methods, the subclass's
|
||||
// own fields and methods, and how console.log and JSON show the instance.
|
||||
class Stack extends Array<number> {
|
||||
pushes = 0;
|
||||
|
||||
top(): number | undefined {
|
||||
return this[this.length - 1];
|
||||
}
|
||||
|
||||
add(value: number): this {
|
||||
this.push(value);
|
||||
this.pushes++;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const s = new Stack();
|
||||
s.add(1).add(2).add(3);
|
||||
console.log(s.length, s.top(), s.pushes);
|
||||
console.log(Array.isArray(s), s instanceof Stack, s instanceof Array, s.constructor === Stack);
|
||||
console.log(s);
|
||||
console.log(JSON.stringify(s));
|
||||
s[5] = 9;
|
||||
console.log(s.length, s[4], s);
|
||||
s.length = 2;
|
||||
console.log(s, s.indexOf(2), s.includes(3), s.join("-"));
|
||||
s.reverse();
|
||||
console.log(s.top(), s);
|
||||
|
||||
const sized = new Stack(3);
|
||||
console.log(sized.length, sized, 0 in sized);
|
||||
const listed = new Stack(4, 5);
|
||||
console.log(listed.length, listed);
|
||||
|
||||
try {
|
||||
new Stack(-1);
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
|
||||
for (const v of listed) console.log("item", v);
|
||||
console.log([...listed, 6], Object.keys(listed));
|
||||
@@ -0,0 +1,37 @@
|
||||
// Array.prototype methods that create a new array construct it through the
|
||||
// subclass (ArraySpeciesCreate): map, filter, slice, splice, concat, flat
|
||||
// and flatMap return an instance of the subclass, built by calling its
|
||||
// constructor with the length the specification passes. Methods that
|
||||
// return the receiver keep it, and Array.of / Array.from inherited as
|
||||
// statics construct the subclass too.
|
||||
class Logged extends Array<number> {
|
||||
constructor(...args: number[]) {
|
||||
super(...args);
|
||||
console.log("construct", args.length === 1 ? `length ${args[0]}` : `items ${args.join(",")}`);
|
||||
}
|
||||
|
||||
sum(): number {
|
||||
let total = 0;
|
||||
for (const v of this) total += v;
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
const base = Logged.of(1, 2, 3, 4);
|
||||
console.log(base instanceof Logged, base);
|
||||
const doubled = base.map((v) => v * 2);
|
||||
console.log(doubled instanceof Logged, (doubled as Logged).sum(), doubled);
|
||||
const even = base.filter((v) => v % 2 === 0);
|
||||
console.log(even instanceof Logged, even);
|
||||
const part = base.slice(1, 3);
|
||||
console.log(part instanceof Logged, part);
|
||||
const removed = base.splice(0, 1);
|
||||
console.log(removed instanceof Logged, removed, base);
|
||||
const joined = base.concat([7, 8]);
|
||||
console.log(joined instanceof Logged, joined.length, joined);
|
||||
const flat = base.flatMap((v) => [v, v]);
|
||||
console.log(flat instanceof Logged, flat.length);
|
||||
const filled = base.fill(0, 0, 1);
|
||||
console.log(filled === base, base);
|
||||
const from = Logged.from([5, 6]);
|
||||
console.log(from instanceof Logged, (from as Logged).sum());
|
||||
@@ -0,0 +1,16 @@
|
||||
// lru-cache's ZeroArray: a JavaScript class extending Array whose
|
||||
// constructor fills the new array with zeros, constructed directly and
|
||||
// through map (species construction runs the constructor again).
|
||||
class ZeroArray extends Array {
|
||||
constructor(size) {
|
||||
super(size);
|
||||
this.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
const z = new ZeroArray(4);
|
||||
z[1] = 7;
|
||||
console.log(z, z.length, z instanceof ZeroArray, Array.isArray(z));
|
||||
const m = z.map((v) => v + 1);
|
||||
console.log(m, m instanceof ZeroArray);
|
||||
console.log(z.slice(1) instanceof ZeroArray, String(z));
|
||||
@@ -0,0 +1,21 @@
|
||||
// A subclass instance passed where a plain array is expected is the same
|
||||
// object: writes through either view are seen through the other, and
|
||||
// species construction still applies in code typed with the plain array.
|
||||
class Path extends Array<string> {
|
||||
label = "path";
|
||||
}
|
||||
|
||||
function addSegment(parts: string[], segment: string): string[] {
|
||||
parts.push(segment);
|
||||
return parts.filter((part) => part.length > 0);
|
||||
}
|
||||
|
||||
const p = new Path();
|
||||
p.push("a", "", "b");
|
||||
const filtered = addSegment(p, "c");
|
||||
console.log(p.length, filtered.length, filtered instanceof Path, (filtered as Path).label);
|
||||
const view: string[] = p;
|
||||
view[0] = "z";
|
||||
console.log(p[0], view === p, p.label);
|
||||
console.log(p.map((s) => s.toUpperCase()));
|
||||
console.log(Object.keys(p));
|
||||
@@ -0,0 +1,36 @@
|
||||
// An Array subclass whose elements and fields refer back to the instance:
|
||||
// the cycles are collected once nothing outside them is reachable, and
|
||||
// the elements and fields read back as Node reads them.
|
||||
class Member {
|
||||
name: string;
|
||||
group: Group | undefined;
|
||||
constructor(name: string, group: Group | undefined) {
|
||||
this.name = name;
|
||||
this.group = group;
|
||||
}
|
||||
}
|
||||
|
||||
class Group extends Array<Member> {
|
||||
owner: Member | undefined = undefined;
|
||||
|
||||
add(name: string): Member {
|
||||
const member = new Member(name, this);
|
||||
this.push(member);
|
||||
return member;
|
||||
}
|
||||
}
|
||||
|
||||
function build(round: number): number {
|
||||
const g = new Group();
|
||||
const lead = g.add(`lead${round}`);
|
||||
g.add(`member${round}`);
|
||||
g.owner = lead;
|
||||
return g.length + (g[0]!.group === g ? 1 : 0) + (g.owner === lead ? 1 : 0);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 200; i++) total += build(i);
|
||||
console.log(total);
|
||||
const kept = new Group();
|
||||
kept.add("a").group?.add("b");
|
||||
console.log(kept.length, kept.map((m) => m.name).join(","), kept[1]!.group === kept);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Array operations on an Array subclass instance: iteration, spread,
|
||||
// destructuring, the mutators that return the instance itself, searches,
|
||||
// length writes, Array.isArray through unknown, instanceof through unknown,
|
||||
// and JSON.
|
||||
class Row extends Array<string> {
|
||||
tag = "row";
|
||||
#reads = 0;
|
||||
|
||||
static of2(a: string, b: string): Row {
|
||||
const row = new Row();
|
||||
row.push(a, b);
|
||||
return row;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
this.#reads++;
|
||||
return this.length;
|
||||
}
|
||||
|
||||
get reads(): number {
|
||||
return this.#reads;
|
||||
}
|
||||
|
||||
label(): string {
|
||||
return `${this.tag}:${this.join("|")}`;
|
||||
}
|
||||
}
|
||||
|
||||
const r = new Row();
|
||||
console.log(r.push("b", "a", "c"), r.length, r.label());
|
||||
const [first, ...rest] = r;
|
||||
console.log(first, rest, [...r].length);
|
||||
for (const [i, v] of r.entries()) console.log(i, v);
|
||||
console.log(r.sort() === r, r.label(), r.reverse() === r, r.label());
|
||||
console.log(r.fill("x", 1) === r, r.label(), r.indexOf("x"), r.lastIndexOf("x"), r.at(-1));
|
||||
console.log(r.pop(), r.shift(), r.length, r.unshift("z"), r.label());
|
||||
r.length = 0;
|
||||
console.log(r.length, r.label(), JSON.stringify({ r }));
|
||||
const u: unknown = r;
|
||||
console.log(Array.isArray(u), u instanceof Row, u instanceof Array);
|
||||
const rows: Row[] = [new Row(), new Row()];
|
||||
rows[1]!.push("q");
|
||||
console.log(rows.map((row) => row.label()).join(" "));
|
||||
console.log({ nested: rows[1] });
|
||||
const pair = Row.of2("m", "n");
|
||||
console.log(pair.size, pair.size, pair.reads, pair.label(), pair instanceof Row);
|
||||
@@ -0,0 +1,21 @@
|
||||
// Classes extending Array reached through expressions rather than
|
||||
// declarations and locals: a construction used in place, a class
|
||||
// expression held by a binding, and an instanceof test whose class the
|
||||
// program never constructs.
|
||||
class Sized extends Array {
|
||||
describe() {
|
||||
return `sized ${this.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(new Sized(3).length, new Sized(2).describe(), new Sized(1, 2).join("-"));
|
||||
console.log(new Sized(4).fill(1), (new Sized(2)).length);
|
||||
|
||||
const Holder = class extends Array {};
|
||||
const held = new Holder();
|
||||
held.push("a", "b");
|
||||
console.log(held instanceof Holder, held instanceof Array, [] instanceof Holder, held.length);
|
||||
|
||||
class Unbuilt extends Array {}
|
||||
const values = [[1, 2], "text", new Sized(1)];
|
||||
for (const value of values) console.log(value instanceof Unbuilt, Array.isArray(value));
|
||||
@@ -0,0 +1,52 @@
|
||||
// map, filter and flatMap on a class that extends Array construct their
|
||||
// result through the class before the first callback runs, with the
|
||||
// receiver's length for map and 0 for filter and flatMap.
|
||||
class Logged extends Array<number> {
|
||||
constructor(...args: number[]) {
|
||||
super(...args);
|
||||
console.log("construct", args.length === 1 ? `length ${args[0]}` : `${args.length} items`);
|
||||
}
|
||||
}
|
||||
|
||||
const source = Logged.from([1, 2, 3]);
|
||||
console.log("map");
|
||||
const mapped = source.map((value) => {
|
||||
console.log(" callback", value);
|
||||
return value * 10;
|
||||
});
|
||||
console.log(mapped, mapped instanceof Logged);
|
||||
|
||||
console.log("filter");
|
||||
const kept = source.filter((value) => {
|
||||
console.log(" callback", value);
|
||||
return value !== 2;
|
||||
});
|
||||
console.log(kept, kept instanceof Logged);
|
||||
|
||||
console.log("flatMap");
|
||||
const spread = source.flatMap((value) => {
|
||||
console.log(" callback", value);
|
||||
return [value, -value];
|
||||
});
|
||||
console.log(spread.length, spread instanceof Logged);
|
||||
|
||||
console.log("map to strings");
|
||||
const labels = source.map((value) => {
|
||||
console.log(" callback", value);
|
||||
return `#${value}`;
|
||||
});
|
||||
console.log(labels, labels instanceof Logged);
|
||||
|
||||
console.log("throwing callback");
|
||||
try {
|
||||
source.map((value) => {
|
||||
if (value === 2) throw new Error(`stopped at ${value}`);
|
||||
return value;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
|
||||
console.log("plain receiver");
|
||||
const plain: number[] = [4, 5];
|
||||
console.log(plain.map((value) => value + 1), plain.filter((value) => value > 4));
|
||||
@@ -0,0 +1,33 @@
|
||||
// A cycle-capable class that extends Array (its elements and its own field
|
||||
// can close a cycle) whose map results take other element types: the
|
||||
// collector traces and tears down each instance by the elements it holds.
|
||||
// Garbage cycles make the collector run while the results are alive.
|
||||
class Holder {
|
||||
group: Group | null = null;
|
||||
}
|
||||
|
||||
class Group extends Array<Holder | null> {
|
||||
owner: Holder | null = null;
|
||||
}
|
||||
|
||||
function churn(rounds: number): number {
|
||||
let made = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const holder = new Holder();
|
||||
const group = new Group();
|
||||
group.owner = holder;
|
||||
holder.group = group;
|
||||
group.push(holder);
|
||||
made++;
|
||||
}
|
||||
return made;
|
||||
}
|
||||
|
||||
const groups = new Group();
|
||||
groups.push(new Holder(), null, new Holder());
|
||||
const numbers = groups.map((_, i) => i * 1.5 + 1e6);
|
||||
const labels = groups.map((_, i) => `label-${i}`);
|
||||
console.log(churn(20000));
|
||||
console.log(numbers instanceof Group, numbers.length, numbers.join(","));
|
||||
console.log(labels instanceof Group, labels.join(","));
|
||||
console.log(churn(20000), numbers[2], labels[1]);
|
||||
@@ -0,0 +1,17 @@
|
||||
// Releasing a long chain of instances of a class that extends Array, each
|
||||
// holding the next one, tears them down without growing the native stack.
|
||||
class Link extends Array<number> {
|
||||
next: Link | null = null;
|
||||
}
|
||||
|
||||
let head: Link | null = new Link();
|
||||
let tail = head;
|
||||
for (let i = 0; i < 300000; i++) {
|
||||
const link = new Link();
|
||||
link.push(i);
|
||||
tail.next = link;
|
||||
tail = link;
|
||||
}
|
||||
console.log(tail.length, tail[0]);
|
||||
head = null;
|
||||
console.log("released", head);
|
||||
@@ -0,0 +1,32 @@
|
||||
// map, filter and flatMap read the receiver's length before the species
|
||||
// constructor runs: elements the constructor appends to the receiver are
|
||||
// not visited.
|
||||
let watched: Growing | null = null;
|
||||
|
||||
class Growing extends Array<number> {
|
||||
constructor(...args: number[]) {
|
||||
super(...args);
|
||||
if (watched !== null) watched.push(watched.length * 100);
|
||||
}
|
||||
}
|
||||
|
||||
const source = Growing.from([1, 2, 3]);
|
||||
watched = source;
|
||||
const visits: number[] = [];
|
||||
const doubled = source.map((value) => {
|
||||
visits.push(value);
|
||||
return value * 2;
|
||||
});
|
||||
console.log(doubled.join(","), visits.join(","), source.join(","));
|
||||
visits.length = 0;
|
||||
const kept = source.filter((value) => {
|
||||
visits.push(value);
|
||||
return value > 1;
|
||||
});
|
||||
console.log(kept.join(","), visits.join(","), source.length);
|
||||
visits.length = 0;
|
||||
const spread = source.flatMap((value) => {
|
||||
visits.push(value);
|
||||
return [value];
|
||||
});
|
||||
console.log(spread.length, visits.length, source.length);
|
||||
@@ -0,0 +1,26 @@
|
||||
// A class that extends Array whose constructor's length parameter is
|
||||
// optional or has a default takes the species length argument: map,
|
||||
// filter and slice construct their results through it.
|
||||
class Optional extends Array<number> {
|
||||
constructor(length?: number) {
|
||||
super(length ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
class Defaulted extends Array<string> {
|
||||
tag: string;
|
||||
constructor(length = 0, tag = "none") {
|
||||
super(length);
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
const optional = Optional.from([1, 2, 3]);
|
||||
const mapped = optional.map((value) => value * 10);
|
||||
console.log(mapped instanceof Optional, mapped.join(","), optional.slice(1) instanceof Optional);
|
||||
console.log(optional.filter((value) => value !== 2).join(","));
|
||||
|
||||
const defaulted = new Defaulted(2).fill("x");
|
||||
const sliced = defaulted.slice(0, 1);
|
||||
if (sliced instanceof Defaulted) console.log(sliced.length, sliced.tag);
|
||||
console.log(defaulted.map((s) => s + "!").join(","));
|
||||
@@ -0,0 +1,118 @@
|
||||
// Callbacks over a number[] or string[] that holds elements written
|
||||
// through an unknown[] view, when the callback never reads the element:
|
||||
// it declares no element parameter, or never references the one it
|
||||
// declares. The methods visit every element as in Node, and filter keeps
|
||||
// a written element as it is.
|
||||
|
||||
const numbers: number[] = [1, 2];
|
||||
const numbersView: unknown[] = numbers;
|
||||
numbersView.push("x", undefined, { k: 1 });
|
||||
numbers.push(5);
|
||||
|
||||
// Inline callbacks (fused loops).
|
||||
console.log(numbers.filter(() => true));
|
||||
console.log(numbers.filter((_, i) => i % 2 === 0));
|
||||
console.log(numbers.map(() => 0));
|
||||
console.log(numbers.map((_, i) => i * 10));
|
||||
console.log(numbers.some(() => false), numbers.every((_v) => true));
|
||||
console.log(numbers.findIndex((_, i) => i === 2), numbers.findLastIndex((_, i) => i < 3));
|
||||
console.log(numbers.find(() => false), numbers.findLast((_, i) => i === 0));
|
||||
console.log(numbers.reduce((s) => s + 1, 0), numbers.reduce((s, v) => s + 1, 100));
|
||||
console.log(numbers.reduceRight((s, _v, i) => s + i, 0));
|
||||
let visits = 0;
|
||||
numbers.forEach((_, i) => {
|
||||
visits += i;
|
||||
});
|
||||
console.log(visits);
|
||||
console.log(numbers.filter((_, i) => i > 1).map((_, i) => i));
|
||||
console.log(numbers.flatMap(() => [7]), numbers.flatMap((_, i) => [i, -i]));
|
||||
console.log(numbers.filter(() => true).length, typeof (numbers.filter(() => true) as unknown[])[2]);
|
||||
|
||||
// Callback values (shared helpers).
|
||||
const always = (): boolean => true;
|
||||
const never = (): boolean => false;
|
||||
const count = (s: number): number => s + 1;
|
||||
console.log(numbers.filter(always));
|
||||
console.log(numbers.some(never), numbers.every(always), numbers.find(never));
|
||||
console.log(numbers.findIndex(never), numbers.reduce(count, 0));
|
||||
console.log(numbers.map(always));
|
||||
|
||||
// A kept written element is the same value in the result.
|
||||
const kept = numbers.filter(always) as unknown[];
|
||||
console.log(kept[2] === "x", kept[3] === undefined, 3 in kept, kept[4], kept.length);
|
||||
|
||||
// string[] the same way.
|
||||
const words: string[] = ["a", "b"];
|
||||
const wordsView: unknown[] = words;
|
||||
wordsView.push(7, null);
|
||||
console.log(words.filter(() => true), words.map((_, i) => `#${i}`));
|
||||
console.log(words.filter((_w, i) => i !== 1), words.reduce((s) => s + 1, 0));
|
||||
|
||||
// Holes stay holes and are skipped as in Node.
|
||||
const sparse: number[] = [1];
|
||||
const sparseView: unknown[] = sparse;
|
||||
sparseView[3] = "z";
|
||||
console.log(sparse.filter(() => true), sparse.map((_, i) => i), sparse.findIndex((_, i) => i === 1));
|
||||
|
||||
// A parameter used only through a shorthand property is still read.
|
||||
const plain = [1, 2, 3];
|
||||
console.log(plain.map((v) => ({ v })), plain.reduce((acc, v) => ({ sum: acc.sum + v, v }), { sum: 0, v: 0 }));
|
||||
console.log(plain.filter((v) => [{ v }].length > 0 && v > 1));
|
||||
|
||||
// A callback that ignores its element still receives the element as it was
|
||||
// before the callback ran: filter keeps it, and find returns it, even when
|
||||
// the callback overwrites or removes the slot.
|
||||
const changing: number[] = [1, 2, 3];
|
||||
const changingView: unknown[] = changing;
|
||||
changingView.push("x");
|
||||
let at = 0;
|
||||
console.log(
|
||||
changing.filter(() => {
|
||||
changingView[at] = at === 1 ? "w" : 100 + at;
|
||||
at++;
|
||||
return true;
|
||||
}),
|
||||
changing,
|
||||
);
|
||||
const found: number[] = [1, 2];
|
||||
(found as unknown[]).push("y");
|
||||
let step = 0;
|
||||
console.log(
|
||||
found.find(() => {
|
||||
found[step] = 50;
|
||||
step++;
|
||||
return step === 2;
|
||||
}),
|
||||
found,
|
||||
);
|
||||
const shrinking: number[] = [4, 5];
|
||||
(shrinking as unknown[]).push("z", 6);
|
||||
console.log(
|
||||
shrinking.findLast(() => {
|
||||
shrinking.length = 1;
|
||||
return true;
|
||||
}),
|
||||
shrinking.length,
|
||||
);
|
||||
const cut: number[] = [7, 8, 9];
|
||||
(cut as unknown[]).push("q");
|
||||
console.log(
|
||||
cut.filter((_v, i) => {
|
||||
if (i === 1) cut.length = 1;
|
||||
return true;
|
||||
}),
|
||||
cut.length,
|
||||
);
|
||||
|
||||
// Arrays proven to hold no undefined keep their elements in filter results
|
||||
// when the predicate ignores them.
|
||||
const dense = [1, 2, 3, 4];
|
||||
let calls = 0;
|
||||
console.log(
|
||||
dense.filter((_v, i) => {
|
||||
calls++;
|
||||
return i % 2 === 0;
|
||||
}),
|
||||
calls,
|
||||
dense.filter((_v, i) => i > 1),
|
||||
);
|
||||
@@ -0,0 +1,98 @@
|
||||
// @no-node-shims
|
||||
// Builtin globals read through the global object by computed names, and
|
||||
// builtin module members that are global objects: each answers what the
|
||||
// global's own spelling reads, so identity holds across the spellings.
|
||||
// for-in over the global object visits Node's enumerable globals first and
|
||||
// the program's own global properties after them.
|
||||
"use strict";
|
||||
const { atob: bufferAtob, btoa: bufferBtoa } = require("buffer");
|
||||
const timers = require("timers");
|
||||
const { performance: perf } = require("perf_hooks");
|
||||
const util = require("util");
|
||||
const url = require("url");
|
||||
const { MessageChannel } = require("worker_threads");
|
||||
const web = require("stream/web");
|
||||
|
||||
const names = [
|
||||
"setTimeout",
|
||||
"setInterval",
|
||||
"setImmediate",
|
||||
"clearTimeout",
|
||||
"clearInterval",
|
||||
"clearImmediate",
|
||||
"queueMicrotask",
|
||||
"global",
|
||||
"globalThis",
|
||||
"fetch",
|
||||
"Map",
|
||||
"Object",
|
||||
"Promise",
|
||||
"Buffer",
|
||||
"crypto",
|
||||
];
|
||||
for (const name of names) {
|
||||
const value = globalThis[name];
|
||||
console.log(name, typeof value, value === globalThis[name]);
|
||||
}
|
||||
console.log(
|
||||
globalThis["setTimeout"] === setTimeout,
|
||||
globalThis["Map"] === Map,
|
||||
globalThis["crypto"] === crypto,
|
||||
globalThis["Buffer"] === Buffer,
|
||||
);
|
||||
console.log(timers.setTimeout === setTimeout, timers.clearInterval === globalThis["clearInterval"]);
|
||||
console.log(
|
||||
bufferAtob === atob,
|
||||
bufferBtoa === globalThis["btoa"],
|
||||
perf === performance,
|
||||
util.TextEncoder === TextEncoder,
|
||||
url.URL === URL,
|
||||
MessageChannel === globalThis["MessageChannel"],
|
||||
web.ReadableStream === ReadableStream,
|
||||
);
|
||||
console.log(globalThis["noSuchGlobal"], typeof globalThis["Temporal"]);
|
||||
// A snapshot of the global object in a function reads the same values.
|
||||
function readThroughSnapshot(name) {
|
||||
const g = globalThis;
|
||||
return g[name];
|
||||
}
|
||||
console.log(
|
||||
readThroughSnapshot("setTimeout") === setTimeout,
|
||||
readThroughSnapshot("Map") === Map,
|
||||
typeof readThroughSnapshot("structuredClone"),
|
||||
readThroughSnapshot("noSuchGlobal"),
|
||||
);
|
||||
const pick = (name) => globalThis[name];
|
||||
console.log(pick("Math") === Math, pick("JSON") === JSON, pick("Reflect") === Reflect, pick("Math").max(1, 3));
|
||||
|
||||
const before = [];
|
||||
for (const key in globalThis) before.push(key);
|
||||
globalThis.ownA = 1;
|
||||
globalThis["own" + "B"] = { b: 2 };
|
||||
const after = [];
|
||||
for (const key in globalThis) after.push(key);
|
||||
console.log(after.length - before.length, after.slice(-2).join(","));
|
||||
console.log(before.slice(0, 9).join(","));
|
||||
console.log(globalThis.ownA, globalThis.ownB.b, globalThis["ownB"] === globalThis.ownB);
|
||||
|
||||
// The harness's leak check: every enumerated global is a known value.
|
||||
const known = new Set([
|
||||
global,
|
||||
clearImmediate,
|
||||
setImmediate,
|
||||
clearInterval,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
setTimeout,
|
||||
queueMicrotask,
|
||||
structuredClone,
|
||||
bufferAtob,
|
||||
bufferBtoa,
|
||||
performance,
|
||||
fetch,
|
||||
crypto,
|
||||
globalThis.ownA,
|
||||
globalThis.ownB,
|
||||
]);
|
||||
const leaked = after.filter((key) => key !== "navigator" && !known.has(globalThis[key]));
|
||||
console.log("leaked", leaked.length);
|
||||
@@ -0,0 +1,47 @@
|
||||
// @no-node-shims
|
||||
// Node 24's navigator global: its members are getters on
|
||||
// Navigator.prototype answering the process's values, and the object
|
||||
// itself owns no properties. It is one of the global object's enumerable
|
||||
// properties.
|
||||
"use strict";
|
||||
const os = require("os");
|
||||
const util = require("util");
|
||||
|
||||
console.log(typeof navigator, navigator.userAgent);
|
||||
console.log(navigator.hardwareConcurrency === os.availableParallelism());
|
||||
console.log(typeof navigator.language, navigator.language.length > 0);
|
||||
console.log(navigator.languages.length, navigator.languages[0] === navigator.language);
|
||||
console.log(navigator.languages === navigator.languages, Object.isFrozen(navigator.languages));
|
||||
const platforms = {
|
||||
linux: { x64: "Linux x86_64", arm64: "Linux arm64" },
|
||||
darwin: { x64: "MacIntel", arm64: "MacIntel" },
|
||||
};
|
||||
console.log(navigator.platform === platforms[process.platform]?.[process.arch]);
|
||||
|
||||
const value = navigator;
|
||||
console.log(value === navigator, value === globalThis.navigator, value === globalThis["navigator"]);
|
||||
console.log(value.userAgent, value.platform === navigator.platform, value.missing);
|
||||
console.log(Object.keys(value), JSON.stringify(value), util.inspect(value));
|
||||
console.log(String(value), Object.prototype.toString.call(value));
|
||||
|
||||
const keys = [];
|
||||
for (const key in globalThis) keys.push(key);
|
||||
console.log(keys.join(","));
|
||||
const known = new Set([
|
||||
global,
|
||||
clearImmediate,
|
||||
setImmediate,
|
||||
clearInterval,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
setTimeout,
|
||||
queueMicrotask,
|
||||
structuredClone,
|
||||
atob,
|
||||
btoa,
|
||||
performance,
|
||||
fetch,
|
||||
crypto,
|
||||
navigator,
|
||||
]);
|
||||
console.log("leaked", keys.filter((key) => !known.has(globalThis[key])).length);
|
||||
@@ -0,0 +1,32 @@
|
||||
// atob, btoa and structuredClone as JavaScript values: function values
|
||||
// whose calls run what the direct calls run, the same value through every
|
||||
// spelling.
|
||||
"use strict";
|
||||
const buffer = require("buffer");
|
||||
|
||||
const fns = [atob, btoa, structuredClone];
|
||||
for (const fn of fns) console.log(typeof fn, fn.name, fn.length);
|
||||
console.log(buffer.atob === atob, buffer.btoa === globalThis.btoa, globalThis["structuredClone"] === structuredClone);
|
||||
|
||||
const pick = (name) => ({ atob, btoa, structuredClone })[name];
|
||||
const encode = pick("btoa");
|
||||
const decode = pick("atob");
|
||||
console.log(encode("scriptc"), decode(encode("scriptc")), decode("aGk="));
|
||||
const clone = pick("structuredClone");
|
||||
const original = { list: [1, { deep: true }], text: "t" };
|
||||
const copy = clone(original);
|
||||
console.log(copy !== original, copy.list[1].deep, copy.list !== original.list, copy.text);
|
||||
|
||||
const report = (label, run) => {
|
||||
try {
|
||||
console.log(label, run());
|
||||
} catch (error) {
|
||||
console.log(label, error.name, error.code, error.message);
|
||||
}
|
||||
};
|
||||
report("atob()", () => decode());
|
||||
report("btoa()", () => encode());
|
||||
report("structuredClone()", () => clone());
|
||||
report("atob invalid", () => decode("a"));
|
||||
report("btoa wide", () => encode("Ā"));
|
||||
report("clone symbol", () => clone(Symbol("s")));
|
||||
@@ -0,0 +1,35 @@
|
||||
// atob checks its input as Node does: only ASCII whitespace and the base64
|
||||
// alphabet are allowed, '=' only at the end and at most twice, and a
|
||||
// length the forgiving-base64 rules refuse is a different error.
|
||||
"use strict";
|
||||
const inputs = [
|
||||
"@@@",
|
||||
"a",
|
||||
"ab",
|
||||
"ab=",
|
||||
"ab==",
|
||||
"ab=c",
|
||||
"abc==",
|
||||
"a===",
|
||||
"ab===",
|
||||
" Y W J j ",
|
||||
"YQ",
|
||||
"Y Q = =",
|
||||
"YWJjZA==",
|
||||
"YWJjZA=",
|
||||
"\tYW\nJj\fZA\r==",
|
||||
"é",
|
||||
"YW-J",
|
||||
"",
|
||||
];
|
||||
const decode = (label, input) => {
|
||||
try {
|
||||
console.log(label, JSON.stringify(atob(input)));
|
||||
} catch (error) {
|
||||
console.log(label, error.name, error.code, error.message);
|
||||
}
|
||||
};
|
||||
for (const input of inputs) decode(JSON.stringify(input), input);
|
||||
decode("null", null);
|
||||
decode("undefined", undefined);
|
||||
decode("12", 12);
|
||||
@@ -0,0 +1,8 @@
|
||||
// @no-node-shims
|
||||
// navigator.language, navigator.languages and navigator.platform read
|
||||
// without the navigator object ever being taken as a value: the runtime
|
||||
// caches the answers and releases them at exit.
|
||||
"use strict";
|
||||
console.log(typeof navigator.language, navigator.language.length > 0);
|
||||
console.log(navigator.languages.length, navigator.languages[0] === navigator.language);
|
||||
console.log(Object.isFrozen(navigator.languages), typeof navigator.platform);
|
||||
@@ -0,0 +1,26 @@
|
||||
// `instanceof Promise` on values that are never promises (records, arrays,
|
||||
// dates, collections, regular expressions, byte arrays, null, and unions of
|
||||
// them) is false, after the operand is evaluated.
|
||||
let evaluated = 0;
|
||||
function next(): { v: number } {
|
||||
evaluated++;
|
||||
return { v: evaluated };
|
||||
}
|
||||
|
||||
const record = { v: 1 };
|
||||
console.log("record", record instanceof Promise);
|
||||
console.log("call", next() instanceof Promise, evaluated);
|
||||
console.log("array", [1, 2] instanceof Promise);
|
||||
console.log("date", new Date(0) instanceof Promise);
|
||||
console.log("map", new Map<string, number>() instanceof Promise);
|
||||
console.log("set", new Set<number>() instanceof Promise);
|
||||
console.log("regex", /a/ instanceof Promise);
|
||||
console.log("bytes", new Uint8Array(2) instanceof Promise);
|
||||
|
||||
function pick(i: number): { v: number } | number[] | null {
|
||||
return i === 0 ? { v: 0 } : i === 1 ? [i] : null;
|
||||
}
|
||||
for (let i = 0; i < 3; i++) console.log("union", i, pick(i) instanceof Promise);
|
||||
|
||||
const promised: unknown = Promise.resolve(1);
|
||||
console.log("promise", promised instanceof Promise);
|
||||
@@ -0,0 +1,128 @@
|
||||
// Counted loops over typed arrays backed by a SharedArrayBuffer in a program
|
||||
// with workers: loops to a length and to a bound, ascending, descending and
|
||||
// by two, reads and writes at the counter and next to it, nested loops, and
|
||||
// each element type. The worker and the main thread run the same loops over
|
||||
// the same shared memory, one after the other.
|
||||
import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads";
|
||||
|
||||
interface Shared {
|
||||
bytes: Uint8Array;
|
||||
shorts: Int16Array;
|
||||
words: Uint32Array;
|
||||
ints: Int32Array;
|
||||
floats: Float32Array;
|
||||
doubles: Float64Array;
|
||||
}
|
||||
|
||||
function codes(a: Uint8Array): void {
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
const x = a[i];
|
||||
if (x === 97) a[i] = 0;
|
||||
else if (x === 116) a[i] = 1;
|
||||
else if (x === 99) a[i] = 2;
|
||||
else a[i] = 3;
|
||||
}
|
||||
}
|
||||
|
||||
function checksum(a: Uint8Array): number {
|
||||
let t = 0;
|
||||
for (let i = 0; i < a.length; i++) t = (t * 31 + a[i]!) % 1000003;
|
||||
return t;
|
||||
}
|
||||
|
||||
function shiftLeft(a: Uint8Array): void {
|
||||
for (let i = 0; i < a.length - 1; i++) a[i] = a[i + 1]!;
|
||||
}
|
||||
|
||||
function descend(a: Int16Array, n: number): number {
|
||||
let t = 0;
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
a[i] = a[i]! - i * 3;
|
||||
t += a[i]!;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function everyOther(a: Uint32Array): number {
|
||||
let t = 0;
|
||||
for (let i = 0; i < a.length; i += 2) {
|
||||
a[i] = a[i]! * 2 + 4000000000;
|
||||
t += a[i]! % 1000;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function grid(a: Int32Array, n: number): number {
|
||||
let t = 0;
|
||||
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) t += a[i]! * a[j]! - j;
|
||||
return t;
|
||||
}
|
||||
|
||||
function mix(f: Float32Array, d: Float64Array, n: number): number {
|
||||
for (let i = 0; i < n; i++) {
|
||||
f[i] = f[i]! * 0.5 + i;
|
||||
d[i] = d[i]! + f[i]! / 3;
|
||||
}
|
||||
let t = 0;
|
||||
for (let i = 0; i < n; i++) t += d[i]!;
|
||||
return Math.round(t * 1000) / 1000;
|
||||
}
|
||||
|
||||
function fill(s: Shared): void {
|
||||
const letters = [97, 116, 99, 103];
|
||||
let seed = 7;
|
||||
for (let i = 0; i < s.bytes.length; i++) {
|
||||
seed = (seed * 1103515245 + 12345) % 2147483648;
|
||||
s.bytes[i] = letters[(seed >> 16) % 4]!;
|
||||
}
|
||||
for (let i = 0; i < s.shorts.length; i++) s.shorts[i] = (i * 37) % 2000 - 1000;
|
||||
for (let i = 0; i < s.words.length; i++) s.words[i] = i * 2654435761;
|
||||
for (let i = 0; i < s.ints.length; i++) s.ints[i] = (i % 7) - 3;
|
||||
for (let i = 0; i < s.floats.length; i++) s.floats[i] = i / 4;
|
||||
for (let i = 0; i < s.doubles.length; i++) s.doubles[i] = i * 1.25;
|
||||
}
|
||||
|
||||
function run(where: string, s: Shared): string {
|
||||
codes(s.bytes);
|
||||
const coded = checksum(s.bytes);
|
||||
shiftLeft(s.bytes);
|
||||
const shifted = checksum(s.bytes);
|
||||
const down = descend(s.shorts, s.shorts.length);
|
||||
const other = everyOther(s.words);
|
||||
const cells = grid(s.ints, 40);
|
||||
const mixed = mix(s.floats, s.doubles, s.floats.length);
|
||||
return `${where}: ${coded} ${shifted} ${down} ${other} ${cells} ${mixed} ${s.bytes[0]} ${s.words[2]}`;
|
||||
}
|
||||
|
||||
function views(buffers: SharedArrayBuffer[]): Shared {
|
||||
return {
|
||||
bytes: new Uint8Array(buffers[0]!),
|
||||
shorts: new Int16Array(buffers[1]!),
|
||||
words: new Uint32Array(buffers[2]!),
|
||||
ints: new Int32Array(buffers[3]!),
|
||||
floats: new Float32Array(buffers[4]!),
|
||||
doubles: new Float64Array(buffers[5]!),
|
||||
};
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const buffers = [
|
||||
new SharedArrayBuffer(10007),
|
||||
new SharedArrayBuffer(2 * 515),
|
||||
new SharedArrayBuffer(4 * 333),
|
||||
new SharedArrayBuffer(4 * 40),
|
||||
new SharedArrayBuffer(4 * 129),
|
||||
new SharedArrayBuffer(8 * 129),
|
||||
];
|
||||
const shared = views(buffers);
|
||||
fill(shared);
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: buffers });
|
||||
worker.on("message", (line: string) => {
|
||||
console.log(line);
|
||||
console.log(run("main", shared));
|
||||
fill(shared);
|
||||
console.log(run("main again", shared));
|
||||
});
|
||||
} else {
|
||||
parentPort!.postMessage(run("worker", views(workerData as SharedArrayBuffer[])));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// An Array subclass written through an unknown[] view: a number pushed
|
||||
// through the view, and values its element type cannot hold, which the
|
||||
// array keeps as written. The instance keeps its class and its fields, and
|
||||
// the species results of slice and concat keep the written values.
|
||||
class Scores extends Array<number> {
|
||||
label = "scores";
|
||||
}
|
||||
|
||||
const plain = new Scores();
|
||||
plain.push(1, 2);
|
||||
const plainView: unknown[] = plain;
|
||||
plainView.push(3);
|
||||
console.log(plain instanceof Scores, plain.label, plain.length, JSON.stringify(plain));
|
||||
|
||||
const scores = new Scores();
|
||||
scores.push(1, 2);
|
||||
const view: unknown[] = scores;
|
||||
view.push("x", { a: 1 });
|
||||
console.log(scores instanceof Scores, scores.label, scores.length, JSON.stringify(scores), scores.join("|"));
|
||||
const tail = scores.slice(1);
|
||||
console.log(tail instanceof Scores, tail.length, JSON.stringify(tail));
|
||||
const joined = scores.concat([9]);
|
||||
console.log(joined instanceof Scores, joined.length, JSON.stringify(joined));
|
||||
scores[2] = 7;
|
||||
console.log(JSON.stringify(scores), view[3]);
|
||||
scores.length = 1;
|
||||
console.log(scores instanceof Scores, scores.label, JSON.stringify(scores), JSON.stringify(tail));
|
||||
@@ -0,0 +1,13 @@
|
||||
// util.inspect of Array subclass instances: the `Name(length)` prefix, and
|
||||
// the `[Name]` placeholder past the depth limit, plain and with colors.
|
||||
import { inspect } from "node:util";
|
||||
|
||||
class Scores extends Array<number> {}
|
||||
|
||||
const s = new Scores();
|
||||
s.push(1, 2);
|
||||
console.log(inspect(new Scores()), inspect(s));
|
||||
console.log(inspect([s], { depth: 0 }), inspect([[1]], { depth: 0 }));
|
||||
console.log(JSON.stringify(inspect([s], { depth: 0, colors: true })));
|
||||
console.log(JSON.stringify(inspect([[1]], { depth: 0, colors: true })));
|
||||
console.log(JSON.stringify(inspect(s, { colors: true })));
|
||||
@@ -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 @@
|
||||
// Teardown of self-recursive records and classes: shared and immortal
|
||||
// children, deep chains, weak-collection keys, and trees rebuilt from freed
|
||||
// blocks.
|
||||
|
||||
interface TreeNode {
|
||||
left: TreeNode | null;
|
||||
right: TreeNode | null;
|
||||
}
|
||||
|
||||
class ListNode {
|
||||
next: ListNode | null;
|
||||
value: number;
|
||||
constructor(next: ListNode | null, value: number) {
|
||||
this.next = next;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class Pair {
|
||||
a: Pair | null;
|
||||
b: Pair | null;
|
||||
constructor(a: Pair | null, b: Pair | null) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
function build(depth: number): TreeNode {
|
||||
return depth > 0 ? { left: build(depth - 1), right: build(depth - 1) } : { left: null, right: null };
|
||||
}
|
||||
|
||||
function count(node: TreeNode | null): number {
|
||||
if (node === null) return 0;
|
||||
return 1 + count(node.left) + count(node.right);
|
||||
}
|
||||
|
||||
// Trees torn down and rebuilt from their freed blocks.
|
||||
let total = 0;
|
||||
for (let depth = 0; depth <= 12; depth++) {
|
||||
for (let i = 0; i < 4; i++) total += count(build(depth));
|
||||
}
|
||||
console.log("trees", total);
|
||||
|
||||
// A child shared by two parents survives the first parent's teardown.
|
||||
const shared = build(3);
|
||||
let left: TreeNode | null = { left: shared, right: build(2) };
|
||||
let right: TreeNode | null = { left: build(1), right: shared };
|
||||
left = null;
|
||||
console.log("shared after first parent", count(shared), count(right));
|
||||
right = null;
|
||||
console.log("shared after second parent", count(shared));
|
||||
|
||||
// A node held from outside keeps its subtree when the tree around it dies.
|
||||
let outer: TreeNode | null = build(6);
|
||||
const kept = outer.left!.right!;
|
||||
outer = null;
|
||||
console.log("kept subtree", count(kept));
|
||||
|
||||
// A chain far deeper than any call stack.
|
||||
let head: ListNode | null = null;
|
||||
for (let i = 0; i < 2000000; i++) head = new ListNode(head, i);
|
||||
let sum = 0;
|
||||
for (let n = head; n !== null; n = n.next) sum += n.value % 7;
|
||||
head = null;
|
||||
console.log("chain", sum);
|
||||
|
||||
// A list whose middle node is still referenced keeps the tail from it.
|
||||
let list: ListNode | null = null;
|
||||
let middle: ListNode | null = null;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
list = new ListNode(list, i);
|
||||
if (i === 500) middle = list;
|
||||
}
|
||||
list = null;
|
||||
let tail = 0;
|
||||
for (let n = middle; n !== null; n = n.next) tail++;
|
||||
console.log("tail from middle", tail);
|
||||
middle = null;
|
||||
|
||||
// Nodes used as WeakMap keys: their entries go away when the tree dies.
|
||||
const weak = new WeakMap<Pair, number>();
|
||||
let root: Pair | null = new Pair(new Pair(null, null), new Pair(new Pair(null, null), null));
|
||||
const probe = root.b!;
|
||||
weak.set(root, 1);
|
||||
weak.set(root.a!, 2);
|
||||
weak.set(probe, 3);
|
||||
root = null;
|
||||
console.log("weak probe", weak.get(probe), weak.has(probe));
|
||||
|
||||
// Immortal leaves shared by many nodes.
|
||||
const leaf: TreeNode = { left: null, right: null };
|
||||
let wide: TreeNode | null = { left: leaf, right: leaf };
|
||||
for (let i = 0; i < 100; i++) wide = { left: wide, right: leaf };
|
||||
console.log("wide", count(wide));
|
||||
wide = null;
|
||||
console.log("leaf", count(leaf));
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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)));
|
||||
@@ -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(","));
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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(","));
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,65 @@
|
||||
// Array.prototype methods called with .call on native arrays run the arrays' own
|
||||
// methods, which mutate the array in place: copyWithin, fill, reverse,
|
||||
// sort, push, pop, splice, and the read-only join, indexOf and includes.
|
||||
const optional: (number | undefined)[] = [1, 2, 3, 4];
|
||||
Array.prototype.copyWithin.call(optional, 0, 2, 4);
|
||||
console.log(optional.join(","));
|
||||
const nums: number[] = [5, 1, 4, 2, 3];
|
||||
Array.prototype.fill.call(nums, 0, 0, 1);
|
||||
console.log(nums.join(","));
|
||||
console.log(Array.prototype.reverse.call(nums) === nums, nums.join(","));
|
||||
Array.prototype.sort.call(nums, (a: number, b: number) => a - b);
|
||||
console.log(nums.join(","));
|
||||
console.log(Array.prototype.push.call(nums, 9, 10), nums.join(","));
|
||||
console.log(Array.prototype.pop.call(nums), nums.length);
|
||||
console.log(Array.prototype.splice.call(nums, 1, 2).join(","), nums.join(","));
|
||||
console.log(Array.prototype.join.call(nums, "-"), Array.prototype.indexOf.call(nums, 4));
|
||||
const words: string[] = ["c", "a", "b"];
|
||||
Array.prototype.sort.call(words);
|
||||
console.log(words.join(","), Array.prototype.includes.call(words, "a"));
|
||||
const records: { id: number }[] = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||
Array.prototype.copyWithin.call(records, 1, 0, 1);
|
||||
console.log(records.map((r) => r.id).join(","), records[0] === records[1]);
|
||||
|
||||
// The callback methods bind `this` to their thisArg, for native arrays
|
||||
// through .call and for dynamic arrays called directly.
|
||||
type Scale = { k: number; seen: number[] };
|
||||
const scale: Scale = { k: 10, seen: [] };
|
||||
const values: number[] = [1, 2, 3, 4];
|
||||
Array.prototype.forEach.call(values, function (this: Scale, v: number) {
|
||||
this.seen.push(v);
|
||||
}, scale);
|
||||
console.log(scale.seen.join(","));
|
||||
console.log(Array.prototype.map.call(values, function (this: Scale, v: number) {
|
||||
return v * this.k;
|
||||
}, scale).join(","));
|
||||
console.log(Array.prototype.filter.call(values, function (this: Scale, v: number) {
|
||||
return v * this.k > 15;
|
||||
}, scale).join(","));
|
||||
console.log(
|
||||
Array.prototype.some.call(values, function (this: Scale, v: number) { return v === this.k / 5; }, scale),
|
||||
Array.prototype.every.call(values, function (this: Scale, v: number) { return v < this.k; }, scale),
|
||||
Array.prototype.find.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale),
|
||||
Array.prototype.findIndex.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale),
|
||||
Array.prototype.findLast.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale),
|
||||
Array.prototype.findLastIndex.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale),
|
||||
);
|
||||
console.log(Array.prototype.flatMap.call(values, function (this: unknown, v: number) {
|
||||
return [v, v * (this as Scale).k];
|
||||
}, scale).join(","));
|
||||
const parsed: any = JSON.parse("[1, 2, 3]");
|
||||
const sum = { total: 0 };
|
||||
parsed.forEach(function (this: { total: number }, v: number) {
|
||||
this.total += v;
|
||||
}, sum);
|
||||
console.log(sum.total, parsed.map(function (this: Scale, v: number) { return v * this.k; }, scale).join(","));
|
||||
class Counter {
|
||||
total = 0;
|
||||
addAll(items: number[]): number {
|
||||
Array.prototype.forEach.call(items, (v: number) => {
|
||||
this.total += v;
|
||||
});
|
||||
return this.total;
|
||||
}
|
||||
}
|
||||
console.log(new Counter().addAll(values));
|
||||
@@ -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,128 @@
|
||||
// if/else-if chains that store one of several literals to one typed-array
|
||||
// element, chosen by comparing one number with integer literals: the
|
||||
// subject's values that no case names, repeated and negative cases,
|
||||
// results that do not fit the element, and chains that keep their arms.
|
||||
|
||||
function bases(arr: Uint8Array): void {
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
const x = arr[i];
|
||||
if (x === 97) arr[i] = 0;
|
||||
else if (x === 116) arr[i] = 1;
|
||||
else if (x === 99) arr[i] = 2;
|
||||
else arr[i] = 3;
|
||||
}
|
||||
}
|
||||
|
||||
const sequence = new Uint8Array(64);
|
||||
for (let i = 0; i < sequence.length; i++) sequence[i] = "actgACTGnx"[i % 10]!.charCodeAt(0);
|
||||
bases(sequence);
|
||||
console.log("bases", sequence.join(""));
|
||||
|
||||
// A number subject: NaN, -0, fractions, infinities and values far outside the cases.
|
||||
const subjects = [0, -0, 1, 2, 3, 4, -1, 0.5, 2.0000001, NaN, Infinity, -Infinity, 2 ** 40, -(2 ** 40), 1e300];
|
||||
function classify(out: Int16Array, at: number, v: number): void {
|
||||
if (v === 0) out[at] = 10;
|
||||
else if (v === 1) out[at] = -20;
|
||||
else if (v === 2) out[at] = 30;
|
||||
else if (v === 1) out[at] = 99; // never reached
|
||||
else if (v === -1) out[at] = 40;
|
||||
else out[at] = -50;
|
||||
}
|
||||
const classes = new Int16Array(subjects.length);
|
||||
for (let i = 0; i < subjects.length; i++) classify(classes, i, subjects[i]!);
|
||||
console.log("classes", classes.join(" "));
|
||||
|
||||
// The literal on the left, and results the element converts: 300 wraps in a
|
||||
// Uint8Array, -1 is 255, 1.5 truncates, and a clamped array rounds and clamps.
|
||||
function convert(u8: Uint8Array, c: Uint8ClampedArray, f: Float64Array, i: Int8Array, v: number): void {
|
||||
if (5 === v) u8[0] = 300;
|
||||
else if (6 === v) u8[0] = -1;
|
||||
else if (7 === v) u8[0] = 1.5;
|
||||
else u8[0] = 8589934599; // 2 ** 33 + 7
|
||||
if (v === 5) c[0] = 300;
|
||||
else if (v === 6) c[0] = -1;
|
||||
else if (v === 7) c[0] = 2.5;
|
||||
else c[0] = 3.5;
|
||||
if (v === 5) f[0] = 0.25;
|
||||
else if (v === 6) f[0] = -0;
|
||||
else if (v === 7) f[0] = 1e300;
|
||||
else f[0] = NaN;
|
||||
if (v === 5) i[0] = 128;
|
||||
else if (v === 6) i[0] = -129;
|
||||
else if (v === 7) i[0] = -128;
|
||||
else i[0] = 127;
|
||||
}
|
||||
for (const v of [5, 6, 7, 8, NaN]) {
|
||||
const u8 = new Uint8Array(1);
|
||||
const c = new Uint8ClampedArray(1);
|
||||
const f = new Float64Array(1);
|
||||
const i = new Int8Array(1);
|
||||
convert(u8, c, f, i, v);
|
||||
console.log("convert", v, u8[0], c[0], Object.is(f[0], -0) ? "-0" : f[0], i[0]);
|
||||
}
|
||||
|
||||
// An index past the end ignores the store, whatever the chain chooses.
|
||||
const short = new Uint8Array(2);
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const v = k + 1;
|
||||
if (v === 1) short[k] = 11;
|
||||
else if (v === 2) short[k] = 22;
|
||||
else if (v === 3) short[k] = 33;
|
||||
else short[k] = 44;
|
||||
}
|
||||
console.log("short", short.join(","));
|
||||
|
||||
// The index is the compared local. Inside each arm the range analysis
|
||||
// proves its value there; on the final else's path it is past the end, also
|
||||
// past 2^32 and below -2^32, and the store is ignored.
|
||||
function indexed(flag: boolean): string {
|
||||
const a = new Uint8Array(4);
|
||||
let j = 0;
|
||||
if (flag) j = 4294967297;
|
||||
if (j === 0) a[j] = 11;
|
||||
else if (j === 1) a[j] = 22;
|
||||
else if (j === 2) a[j] = 33;
|
||||
else a[j] = 44;
|
||||
return a.join(",");
|
||||
}
|
||||
function indexedLoop(): string {
|
||||
const a = new Uint8Array(8);
|
||||
for (let k = 0; k < 4; k++) {
|
||||
let j = 1;
|
||||
if (k === 3) j = 5;
|
||||
else if (k === 2) j = 4294967298;
|
||||
else if (k === 1) j = -4294967294;
|
||||
if (j === 1) a[j] = 11;
|
||||
else if (j === 2) a[j] = 22;
|
||||
else if (j === 3) a[j] = 33;
|
||||
else a[j] = 44;
|
||||
}
|
||||
return a.join(",");
|
||||
}
|
||||
console.log("indexed", indexed(false), indexed(true), indexedLoop());
|
||||
|
||||
// Chains that keep their arms: an arm with a second statement, no final
|
||||
// else, a different index, and a subject that is not a local.
|
||||
let calls = 0;
|
||||
function count(): number {
|
||||
calls++;
|
||||
return 9;
|
||||
}
|
||||
const kept = new Uint8Array(8);
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const v = k % 5;
|
||||
if (v === 0) kept[k] = 1;
|
||||
else if (v === 1) {
|
||||
kept[k] = 2;
|
||||
calls += 100;
|
||||
} else if (v === 2) kept[k] = 3;
|
||||
else kept[k] = 4;
|
||||
if (v === 0) kept[0] += 1;
|
||||
else if (v === 1) kept[0] += 2;
|
||||
else if (v === 2) kept[0] += 3;
|
||||
if (count() === v) kept[k] = 7;
|
||||
else if (v === 3) kept[(k + 1) % 8] = 5;
|
||||
else if (v === 4) kept[k] = 6;
|
||||
else kept[k] = kept[k]!;
|
||||
}
|
||||
console.log("kept", kept.join(","), calls);
|
||||
@@ -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,21 @@
|
||||
// Casts of values nothing else holds yet: a parse result and a response
|
||||
// body. They convert outright; the results are ordinary records.
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
type Doc = { title: string; points: Point[] };
|
||||
|
||||
const parsed = JSON.parse('{"title":"t","points":[{"x":1,"y":2},{"x":3,"y":4}]}') as Doc;
|
||||
parsed.points[0]!.x = 10;
|
||||
parsed.points.push({ x: 5, y: 6 });
|
||||
console.log("parse", parsed.title, parsed.points.length, JSON.stringify(parsed.points[0]));
|
||||
|
||||
const again = JSON.parse('{"x":1,"y":2}') as Point;
|
||||
const other = JSON.parse('{"x":1,"y":2}') as Point;
|
||||
console.log("separate parses", again === other, again.x === other.x);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const body = (await new Response('{"title":"r","points":[{"x":7,"y":8}]}').json()) as Doc;
|
||||
body.points[0]!.y = 80;
|
||||
console.log("response", body.title, JSON.stringify(body.points));
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,62 @@
|
||||
// Checked casts of dynamic values held by parameters, fields, closures and
|
||||
// containers keep one identity with the holder's value.
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
|
||||
// A parameter: the caller's value and the cast result are one object.
|
||||
function asPoint(value: unknown): Point {
|
||||
return value as Point;
|
||||
}
|
||||
const shared: unknown = JSON.parse('{"x":1,"y":2}');
|
||||
const fromParam = asPoint(shared);
|
||||
fromParam.x = 5;
|
||||
console.log("parameter", (shared as Point).x, asPoint(shared) === fromParam);
|
||||
|
||||
// A field of a class instance.
|
||||
class Holder {
|
||||
data: unknown;
|
||||
constructor(data: unknown) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
const holder = new Holder(JSON.parse('{"x":3,"y":4}'));
|
||||
const fromField = holder.data as Point;
|
||||
fromField.y = 40;
|
||||
console.log("field", JSON.stringify(holder.data), (holder.data as Point) === fromField);
|
||||
|
||||
// A value captured by a closure.
|
||||
const captured: unknown = JSON.parse('{"x":0,"y":0}');
|
||||
const bump = (): void => {
|
||||
(captured as Point).x++;
|
||||
};
|
||||
bump();
|
||||
bump();
|
||||
console.log("closure", (captured as Point).x);
|
||||
|
||||
// Elements of an unknown[] container.
|
||||
const list: unknown[] = [JSON.parse('{"x":1,"y":1}'), JSON.parse('{"x":2,"y":2}')];
|
||||
const second = list[1] as Point;
|
||||
second.x = 20;
|
||||
console.log("container", JSON.stringify(list), (list[1] as Point) === second);
|
||||
|
||||
// A union target with a record arm.
|
||||
const maybe: unknown = JSON.parse('{"x":7,"y":8}');
|
||||
const viaUnion = maybe as Point | undefined;
|
||||
if (viaUnion) viaUnion.x = 70;
|
||||
console.log("union", (maybe as Point).x);
|
||||
|
||||
// A record whose field is an array, mutated through both casts.
|
||||
type Bag = { items: number[] };
|
||||
const bagRaw: unknown = JSON.parse('{"items":[1]}');
|
||||
const bag = bagRaw as Bag;
|
||||
const items = (bagRaw as { items: unknown }).items as number[];
|
||||
items.push(2);
|
||||
bag.items.push(3);
|
||||
console.log("record array", bag.items.join(","), items.length, bag.items === items);
|
||||
|
||||
// A native value boxed as unknown and cast back to its own type.
|
||||
const nativeList: number[] = [1, 2];
|
||||
const boxedList: unknown = nativeList;
|
||||
const backList = boxedList as number[];
|
||||
backList.push(3);
|
||||
console.log("native array", nativeList.length, backList === nativeList);
|
||||
@@ -0,0 +1,30 @@
|
||||
// Structured clones a worker receives: workerData and message payloads.
|
||||
// Two casts of workerData are one object; a message payload converts once.
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
type Job = { name: string; point: { x: number; y: number }; tags: string[] };
|
||||
|
||||
if (isMainThread) {
|
||||
const worker = new Worker(new URL(import.meta.url), {
|
||||
workerData: { name: "job", point: { x: 1, y: 2 }, tags: ["a"] },
|
||||
});
|
||||
worker.on("message", (message: unknown) => {
|
||||
if (typeof message === "string") {
|
||||
console.log(message);
|
||||
return;
|
||||
}
|
||||
const reply = message as Job;
|
||||
reply.tags.push("main");
|
||||
console.log("reply", reply.name, reply.point.x, reply.tags.join(","));
|
||||
});
|
||||
} else {
|
||||
const job = workerData as Job;
|
||||
const same = workerData as Job;
|
||||
job.point.x = 10;
|
||||
same.tags.push("worker");
|
||||
// Node forwards a worker's stdout to the parent asynchronously, so the
|
||||
// worker's line goes through the port: messages on one port keep their
|
||||
// order, and the line prints before the reply.
|
||||
parentPort!.postMessage(`worker ${same.point.x} ${job.tags.join(",")} ${job === same}`);
|
||||
parentPort!.postMessage(job);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// A checked cast of a dynamic value to a record or array (`value as T`)
|
||||
// keeps the value's identity: every cast of the same source, and the source
|
||||
// itself, read and write one object.
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
|
||||
// Two casts of one parsed object alias each other and the source.
|
||||
const raw: unknown = JSON.parse('{"x":1,"y":2}');
|
||||
const a = raw as Point;
|
||||
const b = raw as Point;
|
||||
a.x = 10;
|
||||
b.y = 20;
|
||||
console.log("aliasing", a.x, a.y, b.x, b.y);
|
||||
console.log("identity", a === b, (raw as Point) === a);
|
||||
console.log("source", JSON.stringify(raw));
|
||||
|
||||
// Nested objects keep their identity through the outer and inner casts.
|
||||
type Outer = { name: string; inner: { v: number } };
|
||||
const nestedRaw: unknown = JSON.parse('{"name":"o","inner":{"v":1}}');
|
||||
const outer = nestedRaw as Outer;
|
||||
const inner = (nestedRaw as { inner: unknown }).inner as { v: number };
|
||||
inner.v = 2;
|
||||
console.log("nested", outer.inner.v, outer.inner === inner);
|
||||
outer.inner.v = 3;
|
||||
console.log("nested back", inner.v);
|
||||
|
||||
// Arrays: pushes and element writes show through every cast.
|
||||
const listRaw: unknown = JSON.parse("[1,2]");
|
||||
const first = listRaw as number[];
|
||||
const second = listRaw as number[];
|
||||
first.push(3);
|
||||
second[0] = 9;
|
||||
console.log("array", first.length, second.length, first[0], first === second, JSON.stringify(listRaw));
|
||||
|
||||
// Arrays of records.
|
||||
type Item = { id: number; tags: string[] };
|
||||
const itemsRaw: unknown = JSON.parse('[{"id":1,"tags":["a"]},{"id":2,"tags":[]}]');
|
||||
const items = itemsRaw as Item[];
|
||||
const again = itemsRaw as Item[];
|
||||
items[0]!.tags.push("b");
|
||||
again[1]!.id = 7;
|
||||
console.log("array of records", again[0]!.tags.join(","), items[1]!.id, items[0] === again[0]);
|
||||
|
||||
// The source stays an ordinary object for untyped reads and writes.
|
||||
const bagRaw: unknown = JSON.parse('{"y":2,"x":1,"extra":"e"}');
|
||||
const point = bagRaw as Point;
|
||||
const bag = bagRaw as Record<string, unknown>;
|
||||
bag.z = 3;
|
||||
console.log("added", JSON.stringify(bagRaw), Object.keys(bag).join(","));
|
||||
delete bag.extra;
|
||||
console.log("deleted", JSON.stringify(bagRaw), JSON.stringify(point));
|
||||
for (const k in bag) console.log("key", k, bag[k]);
|
||||
point.x = 5;
|
||||
console.log("typed write", bag.x);
|
||||
|
||||
// A native record boxed as unknown comes back as itself.
|
||||
const native: Point = { x: 1, y: 2 };
|
||||
const boxed: unknown = native;
|
||||
const unboxed = boxed as Point;
|
||||
unboxed.x = 10;
|
||||
console.log("native", native.x, unboxed === native);
|
||||
|
||||
// A fresh parse result has no other holder.
|
||||
const fresh = JSON.parse('{"x":4,"y":5}') as Point;
|
||||
console.log("fresh", fresh.x + fresh.y);
|
||||
|
||||
// A frozen source keeps working as a copy.
|
||||
const frozen: unknown = Object.freeze(JSON.parse('{"x":1,"y":2}'));
|
||||
const held = [frozen];
|
||||
const thawed = frozen as Point;
|
||||
console.log("frozen", thawed.x, held.length, Object.isFrozen(frozen));
|
||||
@@ -0,0 +1,83 @@
|
||||
// Object and array literals in untyped contexts (an `unknown` or `any`
|
||||
// slot, an `unknown[]` element, a thrown value, an `unknown`/`any`
|
||||
// parameter) keep one identity through later checked casts.
|
||||
|
||||
type Counter = { count: number };
|
||||
type Item = { name: string; tags: string[] };
|
||||
|
||||
// The original example: a write through the cast shows through the holder.
|
||||
const holder: any = { count: 1 };
|
||||
const item = holder as Counter;
|
||||
item.count++;
|
||||
console.log("any holder", holder.count, item === holder);
|
||||
|
||||
const box: unknown = { count: 10 };
|
||||
const viewA = box as Counter;
|
||||
const viewB = box as Counter;
|
||||
viewA.count += 5;
|
||||
console.log("unknown holder", viewB.count, viewA === viewB, JSON.stringify(box));
|
||||
|
||||
// Elements of an unknown[] array literal.
|
||||
const values: unknown[] = [
|
||||
{ name: "a", tags: ["x"] },
|
||||
{ name: "b", tags: [] },
|
||||
];
|
||||
const second = values[1] as Item;
|
||||
second.tags.push("y");
|
||||
console.log("unknown[] element", JSON.stringify(values), (values[1] as Item) === second);
|
||||
|
||||
// Nested literals inside an untyped literal.
|
||||
const tree: unknown = { name: "root", children: [{ name: "leaf", tags: ["t"] }] };
|
||||
const root = tree as { name: string; children: Item[] };
|
||||
const leaf = (tree as { children: unknown[] }).children[0] as Item;
|
||||
leaf.tags.push("u");
|
||||
console.log("nested", root.children[0]!.tags.join(","), root.children[0] === leaf);
|
||||
|
||||
// A thrown literal.
|
||||
try {
|
||||
throw { code: "E_BUSY", attempts: [1] };
|
||||
} catch (error) {
|
||||
const failure = error as { code: string; attempts: number[] };
|
||||
failure.attempts.push(2);
|
||||
console.log("thrown", failure.code, (error as { attempts: number[] }).attempts.length, failure === error);
|
||||
}
|
||||
|
||||
// Literals passed to unknown and any parameters.
|
||||
function remember(value: unknown): Counter {
|
||||
const counter = value as Counter;
|
||||
counter.count *= 2;
|
||||
return value as Counter;
|
||||
}
|
||||
console.log("unknown parameter", remember({ count: 4 }).count);
|
||||
|
||||
function relabel(value: any): Item {
|
||||
const target = value as Item;
|
||||
target.name = target.name.toUpperCase();
|
||||
return value;
|
||||
}
|
||||
const relabeled = relabel({ name: "pkg", tags: [] });
|
||||
console.log("any parameter", relabeled.name, relabeled.tags.length);
|
||||
|
||||
// A literal returned as unknown, and one stored in an unknown field.
|
||||
function make(): unknown {
|
||||
return { count: 0 };
|
||||
}
|
||||
const made = make();
|
||||
(made as Counter).count = 7;
|
||||
console.log("returned", (made as Counter).count);
|
||||
|
||||
class Slot {
|
||||
value: unknown = { name: "slot", tags: ["s"] };
|
||||
}
|
||||
const slot = new Slot();
|
||||
(slot.value as Item).tags.push("t");
|
||||
console.log("field", JSON.stringify(slot.value));
|
||||
|
||||
// Untyped literals keep JavaScript's own key order and extra keys.
|
||||
const extra: unknown = { name: "e", z: 1, tags: [], a: 2 };
|
||||
const typed = extra as Item;
|
||||
typed.tags.push("k");
|
||||
console.log("extra keys", JSON.stringify(extra), Object.keys(typed).join(","));
|
||||
|
||||
// Literals handed to library calls are unchanged.
|
||||
console.log(JSON.stringify({ count: 3 }), [{ n: 1 }].length);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,52 @@
|
||||
// readdirSync(path, { recursive: true }) lists every entry below the path,
|
||||
// relative to it: the path's own entries first, then each directory's in the
|
||||
// order its entry was listed. A symlink to a directory is followed.
|
||||
|
||||
import { mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const root = mkdtempSync(join(tmpdir(), "readdir-recursive-"));
|
||||
writeFileSync(join(root, "a.txt"), "a");
|
||||
mkdirSync(join(root, "b", "d"), { recursive: true });
|
||||
writeFileSync(join(root, "b", "c.txt"), "c");
|
||||
writeFileSync(join(root, "b", "d", "e.txt"), "e");
|
||||
mkdirSync(join(root, "empty"));
|
||||
mkdirSync(join(root, "z"));
|
||||
writeFileSync(join(root, "z", "y.txt"), "y");
|
||||
symlinkSync(join(root, "b", "d"), join(root, "link"));
|
||||
|
||||
console.log(readdirSync(root, { recursive: true }).join(" "));
|
||||
console.log(readdirSync(root, { recursive: false }).join(" "));
|
||||
console.log(readdirSync(`${root}/b/`, { recursive: true }).join(" "));
|
||||
|
||||
let deep = false;
|
||||
for (const flag of [false, true]) {
|
||||
deep = flag;
|
||||
console.log(deep, readdirSync(join(root, "z"), { recursive: deep }).length);
|
||||
}
|
||||
console.log(readdirSync(join(root, "empty"), { recursive: true }).length);
|
||||
|
||||
// A flag expression typed `true` or `false` still evaluates, once.
|
||||
let probes = 0;
|
||||
function yes(): true {
|
||||
probes++;
|
||||
return true;
|
||||
}
|
||||
function no(): false {
|
||||
probes++;
|
||||
return false;
|
||||
}
|
||||
console.log(
|
||||
readdirSync(join(root, "b"), { recursive: yes() }).length,
|
||||
readdirSync(join(root, "b"), { recursive: no() }).length,
|
||||
probes,
|
||||
);
|
||||
|
||||
try {
|
||||
readdirSync(join(root, "missing"), { recursive: true });
|
||||
} catch (e) {
|
||||
console.log((e as NodeJS.ErrnoException).code);
|
||||
}
|
||||
|
||||
rmSync(root, { recursive: true });
|
||||
@@ -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,29 @@
|
||||
// Localized failure, stage L2: reference cycles through dynamic slots are
|
||||
// collected (instance -> dynamic field -> capsule -> instance).
|
||||
type Peer = Link | Map<string, (x: number) => string> | AbortSignal | (() => number);
|
||||
|
||||
class Link {
|
||||
id: number;
|
||||
peer: Peer | undefined;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
this.peer = undefined;
|
||||
}
|
||||
peerId(): number {
|
||||
const p = this.peer;
|
||||
return p instanceof Link ? p.id : -1;
|
||||
}
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
const a = new Link(i);
|
||||
const b = new Link(i + 1);
|
||||
a.peer = b;
|
||||
b.peer = a;
|
||||
total += a.peerId() - b.peerId();
|
||||
}
|
||||
const keep = new Link(7);
|
||||
keep.peer = () => keep.id * 2;
|
||||
const f = keep.peer;
|
||||
console.log(total, typeof f === "function" ? f() : 0, keep.peerId());
|
||||
@@ -0,0 +1,64 @@
|
||||
// Localized failure, stage L2: fields, parameters and returns whose
|
||||
// declared type has no static lowering become checked dynamic slots, so
|
||||
// their class or function still compiles.
|
||||
interface ToolDefinition {
|
||||
name: string;
|
||||
execute(id: string, params: any): string;
|
||||
}
|
||||
type Progress = (done: number, total: number) => void;
|
||||
type ProgressOrSignal = Progress | AbortSignal | Map<string, (x: number) => string> | Set<number>;
|
||||
|
||||
class Session {
|
||||
// A field: the class registers, the field holds the array by reference.
|
||||
private tools: ToolDefinition[] = [];
|
||||
state: any = { count: 0 };
|
||||
readonly label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
add(tool: ToolDefinition): void {
|
||||
this.tools.push(tool);
|
||||
}
|
||||
run(id: string, params: unknown): string[] {
|
||||
const out: string[] = [];
|
||||
for (const tool of this.tools) out.push(tool.execute(id, params));
|
||||
this.state.count = this.state.count + 1;
|
||||
return out;
|
||||
}
|
||||
count(): number {
|
||||
return this.tools.length;
|
||||
}
|
||||
// A return slot.
|
||||
first(): ToolDefinition | ProgressOrSignal | undefined {
|
||||
return this.tools[0];
|
||||
}
|
||||
}
|
||||
|
||||
// A parameter: the function registers and is called from typed code.
|
||||
function listAll(dir: string, onProgress?: ProgressOrSignal): number {
|
||||
if (typeof onProgress === "function") onProgress(1, dir.length);
|
||||
else if (onProgress instanceof Set) return onProgress.size;
|
||||
return dir.length;
|
||||
}
|
||||
|
||||
// A base class with a dynamic parameter, and an override.
|
||||
class Base {
|
||||
notify(target: ProgressOrSignal | undefined): string {
|
||||
return target === undefined ? "base:none" : "base:" + typeof target;
|
||||
}
|
||||
}
|
||||
class Derived extends Base {
|
||||
override notify(target: ProgressOrSignal | undefined): string {
|
||||
return "derived/" + super.notify(target);
|
||||
}
|
||||
}
|
||||
|
||||
const echo: ToolDefinition = { name: "echo", execute: (id, params) => `${id}:${JSON.stringify(params)}` };
|
||||
const s = new Session("main");
|
||||
s.add(echo);
|
||||
s.add({ name: "len", execute: (id) => String(id.length) });
|
||||
console.log(s.label, s.count(), s.run("abc", { k: 1 }).join(" | "), s.state.count);
|
||||
console.log("same tool", s.first() === echo);
|
||||
console.log(listAll("hello"), listAll("hello", (d, t) => console.log("progress", d, t)), listAll("x", new Set([1, 2])));
|
||||
const objs: Base[] = [new Base(), new Derived()];
|
||||
for (const o of objs) console.log(o.notify(undefined), o.notify((a, b) => a + b));
|
||||
@@ -0,0 +1,30 @@
|
||||
// Localized failure, stage L4, at a forward-captured binding: functions
|
||||
// declared before a const capture it, and the const's initializer has an
|
||||
// expression whose type does not compile. The statement lowers again with
|
||||
// that expression degraded, and it initializes the binding the functions
|
||||
// captured: before the declaration a call throws ReferenceError, after it
|
||||
// the call reads the value.
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
function load(text: string): unknown {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
function main(): void {
|
||||
function nameOf(): string {
|
||||
return plugin.name;
|
||||
}
|
||||
function attempt(): string {
|
||||
try {
|
||||
return nameOf();
|
||||
} catch (e) {
|
||||
return e instanceof ReferenceError ? "ReferenceError" : "other";
|
||||
}
|
||||
}
|
||||
console.log(attempt());
|
||||
const plugin = { name: (load('{"name":"tags","hook":null}') as Plugin).name };
|
||||
console.log(attempt(), nameOf().length);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,24 @@
|
||||
// Localized failure, stage L4: expressions whose own type does not
|
||||
// compile (casts, literals with an unmappable contextual type, spread
|
||||
// clones, temporaries).
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
function load(text: string): unknown {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
function count(p: { name: string }): number {
|
||||
return p.name.length;
|
||||
}
|
||||
const raw = load('{"name":"tags","hook":null}');
|
||||
// A cast to a type that does not compile.
|
||||
console.log(count(raw as Plugin));
|
||||
// A literal whose contextual type does not compile, used without a slot.
|
||||
console.log(count({ name: "lit", hook: () => 1 } as Plugin));
|
||||
// A spread clone of such a value.
|
||||
const base = { name: "base", hook: new Set(["a"]) };
|
||||
console.log(count({ ...(base as Plugin), name: "clone" }));
|
||||
// A temporary: an array literal of them, mapped.
|
||||
console.log([{ name: "a", hook: () => 2 } as Plugin, { name: "bb", hook: new Set<string>() } as Plugin].map((p) => p.name).join("+"));
|
||||
@@ -0,0 +1,19 @@
|
||||
// Localized failure, stage L1: a statement that declares a binding an
|
||||
// earlier closure captures, and then a binding whose declared type has no
|
||||
// static lowering, still initializes the captured binding.
|
||||
type Callbacks = ((done: number) => void) | AbortSignal | Map<string, (x: number) => string> | Set<number>;
|
||||
|
||||
function run(): void {
|
||||
const read = (): number => total;
|
||||
const total = 3,
|
||||
hook: Callbacks = new Set<number>([1, 2]);
|
||||
console.log(read(), hook instanceof Set, hook instanceof Set && hook.size);
|
||||
let late = (): string => label;
|
||||
let label = "first",
|
||||
other: Callbacks = new Set<number>([3]);
|
||||
console.log(late(), other instanceof Set);
|
||||
label = "second";
|
||||
late = (): string => label + "!";
|
||||
console.log(late());
|
||||
}
|
||||
run();
|
||||
@@ -0,0 +1,28 @@
|
||||
// Localized failure, stage L2: parameters and returns of arrows, function
|
||||
// expressions and generic instances become checked dynamic slots.
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
|
||||
interface Model<T> {
|
||||
id: T;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
function retry<T>(label: string, request: () => Promise<T> | Hook): T | string {
|
||||
const r = request();
|
||||
return typeof r === "function" ? label + ":" + String(r()) : label + ":other";
|
||||
}
|
||||
|
||||
function describe<T>(model: Model<T>): string {
|
||||
return `${String(model.id)}/${model.hook instanceof Set ? model.hook.size : typeof model.hook}`;
|
||||
}
|
||||
|
||||
const run = (hook: Hook, n: number): string => (typeof hook === "function" ? String(hook() + n) : "set");
|
||||
const make = function (size: number): Hook {
|
||||
return new Set(Array.from({ length: size }, (_, i) => `k${i}`));
|
||||
};
|
||||
|
||||
console.log(run(() => 40, 2), run(new Set(["a"]), 1));
|
||||
const h = make(3);
|
||||
console.log(h instanceof Set ? h.size : -1);
|
||||
console.log(retry("a", () => () => 7), retry<number>("b", () => new Set<string>()));
|
||||
console.log(describe({ id: 5, hook: new Set(["x", "y"]) }), describe({ id: "s", hook: () => 1 }));
|
||||
@@ -0,0 +1,70 @@
|
||||
// Localized failure, stage L1: values of every kind held in a local or
|
||||
// module global whose declared type has no static lowering keep their
|
||||
// identity and behavior.
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
norm(): number {
|
||||
return Math.abs(this.x) + Math.abs(this.y);
|
||||
}
|
||||
}
|
||||
interface Cfg {
|
||||
name: string;
|
||||
depth: number;
|
||||
}
|
||||
type Anything =
|
||||
| Point
|
||||
| Cfg
|
||||
| number[]
|
||||
| Map<string, (x: number) => string>
|
||||
| AbortSignal
|
||||
| (() => number)
|
||||
| Promise<number>;
|
||||
|
||||
const p = new Point(3, -4);
|
||||
const cfg: Cfg = { name: "a", depth: 1 };
|
||||
const xs = [1, 2, 3];
|
||||
let counter = 0;
|
||||
const tick = (): number => ++counter;
|
||||
|
||||
// A module global holding a class instance.
|
||||
let held: Anything = p;
|
||||
if (held instanceof Point) {
|
||||
console.log("point", held.norm(), held === p);
|
||||
held.x = 10;
|
||||
}
|
||||
console.log("typed alias", p.x, p.norm());
|
||||
|
||||
// A record: mutation through the hole is visible in the typed alias.
|
||||
held = cfg;
|
||||
if (!(held instanceof Point) && !Array.isArray(held) && typeof held === "object" && "depth" in held) {
|
||||
held.depth = 5;
|
||||
console.log("record", held.name, held === cfg);
|
||||
}
|
||||
console.log("typed alias", cfg.depth, JSON.stringify(cfg), Object.keys(cfg).join(","));
|
||||
|
||||
// An array: pushes through the hole show in typed code.
|
||||
held = xs;
|
||||
if (Array.isArray(held)) {
|
||||
held.push(4);
|
||||
console.log("array", held.length, held === xs);
|
||||
}
|
||||
console.log("typed alias", xs.length, xs.join("+"));
|
||||
|
||||
function main(): void {
|
||||
// A local holding a closure, called through the hole.
|
||||
let local: Anything = tick;
|
||||
if (typeof local === "function") {
|
||||
console.log("closure", local(), local(), counter, local === tick);
|
||||
}
|
||||
// A promise, awaited through the hole.
|
||||
local = Promise.resolve(42);
|
||||
if (local instanceof Promise) {
|
||||
local.then((v) => console.log("promise", v));
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,43 @@
|
||||
// Localized failure, stage L1: locals, loop variables and module globals
|
||||
// whose declared type has no static lowering hold checked-dynamic values.
|
||||
type Progress = (done: number, total: number) => void;
|
||||
type Callbacks = Progress | AbortSignal | Map<string, (x: number) => string> | Set<number>;
|
||||
|
||||
const report: Progress = (done, total) => console.log(`progress ${done}/${total}`);
|
||||
const seen = new Set<number>([1, 2, 3]);
|
||||
|
||||
// A module global, written with typed values of several arms.
|
||||
let current: Callbacks | undefined = report;
|
||||
|
||||
function describe(label: string): void {
|
||||
// A local of the same declared type.
|
||||
const local: Callbacks | undefined = current;
|
||||
if (local === undefined) {
|
||||
console.log(label, "none");
|
||||
} else if (typeof local === "function") {
|
||||
local(1, 2);
|
||||
console.log(label, "function", local === report);
|
||||
} else if (local instanceof Set) {
|
||||
console.log(label, "set", local.size, local.has(2), local === seen);
|
||||
local.add(4);
|
||||
} else {
|
||||
console.log(label, "other");
|
||||
}
|
||||
}
|
||||
|
||||
describe("first");
|
||||
current = seen;
|
||||
describe("second");
|
||||
console.log("typed alias sees the write", seen.size, seen.has(4));
|
||||
current = undefined;
|
||||
describe("third");
|
||||
|
||||
// A loop variable over an array whose element type does not compile.
|
||||
const all: (Callbacks | number)[] = [report, seen, 7];
|
||||
for (const item of all) {
|
||||
if (typeof item === "number") console.log("number", item + 1);
|
||||
else if (typeof item === "function") item(3, 4);
|
||||
else console.log("object", item instanceof Set);
|
||||
}
|
||||
all.push(9);
|
||||
console.log(all.length);
|
||||
@@ -0,0 +1,37 @@
|
||||
// Localized failure, stage L2: pi-shaped declarations.
|
||||
interface Token {
|
||||
text: string;
|
||||
}
|
||||
interface AgentMessage {
|
||||
role: "user" | "assistant";
|
||||
content: string[];
|
||||
}
|
||||
type CreateRuntime = (cwd: string) => Promise<{ session: Session; extra: Map<string, (x: number) => string> | AbortSignal }>;
|
||||
|
||||
class Session {
|
||||
messages: AgentMessage[] = [];
|
||||
// A field whose union mentions a stdlib type with no lowering: the class
|
||||
// registers; constructing a WeakRef would still be refused.
|
||||
cachedTokens?: WeakRef<{ source: string; tokens: Token[] }> | undefined;
|
||||
rendererState: any = {};
|
||||
effectState: Record<string, unknown> = {};
|
||||
private readonly createRuntime: CreateRuntime | undefined;
|
||||
constructor(createRuntime: CreateRuntime | undefined) {
|
||||
this.createRuntime = createRuntime;
|
||||
}
|
||||
hasRuntime(): boolean {
|
||||
return this.createRuntime !== undefined;
|
||||
}
|
||||
clear(): AgentMessage[] {
|
||||
// An inferred return over a spread clone.
|
||||
return this.messages.map((message) => ({ ...message, content: [] }));
|
||||
}
|
||||
}
|
||||
|
||||
const s = new Session(undefined);
|
||||
const withRuntime = new Session(async (cwd) => ({ session: s, extra: new Map() }));
|
||||
s.messages.push({ role: "user", content: ["hi"] }, { role: "assistant", content: ["yo", "!"] });
|
||||
s.rendererState.mode = "fast";
|
||||
s.effectState["k"] = 3;
|
||||
console.log(s.hasRuntime(), s.cachedTokens === undefined, s.rendererState.mode, s.effectState["k"]);
|
||||
console.log(JSON.stringify(s.clear()), s.messages[1]!.content.length, withRuntime.hasRuntime());
|
||||
@@ -0,0 +1,52 @@
|
||||
// Localized failure, stage L3: push-down. A declared type that does not
|
||||
// compile keeps its compilable shape; only the failing component is a
|
||||
// checked dynamic piece.
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
|
||||
interface Plugin {
|
||||
name: string;
|
||||
priority: number;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
class Registry {
|
||||
plugins: Plugin[] = [];
|
||||
byName = new Map<string, Plugin>();
|
||||
pending: Promise<Plugin> | undefined;
|
||||
add(p: Plugin): void {
|
||||
this.plugins.push(p);
|
||||
this.byName.set(p.name, p);
|
||||
}
|
||||
total(): number {
|
||||
let sum = 0;
|
||||
for (const p of this.plugins) sum += p.priority;
|
||||
return sum;
|
||||
}
|
||||
names(): string {
|
||||
return this.plugins.map((p) => p.name).join(",");
|
||||
}
|
||||
run(name: string): string {
|
||||
const p = this.byName.get(name);
|
||||
if (p === undefined) return "missing";
|
||||
const h = p.hook;
|
||||
return typeof h === "function" ? `${p.name}=${h()}` : h instanceof Set ? `${p.name}#${h.size}` : "other";
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = (event: string, extra: Hook) => number;
|
||||
function dispatch(handlers: Handler[], event: string): number {
|
||||
let n = 0;
|
||||
for (const h of handlers) n += h(event, () => event.length);
|
||||
return n;
|
||||
}
|
||||
|
||||
const r = new Registry();
|
||||
const counter: Plugin = { name: "counter", priority: 2, hook: () => 41 };
|
||||
r.add(counter);
|
||||
r.add({ name: "tags", priority: 5, hook: new Set(["a", "b"]) });
|
||||
console.log(r.total(), r.names(), r.run("counter"), r.run("tags"), r.run("x"));
|
||||
counter.priority = 10;
|
||||
console.log(r.total(), r.byName.get("counter") === counter, JSON.stringify(r.plugins.map((p) => [p.name, p.priority])));
|
||||
r.pending = Promise.resolve(counter);
|
||||
r.pending.then((p) => console.log("pending", p.name, p === counter));
|
||||
console.log(dispatch([(e, x) => e.length, (e, x) => (typeof x === "function" ? x() * 2 : 0)], "hello"));
|
||||
@@ -0,0 +1,46 @@
|
||||
// Localized failure, stage L2: a class that registers with dynamic slots
|
||||
// runs its static field initializers where its statement evaluates, and a
|
||||
// generic class's static methods compile once it registers.
|
||||
type Progress = (done: number, total: number) => void;
|
||||
type Callbacks = Progress | AbortSignal | Map<string, (x: number) => string> | Set<number>;
|
||||
|
||||
function sideEffect(label: string): number {
|
||||
console.log("static init", label);
|
||||
return label.length;
|
||||
}
|
||||
|
||||
// Registers at its statement: the initializers run before the next line.
|
||||
class Registry {
|
||||
state: any = { n: 1 };
|
||||
static count = sideEffect("registry");
|
||||
static names: string[] = ["a", "b"];
|
||||
read(): number {
|
||||
return this.state.n;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("after the class statement");
|
||||
|
||||
// Never referenced: its initializer runs all the same.
|
||||
class Unused {
|
||||
slot: any = null;
|
||||
static ready = sideEffect("unused");
|
||||
}
|
||||
|
||||
// A generic class whose static method has a dynamic parameter.
|
||||
class Box<T> {
|
||||
value: T;
|
||||
constructor(v: T) {
|
||||
this.value = v;
|
||||
}
|
||||
static describe(target: Callbacks | undefined): string {
|
||||
return target === undefined ? "none" : typeof target;
|
||||
}
|
||||
static label(): string {
|
||||
return "box";
|
||||
}
|
||||
}
|
||||
|
||||
const r = new Registry();
|
||||
console.log(r.read(), Registry.count, Registry.names.join(","));
|
||||
console.log(Box.label(), Box.describe(undefined), Box.describe(new Set([1])), new Box(3).value);
|
||||
@@ -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,22 @@
|
||||
// net.createConnection(path) through the default import of node:net, as a
|
||||
// JavaScript module reads it: a client of a Unix domain socket server.
|
||||
import net from "node:net";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-default-"));
|
||||
const socketPath = path.join(dir, "server.sock");
|
||||
const server = net.createServer((socket) => socket.end("hello"));
|
||||
server.listen(socketPath, () => {
|
||||
const sock = net.createConnection(socketPath);
|
||||
sock.once("connect", () => console.log("connected"));
|
||||
sock.on("data", (chunk) => console.log("data", String(chunk)));
|
||||
sock.once("error", (e) => console.log("error", String(e)));
|
||||
sock.on("close", () => {
|
||||
server.close(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log("closed");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
// net.createConnection and net.connect to a Unix domain socket: a path
|
||||
// argument, the `path` option, a connect listener, data both ways, the
|
||||
// error for a path nothing listens on, and a numeric string, which Node
|
||||
// treats as a localhost port.
|
||||
import * as net from "node:net";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-connect-"));
|
||||
const socketPath = path.join(dir, "server.sock");
|
||||
const missingPath = path.join(dir, "missing.sock");
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
socket.on("data", (chunk) => {
|
||||
socket.write(`echo:${chunk.toString()}`);
|
||||
});
|
||||
});
|
||||
|
||||
function exchange(label: string, socket: net.Socket, message: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
socket.on("data", (chunk) => {
|
||||
console.log(label, chunk.toString());
|
||||
socket.end();
|
||||
});
|
||||
socket.on("close", () => resolve());
|
||||
socket.write(message);
|
||||
});
|
||||
}
|
||||
|
||||
server.listen(socketPath, async () => {
|
||||
console.log("listening");
|
||||
const first = net.createConnection(socketPath);
|
||||
first.once("connect", () => console.log("first connected"));
|
||||
await exchange("first", first, "one");
|
||||
|
||||
let listenerCalls = 0;
|
||||
const second = net.connect(socketPath, () => {
|
||||
listenerCalls++;
|
||||
});
|
||||
await exchange("second", second, "two");
|
||||
console.log("second listener calls", listenerCalls);
|
||||
|
||||
const third = net.createConnection({ path: socketPath });
|
||||
await exchange("third", third, "three");
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const missing = net.createConnection(missingPath);
|
||||
missing.once("connect", () => console.log("missing connected"));
|
||||
missing.once("error", (error: NodeJS.ErrnoException) => {
|
||||
console.log("missing", error.code, error.message === `connect ENOENT ${missingPath}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
server.close(() => {
|
||||
const tcp = net.createServer((socket) => socket.end("tcp"));
|
||||
tcp.listen(0, "localhost", () => {
|
||||
const { port } = tcp.address() as net.AddressInfo;
|
||||
const client = net.createConnection(String(port));
|
||||
client.on("data", (chunk) => console.log("numeric string", chunk.toString()));
|
||||
client.on("close", () => {
|
||||
tcp.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log("done");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// A string-literal union narrowed to `never` passes to a `never` parameter:
|
||||
// the exhaustiveness-check idiom over if-chains and switches compiles, and the
|
||||
// unreachable call never runs.
|
||||
|
||||
function unreachable(_: never): never {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
type Kind = "a" | "b";
|
||||
function viaIf(kind: Kind): number {
|
||||
if (kind === "a") return 1;
|
||||
if (kind === "b") return 2;
|
||||
return unreachable(kind);
|
||||
}
|
||||
|
||||
type Shape = "circle" | "square" | "triangle";
|
||||
function corners(shape: Shape): number {
|
||||
switch (shape) {
|
||||
case "circle":
|
||||
return 0;
|
||||
case "square":
|
||||
return 4;
|
||||
case "triangle":
|
||||
return 3;
|
||||
default:
|
||||
return unreachable(shape);
|
||||
}
|
||||
}
|
||||
|
||||
function asserted(kind: Kind): string {
|
||||
if (kind === "a" || kind === "b") return kind.toUpperCase();
|
||||
return unreachable(kind as never);
|
||||
}
|
||||
|
||||
type Level = 1 | 2;
|
||||
function viaNumbers(level: Level): string {
|
||||
if (level === 1) return "one";
|
||||
if (level === 2) return "two";
|
||||
return unreachable(level);
|
||||
}
|
||||
|
||||
const kinds: Kind[] = ["a", "b"];
|
||||
console.log(kinds.map(viaIf).join(","), viaIf(Date.now() > 0 ? "a" : "b"));
|
||||
const shapes: Shape[] = ["circle", "square", "triangle"];
|
||||
console.log(shapes.map(corners).join(","));
|
||||
console.log(kinds.map(asserted).join(""), viaNumbers(1), viaNumbers(2));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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(","));
|
||||
@@ -0,0 +1,23 @@
|
||||
// A rest parameter typed as a fixed tuple, read through a shorthand property
|
||||
// (`{ xs }`), directly or through a const alias, is the tuple of the call's
|
||||
// arguments.
|
||||
|
||||
function wrap(...xs: [number]) {
|
||||
return { xs };
|
||||
}
|
||||
function pair(...args: [string, number]) {
|
||||
const kept = args;
|
||||
return { kept, size: args.length };
|
||||
}
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
function forward(...args: [number, number]): number {
|
||||
return add(...args);
|
||||
}
|
||||
|
||||
const w = wrap(1);
|
||||
console.log(w.xs[0], w.xs.length, JSON.stringify(w));
|
||||
const p = pair("a", 2);
|
||||
console.log(p.kept[0], p.kept[1], p.size, JSON.stringify(p));
|
||||
console.log(forward(3, 4));
|
||||
@@ -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);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Speculative integer loops over typed-array views held by a const local
|
||||
// and by a parameter. The fast copy, the copy a failed check resumes in and
|
||||
// the ordinary copy all read and write the same views: a product that
|
||||
// leaves the safe integers fails the fast copy's check partway through,
|
||||
// and writes past the end are ignored.
|
||||
function fill(buffer: ArrayBuffer, count: number, seed: number): number {
|
||||
const view = new Float64Array(buffer);
|
||||
let l = seed;
|
||||
for (let i = 0; i < count; i++) {
|
||||
l = (l * 3877 + 29573) % 139968;
|
||||
view[i] = l;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
function grow(out: Int32Array, count: number, start: number): number {
|
||||
let x = start;
|
||||
for (let i = 0; i < count; i++) {
|
||||
x = x * 3 + 1;
|
||||
out[i] = x % 1000;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
function sum(view: Float64Array | Int32Array): number {
|
||||
let s = 0;
|
||||
for (const v of view) s += v;
|
||||
return s;
|
||||
}
|
||||
|
||||
const buffer = new ArrayBuffer(8 * 64);
|
||||
console.log(fill(buffer, 64, 42), sum(new Float64Array(buffer)));
|
||||
console.log(fill(buffer, 100, 7), new Float64Array(buffer)[63]);
|
||||
const out = new Int32Array(40);
|
||||
console.log(grow(out, 60, 1), sum(out), out[0], out[39]);
|
||||
console.log(grow(out, 10, 5), out.join(","));
|
||||
@@ -0,0 +1,42 @@
|
||||
// A function declared to return a string concatenates a string with an
|
||||
// element of a readonly string array. A missing element reads as undefined,
|
||||
// which `+` turns into "undefined" next to a string, so the result is always
|
||||
// a string and its call can be used inline.
|
||||
|
||||
const PAD: readonly string[] = ["00", "01", "02"];
|
||||
|
||||
function label(i: number): string {
|
||||
return "x-" + PAD[i];
|
||||
}
|
||||
function date(y: number, m: number, d: number): string {
|
||||
return y + "-" + PAD[m] + "-" + PAD[d];
|
||||
}
|
||||
function suffix(i: number): string {
|
||||
return PAD[i] + "!";
|
||||
}
|
||||
|
||||
console.log(label(1).length, label(1), label(7));
|
||||
console.log(date(2026, 1, 2).length, date(2026, 2, 9));
|
||||
console.log(suffix(0).toUpperCase(), suffix(5));
|
||||
|
||||
// Two possibly missing reads add to NaN when both are missing.
|
||||
function pair(i: number, j: number): string {
|
||||
return PAD[i] + PAD[j];
|
||||
}
|
||||
const both = pair(8, 9);
|
||||
console.log(pair(0, 1), typeof both, String(both));
|
||||
|
||||
// A missing element held in a binding or returned by a function is undefined,
|
||||
// so two of them still add to NaN.
|
||||
function at(i: number): string {
|
||||
return PAD[i];
|
||||
}
|
||||
function viaBinding(i: number, j: number): string {
|
||||
const head = PAD[i];
|
||||
return head + PAD[j];
|
||||
}
|
||||
function viaCall(i: number, j: number): string {
|
||||
return at(i) + PAD[j];
|
||||
}
|
||||
console.log(viaBinding(0, 1), String(viaBinding(5, 6)), typeof viaBinding(5, 6));
|
||||
console.log(viaCall(0, 2), String(viaCall(5, 6)), viaBinding(0, 9));
|
||||
@@ -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,35 @@
|
||||
// A recursive record keyed by unique symbols, built from literals whose
|
||||
// child arrays start empty, holds the nodes later pushed into those arrays.
|
||||
|
||||
const kValue: unique symbol = Symbol("value");
|
||||
const kChildren: unique symbol = Symbol("children");
|
||||
|
||||
interface TreeNode {
|
||||
[kChildren]: TreeNode[];
|
||||
[kValue]: number;
|
||||
}
|
||||
|
||||
function node(value: number): TreeNode {
|
||||
return { [kValue]: value, [kChildren]: [] };
|
||||
}
|
||||
|
||||
const root = node(0);
|
||||
root[kChildren].push(node(1));
|
||||
root[kChildren].push(node(2));
|
||||
root[kChildren][0]![kChildren].push(node(3));
|
||||
console.log(root[kChildren].length, root[kChildren][0]![kValue], root[kChildren][1]![kValue]);
|
||||
|
||||
function sum(n: TreeNode): number {
|
||||
let total = n[kValue];
|
||||
for (const child of n[kChildren]) total += sum(child);
|
||||
return total;
|
||||
}
|
||||
function depth(n: TreeNode): number {
|
||||
return 1 + Math.max(0, ...n[kChildren].map(depth));
|
||||
}
|
||||
console.log(sum(root), depth(root));
|
||||
|
||||
// A literal that starts with children.
|
||||
const seeded: TreeNode = { [kValue]: 10, [kChildren]: [node(11)] };
|
||||
seeded[kChildren].push(node(12));
|
||||
console.log(seeded[kChildren].map((c) => c[kValue]).join(","), sum(seeded));
|
||||
@@ -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,21 @@
|
||||
// typeof on names the program declares (`declare const`, `declare function`)
|
||||
// but nothing defines at run time answers "undefined"; reading them throws
|
||||
// ReferenceError.
|
||||
declare const Bun: { randomUUIDv7: () => string } | undefined;
|
||||
declare const Deno: { version: { deno: string } };
|
||||
declare function webkitRequestAnimationFrame(callback: () => void): number;
|
||||
console.log("start");
|
||||
console.log(typeof Bun, typeof Deno, typeof webkitRequestAnimationFrame);
|
||||
console.log(typeof Bun !== "undefined" ? Bun?.randomUUIDv7() : "no Bun");
|
||||
const runtime = typeof Deno === "object" ? "deno" : typeof Bun === "object" ? "bun" : "node";
|
||||
console.log(runtime);
|
||||
try {
|
||||
console.log(Bun);
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
try {
|
||||
console.log(typeof Deno.version);
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// A package typed from its JavaScript returns profiles without a key its
|
||||
// declarations promise ("a;b"). An optional read of that key answers
|
||||
// undefined for a profile, and for a missing one (the chain reads the
|
||||
// nullish receiver as undefined), in presence tests and in values.
|
||||
const { loadProfiles } = require("hostilekit");
|
||||
const profiles = loadProfiles();
|
||||
const present = profiles[0];
|
||||
const missing = profiles[5];
|
||||
console.log(present?.["a;b"] === undefined, present?.["a;b"] ?? "none", typeof present?.["a;b"]);
|
||||
console.log(missing?.["a;b"] === undefined, missing?.["a;b"] ?? "none", typeof missing?.["a;b"]);
|
||||
console.log(present["a;b"] === undefined, present.id);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export type Profile = { id: number; "a;b": string };
|
||||
export declare function loadProfiles(): Profile[];
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
function loadProfiles() {
|
||||
return [{ id: 1 }];
|
||||
}
|
||||
exports.loadProfiles = loadProfiles;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user