mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Merge main
This commit is contained in:
@@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, and `reduce`/`some`/`map` callbacks |
|
||||
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
|
||||
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Element-wise loops over several typed arrays: each loop runs to one
|
||||
// array's length and reads or writes the others at the same index.
|
||||
// array's length and reads or writes the others at the same index. Also a
|
||||
// row-major matrix-vector product with four accumulators per row.
|
||||
|
||||
function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void {
|
||||
for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!;
|
||||
@@ -21,6 +22,23 @@ function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray):
|
||||
for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8;
|
||||
}
|
||||
|
||||
function matvec(w: Float32Array, x: Float32Array, out: Float32Array, cols: number): void {
|
||||
for (let r = 0; r < out.length; r++) {
|
||||
const base = r * cols;
|
||||
let s0 = 0;
|
||||
let s1 = 0;
|
||||
let s2 = 0;
|
||||
let s3 = 0;
|
||||
for (let j = 0; j < cols; j += 4) {
|
||||
s0 += w[base + j]! * x[j]!;
|
||||
s1 += w[base + j + 1]! * x[j + 1]!;
|
||||
s2 += w[base + j + 2]! * x[j + 2]!;
|
||||
s3 += w[base + j + 3]! * x[j + 3]!;
|
||||
}
|
||||
out[r] = s0 + s1 + s2 + s3;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback methods over a whole typed array.
|
||||
function callbacks(samples: Float32Array, limit: number, data: Uint8Array): number {
|
||||
const energy = samples.reduce((sum, v) => sum + v * v, 0);
|
||||
@@ -53,6 +71,10 @@ const io = new Int32Array(n);
|
||||
const pixels = new Uint8ClampedArray(n);
|
||||
const bright = new Uint8ClampedArray(n);
|
||||
const rgba = new Uint8Array(n * 4);
|
||||
const side = 256;
|
||||
const weights = new Float32Array(side * side);
|
||||
const vector = new Float32Array(side);
|
||||
const product = new Float32Array(side);
|
||||
const luma = new Uint8Array(n);
|
||||
let state = 2463534242;
|
||||
const next = (): number => {
|
||||
@@ -73,6 +95,8 @@ for (let i = 0; i < n; i++) {
|
||||
pixels[i] = next() & 255;
|
||||
}
|
||||
for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255;
|
||||
for (let i = 0; i < weights.length; i++) weights[i] = (next() % 2001) / 1000 - 1;
|
||||
for (let i = 0; i < side; i++) vector[i] = (next() % 1001) / 1000;
|
||||
|
||||
const rounds = Math.round(1200 * scale);
|
||||
let f64Sum = 0;
|
||||
@@ -81,6 +105,7 @@ let byteSum = 0;
|
||||
let intSum = 0;
|
||||
let brightSum = 0;
|
||||
let lumaSum = 0;
|
||||
let productSum = 0;
|
||||
let callbackSum = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
// Change one input element per round so no round repeats the last.
|
||||
@@ -91,12 +116,14 @@ for (let r = 0; r < rounds; r++) {
|
||||
ia[at] = r * 65537;
|
||||
pixels[at] = r & 255;
|
||||
rgba[at * 4] = r & 255;
|
||||
vector[r & (side - 1)] = (r % 1001) / 1000;
|
||||
axpy(1.5, x, y, d);
|
||||
blend((r & 15) / 16, fa, fb, fo);
|
||||
mask(data, key, masked);
|
||||
addWrap(ia, ib, io);
|
||||
brighten(pixels, 0.75 + (r & 7) / 8, bright);
|
||||
gray(rgba, luma);
|
||||
matvec(weights, vector, product, side);
|
||||
if ((r & 3) === 0) callbackSum += callbacks(fo, 140 + (r & 12), masked);
|
||||
const probe = (r * 104729) & (n - 1);
|
||||
f64Sum += d[probe]!;
|
||||
@@ -105,6 +132,7 @@ for (let r = 0; r < rounds; r++) {
|
||||
intSum = (intSum + io[probe]!) | 0;
|
||||
brightSum += bright[probe]!;
|
||||
lumaSum += luma[probe]!;
|
||||
productSum += product[probe & (side - 1)]!;
|
||||
}
|
||||
console.log(`float64 ${f64Sum.toFixed(3)}`);
|
||||
console.log(`float32 ${f32Sum.toFixed(3)}`);
|
||||
@@ -112,4 +140,5 @@ console.log(`uint8 ${byteSum}`);
|
||||
console.log(`int32 ${intSum}`);
|
||||
console.log(`clamped ${brightSum}`);
|
||||
console.log(`gray ${lumaSum}`);
|
||||
console.log(`matvec ${productSum.toFixed(3)}`);
|
||||
console.log(`callbacks ${callbackSum.toFixed(3)}`);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v26");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v27");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,39 @@ test("a counted loop's affine accesses get one guard and exact operand ranges",
|
||||
expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER });
|
||||
});
|
||||
|
||||
test("invariant terms without a range are tested by the guard", () => {
|
||||
// for (let i = 0; i < n; i++) out[i] = src[k + i], with `k` a parameter
|
||||
const shifted = bin("+", ref("k"), ref("i"));
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]);
|
||||
const kernel = fn([loop]);
|
||||
const version = match(kernel, loop)!;
|
||||
expect([...version.guarded]).toEqual(["v1:k"]);
|
||||
expect(version.affine.map((c) => c.terms.map((t) => t.key))).toEqual([[], ["v1:k"]]);
|
||||
// In the fast copy the term has the range the guard tests.
|
||||
expect(version.ranges.get(shifted.kind === "bin" ? shifted.left : shifted)).toEqual({
|
||||
min: -(2 ** 32),
|
||||
max: 2 ** 32,
|
||||
});
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
|
||||
// -2^32 <= k <= 2^32, and k is the double of its frozen integer.
|
||||
expect(guard).toMatch(/fcmp oge double %\S+, 0xC1F0000000000000/);
|
||||
expect(guard).toMatch(/fcmp ole double %\S+, 0x41F0000000000000/);
|
||||
expect(guard).toMatch(/freeze i64/);
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
expect(fast).not.toContain("bytes.index");
|
||||
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
|
||||
});
|
||||
|
||||
test("statically ranged indices compare one constant with the length", () => {
|
||||
const masked = bin("&", bin("^", ref("i"), ref("k")), num(255));
|
||||
const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -50,6 +51,9 @@ export interface BytesLoopVersion {
|
||||
ownedReceivers: number[];
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** Keys of affine terms the guard tests at run time: exact integers
|
||||
* within GUARDED_TERM. */
|
||||
guarded: ReadonlySet<string>;
|
||||
/** The counted loop whose iteration space the affine checks cover. */
|
||||
counted: IntegerCountedForLoop | null;
|
||||
/** The guard also requires every counter value within [-bound, bound]. */
|
||||
@@ -83,6 +87,9 @@ const MAX_FIXED_END = 2 ** 32;
|
||||
const MAX_CHECKS = 8;
|
||||
const MAX_OFFSET = 2 ** 31;
|
||||
const SAFE = Number.MAX_SAFE_INTEGER;
|
||||
/** The range the guard requires of an invariant index term without a
|
||||
* proven one: every index of a typed array of at most 2^32 elements. */
|
||||
const GUARDED_TERM: IntegerRange = { min: -(2 ** 32), max: 2 ** 32 };
|
||||
const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]);
|
||||
|
||||
/** Loop versioning for typed-array bounds. A typed array's length and data
|
||||
@@ -95,7 +102,9 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"])
|
||||
* `hi < length`, such as `table[x & 0xff]` or a constant-trip counter;
|
||||
* - an index `k * counter + invariant + c` of a counted loop needs its first
|
||||
* and last values in bounds, such as `out[base + i]` for `i < n` or
|
||||
* `view.getUint32(i * 4)`.
|
||||
* `view.getUint32(i * 4)`. An invariant without a proven integer range,
|
||||
* such as `base` for `const base = r * cols`, must also be an exact
|
||||
* integer between -2^32 and 2^32, which the guard tests.
|
||||
*
|
||||
* In worker programs, every access of a view that may be shared tests its
|
||||
* shared storage. A view's storage is also fixed at construction, so the
|
||||
@@ -198,9 +207,8 @@ export function matchBytesLoopVersion(
|
||||
}
|
||||
return invariant(e.localId) ? e : null;
|
||||
};
|
||||
// A pure integer operand evaluated again before the loop.
|
||||
const invariantKey = (e: IrExpr): string | null => {
|
||||
if (!facts.ranges.get(e)) return null;
|
||||
// A pure loop-invariant number, evaluated again before the loop.
|
||||
const pureKey = (e: IrExpr): string | null => {
|
||||
if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null;
|
||||
if (e.kind === "varRef")
|
||||
return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null;
|
||||
@@ -210,12 +218,27 @@ export function matchBytesLoopVersion(
|
||||
e.left.type.kind === "f64" &&
|
||||
e.right.type.kind === "f64"
|
||||
) {
|
||||
const left = invariantKey(e.left),
|
||||
right = invariantKey(e.right);
|
||||
const left = pureKey(e.left),
|
||||
right = pureKey(e.right);
|
||||
return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// Invariant terms without a proven range, such as `base` in
|
||||
// `w[base + j]` with `const base = r * cols` for a parameter `cols`. The
|
||||
// guard tests each one: an exact integer (not -0) within GUARDED_TERM.
|
||||
const guardedRanges = new Map<IrExpr, IntegerRange>();
|
||||
const guardedNodes = new Map<IrExpr, string>();
|
||||
// The key of a loop-invariant index term, with a proven range or guarded.
|
||||
const invariantKey = (e: IrExpr): string | null => {
|
||||
const key = pureKey(e);
|
||||
if (key === null || facts.ranges.get(e)) return key;
|
||||
if (e.kind === "numLit") return null;
|
||||
guardedRanges.set(e, GUARDED_TERM);
|
||||
guardedNodes.set(e, key);
|
||||
return key;
|
||||
};
|
||||
const termRange = (e: IrExpr): IntegerRange => facts.ranges.get(e) ?? guardedRanges.get(e)!;
|
||||
interface Affine {
|
||||
/** The counter's coefficient; 0 when the value is loop-invariant. */
|
||||
counter: number;
|
||||
@@ -223,9 +246,10 @@ export function matchBytesLoopVersion(
|
||||
offset: number;
|
||||
}
|
||||
// Every operand of an affine index is `counter * k + Σ terms + offset`.
|
||||
// Invariant terms need proven ranges. Operands that involve the counter
|
||||
// need none: the guard bounds the counter so that each of them is a safe
|
||||
// integer, which makes the index's double arithmetic exact.
|
||||
// Invariant terms need proven ranges or the guard's test (guarded terms).
|
||||
// Operands that involve the counter need none: the guard bounds the
|
||||
// counter so that each of them is a safe integer, which makes the index's
|
||||
// double arithmetic exact.
|
||||
const shapes = new Map<IrExpr, Affine | null>();
|
||||
const affine = (e: IrExpr): Affine | null => {
|
||||
if (shapes.has(e)) return shapes.get(e)!;
|
||||
@@ -309,7 +333,7 @@ export function matchBytesLoopVersion(
|
||||
};
|
||||
const termMagnitude = (terms: Term[]): number =>
|
||||
terms.reduce((sum, t) => {
|
||||
const r = facts.ranges.get(t.expr)!;
|
||||
const r = termRange(t.expr);
|
||||
return sum + Math.max(Math.abs(r.min), Math.abs(r.max));
|
||||
}, 0);
|
||||
|
||||
@@ -433,7 +457,7 @@ export function matchBytesLoopVersion(
|
||||
let min = Math.min(a, b) + shape.offset,
|
||||
max = Math.max(a, b) + shape.offset;
|
||||
for (const term of shape.terms) {
|
||||
const r = facts.ranges.get(term.expr)!;
|
||||
const r = termRange(term.expr);
|
||||
min += term.sign > 0 ? r.min : -r.max;
|
||||
max += term.sign > 0 ? r.max : -r.min;
|
||||
}
|
||||
@@ -447,6 +471,12 @@ export function matchBytesLoopVersion(
|
||||
ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) });
|
||||
}
|
||||
}
|
||||
// Guarded terms of the kept checks, and the range the guard proves for
|
||||
// every node of each in the fast copy.
|
||||
const guarded = new Set<string>();
|
||||
for (const group of affineKept)
|
||||
for (const term of group.check.terms) if (guardedRanges.has(term.expr)) guarded.add(term.key);
|
||||
for (const [node, key] of guardedNodes) if (guarded.has(key)) ranges.set(node, GUARDED_TERM);
|
||||
// Renumber the receivers the kept checks use.
|
||||
const order = [...used].sort((a, b) => a - b);
|
||||
const renumber = new Map(order.map((old, index) => [old, index]));
|
||||
@@ -470,6 +500,7 @@ export function matchBytesLoopVersion(
|
||||
minOffset: g.check.minOffset,
|
||||
maxEnd: g.check.maxEnd,
|
||||
})),
|
||||
guarded,
|
||||
counted: affineKept.length > 0 ? counted : null,
|
||||
counterBound: needsBound ? bound : null,
|
||||
ranges,
|
||||
@@ -567,6 +598,14 @@ export function emitBytesLoopVersionGuard(
|
||||
let base = "0";
|
||||
for (const term of check.terms) {
|
||||
const value = host.emitExpr(term.expr);
|
||||
if (version.guarded.has(term.key)) {
|
||||
const tested = emitGuardedTerm(host, value.name);
|
||||
conditions.push(tested.ok);
|
||||
const sum = B.tmp();
|
||||
B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`);
|
||||
base = sum;
|
||||
continue;
|
||||
}
|
||||
const integer = exactInteger(host, value, term.expr);
|
||||
if (!integer) {
|
||||
// Unreachable: every term has a proven range. Stay checked.
|
||||
@@ -614,6 +653,34 @@ export function emitBytesLoopVersionGuard(
|
||||
else B.condBr(valid, fast, slow);
|
||||
}
|
||||
|
||||
/** Tests that a guarded term is an exact integer within GUARDED_TERM (not
|
||||
* -0; NaN fails the ordered comparisons) and gives its i64. The integer is
|
||||
* frozen, so it is a fixed value even when the test fails. */
|
||||
function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; integer: string } {
|
||||
const B = host.B;
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
inRange = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
integer = B.tmp(),
|
||||
back = B.tmp(),
|
||||
bits = B.tmp(),
|
||||
backBits = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = fcmp oge double ${value}, ${f64Lit(GUARDED_TERM.min)}`);
|
||||
B.line(`${high} = fcmp ole double ${value}, ${f64Lit(GUARDED_TERM.max)}`);
|
||||
B.line(`${inRange} = and i1 ${low}, ${high}`);
|
||||
B.line(`${raw} = fptosi double ${value} to i64`);
|
||||
B.line(`${integer} = freeze i64 ${raw}`);
|
||||
B.line(`${back} = sitofp i64 ${integer} to double`);
|
||||
B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
B.line(`${backBits} = bitcast double ${back} to i64`);
|
||||
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
return { ok, integer };
|
||||
}
|
||||
|
||||
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
|
||||
if (scale === 1) return value;
|
||||
const scaled = host.B.tmp();
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type {
|
||||
IntegerCountedForLoop,
|
||||
IntegerLoopFollower,
|
||||
IntegerPairedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js";
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
@@ -61,9 +66,47 @@ export function emitCountedLoopSafety(
|
||||
);
|
||||
}
|
||||
if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start));
|
||||
for (const follower of loop.followers) {
|
||||
if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start));
|
||||
if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower));
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
/** `|start| + |step| * span / |driver step| <= 2^52` for a follower, where
|
||||
* `span` is the distance from the driver's start to the limit. The body runs
|
||||
* at most `span / |driver step| + 1` times, so every value the follower holds
|
||||
* on a body entry is within the bound. NaN and infinities fail the ordered
|
||||
* test. */
|
||||
function emitFollowerReach(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
follower: IntegerLoopFollower,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const start = host.emitExpr(loop.start),
|
||||
limit = host.emitExpr(loop.limit),
|
||||
from = host.emitExpr(follower.start);
|
||||
const span = B.tmp(),
|
||||
reach = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
total = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(
|
||||
loop.step > 0
|
||||
? `${span} = fsub double ${limit.name}, ${start.name}`
|
||||
: `${span} = fsub double ${start.name}, ${limit.name}`,
|
||||
);
|
||||
B.line(
|
||||
`${reach} = fmul double ${span}, ${f64Lit(Math.abs(follower.step) / Math.abs(loop.step))}`,
|
||||
);
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${from.name})`);
|
||||
B.line(`${total} = fadd double ${magnitude}, ${reach}`);
|
||||
B.line(`${ok} = fcmp ole double ${total}, ${f64Lit(FOLLOWER_GUARD_LIMIT)}`);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** The run-time test of a guarded paired loop: each unproven start is an
|
||||
* exact safe integer. */
|
||||
export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string {
|
||||
|
||||
@@ -120,6 +120,7 @@ const counted = (
|
||||
limitGuarded: false,
|
||||
startGuarded: false,
|
||||
guardLimit: 0,
|
||||
followers: [],
|
||||
...fields,
|
||||
});
|
||||
|
||||
|
||||
@@ -108,6 +108,15 @@ const DYN_HANDLE_UNBOX_FN: Record<string, string> = {
|
||||
dgramSocket: "scr_dgram_from_dyn",
|
||||
};
|
||||
|
||||
/** ScrMessageKind (scr_message.h): the node kinds message readers test. */
|
||||
const MESSAGE_KIND = {
|
||||
UNDEFINED: 0,
|
||||
NULL: 1,
|
||||
BOOL: 2,
|
||||
NUMBER: 3,
|
||||
STRING: 4,
|
||||
} as const;
|
||||
|
||||
export const DYN_KIND = {
|
||||
NULL: 0,
|
||||
BOOL: 1,
|
||||
@@ -173,6 +182,8 @@ export class LlDyn {
|
||||
private readonly dynFuncThunks = new Map<string, string>();
|
||||
private readonly dynFuncBoxes = new Map<string, string>();
|
||||
private readonly dynFuncAdapters = new Map<string, string>();
|
||||
private readonly messageListeners = new Map<string, string | null>();
|
||||
private readonly messageReaders = new Map<string, string>();
|
||||
private readonly promiseDynAdapters = new Map<string, string>();
|
||||
private readonly mapDynOps = new Map<string, string>();
|
||||
private dynToStrFn: string | null = null;
|
||||
@@ -4610,6 +4621,297 @@ export class LlDyn {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** `sc_dml_<n>(ptr c, ptr message) -> i1`, the typed message entry of a
|
||||
* closure registered as a 'message' listener (ScrDynMessageListener):
|
||||
* builds the closure's parameter from the message's nodes and calls the
|
||||
* closure. It answers false, without calling it, when the message does
|
||||
* not have the parameter's exact layout, and the listener then runs
|
||||
* through its thunk on the decoded message, as before. Null when the
|
||||
* signature's parameter has no message reader (messageReadable). */
|
||||
messageListenerHelper(t: IrType & { kind: "func" }): string | null {
|
||||
const key = typeKey(t);
|
||||
const existing = this.messageListeners.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
const param = t.params[0];
|
||||
const supported =
|
||||
t.params.length === 1 &&
|
||||
t.rest !== true &&
|
||||
param !== undefined &&
|
||||
(t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") &&
|
||||
this.messageReadable(param, new Set());
|
||||
if (!supported) {
|
||||
this.messageListeners.set(key, null);
|
||||
return null;
|
||||
}
|
||||
const name = `sc_dml_${this.messageListeners.size}`;
|
||||
this.messageListeners.set(key, name);
|
||||
const B = new BlockBuilder();
|
||||
B.entryAllocas.push(`%fail = alloca i1`);
|
||||
B.line(`store i1 false, ptr %fail`);
|
||||
const value = B.tmp();
|
||||
B.line(
|
||||
`${value} = call ${this.valTy(param)} @${this.messageReaderHelper(param)}(ptr %m, ${this.host.sizeType} 0, ptr %fail)`,
|
||||
);
|
||||
const failed = B.tmp();
|
||||
B.line(`${failed} = load i1, ptr %fail`);
|
||||
const lMiss = B.newLabel("dml.miss");
|
||||
const lCall = B.newLabel("dml.call");
|
||||
B.condBr(failed, lMiss, lCall);
|
||||
B.startBlock(lMiss);
|
||||
B.terminate(`ret i1 false`);
|
||||
B.startBlock(lCall);
|
||||
// The closure consumes its parameter; a pending exception stays for
|
||||
// the emitter, as after the thunk.
|
||||
const fnp = B.tmp();
|
||||
const fn = B.tmp();
|
||||
B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`);
|
||||
B.line(`${fn} = load ptr, ptr ${fnp}`);
|
||||
const args = `ptr %c, ${this.valTy(param)} ${value}`;
|
||||
if (t.ret.kind === "void") B.line(`call void ${fn}(${args})`);
|
||||
else B.line(`${B.tmp()} = call ${this.valTy(t.ret)} ${fn}(${args})`);
|
||||
B.terminate(`ret i1 true`);
|
||||
this.defs.push(
|
||||
`define internal zeroext i1 @${name}(ptr %c, ptr %m) ${FN_ATTRS} { ; message entry for ${llvmCommentText(key)}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Whether a message reader can build T: numbers, booleans and strings;
|
||||
* records of them with only declared fields, none of union type; and
|
||||
* boxed unions of those arms and null or undefined without a literal
|
||||
* discriminator, with at most one number, boolean and string arm. */
|
||||
private messageReadable(t: IrType, seen: Set<string>): boolean {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "bool":
|
||||
case "string":
|
||||
return true;
|
||||
case "record": {
|
||||
const shape = this.host.recordsById.get(t.shapeId);
|
||||
if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0) return false;
|
||||
if (seen.has(t.shapeId) || this.isRecursiveShape(t)) return false;
|
||||
seen.add(t.shapeId);
|
||||
const readable = shape.fields.every(
|
||||
(f) =>
|
||||
!f.name.startsWith("%") &&
|
||||
f.type.kind !== "union" &&
|
||||
llFieldType(f.type, this.host.inlineUnions.record(f.type)) !== INLINE_UNION_FIELD &&
|
||||
this.messageReadable(f.type, seen),
|
||||
);
|
||||
seen.delete(t.shapeId);
|
||||
return readable;
|
||||
}
|
||||
case "union": {
|
||||
const def = this.host.unionsById.get(t.unionId);
|
||||
// A discriminated union's checked conversion also tests the arm's
|
||||
// literal discriminator, which the readers do not.
|
||||
if (!def || def.discriminant || this.host.nullableUnions.has(t.unionId)) return false;
|
||||
const scalars = new Set<string>();
|
||||
for (const arm of def.arms) {
|
||||
if (arm.kind === "nullT" || arm.kind === "undefinedT") continue;
|
||||
if (arm.kind === "f64" || arm.kind === "bool" || arm.kind === "string") {
|
||||
if (scalars.has(arm.kind)) return false;
|
||||
scalars.add(arm.kind);
|
||||
continue;
|
||||
}
|
||||
if (arm.kind !== "record" || !this.messageReadable(arm, seen)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** `sc_dmr_<n>(ptr message, size node, ptr fail) -> T`: T (+1) built
|
||||
* from a message node, for a type messageReadable accepts. Sets *fail
|
||||
* and returns a dummy when the node does not have T's layout: the arm
|
||||
* the checked conversion would choose, a record's declared keys in
|
||||
* declaration order and no others. */
|
||||
private messageReaderHelper(t: IrType): string {
|
||||
const key = typeKey(t);
|
||||
const existing = this.messageReaders.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `sc_dmr_${this.messageReaders.size}`;
|
||||
this.messageReaders.set(key, name);
|
||||
const host = this.host;
|
||||
const S = host.sizeType;
|
||||
if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`);
|
||||
const retTy = this.valTy(t);
|
||||
const dummy = retTy === "double" ? f64Lit(0) : retTy === "i1" ? "false" : "null";
|
||||
const B = new BlockBuilder();
|
||||
const lFail = B.newLabel("dmr.fail");
|
||||
// scr_message_fields tests a record's kind itself.
|
||||
const kind = B.tmp();
|
||||
if (t.kind !== "record") B.line(`${kind} = call i32 @scr_message_kind(ptr %m, ${S} %n)`);
|
||||
const requireKind = (want: number): void => {
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = icmp eq i32 ${kind}, ${want}`);
|
||||
const lOk = B.newLabel("dmr.kind");
|
||||
B.condBr(ok, lOk, lFail);
|
||||
B.startBlock(lOk);
|
||||
};
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "bool": {
|
||||
host.declare(`declare double @scr_message_number(ptr, ${S})`);
|
||||
requireKind(t.kind === "f64" ? MESSAGE_KIND.NUMBER : MESSAGE_KIND.BOOL);
|
||||
const n = B.tmp();
|
||||
B.line(`${n} = call double @scr_message_number(ptr %m, ${S} %n)`);
|
||||
if (t.kind === "f64") B.terminate(`ret double ${n}`);
|
||||
else {
|
||||
const b = B.tmp();
|
||||
B.line(`${b} = fcmp une double ${n}, ${f64Lit(0)}`);
|
||||
B.terminate(`ret i1 ${b}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "string": {
|
||||
host.declare(`declare ptr @scr_message_string(ptr, ${S})`);
|
||||
requireKind(MESSAGE_KIND.STRING);
|
||||
const str = B.tmp();
|
||||
B.line(`${str} = call ptr @scr_message_string(ptr %m, ${S} %n)`);
|
||||
B.terminate(`ret ptr ${str}`);
|
||||
break;
|
||||
}
|
||||
case "record": {
|
||||
const shape = host.recordsById.get(t.shapeId)!;
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
const fields = new Map(shape.fields.map((f, i) => [f.name, { field: f, index: i }]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const names = this.recordNameTable(t.shapeId, shape);
|
||||
host.declare(`declare zeroext i1 @scr_message_fields(ptr, ${S}, ptr, ptr, ${S}, ptr)`);
|
||||
B.entryAllocas.push(`%kids = alloca [${order.length} x ${S}]`);
|
||||
const matched = B.tmp();
|
||||
B.line(
|
||||
`${matched} = call zeroext i1 @scr_message_fields(ptr %m, ${S} %n, ptr ${names.names}, ptr ${names.lens}, ${S} ${order.length}, ptr %kids)`,
|
||||
);
|
||||
const lBuild = B.newLabel("dmr.build");
|
||||
B.condBr(matched, lBuild, lFail);
|
||||
B.startBlock(lBuild);
|
||||
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
|
||||
order.forEach((fieldName, i) => {
|
||||
const { field, index } = fields.get(fieldName)!;
|
||||
const kidp = B.tmp();
|
||||
const kid = B.tmp();
|
||||
B.line(
|
||||
`${kidp} = getelementptr inbounds [${order.length} x ${S}], ptr %kids, i64 0, i64 ${i}`,
|
||||
);
|
||||
B.line(`${kid} = load ${S}, ptr ${kidp}`);
|
||||
const v = B.tmp();
|
||||
B.line(
|
||||
`${v} = call ${this.valTy(field.type)} @${this.messageReaderHelper(field.type)}(ptr %m, ${S} ${kid}, ptr %fail)`,
|
||||
);
|
||||
const failed = B.tmp();
|
||||
B.line(`${failed} = load i1, ptr %fail`);
|
||||
const lDrop = B.newLabel("dmr.drop");
|
||||
const lStore = B.newLabel("dmr.store");
|
||||
B.condBr(failed, lDrop, lStore);
|
||||
B.startBlock(lDrop);
|
||||
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
|
||||
B.terminate(`ret ptr null`);
|
||||
B.startBlock(lStore);
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${index + base} ; .${llvmCommentText(fieldName)}`,
|
||||
);
|
||||
const fieldTy = llFieldType(field.type, host.inlineUnions.record(field.type));
|
||||
if (fieldTy === "i8") {
|
||||
const z = B.tmp();
|
||||
B.line(`${z} = zext i1 ${v} to i8`);
|
||||
B.line(`store i8 ${z}, ptr ${p}`);
|
||||
} else {
|
||||
B.line(`store ${fieldTy} ${v}, ptr ${p}`);
|
||||
}
|
||||
});
|
||||
B.terminate(`ret ptr %r0`);
|
||||
break;
|
||||
}
|
||||
case "union": {
|
||||
const def = host.unionsById.get(t.unionId)!;
|
||||
def.arms.forEach((arm, i) => {
|
||||
const lNext = B.newLabel("dmr.next");
|
||||
if (arm.kind === "nullT" || arm.kind === "undefinedT") {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
`${hit} = icmp eq i32 ${kind}, ${arm.kind === "nullT" ? MESSAGE_KIND.NULL : MESSAGE_KIND.UNDEFINED}`,
|
||||
);
|
||||
const lHit = B.newLabel("dmr.unit");
|
||||
B.condBr(hit, lHit, lNext);
|
||||
B.startBlock(lHit);
|
||||
B.terminate(`ret ptr ${host.unitInstanceRef(t.unionId, i)}`);
|
||||
B.startBlock(lNext);
|
||||
return;
|
||||
}
|
||||
if (arm.kind !== "record") {
|
||||
const want =
|
||||
arm.kind === "f64"
|
||||
? MESSAGE_KIND.NUMBER
|
||||
: arm.kind === "bool"
|
||||
? MESSAGE_KIND.BOOL
|
||||
: MESSAGE_KIND.STRING;
|
||||
const hit = B.tmp();
|
||||
B.line(`${hit} = icmp eq i32 ${kind}, ${want}`);
|
||||
const lHit = B.newLabel("dmr.arm");
|
||||
B.condBr(hit, lHit, lNext);
|
||||
B.startBlock(lHit);
|
||||
}
|
||||
// A record arm that does not match leaves *fail clear for the
|
||||
// next arm, like the checked conversion's next-arm match.
|
||||
const v = B.tmp();
|
||||
B.line(
|
||||
`${v} = call ${this.valTy(arm)} @${this.messageReaderHelper(arm)}(ptr %m, ${S} %n, ptr %fail)`,
|
||||
);
|
||||
if (arm.kind === "record") {
|
||||
const failed = B.tmp();
|
||||
B.line(`${failed} = load i1, ptr %fail`);
|
||||
const lHit = B.newLabel("dmr.arm");
|
||||
const lMiss = B.newLabel("dmr.miss");
|
||||
B.condBr(failed, lMiss, lHit);
|
||||
B.startBlock(lMiss);
|
||||
B.line(`store i1 false, ptr %fail`);
|
||||
B.br(lNext);
|
||||
B.startBlock(lHit);
|
||||
}
|
||||
const u = B.tmp();
|
||||
if (arm.kind === "f64") {
|
||||
host.declare(`declare ptr @scr_union_new_f64(i32, double)`);
|
||||
B.line(`${u} = call ptr @scr_union_new_f64(i32 ${i}, double ${v})`);
|
||||
} else if (arm.kind === "bool") {
|
||||
host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`);
|
||||
B.line(`${u} = call ptr @scr_union_new_bool(i32 ${i}, i1 ${v})`);
|
||||
} else {
|
||||
const rc = vAdapters(host, arm);
|
||||
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${v}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
B.startBlock(lNext);
|
||||
});
|
||||
B.br(lFail);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: message reader of ${key}`);
|
||||
}
|
||||
B.startBlock(lFail);
|
||||
B.line(`store i1 true, ptr %fail`);
|
||||
B.terminate(`ret ${retTy} ${dummy}`);
|
||||
this.defs.push(
|
||||
`define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${name}(ptr %m, ${S} %n, ptr %fail) ${FN_ATTRS} { ; message reader for ${llvmCommentText(key)}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The adapter closure body for one TARGET signature: caps[0] is an
|
||||
* untraced obj-box owning the dyn function value. */
|
||||
dynFuncAdapterHelper(t: IrType & { kind: "func" }): string {
|
||||
|
||||
@@ -85,6 +85,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.runningExtrema = emitter.debug === null ? runningExtremumUpdates(fn) : new Set();
|
||||
emitter.currentConstantCallbacks =
|
||||
emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map();
|
||||
emitter.knownCallbacks.clear();
|
||||
emitter.callbackVersionsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
// Preserve concrete source bindings for debugger inspection. Suspended
|
||||
// functions keep the established array lifetime across continuations.
|
||||
emitter.streamingSplitsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
|
||||
@@ -55,6 +55,7 @@ import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js";
|
||||
import { mangleLocal } from "../mangle.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlValue } from "./expr-context.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { widenInteger } from "./integer-values.js";
|
||||
import { SCALAR_UNION_RESULT } from "./int32-abi.js";
|
||||
import { emitLiteralSwitch } from "./switch-dispatch.js";
|
||||
@@ -76,6 +77,9 @@ import {
|
||||
} from "./string-construction.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
|
||||
/** A callback loop version copies the loop; keep the copies small. */
|
||||
const MAX_CALLBACK_VERSION_COST = 400;
|
||||
|
||||
export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const B = emitter.B;
|
||||
emitter.frames.push([]);
|
||||
@@ -800,6 +804,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
case "while": {
|
||||
const callbacks = callbackVersionBindings(emitter, s);
|
||||
if (callbacks) {
|
||||
emitCallbackVersionedLoop(emitter, s, callbacks);
|
||||
break;
|
||||
}
|
||||
const workerBudget = emitter.workerLoopBudget(s);
|
||||
const le = B.newLabel("loop.e");
|
||||
emitter.emitLoopCopies(s, true, le, (exit) => {
|
||||
@@ -808,6 +817,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const lp = workerBudget !== null ? B.newLabel("loop.p") : lc;
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
enterLoopHeader(emitter, lc);
|
||||
B.condBr(emitter.emitCondition(s.cond), lb, exit);
|
||||
B.startBlock(lb);
|
||||
emitter.jumpTargets.push({
|
||||
@@ -832,6 +842,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
case "doWhile": {
|
||||
const callbacks = callbackVersionBindings(emitter, s);
|
||||
if (callbacks) {
|
||||
emitCallbackVersionedLoop(emitter, s, callbacks);
|
||||
break;
|
||||
}
|
||||
const workerBudget = emitter.workerLoopBudget(s);
|
||||
// Body first (runs at least once); continue jumps to the CONDITION.
|
||||
const le = B.newLabel("loop.e");
|
||||
@@ -840,6 +855,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const lc = B.newLabel("loop.c");
|
||||
B.br(lb);
|
||||
B.startBlock(lb);
|
||||
enterLoopHeader(emitter, lb);
|
||||
emitter.jumpTargets.push({
|
||||
kind: "loop",
|
||||
brkLabel: exit,
|
||||
@@ -860,6 +876,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
const callbacks = callbackVersionBindings(emitter, s);
|
||||
if (callbacks) {
|
||||
emitCallbackVersionedLoop(emitter, s, callbacks);
|
||||
break;
|
||||
}
|
||||
// The init's scope wraps the whole loop (break/continue must NOT
|
||||
// release it — scopeDepth captured after the push, C parity).
|
||||
const integerArrayLoop =
|
||||
@@ -884,8 +905,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
!integerLoop && !countedLoop && emitter.countedLoopsEnabled
|
||||
? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges)
|
||||
: null;
|
||||
// Byte windows and dense loops reason about one counter.
|
||||
const window =
|
||||
countedLoop && emitter.byteWindowsEnabled
|
||||
countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled
|
||||
? matchByteWindow(
|
||||
s,
|
||||
countedLoop,
|
||||
@@ -903,7 +925,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
|
||||
range: { min: 0, max: 4294967294 },
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded
|
||||
: countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
@@ -1035,6 +1057,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
let integerSlot: string | null = null;
|
||||
let countedLimit: string | null = null;
|
||||
const pairedSlots: string[] = [];
|
||||
const followerSlots: string[] = [];
|
||||
if (integerLoop) {
|
||||
integerSlot = B.slot();
|
||||
B.entryAllocas.push(
|
||||
@@ -1078,6 +1101,23 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
? emitter.denseLoop.range
|
||||
: countedLoop.range,
|
||||
});
|
||||
for (const follower of countedLoop.followers) {
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(
|
||||
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`,
|
||||
);
|
||||
const value = emitter.emitExpr(follower.start);
|
||||
const followerStart = B.tmp();
|
||||
B.line(`${followerStart} = fptosi double ${value.name} to i64`);
|
||||
B.line(`store i64 ${followerStart}, ptr ${slot}`);
|
||||
followerSlots.push(slot);
|
||||
emitter.integerLoopBindings.set(follower.localId, {
|
||||
slot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: follower.range,
|
||||
});
|
||||
}
|
||||
countedLimit = emitCountedLoopLimit(emitter, countedLoop);
|
||||
} else if (pairedLoop) {
|
||||
for (const counter of [pairedLoop.low, pairedLoop.high]) {
|
||||
@@ -1140,6 +1180,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc;
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
enterLoopHeader(emitter, lc);
|
||||
if (integerLoop && integerSlot) {
|
||||
const receiver = emitter.emitStableReceiver(integerLoop.limitReceiver, []);
|
||||
const lenPtr = B.tmp();
|
||||
@@ -1217,6 +1258,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`${old} = load i64, ptr ${integerSlot}`);
|
||||
B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`);
|
||||
B.line(`store i64 ${next}, ptr ${integerSlot}`);
|
||||
countedLoop.followers.forEach((follower, i) => {
|
||||
const was = B.tmp();
|
||||
const now = B.tmp();
|
||||
B.line(`${was} = load i64, ptr ${followerSlots[i]}`);
|
||||
B.line(`${now} = add nsw i64 ${was}, ${follower.step}`);
|
||||
B.line(`store i64 ${now}, ptr ${followerSlots[i]}`);
|
||||
});
|
||||
} else if (pairedLoop) {
|
||||
[pairedLoop.low, pairedLoop.high].forEach((counter, i) => {
|
||||
const old = B.tmp();
|
||||
@@ -1232,7 +1280,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.startBlock(le);
|
||||
emitter.releaseScope(emitter.scopes.pop()!);
|
||||
if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId);
|
||||
if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId);
|
||||
if (countedLoop) {
|
||||
emitter.integerLoopBindings.delete(countedLoop.localId);
|
||||
for (const follower of countedLoop.followers)
|
||||
emitter.integerLoopBindings.delete(follower.localId);
|
||||
}
|
||||
if (pairedLoop) {
|
||||
emitter.integerLoopBindings.delete(pairedLoop.low.localId);
|
||||
emitter.integerLoopBindings.delete(pairedLoop.high.localId);
|
||||
@@ -1890,6 +1942,105 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }):
|
||||
B.startBlock(end);
|
||||
}
|
||||
|
||||
/** The loop copies a callback loop version needs: the bindings the loop
|
||||
* calls that the constant-callback census resolves to one function, each
|
||||
* with that function's direct entry. Only small loops of optimized
|
||||
* synchronous functions, and only bindings declared before the loop,
|
||||
* qualify: the guard reads each binding before the loop runs. Versions do
|
||||
* not nest: a loop inside either copy of a version (including the
|
||||
* versioned loop itself, emitted again for each copy) keeps its calls as
|
||||
* they are, so a loop nest is emitted at most twice. */
|
||||
function callbackVersionBindings(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "for" | "while" | "doWhile" },
|
||||
): { ref: IrExpr & { kind: "varRef" }; direct: string }[] | null {
|
||||
if (
|
||||
!emitter.callbackVersionsEnabled ||
|
||||
emitter.currentConstantCallbacks.size === 0 ||
|
||||
emitter.callbackVersioning.size > 0
|
||||
)
|
||||
return null;
|
||||
const found = new Map<string, { ref: IrExpr & { kind: "varRef" }; direct: string }>();
|
||||
// Bindings the loop declares or writes (a callback declared in its body).
|
||||
const bound = new Set<string>();
|
||||
let cost = 0;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++cost > MAX_CALLBACK_VERSION_COST) return false;
|
||||
if (e.kind === "assignExpr") bound.add(e.localId);
|
||||
if (e.kind === "callValue" && e.callee.kind === "varRef" && !found.has(e.callee.localId)) {
|
||||
const local = emitter.currentLocals.get(e.callee.localId);
|
||||
const target =
|
||||
local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined;
|
||||
if (target) found.set(e.callee.localId, { ref: e.callee, direct: target.direct });
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (x: IrStmt): boolean => {
|
||||
if (++cost > MAX_CALLBACK_VERSION_COST) return false;
|
||||
if (x.kind === "varDecl" || x.kind === "assign" || x.kind === "forOf") bound.add(x.localId);
|
||||
else if (x.kind === "tryCatch" && x.catchLocalId !== null) bound.add(x.catchLocalId);
|
||||
return everyStmtChild(x, expr, stmt);
|
||||
};
|
||||
if (!stmt(s)) return null;
|
||||
const bindings = [...found.values()].filter(({ ref }) => !bound.has(ref.localId));
|
||||
return bindings.length > 0 ? bindings : null;
|
||||
}
|
||||
|
||||
/** A callback loop version. The guard tests, once before the loop, that
|
||||
* each binding's closure holds its known function: a closure's code
|
||||
* pointer is written only when the closure is built, and the census
|
||||
* resolves only bindings the function never rebinds. The copy where the
|
||||
* guard holds calls those functions directly, with no indirect fallback
|
||||
* that could rewrite the closure or what it captures, so LLVM can keep a
|
||||
* captured scalar the callback updates in a register. The other copy is the
|
||||
* ordinary loop. */
|
||||
function emitCallbackVersionedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "for" | "while" | "doWhile" },
|
||||
bindings: { ref: IrExpr & { kind: "varRef" }; direct: string }[],
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
let ok = "true";
|
||||
for (const { ref, direct } of bindings) {
|
||||
// A borrowed read: the guard only compares the code pointer.
|
||||
const closure = emitter.emitReadReceiver(ref).name;
|
||||
const fnp = B.tmp(),
|
||||
fn = B.tmp(),
|
||||
same = B.tmp();
|
||||
B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr ${closure}, i64 0, i32 1`);
|
||||
B.line(`${fn} = load ptr, ptr ${fnp}`);
|
||||
B.line(`${same} = icmp eq ptr ${fn}, ${direct}`);
|
||||
if (ok === "true") ok = same;
|
||||
else {
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${ok}, ${same}`);
|
||||
ok = both;
|
||||
}
|
||||
}
|
||||
const fast = B.newLabel("callback.version.fast"),
|
||||
slow = B.newLabel("callback.version.slow"),
|
||||
done = B.newLabel("callback.version.done");
|
||||
B.condBr(ok, fast, slow);
|
||||
emitter.callbackVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
for (const { ref } of bindings) emitter.knownCallbacks.delete(ref.localId);
|
||||
emitter.callbackVersioning.delete(s);
|
||||
}
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
/** Entries in a row whose fast copy bailed before a speculative loop site
|
||||
* stops trying it. */
|
||||
const SPECULATION_BAILS = 8;
|
||||
|
||||
/** Emit a loop's condition, body and update through `emitCopy`, twice
|
||||
* when speculative-integers.ts plans a fast integer copy. Both copies
|
||||
* share the loop's setup and exit; the ordinary copy comes first so the
|
||||
@@ -1944,23 +2095,66 @@ export function emitLoopCopies(
|
||||
ok = both;
|
||||
}
|
||||
}
|
||||
// Each entry whose fast copy bails counts at the loop site, and a fast
|
||||
// copy that completes a speculated write and leaves the loop clears the
|
||||
// count. After SPECULATION_BAILS bailing entries in a row, the site runs
|
||||
// the ordinary copy only. The count is a hint: both copies give the same
|
||||
// results, and a race between workers loses at most an update.
|
||||
const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`;
|
||||
emitter.declare(`${bails} = internal global i8 0`);
|
||||
const count = B.tmp(),
|
||||
trying = B.tmp();
|
||||
B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`);
|
||||
B.line(`${trying} = icmp ult i8 ${count}, ${SPECULATION_BAILS}`);
|
||||
if (ok === "true") ok = trying;
|
||||
else {
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${ok}, ${trying}`);
|
||||
ok = both;
|
||||
}
|
||||
const fast = B.newLabel("spec.fast"),
|
||||
slow = B.newLabel("spec.slow");
|
||||
B.condBr(ok, fast, slow);
|
||||
const resume = new Map<IrStmt, { label: string; placed: boolean }>();
|
||||
for (const s of plan.writes) resume.set(s, { label: B.newLabel("spec.resume"), placed: false });
|
||||
// A failed check finishes its iteration in a third copy, entered only
|
||||
// there: it re-executes the statement and the rest of the body and the
|
||||
// update, then enters the ordinary copy through its header. Jumping
|
||||
// into the middle of the ordinary copy instead would give that loop a
|
||||
// second entry, and LLVM does not optimize an irreducible loop.
|
||||
const points = [...resume.values()];
|
||||
let header: string | null = null;
|
||||
B.startBlock(slow);
|
||||
emitter.speculationHeader = (label) => {
|
||||
header = label;
|
||||
};
|
||||
try {
|
||||
emitCopy(exit);
|
||||
} finally {
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
if (header === null)
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header");
|
||||
B.startBlock(B.newLabel("spec.tail"));
|
||||
emitter.speculationResume = resume;
|
||||
emitter.speculationHeader = () => {
|
||||
B.br(header!);
|
||||
B.startBlock(B.newLabel("spec.unused"));
|
||||
};
|
||||
try {
|
||||
emitCopy(exit);
|
||||
} finally {
|
||||
emitter.speculationResume = null;
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
B.startBlock(fast);
|
||||
if (![...resume.values()].every((r) => r.placed)) {
|
||||
if (!points.every((r) => r.placed)) {
|
||||
B.br(slow);
|
||||
return;
|
||||
}
|
||||
const ran = B.slot();
|
||||
B.entryAllocas.push(`${ran} = alloca i1 ; speculated write completed`);
|
||||
B.line(`store i1 false, ptr ${ran}`);
|
||||
for (const id of plan.locals.keys()) {
|
||||
const shadow = B.slot();
|
||||
B.entryAllocas.push(
|
||||
@@ -1997,22 +2191,43 @@ export function emitLoopCopies(
|
||||
writes: plan.writes,
|
||||
resume: new Map([...resume].map(([s, r]) => [s, r.label])),
|
||||
deferred,
|
||||
bails,
|
||||
ran,
|
||||
};
|
||||
try {
|
||||
if (deferred.size === 0) emitCopy(exit);
|
||||
else {
|
||||
const leave = B.newLabel("spec.leave");
|
||||
emitCopy(leave);
|
||||
B.startBlock(leave);
|
||||
emitter.writeBackDeferred(deferred);
|
||||
B.br(exit);
|
||||
}
|
||||
const leave = B.newLabel("spec.leave");
|
||||
emitCopy(leave);
|
||||
B.startBlock(leave);
|
||||
emitter.writeBackDeferred(deferred);
|
||||
// An entry that never ran a speculated write (a loop of no iterations)
|
||||
// says nothing about the site.
|
||||
const wrote = B.tmp(),
|
||||
left = B.tmp(),
|
||||
counted = B.tmp(),
|
||||
both = B.tmp(),
|
||||
clear = B.newLabel("spec.clear");
|
||||
B.line(`${wrote} = load i1, ptr ${ran}`);
|
||||
B.line(`${left} = load atomic i8, ptr ${bails} unordered, align 1`);
|
||||
B.line(`${counted} = icmp ne i8 ${left}, 0`);
|
||||
B.line(`${both} = and i1 ${wrote}, ${counted}`);
|
||||
B.condBr(both, clear, exit);
|
||||
B.startBlock(clear);
|
||||
B.line(`store atomic i8 0, ptr ${bails} unordered, align 1`);
|
||||
B.br(exit);
|
||||
} finally {
|
||||
emitter.speculation = null;
|
||||
for (const id of plan.locals.keys()) emitter.integerLoopBindings.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
function enterLoopHeader(emitter: LlEmitter, header: string): void {
|
||||
const dispatch = emitter.speculationHeader;
|
||||
emitter.speculationHeader = null;
|
||||
dispatch?.(header);
|
||||
}
|
||||
|
||||
/** A proven integer loop (speculative-integers.ts) for this loop, or null.
|
||||
* A shadowed local's double is stale until the loop is left, which only
|
||||
* the loop's exit, a return and an exception do: the loop is planned only
|
||||
@@ -2139,7 +2354,8 @@ export function emitSpeculativeWrite(
|
||||
if (emitter.frames.pop()!.length > 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary");
|
||||
B.line(`store i64 ${result}, ptr ${binding.slot}`);
|
||||
const deferred = emitter.speculation!.deferred;
|
||||
const { deferred, bails: site, ran } = emitter.speculation!;
|
||||
if (ran !== undefined) B.line(`store i1 true, ptr ${ran}`);
|
||||
if (!deferred.has(target.localId)) {
|
||||
const number = B.tmp();
|
||||
B.line(`${number} = sitofp i64 ${result} to double`);
|
||||
@@ -2150,6 +2366,14 @@ export function emitSpeculativeWrite(
|
||||
B.br(done);
|
||||
B.startBlock(failed);
|
||||
emitter.writeBackDeferred(deferred);
|
||||
if (site !== undefined) {
|
||||
const count = B.tmp(),
|
||||
next = B.tmp();
|
||||
emitter.declare("declare i8 @llvm.uadd.sat.i8(i8, i8)");
|
||||
B.line(`${count} = load atomic i8, ptr ${site} unordered, align 1`);
|
||||
B.line(`${next} = call i8 @llvm.uadd.sat.i8(i8 ${count}, i8 1)`);
|
||||
B.line(`store atomic i8 ${next}, ptr ${site} unordered, align 1`);
|
||||
}
|
||||
B.br(resume!);
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
@@ -834,6 +834,12 @@ export class LlEmitter {
|
||||
/** The current function's running maximum and minimum updates, whose
|
||||
* branches are weighted as rarely taken (running-extrema.ts). */
|
||||
runningExtrema: ReadonlySet<IrStmt> = new Set();
|
||||
/** Callback bindings whose closures an enclosing callback loop version
|
||||
* proved to hold their known function: calls take the direct entry with
|
||||
* no fallback (emitCallbackVersionedLoop). */
|
||||
knownCallbacks = new Set<string>();
|
||||
readonly callbackVersioning = new Set<IrStmt>();
|
||||
callbackVersionsEnabled = false;
|
||||
readonly stackCallbacks: StackCallbacks;
|
||||
readonly receiverReaders: AmbientReceiverReaders;
|
||||
readonly stackCaptures: StackCaptures;
|
||||
@@ -1105,9 +1111,20 @@ export class LlEmitter {
|
||||
resume: Map<IrStmt, string>;
|
||||
/** Globals whose double is written back only when the copy is left. */
|
||||
deferred: Set<string>;
|
||||
/** The loop site's count of bailing entries in a row (a global), and
|
||||
* whether this entry completed a speculated write (a slot). A proven
|
||||
* integer loop has neither. */
|
||||
bails?: string;
|
||||
ran?: string;
|
||||
} | null = null;
|
||||
/** Speculative loop sites numbered so far in the module. */
|
||||
speculativeLoopSites = 0;
|
||||
/** The ordinary copy being emitted: resume labels still to place. */
|
||||
speculationResume: Map<IrStmt, { label: string; placed: boolean }> | null = null;
|
||||
/** Run once at the top of the next loop copy's header
|
||||
* (`enterLoopHeader`): records the ordinary copy's header, or sends the
|
||||
* third copy's header there. */
|
||||
speculationHeader: ((header: string) => void) | null = null;
|
||||
/** The function being emitted, for speculative globals. */
|
||||
speculativeFunction = "";
|
||||
/** Every function that writes each number global (computed on demand). */
|
||||
@@ -3904,6 +3921,25 @@ export class LlEmitter {
|
||||
return b;
|
||||
}
|
||||
|
||||
/** The function a call through `callee` is known to reach (a literal
|
||||
* closure, or a binding the constant-callback census resolves) and the
|
||||
* entry it calls directly. Async and generator functions stay indirect. */
|
||||
knownCallbackTarget(callee: IrExpr): { known: IrFunction; direct: string } | undefined {
|
||||
const knownName =
|
||||
callee.kind === "closure"
|
||||
? callee.fnName
|
||||
: callee.kind === "varRef"
|
||||
? this.currentConstantCallbacks.get(callee.localId)
|
||||
: undefined;
|
||||
const known = knownName ? this.fnByName.get(knownName) : undefined;
|
||||
if (!known || known.async || known.generator) return undefined;
|
||||
if (known.captures === undefined) {
|
||||
this.fnValues.add(known.name);
|
||||
return { known, direct: `@${mangleWrapper(known.name)}` };
|
||||
}
|
||||
return { known, direct: `@${this.callTarget(known.name)}` };
|
||||
}
|
||||
|
||||
/** The payload slot of a capture box (ScrBox.slot, field 5). */
|
||||
boxSlot(box: string): string {
|
||||
const slot = this.B.tmp();
|
||||
@@ -3917,12 +3953,18 @@ export class LlEmitter {
|
||||
const r = B.tmp();
|
||||
// Scalar payloads are read in place (scr_box_get_f64/bool are a
|
||||
// memcpy and a nonzero test of the same slot), so LLVM can keep a
|
||||
// captured number in a register across a loop of calls.
|
||||
// captured number in a register across a loop of calls. They have an
|
||||
// alias category of their own: every other access to a box's payload
|
||||
// is untagged or a runtime call.
|
||||
if (acc === "f64") {
|
||||
B.line(`${r} = load double, ptr ${this.boxSlot(box)}`);
|
||||
B.line(
|
||||
`${r} = load double, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
|
||||
);
|
||||
} else if (acc === "bool") {
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = load i64, ptr ${this.boxSlot(box)}`);
|
||||
B.line(
|
||||
`${raw} = load i64, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
|
||||
);
|
||||
B.line(`${r} = icmp ne i64 ${raw}, 0`);
|
||||
} else {
|
||||
this.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
@@ -3936,11 +3978,15 @@ export class LlEmitter {
|
||||
const B = this.B;
|
||||
const acc = boxAccess(t);
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${this.boxSlot(box)}`);
|
||||
B.line(
|
||||
`store double ${value}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
|
||||
);
|
||||
} else if (acc === "bool") {
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = zext i1 ${value} to i64`);
|
||||
B.line(`store i64 ${raw}, ptr ${this.boxSlot(box)}`);
|
||||
B.line(
|
||||
`store i64 ${raw}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
|
||||
);
|
||||
} else {
|
||||
this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
|
||||
B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${value})`);
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
mangleClassStruct,
|
||||
mangleFnClosure,
|
||||
mangleFunction,
|
||||
mangleWrapper,
|
||||
mangleVtStruct,
|
||||
mangleAsyncDirectSpawn,
|
||||
} from "../mangle.js";
|
||||
@@ -839,21 +838,9 @@ export function emitCallExpr(
|
||||
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
};
|
||||
const knownName =
|
||||
e.callee.kind === "closure"
|
||||
? e.callee.fnName
|
||||
: e.callee.kind === "varRef"
|
||||
? host.currentConstantCallbacks.get(e.callee.localId)
|
||||
: undefined;
|
||||
const candidate = knownName ? host.fnByName.get(knownName) : undefined;
|
||||
const known = candidate && !candidate.async && !candidate.generator ? candidate : undefined;
|
||||
let direct: string | undefined;
|
||||
if (known) {
|
||||
if (known.captures === undefined) {
|
||||
host.fnValues.add(known.name);
|
||||
direct = `@${mangleWrapper(known.name)}`;
|
||||
} else direct = `@${host.callTarget(known.name)}`;
|
||||
}
|
||||
const target = host.knownCallbackTarget(e.callee);
|
||||
const known = target?.known;
|
||||
const direct = target?.direct;
|
||||
// A known closure body that borrows reference parameters is entered
|
||||
// directly with lent arguments, as a direct call to a borrowing body
|
||||
// is: an argument is read without a reference when neither a later
|
||||
@@ -939,7 +926,13 @@ export function emitCallExpr(
|
||||
let pending = host.indirectMayThrow;
|
||||
let pendingGuard: string | undefined;
|
||||
let t: string;
|
||||
if (direct && e.callee.kind === "closure") {
|
||||
// A literal closure, or a binding an enclosing callback loop version
|
||||
// proved to hold its known function, enters it directly.
|
||||
if (
|
||||
direct &&
|
||||
(e.callee.kind === "closure" ||
|
||||
(e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId)))
|
||||
) {
|
||||
t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!));
|
||||
if (quiet) pending = false;
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import {
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
@@ -221,6 +222,19 @@ export function emitDynamicExpr(
|
||||
host.own({ name: prepared, type: { kind: "dyn" } });
|
||||
host.emitPendingCheck();
|
||||
const args = e.args.map((a) => emitBorrowedInput(host, a));
|
||||
// A closure registered as a 'message' listener gets a typed message
|
||||
// entry: ports deliver messages of its parameter's layout without
|
||||
// decoding them to checked-dynamic values first.
|
||||
const listener = messageListenerArg(e);
|
||||
if (listener !== null) {
|
||||
const entry = host.dyn.messageListenerHelper(listener);
|
||||
if (entry !== null) {
|
||||
host.declare(`declare void @scr_dyn_func_set_message(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_dyn_func_set_message(ptr ${args[1]!.name}, ptr @${host.dyn.dynFuncThunkHelper(listener)}, ptr @${entry})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
let argsPtr = "null";
|
||||
if (args.length > 0) {
|
||||
const arr = B.slot();
|
||||
@@ -1323,3 +1337,24 @@ export const INHERITED_OBJECT_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"hasOwnProperty",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
/** EventEmitter methods that register their second argument as a listener. */
|
||||
const LISTENER_METHODS: ReadonlySet<string> = new Set([
|
||||
"on",
|
||||
"once",
|
||||
"addListener",
|
||||
"prependListener",
|
||||
"prependOnceListener",
|
||||
]);
|
||||
|
||||
/** The closure type of `recv.on("message", listener)` and the other
|
||||
* listener registrations with a typed closure for the 'message' event, or
|
||||
* null. */
|
||||
function messageListenerArg(e: ExprOf<"dynInvoke">): (IrType & { kind: "func" }) | null {
|
||||
if (!LISTENER_METHODS.has(e.method) || e.args.length !== 2) return null;
|
||||
const [event, listener] = e.args;
|
||||
if (event?.kind !== "dynFrom" || event.value.kind !== "strLit" || event.value.value !== "message")
|
||||
return null;
|
||||
if (listener?.kind !== "dynFrom" || listener.value.type.kind !== "func") return null;
|
||||
return listener.value.type;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,25 @@ describe("regex literal matcher code", () => {
|
||||
expect(body("a\\d{1,3}")).not.toContain("<16 x i8>");
|
||||
});
|
||||
|
||||
test("fixed leading characters filter 16 start positions at once", () => {
|
||||
// Both alternatives' first characters are tested at every offset and
|
||||
// combined, and the reads reach seven characters past the block.
|
||||
const text = body("agggtaaa|tttaccct", "gi");
|
||||
expect(text.match(/load <16 x i8>/g)!.length).toBeGreaterThanOrEqual(16);
|
||||
expect(text).toMatch(/and i16 /);
|
||||
expect(text).toMatch(/or i16 /);
|
||||
expect(text).toMatch(/add i32 %t\d+, 7\n/);
|
||||
// A class sequence with a fixed count extends the filter.
|
||||
expect(body("[cg]x{3}y")).toMatch(/and i16 /);
|
||||
// An optional first character, a single first character and a
|
||||
// variable-length start keep the first-character search.
|
||||
expect(body("[ab]?cd")).not.toMatch(/and i16 /);
|
||||
expect(body("[ab]*cd")).not.toMatch(/and i16 /);
|
||||
expect(body("xy|zw")).toMatch(/and i16 /);
|
||||
expect(body("[ab]|cd")).not.toMatch(/and i16 /);
|
||||
expect(body("agggtaaa|tttaccct", "gi", false)).not.toContain("<16 x i8>");
|
||||
});
|
||||
|
||||
test("loops no continuation can share push no backtrack entries", () => {
|
||||
// \d+ then a dot; [^\]]+ then ]; the run at the end of the pattern.
|
||||
expect(body("\\d+\\.\\d+")).not.toContain("@scr_regex_stack_grow");
|
||||
|
||||
@@ -589,6 +589,90 @@ function firstOf(node: RxNode): FirstInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/** Positions the start filter tests and alternatives it keeps. */
|
||||
const PREFIX_LENGTH = 8;
|
||||
const PREFIX_ALTERNATIVES = 8;
|
||||
|
||||
/** The character sets a match's first characters must belong to: every
|
||||
* match of the node begins with the characters of some entry of `alts`,
|
||||
* one set per character. `complete` means every match is exactly as long as
|
||||
* its entry (so a following node continues the sequences). Null when the
|
||||
* node gives no usable condition (an alternative that can start with
|
||||
* anything, or too many alternatives). Assertions add no characters: the
|
||||
* result stays a necessary condition. */
|
||||
interface Prefix {
|
||||
alts: CharSet[][];
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
/** Each head followed by each tail, cut to PREFIX_LENGTH sets; null past
|
||||
* PREFIX_ALTERNATIVES entries. */
|
||||
function crossPrefixes(heads: CharSet[][], tails: CharSet[][]): CharSet[][] | null {
|
||||
if (heads.length * tails.length > PREFIX_ALTERNATIVES) return null;
|
||||
const out: CharSet[][] = [];
|
||||
for (const head of heads)
|
||||
for (const tail of tails) out.push([...head, ...tail].slice(0, PREFIX_LENGTH));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether no entry was cut at PREFIX_LENGTH. */
|
||||
function uncut(alts: CharSet[][]): boolean {
|
||||
return alts.every((alt) => alt.length < PREFIX_LENGTH);
|
||||
}
|
||||
|
||||
function prefixOf(node: RxNode): Prefix | null {
|
||||
switch (node.kind) {
|
||||
case "set":
|
||||
return { alts: [[node.set]], complete: true };
|
||||
case "assert":
|
||||
return { alts: [[]], complete: true };
|
||||
case "cap":
|
||||
return prefixOf(node.body);
|
||||
case "alt": {
|
||||
const alts: CharSet[][] = [];
|
||||
let complete = true;
|
||||
for (const alt of node.alts) {
|
||||
const p = prefixOf(alt);
|
||||
if (p === null) return null;
|
||||
alts.push(...p.alts);
|
||||
complete &&= p.complete;
|
||||
}
|
||||
return alts.length <= PREFIX_ALTERNATIVES ? { alts, complete } : null;
|
||||
}
|
||||
case "seq": {
|
||||
let alts: CharSet[][] = [[]];
|
||||
for (const item of node.items) {
|
||||
const p = prefixOf(item);
|
||||
if (p === null) return { alts, complete: false };
|
||||
const joined = crossPrefixes(alts, p.alts);
|
||||
if (joined === null) return { alts, complete: false };
|
||||
alts = joined;
|
||||
if (!p.complete || !uncut(alts)) return { alts, complete: false };
|
||||
}
|
||||
return { alts, complete: true };
|
||||
}
|
||||
case "rep": {
|
||||
if (node.max === 0) return { alts: [[]], complete: true };
|
||||
if (node.min === 0) return { alts: [[]], complete: false };
|
||||
const body = prefixOf(node.body);
|
||||
if (body === null) return null;
|
||||
// The first `min` iterations are required; they extend the sequences
|
||||
// while the body has a fixed length.
|
||||
let alts = body.alts;
|
||||
let covered = 1;
|
||||
while (covered < node.min && body.complete && uncut(alts)) {
|
||||
const joined = crossPrefixes(alts, body.alts);
|
||||
if (joined === null) break;
|
||||
alts = joined;
|
||||
covered++;
|
||||
}
|
||||
const complete =
|
||||
body.complete && covered === node.min && node.min === node.max && uncut(alts);
|
||||
return { alts, complete };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a node can match without consuming (assertions included). */
|
||||
function canMatchEmpty(node: RxNode): boolean {
|
||||
switch (node.kind) {
|
||||
@@ -721,16 +805,74 @@ class RxGen {
|
||||
return wide;
|
||||
}
|
||||
|
||||
/** The vector plans of a start filter (prefixOf): per alternative, the
|
||||
* plans of its first sets up to the first set without one. Null unless
|
||||
* every alternative keeps at least two positions, where the filter is
|
||||
* more selective than the first character alone. */
|
||||
prefixPlans(prefix: Prefix | null): { ranges: number[][]; negate: boolean }[][] | null {
|
||||
if (prefix === null || !this.vector) return null;
|
||||
const plans: { ranges: number[][]; negate: boolean }[][] = [];
|
||||
for (const alt of prefix.alts) {
|
||||
const row: { ranges: number[][]; negate: boolean }[] = [];
|
||||
for (const set of alt) {
|
||||
const plan = this.vectorPlan(set);
|
||||
if (plan === null) break;
|
||||
row.push(plan);
|
||||
}
|
||||
if (row.length < 2) return null;
|
||||
plans.push(row);
|
||||
}
|
||||
return plans.length === 0 ? null : plans;
|
||||
}
|
||||
|
||||
/** Start positions s[index..index+16) where some alternative of the
|
||||
* filter matches its first sets: an i16 mask. */
|
||||
testPrefix(plans: { ranges: number[][]; negate: boolean }[][], index: string): string {
|
||||
let any = "";
|
||||
for (const row of plans) {
|
||||
let all = "";
|
||||
for (let k = 0; k < row.length; k++) {
|
||||
let at = index;
|
||||
if (k > 0) {
|
||||
at = this.tmp();
|
||||
this.line(`${at} = add i32 ${index}, ${k}`);
|
||||
}
|
||||
const bits = this.testVector(row[k]!, at);
|
||||
if (all === "") all = bits;
|
||||
else {
|
||||
const both = this.tmp();
|
||||
this.line(`${both} = and i16 ${all}, ${bits}`);
|
||||
all = both;
|
||||
}
|
||||
}
|
||||
if (any === "") any = all;
|
||||
else {
|
||||
const either = this.tmp();
|
||||
this.line(`${either} = or i16 ${any}, ${all}`);
|
||||
any = either;
|
||||
}
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
/** The first-character search: from %at, store the next position whose
|
||||
* character is in `set` and branch to %attempt, or branch to %nomatch. */
|
||||
emitSeek(set: CharSet): void {
|
||||
* character is in `set` and branch to %attempt, or branch to %nomatch.
|
||||
* With a start filter (`prefix`), the vector loop tests the filter's
|
||||
* first sets of each alternative at 16 start positions at once and stops
|
||||
* only where one of them matches all of its sets; a match can start
|
||||
* nowhere else, so the attempts that remain are the ones that can
|
||||
* succeed, in the same order. */
|
||||
emitSeek(set: CharSet, prefix: Prefix | null = null): void {
|
||||
this.block("seek");
|
||||
const a = this.tmp();
|
||||
this.line(`${a} = load i32, ptr %at`);
|
||||
const filter = this.prefixPlans(prefix);
|
||||
const plan = this.vectorPlan(set);
|
||||
// The filter reads s[i + reach + 15] at its farthest.
|
||||
const reach = filter === null ? 0 : Math.max(...filter.map((row) => row.length)) - 1;
|
||||
let from = "seek",
|
||||
start = a;
|
||||
if (plan !== null) {
|
||||
if (plan !== null || filter !== null) {
|
||||
const head = this.label("vsh"),
|
||||
load = this.label("vsl"),
|
||||
next = this.label("vsn"),
|
||||
@@ -740,17 +882,21 @@ class RxGen {
|
||||
this.block(head);
|
||||
const i = this.tmp(),
|
||||
i16 = this.tmp(),
|
||||
rest = this.tmp(),
|
||||
fits = this.tmp();
|
||||
this.line(`${i} = phi i32 [ ${a}, %seek ], [ ${i16}, %${next} ]`);
|
||||
this.line(`${i16} = add i32 ${i}, 16`);
|
||||
this.line(`${fits} = icmp sle i32 ${i16}, %len`);
|
||||
// The characters left (0 <= i <= len, so this cannot wrap; i + 16
|
||||
// can for subjects near 2^31 characters).
|
||||
this.line(`${rest} = sub i32 %len, ${i}`);
|
||||
this.line(`${fits} = icmp sge i32 ${rest}, ${16 + reach}`);
|
||||
this.line(`br i1 ${fits}, label %${load}, label %${tail}`);
|
||||
this.block(load);
|
||||
const mask = this.testVector(plan, i),
|
||||
const mask = filter !== null ? this.testPrefix(filter, i) : this.testVector(plan!, i),
|
||||
any = this.tmp();
|
||||
this.line(`${any} = icmp ne i16 ${mask}, 0`);
|
||||
this.line(`br i1 ${any}, label %${hit}, label %${next}`);
|
||||
this.block(next);
|
||||
this.line(`${i16} = add i32 ${i}, 16`);
|
||||
this.line(`br label %${head}`);
|
||||
this.block(hit);
|
||||
const k = this.lowestBit(mask),
|
||||
@@ -1001,10 +1147,10 @@ class RxGen {
|
||||
check = this.label("chk");
|
||||
this.block(entry);
|
||||
const p = this.loadPos(),
|
||||
end = this.tmp(),
|
||||
rest = this.tmp(),
|
||||
inb = this.tmp();
|
||||
this.line(`${end} = add i32 ${p}, ${sets.length}`);
|
||||
this.line(`${inb} = icmp sle i32 ${end}, %len`);
|
||||
this.line(`${rest} = sub i32 %len, ${p}`);
|
||||
this.line(`${inb} = icmp sge i32 ${rest}, ${sets.length}`);
|
||||
this.line(`br i1 ${inb}, label %${check}, label %fail`);
|
||||
this.block(check);
|
||||
let acc = "";
|
||||
@@ -1023,6 +1169,8 @@ class RxGen {
|
||||
acc = both;
|
||||
}
|
||||
}
|
||||
const end = this.tmp();
|
||||
this.line(`${end} = add i32 ${p}, ${sets.length}`);
|
||||
// Storing before the branch is safe: every resume sets the position.
|
||||
this.storePos(end);
|
||||
this.line(`br i1 ${acc}, label %${succ}, label %fail`);
|
||||
@@ -1229,10 +1377,11 @@ class RxGen {
|
||||
this.block(vhead);
|
||||
const kv = this.tmp(),
|
||||
kv16 = this.tmp(),
|
||||
left = this.tmp(),
|
||||
fits = this.tmp();
|
||||
this.line(`${kv} = phi i32 [ 0, %${entry} ], [ ${kv16}, %${vnext} ]`);
|
||||
this.line(`${kv16} = add i32 ${kv}, 16`);
|
||||
this.line(`${fits} = icmp sle i32 ${kv16}, ${lim}`);
|
||||
this.line(`${left} = sub i32 ${lim}, ${kv}`);
|
||||
this.line(`${fits} = icmp sge i32 ${left}, 16`);
|
||||
this.line(`br i1 ${fits}, label %${vload}, label %${tail}`);
|
||||
this.block(vload);
|
||||
const vat = this.tmp();
|
||||
@@ -1242,6 +1391,7 @@ class RxGen {
|
||||
this.line(`${all} = icmp eq i16 ${mask}, -1`);
|
||||
this.line(`br i1 ${all}, label %${vnext}, label %${vexit}`);
|
||||
this.block(vnext);
|
||||
this.line(`${kv16} = add i32 ${kv}, 16`);
|
||||
this.line(`br label %${vhead}`);
|
||||
this.block(vexit);
|
||||
const missing = this.tmp();
|
||||
@@ -1769,7 +1919,10 @@ export function compileRegexMatcher(
|
||||
fn.push(` store i32 %off.n, ptr %at`);
|
||||
fn.push(` br label %attempt`);
|
||||
} else {
|
||||
gen.emitSeek(first.set);
|
||||
// A start filter over the first few characters (a single first byte
|
||||
// keeps memchr).
|
||||
const prefix = prefixOf(root);
|
||||
gen.emitSeek(first.set, gen.prefixPlans(prefix) === null ? null : prefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 26 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v26" as const;
|
||||
export const RUNTIME_ABI_VERSION = 27 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { classInstanceOf } from "./class-dynamic-dispatch.js";
|
||||
import type { DispatchSites } from "./dispatch-sites.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
@@ -26,25 +27,16 @@ export class CaughtValueDispatch {
|
||||
private readonly payloadHelpers = new Map<string, IrFunction>();
|
||||
private readonly payloadHelperFns = new Set<IrFunction>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean {
|
||||
const consumers: IrFunction[] = [];
|
||||
for (const fn of functions) {
|
||||
if (fn === this.helper) continue;
|
||||
let consumes = false;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (stmt) => {
|
||||
if (stmt.kind === "throw" && stmt.value.type.kind === "object") {
|
||||
const info = lowerer.classes.get(stmt.value.type.className);
|
||||
if (info) this.thrownClasses.add(info.def.name);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
if (expr.kind === "caughtToDyn") consumes = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (consumes) consumers.push(fn);
|
||||
const found = sites.of(fn);
|
||||
for (const className of found.thrown) {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (info) this.thrownClasses.add(info.def.name);
|
||||
}
|
||||
if (found.caughtValues) consumers.push(fn);
|
||||
}
|
||||
if (!this.helper && consumers.length === 0) return false;
|
||||
let changed = false;
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js";
|
||||
import { transformExpr, transformStmtList } from "../../ir/traverse.js";
|
||||
import type { DispatchSites } from "./dispatch-sites.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ParamShape } from "./call-signatures.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
@@ -254,22 +255,17 @@ export class ClassConstructionDispatch {
|
||||
private prototypeHelper: IrFunction | null = null;
|
||||
private readonly prototypes = new Set<string>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean {
|
||||
const discover = (type: IrType): void => {
|
||||
if (type.kind === "classval") this.constructors.add(type.className);
|
||||
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
|
||||
};
|
||||
const prototypeReads = new Set<IrFunction>();
|
||||
for (const fn of functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => {
|
||||
if (expr.kind === "dynFrom") discover(expr.value.type);
|
||||
if (expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype")
|
||||
prototypeReads.add(fn);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
for (const fn of functions) {
|
||||
const found = sites.of(fn);
|
||||
found.values.forEach(discover);
|
||||
if (found.basePrototypes) prototypeReads.add(fn);
|
||||
}
|
||||
let changed = false;
|
||||
for (const fn of functions) {
|
||||
if (fn === this.prototypeHelper || !prototypeReads.has(fn)) continue;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { primitiveConstructorOr } from "./lower-exprs.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
|
||||
@@ -77,6 +78,7 @@ import { isClassCallback } from "./class-callbacks.js";
|
||||
import { CaughtValueDispatch } from "./caught-value-dispatch.js";
|
||||
import { refreshDescriptorGuards } from "./class-descriptors.js";
|
||||
import { ClassConstructionDispatch } from "./class-construction.js";
|
||||
import { DispatchSites } from "./dispatch-sites.js";
|
||||
import {
|
||||
emitterRooted,
|
||||
lowerDynamicEmitterEmit,
|
||||
@@ -226,7 +228,7 @@ const KEY_CALLS = new Map<string, "object" | "any" | "value">([
|
||||
["dyn.keySetComputed", "value"],
|
||||
]);
|
||||
|
||||
function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null {
|
||||
export function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null {
|
||||
if (expr.kind === "toString") return expr.operand.type.kind === "dyn" ? "string" : null;
|
||||
if (expr.kind === "dynKeyGet") return expr.key.type.kind === "dyn" ? "string" : null;
|
||||
if (expr.kind !== "libCall") return null;
|
||||
@@ -467,6 +469,79 @@ function sortAdded(lowerer: Lowerer, set: Set<string>, from: number): void {
|
||||
for (const name of added) set.add(name);
|
||||
}
|
||||
|
||||
/** The receivers of one property-receiver list (propertyReceivers) by the
|
||||
* names that can make them answer a member, as positions in the list. A
|
||||
* receiver whose root answers names of its own (an emitter, a stream, an
|
||||
* EventTarget or a Date) is in `rooted` instead. */
|
||||
interface ReceiverIndex {
|
||||
plans: readonly PropertyReceiver[];
|
||||
/** By a method or generic method its base chain declares. */
|
||||
methods: Map<string, number[]>;
|
||||
/** By a method, generic method or accessor its base chain declares (an
|
||||
* accessor under its key and under its property name), or a field it has. */
|
||||
members: Map<string, number[]>;
|
||||
rooted: number[];
|
||||
}
|
||||
|
||||
function receiverIndex(lowerer: Lowerer, plans: readonly PropertyReceiver[]): ReceiverIndex {
|
||||
const methods = new Map<string, number[]>();
|
||||
const members = new Map<string, number[]>();
|
||||
const rooted: number[] = [];
|
||||
const add = (table: Map<string, number[]>, name: string, position: number): void => {
|
||||
const positions = table.get(name);
|
||||
if (positions === undefined) table.set(name, [position]);
|
||||
else if (positions[positions.length - 1] !== position) positions.push(position);
|
||||
};
|
||||
plans.forEach(({ info }, position) => {
|
||||
if (
|
||||
emitterRooted(lowerer, info) ||
|
||||
info.builtinStream !== undefined ||
|
||||
eventTargetApiMembers(info) !== null ||
|
||||
dateRootOf(info) !== null
|
||||
) {
|
||||
rooted.push(position);
|
||||
return;
|
||||
}
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
for (const name of c.methods.keys()) {
|
||||
add(methods, name, position);
|
||||
// A `get:`/`set:` key is an accessor of the property after the
|
||||
// prefix, or a method whose quoted name has that prefix; the test
|
||||
// looks up both.
|
||||
add(members, name, position);
|
||||
if (name.startsWith("get:") || name.startsWith("set:"))
|
||||
add(members, name.slice(4), position);
|
||||
}
|
||||
if (c.genericMethods)
|
||||
for (const name of c.genericMethods.keys()) {
|
||||
add(methods, name, position);
|
||||
add(members, name, position);
|
||||
}
|
||||
}
|
||||
for (const name of info.fields.keys()) add(members, name, position);
|
||||
});
|
||||
for (const table of [methods, members])
|
||||
for (const positions of table.values()) positions.sort((a, b) => a - b);
|
||||
return { plans, methods, members, rooted };
|
||||
}
|
||||
|
||||
/** The ascending union of two ascending position lists. */
|
||||
function mergePositions(left: readonly number[], right: readonly number[]): number[] {
|
||||
const out: number[] = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < left.length || j < right.length) {
|
||||
const next =
|
||||
j >= right.length || (i < left.length && left[i]! <= right[j]!) ? left[i++]! : right[j++]!;
|
||||
if (out[out.length - 1] !== next) out.push(next);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** SCRIPTC_CHECK_DISPATCH_CANDIDATES=1 checks every indexed receiver selection
|
||||
* against a test of every receiver. */
|
||||
const CHECK_CANDIDATES = process.env["SCRIPTC_CHECK_DISPATCH_CANDIDATES"] === "1";
|
||||
|
||||
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction {
|
||||
const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties";
|
||||
lowerer.shareHelper(name);
|
||||
@@ -780,6 +855,8 @@ export class ClassDynamicDispatch {
|
||||
private readonly generatorDispatch = new GeneratorDynamicDispatch();
|
||||
private readonly caughtDispatch = new CaughtValueDispatch();
|
||||
private readonly constructionDispatch = new ClassConstructionDispatch();
|
||||
/** What each function's body holds for this pass and its sub-passes. */
|
||||
readonly sites = new DispatchSites();
|
||||
/** Property names a delete can remove (null: any name), for the field
|
||||
* creation tracking decision. */
|
||||
private deletedNames: Set<string> | null | undefined;
|
||||
@@ -862,12 +939,13 @@ export class ClassDynamicDispatch {
|
||||
)
|
||||
fn.body.shift();
|
||||
}
|
||||
let changed = this.caughtDispatch.process(lowerer, functions);
|
||||
let changed = this.caughtDispatch.process(lowerer, functions, this.sites);
|
||||
changed = refreshErrorMethodDispatch(lowerer, functions) || changed;
|
||||
changed = refreshClassMethodValueSelections(lowerer) || changed;
|
||||
changed = refreshErrorPropertyDispatch(lowerer, functions) || changed;
|
||||
changed = this.generatorDispatch.process(lowerer, functions, this.generated) || changed;
|
||||
changed = this.constructionDispatch.process(lowerer, functions) || changed;
|
||||
changed =
|
||||
this.generatorDispatch.process(lowerer, functions, this.generated, this.sites) || changed;
|
||||
changed = this.constructionDispatch.process(lowerer, functions, this.sites) || changed;
|
||||
const seenTypes = new Set<string>();
|
||||
const rewrite = new Set<IrFunction>();
|
||||
const toPrimitiveSites = new Set<IrFunction>();
|
||||
@@ -891,100 +969,18 @@ export class ClassDynamicDispatch {
|
||||
};
|
||||
const boxedBefore = this.boxed.size;
|
||||
const boxedConstructorsBefore = this.boxedConstructors.size;
|
||||
for (const fn of functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (statement) => {
|
||||
// Field-creation instrumentation can box a base-constructor receiver
|
||||
// even when only its derived instances otherwise cross the boundary.
|
||||
if (
|
||||
statement.kind === "fieldSet" &&
|
||||
lowerer.classes.get(statement.className)?.def.tracksOwnFields &&
|
||||
isClassOwnEnumerableFieldName(statement.field)
|
||||
)
|
||||
discover(statement.obj.type);
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
if (
|
||||
expr.kind === "jsonStringify" ||
|
||||
(expr.kind === "libCall" &&
|
||||
(expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer"))
|
||||
) {
|
||||
this.serializesClasses = true;
|
||||
}
|
||||
if (
|
||||
toPrimitiveHint(expr) !== null ||
|
||||
(expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString"))
|
||||
)
|
||||
toPrimitiveSites.add(fn);
|
||||
switch (expr.kind) {
|
||||
case "dynFrom":
|
||||
discover(expr.value.type);
|
||||
break;
|
||||
case "fieldIncDec":
|
||||
if (
|
||||
lowerer.classes.get(expr.className)?.def.tracksOwnFields &&
|
||||
isClassOwnEnumerableFieldName(expr.field)
|
||||
)
|
||||
discover(expr.obj.type);
|
||||
break;
|
||||
case "call":
|
||||
if (
|
||||
(expr.callee === "%dyn.class.properties" ||
|
||||
expr.callee === "%dyn.class.readProperties") &&
|
||||
expr.args[0]?.kind === "dynFrom" &&
|
||||
isDynTypedRefType(expr.args[0].value.type)
|
||||
)
|
||||
rewrite.add(fn);
|
||||
break;
|
||||
case "dynHasKey":
|
||||
hasKeySites.add(fn);
|
||||
break;
|
||||
case "caughtTest":
|
||||
if (expr.test === "instanceof") caughtSites.add(fn);
|
||||
break;
|
||||
case "caughtNarrow":
|
||||
case "caughtCheck":
|
||||
if (expr.type.kind === "object") caughtSites.add(fn);
|
||||
break;
|
||||
case "dynKeyGet":
|
||||
case "dynInvoke":
|
||||
rewrite.add(fn);
|
||||
break;
|
||||
case "dynCheck":
|
||||
if (isDynTypedRefType(expr.type)) rewrite.add(fn);
|
||||
// A dynamic function checked into a native function type
|
||||
// receives its arguments as dynamic values: class instances
|
||||
// passed to it cross the boundary when it is called (a JS
|
||||
// `list.map((rec) => rec.match(x))` callback with an untyped
|
||||
// parameter).
|
||||
if (expr.type.kind === "func") expr.type.params.forEach(discover);
|
||||
break;
|
||||
case "libCall":
|
||||
if (
|
||||
expr.fn === "bytes.construct" ||
|
||||
expr.fn === "dyn.keySet" ||
|
||||
expr.fn === "dyn.keySetComputed" ||
|
||||
expr.fn === "dyn.iterator" ||
|
||||
expr.fn === "dyn.arrayFromIterator" ||
|
||||
expr.fn === "dyn.toString" ||
|
||||
expr.fn === "dyn.stringConstructor" ||
|
||||
expr.fn === "dyn.forInKeys" ||
|
||||
expr.fn === "dyn.forInKeyStrings" ||
|
||||
expr.fn === "dyn.hasKeyComputed" ||
|
||||
expr.fn === "dyn.hasOwnComputed" ||
|
||||
expr.fn === "dyn.propertyIsEnumerableComputed" ||
|
||||
expr.fn === "dyn.defineProperty" ||
|
||||
expr.fn === "dyn.getPrototype" ||
|
||||
expr.fn === "dyn.reflectGet" ||
|
||||
expr.fn === "dyn.reflectSet"
|
||||
)
|
||||
rewrite.add(fn);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
for (const fn of functions) {
|
||||
const found = this.sites.of(fn);
|
||||
// A class starts tracking its own fields during this pass.
|
||||
for (const { type, fieldClass } of found.boxing)
|
||||
if (fieldClass === undefined || lowerer.classes.get(fieldClass)?.def.tracksOwnFields)
|
||||
discover(type);
|
||||
if (found.serializes) this.serializesClasses = true;
|
||||
if (found.conversions) toPrimitiveSites.add(fn);
|
||||
if (found.dispatches) rewrite.add(fn);
|
||||
if (found.keyTests) hasKeySites.add(fn);
|
||||
if (found.caughtClassTests) caughtSites.add(fn);
|
||||
}
|
||||
// The classes this pass found join the tables in a fixed order, not in
|
||||
// the order the scan met them.
|
||||
sortAdded(lowerer, this.boxed, boxedBefore);
|
||||
@@ -1342,10 +1338,30 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
refreshDescriptorGuards(lowerer);
|
||||
const byMethod = new Map<string, ClassInfo[]>();
|
||||
// A query asks every receiver about one name, and the program's dispatch
|
||||
// sites ask about thousands: index the receivers by name once per list.
|
||||
let index: ReceiverIndex | null = null;
|
||||
const indexOf = (plans: readonly PropertyReceiver[]): ReceiverIndex =>
|
||||
index?.plans === plans ? index : (index = receiverIndex(lowerer, plans));
|
||||
/** The receivers of `plans` that can answer `name`, in list order: those
|
||||
* `table` lists under it and the rooted ones. Emit-override
|
||||
* specializations (`emit:<event>`) join a class's methods while bodies
|
||||
* lower, so they keep every receiver. */
|
||||
const receiversFor = (
|
||||
plans: readonly PropertyReceiver[],
|
||||
name: string,
|
||||
table: "methods" | "members",
|
||||
): readonly PropertyReceiver[] => {
|
||||
if (name.startsWith("emit:")) return plans;
|
||||
const found = indexOf(plans);
|
||||
const positions = (table === "methods" ? found.methods : found.members).get(name);
|
||||
return mergePositions(positions ?? [], found.rooted).map((position) => plans[position]!);
|
||||
};
|
||||
const candidates = (method: string): ClassInfo[] => {
|
||||
const found = byMethod.get(method);
|
||||
if (found) return found;
|
||||
const matching = this.propertyReceivers(lowerer, true).flatMap(({ info }) => {
|
||||
const plans = this.propertyReceivers(lowerer, true);
|
||||
const test = ({ info }: PropertyReceiver): ClassInfo[] => {
|
||||
if (!info || info.fields.has(method) || info.builtinError) return [];
|
||||
if (emitterRooted(lowerer, info) && EMITTER_METHODS.has(method)) return [info];
|
||||
if (
|
||||
@@ -1364,7 +1380,13 @@ export class ClassDynamicDispatch {
|
||||
return findMethodOn(lowerer, info, method) || findGenericMethodOn(lowerer, info, method)
|
||||
? [info]
|
||||
: [];
|
||||
});
|
||||
};
|
||||
const matching = receiversFor(plans, method, "methods").flatMap(test);
|
||||
if (CHECK_CANDIDATES) {
|
||||
const every = plans.flatMap(test);
|
||||
if (every.length !== matching.length || every.some((info, i) => info !== matching[i]))
|
||||
throw new InternalCompilerError(`indexed candidates for ${method} differ`);
|
||||
}
|
||||
byMethod.set(method, matching);
|
||||
return matching;
|
||||
};
|
||||
@@ -2898,24 +2920,40 @@ export class ClassDynamicDispatch {
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
for (const plan of this.propertyReceivers(lowerer, true)) {
|
||||
// Only the receivers that declare or hold the name, and the rooted
|
||||
// ones, can pass the test, except for names it decides on every
|
||||
// receiver. Fields added while bodies lower (the property bag and the
|
||||
// attribute words) have compiler names, which take every receiver.
|
||||
const plans = this.propertyReceivers(lowerer, true);
|
||||
const anyReceiver =
|
||||
!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
dispatch.name === "constructor" ||
|
||||
dispatch.name === "stack";
|
||||
const skips = (plan: PropertyReceiver): boolean =>
|
||||
!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
(!(plan.info.builtinStream !== undefined && streamProperty(dispatch.name)) &&
|
||||
!eventTargetInheritedMember(plan.info, dispatch.name) &&
|
||||
!dateInheritedMember(lowerer, plan.info, dispatch.name) &&
|
||||
!findMethodOn(lowerer, plan.info, dispatch.name) &&
|
||||
!findGenericMethodOn(lowerer, plan.info, dispatch.name) &&
|
||||
dispatch.name !== "constructor" &&
|
||||
!plan.info.fields.has(dispatch.name) &&
|
||||
!(dispatch.name === "stack" && lowerer.isSubclassOf(plan.info.def.name, "%Error")) &&
|
||||
!findMethodOn(lowerer, plan.info, `get:${dispatch.name}`) &&
|
||||
!findMethodOn(lowerer, plan.info, `set:${dispatch.name}`));
|
||||
const selected = anyReceiver ? plans : receiversFor(plans, dispatch.name, "members");
|
||||
if (
|
||||
CHECK_CANDIDATES &&
|
||||
plans.some(
|
||||
(plan) => !dispatch.classes.has(plan.key) && !skips(plan) && !selected.includes(plan),
|
||||
)
|
||||
)
|
||||
throw new InternalCompilerError(`indexed receivers for ${dispatch.name} differ`);
|
||||
for (const plan of selected) {
|
||||
const { info } = plan;
|
||||
// Classes answered in an earlier pass skip the member lookups.
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
if (
|
||||
!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
(!(info.builtinStream !== undefined && streamProperty(dispatch.name)) &&
|
||||
!eventTargetInheritedMember(info, dispatch.name) &&
|
||||
!dateInheritedMember(lowerer, info, dispatch.name) &&
|
||||
!findMethodOn(lowerer, info, dispatch.name) &&
|
||||
!findGenericMethodOn(lowerer, info, dispatch.name) &&
|
||||
dispatch.name !== "constructor" &&
|
||||
!info.fields.has(dispatch.name) &&
|
||||
!(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) &&
|
||||
!findMethodOn(lowerer, info, `get:${dispatch.name}`) &&
|
||||
!findMethodOn(lowerer, info, `set:${dispatch.name}`))
|
||||
)
|
||||
continue;
|
||||
if (skips(plan)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
|
||||
@@ -159,6 +159,15 @@ test("fills every empty copy with the exact file and span while preserving popul
|
||||
for (const untouched of [otherFile, otherStart, otherEnd]) expect(untouched.body).toEqual([]);
|
||||
});
|
||||
|
||||
test("reports the functions whose placeholders it fills", () => {
|
||||
const { initializers, pending, lowerer } = fixture();
|
||||
const functions = [fn("init", [block(loc)]), fn("use", [read("registry")])];
|
||||
pending.set("registry", [{ loc, lower: () => [value(4)] }]);
|
||||
const filled: IrFunction[] = [];
|
||||
expect(initializers.process(lowerer, functions, (f) => filled.push(f))).toBe(true);
|
||||
expect(filled).toEqual([functions[0]]);
|
||||
});
|
||||
|
||||
test("an update of the binding demands its initializer", () => {
|
||||
const { initializers, pending, lowerer } = fixture();
|
||||
const updated = block(loc),
|
||||
|
||||
@@ -129,7 +129,14 @@ export class DeferredModuleInitializers {
|
||||
return block;
|
||||
}
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
/** Materializes the initializers that reached bodies observe. `filled`
|
||||
* receives each function whose body changes inside an unchanged statement
|
||||
* list. */
|
||||
process(
|
||||
lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
filled?: (fn: IrFunction) => void,
|
||||
): boolean {
|
||||
if (this.pending.size === 0) return false;
|
||||
// Scans keep only the reads of deferred bindings.
|
||||
for (const key of this.pending.keys()) {
|
||||
@@ -192,7 +199,10 @@ export class DeferredModuleInitializers {
|
||||
}
|
||||
if (holders.length === 0) continue;
|
||||
// These bodies changed inside unchanged statement lists.
|
||||
for (const fn of holders) this.scans.delete(fn);
|
||||
for (const fn of holders) {
|
||||
this.scans.delete(fn);
|
||||
filled?.(fn);
|
||||
}
|
||||
// An inserted body can introduce a placeholder needed by another
|
||||
// action in this pass. Unattached bodies are not part of the live IR.
|
||||
for (const nested of scanStatements(body).placeholders) add(nested, holders);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { DYN, F64, VOID, type IrFunction, type IrStmt, type SrcLoc } from "../../ir/ir.js";
|
||||
import * as traversal from "../../ir/traverse.js";
|
||||
import { DispatchSites } from "./dispatch-sites.js";
|
||||
|
||||
const loc: SrcLoc = { file: "sites.ts", start: 0, end: 1 };
|
||||
const keyRead = (key: string): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "dynKeyGet",
|
||||
value: { kind: "varRef", localId: "value", type: DYN, loc },
|
||||
key: { kind: "strLit", value: key, type: DYN, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const value = (n: number): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "numLit", value: n, type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const fn = (body: IrStmt[]): IrFunction => ({
|
||||
name: "f",
|
||||
body,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: VOID,
|
||||
loc,
|
||||
});
|
||||
|
||||
test("reuses a function's sites while its statement list is unchanged", () => {
|
||||
const sites = new DispatchSites();
|
||||
const f = fn([value(1)]);
|
||||
const walk = vi.spyOn(traversal, "everyStmtList");
|
||||
try {
|
||||
expect(sites.of(f).dispatches).toBe(false);
|
||||
expect(sites.of(f).dispatches).toBe(false);
|
||||
expect(walk).toHaveBeenCalledTimes(1);
|
||||
// A statement added to the same list is found.
|
||||
f.body.push(keyRead("next"));
|
||||
expect(sites.of(f)).toMatchObject({ dispatches: true, generatorSteps: true });
|
||||
// So is a replaced list.
|
||||
f.body = [value(2)];
|
||||
expect(sites.of(f)).toMatchObject({ dispatches: false, generatorSteps: false });
|
||||
expect(walk).toHaveBeenCalledTimes(3);
|
||||
} finally {
|
||||
walk.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("scans a body again once a change inside an unchanged list is reported", () => {
|
||||
const sites = new DispatchSites();
|
||||
const placeholder: Extract<IrStmt, { kind: "block" }> = { kind: "block", body: [], loc };
|
||||
const f = fn([placeholder]);
|
||||
expect(sites.of(f).dispatches).toBe(false);
|
||||
placeholder.body.push(keyRead("map"));
|
||||
sites.forget(f);
|
||||
expect(sites.of(f)).toMatchObject({ dispatches: true, runtimeSteps: true });
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDynTypedRefType,
|
||||
typeKey,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
|
||||
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
|
||||
|
||||
/** What one function's body holds for the class dispatch pass and its
|
||||
* caught-value, generator and class construction sub-passes, collected in
|
||||
* one walk. Lists keep the order the walk meets their entries. */
|
||||
export interface FunctionSites {
|
||||
statements: IrStmt[];
|
||||
length: number;
|
||||
first: IrStmt | undefined;
|
||||
last: IrStmt | undefined;
|
||||
/** The value types of `dynFrom` expressions, once per type key. */
|
||||
values: IrType[];
|
||||
/** The types the class dispatch scan boxes, once per type key and class:
|
||||
* `dynFrom` values, the parameter types of dynamic functions checked into
|
||||
* native function types, and the receivers of own enumerable field writes
|
||||
* and updates. A receiver counts only while its class (`fieldClass`)
|
||||
* tracks its own fields. */
|
||||
boxing: { type: IrType; fieldClass?: string }[];
|
||||
/** JSON serialization of dynamic values. */
|
||||
serializes: boolean;
|
||||
/** ToPrimitive conversions, and `dyn.valueOf` and `dyn.toString` calls. */
|
||||
conversions: boolean;
|
||||
/** Sites the class dispatch rewrite can change. */
|
||||
dispatches: boolean;
|
||||
/** `dynHasKey` tests. */
|
||||
keyTests: boolean;
|
||||
/** `instanceof` tests of caught values, and caught values narrowed or
|
||||
* checked to class types. */
|
||||
caughtClassTests: boolean;
|
||||
/** Calls and reads of generator steps, and reads of iterator helper
|
||||
* methods. */
|
||||
generatorSteps: boolean;
|
||||
/** Values the runtime steps: iterator helper calls and reads, `dynIterN`,
|
||||
* and rest destructuring. */
|
||||
runtimeSteps: boolean;
|
||||
/** `dyn.classBasePrototype` calls. */
|
||||
basePrototypes: boolean;
|
||||
/** The classes of thrown class instances, once each. */
|
||||
thrown: string[];
|
||||
/** `caughtToDyn` conversions. */
|
||||
caughtValues: boolean;
|
||||
}
|
||||
|
||||
/** The `libCall`s the class dispatch rewrite can change. */
|
||||
const DISPATCH_LIB_CALLS = new Set<string>([
|
||||
"bytes.construct",
|
||||
"dyn.keySet",
|
||||
"dyn.keySetComputed",
|
||||
"dyn.iterator",
|
||||
"dyn.arrayFromIterator",
|
||||
"dyn.toString",
|
||||
"dyn.stringConstructor",
|
||||
"dyn.forInKeys",
|
||||
"dyn.forInKeyStrings",
|
||||
"dyn.hasKeyComputed",
|
||||
"dyn.hasOwnComputed",
|
||||
"dyn.propertyIsEnumerableComputed",
|
||||
"dyn.defineProperty",
|
||||
"dyn.getPrototype",
|
||||
"dyn.reflectGet",
|
||||
"dyn.reflectSet",
|
||||
]);
|
||||
|
||||
/** Prototype data helpers (classPrototypeData) gain method slots inside
|
||||
* their initialization block, so their summaries are not reused. */
|
||||
const PROTOTYPE_DATA_PREFIX = "%prototype.data.";
|
||||
|
||||
/** SCRIPTC_CHECK_DISPATCH_SCANS=1 checks each reused summary against a fresh walk. */
|
||||
const CHECK_SCANS = process.env["SCRIPTC_CHECK_DISPATCH_SCANS"] === "1";
|
||||
|
||||
/** The function summaries (FunctionSites) of one class dispatch pass, kept
|
||||
* across its runs. */
|
||||
export class DispatchSites {
|
||||
private readonly sites = new Map<IrFunction, FunctionSites>();
|
||||
|
||||
/** The sites of `fn`. A summary is reused while the function's statement
|
||||
* list is the same array with the same length and the same first and last
|
||||
* statements: the dispatch passes replace the lists they rewrite and add
|
||||
* statements to the helpers they extend. Other changes inside an
|
||||
* unchanged list are reported with `forget`, except in prototype data
|
||||
* helpers, which are scanned on every use. */
|
||||
of(fn: IrFunction): FunctionSites {
|
||||
const cached = this.sites.get(fn);
|
||||
if (
|
||||
cached !== undefined &&
|
||||
!fn.name.startsWith(PROTOTYPE_DATA_PREFIX) &&
|
||||
cached.statements === fn.body &&
|
||||
cached.length === fn.body.length &&
|
||||
cached.first === fn.body[0] &&
|
||||
cached.last === fn.body[fn.body.length - 1]
|
||||
) {
|
||||
if (CHECK_SCANS && !sameSites(cached, scanSites(fn.body)))
|
||||
throw new InternalCompilerError(
|
||||
`reused dispatch sites of ${fn.name} disagree with a fresh scan`,
|
||||
);
|
||||
return cached;
|
||||
}
|
||||
const sites = scanSites(fn.body);
|
||||
this.sites.set(fn, sites);
|
||||
return sites;
|
||||
}
|
||||
|
||||
/** Drops the summary of a function whose body changed inside an unchanged
|
||||
* statement list. */
|
||||
forget(fn: IrFunction): void {
|
||||
this.sites.delete(fn);
|
||||
}
|
||||
}
|
||||
|
||||
function scanSites(statements: IrStmt[]): FunctionSites {
|
||||
const sites: FunctionSites = {
|
||||
statements,
|
||||
length: statements.length,
|
||||
first: statements[0],
|
||||
last: statements[statements.length - 1],
|
||||
values: [],
|
||||
boxing: [],
|
||||
serializes: false,
|
||||
conversions: false,
|
||||
dispatches: false,
|
||||
keyTests: false,
|
||||
caughtClassTests: false,
|
||||
generatorSteps: false,
|
||||
runtimeSteps: false,
|
||||
basePrototypes: false,
|
||||
thrown: [],
|
||||
caughtValues: false,
|
||||
};
|
||||
const valueKeys = new Set<string>();
|
||||
const boxingKeys = new Set<string>();
|
||||
const thrown = new Set<string>();
|
||||
const box = (type: IrType, fieldClass?: string): void => {
|
||||
const key = `${fieldClass ?? ""}\0${typeKey(type)}`;
|
||||
if (boxingKeys.has(key)) return;
|
||||
boxingKeys.add(key);
|
||||
sites.boxing.push(fieldClass === undefined ? { type } : { type, fieldClass });
|
||||
};
|
||||
everyStmtList(statements, {
|
||||
stmt: (statement) => {
|
||||
if (statement.kind === "throw" && statement.value.type.kind === "object") {
|
||||
const className = statement.value.type.className;
|
||||
if (!thrown.has(className)) {
|
||||
thrown.add(className);
|
||||
sites.thrown.push(className);
|
||||
}
|
||||
}
|
||||
// Field-creation instrumentation can box a base-constructor receiver
|
||||
// even when only its derived instances otherwise cross the boundary.
|
||||
if (statement.kind === "fieldSet" && isClassOwnEnumerableFieldName(statement.field))
|
||||
box(statement.obj.type, statement.className);
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
if (
|
||||
expr.kind === "jsonStringify" ||
|
||||
(expr.kind === "libCall" &&
|
||||
(expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer"))
|
||||
)
|
||||
sites.serializes = true;
|
||||
if (
|
||||
toPrimitiveHint(expr) !== null ||
|
||||
(expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString"))
|
||||
)
|
||||
sites.conversions = true;
|
||||
if (
|
||||
(expr.kind === "dynInvoke" && GENERATOR_STEP_METHODS.has(expr.method)) ||
|
||||
(expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
(GENERATOR_STEP_METHODS.has(expr.key.value) || HELPER_METHODS.has(expr.key.value)))
|
||||
)
|
||||
sites.generatorSteps = true;
|
||||
if (
|
||||
(expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) ||
|
||||
(expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
HELPER_METHODS.has(expr.key.value)) ||
|
||||
expr.kind === "dynIterN" ||
|
||||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
|
||||
)
|
||||
sites.runtimeSteps = true;
|
||||
switch (expr.kind) {
|
||||
case "dynFrom": {
|
||||
const key = typeKey(expr.value.type);
|
||||
if (!valueKeys.has(key)) {
|
||||
valueKeys.add(key);
|
||||
sites.values.push(expr.value.type);
|
||||
}
|
||||
box(expr.value.type);
|
||||
break;
|
||||
}
|
||||
case "fieldIncDec":
|
||||
if (isClassOwnEnumerableFieldName(expr.field)) box(expr.obj.type, expr.className);
|
||||
break;
|
||||
case "call":
|
||||
if (
|
||||
(expr.callee === "%dyn.class.properties" ||
|
||||
expr.callee === "%dyn.class.readProperties") &&
|
||||
expr.args[0]?.kind === "dynFrom" &&
|
||||
isDynTypedRefType(expr.args[0].value.type)
|
||||
)
|
||||
sites.dispatches = true;
|
||||
break;
|
||||
case "dynHasKey":
|
||||
sites.keyTests = true;
|
||||
break;
|
||||
case "caughtTest":
|
||||
if (expr.test === "instanceof") sites.caughtClassTests = true;
|
||||
break;
|
||||
case "caughtNarrow":
|
||||
case "caughtCheck":
|
||||
if (expr.type.kind === "object") sites.caughtClassTests = true;
|
||||
break;
|
||||
case "caughtToDyn":
|
||||
sites.caughtValues = true;
|
||||
break;
|
||||
case "dynKeyGet":
|
||||
case "dynInvoke":
|
||||
sites.dispatches = true;
|
||||
break;
|
||||
case "dynCheck":
|
||||
if (isDynTypedRefType(expr.type)) sites.dispatches = true;
|
||||
// A dynamic function checked into a native function type
|
||||
// receives its arguments as dynamic values: class instances
|
||||
// passed to it cross the boundary when it is called (a JS
|
||||
// `list.map((rec) => rec.match(x))` callback with an untyped
|
||||
// parameter).
|
||||
if (expr.type.kind === "func") expr.type.params.forEach((type) => box(type));
|
||||
break;
|
||||
case "libCall":
|
||||
if (expr.fn === "dyn.classBasePrototype") sites.basePrototypes = true;
|
||||
if (DISPATCH_LIB_CALLS.has(expr.fn)) sites.dispatches = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return sites;
|
||||
}
|
||||
|
||||
function sameSites(left: FunctionSites, right: FunctionSites): boolean {
|
||||
const sameList = <T>(a: readonly T[], b: readonly T[], same: (x: T, y: T) => boolean): boolean =>
|
||||
a.length === b.length && a.every((item, index) => same(item, b[index]!));
|
||||
return (
|
||||
sameList(left.values, right.values, (a, b) => a === b) &&
|
||||
sameList(
|
||||
left.boxing,
|
||||
right.boxing,
|
||||
(a, b) => a.type === b.type && a.fieldClass === b.fieldClass,
|
||||
) &&
|
||||
sameList(left.thrown, right.thrown, (a, b) => a === b) &&
|
||||
left.serializes === right.serializes &&
|
||||
left.conversions === right.conversions &&
|
||||
left.dispatches === right.dispatches &&
|
||||
left.keyTests === right.keyTests &&
|
||||
left.caughtClassTests === right.caughtClassTests &&
|
||||
left.generatorSteps === right.generatorSteps &&
|
||||
left.runtimeSteps === right.runtimeSteps &&
|
||||
left.basePrototypes === right.basePrototypes &&
|
||||
left.caughtValues === right.caughtValues
|
||||
);
|
||||
}
|
||||
@@ -13,16 +13,20 @@ import {
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { transformStmtList } from "../../ir/traverse.js";
|
||||
import { genResultRecord } from "../type-mapper.js";
|
||||
import type { DispatchSites } from "./dispatch-sites.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
type GeneratorType = Extract<IrType, { kind: "generator" }>;
|
||||
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
|
||||
type KeyGet = Extract<IrExpr, { kind: "dynKeyGet" }>;
|
||||
|
||||
/** The generator methods a dynamic call or read steps. */
|
||||
export const GENERATOR_STEP_METHODS = new Set(["next", "return", "throw"]);
|
||||
|
||||
/** The iterator helper methods the runtime answers on checked iterators. */
|
||||
const HELPER_METHODS = new Set([
|
||||
export const HELPER_METHODS = new Set([
|
||||
"map",
|
||||
"filter",
|
||||
"take",
|
||||
@@ -101,6 +105,7 @@ export class GeneratorDynamicDispatch {
|
||||
lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
classHelpers: ReadonlySet<IrFunction>,
|
||||
sites: DispatchSites,
|
||||
): boolean {
|
||||
const seen = new Set<string>();
|
||||
const rewrite = new Set<IrFunction>();
|
||||
@@ -117,34 +122,14 @@ export class GeneratorDynamicDispatch {
|
||||
lowerer.shapes.get(type.shapeId)?.fields.forEach((field) => discover(field.type));
|
||||
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
|
||||
};
|
||||
for (const fn of functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => {
|
||||
if (expr.kind === "dynFrom") discover(expr.value.type);
|
||||
if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method))
|
||||
rewrite.add(fn);
|
||||
// The runtime steps the values iterator helpers and destructuring
|
||||
// receive.
|
||||
if (
|
||||
(expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) ||
|
||||
(expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
HELPER_METHODS.has(expr.key.value)) ||
|
||||
expr.kind === "dynIterN" ||
|
||||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
|
||||
)
|
||||
runtimeSteps = true;
|
||||
if (
|
||||
expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
(["next", "return", "throw"].includes(expr.key.value) ||
|
||||
HELPER_METHODS.has(expr.key.value))
|
||||
)
|
||||
rewrite.add(fn);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
for (const fn of functions) {
|
||||
const found = sites.of(fn);
|
||||
found.values.forEach(discover);
|
||||
// The runtime steps the values iterator helpers and destructuring
|
||||
// receive.
|
||||
if (found.runtimeSteps) runtimeSteps = true;
|
||||
if (found.generatorSteps) rewrite.add(fn);
|
||||
}
|
||||
let changed = false;
|
||||
for (const fn of functions) {
|
||||
// The generator helper's fallback may itself dispatch native classes.
|
||||
@@ -159,7 +144,7 @@ export class GeneratorDynamicDispatch {
|
||||
if (
|
||||
expr.kind === "dynKeyGet" &&
|
||||
expr.key.kind === "strLit" &&
|
||||
(["next", "return", "throw"].includes(expr.key.value) ||
|
||||
(GENERATOR_STEP_METHODS.has(expr.key.value) ||
|
||||
(HELPER_METHODS.has(expr.key.value) && this.generators.size > 0))
|
||||
) {
|
||||
const method = expr.key.value;
|
||||
@@ -173,8 +158,7 @@ export class GeneratorDynamicDispatch {
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (expr.kind !== "dynInvoke" || !["next", "return", "throw"].includes(expr.method))
|
||||
return expr;
|
||||
if (expr.kind !== "dynInvoke" || !GENERATOR_STEP_METHODS.has(expr.method)) return expr;
|
||||
const key = JSON.stringify([expr.method, expr.args.length, expr.calleeName]);
|
||||
let dispatch = this.dispatches.get(key);
|
||||
if (!dispatch) {
|
||||
|
||||
@@ -289,6 +289,7 @@ function lowerStreamCallback(
|
||||
node: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
|
||||
thisType: IrType,
|
||||
fullTuple: IrType[],
|
||||
chunkValues = true,
|
||||
): IrExpr {
|
||||
const loc = locOf(node);
|
||||
const { shapes, funcType } = lowerer.lambdaSignature(node);
|
||||
@@ -309,11 +310,15 @@ function lowerStreamCallback(
|
||||
// callable dyn whose glue dispatches to the runtime's *_done).
|
||||
if (funcType.params[i]!.kind === "dyn") continue;
|
||||
// A write or transform chunk of another type converts from the
|
||||
// chunk's value when the callback runs (an object-mode side's value,
|
||||
// or the Buffer), or throws a TypeError there; an encoding of another
|
||||
// type (`string | null | undefined`) from the encoding argument's.
|
||||
// chunk's value when the callback runs (an object-mode side's value),
|
||||
// or throws a TypeError there — on a side known to be byte mode the
|
||||
// chunk is a Buffer, so the declared type must be Node's or a
|
||||
// supertype of it (`Buffer | string`); an encoding of another type
|
||||
// (`string | null | undefined`) converts from the encoding argument's
|
||||
// value.
|
||||
if (
|
||||
(i === 0 || i === 1) &&
|
||||
((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) ||
|
||||
i === 1) &&
|
||||
(which === "write" || which === "transform") &&
|
||||
funcType.params[i]!.kind !== "func" &&
|
||||
canConvertToDyn(
|
||||
@@ -465,6 +470,20 @@ function streamObjectModes(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether an options literal sets the writable side's object mode with a
|
||||
* `true` literal (objectMode, or writableObjectMode on a duplex stream).
|
||||
* Reports nothing: streamObjectModes fences the other value forms. */
|
||||
function literalWritesValues(opts: ts.ObjectLiteralExpression, duplexish: boolean): boolean {
|
||||
for (const prop of opts.properties?.nodes ?? []) {
|
||||
if (!ts.isPropertyAssignment(prop)) continue;
|
||||
const name =
|
||||
ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null;
|
||||
if (name !== "objectMode" && !(duplexish && name === "writableObjectMode")) continue;
|
||||
if (prop.initializer.kind !== ts.SyntaxKind.FalseKeyword) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The full Node parameter tuples of the option callbacks, given the
|
||||
* concrete stream's `this` type. The trailing completion callbacks keep
|
||||
* whatever func type the checker contextually gave the user's parameter
|
||||
@@ -802,7 +821,10 @@ function parseStreamOptions(
|
||||
}
|
||||
return declared ?? funcOf([], VOID);
|
||||
});
|
||||
present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
|
||||
present.set(
|
||||
name,
|
||||
lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, out.writableObject),
|
||||
);
|
||||
} finally {
|
||||
if (pinned) lowerer.stageSourceTypes.delete(pinned);
|
||||
}
|
||||
@@ -1151,6 +1173,7 @@ function streamMethodWrapper(
|
||||
thisType: IrType,
|
||||
loc: SrcLoc,
|
||||
blame: ts.Node,
|
||||
chunkValues = true,
|
||||
): IrExpr | null {
|
||||
const found = lowerer.findMethodOn(info, methodName);
|
||||
if (!found) {
|
||||
@@ -1235,10 +1258,11 @@ function streamMethodWrapper(
|
||||
continue;
|
||||
}
|
||||
// A write or transform chunk of another type converts from the
|
||||
// chunk's value when the method runs (an object-mode side's value,
|
||||
// or the Buffer), or throws a TypeError there.
|
||||
// chunk's value when the method runs (an object-mode side's value),
|
||||
// or throws a TypeError there; a byte-mode side's chunk is a Buffer.
|
||||
if (
|
||||
i === 0 &&
|
||||
chunkValues &&
|
||||
(which === "write" || which === "transform") &&
|
||||
got.type.kind !== "func" &&
|
||||
canConvertToDyn(
|
||||
@@ -1343,10 +1367,22 @@ export function lowerStreamSuperCall(
|
||||
);
|
||||
}
|
||||
const shape = streamCtorShape(cls);
|
||||
// Whether the writable side holds values: options read at run time may
|
||||
// set objectMode, and a literal says so with objectMode (or a Duplex's
|
||||
// writableObjectMode) true; otherwise _write and _transform receive
|
||||
// Buffers.
|
||||
// super(undefined) and super(null) pass no options (followOptionsArg
|
||||
// answers null), so their sides are byte mode too.
|
||||
const optsNode = argNodes[0] !== undefined ? followOptionsArg(lowerer, argNodes[0]) : null;
|
||||
const chunkValues =
|
||||
optsNode !== null &&
|
||||
optsNode !== undefined &&
|
||||
(!ts.isObjectLiteralExpression(optsNode) ||
|
||||
literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable"));
|
||||
const fallback = (name: string): IrExpr | null => {
|
||||
const methodName = UNDERSCORE_METHODS.get(name);
|
||||
return methodName
|
||||
? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame)
|
||||
? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame, chunkValues)
|
||||
: null;
|
||||
};
|
||||
const thisRef = (): IrExpr => ({ kind: "varRef", localId: thisLocal.id, type: thisType, loc });
|
||||
|
||||
@@ -181,7 +181,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js";
|
||||
* - Lexical scoping is resolved here: locals get function-unique ids
|
||||
* ("x.0", "x.1" for shadowing); the IR is scope-flat.
|
||||
*/
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import { tsgoPath } from "../dts-paths.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
@@ -3192,12 +3192,14 @@ export class Lowerer {
|
||||
errorPropertyDispatchRevision: string | null = null;
|
||||
/** Structural source positions (structural-positions.ts) for names that
|
||||
* stay the same when unrelated code moves. */
|
||||
/** Default library files are named by their base name: two checkers
|
||||
* install them in different directories. */
|
||||
/** Files are named by their path relative to the entry's directory, so
|
||||
* names do not depend on where the program sits, and default library
|
||||
* files by their base name: two checkers install them in different
|
||||
* directories. */
|
||||
readonly positions = new StructuralPositions((sf) =>
|
||||
this.program.isSourceFileDefaultLibrary(sf)
|
||||
? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}`
|
||||
: sf.fileName,
|
||||
: relative(dirname(this.entry.fileName), sf.fileName),
|
||||
);
|
||||
|
||||
/** The outermost node whose token span is exactly `loc`, or null. */
|
||||
@@ -8231,13 +8233,17 @@ export class Lowerer {
|
||||
for (;;) {
|
||||
drainInstances();
|
||||
const declared = unitsInOrder();
|
||||
const initializerChanged = this.deferredModuleInitializers.process(this, [
|
||||
...declared,
|
||||
...initFunctions,
|
||||
...instanceFunctions,
|
||||
...byName(this.liftedFns),
|
||||
...byName(this.implicitFns),
|
||||
]);
|
||||
const initializerChanged = this.deferredModuleInitializers.process(
|
||||
this,
|
||||
[
|
||||
...declared,
|
||||
...initFunctions,
|
||||
...instanceFunctions,
|
||||
...byName(this.liftedFns),
|
||||
...byName(this.implicitFns),
|
||||
],
|
||||
(fn) => classDispatch.sites.forget(fn),
|
||||
);
|
||||
const dispatchChanged = classDispatch.process(this, [
|
||||
...declared,
|
||||
...initFunctions,
|
||||
|
||||
@@ -426,3 +426,119 @@ test("paired counters closing in on each other run as integers", () => {
|
||||
unused.body = [];
|
||||
expect(matchIntegerPairedForLoop(unused, vars)).toBeNull();
|
||||
});
|
||||
|
||||
test("counters stepped beside the tested one run as integers", () => {
|
||||
const vars = new Map(locals);
|
||||
for (const id of ["j", "k", "limit", "from"])
|
||||
vars.set(id, { id, name: id, type: F64, mutable: true });
|
||||
vars.set("s", { id: "s", name: "s", type: STRING, mutable: false });
|
||||
const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: id,
|
||||
value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
const read = (index: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver: { kind: "varRef", localId: "s", type: STRING, loc },
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const comoving = (
|
||||
limit: IrExpr,
|
||||
followerStart: IrExpr = number(0),
|
||||
update: IrStmt = { kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc },
|
||||
) =>
|
||||
({
|
||||
kind: "for",
|
||||
loc,
|
||||
init: {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: "i", init: number(0), loc },
|
||||
{ kind: "varDecl", localId: "j", init: followerStart, loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
cond: { kind: "bin", op: "<", left: ref("i"), right: limit, type: BOOL, loc },
|
||||
update,
|
||||
body: [read(ref("j"))],
|
||||
}) as IrStmt & { kind: "for" };
|
||||
// A literal limit bounds the follower: at most 9 updates of 4 before the
|
||||
// last body entry.
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10)), vars)).toMatchObject({
|
||||
localId: "i",
|
||||
guarded: false,
|
||||
range: { min: 0, max: 9 },
|
||||
followers: [
|
||||
{
|
||||
localId: "j",
|
||||
step: 4,
|
||||
startGuarded: false,
|
||||
tripGuarded: false,
|
||||
range: { min: 0, max: 36 },
|
||||
},
|
||||
],
|
||||
});
|
||||
// An unknown limit or start is tested at run time.
|
||||
expect(matchIntegerCountedForLoop(comoving(ref("limit")), vars)).toMatchObject({
|
||||
guarded: true,
|
||||
followers: [{ tripGuarded: true, range: { min: 0, max: 2 ** 52 + 2 } }],
|
||||
});
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10), ref("from")), vars)).toMatchObject({
|
||||
guarded: true,
|
||||
followers: [{ startGuarded: true, tripGuarded: true }],
|
||||
});
|
||||
// A literal limit with known starts that can pass 2^53 is refused.
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10), number(2 ** 53 - 9)), vars)).toBeNull();
|
||||
// Nested update blocks (`i++, j += 4, k--`) step three counters.
|
||||
const three = comoving(number(10), number(0), {
|
||||
kind: "block",
|
||||
body: [{ kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc }, step("k", "-")],
|
||||
loc,
|
||||
});
|
||||
if (three.init?.kind === "block")
|
||||
three.init.body.push({ kind: "varDecl", localId: "k", init: number(5), loc });
|
||||
expect(matchIntegerCountedForLoop(three, vars)?.followers).toMatchObject([
|
||||
{ localId: "j", range: { min: 0, max: 36 } },
|
||||
{ localId: "k", step: -1, range: { min: -4, max: 5 } },
|
||||
]);
|
||||
// A follower without a step, one the body writes, one the limit reads, a
|
||||
// captured one, and a start that reads another counter are refused.
|
||||
expect(
|
||||
matchIntegerCountedForLoop(
|
||||
comoving(number(10), number(0), { kind: "block", body: [step("i", "+")], loc }),
|
||||
vars,
|
||||
),
|
||||
).toBeNull();
|
||||
const writing = comoving(number(10));
|
||||
writing.body.push(step("j", "+"));
|
||||
expect(matchIntegerCountedForLoop(writing, vars)).toBeNull();
|
||||
expect(matchIntegerCountedForLoop(comoving(ref("j")), vars)).toBeNull();
|
||||
const boxed = new Map(vars);
|
||||
boxed.set("j", { ...vars.get("j")!, boxed: true });
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10)), boxed)).toBeNull();
|
||||
expect(matchIntegerCountedForLoop(comoving(number(10), ref("i")), vars)).toBeNull();
|
||||
// A driver span above 2^53 - 1 bounds no trip count. From -(2^53 - 1) to
|
||||
// below 2^52 - 1 the span 3 * 2^52 - 3 rounds down to 3 * 2^52 - 4, which
|
||||
// would put this follower's last value at 2^53 - 1 instead of 2^53.
|
||||
const wide = comoving(number(2 ** 52 - 1), number(-(2 ** 52) + 3), {
|
||||
kind: "block",
|
||||
body: [step("i", "+"), step("j", "+")],
|
||||
loc,
|
||||
});
|
||||
if (wide.init?.kind === "block" && wide.init.body[0]?.kind === "varDecl")
|
||||
wide.init.body[0].init = number(-Number.MAX_SAFE_INTEGER);
|
||||
expect(matchIntegerCountedForLoop(wide, vars)).toBeNull();
|
||||
if (wide.init?.kind === "block" && wide.init.body[1]?.kind === "varDecl")
|
||||
wide.init.body[1].init = ref("from");
|
||||
expect(matchIntegerCountedForLoop(wide, vars)?.followers).toMatchObject([
|
||||
{ startGuarded: true, tripGuarded: true },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,8 +28,36 @@ export interface IntegerCountedForLoop {
|
||||
startGuarded: boolean;
|
||||
guardLimit: number;
|
||||
range: IntegerRange;
|
||||
/** Counters declared and stepped beside this one that the condition does
|
||||
* not test, such as `j` in `for (let i = 0, j = 0; i < n; i++, j += 4)`. */
|
||||
followers: IntegerLoopFollower[];
|
||||
}
|
||||
|
||||
/** A counter that steps by a constant on every update of a counted loop.
|
||||
* After `k` updates it holds `start + step * k`. */
|
||||
export interface IntegerLoopFollower {
|
||||
localId: string;
|
||||
start: IrExpr;
|
||||
step: number;
|
||||
/** As the driver's `startGuarded` (part of the loop's `guarded`). */
|
||||
startGuarded: boolean;
|
||||
/** The follower's range depends on the run-time trip count: the versioned
|
||||
* loop also tests `|start| + |step| * span / |driver step|` against
|
||||
* `FOLLOWER_GUARD_LIMIT`, where `span` is the distance from the driver's
|
||||
* start to the limit (part of the loop's `guarded`). */
|
||||
tripGuarded: boolean;
|
||||
/** The follower's values on every body entry. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** The bound of the run-time follower test. */
|
||||
export const FOLLOWER_GUARD_LIMIT = 2 ** 52;
|
||||
/** The magnitude a follower that passed the test can reach. The test's four
|
||||
* f64 operations (the step ratio, the span, its product and the sum) each
|
||||
* round down by at most a factor of 1 - 2^-53, so the exact sum is below
|
||||
* `2^52 / (1 - 2^-53)^4`, which is less than 2^52 + 3. */
|
||||
const FOLLOWER_GUARD_RANGE = FOLLOWER_GUARD_LIMIT + 2;
|
||||
|
||||
function constantStep(update: IrStmt | null, id: string): number | null {
|
||||
if (update?.kind === "exprStmt" && update.expr.kind === "incDec" && update.expr.localId === id)
|
||||
return update.expr.op === "+" ? 1 : -1;
|
||||
@@ -115,33 +143,61 @@ export function matchIntegerCountedForLoop(
|
||||
ranges: IntegerRanges = new Map(),
|
||||
unaliased: ReadonlySet<string> | null = null,
|
||||
): IntegerCountedForLoop | null {
|
||||
const init = stmt.init;
|
||||
if (init?.kind !== "varDecl" || !init.init) return null;
|
||||
const local = locals.get(init.localId);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
const start = init.init;
|
||||
let startRange =
|
||||
start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0)
|
||||
? { min: start.value, max: start.value }
|
||||
: ranges.get(start);
|
||||
// An unproven start is tested at run time, on a versioned loop.
|
||||
const startGuarded = !startRange && start.kind !== "numLit" && start.type.kind === "f64";
|
||||
if (startGuarded) startRange = { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
|
||||
if (
|
||||
!startRange ||
|
||||
startRange.min < -Number.MAX_SAFE_INTEGER ||
|
||||
startRange.max > Number.MAX_SAFE_INTEGER ||
|
||||
!stableLoopValue(start, locals, stmt.body, false, local.id)
|
||||
)
|
||||
return null;
|
||||
const step = constantStep(stmt.update, local.id);
|
||||
if (step === null || writesLocal(stmt.body, local.id)) return null;
|
||||
const cond = stmt.cond;
|
||||
if (cond?.kind !== "bin") return null;
|
||||
const decls = stmt.init?.kind === "block" ? stmt.init.body : stmt.init ? [stmt.init] : [];
|
||||
// `i++, j += 2, k--` lowers to nested blocks of steps.
|
||||
const flat = (u: IrStmt | null): (IrStmt | null)[] =>
|
||||
u?.kind === "block" ? u.body.flatMap(flat) : [u];
|
||||
const updates = flat(stmt.update);
|
||||
if (decls.length === 0 || updates.length !== decls.length) return null;
|
||||
const declared = (e: IrExpr): string | null =>
|
||||
e.kind === "varRef" && decls.some((d) => d.kind === "varDecl" && d.localId === e.localId)
|
||||
? e.localId
|
||||
: null;
|
||||
// The condition tests the driving counter; every other declaration
|
||||
// follows it.
|
||||
const driver = declared(cond.left) ?? declared(cond.right);
|
||||
const counterIds = decls.map((d) => (d.kind === "varDecl" ? d.localId : ""));
|
||||
const stepOf = (id: string): number | null => {
|
||||
const steps = updates.map((u) => constantStep(u, id)).filter((v) => v !== null);
|
||||
return steps.length === 1 ? steps[0]! : null;
|
||||
};
|
||||
// A counter's start is a stable integer that refers to no counter. An
|
||||
// unproven start is tested at run time, on a versioned loop.
|
||||
const counterStart = (
|
||||
id: string,
|
||||
): { start: IrExpr; range: IntegerRange; guarded: boolean; step: number } | null => {
|
||||
const decl = decls.find((d) => d.kind === "varDecl" && d.localId === id);
|
||||
if (decl?.kind !== "varDecl" || !decl.init) return null;
|
||||
const local = locals.get(id);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
const start = decl.init;
|
||||
const known =
|
||||
start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0)
|
||||
? { min: start.value, max: start.value }
|
||||
: ranges.get(start);
|
||||
const guarded = !known && start.kind !== "numLit" && start.type.kind === "f64";
|
||||
const range = guarded ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } : known;
|
||||
const step = stepOf(id);
|
||||
if (
|
||||
!range ||
|
||||
range.min < -Number.MAX_SAFE_INTEGER ||
|
||||
range.max > Number.MAX_SAFE_INTEGER ||
|
||||
!counterIds.every((counter) => stableLoopValue(start, locals, stmt.body, false, counter)) ||
|
||||
step === null ||
|
||||
writesLocal(stmt.body, id)
|
||||
)
|
||||
return null;
|
||||
return { start, range, guarded, step };
|
||||
};
|
||||
const head = driver === null ? null : counterStart(driver);
|
||||
if (!head) return null;
|
||||
const local = locals.get(driver!)!;
|
||||
const { start, step, guarded: startGuarded, range: startRange } = head;
|
||||
let limit = cond.right,
|
||||
op: string = cond.op;
|
||||
if (cond.left.kind !== "varRef" || cond.left.localId !== local.id) {
|
||||
if (cond.right.kind !== "varRef" || cond.right.localId !== local.id) return null;
|
||||
limit = cond.left;
|
||||
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
|
||||
}
|
||||
@@ -185,11 +241,70 @@ export function matchIntegerCountedForLoop(
|
||||
const limitGuarded =
|
||||
!boundRange || (step > 0 ? !(boundRange.max <= guardLimit) : !(boundRange.min >= guardLimit));
|
||||
if (limitGuarded && limit.kind === "numLit") return null;
|
||||
const guarded = limitGuarded || startGuarded;
|
||||
const range =
|
||||
step > 0
|
||||
? {
|
||||
min: startRange.min,
|
||||
max: Math.max(
|
||||
startRange.min,
|
||||
Math.min(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.ceil(boundRange.max) - (inclusive ? 0 : 1)
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
}
|
||||
: {
|
||||
min: Math.min(
|
||||
startRange.max,
|
||||
Math.max(
|
||||
-Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.floor(boundRange.min) + (inclusive ? 0 : 1)
|
||||
: -Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
max: startRange.max,
|
||||
};
|
||||
// On body entry k the driver holds `start + step * k`, so k is at most the
|
||||
// driver's span over |step|, and a follower holds its start plus its own
|
||||
// step times k. A span above 2^53 - 1 can round down, so it bounds no
|
||||
// trip count. Below it the quotient rounds to at least its floor, and the
|
||||
// follower's products and sums below are exact while they stay within
|
||||
// ±(2^53 - 1) and round outside it otherwise.
|
||||
const span = step > 0 ? range.max - startRange.min : startRange.max - range.min;
|
||||
const trips = span <= Number.MAX_SAFE_INTEGER ? Math.floor(span / Math.abs(step)) : Infinity;
|
||||
const followers: IntegerLoopFollower[] = [];
|
||||
for (const id of counterIds) {
|
||||
if (id === local.id) continue;
|
||||
const follower = counterStart(id);
|
||||
// The limit is evaluated once; a follower changes on every update.
|
||||
if (!follower || !stableLoopValue(limit, locals, stmt.body, false, id)) return null;
|
||||
const reach = follower.step * trips;
|
||||
const min = follower.range.min + Math.min(0, reach),
|
||||
max = follower.range.max + Math.max(0, reach);
|
||||
const tripGuarded = min < -Number.MAX_SAFE_INTEGER || max > Number.MAX_SAFE_INTEGER;
|
||||
// Known starts and a literal limit are not tested at run time.
|
||||
if (tripGuarded && !follower.guarded && !startGuarded && limit.kind === "numLit") return null;
|
||||
followers.push({
|
||||
localId: id,
|
||||
start: follower.start,
|
||||
step: follower.step,
|
||||
startGuarded: follower.guarded,
|
||||
tripGuarded,
|
||||
range: tripGuarded
|
||||
? { min: Math.max(-FOLLOWER_GUARD_RANGE, min), max: Math.min(FOLLOWER_GUARD_RANGE, max) }
|
||||
: { min, max },
|
||||
});
|
||||
}
|
||||
const guarded =
|
||||
limitGuarded || startGuarded || followers.some((f) => f.startGuarded || f.tripGuarded);
|
||||
let useful = false;
|
||||
let cost = 0;
|
||||
const counter = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && e.localId === local.id) ||
|
||||
(e.kind === "varRef" &&
|
||||
(e.localId === local.id || followers.some((f) => f.localId === e.localId))) ||
|
||||
(e.kind === "bin" &&
|
||||
(e.op === "+" || e.op === "-" || e.op === "*") &&
|
||||
(counter(e.left) || counter(e.right)));
|
||||
@@ -241,32 +356,6 @@ export function matchIntegerCountedForLoop(
|
||||
return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt);
|
||||
};
|
||||
if (!stmt.body.every(bodyStmt) || !useful) return null;
|
||||
const range =
|
||||
step > 0
|
||||
? {
|
||||
min: startRange.min,
|
||||
max: Math.max(
|
||||
startRange.min,
|
||||
Math.min(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.ceil(boundRange.max) - (inclusive ? 0 : 1)
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
}
|
||||
: {
|
||||
min: Math.min(
|
||||
startRange.max,
|
||||
Math.max(
|
||||
-Number.MAX_SAFE_INTEGER,
|
||||
boundRange
|
||||
? Math.floor(boundRange.min) + (inclusive ? 0 : 1)
|
||||
: -Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
),
|
||||
max: startRange.max,
|
||||
};
|
||||
return {
|
||||
localId: local.id,
|
||||
start,
|
||||
@@ -278,6 +367,7 @@ export function matchIntegerCountedForLoop(
|
||||
startGuarded,
|
||||
guardLimit,
|
||||
range,
|
||||
followers,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-callback-versions-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const body = (llvm: string, marker: string): string => {
|
||||
const match = new RegExp(`^define internal [^\\n]*${marker}[^]*?^}`, "m").exec(llvm);
|
||||
if (!match) throw new Error(`no function ${marker}`);
|
||||
return match[0];
|
||||
};
|
||||
|
||||
test("loops that call a known callback test its closure once and call it directly", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
function each(values: number[], visit: (v: number) => void): void {
|
||||
for (let i = 0; i < values.length; i++) visit(values[i]!);
|
||||
}
|
||||
let sum = 0;
|
||||
each([1, 2, 3], (v) => { sum += v; });
|
||||
console.log(sum);
|
||||
`);
|
||||
const fn = body(llvm, "@sc_bf_each");
|
||||
const fast = /^callback\.version\.fast\d+:$[^]*?^callback\.version\.done\d+:$/m.exec(fn)![0];
|
||||
// The guard compares the closure's code pointer once, before the loop.
|
||||
expect(fn.slice(0, fn.indexOf(fast))).toMatch(/icmp eq ptr %\S+, @/);
|
||||
expect(fast).not.toContain("callback.indirect");
|
||||
expect(fast).not.toContain("scr_dyn_this_push_dyn");
|
||||
// The ordinary copy keeps the fallback through the closure's pointer.
|
||||
expect(fn).toContain("callback.indirect");
|
||||
});
|
||||
|
||||
test("captured number payloads carry their own alias tag", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
function total(values: number[]): number {
|
||||
let sum = 0;
|
||||
values.forEach((v) => { sum += v; });
|
||||
return sum;
|
||||
}
|
||||
console.log(total([1, 2, 3]));
|
||||
`);
|
||||
// Every read and write through a box's payload slot (field 5) is tagged.
|
||||
const slots = [
|
||||
...llvm.matchAll(/(%\S+) = getelementptr inbounds %ScrBox, ptr %\S+, i32 0, i32 5/g),
|
||||
];
|
||||
expect(slots.length).toBeGreaterThan(0);
|
||||
for (const [, slot] of slots) {
|
||||
for (const access of llvm.matchAll(
|
||||
new RegExp(`(load double, ptr|store double %\\S+, ptr) \\${slot}\\b[^\\n]*`, "g"),
|
||||
))
|
||||
expect(access[0]).toMatch(/!tbaa !\d+$/);
|
||||
}
|
||||
expect(llvm).toMatch(/load double, ptr %\S+, !tbaa !\d+/);
|
||||
});
|
||||
|
||||
test("a callback declared in the loop body is not read before the loop", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
function strides(): number {
|
||||
let calls = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const bump = (): void => { i += 2; };
|
||||
bump();
|
||||
calls++;
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
console.log(strides());
|
||||
`);
|
||||
expect(llvm).not.toContain("callback.version");
|
||||
});
|
||||
|
||||
test("loops inside a callback version are not versioned again", async () => {
|
||||
// Each inner loop calls a callback a factory builds just before it; without
|
||||
// the limit, every copy of a version would version the next loop again.
|
||||
const llvm = await emitLlvm(`
|
||||
function adder(k: number): (v: number) => number {
|
||||
return (v: number) => v + k;
|
||||
}
|
||||
function nest(n: number, f: (v: number) => number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
s += f(i);
|
||||
const g = adder(i);
|
||||
for (let j = 0; j < n; j++) {
|
||||
s += g(j);
|
||||
const h = adder(j);
|
||||
for (let k = 0; k < n; k++) s += h(k);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
console.log(nest(3, (v) => v - 1), nest(2, (v) => v * 3));
|
||||
`);
|
||||
// One version for the outer loop of each compiled `nest`; its two copies
|
||||
// keep the inner loops whole.
|
||||
const versions = [...llvm.matchAll(/^define [^\n]*@sc_bf_nest[^]*?^}/gm)].map(
|
||||
([body]) => body.match(/^callback\.version\.fast\d+:$/gm)?.length ?? 0,
|
||||
);
|
||||
expect(versions.every((count) => count <= 1)).toBe(true);
|
||||
expect(versions.some((count) => count === 1)).toBe(true);
|
||||
});
|
||||
@@ -378,7 +378,7 @@ done:
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v26");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -712,7 +712,7 @@ done:
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v26");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v27");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Compiled regex matchers bound every read by the characters left in the
|
||||
* subject, so subjects near the runtime's 2^31 - 2 character limit are
|
||||
* read only inside the subject. Each matcher runs on a subject of exactly
|
||||
* that length whose last byte sits just before an inaccessible page: a
|
||||
* bounds check that wraps past INT32_MAX reads that page and faults. The
|
||||
* subject is a lazily mapped zero region (no memory is committed for it)
|
||||
* with the characters each case needs written near its end. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compileRegexMatcher, REGEX_MATCHER_DECLS } from "../src/backend/llvm/regex-matchers.js";
|
||||
|
||||
const LIMIT = 2 ** 31 - 2;
|
||||
|
||||
/** [symbol, pattern, flags, start offset from the end, tail text]. */
|
||||
const CASES: [string, string, string, number, string][] = [
|
||||
// The first-character vector search, 10 characters before the end.
|
||||
["seek", "[b-d]", "", 10, "c"],
|
||||
// The start filter (two 8-character alternatives), 20 before the end.
|
||||
["filter", "agggtaaa|tttaccct", "i", 20, "TTTACCCT"],
|
||||
// A run of 8 literal characters beginning 2 before the end.
|
||||
["run", "xy234567", "", 30, "xy"],
|
||||
// A greedy class over the whole subject, 16 characters per step.
|
||||
["greedy", "[^x]+", "", LIMIT, ""],
|
||||
];
|
||||
|
||||
const DRIVER = `
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
void scr_regex_stack_grow(void *a, void *b, void *c, int32_t d) { (void)a; (void)b; (void)c; (void)d; _exit(3); }
|
||||
void scr_regex_stack_free(void *p) { (void)p; }
|
||||
${CASES.map(([sym]) => `int32_t ${sym}(const char *, int32_t, int32_t, const char **);`).join("\n")}
|
||||
static void check(const char *name, int32_t (*m)(const char *, int32_t, int32_t, const char **),
|
||||
int32_t start, const char *tail) {
|
||||
const int32_t len = ${LIMIT};
|
||||
size_t page = (size_t)sysconf(_SC_PAGESIZE);
|
||||
size_t body = ((size_t)len + page - 1) / page * page;
|
||||
char *base = mmap(NULL, body + page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (base == MAP_FAILED || mprotect(base + body, page, PROT_NONE) != 0) _exit(4);
|
||||
char *s = base + (body - (size_t)len);
|
||||
memcpy(s + len - strlen(tail), tail, strlen(tail));
|
||||
const char *out[16] = {0};
|
||||
int32_t found = m(s, len, start, out);
|
||||
if (found) printf("%s %ld %ld\\n", name, (long)(out[0] - s), (long)(out[1] - s));
|
||||
else printf("%s none\\n", name);
|
||||
fflush(stdout);
|
||||
munmap(base, body + page);
|
||||
}
|
||||
int main(void) {
|
||||
${CASES.map(([sym, , , back, tail]) => ` check("${sym}", ${sym}, ${LIMIT - back}, ${JSON.stringify(tail)});`).join("\n")}
|
||||
return 0;
|
||||
}
|
||||
`;
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"compiled regex matchers stay inside subjects near 2^31 characters",
|
||||
{ timeout: 120_000 },
|
||||
() => {
|
||||
const lines: string[] = [];
|
||||
const decls = new Set(REGEX_MATCHER_DECLS);
|
||||
for (const [sym, pattern, flags] of CASES) {
|
||||
const code = compileRegexMatcher(sym, pattern, flags, "i64", true);
|
||||
if (code === null) throw new Error(`/${pattern}/${flags} did not compile`);
|
||||
lines.push(...code.lines.map((line) => line.replace(/^define internal /, "define ")));
|
||||
for (const decl of code.decls) decls.add(decl);
|
||||
}
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-regex-bounds-"));
|
||||
try {
|
||||
const ir = join(dir, "matchers.ll");
|
||||
const driver = join(dir, "driver.c");
|
||||
const binary = join(dir, "bounds");
|
||||
writeFileSync(ir, [...decls, ...lines, "attributes #0 = { nounwind }", ""].join("\n"));
|
||||
writeFileSync(driver, DRIVER);
|
||||
execFileSync("clang", ["-O2", "-Wno-override-module", ir, driver, "-o", binary]);
|
||||
const output = execFileSync(binary, { encoding: "utf8" });
|
||||
expect(output.trim().split("\n")).toEqual([
|
||||
`seek ${LIMIT - 1} ${LIMIT}`,
|
||||
`filter ${LIMIT - 8} ${LIMIT}`,
|
||||
"run none",
|
||||
`greedy 0 ${LIMIT}`,
|
||||
]);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-speculative-loop-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const source = `
|
||||
class Point {
|
||||
x: number;
|
||||
w = 0;
|
||||
constructor(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
function scaled(points: Point[], scale: number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const p = points[i]!;
|
||||
p.w += 1;
|
||||
const k = p.x * scale;
|
||||
s += k * k;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
console.log(scaled([new Point(1), new Point(2)], 2), scaled([new Point(1)], 0.5));
|
||||
`;
|
||||
|
||||
function labels(fn: string): string[] {
|
||||
return [...fn.matchAll(/^([\w.]+):/gm)].map((m) => m[1]!);
|
||||
}
|
||||
|
||||
test("a failed check finishes its iteration outside the ordinary copy", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? "";
|
||||
const order = labels(fn);
|
||||
// The loop may have several speculative sites (one in each copy of a
|
||||
// dense-array loop). Each site emits its ordinary copy, its third copy
|
||||
// and its fast copy in that order.
|
||||
const slows = order.flatMap((l, i) => (l.startsWith("spec.slow") ? [i] : []));
|
||||
expect(slows.length).toBeGreaterThan(0);
|
||||
let resumes = 0;
|
||||
for (const slow of slows) {
|
||||
const next = (prefix: string): number =>
|
||||
order.findIndex((l, i) => i > slow && l.startsWith(prefix));
|
||||
const tail = next("spec.tail");
|
||||
const fast = next("spec.fast");
|
||||
expect(tail).toBeGreaterThan(slow);
|
||||
expect(fast).toBeGreaterThan(tail);
|
||||
// Every resume point of the site is in its third copy, between that
|
||||
// copy's start and the fast copy, so the ordinary copy has one entry.
|
||||
for (let i = slow + 1; i < fast; i++) {
|
||||
if (!order[i]!.startsWith("spec.resume")) continue;
|
||||
expect(i).toBeGreaterThan(tail);
|
||||
resumes++;
|
||||
}
|
||||
// The third copy's header leaves for the ordinary copy's header.
|
||||
const header = order[slow + 1]!;
|
||||
const third = fn.slice(fn.indexOf(`\n${order[tail]}:`), fn.indexOf(`\n${order[fast]}:`));
|
||||
expect(third).toContain(`br label %${header}`);
|
||||
}
|
||||
expect(resumes).toBe(order.filter((l) => l.startsWith("spec.resume")).length);
|
||||
expect(resumes).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("a loop site stops entering its fast copy after repeated bails", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const site = /^(@scr\.spec\.bails\.\d+) = internal global i8 0$/m.exec(llvm)?.[1];
|
||||
expect(site).toBeDefined();
|
||||
const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? "";
|
||||
const escaped = site!.replace(/\./g, "\\.");
|
||||
// The entry test reads the count, a bail adds one, and a completed fast
|
||||
// copy clears it.
|
||||
expect(fn).toMatch(
|
||||
new RegExp(
|
||||
`(%\\S+) = load atomic i8, ptr ${escaped} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`,
|
||||
),
|
||||
);
|
||||
expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`));
|
||||
expect(fn).toContain(`store atomic i8 0, ptr ${site} unordered, align 1`);
|
||||
});
|
||||
@@ -110,3 +110,10 @@ console.log(box.value);
|
||||
expect(locals).toHaveLength(2);
|
||||
expect(locals[0]!.name).not.toBe(locals[1]!.name);
|
||||
});
|
||||
|
||||
test("names do not depend on the directory the program sits in", async () => {
|
||||
// Each call lowers in its own temporary directory.
|
||||
const [first] = await lower(recursive);
|
||||
const [second] = await lower(recursive);
|
||||
expect(names(second!)).toEqual(names(first!));
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6872-regex-start-filter.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6997-typed-array-invariant-index-terms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7130-typed-message-listeners.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7131-typed-worker-messages.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7141-comoving-loop-counters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7300-callback-captured-scalars.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7321-speculative-loop-bail-sites.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7361-message-listeners-share-value.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-quoted-accessor-keys.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/stream-byte-mode-chunk-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -340,7 +340,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: RUNTIME_PACK_MATRIX.target,
|
||||
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -437,7 +437,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: matrix.target,
|
||||
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_runtime_abi_v26(void) {}
|
||||
void scr_runtime_abi_v27(void) {}
|
||||
|
||||
void scr_init(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
|
||||
@@ -81,7 +81,7 @@ void scr_init(void);
|
||||
/* Program objects emitted by the bundled LLVM helper reference this symbol.
|
||||
* Its versioned spelling makes a mismatched manual runtime link fail before
|
||||
* the program can start. */
|
||||
void scr_runtime_abi_v26(void);
|
||||
void scr_runtime_abi_v27(void);
|
||||
void scr_runtime_workers_v8(void);
|
||||
|
||||
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* scriptc runtime — dynamic values: the checked-dynamic tree, native handles,
|
||||
* dyn promises, the ambient receiver and listener adapters (scr_json.c,
|
||||
* scr_dyn_handle.c, scr_async_dyn.c). Included by scr_runtime.h, which says
|
||||
* scr_dyn_handle.c, scr_async_dyn.c), and the message readers of typed
|
||||
* 'message' listeners (scr_message.c). Included by scr_runtime.h, which says
|
||||
* where new declarations go. */
|
||||
#ifndef SCR_DYN_H
|
||||
#define SCR_DYN_H
|
||||
@@ -254,6 +255,55 @@ typedef struct ScrDynTypedCast {
|
||||
* result as an owned (+1) dyn value. `args` entries are BORROWED. */
|
||||
typedef ScrDyn *(*ScrDynThunk)(ScrClosure *clo, ScrDyn *const *args, size_t argc);
|
||||
|
||||
/* A compiled 'message' listener's typed entry: builds the closure's
|
||||
* parameter from the message's nodes (the readers below) and calls
|
||||
* the closure. Answers false, without calling it, when the message does
|
||||
* not have the parameter's layout; the caller then decodes the message
|
||||
* and calls the thunk. */
|
||||
struct ScrMessage;
|
||||
typedef bool (*ScrDynMessageListener)(ScrClosure *clo, const struct ScrMessage *message);
|
||||
|
||||
/* The kinds of a message's nodes (scr_message.c). */
|
||||
typedef enum {
|
||||
SCR_MESSAGE_UNDEFINED,
|
||||
SCR_MESSAGE_NULL,
|
||||
SCR_MESSAGE_BOOL,
|
||||
SCR_MESSAGE_NUMBER,
|
||||
SCR_MESSAGE_STRING,
|
||||
SCR_MESSAGE_BIGINT,
|
||||
SCR_MESSAGE_ARRAY,
|
||||
SCR_MESSAGE_OBJECT,
|
||||
SCR_MESSAGE_STORAGE,
|
||||
SCR_MESSAGE_BYTES,
|
||||
SCR_MESSAGE_ARRAY_BUFFER,
|
||||
SCR_MESSAGE_ERROR,
|
||||
/* A deeply frozen plain-data graph shared by pointer (see publication). */
|
||||
SCR_MESSAGE_PUBLISHED,
|
||||
/* A typed value published with @scriptc/threads (immortal), delivered as
|
||||
* a fresh typed-ref capsule over the same pointer. */
|
||||
SCR_MESSAGE_TYPED_REF,
|
||||
/* A transferred MessagePort: its channel end (+1) and side. */
|
||||
SCR_MESSAGE_PORT,
|
||||
/* A native handle cloned through its unit's ScrDynHandleTransfer. */
|
||||
SCR_MESSAGE_HANDLE,
|
||||
} ScrMessageKind;
|
||||
|
||||
/* Readers for compiled 'message' listeners (ScrDynMessageListener), which
|
||||
* build their typed parameter from the nodes. Node 0 is the root, and a
|
||||
* node's values have higher numbers. scr_message_readable answers whether
|
||||
* the message can reach these listeners: no node is reached twice, and it
|
||||
* holds no ports or native handles, which the receiver adopts when it
|
||||
* decodes the message. kind answers a ScrMessageKind; number reads a
|
||||
* NUMBER or BOOL node, string a STRING node (+1). fields answers whether an
|
||||
* OBJECT node has exactly the given keys, in order, and writes their value
|
||||
* nodes. */
|
||||
bool scr_message_readable(const struct ScrMessage *message);
|
||||
int32_t scr_message_kind(const struct ScrMessage *message, size_t node);
|
||||
double scr_message_number(const struct ScrMessage *message, size_t node);
|
||||
ScrStr *scr_message_string(const struct ScrMessage *message, size_t node);
|
||||
bool scr_message_fields(const struct ScrMessage *message, size_t node, const char *const *names,
|
||||
const size_t *lengths, size_t count, size_t *values);
|
||||
|
||||
/* Object member. Keys own shared immutable UTF-8 bytes (NUL-terminated for
|
||||
* convenience; key_len excludes the NUL) — duplicate keys were already
|
||||
* collapsed at parse time (later wins, like JS JSON.parse). */
|
||||
@@ -391,7 +441,7 @@ struct ScrDyn {
|
||||
* and `name` are static compiler-emitted literals (never freed); name
|
||||
* may be NULL (anonymous — inspect prints [Function (anonymous)]).
|
||||
* The dyn→closure ownership edge is traced by the cycle collector. */
|
||||
struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; } fn;
|
||||
struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; ScrDynMessageListener message; } fn;
|
||||
/* Native handles normally keep conservative ownership edges. Sets
|
||||
* backed by cycle-capable maps expose their edge to the collector.
|
||||
* Cache that allocation property: during collection the map may be
|
||||
@@ -1246,6 +1296,9 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr
|
||||
* (callers retain first when they keep their own reference); `sig`/`name`
|
||||
* must be static literals (the box never frees them; name may be NULL). */
|
||||
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
|
||||
/* Gives a function box made with thunk its typed message entry (an
|
||||
* EventEmitter listener registered from it keeps the entry). */
|
||||
void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener);
|
||||
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
|
||||
/* EventEmitter and the stream constructors as untyped constructor values,
|
||||
* and instanceof over untyped constructor functions
|
||||
|
||||
@@ -82,6 +82,8 @@ typedef struct ScrEeEntry {
|
||||
* the meta events. */
|
||||
ScrClosure *flex_cb;
|
||||
ScrDynThunk flex_invoke;
|
||||
/* The function box's typed message entry (scr_emitter_emit_message). */
|
||||
ScrDynMessageListener flex_message;
|
||||
const char *flex_sig;
|
||||
const char *flex_name;
|
||||
uint32_t flex_arity;
|
||||
@@ -512,6 +514,7 @@ static ScrEmitter *scr_ee_add(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*mov
|
||||
if (dyn) {
|
||||
e->flex_cb = scr_closure_retain(dyn->v.fn.clo);
|
||||
e->flex_invoke = dyn->v.fn.thunk;
|
||||
e->flex_message = dyn->v.fn.message;
|
||||
e->flex_sig = dyn->v.fn.sig;
|
||||
e->flex_name = dyn->v.fn.name;
|
||||
e->flex_arity = dyn->v.fn.arity;
|
||||
@@ -1013,15 +1016,16 @@ static ScrDyn *const scr_ee_no_args[1] = { NULL };
|
||||
* a nested emit's snapshot still holds it (the shared fired flag — Node's
|
||||
* wrapper.fired). A listener throw stops the pass (the exception stays
|
||||
* pending for the caller). Returns whether the bucket had listeners. */
|
||||
typedef struct ScrEeMessage ScrEeMessage;
|
||||
static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template,
|
||||
ScrDyn *const *flex_args, size_t flex_argc);
|
||||
ScrDyn *const *flex_args, size_t flex_argc, ScrEeMessage *message);
|
||||
|
||||
/* emit('error', ...) first emits the same arguments to errorMonitor. */
|
||||
static void scr_ee_emit_error_monitor(ScrEmitter *em, va_list *ap_template,
|
||||
ScrDyn *const *flex_args, size_t flex_argc) {
|
||||
ScrEeBucket *b = scr_ee_bucket_of(em->reg, SCR_EE_ERROR_MONITOR);
|
||||
if (!b || b->n == 0) return;
|
||||
scr_ee_emit_core(em, SCR_EE_ERROR_MONITOR, ap_template, flex_args, flex_argc);
|
||||
scr_ee_emit_core(em, SCR_EE_ERROR_MONITOR, ap_template, flex_args, flex_argc, NULL);
|
||||
}
|
||||
|
||||
/* A listener this emit has no argument form for: an untyped emit whose
|
||||
@@ -1044,8 +1048,17 @@ SCR_COLD static void scr_ee_listener_mismatch(const ScrStr *name, bool typed_emi
|
||||
scr_throw_error(typed_emit ? SCR_ERR_ERROR : SCR_ERR_TYPE, message);
|
||||
}
|
||||
|
||||
/* A message emit (scr_emitter_emit_message): listeners with a typed message
|
||||
* entry read the message until a listener without one runs; that listener
|
||||
* and every later one share one decoded argument. */
|
||||
struct ScrEeMessage {
|
||||
const struct ScrMessage *message;
|
||||
ScrDyn *(*decode)(const struct ScrMessage *);
|
||||
ScrDyn *decoded;
|
||||
};
|
||||
|
||||
static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template,
|
||||
ScrDyn *const *flex_args, size_t flex_argc) {
|
||||
ScrDyn *const *flex_args, size_t flex_argc, ScrEeMessage *message) {
|
||||
if (name->len == 5 && memcmp(name->data, "error", 5) == 0) {
|
||||
scr_ee_emit_error_monitor(em, ap_template, flex_args, flex_argc);
|
||||
if (scr_exc_pending()) return true;
|
||||
@@ -1088,6 +1101,22 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template,
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/* Once a listener has the decoded value, later listeners read that
|
||||
* value: an earlier listener may have changed it, as in Node. */
|
||||
if (message && !message->decoded && e->flex_message &&
|
||||
e->flex_message(e->flex_cb, message->message)) {
|
||||
scr_ee_entry_unref(e);
|
||||
continue;
|
||||
}
|
||||
if (message && !message->decoded) {
|
||||
message->decoded = message->decode(message->message);
|
||||
if (!message->decoded) {
|
||||
scr_ee_entry_unref(e);
|
||||
continue;
|
||||
}
|
||||
flex_args = &message->decoded;
|
||||
flex_argc = 1;
|
||||
}
|
||||
if (flex_args && e->flex_invoke) {
|
||||
ScrDyn *answer = e->flex_invoke(e->flex_cb, flex_args, flex_argc);
|
||||
scr_dyn_release(answer); /* EventEmitter ignores callback results. */
|
||||
@@ -1108,7 +1137,7 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template,
|
||||
static bool scr_ee_emit_va(ScrEmitter *em, ScrStr *name, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, name);
|
||||
bool had = scr_ee_emit_core(em, name, &ap, NULL, 0);
|
||||
bool had = scr_ee_emit_core(em, name, &ap, NULL, 0, NULL);
|
||||
va_end(ap);
|
||||
return had;
|
||||
}
|
||||
@@ -1119,7 +1148,7 @@ static bool scr_ee_emit_mixed_va(ScrEmitter *em, ScrStr *name, ScrDyn *const *ar
|
||||
int typed, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, typed);
|
||||
bool had = scr_ee_emit_core(em, name, typed ? &ap : NULL, args ? args : scr_ee_no_args, argc);
|
||||
bool had = scr_ee_emit_core(em, name, typed ? &ap : NULL, args ? args : scr_ee_no_args, argc, NULL);
|
||||
va_end(ap);
|
||||
return had;
|
||||
}
|
||||
@@ -1131,7 +1160,7 @@ static bool scr_ee_emit_mixed_va(ScrEmitter *em, ScrStr *name, ScrDyn *const *ar
|
||||
bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, name);
|
||||
bool had = scr_ee_emit_core(em, name, &ap, NULL, 0);
|
||||
bool had = scr_ee_emit_core(em, name, &ap, NULL, 0, NULL);
|
||||
va_end(ap);
|
||||
return had;
|
||||
}
|
||||
@@ -1143,7 +1172,7 @@ bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...) {
|
||||
bool scr_emitter_emit_mixed(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, argc);
|
||||
bool had = scr_ee_emit_core(em, name, &ap, args ? args : scr_ee_no_args, argc);
|
||||
bool had = scr_ee_emit_core(em, name, &ap, args ? args : scr_ee_no_args, argc, NULL);
|
||||
va_end(ap);
|
||||
return had;
|
||||
}
|
||||
@@ -1241,7 +1270,15 @@ static bool scr_ee_emit_error_dyn(ScrEmitter *em, ScrStr *name, ScrDyn *const *a
|
||||
/* An untyped emit of a name typed code never fixes a tuple for (computed
|
||||
* names, runtime-internal registries): the arguments as given. */
|
||||
bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc) {
|
||||
return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc);
|
||||
return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc, NULL);
|
||||
}
|
||||
|
||||
bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message,
|
||||
ScrDyn *(*decode)(const struct ScrMessage *)) {
|
||||
ScrEeMessage state = {message, decode, NULL};
|
||||
bool had = scr_ee_emit_core(em, name, NULL, NULL, 0, &state);
|
||||
scr_dyn_release(state.decoded);
|
||||
return had;
|
||||
}
|
||||
|
||||
/* emit(name, ...args) through an untyped value: the arguments as given.
|
||||
@@ -1255,7 +1292,7 @@ bool scr_emitter_emit_any(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, siz
|
||||
return scr_ee_emit_mixed_va(em, name, args, argc, s != NULL, s);
|
||||
}
|
||||
}
|
||||
return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc);
|
||||
return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc, NULL);
|
||||
}
|
||||
|
||||
/* Handle event registries use checked callbacks for every tuple, including
|
||||
@@ -1268,7 +1305,7 @@ bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error) {
|
||||
return false;
|
||||
}
|
||||
ScrStr *name = scr_str_new("error", 5);
|
||||
bool had = scr_ee_emit_core(em, name, NULL, &error, 1);
|
||||
bool had = scr_ee_emit_core(em, name, NULL, &error, 1, NULL);
|
||||
scr_str_release(name);
|
||||
return had;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,14 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all);
|
||||
* emit('error', err): no listener ⇒ THROWS err (borrowed; +1 taken). */
|
||||
bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...);
|
||||
bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc);
|
||||
/* An untyped emit of one message: a listener registered from a function
|
||||
* box with a typed message entry (scr_dyn_func_set_message) reads the
|
||||
* message itself while no listener has needed the decoded value; from the
|
||||
* first listener without an entry on, every listener gets decode(message),
|
||||
* made once. */
|
||||
struct ScrMessage;
|
||||
bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message,
|
||||
ScrDyn *(*decode)(const struct ScrMessage *));
|
||||
/* An untyped emit of an event with a fixed tuple: the arguments as given
|
||||
* for dynamic listeners, plus the tuple converted by the caller as
|
||||
* scr_emitter_emit's variadics for typed listeners. has_typed answers
|
||||
|
||||
@@ -4122,9 +4122,16 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con
|
||||
/* A signature adapter reports the length of the function it adapts. */
|
||||
d->v.fn.arity = clo->identity != NULL ? clo->identity_length : arity;
|
||||
d->v.fn.class_obj = NULL;
|
||||
d->v.fn.message = NULL;
|
||||
return d;
|
||||
}
|
||||
|
||||
void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener) {
|
||||
/* scr_dyn_new_func answers an adapted function's original box, whose
|
||||
* closure has other parameter types. */
|
||||
if (fn && fn->kind == SCR_DYN_FUNC && fn->v.fn.thunk == thunk) fn->v.fn.message = listener;
|
||||
}
|
||||
|
||||
/* A class is function-shaped for typeof and identity, but calling it without
|
||||
* new always throws. The closure owns the class object and its lexical boxes. */
|
||||
static ScrDyn *scr_dyn_class_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
|
||||
@@ -3,30 +3,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef enum {
|
||||
SCR_MESSAGE_UNDEFINED,
|
||||
SCR_MESSAGE_NULL,
|
||||
SCR_MESSAGE_BOOL,
|
||||
SCR_MESSAGE_NUMBER,
|
||||
SCR_MESSAGE_STRING,
|
||||
SCR_MESSAGE_BIGINT,
|
||||
SCR_MESSAGE_ARRAY,
|
||||
SCR_MESSAGE_OBJECT,
|
||||
SCR_MESSAGE_STORAGE,
|
||||
SCR_MESSAGE_BYTES,
|
||||
SCR_MESSAGE_ARRAY_BUFFER,
|
||||
SCR_MESSAGE_ERROR,
|
||||
/* A deeply frozen plain-data graph shared by pointer (see publication). */
|
||||
SCR_MESSAGE_PUBLISHED,
|
||||
/* A typed value published with @scriptc/threads (immortal), delivered as
|
||||
* a fresh typed-ref capsule over the same pointer. */
|
||||
SCR_MESSAGE_TYPED_REF,
|
||||
/* A transferred MessagePort: its channel end (+1) and side. */
|
||||
SCR_MESSAGE_PORT,
|
||||
/* A native handle cloned through its unit's ScrDynHandleTransfer. */
|
||||
SCR_MESSAGE_HANDLE,
|
||||
} ScrMessageKind;
|
||||
|
||||
const ScrMessagePortOps *scr_message_port_ops = NULL;
|
||||
|
||||
typedef struct {
|
||||
@@ -95,6 +71,7 @@ struct ScrMessage {
|
||||
size_t count;
|
||||
bool storage; /* has STORAGE nodes */
|
||||
bool releases; /* has STORAGE, PORT or HANDLE nodes, which own resources */
|
||||
bool readable; /* see scr_message_readable */
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -143,6 +120,8 @@ typedef struct {
|
||||
bool uncaught; /* the next value is an uncaught exception or its cause */
|
||||
bool storage;
|
||||
bool releases;
|
||||
bool shared; /* a node was reached twice */
|
||||
bool adopts; /* has PORT or HANDLE nodes */
|
||||
ScrMessageNode inline_nodes[SCR_MESSAGE_INLINE];
|
||||
ScrMessageVisit inline_visits[SCR_MESSAGE_INLINE];
|
||||
ScrMessagePending inline_properties[SCR_MESSAGE_INLINE];
|
||||
@@ -168,7 +147,7 @@ static void scr_message_encoder_init(ScrMessageEncoder *encoder, const ScrDyn *t
|
||||
encoder->stack_capacity = SCR_MESSAGE_INLINE;
|
||||
encoder->transfer = transfer;
|
||||
encoder->uncaught = uncaught;
|
||||
encoder->storage = encoder->releases = false;
|
||||
encoder->storage = encoder->releases = encoder->shared = encoder->adopts = false;
|
||||
}
|
||||
|
||||
static bool scr_message_listed(const ScrMessageEncoder *encoder, const ScrDyn *value) {
|
||||
@@ -290,7 +269,10 @@ static void scr_message_push(ScrMessageEncoder *encoder, size_t node) {
|
||||
static size_t scr_message_storage(ScrMessageEncoder *encoder, const ScrBytes *bytes) {
|
||||
const ScrBytes *owner = bytes->backing ? bytes->backing : bytes;
|
||||
size_t found = scr_message_find(encoder, owner, SCR_MESSAGE_STORAGE + 1);
|
||||
if (found != SIZE_MAX) return found;
|
||||
if (found != SIZE_MAX) {
|
||||
encoder->shared = true;
|
||||
return found;
|
||||
}
|
||||
size_t node = scr_message_node(encoder);
|
||||
scr_message_identify(encoder, owner, SCR_MESSAGE_STORAGE + 1, node);
|
||||
ScrMessageNode *storage = &encoder->nodes[node];
|
||||
@@ -360,7 +342,10 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
|
||||
return SIZE_MAX;
|
||||
}
|
||||
size_t found = scr_message_find(encoder, identity, domain);
|
||||
if (found != SIZE_MAX) return found;
|
||||
if (found != SIZE_MAX) {
|
||||
encoder->shared = true;
|
||||
return found;
|
||||
}
|
||||
size_t id = scr_message_node(encoder);
|
||||
scr_message_identify(encoder, identity, domain, id);
|
||||
encoder->visits[id].input = scr_dyn_retain((ScrDyn *)input);
|
||||
@@ -482,7 +467,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
|
||||
node->kind = SCR_MESSAGE_PORT;
|
||||
node->v.port.channel = channel;
|
||||
node->v.port.side = side;
|
||||
encoder->releases = true;
|
||||
encoder->releases = encoder->adopts = true;
|
||||
break;
|
||||
}
|
||||
bool buffer = scr_buffer_storage_is(value);
|
||||
@@ -498,7 +483,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
|
||||
node->kind = SCR_MESSAGE_HANDLE;
|
||||
node->v.handle.transfer = transfer;
|
||||
node->v.handle.payload = payload;
|
||||
encoder->releases = true;
|
||||
encoder->releases = encoder->adopts = true;
|
||||
break;
|
||||
}
|
||||
const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes;
|
||||
@@ -678,6 +663,7 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) {
|
||||
message = scr_message_alloc(1, sizeof(*message) + sizeof(*message->nodes));
|
||||
message->nodes = (ScrMessageNode *)(message + 1);
|
||||
message->count = 1;
|
||||
message->readable = true;
|
||||
message->nodes[0].kind = SCR_MESSAGE_PUBLISHED;
|
||||
message->nodes[0].v.published = (ScrDyn *)value;
|
||||
if (getenv("SCRIPTC_PUBLISH_TRACE"))
|
||||
@@ -770,7 +756,8 @@ static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) {
|
||||
ScrMessageProperty *slots = (ScrMessageProperty *)(nodes + count);
|
||||
ScrMessageTyped *records = (ScrMessageTyped *)(slots + properties);
|
||||
char *text = (char *)(records + typed);
|
||||
*message = (ScrMessage){nodes, count, encoder->storage, encoder->releases};
|
||||
*message = (ScrMessage){nodes, count, encoder->storage, encoder->releases,
|
||||
!encoder->shared && !encoder->adopts};
|
||||
memcpy(nodes, encoder->nodes, count * sizeof(*nodes));
|
||||
if (typed) memcpy(records, encoder->typed, typed * sizeof(*records));
|
||||
size_t first = 0;
|
||||
@@ -1014,6 +1001,28 @@ done:;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool scr_message_readable(const ScrMessage *message) { return message->readable; }
|
||||
|
||||
int32_t scr_message_kind(const ScrMessage *message, size_t node) { return message->nodes[node].kind; }
|
||||
|
||||
double scr_message_number(const ScrMessage *message, size_t node) { return message->nodes[node].v.number; }
|
||||
|
||||
ScrStr *scr_message_string(const ScrMessage *message, size_t node) {
|
||||
return scr_str_new(message->nodes[node].v.text.data, message->nodes[node].v.text.length);
|
||||
}
|
||||
|
||||
bool scr_message_fields(const ScrMessage *message, size_t node, const char *const *names,
|
||||
const size_t *lengths, size_t count, size_t *values) {
|
||||
const ScrMessageNode *object = &message->nodes[node];
|
||||
if (object->kind != SCR_MESSAGE_OBJECT || object->count != count) return false;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const ScrMessageProperty *property = &object->properties[i];
|
||||
if (property->length != lengths[i] || memcmp(property->key, names[i], lengths[i])) return false;
|
||||
values[i] = property->value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void scr_message_free(ScrMessage *message) {
|
||||
if (!message) return;
|
||||
if (message->releases)
|
||||
|
||||
@@ -250,6 +250,52 @@ static void check_large(void) {
|
||||
scr_message_free(message);
|
||||
}
|
||||
|
||||
/* The readers compiled 'message' listeners use: node kinds and values, a
|
||||
* record's keys in order, and which messages they may read. */
|
||||
static void check_readers(void) {
|
||||
ScrDyn *root = scr_dyn_new_obj();
|
||||
put_number(root, "id", 7);
|
||||
ScrStr *text = scr_str_new("seven", 5);
|
||||
scr_dyn_obj_set(root, "name", 4, scr_dyn_new_str(text));
|
||||
scr_str_release(text);
|
||||
scr_dyn_obj_set(root, "done", 4, scr_dyn_new_bool(true));
|
||||
ScrMessage *message = scr_message_encode(root);
|
||||
assert(message && scr_message_readable(message));
|
||||
assert(scr_message_kind(message, 0) == SCR_MESSAGE_OBJECT);
|
||||
static const char *const names[] = {"id", "name", "done"};
|
||||
static const size_t lengths[] = {2, 4, 4};
|
||||
size_t values[3];
|
||||
assert(scr_message_fields(message, 0, names, lengths, 3, values));
|
||||
assert(scr_message_kind(message, values[0]) == SCR_MESSAGE_NUMBER);
|
||||
assert(scr_message_number(message, values[0]) == 7);
|
||||
assert(scr_message_kind(message, values[1]) == SCR_MESSAGE_STRING);
|
||||
ScrStr *name = scr_message_string(message, values[1]);
|
||||
assert(name->len == 5 && !memcmp(name->data, "seven", 5));
|
||||
scr_str_release(name);
|
||||
assert(scr_message_kind(message, values[2]) == SCR_MESSAGE_BOOL);
|
||||
assert(scr_message_number(message, values[2]) == 1);
|
||||
/* Another order, a missing key or an extra key does not match. */
|
||||
static const char *const reordered[] = {"name", "id", "done"};
|
||||
static const size_t reordered_lengths[] = {4, 2, 4};
|
||||
assert(!scr_message_fields(message, 0, reordered, reordered_lengths, 3, values));
|
||||
assert(!scr_message_fields(message, 0, names, lengths, 2, values));
|
||||
assert(!scr_message_fields(message, values[0], names, lengths, 3, values));
|
||||
scr_message_free(message);
|
||||
put_number(root, "extra", 1);
|
||||
message = scr_message_encode(root);
|
||||
assert(!scr_message_fields(message, 0, names, lengths, 3, values));
|
||||
scr_message_free(message);
|
||||
/* A value reached twice makes the message unreadable. */
|
||||
ScrDyn *shared = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(shared));
|
||||
scr_dyn_obj_set(root, "second", 6, shared);
|
||||
message = scr_message_encode(root);
|
||||
assert(message && !scr_message_readable(message));
|
||||
scr_message_free(message);
|
||||
scr_dyn_release(root);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
/* A link-gated unit's handle crosses threads through the transfer hooks in
|
||||
* its ops: share on the sender, adopt on the receiver, drop with the
|
||||
* message. The test borrows the last capacity slot, which no unit uses, so
|
||||
@@ -376,6 +422,7 @@ int main(void) {
|
||||
check_rejection();
|
||||
check_documents();
|
||||
check_large();
|
||||
check_readers();
|
||||
check_handle_transfer();
|
||||
scr_context_cleanup();
|
||||
puts("message graphs preserve identity, sparse values, byte aliases and getter order");
|
||||
|
||||
@@ -665,6 +665,17 @@ static bool scr_channel_dispatch(void) {
|
||||
continue;
|
||||
}
|
||||
scr_worker_view_retain(view);
|
||||
if (event->message && !view->broadcast && scr_message_readable(event->message)) {
|
||||
/* Compiled listeners with a typed message entry read the message. */
|
||||
ScrStr *name = scr_str_new("message", 7);
|
||||
scr_dyn_this_push(view, SCR_DYNH_MESSAGE_PORT);
|
||||
scr_emitter_emit_message(view->events, name, event->message, scr_message_decode);
|
||||
scr_dyn_this_pop();
|
||||
scr_str_release(name);
|
||||
scr_mail_event_free(event);
|
||||
scr_worker_view_release(view);
|
||||
return true;
|
||||
}
|
||||
ScrDyn *argument = event->message ? scr_message_decode(event->message) : NULL;
|
||||
if (argument && view->broadcast) {
|
||||
ScrDyn *self = scr_dyn_new_handle(view, SCR_DYNH_BROADCAST_CHANNEL);
|
||||
@@ -815,6 +826,19 @@ static void scr_worker_dispatch(void) {
|
||||
}
|
||||
}
|
||||
scr_worker_view_retain(view);
|
||||
if (event->kind == SCR_WORKER_MESSAGE && event->message && scr_message_readable(event->message)) {
|
||||
/* Compiled listeners with a typed message entry read the message. */
|
||||
if (!scr_exc_pending()) {
|
||||
ScrStr *name = scr_str_new("message", 7);
|
||||
scr_dyn_this_push(view, view->port ? SCR_DYNH_MESSAGE_PORT : SCR_DYNH_WORKER);
|
||||
scr_emitter_emit_message(view->events, name, event->message, scr_message_decode);
|
||||
scr_dyn_this_pop();
|
||||
scr_str_release(name);
|
||||
}
|
||||
scr_worker_view_release(view);
|
||||
scr_mail_event_free(event);
|
||||
return;
|
||||
}
|
||||
const char *event_name = event->kind == SCR_WORKER_ONLINE ? "online" :
|
||||
event->kind == SCR_WORKER_EXIT ? "exit" : event->kind == SCR_WORKER_ERROR ? "error" : "message";
|
||||
ScrDyn *argument = event->message ? scr_message_decode(event->message) :
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Searches whose patterns begin with fixed sequences of characters and
|
||||
// classes (literal alternations, classes, fixed repetitions), over subjects
|
||||
// of every length from 0 to 80 and with matches at every offset: match,
|
||||
// matchAll, search, exec with captures, global and sticky lastIndex,
|
||||
// replace, case-insensitive letters, multiline anchors and word boundaries.
|
||||
// Results must not depend on where a match falls relative to 16-character
|
||||
// blocks or the end of the subject.
|
||||
const patterns: RegExp[] = [
|
||||
/agggtaaa|tttaccct/gi,
|
||||
/[cgt]gggtaaa|tttaccc[acg]/gi,
|
||||
/ab|abc|b/g,
|
||||
/(ab)(c|d)x/g,
|
||||
/x[0-9]{3}y/g,
|
||||
/(?:no|yes)(!+)/gi,
|
||||
/^ab/gm,
|
||||
/\bcat\b/g,
|
||||
/a{2}(b+)c/g,
|
||||
/[A-Z][a-z]{2}-[0-9]/g,
|
||||
];
|
||||
|
||||
function describe(re: RegExp, subject: string): string {
|
||||
const parts: string[] = [];
|
||||
const all = subject.match(re);
|
||||
parts.push(`match=${all === null ? "null" : JSON.stringify(all)}`);
|
||||
parts.push(`search=${subject.search(re)}`);
|
||||
const rows: string[] = [];
|
||||
for (const m of subject.matchAll(re)) rows.push(`${m.index}:${JSON.stringify(m.slice(0))}`);
|
||||
parts.push(`all=[${rows.join(" ")}]`);
|
||||
parts.push(`replace=${JSON.stringify(subject.replace(re, "<$&>"))}`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
const filler = "qwertyuiopasdfghjklzxcvbnm";
|
||||
const needles = ["agggtaaa", "TTTACCCT", "tttacccg", "abcx", "abdx", "x123y", "NO!!", "yes!", "cat", "aabbbc", "Abc-1", "ab"];
|
||||
let checksum = 0;
|
||||
let lines = 0;
|
||||
for (let length = 0; length <= 80; length += 7) {
|
||||
for (const needle of needles) {
|
||||
for (const offset of [0, 1, 14, 15, 16, 17, 31, 32, 33]) {
|
||||
if (offset + needle.length > length) continue;
|
||||
let subject = "";
|
||||
while (subject.length < length) subject += filler;
|
||||
subject = subject.slice(0, offset) + needle + subject.slice(offset + needle.length, length);
|
||||
for (const re of patterns) {
|
||||
const text = describe(re, subject);
|
||||
for (let i = 0; i < text.length; i++) checksum = (checksum * 31 + text.charCodeAt(i)) >>> 0;
|
||||
lines++;
|
||||
if (length === 35 && (offset === 15 || offset === 16)) console.log(re.source, offset, needle, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("lines", lines, "checksum", checksum);
|
||||
|
||||
// Overlapping candidates: every start position looks like a match for the
|
||||
// first characters, and only some complete.
|
||||
const dense = "agggtaa".repeat(9) + "agggtaaa" + "tttaccc".repeat(5) + "tttaccct";
|
||||
console.log(describe(/agggtaaa|tttaccct/g, dense));
|
||||
|
||||
// exec with lastIndex, global and sticky.
|
||||
const re = /x[0-9]{3}y/g;
|
||||
const text = "zzx111yzzzzzzzzzzzzzzzzzzx222yx333y" + "z".repeat(20) + "x444y";
|
||||
let m: RegExpExecArray | null;
|
||||
const found: string[] = [];
|
||||
while ((m = re.exec(text)) !== null) found.push(`${m[0]}@${m.index}->${re.lastIndex}`);
|
||||
console.log("exec", found.join(" "));
|
||||
const sticky = /x[0-9]{3}y/y;
|
||||
sticky.lastIndex = 2;
|
||||
console.log("sticky", sticky.test(text), sticky.lastIndex, sticky.test(text), sticky.lastIndex);
|
||||
|
||||
// Short subjects and the tail.
|
||||
for (const subject of ["", "a", "agggtaa", "agggtaaa", "zagggtaaa", "z".repeat(16) + "agggtaaa", "z".repeat(23) + "tttaccct"]) {
|
||||
console.log(JSON.stringify(subject.slice(-12)), subject.length, describe(/agggtaaa|tttaccct/gi, subject));
|
||||
}
|
||||
|
||||
// A long subject with matches spread through it.
|
||||
let long = "";
|
||||
for (let i = 0; i < 2000; i++) long += i % 97 === 0 ? "aGgGtAaA" : i % 89 === 0 ? "tttACCCt" : filler[i % filler.length];
|
||||
console.log("long", (long.match(/agggtaaa|tttaccct/gi) ?? []).length, long.search(/tttaccct/i));
|
||||
@@ -0,0 +1,78 @@
|
||||
// Typed-array loops whose index adds a loop-invariant number with no known
|
||||
// integer range, such as the row base of a matrix-vector product. Rows
|
||||
// with integer, fractional, negative, -0 and NaN bases, windows at 2^32 and
|
||||
// beyond, and matrices too short for their last rows, read and write as
|
||||
// Node does.
|
||||
|
||||
function matvec(w: Float32Array, v: Float32Array, out: Float32Array, cols: number): void {
|
||||
for (let r = 0; r < out.length; r++) {
|
||||
const base = r * cols;
|
||||
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
||||
for (let j = 0; j < cols; j += 4) {
|
||||
s0 += w[base + j]! * v[j]!;
|
||||
s1 += w[base + j + 1]! * v[j + 1]!;
|
||||
s2 += w[base + j + 2]! * v[j + 2]!;
|
||||
s3 += w[base + j + 3]! * v[j + 3]!;
|
||||
}
|
||||
out[r] = s0 + s1 + s2 + s3;
|
||||
}
|
||||
}
|
||||
|
||||
function rowSums(m: Float64Array, rows: number, cols: number, first: number): number[] {
|
||||
const sums: number[] = [];
|
||||
for (let r = first; r < first + rows; r++) {
|
||||
let s = 0;
|
||||
for (let c = 0; c < cols; c++) s += m[r * cols + c]!;
|
||||
sums.push(s);
|
||||
}
|
||||
return sums;
|
||||
}
|
||||
|
||||
function shiftedCopy(src: Int16Array, out: Int16Array, offset: number): void {
|
||||
for (let i = 0; i < out.length; i++) out[i] = src[i - offset]! + 1;
|
||||
}
|
||||
|
||||
function window(src: Uint8Array, start: number, k: number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < k; i++) s += src[start + i]! * (i + 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
const rows = 6, cols = 8;
|
||||
const w = new Float32Array(rows * cols);
|
||||
for (let i = 0; i < w.length; i++) w[i] = ((i * 37) % 23) / 4 - 2;
|
||||
const v = new Float32Array(cols);
|
||||
for (let i = 0; i < cols; i++) v[i] = (i % 5) - 1.5;
|
||||
const out = new Float32Array(rows);
|
||||
matvec(w, v, out, cols);
|
||||
console.log("matvec", out.join(","));
|
||||
const tall = new Float32Array(rows + 2);
|
||||
matvec(w, v, tall, cols);
|
||||
console.log("matvec past rows", tall.join(","));
|
||||
const odd = new Float32Array(3);
|
||||
matvec(w, v, odd, 6.5);
|
||||
console.log("matvec fractional cols", odd.join(","));
|
||||
matvec(w, v, odd, NaN);
|
||||
console.log("matvec NaN cols", odd.join(","));
|
||||
|
||||
const m = new Float64Array(30);
|
||||
for (let i = 0; i < m.length; i++) m[i] = i * 1.5 - 7;
|
||||
console.log("rows", rowSums(m, 5, 6, 0).join(","));
|
||||
console.log("rows from -1", rowSums(m, 3, 6, -1).join(","));
|
||||
console.log("rows fractional", rowSums(m, 2, 2.5, 1).join(","));
|
||||
console.log("rows past end", rowSums(m, 3, 6, 4).join(","));
|
||||
console.log("rows -0 base", rowSums(m, 1, 6, -0).join(","));
|
||||
|
||||
const src16 = new Int16Array([10, 20, 30, 40, 50, 60]);
|
||||
const shifted = new Int16Array(6);
|
||||
shiftedCopy(src16, shifted, 2);
|
||||
console.log("shifted", shifted.join(","));
|
||||
shiftedCopy(src16, shifted, -3);
|
||||
console.log("shifted back", shifted.join(","));
|
||||
shiftedCopy(src16, shifted, 0.5);
|
||||
console.log("shifted half", shifted.join(","));
|
||||
|
||||
const bytes = new Uint8Array(40);
|
||||
for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 29) & 255;
|
||||
console.log("window", window(bytes, 3, 8), window(bytes, 36, 8), window(bytes, -2, 4));
|
||||
console.log("window far", window(bytes, 2 ** 32, 3), window(bytes, 2 ** 32 + 1, 3), window(bytes, 2 ** 40, 2), window(bytes, -(2 ** 32), 2));
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MessageChannel } from "node:worker_threads";
|
||||
|
||||
// 'message' listeners with typed parameters on MessageChannel ports. A
|
||||
// message with the parameter's exact layout, and one with other keys,
|
||||
// shared values or other kinds, reach the listener with the same values as
|
||||
// in Node.
|
||||
type Item = { id: number; name: string; done: boolean };
|
||||
type Nested = { label: string; item: Item };
|
||||
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
// Each port's lines print in delivery order once every message arrived.
|
||||
const lines: string[] = [];
|
||||
const nestedLines: string[] = [];
|
||||
const otherLines: string[] = [];
|
||||
let pending = 0;
|
||||
function finish(): void {
|
||||
if (--pending === 0) {
|
||||
for (const line of [...lines, ...nestedLines, ...otherLines]) console.log(line);
|
||||
port1.close();
|
||||
channel.port1.close();
|
||||
other.port1.close();
|
||||
}
|
||||
}
|
||||
|
||||
port1.on("message", (value: number | string | Item | null | undefined) => {
|
||||
if (value === null || value === undefined) lines.push(`unit ${String(value)}`);
|
||||
else if (typeof value === "number") lines.push(`number ${value.toFixed(2)}`);
|
||||
else if (typeof value === "string") lines.push(`string ${value.length} ${value}`);
|
||||
else lines.push(`item ${value.id} ${value.name} ${value.done} ${JSON.stringify(value)}`);
|
||||
finish();
|
||||
});
|
||||
|
||||
const sent: unknown[] = [
|
||||
1.5,
|
||||
-0,
|
||||
"",
|
||||
"héllo wörld",
|
||||
null,
|
||||
undefined,
|
||||
{ id: 1, name: "first", done: false },
|
||||
{ id: 2, name: "", done: true },
|
||||
// An extra key stays beside the record.
|
||||
{ id: 4, name: "extra", done: false, note: "kept" },
|
||||
];
|
||||
pending += sent.length;
|
||||
for (const value of sent) port2.postMessage(value);
|
||||
|
||||
// Nested records, a value shared by two fields, and a second listener
|
||||
// that reads the message untyped.
|
||||
const channel = new MessageChannel();
|
||||
channel.port1.on("message", (value: Nested) => {
|
||||
nestedLines.push(`nested ${value.label} ${value.item.id} ${value.item.name} ${JSON.stringify(value)}`);
|
||||
finish();
|
||||
});
|
||||
channel.port1.on("message", (value: unknown) => {
|
||||
nestedLines.push(`untyped ${JSON.stringify(value)}`);
|
||||
finish();
|
||||
});
|
||||
const shared = { id: 5, name: "shared", done: true };
|
||||
const nested: unknown[] = [
|
||||
{ label: "plain", item: { id: 6, name: "six", done: false } },
|
||||
{ label: "shared", item: shared, again: shared },
|
||||
];
|
||||
pending += 2 * nested.length;
|
||||
for (const value of nested) channel.port2.postMessage(value);
|
||||
|
||||
// once listeners, and a listener added while the port delivers.
|
||||
const other = new MessageChannel();
|
||||
other.port1.once("message", (value: Item) => {
|
||||
otherLines.push(`once ${value.id} ${value.name}`);
|
||||
other.port1.on("message", (later: number) => {
|
||||
otherLines.push(`later ${later}`);
|
||||
finish();
|
||||
});
|
||||
finish();
|
||||
});
|
||||
pending += 3;
|
||||
other.port2.postMessage({ id: 7, name: "seven", done: true });
|
||||
other.port2.postMessage(8);
|
||||
other.port2.postMessage(9);
|
||||
@@ -0,0 +1,54 @@
|
||||
import { MessageChannel, MessagePort, Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
// 'message' listeners with typed parameters on a worker and its
|
||||
// parentPort: records and numbers in both directions, and a message that
|
||||
// carries a transferred MessagePort.
|
||||
type Job = { id: number; text: string; urgent: boolean };
|
||||
type Reply = { id: number; length: number; upper: string };
|
||||
|
||||
if (isMainThread) {
|
||||
const worker = new Worker(new URL(import.meta.url));
|
||||
// The worker's replies, then the port's, each in delivery order.
|
||||
const replies: string[] = [];
|
||||
const portLines: string[] = [];
|
||||
let expected = 5;
|
||||
const done = (): void => {
|
||||
if (--expected > 0) return;
|
||||
for (const line of [...replies, ...portLines]) console.log(line);
|
||||
void worker.terminate();
|
||||
};
|
||||
worker.on("message", (reply: Reply | number) => {
|
||||
if (typeof reply === "number") replies.push(`count ${reply}`);
|
||||
else replies.push(`reply ${reply.id} ${reply.length} ${reply.upper}`);
|
||||
done();
|
||||
});
|
||||
const jobs: Job[] = [
|
||||
{ id: 1, text: "alpha", urgent: false },
|
||||
{ id: 2, text: "βeta", urgent: true },
|
||||
{ id: 3, text: "", urgent: false },
|
||||
];
|
||||
for (const job of jobs) worker.postMessage(job);
|
||||
worker.postMessage(jobs.length);
|
||||
// A port in the message: the worker answers through it.
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
port1.on("message", (text: string) => {
|
||||
portLines.push(`port ${text}`);
|
||||
port1.close();
|
||||
done();
|
||||
});
|
||||
worker.postMessage({ port: port2 }, [port2]);
|
||||
} else {
|
||||
let count = 0;
|
||||
parentPort!.on("message", (message: Job | number | { port: MessagePort }) => {
|
||||
if (typeof message === "number") {
|
||||
parentPort!.postMessage(count + message);
|
||||
} else if ("port" in message) {
|
||||
message.port.postMessage("through the port");
|
||||
message.port.close();
|
||||
} else {
|
||||
count++;
|
||||
const text = message.urgent ? message.text.toUpperCase() : message.text;
|
||||
parentPort!.postMessage({ id: message.id, length: message.text.length, upper: text });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// For loops with a second counter that steps beside the tested one
|
||||
// (`i++, j += 4`): pixel strides over typed arrays, interleaving into number
|
||||
// arrays, descending and inclusive loops, three counters, early exits,
|
||||
// starts and limits that are fractions, negative zero, NaN, infinite or
|
||||
// near 2^53, counters that live across a yield or an await, and counters a
|
||||
// closure captures, the body writes or the condition reads.
|
||||
function luminance(px: Uint8Array, count: number): number[] {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, p = 0; i < count; i++, p += 4) {
|
||||
out.push((px[p]! * 77 + px[p + 1]! * 150 + px[p + 2]! * 29) >> 8);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const px = new Uint8Array(32);
|
||||
for (let k = 0; k < px.length; k++) px[k] = (k * 37 + 11) & 255;
|
||||
console.log(luminance(px, 8).join(","));
|
||||
// Past the end, reads are undefined and the sums NaN, which `>> 8` makes 0.
|
||||
console.log(luminance(px, 10).join(","));
|
||||
console.log(luminance(px, 0).length, luminance(px, -3).length, luminance(px, 2.5).join(","));
|
||||
console.log(luminance(px, NaN).length);
|
||||
|
||||
function interleave(a: number[], b: number[]): number[] {
|
||||
const out: number[] = new Array(a.length * 2).fill(-1);
|
||||
const n = a.length;
|
||||
for (let i = 0, j = 0; i < n; i++, j += 2) {
|
||||
out[j] = a[i]!;
|
||||
out[j + 1] = b[i]!;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
console.log(interleave([1, 2, 3, 4], [10, 20, 30, 40]).join(","));
|
||||
console.log(interleave([], []).length);
|
||||
|
||||
// The second counter's start is not proven: charCodeAt truncates its index,
|
||||
// and the counter's own values keep JS semantics.
|
||||
function codes(s: string, count: number, from: number, step: number): string {
|
||||
const out: (number | string)[] = [];
|
||||
if (step === 3) {
|
||||
for (let i = 0, j = from; i < count; i++, j += 3) out.push(s.charCodeAt(j), j);
|
||||
} else {
|
||||
for (let i = 0, j = from; i < count; i++, j -= 2) out.push(s.charCodeAt(j), j);
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
const word = "abcdefghijklmnop";
|
||||
console.log(codes(word, 4, 0, 3), "|", codes(word, 4, 15, -2));
|
||||
console.log(codes(word, 4, 0.5, 3), "|", codes(word, 3, 9.75, -2));
|
||||
const negativeZero = -0;
|
||||
console.log(codes(word, 2, negativeZero, 3), Object.is(negativeZero, -0));
|
||||
console.log(codes(word, 2, NaN, 3), "|", codes(word, 2, Infinity, -2), "|", codes(word, 2, -Infinity, 3));
|
||||
// Near 2^52 the run-time test passes or fails on the trip count; near 2^53
|
||||
// the double counter rounds (odd values above 2^53 are not representable).
|
||||
console.log(codes(word, 4, 2 ** 52 - 16, 3));
|
||||
console.log(codes(word, 6, 2 ** 52 - 4, 3));
|
||||
console.log(codes(word, 6, 2 ** 53 - 10, 3));
|
||||
console.log(codes(word, 4, -(2 ** 53) + 3, -2));
|
||||
// A limit that is not finite or not an integer.
|
||||
function capped(s: string, limit: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, j = 1; i < limit; i++, j += 2) {
|
||||
if (j >= s.length) break;
|
||||
out.push(s.charCodeAt(j));
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(capped(word, Infinity), "|", capped(word, 3.5), "|", capped(word, NaN), "|", capped(word, -Infinity));
|
||||
|
||||
// A descending driver with an ascending follower, inclusive bounds and
|
||||
// larger steps.
|
||||
function mirror(s: string): string {
|
||||
const out: string[] = [];
|
||||
for (let i = s.length - 1, j = 0; i >= 0; i--, j += 2) out.push(`${s.charCodeAt(i)}@${j}`);
|
||||
return out.join(" ");
|
||||
}
|
||||
console.log(mirror("scriptc"));
|
||||
function stepped(bytes: Uint8Array, last: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, j = bytes.length - 1; i <= last; i += 3, j -= 5)
|
||||
out.push(bytes[i]! | 0, bytes[j]! | 0, j);
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(stepped(px, 9), "|", stepped(px, 30));
|
||||
|
||||
// Three counters, continue and break.
|
||||
function triple(a: Uint8Array, n: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = 0, j = 10, k = 100; i < n; i++, j -= 1, k += 7) {
|
||||
if (i % 3 === 1) continue;
|
||||
if (k > 150) break;
|
||||
out.push(a[i]!, a[j]!, k);
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(triple(px, 12), "|", triple(px, 4));
|
||||
|
||||
// A counted limit with a known range and a follower used by a remainder.
|
||||
let rotated = 0;
|
||||
for (let i = 0, j = 5; i < 1000; i++, j += 3) rotated = (rotated * 31 + px[j % 32]!) % 1000003;
|
||||
console.log(rotated);
|
||||
|
||||
// The follower starting from a driver's start of unproven exactness.
|
||||
function window(bytes: Uint8Array, from: number, to: number): string {
|
||||
const out: number[] = [];
|
||||
for (let i = from, j = 0; i < to; i++, j += 2) out.push(bytes[i]! | 0, bytes[j]! | 0, i, j);
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(window(px, 2, 5), "|", window(px, 1.5, 4), "|", window(px, -0, 2));
|
||||
|
||||
// Counters a closure captures keep one binding per iteration; a follower the
|
||||
// body writes, one the condition reads, and a start that reads another
|
||||
// counter keep their JS values.
|
||||
const reads: (() => number)[] = [];
|
||||
for (let i = 0, j = 0; i < 3; i++, j += 4) {
|
||||
reads.push(() => j);
|
||||
void px[j];
|
||||
}
|
||||
console.log(reads.map((read) => read()).join(","));
|
||||
const written: number[] = [];
|
||||
for (let i = 0, j = 0; i < 5; i++, j += 2) {
|
||||
if (i === 2) j += 7;
|
||||
written.push(px[j]!, j);
|
||||
}
|
||||
console.log(written.join(","));
|
||||
const chased: number[] = [];
|
||||
for (let i = 0, j = 5; i < j; i += 2, j++) chased.push(px[i]!, i, j);
|
||||
console.log(chased.join(","));
|
||||
const offset: number[] = [];
|
||||
for (let i = 1, j = i + 1; i < 4; i++, j += 2) offset.push(px[j]!, j);
|
||||
console.log(offset.join(","));
|
||||
|
||||
// Counters that live across a yield or an await.
|
||||
function* strided(bytes: Uint8Array, n: number): Generator<number> {
|
||||
for (let i = 0, j = 1; i < n; i++, j += 3) yield bytes[j]! + i;
|
||||
}
|
||||
console.log([...strided(px, 6)].join(","));
|
||||
async function weighted(bytes: Uint8Array, n: number): Promise<number> {
|
||||
let sum = 0;
|
||||
for (let i = 0, j = 2; i < n; i++, j += 5) {
|
||||
await null;
|
||||
sum += bytes[j]! * (i + 1);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
void (async () => console.log(await weighted(px, 6), await weighted(px, 3)))();
|
||||
@@ -2,7 +2,8 @@
|
||||
// and Writable in pipeline(), typed 'data' listeners, a Transform with
|
||||
// readableObjectMode parsing bytes into records, an object-mode Duplex,
|
||||
// a PassThrough's object-counted high-water mark, and Readable.from over
|
||||
// a Map, a Set of records and an array mixing strings and Buffers.
|
||||
// a Map, a Set of records and an array mixing strings and Buffers, and
|
||||
// byte-mode chunk parameters typed Buffer | string.
|
||||
import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
|
||||
@@ -119,6 +120,34 @@ async function collections(): Promise<void> {
|
||||
for await (const t of Readable.from(tags)) console.log("set", JSON.stringify(t));
|
||||
}
|
||||
|
||||
// Byte-mode sides pass Buffers: a chunk parameter typed as a supertype
|
||||
// of Buffer takes them, inline and as an underscore method, and
|
||||
// super(undefined) leaves the stream in byte mode.
|
||||
class Collector extends Writable {
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
_write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
|
||||
console.log("collector", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString());
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
async function byteChunks(): Promise<void> {
|
||||
const inline = new Writable({
|
||||
write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
|
||||
console.log("inline", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString());
|
||||
cb();
|
||||
},
|
||||
});
|
||||
inline.write("one");
|
||||
inline.end(Buffer.from("two"));
|
||||
await new Promise<void>((resolve) => inline.on("finish", () => resolve()));
|
||||
const collector = new Collector();
|
||||
collector.end("three");
|
||||
await new Promise<void>((resolve) => collector.on("finish", () => resolve()));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await records();
|
||||
await collections();
|
||||
@@ -126,5 +155,7 @@ async function main(): Promise<void> {
|
||||
sided();
|
||||
duplex();
|
||||
passthrough();
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
||||
await byteChunks();
|
||||
}
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Callbacks that update captured `let` numbers and booleans from loops: typed
|
||||
// array and array methods, an authored higher-order function called with one
|
||||
// function at several calls and with two functions at one, closures of one
|
||||
// function over different bindings, a callback that throws part way, nested
|
||||
// callbacks, callbacks declared in the loop body, and reads after the loop.
|
||||
|
||||
function each(values: number[], visit: (v: number, i: number) => void): void {
|
||||
for (let i = 0; i < values.length; i++) visit(values[i]!, i);
|
||||
}
|
||||
|
||||
function until(limit: number, step: (i: number) => boolean): number {
|
||||
let i = 0;
|
||||
while (i < limit) {
|
||||
if (!step(i)) break;
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function twice(values: Int16Array, f: (v: number) => void): void {
|
||||
let i = 0;
|
||||
do {
|
||||
for (let j = 0; j < values.length; j++) f(values[j]!);
|
||||
i++;
|
||||
} while (i < 2);
|
||||
}
|
||||
|
||||
const ints = new Int32Array([5, -3, 8, 0, 12, -7, 4]);
|
||||
let total = 0;
|
||||
let negatives = 0;
|
||||
let sawZero = false;
|
||||
ints.forEach((v) => {
|
||||
total = (total + v) | 0;
|
||||
if (v < 0) negatives++;
|
||||
if (v === 0) sawZero = true;
|
||||
});
|
||||
console.log("forEach", total, negatives, sawZero);
|
||||
|
||||
let max = -Infinity;
|
||||
let maxAt = -1;
|
||||
const floats = new Float64Array([0.5, 9.25, -2, 9.25, 3]);
|
||||
floats.forEach((v, i) => {
|
||||
if (v > max) {
|
||||
max = v;
|
||||
maxAt = i;
|
||||
}
|
||||
});
|
||||
console.log("max", max, maxAt);
|
||||
|
||||
let calls = 0;
|
||||
const doubled = ints.map((v) => {
|
||||
calls++;
|
||||
return v * 2 + calls;
|
||||
});
|
||||
console.log("map", doubled.join(","), calls);
|
||||
|
||||
let seen = 0;
|
||||
const big = [3, 14, 15, 92, 65].filter((v) => {
|
||||
seen += v;
|
||||
return v > 10;
|
||||
});
|
||||
console.log("filter", big.join(","), seen);
|
||||
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
const summing = (v: number): void => {
|
||||
sum += v;
|
||||
count++;
|
||||
};
|
||||
each([1, 2, 3], summing);
|
||||
each([10, 20], summing);
|
||||
console.log("each shared", sum, count);
|
||||
|
||||
let a = 0;
|
||||
let b = 0;
|
||||
each([1, 2], (v) => {
|
||||
a += v;
|
||||
});
|
||||
each([1, 2], (v, i) => {
|
||||
b += v * (i + 1);
|
||||
});
|
||||
console.log("each two", a, b);
|
||||
|
||||
const counters: (() => number)[] = [];
|
||||
for (let k = 0; k < 3; k++) {
|
||||
let local = k * 100;
|
||||
each([1, 1, 1], () => {
|
||||
local++;
|
||||
});
|
||||
counters.push(() => local);
|
||||
}
|
||||
console.log("closures", counters.map((c) => c()).join(","));
|
||||
|
||||
let steps = 0;
|
||||
console.log("until", until(10, (i) => (steps += i, steps < 12)), steps);
|
||||
|
||||
let shorts = 0;
|
||||
twice(new Int16Array([7, -8, 9]), (v) => {
|
||||
shorts = shorts * 2 + v;
|
||||
});
|
||||
console.log("twice", shorts);
|
||||
|
||||
let partial = 0;
|
||||
try {
|
||||
floats.forEach((v) => {
|
||||
if (v < 0) throw new RangeError(`at ${partial}`);
|
||||
partial += v;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("throw", (error as Error).message, partial);
|
||||
}
|
||||
|
||||
let outer = 0;
|
||||
let inner = 0;
|
||||
[1, 2, 3].forEach((x) => {
|
||||
outer += x;
|
||||
ints.forEach((y) => {
|
||||
if (y > x) inner++;
|
||||
});
|
||||
});
|
||||
console.log("nested", outer, inner);
|
||||
|
||||
let flag = false;
|
||||
let flips = 0;
|
||||
new Uint8Array([1, 0, 1, 1]).forEach((v) => {
|
||||
if (v === 1) flag = !flag;
|
||||
if (flag) flips++;
|
||||
});
|
||||
console.log("bool", flag, flips);
|
||||
|
||||
// Callbacks declared in the loop body: each iteration binds its own.
|
||||
let strides = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const bump = (): void => {
|
||||
i += 2;
|
||||
strides++;
|
||||
};
|
||||
bump();
|
||||
}
|
||||
let countdown = 0;
|
||||
let k = 3;
|
||||
while (k > 0) {
|
||||
const tick = (n: number): number => (n > 0 ? tick(n - 1) + 1 : 0);
|
||||
countdown += tick(k);
|
||||
k--;
|
||||
}
|
||||
console.log("declared in loop", strides, countdown);
|
||||
@@ -0,0 +1,111 @@
|
||||
// Speculative integer loops whose checks fail: on the first statement of
|
||||
// every entry (fractional field data), after an effect earlier in the same
|
||||
// iteration, before a `continue`, `break` or `return`, and on entries
|
||||
// that alternate between integer and fractional data. A site that keeps
|
||||
// failing stops trying its integer copy; every result is Node's.
|
||||
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
w = 0;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
function show(label: string, value: number): void {
|
||||
console.log(label, Object.is(value, -0) ? "-0" : String(value));
|
||||
}
|
||||
|
||||
// Each inner entry fails at its first statement when the fields hold
|
||||
// fractions, and runs no iteration at all for the last point.
|
||||
function pairs(points: Point[]): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const p = points[i]!;
|
||||
let acc = p.w;
|
||||
for (let j = i + 1; j < points.length; j++) {
|
||||
const q = points[j]!;
|
||||
const dx = p.x - q.x;
|
||||
const dy = p.y - q.y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
acc += d2;
|
||||
q.w += d2 * 0.5;
|
||||
}
|
||||
p.w = acc;
|
||||
total += acc;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const fractional = [
|
||||
new Point(0.5, 1),
|
||||
new Point(1.25, -2),
|
||||
new Point(3, 4.75),
|
||||
new Point(-1.5, 0),
|
||||
];
|
||||
const integral = [new Point(0, 1), new Point(2, -2), new Point(3, 4), new Point(-1, 0)];
|
||||
for (let round = 0; round < 12; round++) {
|
||||
show(`pairs fractional ${round}`, pairs(fractional));
|
||||
}
|
||||
// The same site, now with integer data after it stopped speculating.
|
||||
show("pairs integral", pairs(integral));
|
||||
show("pairs integral again", pairs(integral));
|
||||
|
||||
// A field write comes before the failing statement: the iteration resumes
|
||||
// after it and must not repeat it.
|
||||
function afterEffect(points: Point[], scale: number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const p = points[i]!;
|
||||
p.w += 1;
|
||||
const k = p.x * scale;
|
||||
s += k * k;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
const marks = [new Point(1, 0), new Point(2, 0), new Point(3, 0)];
|
||||
show("after effect 2", afterEffect(marks, 2));
|
||||
show("after effect 0.5", afterEffect(marks, 0.5));
|
||||
show("after effect 3", afterEffect(marks, 3));
|
||||
console.log(marks.map((p) => p.w).join(" "));
|
||||
|
||||
// The failing statement is followed by `continue`, `break` and `return`.
|
||||
function control(n: number, step: number): number {
|
||||
let acc = 0;
|
||||
let i = 0;
|
||||
while (i < n) {
|
||||
const v = i * step;
|
||||
i++;
|
||||
if (v * v > 400) break;
|
||||
if ((i & 1) === 0) continue;
|
||||
acc += v * 3;
|
||||
if (acc > 1000) return -acc;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
for (const step of [1, 0.5, 2, 0.75, 7, 1.5, 3, 0.25, 5, 1.75, 4]) {
|
||||
show(`control ${step}`, control(40, step));
|
||||
}
|
||||
|
||||
// do-while: the check fails part-way through, on some entries only.
|
||||
function steps(start: number, factor: number): number {
|
||||
let v = start;
|
||||
let k = 0;
|
||||
do {
|
||||
v = v * factor - k;
|
||||
k++;
|
||||
} while (k < 6);
|
||||
return v;
|
||||
}
|
||||
for (let r = 0; r < 20; r++) show(`steps ${r}`, steps(r % 3 === 0 ? r + 0.5 : r, 3));
|
||||
|
||||
// Overflow past 2^53 on later entries only.
|
||||
function powers(base: number, n: number): number {
|
||||
let p = 1;
|
||||
for (let i = 0; i < n; i++) p = p * base;
|
||||
return p;
|
||||
}
|
||||
for (const n of [3, 10, 40, 80, 5, 60, 2, 70, 1, 90, 0, 4]) show(`powers ${n}`, powers(3, n));
|
||||
@@ -0,0 +1,45 @@
|
||||
import { MessageChannel, type MessagePort } from "node:worker_threads";
|
||||
|
||||
// Listeners of one message share its value, as in Node: a typed listener
|
||||
// that runs after an untyped one sees the changes the untyped one made,
|
||||
// and a listener on a discriminated union reads the arm its discriminator
|
||||
// names.
|
||||
type Item = { id: number; name: string };
|
||||
type Shape = { kind: "circle"; size: number } | { kind: "square"; size: number; corner: string };
|
||||
|
||||
const lines: string[] = [];
|
||||
let pending = 0;
|
||||
const ports: MessagePort[] = [];
|
||||
function finish(): void {
|
||||
if (--pending > 0) return;
|
||||
for (const line of lines) console.log(line);
|
||||
for (const port of ports) port.close();
|
||||
}
|
||||
|
||||
const shared = new MessageChannel();
|
||||
ports.push(shared.port1);
|
||||
shared.port1.on("message", (value: Item) => {
|
||||
lines.push(`typed first ${value.id} ${value.name}`);
|
||||
});
|
||||
shared.port1.on("message", (value: any) => {
|
||||
value.id = value.id * 10;
|
||||
value.name = `${value.name}!`;
|
||||
});
|
||||
shared.port1.on("message", (value: Item) => {
|
||||
lines.push(`typed after ${value.id} ${value.name}`);
|
||||
finish();
|
||||
});
|
||||
pending += 2;
|
||||
shared.port2.postMessage({ id: 1, name: "a" });
|
||||
shared.port2.postMessage({ id: 2, name: "b" });
|
||||
|
||||
const shapes = new MessageChannel();
|
||||
ports.push(shapes.port1);
|
||||
shapes.port1.on("message", (shape: Shape) => {
|
||||
if (shape.kind === "circle") lines.push(`circle ${shape.size}`);
|
||||
else lines.push(`square ${shape.size} ${shape.corner}`);
|
||||
finish();
|
||||
});
|
||||
pending += 2;
|
||||
shapes.port2.postMessage({ kind: "circle", size: 3 });
|
||||
shapes.port2.postMessage({ kind: "square", size: 4, corner: "round" });
|
||||
@@ -0,0 +1,26 @@
|
||||
// Methods whose quoted names begin with "get:" or "set:", next to an
|
||||
// accessor of the property after the prefix, read and called through
|
||||
// untyped values.
|
||||
class Quoted {
|
||||
"get:size"() {
|
||||
return "method get:size";
|
||||
}
|
||||
"set:size"(value) {
|
||||
return `method set:size ${value}`;
|
||||
}
|
||||
get size() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
class Sized {
|
||||
constructor() {
|
||||
this.size = 5;
|
||||
}
|
||||
}
|
||||
const values = [new Quoted(), new Sized(), { "get:size": () => "record get:size", size: 1 }];
|
||||
for (const value of values) {
|
||||
const getter = value["get:size"];
|
||||
console.log(typeof getter, getter?.call(value));
|
||||
console.log(typeof value["set:size"], value.size);
|
||||
if (value instanceof Quoted) console.log(value["get:size"](), value["set:size"](7));
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// A byte-mode stream's write chunks are Buffers: an underscore method
|
||||
// whose chunk parameter has another type is fenced, and super(undefined)
|
||||
// passes no options, so the stream is in byte mode. A parameter that
|
||||
// admits the Buffer (Buffer | string) is not fenced, inline or as a
|
||||
// method.
|
||||
import { Writable } from "node:stream";
|
||||
|
||||
class Numbers extends Writable {
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
_write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
|
||||
console.log(chunk);
|
||||
cb();
|
||||
}
|
||||
}
|
||||
new Numbers().end("x");
|
||||
|
||||
const accepted = new Writable({
|
||||
write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
|
||||
console.log(typeof chunk === "string" ? chunk : chunk.toString());
|
||||
cb();
|
||||
},
|
||||
});
|
||||
accepted.end("z");
|
||||
|
||||
class Wide extends Writable {
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
_write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
|
||||
console.log(typeof chunk === "string" ? chunk : chunk.toString());
|
||||
cb();
|
||||
}
|
||||
}
|
||||
new Wide().end("w");
|
||||
@@ -0,0 +1,8 @@
|
||||
stream-byte-mode-chunk-fences.ts:10:5 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
9 | constructor() {
|
||||
10 | super(undefined);
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
11 | }
|
||||
|
||||
hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here)
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2430, "san": 8860 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3400, "san": 4800 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2200, "san": 2200 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1700, "san": 1600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4260, "san": 6600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6000, "san": 7900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 2600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 4600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3900, "san": 4000 }
|
||||
@@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
Reference in New Issue
Block a user