Merge main into EventTarget

This commit is contained in:
Chris Tate
2026-10-14 08:03:27 -05:00
121 changed files with 10320 additions and 350 deletions
+1
View File
@@ -14,6 +14,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `word-graph` | cpu | string-keyed Map/Set, BFS |
| `alloc-trees` | cpu | allocation-heavy trees, immutable object-spread updates |
| `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` |
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
+7
View File
@@ -70,6 +70,13 @@
"0.5"
]
},
{
"name": "value-objects",
"entry": "workloads/value-objects.ts",
"args": [
"2"
]
},
{
"name": "validate-errors",
"entry": "workloads/validate-errors.ts",
@@ -0,0 +1,132 @@
// Small immutable value objects: Vec3 records built by helper functions,
// a Complex class whose methods return new instances, and an immutable
// particle update. Most temporaries never leave the expression that
// creates them.
interface Vec3 {
x: number;
y: number;
z: number;
}
function vec(x: number, y: number, z: number): Vec3 {
return { x, y, z };
}
function add(a: Vec3, b: Vec3): Vec3 {
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
}
function sub(a: Vec3, b: Vec3): Vec3 {
return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
}
function scale(a: Vec3, k: number): Vec3 {
return { x: a.x * k, y: a.y * k, z: a.z * k };
}
function dot(a: Vec3, b: Vec3): number {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
function cross(a: Vec3, b: Vec3): Vec3 {
return { x: a.y * b.z - a.z * b.y, y: a.z * b.x - a.x * b.z, z: a.x * b.y - a.y * b.x };
}
function normalize(a: Vec3): Vec3 {
const len = Math.sqrt(dot(a, a));
return len === 0 ? a : scale(a, 1 / len);
}
interface Sphere {
center: Vec3;
radius: number;
shade: number;
}
function hit(origin: Vec3, dir: Vec3, sphere: Sphere): number {
const oc = sub(origin, sphere.center);
const b = dot(oc, dir);
const c = dot(oc, oc) - sphere.radius * sphere.radius;
const disc = b * b - c;
if (disc < 0) return -1;
const t = -b - Math.sqrt(disc);
return t > 0 ? t : -1;
}
function trace(width: number, height: number, spheres: Sphere[]): number {
const eye = vec(0, 0, -4);
const light = normalize(vec(-1, 1, -1));
let total = 0;
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const dir = normalize(vec((px - width / 2) / width, (py - height / 2) / height, 1));
let nearest = Infinity;
let shade = 0;
for (const sphere of spheres) {
const t = hit(eye, dir, sphere);
if (t > 0 && t < nearest) {
nearest = t;
const point = add(eye, scale(dir, t));
const normal = normalize(sub(point, sphere.center));
shade = sphere.shade * Math.max(0, dot(normal, light));
}
}
total += shade;
}
}
return total;
}
class Complex {
re: number;
im: number;
constructor(re: number, im: number) {
this.re = re;
this.im = im;
}
plus(o: Complex): Complex {
return new Complex(this.re + o.re, this.im + o.im);
}
times(o: Complex): Complex {
return new Complex(this.re * o.re - this.im * o.im, this.re * o.im + this.im * o.re);
}
norm(): number {
return this.re * this.re + this.im * this.im;
}
}
function mandelbrot(size: number, limit: number): number {
let inside = 0;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const c = new Complex((x / size) * 3 - 2, (y / size) * 3 - 1.5);
let z = new Complex(0, 0);
let n = 0;
while (n < limit && z.norm() <= 4) {
z = z.times(z).plus(c);
n++;
}
if (n === limit) inside++;
}
}
return inside;
}
function particles(count: number, steps: number): number {
const positions: Vec3[] = [];
const velocities: Vec3[] = [];
for (let i = 0; i < count; i++) {
positions.push(vec(i % 7, (i * 3) % 11, (i * 5) % 13));
velocities.push(vec(((i % 5) - 2) / 10, ((i % 3) - 1) / 10, 0.05));
}
const gravity = vec(0, -0.01, 0);
for (let s = 0; s < steps; s++) {
for (let i = 0; i < count; i++) {
const v = add(velocities[i]!, gravity);
velocities[i] = v;
positions[i] = add(positions[i]!, scale(v, 0.5));
}
}
let sum = 0;
for (const p of positions) sum += dot(p, cross(p, vec(1, 2, 3))) + p.y;
return sum;
}
const scaleArg = Number(process.argv[2] ?? "1");
const spheres: Sphere[] = [];
for (let i = 0; i < 8; i++)
spheres.push({ center: vec((i % 4) - 1.5, Math.floor(i / 4) - 0.5, 2 + (i % 3)), radius: 0.45, shade: 1 + i / 8 });
const image = trace(Math.floor(320 * scaleArg), Math.floor(240 * scaleArg), spheres);
const fractal = mandelbrot(Math.floor(220 * scaleArg), 200);
const motion = particles(Math.floor(2000 * scaleArg), 200);
console.log("trace", image.toFixed(6), "mandelbrot", fractal, "particles", motion.toFixed(6));
+1 -1
View File
@@ -70,7 +70,7 @@ Without `-o`, artifacts are written to `.scriptc/` next to the input. Different
### External linking
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v19` checks runtime compatibility at link time.
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v20` checks runtime compatibility at link time.
`--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "ad22ef7f9c0a3e2d2f5a",
"artifactVersion": "e82ab018313a4ad5b14f",
"rowCount": 3662
}
@@ -509,9 +509,9 @@
"apiEntries": 17,
"static": {
"supported": 0,
"partial": 16,
"partial": 17,
"refused": 0,
"not-implemented": 1,
"not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -50077,9 +50077,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
+1 -1
View File
@@ -15,4 +15,4 @@ $ ./app-ld
`cc` uses the C compiler as a linker driver. `ld` links the same precompiled runtime objects directly with the selected Apple SDK. The object defines `main`; it is a complete program object, not a library to load into another process. Use `scriptc build --lib --profile ...` for a host-callable static library.
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v19`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v20`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 40,
"verify-gap": 2354,
"audit-partial": 794,
"verify-gap": 2353,
"audit-partial": 795,
"classify": 2,
"implement": 80
}
@@ -49144,12 +49144,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:path",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.path.matchesGlob",
"tests": [
"tests/corpus/5145-path-matches-glob-basics.ts",
"tests/corpus/5146-path-matches-glob-braces.ts",
"tests/corpus/5147-path-matches-glob-extglob.ts",
"tests/corpus/5148-path-matches-glob-classes.ts",
"packages/runtime/test/glob.test.ts"
]
}
}
},
@@ -536,9 +536,9 @@
"apiEntries": 17,
"static": {
"supported": 0,
"partial": 16,
"partial": 17,
"refused": 0,
"not-implemented": 1,
"not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -69616,12 +69616,17 @@
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"packages/runtime/test/glob.test.ts",
"packages/runtime/test/path.test.ts",
"tests/corpus/2915-path-function-values.ts",
"tests/corpus/2916-path-function-values-cjs.ts",
"tests/corpus/2917-builtin-callable-widths.ts",
"tests/corpus/2918-builtin-callable-widths-cjs.ts",
"tests/corpus/5142-path-parse-format.ts",
"tests/corpus/5145-path-matches-glob-basics.ts",
"tests/corpus/5146-path-matches-glob-braces.ts",
"tests/corpus/5147-path-matches-glob-extglob.ts",
"tests/corpus/5148-path-matches-glob-classes.ts",
"tests/corpus/957-builtins-namespace.ts"
]
},
@@ -69785,8 +69790,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:path"
"status": "partial",
"evidence": "surface-manifest:node-builtin.path.matchesGlob",
"tests": [
"tests/corpus/5145-path-matches-glob-basics.ts",
"tests/corpus/5146-path-matches-glob-braces.ts",
"tests/corpus/5147-path-matches-glob-extglob.ts",
"tests/corpus/5148-path-matches-glob-classes.ts",
"packages/runtime/test/glob.test.ts"
]
},
"anchorSource": "exact"
},
@@ -100,6 +100,9 @@
"node-builtin.path.posix.format": ["tests/corpus/5142-path-parse-format.ts", "packages/runtime/test/path.test.ts"],
"node-builtin.path.win32.parse": ["tests/corpus/5142-path-parse-format.ts", "packages/runtime/test/path.test.ts"],
"node-builtin.path.win32.format": ["tests/corpus/5142-path-parse-format.ts", "packages/runtime/test/path.test.ts"],
"node-builtin.path.matchesGlob": ["tests/corpus/5145-path-matches-glob-basics.ts", "tests/corpus/5146-path-matches-glob-braces.ts", "tests/corpus/5147-path-matches-glob-extglob.ts", "tests/corpus/5148-path-matches-glob-classes.ts", "packages/runtime/test/glob.test.ts"],
"node-builtin.path.posix.matchesGlob": ["tests/corpus/5145-path-matches-glob-basics.ts", "tests/corpus/5146-path-matches-glob-braces.ts", "tests/corpus/5147-path-matches-glob-extglob.ts", "tests/corpus/5148-path-matches-glob-classes.ts", "packages/runtime/test/glob.test.ts"],
"node-builtin.path.win32.matchesGlob": ["tests/corpus/5149-path-matches-glob-win32.ts", "packages/runtime/test/glob.test.ts"],
"node-builtin.perf_hooks": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"],
"node-builtin.process.hrtime": ["tests/corpus/process-hrtime.js"],
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
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_v19");
expect(error?.stderr).toContain("scr_runtime_abi_v20");
});
});
+5
View File
@@ -1600,6 +1600,9 @@ declare module "path" {
export function isAbsolute(path: string): boolean;
export function relative(from: string, to: string): string;
export function toNamespacedPath(path: string): string;
/* matchesGlob() is Node's glob matcher: brace expansion, extglobs, POSIX
* classes and the ** and dot rules. */
export function matchesGlob(path: string, pattern: string): boolean;
/* parse() splits a path into its root, directory, base, extension and
* name; format() joins those fields back (Node's `||` defaults). */
export interface ParsedPath {
@@ -1643,6 +1646,7 @@ declare module "path/posix" {
export function isAbsolute(path: string): boolean;
export function relative(from: string, to: string): string;
export function toNamespacedPath(path: string): string;
export function matchesGlob(path: string, pattern: string): boolean;
export function parse(path: string): import("path").ParsedPath;
export function format(pathObject: import("path").FormatInputPathObject): string;
export const sep: "/";
@@ -1661,6 +1665,7 @@ declare module "path/win32" {
export function isAbsolute(path: string): boolean;
export function relative(from: string, to: string): string;
export function toNamespacedPath(path: string): string;
export function matchesGlob(path: string, pattern: string): boolean;
export function parse(path: string): import("path").ParsedPath;
export function format(pathObject: import("path").FormatInputPathObject): string;
export const sep: "\\";
@@ -85,7 +85,17 @@ describe("emitted object allocation", () => {
const cyc = emitObjectAlloc(h, "40", { type: "@ty" });
expectSingleDefinitions(cyc);
expect(cyc).toContain(" %sa.n = add i64 40, 16");
expect(cyc).toContain(" %sa.n = add i64 40, 8");
// The 8-byte header: the descriptor's i16 registry id, then zero color,
// generation and buffer state; an unregistered descriptor (id 0) takes
// the slow path.
expect(cyc).toContain(
" %sa.tidp = getelementptr inbounds { ptr, ptr, i32, i32 }, ptr @ty, i32 0, i32 3",
);
expect(cyc).toContain(" %sa.tid = load atomic i32, ptr %sa.tidp acquire, align 4");
expect(cyc).toContain(" %sa.small = and i1 %sa.reg, %sa.fits0");
expect(cyc).toContain(" store i16 %sa.id, ptr %sa.blk ; ScrCycHdr.id");
expect(cyc).toContain(" store i32 0, ptr %sa.bufp ; ScrCycHdr.buf");
expect(cyc).toContain(" %sa.s = call ptr @scr_cyc_alloc(i64 40, ptr @ty)");
expect(cyc).toContain(" store i64 %sa.live1, ptr @scr_cyc_live");
expect(h.decls).toContain(
@@ -104,6 +114,12 @@ describe("emitted object allocation", () => {
expect(sys).toContain(` call void @scr_rt_free(ptr ${blk})`);
expect(lines).toContain(" call void %sf.hook(ptr %o)");
expect(lines.some((l) => l.includes("@scr_cyc_live"))).toBe(cyc);
if (cyc) {
// The generation is the i8 at obj-5.
expect(lines).toContain(" %sf.blk = getelementptr inbounds i8, ptr %o, i64 -8");
expect(lines).toContain(" %sf.genp = getelementptr inbounds i8, ptr %sf.blk, i64 3");
expect(lines).toContain(" %sf.old = icmp eq i8 %sf.gen, 2");
}
expect(lines.at(-1)).toBe("sf.done:");
}
});
+36 -15
View File
@@ -117,11 +117,16 @@ const SA_BUMP = 280;
const SA_LIM = 536;
const SA_STEP_SHIFT = 4; // 16-byte classes
const SA_MAX = 512;
/** sizeof(ScrCycHdr); on 64-bit targets the type descriptor is its first
* word, and every other header word starts zero. */
const CYC_HDR = 16;
/** ScrCycHdr.gen, an i8 at this header offset (scr_runtime.h asserts it). */
const CYC_GEN = 9;
/** sizeof(ScrCycHdr): the descriptor's i16 registry id, i8 color and
* generation, and an i32 `buf` whose sign bit marks a buffered candidate
* (scr_runtime.h asserts the offsets). */
export const CYC_HDR = 8;
/** ScrCycHdr.gen, an i8 at this header offset. */
const CYC_GEN = 3;
/** ScrCycHdr.buf, an i32 at this header offset. */
export const CYC_BUF = 4;
/** The emitted descriptor `{ trace, free_fn, kind, id }` (ScrCycType). */
const CYC_TYPE = "{ ptr, ptr, i32, i32 }";
function declareState(host: AllocHost): void {
if (host.threadLocalAlloc !== true) host.declare(`@scr_sa = external global ${SA_TYPE}`);
@@ -144,12 +149,13 @@ export interface CycHeader {
export const FIRST_EMITTED_CYC_KIND = 32;
/** The definition of a shape's ScrCycType descriptor `{ trace, free_fn,
* kind }`, referenced by every header of that shape. `kind` indexes the
* collector's per-type statistics (scr_runtime.h); emitted shapes number
* themselves from FIRST_EMITTED_CYC_KIND (ShapeHost.nextCycKind) and 0
* keeps none. */
* kind, id }`, named by every header of that shape through its registry
* id. `kind` indexes the collector's per-type statistics (scr_runtime.h);
* emitted shapes number themselves from FIRST_EMITTED_CYC_KIND
* (ShapeHost.nextCycKind) and 0 keeps none. The runtime assigns `id` on the
* first allocation, so the descriptor is writable. */
export function cycTypeDescriptor(name: string, trace: string, free: string, kind = 0): string {
return `${name} = internal constant { ptr, ptr, i32 } { ptr ${trace}, ptr ${free}, i32 ${kind} }`;
return `${name} = internal global ${CYC_TYPE} { ptr ${trace}, ptr ${free}, i32 ${kind}, i32 0 }`;
}
/**
@@ -203,11 +209,21 @@ function allocLines(
}
declareState(host);
// Bytes of the block that the fast path itself stores after the clear:
// the header's descriptor word, or the object's leading words.
const keep = cyc ? 8 : stored;
// the header, or the object's leading words.
const keep = cyc ? CYC_HDR : stored;
// A type's first object registers its descriptor on the slow path.
const registered = cyc
? [
` %sa.tidp = getelementptr inbounds ${CYC_TYPE}, ptr ${cyc.type}, i32 0, i32 3`,
` %sa.tid = load atomic i32, ptr %sa.tidp acquire, align 4`,
` %sa.reg = icmp ne i32 %sa.tid, 0`,
` %sa.fits0 = icmp ule i64 %sa.n, ${SA_MAX}`,
` %sa.small = and i1 %sa.reg, %sa.fits0`,
]
: [` %sa.small = icmp ule i64 %sa.n, ${SA_MAX}`];
const lines = [
` %sa.n = add i64 ${size}, ${cyc ? CYC_HDR : 0}`,
` %sa.small = icmp ule i64 %sa.n, ${SA_MAX}`,
...registered,
` br i1 %sa.small, label %sa.class, label %sa.slow`,
`sa.class:`,
` %sa.nm1 = sub i64 %sa.n, 1`,
@@ -253,7 +269,12 @@ function allocLines(
lines.push(
`sa.fast:`,
` %sa.blk = phi ptr [ %sa.b, %sa.pop ], [ %sa.p, %sa.carve ]`,
` store ptr ${cyc.type}, ptr %sa.blk ; ScrCycHdr.type`,
` %sa.id = trunc i32 %sa.tid to i16`,
` store i16 %sa.id, ptr %sa.blk ; ScrCycHdr.id`,
` %sa.cgp = getelementptr inbounds i8, ptr %sa.blk, i64 2`,
` store i16 0, ptr %sa.cgp ; ScrCycHdr.color, gen`,
` %sa.bufp = getelementptr inbounds i8, ptr %sa.blk, i64 ${CYC_BUF}`,
` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
` %sa.live = load i64, ptr @scr_cyc_live`,
` %sa.live1 = add i64 %sa.live, 1`,
` store i64 %sa.live1, ptr @scr_cyc_live`,
@@ -339,7 +360,7 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
` store i64 %sf.live1, ptr @scr_cyc_live`,
` %sf.blk = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR}`,
` %sf.genp = getelementptr inbounds i8, ptr %sf.blk, i64 ${CYC_GEN}`,
` %sf.gen = load i8, ptr %sf.genp`,
` %sf.gen = load i8, ptr %sf.genp ; ScrCycHdr.gen`,
` %sf.old = icmp eq i8 %sf.gen, 2`,
` br i1 %sf.old, label %sf.oldcount, label %sf.accounted`,
`sf.oldcount:`,
@@ -73,6 +73,10 @@ function eligible(local: IrLocal): boolean {
* extracts and retains the payload, so the stored union is only projected. */
export type NullableFieldTest = (className: string, field: string) => boolean;
function scalarValue(value: IrExpr): boolean {
return value.type.kind === "f64" || value.type.kind === "bool";
}
function collectUses(
fn: IrFunction,
nullableField: NullableFieldTest,
@@ -84,6 +88,7 @@ function collectUses(
declarations: new Map(),
forwards: new Map(),
};
const aliases: { alias: string; source: string }[] = [];
const stringInput = (value: IrExpr): boolean => {
if (value.kind === "varRef" && value.type.kind === "string") return true;
return expr(value);
@@ -171,6 +176,15 @@ function collectUses(
break;
case "varDecl":
uses.declarations.set(node.localId, (uses.declarations.get(node.localId) ?? 0) + 1);
// A receiver temporary (`%compoundReceiver = this`) projects its
// source as long as the temporary itself is only projected.
if (
node.init?.kind === "varRef" &&
(node.init.type.kind === "object" || node.init.type.kind === "record")
) {
aliases.push({ alias: node.localId, source: node.init.localId });
return true;
}
break;
case "fieldSet":
if (
@@ -179,6 +193,12 @@ function collectUses(
nullableField(node.className, node.field)
)
return expr(node.obj);
if (node.obj.kind === "varRef" && scalarValue(node.value)) return expr(node.value);
break;
case "recordSet":
// A number or boolean stored through the binding leaves the object
// where it is; the write itself stays visible to effect proofs.
if (node.obj.kind === "varRef" && scalarValue(node.value)) return expr(node.value);
break;
case "assign":
uses.written.add(node.localId);
@@ -205,6 +225,29 @@ function collectUses(
return everyStmtChild(node, expr, (node) => stmt(node, inExpression));
}
fn.body.forEach((node) => stmt(node, false));
// An alias of a rebound source would keep the old object; chains of
// aliases settle to a fixpoint.
const locals = new Map(fn.locals.map((local) => [local.id, local]));
for (let changed = aliases.length > 0; changed;) {
changed = false;
for (const { alias, source } of aliases) {
const local = locals.get(alias);
if (
!uses.invalid.has(source) &&
(!local ||
local.boxed ||
local.tdz ||
uses.invalid.has(alias) ||
uses.written.has(alias) ||
uses.written.has(source) ||
uses.declarations.get(alias) !== 1 ||
uses.forwards.has(alias))
) {
uses.invalid.add(source);
changed = true;
}
}
}
for (const capture of [...(fn.captures ?? []), ...(fn.classCaptures ?? [])]) {
uses.invalid.add(capture.localId);
uses.written.add(capture.localId);
@@ -79,7 +79,7 @@ test("releases split into an inlinable fast path and an out-of-line remainder",
// takes the possible-root step without loading the vtable.
const fast = body(mangleClassRelease("Node"));
expect(fast).toContain("%shared = icmp ult i64 %rcm2, -3");
expect(fast).toContain("load i16, ptr %bufp");
expect(fast).toContain("load i32, ptr %bufp");
expect(fast).toContain(`call void @${mangleClassRelease("Node")}_slow(ptr %o)`);
expect(fast).not.toContain("%ScrVt");
// Only the last reference dispatches, from a noinline remainder.
@@ -136,7 +136,7 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
// The mortal Owner keeps its header, counts and possible-root step.
expect(body(mangleClassNew("Owner"))).toContain("store i64 1, ptr %o");
expect(body(mangleClassNew("Owner"))).toContain("scr_cyc_alloc");
expect(body(mangleClassRelease("Owner"))).toContain("load i16, ptr %bufp");
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
});
test("published ProgramLifetime instances use the published mark every other object uses", () => {
+66 -9
View File
@@ -1,6 +1,6 @@
import { emitLlvmLayouts } from "./layouts.js";
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
import { FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
import { InternalCompilerError } from "../../errors.js";
import { createHash } from "node:crypto";
@@ -74,6 +74,7 @@ import {
type StringSplit,
} from "./split-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { addRecordResultEntries } from "./record-results.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
@@ -123,6 +124,13 @@ import {
} from "../mangle.js";
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
import { canStackUnion, emitStackUnion, findLocalStackUnions } from "./stack-unions.js";
import {
emitStackObject,
findLocalStackObjects,
findReassignedStackObjects,
stackObjectStruct,
type StackObjectSource,
} from "./stack-objects.js";
import {
allocateLocalUnion,
emitScalarOptional,
@@ -408,7 +416,9 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
// Keep source storage intact for debugger inspection in dev builds.
return new LlEmitter(
options.debugSources === undefined
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
? addRecordResultEntries(
scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction)),
)
: mod,
options,
).emit();
@@ -422,7 +432,9 @@ export function emitLlvmModuleSource(
const mod = unshareNodes(input);
const parts = new LlEmitter(
options.debugSources === undefined
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
? addRecordResultEntries(
scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction)),
)
: mod,
options,
).emitParts();
@@ -476,6 +488,10 @@ export class LlEmitter {
private scalarStringSlices = new Map<string, IrExpr & { kind: "strIntrinsic" }>();
readonly stringSlices = new Map<string, StringSliceSnapshot>();
private localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
private localStackObjects = new Map<string, StackObjectSource>();
private reassignedStackObjects = new Set<string>();
/** The binding slot of each reassigned stack-object local. */
private stackObjectSlots = new Map<string, string>();
private localUnionStorageProofs = new Map<string, LocalUnionStorageProof>();
private localUnionStorage = new Map<string, LocalUnionStorage>();
integerArrayBindings = new Set<string>();
@@ -529,10 +545,12 @@ export class LlEmitter {
return lines;
}
private readonly debug: LlvmDebugInfo | null;
/** Projection-only scalar objects may live in frame storage (stack-objects.ts). */
readonly stackObjects: boolean;
private debugScope: string | null = null;
readonly sizeType: "i32" | "i64";
readonly ffiExtendNarrowIntegers: boolean;
readonly cycleBufferedOffset: number;
readonly cycleBufOffset: number;
/** Emitted ScrCycType descriptors so far (their collector statistics
* indices; see ShapeHost.nextCycKind). */
private cycKinds = 0;
@@ -734,6 +752,8 @@ export class LlEmitter {
* typeKey → thunk symbol. */
readonly resolveThunks = new Map<string, string>();
readonly resolveThunkDefs: string[] = [];
/** Typed-ref recipe constants by type key (lazy receivers). */
readonly typedRefRecipes = new Map<string, string>();
/** ReadableStream.from adapters keep typed arrays by reference and box
* one current element per pull. */
readonly streamFromArrayAdapters = new Map<string, string>();
@@ -931,6 +951,7 @@ export class LlEmitter {
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
this.wasi = options.wasi === true;
this.stacklessGenerators = !this.wasi && mod.workers !== true && this.debug === null;
this.stackObjects = this.debug === null;
// An empty triple targets the host, as for executableTls below: the
// Node-hosted CLI passes none, the native CLI passes its toolchain's, and
// both must emit the same module for the same target.
@@ -957,10 +978,8 @@ export class LlEmitter {
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
this.rcHelpers = options.inlineRc === true ? new Set() : null;
// ScrCycHdr.buffered sits 6 bytes behind every object. The wasm32
// header pads before its descriptor so its payload remains
// double-aligned without changing this ABI offset.
this.cycleBufferedOffset = 6;
// ScrCycHdr.buf sits 4 bytes behind every object on every target.
this.cycleBufOffset = CYC_HDR - CYC_BUF;
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
@@ -1078,7 +1097,7 @@ export class LlEmitter {
this.tracedUnions = traced.unions;
this.shapeHost = {
sizeType: this.sizeType,
cycleBufferedOffset: this.cycleBufferedOffset,
cycleBufOffset: this.cycleBufOffset,
tracedShapes: this.tracedShapes,
tracedUnions: this.tracedUnions,
nullableUnions: this.nullableUnions,
@@ -5509,6 +5528,13 @@ export class LlEmitter {
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
this.localStackObjects = this.stackObjects
? findLocalStackObjects(this, fn, this.callLifetimes)
: new Map();
this.reassignedStackObjects = this.stackObjects
? findReassignedStackObjects(this, fn, this.callLifetimes)
: new Set();
this.stackObjectSlots = new Map();
this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources);
this.localUnionStorage = new Map();
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
@@ -6430,6 +6456,17 @@ export class LlEmitter {
);
break;
}
const stackObject = this.localStackObjects.get(s.localId);
if (stackObject) {
B.line(`store ptr ${emitStackObject(this, stackObject).name}, ptr ${b.slot}`);
break;
}
if (s.init && this.reassignedStackObjects.has(s.localId)) {
const slot = this.stackObjectSlot(s.localId, s.init.type);
emitStackObject(this, s.init as StackObjectSource, slot);
B.line(`store ptr ${slot}, ptr ${b.slot}`);
break;
}
const localUnion = this.localStackUnions.get(s.localId);
if (localUnion) {
const union = emitStackUnion(this, localUnion);
@@ -6577,6 +6614,15 @@ export class LlEmitter {
B.line(`store ptr ${v.name}, ptr ${owner}`);
break;
}
if (this.reassignedStackObjects.has(s.localId)) {
// The new value may read the old one: build it apart, then copy.
const struct = stackObjectStruct(s.value.type);
const value = emitStackObject(this, s.value as StackObjectSource);
const bits = B.tmp();
B.line(`${bits} = load ${struct}, ptr ${value.name}`);
B.line(`store ${struct} ${bits}, ptr ${this.stackObjectSlot(s.localId, s.value.type)}`);
break;
}
const localStorage = this.localUnionStorage.get(s.localId);
if (localStorage) {
storeLocalUnion(this, localStorage, s.value);
@@ -8558,6 +8604,17 @@ export class LlEmitter {
return this.emitExpr(e);
}
/** The frame slot a reassigned stack-object local keeps for its value. */
private stackObjectSlot(localId: string, type: IrType): string {
let slot = this.stackObjectSlots.get(localId);
if (slot === undefined) {
slot = this.B.slot();
this.B.entryAllocas.push(`${slot} = alloca ${stackObjectStruct(type)}, align 8`);
this.stackObjectSlots.set(localId, slot);
}
return slot;
}
/** A private stack union source with an independently owned payload. */
canStackReceiver(e: IrExpr): boolean {
return this.isStackUnionSource(e);
+116 -57
View File
@@ -2,14 +2,19 @@
import { InternalCompilerError } from "../../errors.js";
import { newValueMayThrow } from "../../ir/analysis.js";
import {
isDynTypedRefType,
isFfiCallbackParam,
isFfiContextParam,
isFfiReleaseParam,
isRefCounted,
typeKey,
type IrExpr,
type IrFunction,
} from "../../ir/ir.js";
import { liveRefView } from "./expr-stream-bridges.js";
import { typedRefConstructor, vAdapters } from "./shapes.js";
import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
import { canStackObject, emitStackObject } from "./stack-objects.js";
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
import {
mangleAsyncAdoptCall,
@@ -62,6 +67,9 @@ export function emitCallExpr(
/** A projection consumes a scalar union result (Int32Abi.union): answer
* a private stack box instead of a heap box. */
stackResult = false,
/** The frame storage a result-slot entry (record-results.ts) writes:
* passed after the call's own arguments. */
resultSlot?: LlValue,
): LlValue {
const B = host.B;
switch (e.kind) {
@@ -125,6 +133,7 @@ export function emitCallExpr(
return emitScalarOptional(host, a);
if (a.kind === "call" && host.scalarResultCall(a))
return emitCallExpr(host, a, false, true);
if (host.stackObjects && canStackObject(host, a)) return emitStackObject(host, a);
if (host.nullableFieldGet(a))
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
}
@@ -132,6 +141,7 @@ export function emitCallExpr(
}
return host.emitExpr(a);
});
if (resultSlot) args.push(resultSlot);
// A borrowed parameter's owned argument stays in the caller frame.
// This also preserves snapshots when a later argument mutates its
// source, and releases earlier arguments if evaluation throws.
@@ -723,7 +733,44 @@ export function emitCallExpr(
if (e.args.length !== ft.params.length)
throw new LlvmUnsupportedError("callValue:arity", e.loc);
const callee = host.stackCallbacks.emit(host, e.callee) ?? emitBorrowedInput(host, e.callee);
const receiver = e.receiver === undefined ? null : host.emitExpr(e.receiver);
// A class instance bound as the callee's `this` (`h.fn(x)`) is boxed
// only if the callee reads it: the receiver window records the
// instance (owned by this frame for the whole call) with its typed-ref
// recipe, and dyn.this builds the box on first read.
const lazyReceiver =
e.receiver?.kind === "dynFrom" &&
e.receiver.value.type.kind === "object" &&
isDynTypedRefType(e.receiver.value.type) &&
!host.shapeHost.isErrorClass(e.receiver.value.type.className)
? host.emitExpr(e.receiver.value)
: null;
const receiver = e.receiver === undefined || lazyReceiver ? null : host.emitExpr(e.receiver);
const pushReceiver = (): void => {
if (lazyReceiver) {
const t = lazyReceiver.type;
const key = typeKey(t);
let recipe = host.typedRefRecipes.get(key);
if (recipe === undefined) {
const adapter = host.liveDynRefAdapter(t);
const rc = vAdapters(host.shapeHost, t);
const ctor = typedRefConstructor(host.shapeHost, t);
const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0;
// The box binds the same live view eager boxing attaches, so
// `in`, for-in and Object.keys see the class members.
const view = liveRefView(adapter);
recipe = `@sc_trr_${host.typedRefRecipes.size}`;
host.typedRefRecipes.set(key, recipe);
host.resolveThunkDefs.push(
`${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`,
);
}
host.declare("declare void @scr_dyn_this_push_typed(ptr, ptr)");
B.line(`call void @scr_dyn_this_push_typed(ptr ${lazyReceiver.name}, ptr ${recipe})`);
return;
}
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
@@ -799,9 +846,8 @@ export function emitCallExpr(
: args;
const invoke = (target: string, needsReceiver: boolean, values = args): string => {
if (needsReceiver) {
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
host.declare("declare void @scr_dyn_this_pop()");
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
pushReceiver();
}
const result = e.type.kind === "void" ? "" : B.tmp();
B.line(
@@ -913,60 +959,7 @@ export function emitCallExpr(
const o = B.tmp();
B.line(`${o} = call ptr @${mangleClassNew(e.className)}()`);
const out = host.own({ name: o, type: e.type });
const ctorName = `%${e.className}.constructor`;
const borrowed = host.callLifetimes.borrowed.get(ctorName);
if (borrowed) {
// The borrowed constructor body, like a direct call: this frame
// owns the new object across the call, borrowed arguments stay
// owned by a call frame, and projection-only parameters (stores
// into nullable-pointer fields, tests) receive private stack boxes.
const projected = host.callLifetimes.parameters.get(ctorName);
const inputs = borrowableInputs(
host,
e.args,
host.referenceEffects.functions.has(ctorName),
host.referenceWrites?.constructWrites(e.className),
);
host.frames.push([]);
const args = e.args.map((a, i) => {
if (borrowed.has(i + 1)) {
if (projected?.has(i + 1) && canStackUnion(a, host.boxedUnionsById))
return emitStackUnion(host, a).value;
if (projected?.has(i + 1) && host.nullableFieldGet(a))
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
if (inputs[i]) return host.emitReadReceiver(a);
}
return host.emitExpr(a);
});
args.forEach((a, i) => {
if (!borrowed.has(i + 1)) host.moveTemp(a);
});
const self = borrowed.has(0)
? o
: (() => {
const r = B.tmp();
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
return r;
})();
const argList = [`ptr ${self}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(
", ",
);
B.line(
`call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
);
if (host.mayThrow.has(ctorName)) host.emitPendingCheck();
host.releaseFrame(host.frames.pop()!);
return out;
}
const args = e.args.map((a) => host.emitExpr(a));
for (const a of args) host.moveTemp(a);
const r = B.tmp();
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", ");
B.line(
`call void @${mangleFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
);
if (host.mayThrow.has(`%${e.className}.constructor`)) host.emitPendingCheck();
emitConstruction(host, e, o, ctor);
return out;
}
case "classRef": {
@@ -1239,3 +1232,69 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
return { name: t, type: e.type };
}
/** Run the constructor of `new` over the allocated object `o` (fields
* zeroed, header stamped). The ctor owns and releases its `this` param like
* any callee, so it receives a +1 distinct from the caller's reference. */
export function emitConstruction(
host: LlvmEmitterContext,
e: ExprOf<"new">,
o: string,
ctor: IrFunction,
): void {
const B = host.B;
const ctorName = `%${e.className}.constructor`;
const borrowed = host.callLifetimes.borrowed.get(ctorName);
if (borrowed) {
// The borrowed constructor body, like a direct call: this frame
// owns the new object across the call, borrowed arguments stay
// owned by a call frame, and projection-only parameters (stores
// into nullable-pointer fields, tests) receive private stack boxes.
const projected = host.callLifetimes.parameters.get(ctorName);
const inputs = borrowableInputs(
host,
e.args,
host.referenceEffects.functions.has(ctorName),
host.referenceWrites?.constructWrites(e.className),
);
host.frames.push([]);
const args = e.args.map((a, i) => {
if (borrowed.has(i + 1)) {
if (projected?.has(i + 1) && canStackUnion(a, host.boxedUnionsById))
return emitStackUnion(host, a).value;
if (projected?.has(i + 1) && host.nullableFieldGet(a))
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
if (inputs[i]) return host.emitReadReceiver(a);
}
return host.emitExpr(a);
});
args.forEach((a, i) => {
if (!borrowed.has(i + 1)) host.moveTemp(a);
});
const self = borrowed.has(0)
? o
: (() => {
const r = B.tmp();
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
return r;
})();
const argList = [`ptr ${self}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(
", ",
);
B.line(
`call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
);
if (host.mayThrow.has(ctorName)) host.emitPendingCheck();
host.releaseFrame(host.frames.pop()!);
return;
}
const args = e.args.map((a) => host.emitExpr(a));
for (const a of args) host.moveTemp(a);
const r = B.tmp();
B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`);
const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", ");
B.line(
`call void @${mangleFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`,
);
if (host.mayThrow.has(`%${e.className}.constructor`)) host.emitPendingCheck();
}
@@ -480,6 +480,18 @@ export function emitStrIntrinsic(
}
}
/** The `i32 order, ptr key` arguments of the runtime comparator sorts:
* 0 and null unless the comparator orders by a number key. */
function emitSortKeyArgs(
host: LlvmEmitterContext,
order: IrExpr | undefined,
key: IrExpr | undefined,
): string {
const direction = order?.kind === "numLit" ? order.value : 0;
const reader = key ? emitBorrowedInput(host, key).name : "null";
return `i32 ${direction}, ptr ${reader}`;
}
export function emitStableReceiver(
host: LlvmEmitterContext,
receiver: IrExpr,
@@ -767,9 +779,10 @@ export function emitArrIntrinsic(
const comparator = emitBorrowedInput(host, e.args[1]!);
const cmp = e.args[1]!.type;
const arity = cmp.kind === "func" ? cmp.params.length : 0;
host.declare(`declare void @scr_arr_sort_values(ptr, double, ptr, i32)`);
const keyed = emitSortKeyArgs(host, e.args[2], e.args[3]);
host.declare(`declare void @scr_arr_sort_values(ptr, double, ptr, i32, i32, ptr)`);
B.line(
`call void @scr_arr_sort_values(ptr ${r.name}, double ${count.name}, ptr ${comparator.name}, i32 ${arity})`,
`call void @scr_arr_sort_values(ptr ${r.name}, double ${count.name}, ptr ${comparator.name}, i32 ${arity}, ${keyed})`,
);
host.emitPendingCheck();
return { name: "", type: e.type };
@@ -781,10 +794,11 @@ export function emitArrIntrinsic(
const owned = e.args[1]!.kind === "boolLit" && e.args[1]!.value;
const cmp = e.args[0]!.type;
const arity = cmp.kind === "func" ? cmp.params.length : 0;
host.declare(`declare zeroext i1 @scr_arr_sort_dense(ptr, ptr, i32, i1 zeroext)`);
const keyed = emitSortKeyArgs(host, e.args[2], e.args[3]);
host.declare(`declare zeroext i1 @scr_arr_sort_dense(ptr, ptr, i32, i1 zeroext, i32, ptr)`);
const handled = B.tmp();
B.line(
`${handled} = call zeroext i1 @scr_arr_sort_dense(ptr ${r.name}, ptr ${comparator.name}, i32 ${arity}, i1 ${owned ? 1 : 0})`,
`${handled} = call zeroext i1 @scr_arr_sort_dense(ptr ${r.name}, ptr ${comparator.name}, i32 ${arity}, i1 ${owned ? 1 : 0}, ${keyed})`,
);
host.emitPendingCheck();
return { name: handled, type: e.type };
@@ -1,7 +1,7 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted, type IrExpr } from "../../ir/ir.js";
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
import { arrNewCall, elemAccess } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -968,24 +968,7 @@ export function emitRecordExpr(
const rec = B.tmp();
B.line(`${rec} = call ptr @${mangleRecordNew(shapeId)}()`);
const out = host.own({ name: rec, type: e.type });
for (const f of e.fields) {
const v = host.emitExpr(f.value);
if (f.overflow) {
const lit = host.internLiteral(f.name);
const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
if (acc === "ref") host.moveTemp(v);
const ovf = host.recordOvfPtr(rec, shapeId);
const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr";
host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`);
B.line(
`call void @scr_map_set_str_${acc}(ptr ${ovf}, ptr ${lit}, ${argTy === "i1 zeroext" ? "i1" : argTy} ${v.name})`,
);
continue;
}
if (isRefCounted(v.type)) host.moveTemp(v);
const { ptr, type } = host.recordFieldPtr(rec, shapeId, f.name);
host.storeField(ptr, type, v.name);
}
emitRecordLitFields(host, e, rec);
return out;
}
case "recordClone": {
@@ -1054,3 +1037,34 @@ export function emitRecordExpr(
}
}
}
/** Write a record literal's fields into the fresh record `rec`, IN SOURCE
* ORDER (JS evaluates property values in source order). Ownership of
* refcounted values moves in; the struct is fresh, so there is never an
* old value to release. */
export function emitRecordLitFields(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordLit" },
rec: string,
): void {
const B = host.B;
const shapeId = (e.type as IrType & { kind: "record" }).shapeId;
for (const f of e.fields) {
const v = host.emitExpr(f.value);
if (f.overflow) {
const lit = host.internLiteral(f.name);
const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
if (acc === "ref") host.moveTemp(v);
const ovf = host.recordOvfPtr(rec, shapeId);
const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr";
host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`);
B.line(
`call void @scr_map_set_str_${acc}(ptr ${ovf}, ptr ${lit}, ${argTy === "i1 zeroext" ? "i1" : argTy} ${v.name})`,
);
continue;
}
if (isRefCounted(v.type)) host.moveTemp(v);
const { ptr, type } = host.recordFieldPtr(rec, shapeId, f.name);
host.storeField(ptr, type, v.name);
}
}
@@ -1320,23 +1320,31 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
);
}
/** The live view a capsule of this adapter binds: the runtime bind call
* and its ops constant, with the view code a deferred receiver recipe
* records (1 array, 2 record, 3 class). */
export function liveRefView(
adapter: LlStreamTypedRefAdapter,
): { bind: string; ops: string; code: 1 | 2 | 3 } | null {
if (adapter.classOps !== undefined)
return { bind: "scr_dyn_typed_ref_bind_class", ops: adapter.classOps, code: 3 };
if (adapter.recordOps !== undefined)
return { bind: "scr_dyn_typed_ref_bind_record", ops: adapter.recordOps, code: 2 };
if (adapter.arrayOps !== undefined)
return { bind: "scr_dyn_typed_ref_bind_array", ops: adapter.arrayOps, code: 1 };
return null;
}
/** Attach the element or field hooks right after a live capsule is built. */
export function bindLiveArrayOps(
host: ShapeHost,
adapter: LlStreamTypedRefAdapter,
capsule: string,
): string[] {
if (adapter.classOps !== undefined) {
host.declare(`declare void @scr_dyn_typed_ref_bind_class(ptr, ptr)`);
return [`call void @scr_dyn_typed_ref_bind_class(ptr ${capsule}, ptr ${adapter.classOps})`];
}
if (adapter.recordOps !== undefined) {
host.declare(`declare void @scr_dyn_typed_ref_bind_record(ptr, ptr)`);
return [`call void @scr_dyn_typed_ref_bind_record(ptr ${capsule}, ptr ${adapter.recordOps})`];
}
if (adapter.arrayOps === undefined) return [];
host.declare(`declare void @scr_dyn_typed_ref_bind_array(ptr, ptr)`);
return [`call void @scr_dyn_typed_ref_bind_array(ptr ${capsule}, ptr ${adapter.arrayOps})`];
const view = liveRefView(adapter);
if (view === null) return [];
host.declare(`declare void @${view.bind}(ptr, ptr)`);
return [`call void @${view.bind}(ptr ${capsule}, ptr ${view.ops})`];
}
export function streamFromArrayAdapter(
@@ -154,6 +154,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"path.win32Parse": "scr_path_win32_parse",
"path.format": "scr_path_format",
"path.win32Format": "scr_path_win32_format",
"path.matchesGlob": "scr_path_matches_glob",
"path.win32MatchesGlob": "scr_path_win32_matches_glob",
"path.win32IsAbsolute": "scr_path_win32_is_absolute",
"path.win32Relative": "scr_path_win32_relative",
"path.win32ToNamespacedPath": "scr_path_win32_to_namespaced_path",
@@ -0,0 +1,288 @@
import { constructorEscapePoint } from "../cycle-mutability.js";
import {
VOID,
type IrClassDef,
type IrExpr,
type IrFunction,
type IrModule,
type IrRecordShape,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import { dynamicFieldStores } from "../../ir/int32-slots.js";
import { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js";
/** Prefix of a function's result-slot entry: `%into.<name>(...params, dst)`
* writes the function's fresh result object into `dst` and returns nothing. */
export const RESULT_ENTRY_PREFIX = "%into.";
export function resultEntryName(name: string): string {
return `${RESULT_ENTRY_PREFIX}${name}`;
}
function scalarFields(fields: readonly { type: IrType }[]): boolean {
return fields.every((field) => field.type.kind === "f64" || field.type.kind === "bool");
}
/** A plain record shape whose fields are numbers and booleans. */
export function scalarRecordShape(shape: IrRecordShape | undefined): boolean {
return shape !== undefined && !shape.indexValue && !shape.tuple && scalarFields(shape.fields);
}
/** A literal that stores every field of its shape, none of them through
* the overflow map. */
export function completeRecordLiteral(
e: IrExpr & { kind: "recordLit" },
shape: IrRecordShape | undefined,
): boolean {
if (!shape || e.fields.some((field) => field.overflow)) return false;
const names = new Set(e.fields.map((field) => field.name));
return shape.fields.every((field) => names.has(field.name));
}
/** A standalone class with scalar fields whose constructor never lets
* `this` escape: its instance can be built in caller-provided storage. */
export function scalarStandaloneClass(
cls: IrClassDef | undefined,
ctor: IrFunction | undefined,
isBase: boolean,
programLifetime: boolean,
): boolean {
if (
!cls ||
!ctor ||
isBase ||
programLifetime ||
cls.runtime ||
cls.base !== undefined ||
cls.localCaptures !== undefined ||
cls.tracksOwnFields ||
(cls.symbolFields?.length ?? 0) > 0 ||
ctor.async ||
ctor.generator ||
!scalarFields(cls.fields)
)
return false;
const self = ctor.params[0]?.localId;
return self !== undefined && constructorEscapePoint(ctor, self, null) === ctor.body.length;
}
/** Whether the constructor's top-level statements store every field of
* `this` before anything reads a field it has not stored yet. Storage for
* such an instance needs no zeroing first. */
export function constructorInitializesFields(cls: IrClassDef, ctor: IrFunction): boolean {
const self = ctor.params[0]?.localId;
if (self === undefined) return false;
const stored = new Set<string>();
const unstoredRead = {
expr: (node: IrExpr): boolean =>
!(
node.kind === "fieldGet" &&
node.obj.kind === "varRef" &&
node.obj.localId === self &&
!stored.has(node.field)
),
stmt: () => true,
};
for (const stmt of ctor.body) {
if (stmt.kind === "fieldSet" && stmt.obj.kind === "varRef" && stmt.obj.localId === self) {
if (!everyExpr(stmt.value, unstoredRead)) return false;
stored.add(stmt.field);
} else if (!everyStmtList([stmt], unstoredRead)) return false;
}
return cls.fields.every((field) => stored.has(field.name));
}
/** Add a result-slot entry for every function whose returns all build a
* fresh scalar record or standalone scalar object, return another such
* producer's result (forwarding the slot), or return a parameter of a
* record shape no store can change (copied into the slot). The original
* function stays as it is; the backend calls the entry only where the
* result is proven to be consumed by projections, passing frame storage. */
export function addRecordResultEntries(mod: IrModule): IrModule {
const shapes = new Map((mod.records ?? []).map((shape) => [shape.id, shape]));
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
const bases = new Set((mod.classes ?? []).flatMap((cls) => (cls.base ? [cls.base] : [])));
const lifetime = new Set(mod.contracts?.programLifetime ?? []);
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
const stackClass = (name: string): boolean =>
scalarStandaloneClass(
classes.get(name),
functions.get(`%${name}.constructor`),
bases.has(name),
lifetime.has(name),
);
// Shapes no store can change after construction: no record field write
// in the program, no dynamic store that can name a field, and no native
// boundary. A returned value of such a shape can be copied instead of
// shared, since nothing can tell the copy from the original once `===`
// and field writes are ruled out by the caller's projection proof.
const written = new Set<string>();
for (const fn of mod.functions)
everyStmtList(fn.body, {
expr: () => true,
stmt: (stmt) => {
if (
stmt.kind === "recordSet" ||
stmt.kind === "recordKeySet" ||
stmt.kind === "recordKeyDelete"
)
written.add(stmt.shapeId);
return true;
},
});
const dynamicStores = dynamicFieldStores(mod);
const copyable = (type: IrType): boolean =>
type.kind === "record" &&
mod.lib === undefined &&
!written.has(type.shapeId) &&
!dynamicStores.all &&
!(shapes.get(type.shapeId)?.fields ?? []).some((field) => dynamicStores.names.has(field.name));
const producers = new Set<string>();
const fillable = (value: IrExpr, params: ReadonlySet<string>): boolean => {
switch (value.kind) {
case "recordLit":
return completeRecordLiteral(
value,
shapes.get((value.type as IrType & { kind: "record" }).shapeId),
);
case "new":
return stackClass(value.className);
case "ternary":
return fillable(value.then, params) && fillable(value.else_, params);
case "varRef":
return params.has(value.localId) && copyable(value.type);
case "call":
return producers.has(value.callee);
default:
return false;
}
};
const fill = (value: IrExpr, dst: IrExpr): IrStmt[] => {
switch (value.kind) {
case "recordLit":
return value.fields.map((field) => ({
kind: "recordSet",
obj: dst,
shapeId: (value.type as IrType & { kind: "record" }).shapeId,
field: field.name,
value: field.value,
loc: field.value.loc,
}));
case "varRef": {
const shapeId = (value.type as IrType & { kind: "record" }).shapeId;
return shapes.get(shapeId)!.fields.map((field) => ({
kind: "recordSet",
obj: dst,
shapeId,
field: field.name,
value: {
kind: "recordGet",
obj: value,
shapeId,
field: field.name,
type: field.type,
loc: value.loc,
},
loc: value.loc,
}));
}
case "call":
return [
{
kind: "exprStmt",
expr: {
kind: "call",
callee: resultEntryName(value.callee),
args: [...value.args, dst],
type: VOID,
loc: value.loc,
},
loc: value.loc,
},
];
case "new":
return [
{
kind: "exprStmt",
expr: {
kind: "call",
callee: `%${value.className}.constructor`,
args: [dst, ...value.args],
type: VOID,
loc: value.loc,
},
loc: value.loc,
},
];
case "ternary":
return [
{
kind: "if",
cond: value.cond,
then: fill(value.then, dst),
else_: fill(value.else_, dst),
loc: value.loc,
},
];
default:
throw new Error("unreachable");
}
};
const candidates = mod.functions.filter((fn) => {
if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures !== undefined)
return false;
const ret = fn.returnType;
return (
(ret.kind === "record" && scalarRecordShape(shapes.get(ret.shapeId))) ||
(ret.kind === "object" && stackClass(ret.className))
);
});
for (const fn of candidates) producers.add(fn.name);
// Greatest fixpoint: a producer may return another producer's result.
const eligible = (fn: IrFunction): boolean => {
const params = new Set(fn.params.map((param) => param.localId));
let returns = 0;
const ok = everyStmtList(fn.body, {
expr: (e) => e.kind !== "seqExpr",
stmt: (s) => {
if (s.kind !== "return") return s.kind !== "tryCatch";
returns++;
return s.value !== null && fillable(s.value, params);
},
});
return ok && returns > 0;
};
for (let changed = true; changed;) {
changed = false;
for (const fn of candidates)
if (producers.has(fn.name) && !eligible(fn)) {
producers.delete(fn.name);
changed = true;
}
}
const added: IrFunction[] = [];
for (const fn of candidates) {
if (!producers.has(fn.name)) continue;
const ret = fn.returnType;
const dstId = "%into.dst";
const dst: IrExpr = { kind: "varRef", localId: dstId, type: ret, loc: fn.loc };
const body = transformStmtList(fn.body, {
expr: (e) => e,
stmt: (s) =>
s.kind === "return" && s.value !== null
? { kind: "block", body: [...fill(s.value, dst), { ...s, value: null }], loc: s.loc }
: s,
});
const { sourceName: _sourceName, ownsPrototype: _ownsPrototype, ...rest } = fn;
added.push({
...rest,
name: resultEntryName(fn.name),
params: [...fn.params, { localId: dstId, name: "dst", type: ret }],
locals: [...fn.locals, { id: dstId, name: "dst", type: ret, mutable: false }],
returnType: VOID,
body,
});
}
return added.length === 0 ? mod : { ...mod, functions: [...mod.functions, ...added] };
}
@@ -431,14 +431,17 @@ export class ReferenceWrites {
}
case "arrIntrinsic":
if (!PRESERVING_ARRAY_METHODS.has(e.method)) {
// The comparator sort orders a private snapshot.
// The comparator sort orders a private snapshot. It may call the
// comparator and a key reader.
if (e.method === "sortValues" || e.method === "sortDense") {
const comparator = e.args[e.method === "sortValues" ? 1 : 0];
const targets =
fn !== null && comparator !== undefined ? this.callTargets.of(comparator, fn) : null;
if (targets === null) {
if (!sink.call(ANY_FUNCTION, false)) sink.all();
} else for (const t of targets) if (!sink.call(t, false)) sink.all();
for (const callee of e.args[3] ? [comparator, e.args[3]] : [comparator]) {
const targets =
fn !== null && callee !== undefined ? this.callTargets.of(callee, fn) : null;
if (targets === null) {
if (!sink.call(ANY_FUNCTION, false)) sink.all();
} else for (const t of targets) if (!sink.call(t, false)) sink.all();
}
}
sink.add(ARRAY_KEY);
}
@@ -23,7 +23,7 @@ function declarationHost(
},
needOom() {},
sizeType: pointerBits === 32 ? "i32" : "i64",
cycleBufferedOffset: 6,
cycleBufOffset: 4,
tracedShapes: new Set(),
tracedUnions: new Set(),
nullableUnions: new NullableUnions({
@@ -148,10 +148,11 @@ describe("LLVM runtime RC symbols", () => {
expect(fn("sc_rc_release_map")).toContain("%ScrMapRc, ptr %o, i32 0, i32 8");
expect(fn("sc_rc_release_map")).toContain("br i1 %headered, label %slow, label %dec");
expect(fn("sc_rc_release_map")).not.toContain("store i32 1");
// Unions: no color store, only the i16 `buffered` read at obj-6.
// Unions: no color store, only the sign of the i32 `buf` at obj-4.
expect(fn("sc_rc_release_union")).not.toContain("store i32");
expect(fn("sc_rc_release_union")).toContain("getelementptr i8, ptr %o, i64 -6");
expect(fn("sc_rc_release_union")).toContain("load i16, ptr %bufp");
expect(fn("sc_rc_release_union")).toContain("getelementptr i8, ptr %o, i64 -4");
expect(fn("sc_rc_release_union")).toContain("load i32, ptr %bufp");
expect(fn("sc_rc_release_union")).toContain("icmp slt i32 %buf, 0");
expect(fn("sc_rc_release_union")).toContain("call void @scr_cyc_on_release(ptr %o)");
// Promises tolerate NULL and always carry a header.
expect(fn("sc_rc_retain_promise")).toContain("icmp eq ptr %o, null");
@@ -163,7 +164,7 @@ describe("LLVM runtime RC symbols", () => {
releaseSym(host, { kind: "union", unionId: "u0" } as never);
const ir = emitInlineRcHelpers(host).join("\n");
expect(ir).toContain("load i32, ptr %o");
expect(ir).toContain("getelementptr i8, ptr %o, i32 -6");
expect(ir).toContain("getelementptr i8, ptr %o, i32 -4");
});
});
@@ -182,7 +183,7 @@ describe("emitted object releases", () => {
const start = defs.indexOf("root:");
const root = defs.slice(start, defs.indexOf("done:", start));
expect(root).not.toContain("store i32");
expect(root).toContain("getelementptr i8, ptr %o, i64 -6");
expect(root).toContain("getelementptr i8, ptr %o, i64 -4");
expect(root).toContain("br i1 %queued, label %done, label %enqueue");
expect(root.indexOf("enqueue:")).toBeLessThan(
root.indexOf("call void @scr_cyc_on_release(ptr %o)"),
+14 -14
View File
@@ -39,8 +39,8 @@ export interface ShapeHost {
needOom(): void;
/** The target C ABI's `size_t` / refcount integer type. */
readonly sizeType: "i32" | "i64";
/** Byte offset from an object pointer back to ScrCycHdr.buffered. */
readonly cycleBufferedOffset: number;
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
readonly cycleBufOffset: number;
readonly tracedShapes: Set<string>;
readonly tracedUnions: Set<string>;
/** Unions represented as a nullable arm pointer (nullable-unions.ts):
@@ -866,9 +866,9 @@ export function directChildReleaseLines(
if (target.traced) {
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
lines.push(
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufferedOffset}`,
` %${tag}.buf = load i16, ptr %${tag}.bufp`,
` %${tag}.queued = icmp ne i16 %${tag}.buf, 0`,
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufOffset}`,
` %${tag}.buf = load i32, ptr %${tag}.bufp`,
` %${tag}.queued = icmp slt i32 %${tag}.buf, 0`,
` br i1 %${tag}.queued, label %${tag}.unbuffer, label %${tag}.teardown`,
`${tag}.unbuffer:`,
` call void @scr_cyc_on_dead(ptr ${value})`,
@@ -1052,9 +1052,9 @@ export function releaseFastPath(
}
/** The possible-cycle-root step of a release that left a headered object
* alive: scr_cyc_on_release with its early return inlined. Read `buffered`
* (the i16 at obj-6; nonzero for a queued candidate or a
* tenured object) and call the runtime only to enqueue a new candidate.
* alive: scr_cyc_on_release with its early return inlined. Test the sign
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
* object) and call the runtime only to enqueue a new candidate.
* Every emitted release of a headered object shares this sequence
* (scr_runtime.h asserts the offset); it ends by branching to `done`. */
/** One traced field of an emitted trace function: load the slot and visit
@@ -1084,9 +1084,9 @@ export function cycleRootLines(host: ShapeHost, done: string): string[] {
const S = host.sizeType;
host.declare(`declare void @scr_cyc_on_release(ptr)`);
return [
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufferedOffset}`,
` %buf = load i16, ptr %bufp`,
` %queued = icmp ne i16 %buf, 0`,
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
` %buf = load i32, ptr %bufp`,
` %queued = icmp slt i32 %buf, 0`,
` br i1 %queued, label %${done}, label %enqueue`,
`enqueue:`,
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
@@ -1102,9 +1102,9 @@ export function cycleDeadLines(host: ShapeHost, next: string): string[] {
const S = host.sizeType;
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
return [
` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufferedOffset}`,
` %dbuf = load i16, ptr %dbufp`,
` %dqueued = icmp ne i16 %dbuf, 0`,
` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
` %dbuf = load i32, ptr %dbufp`,
` %dqueued = icmp slt i32 %dbuf, 0`,
` br i1 %dqueued, label %unbuffer, label %${next}`,
`unbuffer:`,
` call void @scr_cyc_on_dead(ptr %o)`,
@@ -0,0 +1,274 @@
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, deserializeModule } from "../../index.js";
import {
F64,
VOID,
type IrExpr,
type IrFunction,
type IrModule,
type IrType,
} from "../../ir/ir.js";
import { validateModule } from "../../ir/validate.js";
import { emitLlvmModule } from "./emitter.js";
import { addRecordResultEntries, RESULT_ENTRY_PREFIX } from "./record-results.js";
async function moduleFor(source: string) {
const dir = await mkdtemp(join(tmpdir(), "scriptc-stack-objects-"));
try {
const entry = join(dir, "main.ts"),
output = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
const module = deserializeModule(await readFile(output, "utf8"));
expect(validateModule(module)).toEqual([]);
return module;
} finally {
await rm(dir, { recursive: true, force: true });
}
}
/** The body of a source function: its borrowing or i32 entry when it has one. */
function body(ir: string, name: string): string {
return new RegExp(`^define internal [^\\n]*@sc_b?f_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
ir,
)![0];
}
const VECTORS = `
interface Vec { x: number; y: number }
function add(a: Vec, b: Vec): Vec { return { x: a.x + b.x, y: a.y + b.y }; }
function dot(a: Vec, b: Vec): number { return a.x * b.x + a.y * b.y; }
function shortest(a: Vec): Vec { return a.x < a.y ? { x: a.x, y: 0 } : { x: 0, y: a.y }; }
const kept: Vec[] = [];
export function projected(n: number): number {
const p = { x: n, y: 1 };
const q = add(p, { x: 2, y: n });
return dot(q, p) + dot(shortest(q), q);
}
export function escaping(n: number): number {
const p = { x: n, y: 1 };
kept.push(p);
const q = add(p, p);
kept.push(q);
return q.x;
}
export function reassigned(n: number): number {
let p = { x: n, y: 0 };
for (let i = 0; i < n; i++) p = add(p, { x: 1, y: i });
return p.x + p.y;
}
console.log(projected(2), escaping(3), reassigned(4), kept.length);
`;
test("projection-only records live in frame storage and producers fill it", async () => {
const ir = emitLlvmModule(await moduleFor(VECTORS));
const projected = body(ir, "projected");
expect(projected).toMatch(/alloca %sc_rs_\w+, align 8/);
expect(projected).toContain("store i64 -1, ptr");
expect(projected).not.toMatch(/@sc_rnew_/);
expect(projected).toMatch(/call void @sc_\w*into_add\(/);
expect(projected).toMatch(/call void @sc_\w*into_shortest\(/);
// A literal stores every field: its storage is not zeroed first.
expect(projected).not.toContain("zeroinitializer");
// Stored values keep the heap path and the ordinary producer.
const escaping = body(ir, "escaping");
expect(escaping).toMatch(/call ptr @sc_rnew_/);
expect(escaping).toMatch(/call ptr @sc_\w*f_add\(/);
expect(escaping).not.toMatch(/alloca %sc_rs_/);
});
test("reassigned bindings build each value apart and copy it into their slot", async () => {
const ir = emitLlvmModule(await moduleFor(VECTORS));
const reassigned = body(ir, "reassigned");
expect(reassigned).not.toMatch(/@sc_rnew_/);
expect(reassigned).toMatch(/load %sc_rs_\w+, ptr %s\d+\n\s+store %sc_rs_\w+ %t\d+, ptr %s\d+/);
});
test("result-slot entries cover producers whose every return is fresh or a parameter", async () => {
const mod = await moduleFor(`
interface Vec { x: number; y: number }
function fresh(n: number): Vec { if (n > 1) return { x: n, y: n }; return { x: 0, y: 1 }; }
function passthrough(v: Vec): Vec { return v; }
function guarded(n: number): Vec { try { return { x: n, y: 0 }; } finally { console.log(n); } }
console.log(fresh(1).x, passthrough({ x: 1, y: 2 }).y, guarded(3).x);
`);
const entries = addRecordResultEntries(mod)
.functions.map((fn) => fn.name)
.filter((name) => name.startsWith(RESULT_ENTRY_PREFIX));
// A returned parameter of a shape nothing writes is copied; a return
// inside \`try\` keeps the ordinary function.
expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}fresh`, `${RESULT_ENTRY_PREFIX}passthrough`]);
});
test("objects stay on the heap when their constructor or a method lets them escape", async () => {
const ir = emitLlvmModule(
await moduleFor(`
const seen: Tracked[] = [];
class Tracked { id: number; constructor(id: number) { this.id = id; seen.push(this); } }
class Plain {
a: number; b: number;
constructor(a: number, b: number) { this.a = a; this.b = b; }
sum(): number { return this.a + this.b; }
self(): Plain { return this; }
}
const selves: Plain[] = [];
export function run(n: number): number {
const t = new Tracked(n);
const p = new Plain(n, 1);
const s = new Plain(n, 2);
selves.push(s.self());
return t.id + p.sum() + s.sum();
}
console.log(run(1), seen.length, selves.length);
`),
);
const run = body(ir, "run");
expect(run.match(/alloca %sc_o_\w+, align 8/g)).toHaveLength(1);
expect(run).toContain("call ptr @sc_new_Tracked()");
expect(run).toContain("call ptr @sc_new_Plain()");
});
test("loop bindings are never reassigned into a fixed slot", () => {
const loc = { file: "loop.ts", start: 0, end: 0 };
const point: IrType = { kind: "record", shapeId: "point" };
const points: IrType = { kind: "array", elem: point };
const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc });
const x = (obj: IrExpr): IrExpr => ({
kind: "recordGet",
obj,
shapeId: "point",
field: "x",
type: F64,
loc,
});
const work: IrFunction = {
name: "work",
loc,
returnType: F64,
params: [{ localId: "items", name: "items", type: points }],
locals: [
{ id: "items", name: "items", type: points, mutable: true },
{ id: "total", name: "total", type: F64, mutable: true },
{ id: "p", name: "p", type: point, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "total",
init: { kind: "numLit", value: 0, type: F64, loc },
loc,
},
{
kind: "forOf",
localId: "p",
iterable: ref("items", points),
body: [
{
kind: "assign",
localId: "p",
value: {
kind: "recordLit",
fields: [
{
name: "x",
value: {
kind: "bin",
op: "+",
left: x(ref("p", point)),
right: { kind: "numLit", value: 10, type: F64, loc },
type: F64,
loc,
},
},
],
type: point,
loc,
},
loc,
},
{
kind: "assign",
localId: "total",
value: {
kind: "bin",
op: "+",
left: ref("total", F64),
right: x(ref("p", point)),
type: F64,
loc,
},
loc,
},
],
loc,
},
{ kind: "return", value: ref("total", F64), loc },
],
};
const mod: IrModule = {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
records: [{ id: "point", fields: [{ name: "x", type: F64 }] }],
functions: [{ name: "main", params: [], locals: [], body: [], returnType: VOID, loc }, work],
};
expect(validateModule(mod)).toEqual([]);
const ir = body(emitLlvmModule(mod), "work");
// The literal keeps its ordinary record; the binding is the element.
expect(ir).toContain("call ptr @sc_rnew_point()");
expect(ir).not.toMatch(/alloca %sc_rs_point/);
});
test("objects updated through their methods and parameters stay in frame storage", async () => {
const ir = emitLlvmModule(
await moduleFor(`
class Stats {
n = 0; sum = 0;
add(v: number): void { this.n++; this.sum += v; }
mean(): number { return this.sum / this.n; }
}
interface Cursor { pos: number }
function advance(c: Cursor): void { c.pos += 2; }
class Box { v: number; constructor(v: number) { this.v = v; } }
export function run(k: number): number {
const s = new Stats();
for (let i = 0; i < k; i++) s.add(i);
const c: Cursor = { pos: k };
advance(c);
let b = new Box(1);
const old = b;
b = new Box(2);
return s.mean() + c.pos + old.v + b.v;
}
console.log(run(3));
`),
);
const run = body(ir, "run");
expect(run).toMatch(/alloca %sc_o_Stats, align 8/);
expect(run).toMatch(/alloca %sc_rs_\w+, align 8/);
// An alias of a reassigned binding must keep seeing the old object.
expect(run).toContain("call ptr @sc_new_Box()");
});
test("producers returning a parameter copy it only for shapes nothing writes", async () => {
const mod = await moduleFor(`
interface Vec { x: number; y: number }
interface Cell { value: number }
function pick(a: Vec, b: Vec, first: boolean): Vec { return first ? a : b; }
function forward(v: Vec): Vec { return pick(v, { x: 0, y: 0 }, v.x > 0); }
function same(c: Cell): Cell { return c; }
const c: Cell = { value: 1 };
same(c).value += 1;
console.log(pick({ x: 1, y: 2 }, { x: 3, y: 4 }, true).y, forward({ x: 1, y: 1 }).x, c.value);
`);
const entries = addRecordResultEntries(mod)
.functions.map((fn) => fn.name)
.filter((name) => name.startsWith(RESULT_ENTRY_PREFIX))
.sort();
expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}forward`, `${RESULT_ENTRY_PREFIX}pick`]);
});
@@ -0,0 +1,160 @@
import { VOID, typeEquals, type IrExpr, type IrFunction, type IrType } from "../../ir/ir.js";
import {
completeRecordLiteral,
constructorInitializesFields,
resultEntryName,
scalarRecordShape,
scalarStandaloneClass,
} from "./record-results.js";
import { everyStmtList } from "../../ir/traverse.js";
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
import type { CallLifetimes } from "./call-lifetimes.js";
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { emitRecordLitFields } from "./expr-primitives.js";
export type StackObjectSource = IrExpr & { kind: "recordLit" | "new" | "call" };
/** A record literal, `new` or result-slot producer call whose object can
* live in the frame's own storage: a plain scalar record shape, or a
* standalone scalar class whose constructor never lets `this` escape
* (record-results.ts). The caller proves that every use of the value only
* projects it. */
export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource {
// A producer with a result-slot entry builds its fresh result in the
// caller's storage.
if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee));
if (e.kind === "recordLit") {
if (e.type.kind !== "record") return false;
const shape = host.recordsById.get(e.type.shapeId);
return scalarRecordShape(shape) && completeRecordLiteral(e, shape);
}
if (e.kind !== "new") return false;
const meta = host.classMeta.get(e.className);
return (
meta !== undefined &&
scalarStandaloneClass(
meta.def,
host.fnByName.get(`%${e.className}.constructor`),
meta.hierarchy,
host.programLifetime.has(e.className),
)
);
}
/** Locals initialized once by a stack-eligible object and only projected
* afterwards (CallLifetimes.locals): field reads, and arguments to
* parameters that are themselves only projected. */
export function findLocalStackObjects(
host: LlvmEmitterContext,
fn: IrFunction,
lifetimes: CallLifetimes,
): Map<string, StackObjectSource> {
const result = new Map<string, StackObjectSource>();
const safe = lifetimes.locals.get(fn.name);
if (!safe?.size || fn.async || fn.generator) return result;
everyStmtList(fn.body, {
expr: () => true,
stmt: (stmt) => {
if (
stmt.kind === "varDecl" &&
safe.has(stmt.localId) &&
stmt.init &&
canStackObject(host, stmt.init)
)
result.set(stmt.localId, stmt.init);
return true;
},
});
return result;
}
/** Reassigned locals declared with an initializer, whose every write
* builds a stack-eligible object of the binding's own type, and whose uses
* only project the value
* (CallLifetimes.projectedLocals). The binding keeps one slot; each
* assignment builds its value in fresh storage, which may read the old
* value, and then copies it into the binding's slot. */
export function findReassignedStackObjects(
host: LlvmEmitterContext,
fn: IrFunction,
lifetimes: CallLifetimes,
): Set<string> {
const result = new Set<string>();
const projected = lifetimes.projectedLocals.get(fn.name);
const stable = lifetimes.locals.get(fn.name);
if (!projected?.size || fn.async || fn.generator) return result;
const types = new Map(fn.locals.map((local) => [local.id, local.type]));
for (const id of projected) {
const type = types.get(id);
if (!stable?.has(id) && (type?.kind === "record" || type?.kind === "object")) result.add(id);
}
// The binding must be declared by its own initializer: a loop that binds
// it (`for...of`) would point it at each element, not at the slot.
const initialized = new Set<string>();
everyStmtList(fn.body, {
expr: () => true,
stmt: (stmt) => {
if (stmt.kind === "forOf") result.delete(stmt.localId);
if (stmt.kind === "varDecl" && stmt.init) initialized.add(stmt.localId);
if ((stmt.kind === "varDecl" || stmt.kind === "assign") && result.has(stmt.localId)) {
const value = stmt.kind === "varDecl" ? stmt.init : stmt.value;
if (
!value ||
!canStackObject(host, value) ||
!typeEquals(value.type, types.get(stmt.localId)!)
)
result.delete(stmt.localId);
}
return true;
},
});
for (const id of result) if (!initialized.has(id)) result.delete(id);
return result;
}
export function stackObjectStruct(type: IrType): string {
return type.kind === "record"
? `%${mangleRecordStruct(type.shapeId)}`
: `%${mangleClassStruct((type as IrType & { kind: "object" }).className)}`;
}
/** Build the object in an entry-block slot (or `into`, an existing one).
* The immortal reference count makes every retain and release a no-op;
* the proof that the value is only projected keeps the pointer from
* outliving the frame. */
export function emitStackObject(
host: LlvmEmitterContext,
e: StackObjectSource,
into?: string,
): LlValue {
const B = host.B;
const type = e.type;
const struct = stackObjectStruct(type);
let slot = into;
if (slot === undefined) {
slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${struct}, align 8`);
}
// Literals store every field; constructors that store every field
// before reading one need no zeroed storage either.
if (type.kind === "object") {
const cls = host.classMeta.get(type.className)!.def;
const ctor = host.fnByName.get(`%${type.className}.constructor`)!;
if (!constructorInitializesFields(cls, ctor))
B.line(`store ${struct} zeroinitializer, ptr ${slot}`);
}
B.line(`store ${host.sizeType} -1, ptr ${slot}`);
if (e.kind === "call") {
// The entry writes the result into the slot it receives last.
emitCallExpr(
host,
{ ...e, callee: resultEntryName(e.callee), type: VOID },
false,
false,
host.markImmortal({ name: slot, type }),
);
} else if (e.kind === "recordLit") emitRecordLitFields(host, e, slot);
else emitConstruction(host, e, slot, host.fnByName.get(`%${e.className}.constructor`)!);
return { name: slot, type: e.type };
}
@@ -479,7 +479,18 @@ export async function compileCInternal(
// musl deliberately has no libc-identification predefine; resolveCc's
// SCR_MUSL flag and this target-selected TU travel together.
...(isMuslTarget(driver) ? [rt(join(rtDir, "scr_musl.c"))] : []),
...(regex ? ["-I", vendorEngineDir(), rt(join(rtDir, "scr_regex.c")), ...lreObjects] : []),
// scr_glob.c (path.matchesGlob) compiles its segments through the
// regex unit, so it rides the same switch; section GC drops it from
// binaries that never match a glob.
...(regex
? [
"-I",
vendorEngineDir(),
rt(join(rtDir, "scr_regex.c")),
rt(join(rtDir, "scr_glob.c")),
...lreObjects,
]
: []),
...(opts.assert || regex || opts.symbol ? [rt(join(rtDir, "scr_assert.c"))] : []),
...(opts.inspect
? [rt(join(rtDir, "scr_inspect.c")), rt(join(rtDir, "scr_console_native.c"))]
@@ -105,7 +105,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
// Zig's musl sysroot does not provide arc4random_buf. Keep the fallback
// inside the archive so library embedders need no extra system library.
...(isMuslTarget(driver) ? ["scr_musl.c"] : []),
...(regex ? ["scr_regex.c"] : []),
...(regex ? ["scr_regex.c", "scr_glob.c"] : []),
...(opts.assert || regex || opts.symbol ? ["scr_assert.c"] : []),
...(opts.inspect ? ["scr_inspect.c", "scr_console_native.c"] : []),
...(opts.symbol ? ["scr_symbol.c"] : []),
+2 -2
View File
@@ -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 = 19 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v19" as const;
export const RUNTIME_ABI_VERSION = 20 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v20" 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: 19, marker: "scr_runtime_abi_v19" },
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -116,7 +116,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -26,7 +26,12 @@ import {
typeEquals,
} from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { buildArraySortFn } from "../lower-array-sort.js";
import {
type SortKeyOrder,
buildArraySortFn,
buildSortKeyFn,
recognizeKeyComparator,
} from "../lower-array-sort.js";
import {
arrayIndexPresent,
arrayValueRead,
@@ -2098,7 +2103,8 @@ export function lowerArraySortCall(
elem,
elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : undefined,
);
const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}`;
const keyed = native ? recognizedSortKey(lowerer, fnArg, elem) : null;
const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${keyed.order}${keyed.read ? ":field" : ""}` : ""}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
@@ -2113,10 +2119,43 @@ export function lowerArraySortCall(
undefinedTag >= 0 ? undefinedTag : null,
loc,
native,
keyed && { order: keyed.order, key: keyed.read !== null },
),
);
}
return { kind: "call", callee: helper, args: [receiver, fnArg], type: arrT, loc };
const args = [receiver, fnArg];
if (keyed?.read) args.push(sortKeyReader(lowerer, elem, keyed.read, loc));
return { kind: "call", callee: helper, args, type: arrT, loc };
}
/** The key order of a capture-free comparator closure the runtime can sort
* by without calling it: over number elements, or over one number field
* of record or class elements. */
function recognizedSortKey(lowerer: Lowerer, fnArg: IrExpr, elem: IrType): SortKeyOrder | null {
if (fnArg.kind !== "closure" || fnArg.captures.length > 0 || fnArg.adapts) return null;
const fn = lowerer.liftedFns.find((lifted) => lifted.name === fnArg.fnName);
const keyed = fn ? recognizeKeyComparator(fn) : null;
if (!keyed) return null;
if (keyed.read === null) return elem.kind === "f64" ? keyed : null;
return elem.kind === "record" || elem.kind === "object" ? keyed : null;
}
/** A zero-capture closure over the shared `(x) => x.field` key reader. */
function sortKeyReader(
lowerer: Lowerer,
elem: IrType,
read: NonNullable<SortKeyOrder["read"]>,
loc: SrcLoc,
): IrExpr {
const owner = read.kind === "recordGet" ? read.shapeId : read.className;
const cacheKey = `sortKey:${typeKey(elem)}:${read.kind}:${owner}:${read.field}`;
let reader = lowerer.arrHofHelpers.get(cacheKey);
if (!reader) {
reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(cacheKey, reader);
lowerer.liftedFns.push(buildSortKeyFn(reader, elem, read, loc));
}
return { kind: "closure", fnName: reader, captures: [], type: funcOf([elem], F64), loc };
}
/** `d.filter(pred)` on a dyn receiver: the receiver must BE a dyn array
@@ -1,9 +1,71 @@
import { expect, test } from "vitest";
import { F64, DYN, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import {
BOOL,
F64,
DYN,
type IrExpr,
type IrFunction,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import { numLit, varRef } from "../../ir/build.js";
import { everyStmtList } from "../../ir/traverse.js";
import { buildArraySortFn, buildBytesSortFn } from "./lower-array-sort.js";
import {
buildArraySortFn,
buildBytesSortFn,
buildSortKeyFn,
recognizeKeyComparator,
} from "./lower-array-sort.js";
const loc = { file: "sort.ts", start: 0, end: 0 };
const ROW: IrType = { kind: "record", shapeId: "r0" };
const a = (type: IrType = F64): IrExpr => varRef("a.0", type, loc);
const b = (type: IrType = F64): IrExpr => varRef("b.0", type, loc);
const field = (obj: IrExpr, name = "k", type: IrType = F64): IrExpr & { kind: "recordGet" } => ({
kind: "recordGet",
obj,
shapeId: "r0",
field: name,
type,
loc,
});
const sub = (left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op: "-",
left,
right,
type: F64,
loc,
});
const cmp = (op: "<" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: BOOL,
loc,
});
const pick = (cond: IrExpr, then: IrExpr, else_: IrExpr): IrExpr => ({
kind: "ternary",
cond,
then,
else_,
type: F64,
loc,
});
const comparator = (value: IrExpr, param: IrType = F64, extra: Partial<IrFunction> = {}) => ({
name: "%fn0",
params: [
{ localId: "a.0", name: "a", type: param },
{ localId: "b.0", name: "b", type: param },
],
returnType: F64,
locals: [],
captures: [],
body: [{ kind: "return" as const, value, loc }],
loc,
...extra,
});
function repeatedNodes(fn: IrFunction): number {
const seen = new Set<IrExpr | IrStmt>();
@@ -24,8 +86,87 @@ test("sort helpers give every IR site its own node", () => {
buildArraySortFn("%arr.toSorted.1", F64, 1, true, null, loc),
buildArraySortFn("%arr.sort.2", DYN, 2, false, null, loc),
buildArraySortFn("%arr.sort.3", union, 2, true, 1, loc),
buildArraySortFn("%arr.sort.6", F64, 2, false, null, loc, true, { order: -1, key: false }),
buildArraySortFn("%arr.toSorted.7", ROW, 2, true, null, loc, true, { order: 1, key: true }),
buildSortKeyFn("%arr.sortKey.8", ROW, field(varRef("a.0", ROW, loc)), loc),
buildBytesSortFn("%bytes.sort.4", 2, true, loc),
buildBytesSortFn("%bytes.sort.5", 0, false, loc),
];
for (const fn of helpers) expect(repeatedNodes(fn), fn.name).toBe(0);
});
test("recognizes subtraction comparators over numbers", () => {
expect(recognizeKeyComparator(comparator(sub(a(), b())))).toEqual({ order: 1, read: null });
expect(recognizeKeyComparator(comparator(sub(b(), a())))).toEqual({ order: -1, read: null });
});
test("recognizes subtraction comparators over one field", () => {
const ascending = recognizeKeyComparator(comparator(sub(field(a(ROW)), field(b(ROW))), ROW));
expect(ascending?.order).toBe(1);
expect(ascending?.read).toMatchObject({ kind: "recordGet", field: "k" });
const descending = recognizeKeyComparator(comparator(sub(field(b(ROW)), field(a(ROW))), ROW));
expect(descending?.order).toBe(-1);
});
test("recognizes three-way ternaries in either test order and direction", () => {
const three = (first: "<" | ">", firstSign: number, x: IrExpr, y: IrExpr) =>
pick(
cmp(first, x, y),
numLit(firstSign, loc),
pick(cmp(first === "<" ? ">" : "<", x, y), numLit(-firstSign, loc), numLit(0, loc)),
);
expect(recognizeKeyComparator(comparator(three("<", -1, a(), b())))?.order).toBe(1);
expect(recognizeKeyComparator(comparator(three(">", 1, a(), b())))?.order).toBe(1);
expect(recognizeKeyComparator(comparator(three("<", 1, a(), b())))?.order).toBe(-1);
expect(recognizeKeyComparator(comparator(three("<", -1, b(), a())))?.order).toBe(-1);
const fields = three("<", -1, field(a(ROW)), field(b(ROW)));
expect(recognizeKeyComparator(comparator(fields, ROW))?.order).toBe(1);
});
test("rejects comparators that are not one consistent numeric key", () => {
const rejected: IrExpr[] = [
sub(a(), a()),
sub(field(a(ROW)), field(b(ROW), "j")),
sub(field(a(ROW)), b()),
sub(sub(a(), b()), numLit(1, loc)),
sub(a(), varRef("c.0", F64, loc)),
pick(
cmp("<", a(), b()),
numLit(-1, loc),
pick(cmp("<", a(), b()), numLit(1, loc), numLit(0, loc)),
),
pick(
cmp("<", a(), b()),
numLit(-1, loc),
pick(cmp(">", a(), b()), numLit(1, loc), numLit(-1, loc)),
),
pick(
cmp("<", a(), b()),
numLit(-2, loc),
pick(cmp(">", a(), b()), numLit(2, loc), numLit(0, loc)),
),
pick(
cmp("<", a(), b()),
numLit(-1, loc),
pick(cmp(">", a(), b()), numLit(-1, loc), numLit(0, loc)),
),
];
for (const value of rejected) expect(recognizeKeyComparator(comparator(value))).toBeNull();
expect(
recognizeKeyComparator(comparator(sub(field(a(ROW), "k", ROW), field(b(ROW), "k", ROW)), ROW)),
).toBeNull();
});
test("rejects comparators with captures or other bodies", () => {
expect(
recognizeKeyComparator(
comparator(sub(a(), b()), F64, { captures: [{ localId: "c.0", name: "c", type: F64 }] }),
),
).toBeNull();
expect(recognizeKeyComparator(comparator(sub(a(), b()), F64, { async: true }))).toBeNull();
const body: IrStmt[] = [
{ kind: "exprStmt", expr: a(), loc },
{ kind: "return", value: sub(a(), b()), loc },
];
expect(recognizeKeyComparator({ ...comparator(sub(a(), b())), body })).toBeNull();
});
@@ -297,6 +297,114 @@ function stableSort(b: SortIr, src: SortLocal, n: IrExpr, greater: Greater): IrS
];
}
type SortKeyRead = IrExpr & { kind: "recordGet" | "fieldGet" };
/** A comparator that orders by one number: the elements themselves or one
* field of each, ascending or descending. */
export interface SortKeyOrder {
/** 1 for ascending, -1 for descending. */
order: 1 | -1;
/** The field read over the comparator's first parameter, or null when
* the elements are the numbers. */
read: SortKeyRead | null;
}
/** Recognizes the lifted body of a capture-free numeric comparator:
* `(a, b) => a - b`, `b - a`, `a < b ? -1 : a > b ? 1 : 0` (either test
* first, either sign), or any of them over one number field of both
* parameters (`a.k - b.k`). Such a comparator runs no user code, cannot
* throw, and is consistent over numbers other than NaN, so the runtime may
* order the elements by their keys without calling it. */
export function recognizeKeyComparator(fn: IrFunction): SortKeyOrder | null {
if (fn.params.length !== 2 || fn.async || fn.generator || (fn.captures?.length ?? 0) > 0)
return null;
const [first, second] = fn.params;
const ret = fn.body.length === 1 ? fn.body[0]! : null;
if (ret?.kind !== "return" || !ret.value) return null;
let read: SortKeyRead | null | undefined;
// Which parameter an operand reads; every operand must read the same key.
const operand = (e: IrExpr): 0 | 1 | null => {
if (e.type.kind !== "f64") return null;
let ref: IrExpr = e;
let field: SortKeyRead | null = null;
if (e.kind === "recordGet" || e.kind === "fieldGet") {
field = e;
ref = e.obj;
}
if (ref.kind !== "varRef") return null;
const param = ref.localId === first!.localId ? 0 : ref.localId === second!.localId ? 1 : null;
if (param === null) return null;
if (read === undefined) read = field;
else if (!sameKeyRead(read, field)) return null;
return param;
};
// `x < y` or `x > y` over both parameters, as the first one's relation.
const relation = (e: IrExpr): "lt" | "gt" | null => {
if (e.kind !== "bin" || (e.op !== "<" && e.op !== ">")) return null;
const left = operand(e.left),
right = operand(e.right);
if (left === null || right === null || left === right) return null;
return (e.op === "<") === (left === 0) ? "lt" : "gt";
};
const sign = (e: IrExpr): number | null =>
e.kind === "numLit" && (e.value === 1 || e.value === -1 || e.value === 0) ? e.value : null;
const value = ret.value;
if (value.kind === "bin" && value.op === "-") {
const left = operand(value.left),
right = operand(value.right);
if (left === null || right === null || left === right) return null;
return { order: left === 0 ? 1 : -1, read: read ?? null };
}
if (value.kind === "ternary" && value.else_.kind === "ternary") {
const outer = relation(value.cond),
inner = relation(value.else_.cond);
const outerSign = sign(value.then),
innerSign = sign(value.else_.then);
if (
outer === null ||
inner === null ||
outer === inner ||
outerSign === null ||
outerSign === 0 ||
innerSign !== -outerSign ||
sign(value.else_.else_) !== 0
)
return null;
// Ascending answers negative when the first parameter is less.
return { order: (outer === "lt") === outerSign < 0 ? 1 : -1, read: read ?? null };
}
return null;
}
function sameKeyRead(left: SortKeyRead | null, right: SortKeyRead | null): boolean {
if (left === null || right === null) return left === right;
if (left.kind === "recordGet" && right.kind === "recordGet")
return left.shapeId === right.shapeId && left.field === right.field;
if (left.kind === "fieldGet" && right.kind === "fieldGet")
return left.className === right.className && left.field === right.field;
return false;
}
/** The key reader of a recognized field comparator: `(x) => x.k`, the same
* field read as the comparator's. */
export function buildSortKeyFn(
name: string,
elem: IrType,
read: SortKeyRead,
loc: SrcLoc,
): IrFunction {
const b = new SortIr(loc);
const x = b.local("x", elem);
return {
name,
params: [{ localId: x.localId, name: "x", type: elem }],
returnType: F64,
locals: b.locals,
body: [{ kind: "return", value: { ...read, obj: x, loc }, loc }],
loc,
};
}
export function buildArraySortFn(
name: string,
elem: IrType,
@@ -305,12 +413,18 @@ export function buildArraySortFn(
undefinedTag: number | null,
loc: SrcLoc,
native = false,
keyed: { order: 1 | -1; key: boolean } | null = null,
): IrFunction {
const b = new SortIr(loc);
const arrT = arrayOf(elem),
fnT = funcOf([elem, elem].slice(0, arity), F64);
const a = b.local("a", arrT),
f = b.local("f", fnT);
const keyT = funcOf([elem], F64);
const key = keyed?.key ? b.local("key", keyT) : null;
// A recognized comparator lets the runtime order by keys; it still
// receives the comparator for inputs whose keys it does not order.
const keyArgs: IrExpr[] = keyed ? [b.num(keyed.order), ...(key ? [key] : [])] : [];
const src = b.local("src", arrT);
const n = b.local("n"),
valueCount = b.local("valueCount"),
@@ -361,7 +475,7 @@ export function buildArraySortFn(
kind: "arrIntrinsic",
method: "sortDense",
receiver,
args: [f, { kind: "boolLit", value: copyFirst, type: BOOL, loc }],
args: [f, { kind: "boolLit", value: copyFirst, type: BOOL, loc }, ...keyArgs],
type: BOOL,
loc,
},
@@ -399,7 +513,7 @@ export function buildArraySortFn(
kind: "arrIntrinsic" as const,
method: "sortValues" as const,
receiver: src,
args: [valueCount, f],
args: [valueCount, f, ...keyArgs],
type: VOID,
loc,
},
@@ -427,6 +541,7 @@ export function buildArraySortFn(
params: [
{ localId: a.localId, name: "a", type: arrT },
{ localId: f.localId, name: "f", type: fnT },
...(key ? [{ localId: key.localId, name: "key", type: keyT }] : []),
],
returnType: arrT,
locals: b.locals,
@@ -918,6 +918,12 @@ const PATH_MODULE_FNS: Record<string, BuiltinModuleFn | undefined> = {
result: STRING,
valueParams: [{ mode: "required", type: STRING }],
},
matchesGlob: {
fn: "path.matchesGlob",
params: [STRING, STRING],
result: BOOL,
valueParams: exactValueParams(STRING, STRING),
},
// Special-cased (lowerPathParseCall / lowerPathFormatCall): the
// ParsedPath record and the pathObject argument follow the call site's
// mapped shapes.
@@ -992,6 +998,12 @@ const PATH_WIN32_MODULE_FNS: Record<string, BuiltinModuleFn | undefined> = {
result: STRING,
valueParams: [{ mode: "required", type: STRING }],
},
matchesGlob: {
fn: "path.win32MatchesGlob",
params: [STRING, STRING],
result: BOOL,
valueParams: exactValueParams(STRING, STRING),
},
// Special-cased (lowerPathParseCall / lowerPathFormatCall): the
// ParsedPath record and the pathObject argument follow the call site's
// mapped shapes.
@@ -466,6 +466,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.arrayProtoCall",
"process.chdir",
"os.getPriority",
// Pattern length TypeError, brace-sequence RangeError, and the
// SyntaxError of a segment regex invalid under the u flag.
"path.matchesGlob",
"path.win32MatchesGlob",
"process.umask",
"process.loadEnvFile",
"fs.realpathSync",
@@ -416,6 +416,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"path.win32Parse": { argTypes: [STRING], result: arrayOf(STRING) },
"path.format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING },
"path.win32Format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING },
/** path.matchesGlob(path, pattern): Node's matchGlobPattern over its
* bundled minimatch (scr_glob.c). Both families match case-insensitively
* on macOS and Windows hosts like Node's; win32 also accepts either
* separator. Throws TypeError for a pattern over 64 Ki code units,
* RangeError for a brace sequence too long to expand, and SyntaxError
* when a segment's regex is invalid under the u flag. */
"path.matchesGlob": { argTypes: [STRING, STRING], result: BOOL },
"path.win32MatchesGlob": { argTypes: [STRING, STRING], result: BOOL },
/** node:os: homedir is $HOME else getpwuid(3); tmpdir is Node's env
* cascade ($TMPDIR/$TMP/$TEMP else /tmp, one trailing slash trimmed).
* os.platform() lowers to process.platform — one implementation. */
+9 -2
View File
@@ -1829,7 +1829,13 @@ export type IrArrIntrinsicMethod =
* arguments are [value count, comparator]. Every slot below the count is
* a value of an `isSortValuesElement` type; the comparator is invoked
* with exactly its declared arity. Returns void; comparator exceptions
* propagate and leave the snapshot unchanged. */
* propagate and leave the snapshot unchanged. Optional trailing
* arguments [order, key] describe a comparator that orders numbers
* ascending (order 1) or descending (-1), a numeric literal: the runtime
* may then sort by key without calling it. Number elements are their own
* keys; record and class elements pass the capture-free key reader
* `(elem) => number` both comparator operands apply. Keys the runtime
* does not order directly (NaN, -0) still sort through the comparator. */
| "sortValues"
/** Internal stable comparator sort of a dense array in place: arguments
* are [comparator, owned]. Returns false, without calling the comparator,
@@ -1837,7 +1843,8 @@ export type IrArrIntrinsicMethod =
* is published; the caller then sorts a compacted snapshot. Otherwise the
* receiver's values are sorted outside it and written back only after
* success, as the spec's sort writes them, and it returns true. `owned`
* (a boolean literal) marks a private copy no user code can reach. */
* (a boolean literal) marks a private copy no user code can reach.
* Optional trailing arguments [order, key] as for sortValues. */
| "sortDense"
| "length"
/** Internal ToNumber(a[index]) for f64-backed arrays: one numeric index,
+3 -1
View File
@@ -116,7 +116,9 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
if (
fn === "regexp.escape" ||
fn === "dyn.nativeRegexIs" ||
fn === "util.stripVTControlCharacters"
fn === "util.stripVTControlCharacters" ||
fn === "path.matchesGlob" ||
fn === "path.win32MatchesGlob"
)
features.regex = true;
if (
+74
View File
@@ -14,6 +14,7 @@ import {
mapOf,
setOf,
type IrExpr,
type IrFunction,
type IrModule,
type IrType,
type IrUnionDef,
@@ -1918,6 +1919,79 @@ test("runtime comparator sort validates element kinds and the comparator shape",
}
});
test("runtime comparator sort validates a recognized comparator's key order", () => {
const row: IrType = { kind: "record", shapeId: "r0" };
const closure = (fnName: string, params: IrType[], ret: IrType = F64): IrExpr => ({
kind: "closure",
fnName,
captures: [],
type: funcOf(params, ret),
loc,
});
const order = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const keyedModule = (elem: IrType, keyArgs: IrExpr[], dense = false): IrModule => {
const receiver: IrExpr = { kind: "arrayLit", elems: [], type: arrayOf(elem), loc };
const cmp = closure("cmp", [elem, elem]);
const mod = numericReadModule(
dense
? {
method: "sortDense",
receiver,
args: [cmp, { kind: "boolLit", value: false, type: BOOL, loc }, ...keyArgs],
type: BOOL,
}
: { method: "sortValues", receiver, args: [order(0), cmp, ...keyArgs], type: VOID },
);
mod.records = [{ id: "r0", fields: [{ name: "k", type: F64 }] }];
const fn = (name: string, params: IrType[], ret: IrType): IrFunction => ({
name,
params: params.map((type, index) => ({ localId: `p${index}`, name: `p${index}`, type })),
locals: params.map((type, index) => ({
id: `p${index}`,
name: `p${index}`,
type,
mutable: false,
})),
returnType: ret,
body: [
{
kind: "return",
value:
ret.kind === "f64"
? { kind: "numLit", value: 0, type: F64, loc }
: { kind: "strLit", value: "", type: STRING, loc },
loc,
},
],
loc,
});
mod.functions.push(
fn("cmp", [elem, elem], F64),
fn("key", [row], F64),
fn("text", [row], STRING),
);
return mod;
};
for (const mod of [
keyedModule(F64, [order(1)]),
keyedModule(F64, [order(-1)], true),
keyedModule(row, [order(1), closure("key", [row])]),
keyedModule(row, [order(-1), closure("key", [row])], true),
]) {
expect(validateModule(mod)).toEqual([]);
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
}
for (const [mod, message] of [
[keyedModule(F64, [order(2)]), "key order must be the literal 1 or -1"],
[keyedModule(F64, [order(1), closure("key", [row])]), "key order on f64"],
[keyedModule(STRING, [order(1)], true), "key order on string"],
[keyedModule(row, [order(1)]), "key reader must be (elem) => number"],
[keyedModule(row, [order(1), closure("text", [row], STRING)]), "key reader must be"],
] as const) {
expect(validateModule(mod).some((error) => error.message.includes(message))).toBe(true);
}
});
test("a caught-typed global records a module error only from a catch binding", () => {
const caught: IrType = { kind: "caught" };
const errorId = "%g.e.%loaded%error";
+39 -3
View File
@@ -2334,9 +2334,15 @@ function validateFunction(
: e.method === "shift"
? { argTypes: [], result: e.type } // union-checked below
: e.method === "sortValues"
? { argTypes: [F64, null], result: VOID }
? {
argTypes: [F64, null, F64, null],
result: VOID,
}
: e.method === "sortDense"
? { argTypes: [null, BOOL], result: BOOL }
? {
argTypes: [null, BOOL, F64, null],
result: BOOL,
}
: { argTypes: [], result: F64 }; // length
if (
(e.method === "sortPrimitive" || e.method === "toSortedPrimitive") &&
@@ -2361,6 +2367,29 @@ function validateFunction(
err(`arrIntrinsic ${e.method} comparator must be (elem, elem) => number`, e.loc);
if (e.method === "sortDense" && e.args[1]?.kind !== "boolLit")
err("arrIntrinsic sortDense owned flag must be a boolean literal", e.loc);
// A recognized comparator's key order: [order] for numbers,
// [order, key reader] for record and class elements.
const order = e.args[2];
if (order) {
if (order.kind !== "numLit" || (order.value !== 1 && order.value !== -1))
err(`arrIntrinsic ${e.method} key order must be the literal 1 or -1`, e.loc);
const key = e.args[3]?.type;
if (
elem.kind === "f64"
? key !== undefined
: elem.kind !== "record" && elem.kind !== "object"
)
err(`arrIntrinsic ${e.method} key order on ${elem.kind} elements`, e.loc);
if (
elem.kind !== "f64" &&
(!key ||
key.kind !== "func" ||
key.params.length !== 1 ||
!typeEquals(key.params[0]!, elem) ||
key.ret.kind !== "f64")
)
err(`arrIntrinsic ${e.method} key reader must be (elem) => number`, e.loc);
}
}
if (e.method === "getNumber" && elem.kind !== "f64") {
err(`arrIntrinsic getNumber requires f64 elements, got ${elem.kind}`, e.loc);
@@ -2420,7 +2449,14 @@ function validateFunction(
// slice's indices and splice's count are optional (omitted args
// omitted from the IR — backends fill the defaults); everything
// else is exact.
const minArgs = e.method === "slice" ? 0 : e.method === "splice" ? 1 : sig.argTypes.length;
const minArgs =
e.method === "slice"
? 0
: e.method === "splice"
? 1
: e.method === "sortValues" || e.method === "sortDense"
? 2
: sig.argTypes.length;
if (e.args.length < minArgs || e.args.length > sig.argTypes.length) {
err(
`arrIntrinsic ${e.method}: ${e.args.length} args, expected ${sig.argTypes.length}`,
+18
View File
@@ -1496,6 +1496,12 @@
"name": "path.join",
"status": "static"
},
{
"id": "node-builtin.path.matchesGlob",
"kind": "node-builtin",
"name": "path.matchesGlob",
"status": "static"
},
{
"id": "node-builtin.path.normalize",
"kind": "node-builtin",
@@ -1558,6 +1564,12 @@
"name": "path/posix.join",
"status": "static"
},
{
"id": "node-builtin.path.posix.matchesGlob",
"kind": "node-builtin",
"name": "path/posix.matchesGlob",
"status": "static"
},
{
"id": "node-builtin.path.posix.normalize",
"kind": "node-builtin",
@@ -1670,6 +1682,12 @@
"name": "path/win32.join",
"status": "static"
},
{
"id": "node-builtin.path.win32.matchesGlob",
"kind": "node-builtin",
"name": "path/win32.matchesGlob",
"status": "static"
},
{
"id": "node-builtin.path.win32.normalize",
"kind": "node-builtin",
@@ -312,7 +312,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
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_v19");
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v20");
const clangExe = join(dir, "clang-program");
const linkDriver = join(dir, "clang-driver.c");
@@ -646,7 +646,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
.split("\n");
expect(undefinedSymbols).toContain("_sf_scale");
expect(undefinedSymbols).toContain("_sf_callback_mix");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v19");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v20");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5120-frame-records.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5121-frame-objects.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5122-frame-result-slots.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5123-frame-reassigned.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5124-frame-objects-control.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5125-frame-mutating-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5126-frame-returned-params.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5127-frame-loop-bindings.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5145-path-matches-glob-basics.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5146-path-matches-glob-braces.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5147-path-matches-glob-extglob.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5148-path-matches-glob-classes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5149-path-matches-glob-win32.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5155-cycle-header-types.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5160-field-callback-receiver.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5161-field-callback-receiver-escape.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5162-field-method-reference-receiver.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5163-field-callback-receiver.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5164-worker-field-callback-receiver.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5200-array-sort-keyed-numbers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5201-array-sort-keyed-records.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5202-array-sort-keyed-lookalikes.ts"
],
"diags": []
}
@@ -57,6 +57,7 @@ const optional = [
["scr_copying.c", "copying"],
["scr_file_handle.c", "fileHandle"],
["scr_regex.c", "regex"],
["scr_glob.c", "regex"],
["scr_assert.c", any("assert", "regex", "symbol")],
["scr_inspect.c", "inspect"],
["scr_console_native.c", "inspect"],
@@ -306,7 +306,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target,
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
compiler: {
command: compiler,
identity: compilerVersion,
@@ -3,6 +3,7 @@ export function withLibraryRuntimeFlavors(matrix) {
const omitted = new Set(["scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_ffi.c"]);
const optional = new Set([
"scr_regex.c",
"scr_glob.c",
"scr_assert.c",
"scr_inspect.c",
"scr_console_native.c",
@@ -47,6 +47,7 @@ const OPTIONAL = [
["scr_copying.c", "copying"],
["scr_file_handle.c", "fileHandle"],
["scr_regex.c", "regex"],
["scr_glob.c", "regex"],
["scr_assert.c", any("assert", "regex", "symbol")],
["scr_inspect.c", "inspect"],
["scr_console_native.c", "inspect"],
@@ -402,7 +402,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: matrix.target,
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
compiler: {
command: compiler,
identity: compilerVersion,
@@ -40,6 +40,7 @@ test("library pack modes preserve pure runtime features and exclude ambient even
expect(sources).not.toContain(source);
for (const source of [
"scr_regex.c",
"scr_glob.c",
"scr_assert.c",
"scr_events_emitter.c",
"scr_zlib.c",
+395 -10
View File
@@ -887,7 +887,7 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s
small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0), false) : 0;
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr);
if (cyc) {
static const ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
a = scr_cyc_alloc_inline(size, &cyc_type);
} else {
a = scr_mem_alloc(size);
@@ -2371,6 +2371,8 @@ typedef struct {
uint32_t arity;
bool numbers;
bool borrowing; /* enter through f->fn_borrowed, lending the slots */
int32_t order; /* 1 or -1: f orders number keys ascending or descending */
ScrClosure *key; /* the key reader of reference elements under `order` */
} ScrSortCompare;
static void *scr_sort_retain_str(void *p) { return scr_str_retain((ScrStr *)p); }
@@ -2431,8 +2433,12 @@ static bool scr_sort_greater(const ScrSortCompare *c, uint64_t left, uint64_t ri
#define SCR_SORT_PREFETCH(c, buf, index, limit) \
SCR_SORT_PREFETCH_AT(c, buf, (index) + SCR_SORT_PREFETCH_DISTANCE, limit)
static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint32_t arity) {
ScrSortCompare c = {f, NULL, arity, a->elem == SCR_ELEM_F64, false};
static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint32_t arity,
int32_t order, ScrClosure *key) {
ScrSortCompare c = {f, NULL, arity, a->elem == SCR_ELEM_F64, false, order, key};
if (order != 0 && (arity != 2 || (order != 1 && order != -1) ||
(c.numbers ? key != NULL : a->elem != SCR_ELEM_REF || key == NULL)))
scr_trap("scriptc: invalid keyed sort\n");
if (!c.numbers) {
c.borrowing = f->fn_borrowed != NULL;
if (a->elem == SCR_ELEM_STR) c.retain = scr_sort_retain_str;
@@ -2444,6 +2450,380 @@ static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint3
return c;
}
/* ── keyed sort for recognized numeric comparators ───────────────────── */
/* `a - b`, `b - a`, `a.k - b.k` and the three-way ternaries order numbers
* other than NaN consistently, call no user code and treat only equal
* numbers as equal, except that -0 equals 0. When no key is NaN or -0, a
* stable sort by key therefore produces exactly the comparator sort's
* permutation. Keys map to unsigned integers in the same order (descending
* order complements them). A first scan reads every key without storing
* it: ordered input stays as it is, strictly descending input reverses,
* a short tail after ascending reference keys is placed by binary search,
* and input of a few ascending runs merges them. Anything else takes an
* LSD radix sort, stable by construction, which skips the digit positions
* every key shares, so small integers take about half the passes. */
typedef struct {
uint64_t key;
uint64_t slot;
} ScrSortItem;
#define SCR_SORT_SIGN (UINT64_C(1) << 63)
/* Inputs of at most this many ascending runs merge instead. */
#ifndef SCR_SORT_KEYED_RUNS
#define SCR_SORT_KEYED_RUNS 16
#endif
/* Below this length, binary insertion beats the radix passes' setup. */
#ifndef SCR_SORT_KEYED_INSERTION
#define SCR_SORT_KEYED_INSERTION 48
#endif
/* The order-preserving unsigned key of a number, false for NaN and -0. */
static bool scr_sort_key_encode(double value, uint64_t flip, uint64_t *key) {
uint64_t bits;
memcpy(&bits, &value, sizeof bits);
if (value != value || bits == SCR_SORT_SIGN) return false;
*key = ((bits & SCR_SORT_SIGN) ? ~bits : bits | SCR_SORT_SIGN) ^ flip;
return true;
}
static uint64_t scr_sort_key_decode(uint64_t key, uint64_t flip) {
key ^= flip;
return (key & SCR_SORT_SIGN) ? key & ~SCR_SORT_SIGN : ~key;
}
/* Longer inputs sort by 11-bit digits (six passes) rather than bytes: the
* larger count tables then pay for themselves. */
#ifndef SCR_SORT_WIDE_DIGITS
#define SCR_SORT_WIDE_DIGITS ((size_t)1 << 17)
#endif
/* What the first scan learned about the keys it read. */
typedef struct {
size_t scanned; /* keys read: all n unless the input has many runs */
size_t head; /* the length of the first ascending run */
size_t runs; /* ascending runs among them */
bool descending; /* strictly */
bool valid; /* no NaN or -0 among them */
} ScrSortScan;
#define SCR_SORT_SCAN(scan, n, READ) \
do { \
uint64_t previous_ = 0; \
(scan).runs = 1; \
(scan).head = (n); \
(scan).descending = true; \
(scan).valid = true; \
size_t i_ = 0; \
for (; i_ < (n); i_++) { \
uint64_t key_; \
if (!scr_sort_key_encode(READ(i_), flip, &key_)) { \
(scan).valid = false; \
break; \
} \
if (i_) { \
if (previous_ > key_ && (scan).runs == 1) (scan).head = i_; \
(scan).runs += previous_ > key_; \
(scan).descending &= previous_ > key_; \
if ((scan).runs > SCR_SORT_KEYED_RUNS && !(scan).descending) { \
i_++; \
break; \
} \
} \
previous_ = key_; \
} \
(scan).scanned = i_; \
} while (0)
/* Binary insertion of buf[end..limit) into the sorted buf[start..end),
* after equal keys; leaves `end` at `limit`. */
#define SCR_SORT_INSERT_UNTIL(T, KEY, buf, start, end, limit) \
do { \
for (; (end) < (limit); (end)++) { \
T pivot_ = (buf)[end]; \
uint64_t pk_ = KEY(pivot_); \
size_t low_ = (start), high_ = (end); \
while (low_ < high_) { \
size_t center_ = low_ + (high_ - low_) / 2; \
if (KEY((buf)[center_]) > pk_) high_ = center_; \
else low_ = center_ + 1; \
} \
memmove((buf) + low_ + 1, (buf) + low_, ((end) - low_) * sizeof(T)); \
(buf)[low_] = pivot_; \
} \
} while (0)
/* Merges runs src[i..mid) and src[j..hi) of very different lengths into
* dst[k..): each record of the shorter run finds its place in the longer one
* by binary search (after equal keys of the left run, before equal keys of
* the right one), and the records between move as blocks. Leaves the
* indices at the ends of both runs. */
#define SCR_SORT_BLOCK_MERGE(T, KEY, src, dst, i, j, k, mid, hi) \
do { \
bool short_right_ = (hi) - (j) <= (mid) - (i); \
size_t *at_ = short_right_ ? &(i) : &(j), *by_ = short_right_ ? &(j) : &(i); \
size_t at_end_ = short_right_ ? (mid) : (hi), by_end_ = short_right_ ? (hi) : (mid); \
for (; *by_ < by_end_; (*by_)++) { \
uint64_t key_ = KEY((src)[*by_]); \
size_t low_ = *at_, high_ = at_end_; \
while (low_ < high_) { \
size_t center_ = low_ + (high_ - low_) / 2; \
uint64_t probe_ = KEY((src)[center_]); \
if (short_right_ ? probe_ > key_ : probe_ >= key_) high_ = center_; \
else low_ = center_ + 1; \
} \
memcpy((dst) + (k), (src) + *at_, (low_ - *at_) * sizeof(T)); \
(k) += low_ - *at_; \
*at_ = low_; \
(dst)[(k)++] = (src)[*by_]; \
} \
} while (0)
/* LSD radix passes over BITS-bit digits from `src` into `dst`, swapping the
* two after each pass; positions where every key has the same digit are
* skipped. */
#define SCR_SORT_RADIX_PASSES(T, KEY, src, dst, n, BITS) \
do { \
enum { digits_ = (64 + (BITS) - 1) / (BITS), radix_ = 1 << (BITS) }; \
size_t(*counts_)[radix_] = calloc(digits_, sizeof(*counts_)); \
if (!counts_) scr_arr_oom(); \
for (size_t i_ = 0; i_ < (n); i_++) { \
uint64_t k_ = KEY((src)[i_]); \
for (unsigned d_ = 0; d_ < digits_; d_++) \
counts_[d_][(k_ >> ((BITS) * d_)) & (radix_ - 1)]++; \
} \
uint64_t first_ = KEY((src)[0]); \
for (unsigned d_ = 0; d_ < digits_; d_++) { \
size_t *count_ = counts_[d_]; \
unsigned shift_ = (BITS) * d_; \
if (count_[(first_ >> shift_) & (radix_ - 1)] == (n)) continue; \
size_t at_ = 0; \
for (unsigned b_ = 0; b_ < radix_; b_++) { \
size_t c_ = count_[b_]; \
count_[b_] = at_; \
at_ += c_; \
} \
for (size_t i_ = 0; i_ < (n); i_++) \
(dst)[count_[(KEY((src)[i_]) >> shift_) & (radix_ - 1)]++] = (src)[i_]; \
T *swap_ = (src); \
(src) = (dst); \
(dst) = swap_; \
} \
free(counts_); \
} while (0)
/* Stable sort of `n` records of type T keyed by KEY(record), between `buf`
* and `tmp` (which may be NULL when `n` is at most the insertion length);
* leaves the sorted records in *out. */
#define SCR_SORT_KEYED_RECORDS(T, KEY, buf, tmp, n, few_runs, out) \
do { \
T *src_ = (buf), *dst_ = (tmp); \
if (few_runs) { \
/* Runs grow to 16 records by insertion, as in the comparator sort; a \
* run starting inside an ascending run extends to that run's end, so \
* there are at most twice as many as ascending runs. */ \
size_t starts_[2 * SCR_SORT_KEYED_RUNS + 1], count_ = 0; \
for (size_t start_ = 0; start_ < (n);) { \
starts_[count_++] = start_; \
size_t end_ = start_ + 1; \
while (end_ < (n) && KEY(src_[end_ - 1]) <= KEY(src_[end_])) end_++; \
size_t limit_ = start_ + 16 < (n) ? start_ + 16 : (n); \
SCR_SORT_INSERT_UNTIL(T, KEY, src_, start_, end_, limit_); \
start_ = end_; \
} \
starts_[count_] = (n); \
while (count_ > 1) { \
size_t next_ = 0; \
for (size_t r_ = 0; r_ < count_; r_ += 2) { \
size_t lo_ = starts_[r_], mid_ = starts_[r_ + 1]; \
size_t hi_ = r_ + 2 <= count_ ? starts_[r_ + 2] : mid_; \
size_t i_ = lo_, j_ = mid_, k_ = lo_; \
if ((hi_ - mid_) * 8 <= mid_ - lo_ || (mid_ - lo_) * 8 <= hi_ - mid_) \
SCR_SORT_BLOCK_MERGE(T, KEY, src_, dst_, i_, j_, k_, mid_, hi_); \
while (i_ < mid_ && j_ < hi_) \
dst_[k_++] = KEY(src_[j_]) < KEY(src_[i_]) ? src_[j_++] : src_[i_++]; \
memcpy(dst_ + k_, src_ + i_, (mid_ - i_) * sizeof(T)); \
k_ += mid_ - i_; \
memcpy(dst_ + k_, src_ + j_, (hi_ - j_) * sizeof(T)); \
starts_[next_++] = lo_; \
} \
starts_[next_] = (n); \
count_ = next_; \
T *swap_ = src_; \
src_ = dst_; \
dst_ = swap_; \
} \
} else if ((n) <= SCR_SORT_KEYED_INSERTION) { \
size_t end_ = 1; \
SCR_SORT_INSERT_UNTIL(T, KEY, src_, 0, end_, (n)); \
} else if ((n) > SCR_SORT_WIDE_DIGITS) { \
SCR_SORT_RADIX_PASSES(T, KEY, src_, dst_, n, 11); \
} else { \
SCR_SORT_RADIX_PASSES(T, KEY, src_, dst_, n, 8); \
} \
*(out) = src_; \
} while (0)
#define SCR_SORT_SELF_KEY(x) (x)
#define SCR_SORT_ITEM_KEY(x) ((x).key)
static void scr_sort_reverse_slots(uint64_t *values, size_t n) {
for (size_t left = 0, right = n - 1; left < right; left++, right--) {
uint64_t v = values[left];
values[left] = values[right];
values[right] = v;
}
}
/* Sorts number slots in place as their keys. Equal keys are equal numbers,
* so the order among them needs no tracking. */
static bool scr_sort_keyed_numbers(uint64_t *values, size_t n, uint64_t flip) {
ScrSortScan scan;
#define SCR_SORT_NUMBER_AT(i) scr_slot_to_f64(values[i])
SCR_SORT_SCAN(scan, n, SCR_SORT_NUMBER_AT);
if (!scan.valid) return false;
bool complete = scan.scanned == n;
if (complete && scan.runs == 1) return true;
if (complete && scan.descending) {
scr_sort_reverse_slots(values, n);
return true;
}
uint64_t key;
for (size_t i = scan.scanned; i < n; i++)
if (!scr_sort_key_encode(SCR_SORT_NUMBER_AT(i), flip, &key)) return false;
#undef SCR_SORT_NUMBER_AT
for (size_t i = 0; i < n; i++) {
scr_sort_key_encode(scr_slot_to_f64(values[i]), flip, &key);
values[i] = key;
}
bool few_runs = complete && scan.runs <= SCR_SORT_KEYED_RUNS;
uint64_t *tmp = NULL, *sorted = values;
if (few_runs || n > SCR_SORT_KEYED_INSERTION) {
tmp = malloc(n * sizeof(*tmp));
if (!tmp) scr_arr_oom();
}
SCR_SORT_KEYED_RECORDS(uint64_t, SCR_SORT_SELF_KEY, values, tmp, n, few_runs, &sorted);
for (size_t i = 0; i < n; i++) values[i] = scr_sort_key_decode(sorted[i], flip);
free(tmp);
return true;
}
/* A reference element's key through the borrowed reader closure. */
static double scr_sort_read_key(const ScrSortCompare *c, uint64_t slot) {
ScrClosure *reader = c->key;
void *x = scr_slot_to_ptr(slot);
if (reader->fn_borrowed)
return ((double (*)(ScrClosure *, void *))reader->fn_borrowed)(reader, x);
return ((double (*)(ScrClosure *, void *))reader->fn)(reader, x ? c->retain(x) : x);
}
/* Ascending keys followed by a short tail (values pushed onto a sorted
* array): sorts the tail alone, finds where each of its slots belongs in
* the head by binary search, then moves the head's slots as blocks, without
* a buffer for the whole input. Every key is read before any slot moves. */
static bool scr_sort_keyed_tail(uint64_t *values, size_t n, size_t head, const ScrSortCompare *c,
uint64_t flip) {
size_t tail = n - head;
ScrSortItem *items = malloc(tail * 2 * sizeof(*items));
if (!items) scr_arr_oom();
for (size_t i = 0; i < tail; i++) {
if (!scr_sort_key_encode(scr_sort_read_key(c, values[head + i]), flip, &items[i].key)) {
free(items);
return false;
}
items[i].slot = values[head + i];
}
ScrSortItem *sorted = items;
SCR_SORT_KEYED_RECORDS(ScrSortItem, SCR_SORT_ITEM_KEY, items, items + tail, tail, false,
&sorted);
/* Each tail slot follows the head's slots with keys up to its own. The
* places only grow, so each search starts at the previous one. */
size_t *places = malloc(tail * sizeof(*places));
if (!places) scr_arr_oom();
for (size_t i = 0, low = 0; i < tail; i++) {
size_t high = head;
while (low < high) {
size_t center = low + (high - low) / 2;
uint64_t key;
scr_sort_key_encode(scr_sort_read_key(c, values[center]), flip, &key);
if (key > sorted[i].key) high = center;
else low = center + 1;
}
places[i] = low;
}
for (size_t i = tail, end = head; i-- > 0;) {
memmove(values + places[i] + i + 1, values + places[i], (end - places[i]) * sizeof(*values));
values[places[i] + i] = sorted[i].slot;
end = places[i];
}
free(places);
free(items);
return true;
}
/* Sorts reference slots by the keys their reader returns. The reader is a
* plain field read; the slots move only after every key has been read. */
static bool scr_sort_keyed_refs(uint64_t *values, size_t n, const ScrSortCompare *c,
uint64_t flip) {
ScrSortScan scan;
#define SCR_SORT_REF_AT(i) scr_sort_read_key(c, values[i])
SCR_SORT_SCAN(scan, n, SCR_SORT_REF_AT);
#undef SCR_SORT_REF_AT
if (!scan.valid) return false;
bool complete = scan.scanned == n;
if (complete && scan.runs == 1) return true;
if (complete && scan.descending) {
/* Strictly descending keys reverse without reordering ties. */
scr_sort_reverse_slots(values, n);
return true;
}
/* The scan has read and checked the head's keys; the tail's are checked
* as they are read again. */
if (scan.head < n && scan.head >= n - n / 8)
return scr_sort_keyed_tail(values, n, scan.head, c, flip);
ScrSortItem *items = malloc(n * sizeof(*items));
if (!items) scr_arr_oom();
for (size_t i = 0; i < n; i++) {
if (!scr_sort_key_encode(scr_sort_read_key(c, values[i]), flip, &items[i].key)) {
free(items);
return false;
}
items[i].slot = values[i];
}
bool few_runs = complete && scan.runs <= SCR_SORT_KEYED_RUNS;
ScrSortItem *tmp = NULL, *sorted = items;
if (few_runs || n > SCR_SORT_KEYED_INSERTION) {
tmp = malloc(n * sizeof(*tmp));
if (!tmp) scr_arr_oom();
}
SCR_SORT_KEYED_RECORDS(ScrSortItem, SCR_SORT_ITEM_KEY, items, tmp, n, few_runs, &sorted);
for (size_t i = 0; i < n; i++) values[i] = sorted[i].slot;
free(items);
free(tmp);
return true;
}
/* Orders `n >= 2` slots by key when the comparator allows it. Returns false,
* leaving the slots as they were, when a key is NaN or -0: the comparator
* sort then runs instead. */
static bool scr_sort_keyed(uint64_t *values, size_t n, const ScrSortCompare *c) {
uint64_t flip = c->order < 0 ? ~UINT64_C(0) : 0;
if (c->numbers) return scr_sort_keyed_numbers(values, n, flip);
scr_dyn_this_push_dyn(NULL);
bool sorted = scr_sort_keyed_refs(values, n, c, flip);
scr_dyn_this_pop();
return sorted;
}
#undef SCR_SORT_SELF_KEY
#undef SCR_SORT_ITEM_KEY
#undef SCR_SORT_KEYED_RECORDS
#undef SCR_SORT_RADIX_PASSES
#undef SCR_SORT_BLOCK_MERGE
#undef SCR_SORT_INSERT_UNTIL
#undef SCR_SORT_SCAN
/* The stable natural merge sort of the compiler's IR helper, in place over
* `n >= 2` slots: strictly descending runs reverse, short runs grow to 16
* with binary insertion after equal values, then merge passes consume pairs
@@ -2581,19 +2961,20 @@ done:
* keeps owning every element and stays untouched while user code runs, so
* the cycle collector and a throwing comparator both see the original
* snapshot; the sorted permutation is published only after success. */
void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity) {
void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity,
int32_t order, ScrClosure *key) {
size_t n = (size_t)count;
if (n < 2) return;
if (n > a->len || n > SIZE_MAX / 2 / sizeof(uint64_t))
scr_trap("scriptc: invalid comparator sort snapshot\n");
ScrSortCompare c = scr_sort_compare_for(a, f, arity);
ScrSortCompare c = scr_sort_compare_for(a, f, arity, order, key);
uint64_t *src = malloc(n * sizeof(*src));
if (!src) scr_arr_oom();
for (size_t i = 0; i < n; i++) {
if (scr_arr_state_at(a, i, &src[i]) != SCR_ARR_VALUE)
scr_trap("scriptc: invalid comparator sort snapshot\n");
}
bool sorted = scr_sort_slots(src, n, &c, true);
bool sorted = (order != 0 && scr_sort_keyed(src, n, &c)) || scr_sort_slots(src, n, &c, true);
/* A permutation of the same owned slots: no reference count changes. */
for (size_t i = 0; sorted && i < n; i++) {
if (i < a->cap) {
@@ -2633,15 +3014,19 @@ static bool scr_arr_dense_values(const ScrArr *a) {
* indices below the snapshot length are still dense storage, else through
* the general element write, which fills holes, regrows a truncated array
* and keeps appended indices. A comparator exception leaves `a` as the
* comparator left it. */
bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned) {
* comparator left it. A keyed sort calls no user code, so it permutes the
* slots of `a` in place. */
bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, int32_t order,
ScrClosure *key) {
if (a->rc == SIZE_MAX || !scr_arr_dense_values(a)) return false;
size_t n = a->len;
if (n < 2) return true;
if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom();
ScrSortCompare c = scr_sort_compare_for(a, f, arity);
ScrSortCompare c = scr_sort_compare_for(a, f, arity, order, key);
/* Nothing else can reach a private copy: its own storage is sorted, and
* the same owned slots only permute. */
* the same owned slots only permute. A keyed sort runs no user code, so
* it permutes a shared array's slots in place too. */
if (order != 0 && scr_sort_keyed(a->data, n, &c)) return true;
if (owned) {
scr_sort_slots(a->data, n, &c, false);
return true;
+1 -1
View File
@@ -318,7 +318,7 @@ static void scr_promise_gcfree(void *o) {
}
ScrPromise *scr_promise_new(void) {
static const ScrCycType cyc_type = { &scr_promise_trace, &scr_promise_gcfree, SCR_CYC_KIND_PROMISE };
static ScrCycType cyc_type = { &scr_promise_trace, &scr_promise_gcfree, SCR_CYC_KIND_PROMISE };
ScrPromise *p = scr_cyc_alloc(sizeof *p, &cyc_type);
p->rc = 1;
#ifdef SCR_RC_AUDIT
+2 -2
View File
@@ -70,7 +70,7 @@ static void scr_box_gcfree(void *o) {
}
ScrBox *scr_box_new(ScrBoxKind kind) {
static const ScrCycType cyc_type = { &scr_box_trace, &scr_box_gcfree, SCR_CYC_KIND_BOX };
static ScrCycType cyc_type = { &scr_box_trace, &scr_box_gcfree, SCR_CYC_KIND_BOX };
ScrBox *b = scr_cyc_alloc_inline(sizeof(ScrBox), &cyc_type);
b->rc = 1;
b->kind = kind;
@@ -186,7 +186,7 @@ static void scr_closure_gcfree(void *o) {
}
ScrClosure *scr_closure_new(void *fn, size_t ncaps) {
static const ScrCycType cyc_type = { &scr_closure_trace, &scr_closure_gcfree, SCR_CYC_KIND_CLOSURE };
static ScrCycType cyc_type = { &scr_closure_trace, &scr_closure_gcfree, SCR_CYC_KIND_CLOSURE };
ScrClosure *c = scr_cyc_alloc_inline(sizeof(ScrClosure) + ncaps * sizeof(ScrBox *), &cyc_type);
c->rc = 1;
c->fn = fn;
+1 -1
View File
@@ -126,7 +126,7 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
void scr_runtime_abi_v19(void) {}
void scr_runtime_abi_v20(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
+70 -48
View File
@@ -266,15 +266,39 @@ SCR_TL size_t scr_cyc_live = 0;
SCR_TL size_t scr_cyc_old_freed = 0;
#endif
void *scr_cyc_alloc(size_t size, const ScrCycType *type) {
/* Descriptor copies by registry id. Id 0 stays empty (an unregistered type). */
ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES];
static uint32_t scr_cyc_ntypes = 1;
/* Assign `type` its id on its first allocation; any thread may get there
* first, and every thread then sees the same id. A lost race leaves one
* table slot unused. */
uint32_t scr_cyc_register(ScrCycType *type) {
uint32_t id = __atomic_load_n(&type->id, __ATOMIC_ACQUIRE);
if (id) return id;
uint32_t fresh = __atomic_fetch_add(&scr_cyc_ntypes, 1, __ATOMIC_RELAXED);
if (fresh >= SCR_CYC_MAX_TYPES) scr_trap("scriptc: too many cycle-collected types\n");
/* Copy the fields another thread never writes; `id` may be published
* concurrently by a racing registration. */
scr_cyc_types[fresh] = (ScrCycType){
.trace = type->trace, .free_fn = type->free_fn, .kind = type->kind, .id = fresh};
__atomic_thread_fence(__ATOMIC_RELEASE);
uint32_t expected = 0;
if (__atomic_compare_exchange_n(&type->id, &expected, fresh, false, __ATOMIC_ACQ_REL,
__ATOMIC_ACQUIRE))
return fresh;
return expected;
}
void *scr_cyc_alloc(size_t size, ScrCycType *type) {
return scr_cyc_alloc_inline(size, type);
}
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
static void scr_cyc_grow(ScrVec *vec) {
/* Candidate positions are 32-bit (ScrCycHdr.buf_index). */
if (vec->cap > (size_t)UINT32_MAX / 2) scr_cyc_oom();
/* Candidate positions are 31-bit (ScrCycHdr.buf). */
if (vec->cap > (size_t)(SCR_CYC_BUFFERED_BIT - 1) / 2) scr_cyc_oom();
vec->cap = vec->cap ? vec->cap * 2 : 64;
void **grown = realloc(vec->v, vec->cap * sizeof *grown);
if (!grown) scr_cyc_oom();
@@ -723,20 +747,20 @@ static inline void scr_cyc_freed(const ScrCycType *type, unsigned gen) {
void scr_cyc_on_dead(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (!h->buffered) return;
if (!scr_cyc_is_buffered(h)) return;
/* O(1) removal: swap the last entry into the hole. A buffered object's
* generation never changes (promotion runs only on objects the pass has
* already drained from the buffers), so `gen` names the right one. */
ScrVec *b = &scr_roots[h->gen];
size_t i = h->buf_index;
ScrVec *b = &scr_roots[scr_cyc_gen(h)];
size_t i = scr_cyc_buf_index(h);
void *last = b->v[--b->n];
b->v[i] = last;
if (last != obj) scr_cyc_hdr(last)->buf_index = (uint32_t)i;
h->buffered = 0;
if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i);
scr_cyc_clear_buffered(h);
scr_cyc_nbuffered--;
/* A death must not consume room reserved for a future candidate. */
scr_cyc_trigger--;
if (h->gen != SCR_CYC_NURSERY && !scr_cyc_older_backlog())
if (scr_cyc_gen(h) != SCR_CYC_NURSERY && !scr_cyc_older_backlog())
scr_cyc_scheduled_mature_age = 0;
}
@@ -747,12 +771,11 @@ void scr_cyc_on_release(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
/* Releasing an existing candidate cannot advance the trigger. A pass
* always re-arms after its teardowns, including any candidates they add. */
if (h->buffered) return;
if (h->gen < SCR_CYC_OLD) h->gen = (uint8_t)scr_cyc_buffer_gen(h->type, h->gen);
ScrVec *b = &scr_roots[h->gen];
if (scr_cyc_is_buffered(h)) return;
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_set_gen(h, scr_cyc_buffer_gen(scr_cyc_type(h), scr_cyc_gen(h)));
ScrVec *b = &scr_roots[scr_cyc_gen(h)];
if (b->n == b->cap) scr_cyc_grow(b);
h->buffered = 1;
h->buf_index = (uint32_t)b->n;
scr_cyc_set_buffered(h, (uint32_t)b->n);
b->v[b->n++] = obj;
scr_cyc_nbuffered++;
/* Never re-entered: a teardown's releases of untraced children can buffer
@@ -770,7 +793,7 @@ void scr_cyc_on_release(void *obj) {
* a generation this pass does not collect. */
#define SCR_CYC_SKIP(child) \
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|| scr_cyc_hdr(child)->gen > scr_gen_limit)
|| scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit)
/* Trace callbacks forward an integer depth through their opaque context;
* it is never dereferenced. The ABI already carries it through every edge. */
@@ -782,31 +805,31 @@ static void scr_mg_visit(void *child, void *ctx) {
}
static void scr_mark_gray(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (h->color == SCR_CYC_GRAY) return;
h->color = SCR_CYC_GRAY;
if (scr_cyc_color(h) == SCR_CYC_GRAY) return;
scr_cyc_set_color(h, SCR_CYC_GRAY);
if (depth == SCR_CYC_WALK_DEPTH) {
scr_cyc_push(&scr_pending, obj);
return;
}
h->type->trace(obj, scr_mg_visit, (void *)(depth + 1));
scr_cyc_type(h)->trace(obj, scr_mg_visit, (void *)(depth + 1));
}
static void scr_scan_black(void *obj, uintptr_t depth);
static void scr_sb_visit(void *child, void *ctx) {
if (SCR_CYC_SKIP(child)) return;
SCR_RC(child) += 1;
if (scr_cyc_hdr(child)->color != SCR_CYC_BLACK)
if (scr_cyc_color(scr_cyc_hdr(child)) != SCR_CYC_BLACK)
scr_scan_black(child, (uintptr_t)ctx);
}
static void scr_scan_black(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
h->color = SCR_CYC_BLACK;
if (h->gen < SCR_CYC_OLD) scr_cyc_push(&scr_promote, obj);
scr_cyc_set_color(h, SCR_CYC_BLACK);
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push(&scr_promote, obj);
if (depth == SCR_CYC_WALK_DEPTH) {
scr_cyc_push(&scr_restore_pending, obj);
return;
}
h->type->trace(obj, scr_sb_visit, (void *)(depth + 1));
scr_cyc_type(h)->trace(obj, scr_sb_visit, (void *)(depth + 1));
}
static void scr_scan(void *obj, uintptr_t depth);
@@ -816,16 +839,16 @@ static void scr_scan_visit(void *child, void *ctx) {
}
static void scr_scan(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (h->color != SCR_CYC_GRAY) return;
if (scr_cyc_color(h) != SCR_CYC_GRAY) return;
if (SCR_RC(obj) > 0) {
scr_scan_black(obj, 0);
while (scr_restore_pending.n) {
void *next = scr_restore_pending.v[--scr_restore_pending.n];
scr_cyc_hdr(next)->type->trace(next, scr_sb_visit, NULL);
scr_cyc_type(scr_cyc_hdr(next))->trace(next, scr_sb_visit, NULL);
}
return;
}
h->color = SCR_CYC_WHITE;
scr_cyc_set_color(h, SCR_CYC_WHITE);
/* Record each whitened object once. A later outside root can restore it;
* filter the list only after every scan finishes. No gather trace needed. */
scr_cyc_push(&scr_white, obj);
@@ -833,7 +856,7 @@ static void scr_scan(void *obj, uintptr_t depth) {
scr_cyc_push(&scr_pending, obj);
return;
}
h->type->trace(obj, scr_scan_visit, (void *)(depth + 1));
scr_cyc_type(h)->trace(obj, scr_scan_visit, (void *)(depth + 1));
}
/* ── cross-generation edges ───────────────────────────────────────────── */
@@ -870,7 +893,7 @@ static void scr_scan(void *obj, uintptr_t depth) {
static SCR_CYC_COMPACT void scr_xg_pin_visit(void *child, void *ctx) {
(void)ctx;
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
if (scr_cyc_hdr(child)->gen <= scr_gen_limit) return; /* markGray had it */
if (scr_cyc_gen(scr_cyc_hdr(child)) <= scr_gen_limit) return; /* markGray had it */
SCR_RC(child) += 1; /* pin across the teardowns */
scr_cyc_push(&scr_xgen, child);
}
@@ -890,8 +913,8 @@ static void scr_xg_child_visit(void *child, void *ctx) {
/* One genuine release, generically: the header carries everything needed. */
static void scr_xg_destroy(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
h->type->trace(obj, scr_xg_child_visit, NULL);
h->type->free_fn(obj);
scr_cyc_type(h)->trace(obj, scr_xg_child_visit, NULL);
scr_cyc_type(h)->free_fn(obj);
scr_xg_freed++;
}
@@ -919,10 +942,10 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
for (size_t i = 0; i < scr_white.n; i++) {
void *obj = scr_white.v[i];
ScrCycHdr *h = scr_cyc_hdr(obj);
if (h->color != SCR_CYC_WHITE) continue;
h->color = SCR_CYC_DOOMED;
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(h->type, gen_limit);
else scr_cyc_mature_dead += h->gen == SCR_CYC_MATURE;
if (scr_cyc_color(h) != SCR_CYC_WHITE) continue;
scr_cyc_set_color(h, SCR_CYC_DOOMED);
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(scr_cyc_type(h), gen_limit);
else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE;
scr_white.v[dead++] = obj;
}
scr_white.n = dead;
@@ -934,7 +957,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
if (gen_limit < SCR_CYC_OLD) {
for (size_t i = 0; i < scr_white.n; i++) {
void *obj = scr_white.v[i];
scr_cyc_hdr(obj)->type->trace(obj, scr_xg_pin_visit, NULL);
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_xg_pin_visit, NULL);
}
}
@@ -943,7 +966,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
* referenced, so none of them is in the white set about to be freed. */
for (size_t i = 0; i < scr_promote.n; i++) {
ScrCycHdr *h = scr_cyc_hdr(scr_promote.v[i]);
if (h->gen < SCR_CYC_OLD) h->gen++;
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_set_gen(h, scr_cyc_gen(h) + 1);
}
scr_promote.n = 0;
@@ -966,15 +989,14 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
for (size_t i = 0; i < scr_cands.n; i++) {
void *obj = scr_cands.v[i];
ScrCycHdr *h = scr_cyc_hdr(obj);
if (h->color == SCR_CYC_DOOMED) continue; /* being freed below */
if (!h->buffered) {
if (scr_cyc_color(h) == SCR_CYC_DOOMED) continue; /* being freed below */
if (!scr_cyc_is_buffered(h)) {
/* A survivor promoted into a level its type bypasses moves past
* it, as a release would buffer it. */
if (h->gen == SCR_CYC_MATURE && scr_cyc_bypassed(h->type, SCR_CYC_MATURE))
h->gen = SCR_CYC_OLD;
h->buffered = 1;
h->buf_index = (uint32_t)scr_roots[h->gen].n;
scr_cyc_push(&scr_roots[h->gen], obj);
if (scr_cyc_gen(h) == SCR_CYC_MATURE && scr_cyc_bypassed(scr_cyc_type(h), SCR_CYC_MATURE))
scr_cyc_set_gen(h, SCR_CYC_OLD);
scr_cyc_set_buffered(h, (uint32_t)scr_roots[scr_cyc_gen(h)].n);
scr_cyc_push(&scr_roots[scr_cyc_gen(h)], obj);
}
}
}
@@ -985,7 +1007,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
size_t freed = scr_white.n;
for (size_t i = 0; i < scr_white.n; i++) {
void *obj = scr_white.v[i];
scr_cyc_hdr(obj)->type->free_fn(obj);
scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj);
}
scr_white.n = 0;
@@ -1033,8 +1055,8 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
for (size_t i = 0; i < scr_roots[g].n; i++) {
void *obj = scr_roots[g].v[i];
ScrCycHdr *h = scr_cyc_hdr(obj);
h->buffered = 0;
if (gen_limit < SCR_CYC_OLD) scr_cyc_walked(h->type, gen_limit);
scr_cyc_clear_buffered(h);
if (gen_limit < SCR_CYC_OLD) scr_cyc_walked(scr_cyc_type(h), gen_limit);
scr_cyc_push(&scr_cands, obj);
}
scr_roots[g].n = 0;
@@ -1042,7 +1064,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i], 0);
while (scr_pending.n) {
void *obj = scr_pending.v[--scr_pending.n];
scr_cyc_hdr(obj)->type->trace(obj, scr_mg_visit, NULL);
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_mg_visit, NULL);
}
scr_white.n = 0;
@@ -1051,8 +1073,8 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
void *obj = scr_pending.v[--scr_pending.n];
/* A later outside root may have restored this deferred white node and
* its descendants already. Never scan a black node's edges again. */
if (scr_cyc_hdr(obj)->color == SCR_CYC_WHITE)
scr_cyc_hdr(obj)->type->trace(obj, scr_scan_visit, NULL);
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_WHITE)
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_scan_visit, NULL);
}
return scr_cyc_settle(gen_limit);
+3 -3
View File
@@ -204,9 +204,9 @@ static ScrError *scr_error_alloc(int kind) {
size_t size = kind == SCR_ERR_DOMEX ? sizeof(ScrDomException)
: kind == SCR_ERR_AGGREGATE ? sizeof(ScrAggregateError)
: sizeof(ScrError);
static const ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
static const ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
static const ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
static ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
static ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
static ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
ScrError *e;
if (scr_error_traced) {
e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type
+1 -1
View File
@@ -316,7 +316,7 @@ ScrEmitter *scr_emitter_new(void) {
/* Always collector-headered: the registry holds closures, so the
* emitter hierarchy is unconditionally cycle-capable (the canonical
* capture cycle is `e.on('x', () => e.emit(...))`). */
static const ScrCycType cyc_type = { &scr_emitter_trace, &scr_emitter_gcfree, SCR_CYC_KIND_EMITTER };
static ScrCycType cyc_type = { &scr_emitter_trace, &scr_emitter_gcfree, SCR_CYC_KIND_EMITTER };
ScrEmitter *em = scr_cyc_alloc(sizeof *em, &cyc_type);
em->rc = 1;
em->vt = &scr_emitter_vt;
+1 -1
View File
@@ -282,7 +282,7 @@ static void scr_caught_gcfree(void *object) {
}
ScrCaught *scr_exc_take(void) {
static const ScrCycType cyc_type = { &scr_caught_trace_v, &scr_caught_gcfree, SCR_CYC_KIND_CAUGHT };
static ScrCycType cyc_type = { &scr_caught_trace_v, &scr_caught_gcfree, SCR_CYC_KIND_CAUGHT };
ScrCaught *c = scr_cyc_alloc(sizeof *c, &cyc_type);
c->rc = 1;
c->kind = scr_exc_kind;
File diff suppressed because it is too large Load Diff
+36 -5
View File
@@ -817,8 +817,8 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
*list = (ScrDyn *)d->v.str; /* freelist link */
scr_dyn_free_count--;
ScrCycHdr *header = scr_cyc_hdr(d);
header->color = SCR_CYC_BLACK;
header->gen = SCR_CYC_NURSERY;
scr_cyc_set_color(header, SCR_CYC_BLACK);
scr_cyc_set_gen(header, SCR_CYC_NURSERY);
d->rc = 1;
d->kind = kind;
d->buffer = false;
@@ -850,7 +850,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
return d;
}
#endif
static const ScrCycType cyc_type = { &scr_dyn_trace_v, &scr_dyn_gcfree, SCR_CYC_KIND_DYN };
static ScrCycType cyc_type = { &scr_dyn_trace_v, &scr_dyn_gcfree, SCR_CYC_KIND_DYN };
ScrDyn *fresh = scr_cyc_alloc_inline(sizeof *fresh, &cyc_type);
if (!fresh) scr_json_oom();
fresh->rc = 1;
@@ -3303,6 +3303,10 @@ typedef struct {
void *ptr; /* handle entry (borrowed); NULL when dv or undefined */
ScrDynHandleTag tag;
ScrDyn *dv; /* dyn entry (+1); NULL for handle/undefined entries */
/* A deferred typed-ref entry (scr_dyn_this_push_typed): the borrowed
* instance in `ptr` boxes with this recipe on the first read, and dv
* caches the box. */
const ScrDynTypedRefRecipe *recipe;
} ScrDynThisEnt;
static SCR_TL ScrDynThisEnt *scr_dyn_this_stack;
@@ -3334,6 +3338,7 @@ void scr_dyn_this_push(void *h, ScrDynHandleTag tag) {
e->ptr = h;
e->tag = tag;
e->dv = NULL;
e->recipe = NULL;
}
void scr_dyn_this_push_dyn(const ScrDyn *v) {
@@ -3341,6 +3346,15 @@ void scr_dyn_this_push_dyn(const ScrDyn *v) {
e->ptr = NULL;
e->tag = 0;
e->dv = v ? scr_dyn_retain((ScrDyn *)v) : NULL;
e->recipe = NULL;
}
void scr_dyn_this_push_typed(void *ptr, const ScrDynTypedRefRecipe *recipe) {
ScrDynThisEnt *e = scr_dyn_this_grow();
e->ptr = ptr;
e->tag = 0;
e->dv = NULL;
e->recipe = recipe;
}
void scr_dyn_this_pop(void) {
@@ -3354,6 +3368,23 @@ void scr_dyn_this_pop(void) {
ScrDyn *scr_dyn_this_get(void) {
if (scr_dyn_this_n > 0) {
ScrDynThisEnt *e = &scr_dyn_this_stack[scr_dyn_this_n - 1];
if (!e->dv && e->recipe) {
const ScrDynTypedRefRecipe *r = e->recipe;
e->dv = r->kind == 2
? scr_dyn_new_typed_ref_traced(e->ptr, r->retain, r->release, r->type_key,
r->type_key_len, r->materialize, r->commit)
: r->kind == 1
? scr_dyn_new_typed_ref_observed(e->ptr, r->retain, r->release, r->type_key,
r->type_key_len, r->materialize, r->commit)
: scr_dyn_new_typed_ref(e->ptr, r->retain, r->release, r->type_key,
r->type_key_len, r->materialize, r->commit);
if (r->view == 1)
scr_dyn_typed_ref_bind_array(e->dv, (const ScrDynTypedArrayOps *)r->ops);
else if (r->view == 2)
scr_dyn_typed_ref_bind_record(e->dv, (const ScrDynTypedRecordOps *)r->ops);
else if (r->view == 3)
scr_dyn_typed_ref_bind_class(e->dv, (const ScrDynTypedClassOps *)r->ops);
}
if (e->dv) return scr_dyn_retain(e->dv);
/* An uninstalled tag never binds: a unit that fires without its dyn
* half (no boxes can exist there) keeps the undefined answer. */
@@ -9959,7 +9990,7 @@ static void scr_weak_remove_impl(ScrWeakEntry *entry, bool collected) {
}
static void scr_weak_remove(ScrWeakEntry *entry) {
scr_weak_remove_impl(entry, scr_cyc_hdr(entry->owner)->color == SCR_CYC_DOOMED);
scr_weak_remove_impl(entry, scr_cyc_color(scr_cyc_hdr(entry->owner)) == SCR_CYC_DOOMED);
}
static void scr_weak_map_trace(void *ptr, ScrTraceVisit visit, void *ctx) {
@@ -10159,7 +10190,7 @@ static ScrDyn *scr_weak_collection_new(ScrDyn *seed, bool is_set) {
ScrDynHandleTag tag = is_set ? SCR_DYNH_WEAK_SET : SCR_DYNH_WEAK_MAP;
scr_weak_dispose_hook = &scr_weak_disposed;
scr_dyn_handle_install(tag, is_set ? &set_ops : &map_ops);
static const ScrCycType cyc_type = { &scr_weak_map_trace, &scr_weak_map_gcfree, SCR_CYC_KIND_WEAK_MAP };
static ScrCycType cyc_type = { &scr_weak_map_trace, &scr_weak_map_gcfree, SCR_CYC_KIND_WEAK_MAP };
ScrWeakMap *map = scr_cyc_alloc(sizeof *map, &cyc_type);
if (!map) scr_json_oom();
map->rc = 1;
+1 -1
View File
@@ -286,7 +286,7 @@ static void checked_leaf_cycles(void) {
ScrStr *text = scr_str_new("survives either edge", 20);
ScrDyn *leaf = scr_dyn_new_str(text);
scr_str_release(text);
scr_cyc_hdr(leaf)->gen = gen;
scr_cyc_set_gen(scr_cyc_hdr(leaf), gen);
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
ScrDyn *array = scr_dyn_new_arr();
ScrClosure *closure = scr_closure_new(NULL, 1);
+1 -1
View File
@@ -720,7 +720,7 @@ ScrMap *scr_map_new_typed(ScrMapKeyKind key_kind, ScrMapValKind val_kind,
ScrMap *m;
if (key_trace || val_trace) {
/* Either side can point back: collector header + trace. */
static const ScrCycType cyc_type = { &scr_map_trace, &scr_map_gcfree, SCR_CYC_KIND_MAP };
static ScrCycType cyc_type = { &scr_map_trace, &scr_map_gcfree, SCR_CYC_KIND_MAP };
m = scr_cyc_alloc(sizeof *m, &cyc_type);
} else {
m = scr_map_alloc_plain();
+1 -1
View File
@@ -17,7 +17,7 @@ static void scr_classobj_gcfree(void *object) {
ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps) {
if (ncaps > (SIZE_MAX - sizeof(ScrClassObj)) / sizeof(ScrBox *))
scr_trap("scriptc: class capture allocation overflow\n");
static const ScrCycType cyc_type = { &scr_classobj_trace_v, &scr_classobj_gcfree, SCR_CYC_KIND_CLASS_OBJECT };
static ScrCycType cyc_type = { &scr_classobj_trace_v, &scr_classobj_gcfree, SCR_CYC_KIND_CLASS_OBJECT };
ScrClassObj *c = scr_cyc_alloc(sizeof(ScrClassObj) + ncaps * sizeof(ScrBox *), &cyc_type);
c->rc = 1;
c->pre = template->pre;
+113 -47
View File
@@ -74,7 +74,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_v19(void);
void scr_runtime_abi_v20(void);
void scr_runtime_workers_v8(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
@@ -393,10 +393,11 @@ char **scr_worker_argv(void);
* Reference counting alone cannot free cycles, so every object that can
* participate in one — capture boxes, heap closures, unions, promises, and
* compiler-emitted class/record shapes with cycle-capable fields — is
* allocated with a hidden header directly BEFORE the object: a trace
* function (enumerates the object's cycle-capable children), a teardown
* function (frees the object WITHOUT releasing traced children), and the
* trial-deletion bookkeeping (color + candidate-root buffer state). Types
* allocated with a hidden 8-byte header directly BEFORE the object: the
* registry id of its type's descriptor (a trace function enumerating the
* object's cycle-capable children, and a teardown function freeing the
* object WITHOUT releasing traced children) and the trial-deletion
* bookkeeping (color, generation and candidate-root buffer state). Types
* that can never be in a cycle — strings above all, plus arrays of scalar/
* string/bytes elements, dyn trees, and shapes whose fields are all acyclic
* — keep the lean 1-word `rc` header and pay nothing. Arrays and maps are
@@ -448,9 +449,11 @@ enum { SCR_CYC_BLACK = 0, SCR_CYC_GRAY = 1, SCR_CYC_WHITE = 2, SCR_CYC_DOOMED =
enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_OLD = 2, SCR_CYC_NGENS = 3 };
/* A headered type's collector entry points, shared by all its objects: the
* header names one constant descriptor instead of carrying both function
* pointers, which keeps the header at 16 bytes. Emitted shapes define one
* per traced type (llvm/shapes.ts); runtime families use SCR_CYC_TYPE. */
* header names its descriptor by a registry id (`id`, assigned on the type's
* first allocation) instead of carrying a pointer, which keeps the header at
* 8 bytes. Emitted shapes define one per traced type (llvm/shapes.ts);
* runtime families define their own. Descriptors are writable (the id) and
* must outlive every object of the type. */
typedef struct ScrCycType {
ScrTraceFn trace;
ScrCycFreeFn free_fn;
@@ -462,6 +465,9 @@ typedef struct ScrCycType {
* share an index (separately emitted code in one process) are merely
* judged together. */
uint32_t kind;
/* Registry id (scr_cyc_types), 0 until scr_cyc_register assigns it.
* Emitted allocation paths read it and take the slow path while it is 0. */
uint32_t id;
} ScrCycType;
/* Runtime families' kinds. Emitted descriptors count from
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
@@ -484,46 +490,71 @@ enum {
SCR_CYC_KIND_WORKER_VIEW,
SCR_CYC_KIND_EMITTED = 32
};
#define SCR_CYC_TYPE(name, trace_fn, free) static const ScrCycType name = { (trace_fn), (free), 0 }
/* Eight bytes in front of the object: the descriptor's registry id, the
* collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD,
* the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in
* its generation's candidate buffer, with its position there below it (O(1)
* removal when rc hits 0). */
typedef struct ScrCycHdr {
#if UINTPTR_MAX == UINT32_MAX
/* calloc aligns the block for doubles; keep the payload aligned too.
* Padding BEFORE the descriptor keeps every object-relative offset of the
* fields below identical to 64-bit targets. */
uint32_t payload_alignment;
#endif
const ScrCycType *type; /* trace and teardown */
uint8_t color; /* SCR_CYC_* */
uint8_t gen; /* SCR_CYC_NURSERY..SCR_CYC_OLD (the walk filter) */
uint16_t buffered; /* 1 = sitting in its generation's candidate buffer */
uint32_t buf_index; /* position there (O(1) removal when rc hits 0) */
uint16_t id;
uint8_t color;
uint8_t gen;
uint32_t buf;
} ScrCycHdr;
#define SCR_CYC_BUFFERED_BIT 0x80000000u
/* This layout is an ABI, not an implementation detail. Every emitted release
* of a headered object (llvm/shapes.ts cycleRootLines, used by the record,
* class and inline RC helpers) mirrors scr_cyc_on_release's early return:
* it reads `buffered` as an i16 at obj-6 (the backend's cycleBufferedOffset)
* and calls scr_cyc_on_release only when it is 0; the emitted inline free
* reads the generation as an i8 at obj-7. Hence the assertions. */
_Static_assert(sizeof(ScrCycHdr) == 16, "LLVM backend expects a 16-byte cycle header");
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, buffered) == 6 &&
sizeof(((ScrCycHdr *)0)->buffered) == 2,
"the emitted release fast path reads `buffered` as an i16 at obj-6");
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, gen) == 7,
"LLVM backend's inline free reads the generation as an i8 at obj-7");
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, type) == 16 ||
UINTPTR_MAX == UINT32_MAX,
"LLVM backend stores the descriptor at the block start on 64-bit targets");
/* This layout is an ABI, not an implementation detail. The emitted
* allocation stores the id and zeroes the rest; every emitted release of a
* headered object (llvm/shapes.ts cycleRootLines, used by the record, class
* and inline RC helpers) mirrors scr_cyc_on_release's early return by
* testing the sign of `buf` as an i32 at obj-4, and the emitted inline free
* reads the generation as an i8 at obj-5. Hence the assertions. */
_Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header");
_Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2,
"the emitted allocation stores the registry id as an i16 at the block start");
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, buf) == 4,
"the emitted release fast path tests `buf` as an i32 at obj-4");
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, gen) == 5,
"LLVM backend's inline free reads the generation as an i8 at obj-5");
_Static_assert(sizeof(ScrCycHdr) % _Alignof(double) == 0,
"cycle-headered payloads must preserve double alignment");
static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; }
/* Registry of descriptors by id (copies, so a header reaches its entry
* points in one step); ids are assigned on first allocation and shared by
* every thread. */
#define SCR_CYC_MAX_TYPES ((uint32_t)1 << 15)
extern ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES];
uint32_t scr_cyc_register(ScrCycType *type);
static inline uint32_t scr_cyc_type_id(ScrCycType *type) {
/* Acquire pairs with scr_cyc_register's publishing compare-exchange, so
* a thread that sees the id also sees its scr_cyc_types entry. */
uint32_t id = __atomic_load_n(&type->id, __ATOMIC_ACQUIRE);
return id ? id : scr_cyc_register(type);
}
static inline const ScrCycType *scr_cyc_type(const ScrCycHdr *h) { return &scr_cyc_types[h->id]; }
static inline unsigned scr_cyc_color(const ScrCycHdr *h) { return h->color; }
static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color = (uint8_t)color; }
static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; }
static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; }
static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) {
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0;
}
static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) {
return h->buf & ~SCR_CYC_BUFFERED_BIT;
}
/* Buffered at position `index` of its generation's buffer, or not at all. */
static inline void scr_cyc_set_buffered(ScrCycHdr *h, uint32_t index) {
h->buf = SCR_CYC_BUFFERED_BIT | index;
}
static inline void scr_cyc_clear_buffered(ScrCycHdr *h) { h->buf = 0; }
/* Zeroed allocation with a cycle header in front; returns the OBJECT
* pointer (header at scr_cyc_hdr). `type` must outlive the object (a
* static descriptor). Aborts on OOM. */
void *scr_cyc_alloc(size_t size, const ScrCycType *type);
void *scr_cyc_alloc(size_t size, ScrCycType *type);
void scr_cyc_free(void *obj); /* frees the block, header included */
/* ── small-object allocator (scr_alloc.c) ───────────────────────────────
@@ -714,26 +745,27 @@ static inline void scr_mem_free(void *p) { free(p); }
* sites (union boxes, arrays, boxes, closures, maps, dyn nodes). With a
* constant size the size class and the zeroing fold at compile time. The
* live count is exported for these and for the emitted inline paths
* (llvm/alloc.ts), which do the same three things: header words, live
* count, block. Every other header word starts zero (SCR_CYC_BLACK,
* SCR_CYC_NURSERY, unbuffered). */
* (llvm/alloc.ts), which do the same three things: header (the registry
* id; the rest starts zero: SCR_CYC_BLACK, SCR_CYC_NURSERY, unbuffered),
* live count, block. */
#ifndef SCR_WORKERS
extern SCR_TL size_t scr_cyc_live;
extern SCR_TL size_t scr_cyc_old_freed;
extern SCR_TL void (*scr_weak_dispose_hook)(void *);
#endif
static inline void *scr_cyc_alloc_inline(size_t size, const ScrCycType *type) {
static inline void *scr_cyc_alloc_inline(size_t size, ScrCycType *type) {
if (size > SIZE_MAX - sizeof(ScrCycHdr)) scr_trap("scriptc: out of memory\n");
uint32_t id = scr_cyc_type_id(type);
ScrCycHdr *h = (ScrCycHdr *)scr_mem_calloc(sizeof(ScrCycHdr) + size);
if (!h) scr_trap("scriptc: out of memory\n");
h->type = type;
h->id = (uint16_t)id;
scr_cyc_live++;
return h + 1;
}
static inline void scr_cyc_free_inline(void *obj) {
if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj);
scr_cyc_live--;
if (scr_cyc_hdr(obj)->gen == SCR_CYC_OLD) scr_cyc_old_freed++;
if (scr_cyc_gen(scr_cyc_hdr(obj)) == SCR_CYC_OLD) scr_cyc_old_freed++;
scr_mem_free(scr_cyc_hdr(obj));
}
_Static_assert(SCR_CYC_NURSERY == 0, "zeroed cycle headers start in the nursery");
@@ -761,10 +793,10 @@ void scr_cyc_on_dead(void *obj);
* object, and the death of an object that was never buffered, need no call.
* Most releases of long-lived containers take the first early return. */
static inline void scr_cyc_released(void *obj) {
if (!scr_cyc_hdr(obj)->buffered) scr_cyc_on_release(obj);
if (!scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_release(obj);
}
static inline void scr_cyc_dead(void *obj) {
if (scr_cyc_hdr(obj)->buffered) scr_cyc_on_dead(obj);
if (scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_dead(obj);
}
/* Full sweep: trial-deletion passes over EVERY generation, to a fixpoint.
@@ -1988,14 +2020,21 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy);
* move without reference-count traffic; a pending exception from `f` stops
* the sort and leaves the snapshot unchanged. */
struct ScrClosure;
void scr_arr_sort_values(ScrArr *a, double count, struct ScrClosure *f, uint32_t arity);
/* A nonzero `order` says `f` orders number keys ascending (1) or descending
* (-1) without side effects: f64 elements are their own keys, and reference
* elements read theirs through the borrowed `key` closure (element ->
* number). Keys that are all numbers other than NaN and -0 then sort without
* calling `f`, with the same stable result. */
void scr_arr_sort_values(ScrArr *a, double count, struct ScrClosure *f, uint32_t arity,
int32_t order, struct ScrClosure *key);
/* Comparator sort of a dense array in place: `a.sort(f)`, or with `owned`
* the private copy of `a.toSorted(f)`. Returns false without calling `f`
* when `a` has holes, storage-level undefined or a sparse tail, or is
* published; the caller then sorts a compacted snapshot. Otherwise sorts
* and returns true; a pending exception from `f` leaves a shared `a`
* unwritten (an owned copy then holds a permutation of its values). */
bool scr_arr_sort_dense(ScrArr *a, struct ScrClosure *f, uint32_t arity, bool owned);
bool scr_arr_sort_dense(ScrArr *a, struct ScrClosure *f, uint32_t arity, bool owned,
int32_t order, struct ScrClosure *key);
/* pop traps on an empty array; _ref transfers ownership out (+1 to the
* caller, no release). */
@@ -4210,6 +4249,15 @@ ScrArr *scr_path_win32_parse(ScrStr *path);
ScrStr *scr_path_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, ScrStr *ext);
ScrStr *scr_path_win32_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name,
ScrStr *ext);
/* path.matchesGlob (scr_glob.c): Node 24's minimatch with matchGlobPattern's
* options, case-insensitive on macOS and Windows hosts like Node. Borrows
* both; a brace sequence over 2^32 - 1 elements throws Node's RangeError, a
* pattern over 65536 units its TypeError, and a segment regex invalid under
* the u flag its SyntaxError. scr_glob_match is the same with the family
* and case mode explicit (the oracle tests use it). */
bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern);
bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern);
bool scr_glob_match(ScrStr *path, ScrStr *pattern, bool windows, bool nocase);
/* fs.openSync(path, flags) → the raw fd as f64 (Node's string flag
* grammar; unknown flags throw Node's ERR_INVALID_ARG_VALUE TypeError
@@ -5592,6 +5640,24 @@ bool scr_dyn_is_nullish(const ScrDyn *d);
* fn.apply(this, arguments)`) forwards the receiver for free. */
void scr_dyn_this_push(void *h, ScrDynHandleTag tag); /* h BORROWED (the firing site holds it); NULL binds undefined */
void scr_dyn_this_push_dyn(const ScrDyn *v); /* retained for the window */
/* A class's typed-ref construction recipe, emitted once per class as a
* constant (kind: 0 plain, 1 observed, 2 traced capsule). `view` names the
* live view bound to the new capsule, as eager boxing binds it: 0 none,
* 1 array ops, 2 record ops, 3 class ops (`ops`). */
typedef struct {
void *(*retain)(void *);
void (*release)(void *);
const char *type_key;
size_t type_key_len;
ScrDyn *(*materialize)(void *);
void (*commit)(void *, const ScrDyn *);
int kind;
int view;
const void *ops;
} ScrDynTypedRefRecipe;
/* A class instance as the window's receiver, boxed with its recipe only when
* read. ptr is BORROWED: the caller holds the instance across the window. */
void scr_dyn_this_push_typed(void *ptr, const ScrDynTypedRefRecipe *recipe);
void scr_dyn_this_pop(void);
/* The innermost binding as a dyn value (+1): a boxed handle, the pushed
* dyn value, or the undefined singleton (empty stack, NULL handle, or a
+1 -1
View File
@@ -1230,7 +1230,7 @@ static ScrStreamState *scr_stream_state_new(bool has_r, bool has_w,
static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w,
double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open) {
static const ScrCycType cyc_type = { &scr_stream_trace, &scr_stream_gcfree, SCR_CYC_KIND_STREAM };
static ScrCycType cyc_type = { &scr_stream_trace, &scr_stream_gcfree, SCR_CYC_KIND_STREAM };
ScrStream *s = scr_cyc_alloc(sizeof *s, &cyc_type);
s->rc = 1;
s->vt = vt;
+1 -1
View File
@@ -39,7 +39,7 @@ static void scr_union_gcfree(void *o) {
}
static ScrUnion *scr_union_alloc(uint32_t tag) {
static const ScrCycType cyc_type = { &scr_union_trace, &scr_union_gcfree, SCR_CYC_KIND_UNION };
static ScrCycType cyc_type = { &scr_union_trace, &scr_union_gcfree, SCR_CYC_KIND_UNION };
ScrUnion *u = scr_cyc_alloc_inline(sizeof(ScrUnion), &cyc_type);
u->rc = 1;
u->tag = tag;
+1 -1
View File
@@ -77,7 +77,7 @@ static void scr_worker_port_listeners(void *ptr, const ScrStr *name, bool presen
}
static ScrWorkerView *scr_worker_view_new(bool port) {
static const ScrCycType cyc_type = { &scr_worker_view_trace, &scr_worker_view_gcfree, SCR_CYC_KIND_WORKER_VIEW };
static ScrCycType cyc_type = { &scr_worker_view_trace, &scr_worker_view_gcfree, SCR_CYC_KIND_WORKER_VIEW };
ScrWorkerView *view = scr_cyc_alloc(sizeof(*view), &cyc_type);
view->rc = 1;
view->port = port;
+469
View File
@@ -0,0 +1,469 @@
// Generates path.matchesGlob test cases with Node as the oracle.
//
// node --expose-internals gen-glob-cases.mjs > glob-cases.txt
//
// Lines are tab-separated:
// p <path> adds a path to the curated list
// c <pattern> <posix> <win32> a pattern against every curated path
// f <path> <pattern> <posix> <win32> one fuzz case
// Strings keep printable ASCII and spell everything else (and "\") as
// \uXXXX UTF-16 code units, so lone surrogates survive. Each answer is two
// characters per path, case-sensitive then case-insensitive ("1", "0", or
// "E" when Node throws): Node 24 matches case-insensitively on macOS and
// Windows hosts, so both modes are recorded through the bundled minimatch
// with matchGlobPattern's exact options. Node v24 is required: the cases pin
// its bundled minimatch.
import { posix, win32 } from "node:path";
import { stdout } from "node:process";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { minimatch } = require("internal/deps/minimatch/index");
// The pattern has no u flag, so each match is one UTF-16 code unit.
const escape = (s) =>
s.replace(/[^\x20-\x5b\x5d-\x7e]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
const options = (windows, nocase) => ({
nocase,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: windows ? "win32" : "posix",
nocaseMagicOnly: true,
});
const lines = [];
const answer = (path, pattern, windows, nocase) => {
try {
return minimatch(path, pattern, options(windows, nocase)) ? "1" : "0";
} catch {
return "E";
}
};
const hostNocase = process.platform === "darwin" || process.platform === "win32";
// Both case modes for one family; the public API on this host must agree
// with the recorded options.
const answers = (path, pattern, windows) => {
const expected = [false, true].map((nocase) => answer(path, pattern, windows, nocase));
let actual;
try {
actual = (windows ? win32 : posix).matchesGlob(path, pattern) ? "1" : "0";
} catch {
actual = "E";
}
if (actual !== expected[hostNocase ? 1 : 0])
throw new Error(`oracle mismatch: ${path} ${pattern}`);
return expected.join("");
};
// ── curated vocabulary ──────────────────────────────────────────────
const paths = [
"",
".",
"..",
"/",
"a",
"A",
"ab",
"a.b",
"a.js",
"A.JS",
".a",
".js",
"a/b",
"a/b/c",
"a/b/c/d",
"a/.b",
"a/.b/c",
"a/../b",
"./a",
"../a",
"a//b",
"a/b/",
"/a",
"/a/b/c.js",
"//a/b",
"x/y/.z.ts",
"src/lib/index.test.ts",
"node_modules/pkg/index.js",
"a\\b",
"C:\\a\\b",
"c:\\a",
"C:/a/b",
"\\\\server\\share\\x",
"//server/share/x",
"//?/C:/x",
"a b",
"a,b",
"a{b}",
"a(b)",
"[a]",
"a!b",
"a+b",
"a*b",
"file.tar.gz",
"file.",
"1",
"12",
"a10",
"é",
"É",
"😀",
"a😀b",
"\ud83d",
"abc/def/ghi.txt",
"+(a)",
"\t",
"a\nb",
"aaa",
"b",
"ba",
"a/../../b",
"../../a",
];
const patterns = [
"",
"*",
"**",
"***",
"?",
"??",
"???",
"a",
"A",
"a*",
"*a",
"*a*",
"a?",
"?a",
".*",
"*.*",
"*.js",
"*.JS",
"**.js",
"**/*.js",
"**/*",
"*/*",
"*/**",
"**/**",
"a/**",
"a/**/b",
"a/**/*",
"**/a",
"a/*/c",
"a/*/*",
"/a",
"/**",
"/*",
"//a",
"a/b/",
"a/b",
"./a",
"../a",
"a/../b",
"a/./b",
"a/.b",
"a/.*",
".a",
".a/*",
"**/.b",
"**/.*",
"**/../a",
"a/**/../b",
"a/**/../b/c",
"{a,b}",
"{a,b}/c",
"{,a}",
"{a,}",
"{a}",
"{}",
"{}a",
"a{}",
"{a,b{c,d}}",
"{a..c}",
"{c..a}",
"{1..3}",
"{01..3}",
"{3..1}",
"{1..10..3}",
"{a..e..2}",
"{-2..2}",
"{a,b,c}{1,2}",
"x{a,b}y",
"{a/b,c}",
"${a,b}",
"{a,b}}",
"{{a,b}",
"{a,{b,c}}",
"{a\\,b}",
"{1..1}",
"{A..c}",
"{x..z,a}",
"[abc]",
"[a-c]",
"[!a]",
"[^a]",
"[a-]",
"[-a]",
"[]]",
"[]a]",
"[!]]",
"[a-z0-9]",
"[z-a]",
"[a",
"a]",
"[[]",
"[\\]]",
"[.]*",
"[[:alpha:]]",
"[[:digit:]]*",
"[[:upper:]]",
"[[:lower:]]",
"[[:alnum:]_]",
"[[:space:]]",
"[[:punct:]]",
"[[:xdigit:]]",
"[[:word:]]",
"[[:ascii:]]",
"[![:alpha:]]",
"[[:graph:]]",
"[[:print:]]",
"[[:cntrl:]]",
"[[:blank:]]",
"[a-[:digit:]]",
"[[:bogus:]]",
"[é]",
"[😀]",
"[à-ü]",
"+(a)",
"*(a)",
"?(a)",
"@(a)",
"!(a)",
"!(a|b)",
"+(a|b)",
"*(a|b)c",
"a+(b)",
"a*(b)",
"a?(b)",
"a@(b|c)",
"a!(b)",
"!(*.js)",
"!(.*)",
"*.!(js)",
"!(a)/b",
"a/!(b)",
"+(a|*.js)",
"@(a|b)/**",
"!(!(a))",
"+(+(a))",
"*(+(a))",
"?(*(a))",
"@(+(a))",
"+(?(a))",
"!(@(a))",
"@(!(a))",
"+(*(a)|b)",
"*(a|+(b))",
"!()",
"+()",
"@()",
"?()",
"*()",
"a!()",
"!(a",
"+(a",
"a)",
"(a)",
"+(a)b)",
"!(a)*",
"*!(a)",
"+(a|)",
"@(|a)",
"!(|a)",
"x/+(a|b)/y",
"+([a-c])",
"!([a-c])",
"@(*.ts|*.js)",
"**/@(a|b).js",
"?(a|b|c)?",
"!(foo).js",
"!(foo)*.js",
"+(!(a))",
"*(!(a))",
"!(+(a))",
"!(*(a))",
"!(?(a))",
"#a",
"!a",
"!!a",
"a\\b",
"\\a",
"a\\",
"C:/a/*",
"c:/a/*",
"C:\\a\\*",
"C:/**",
"//server/share/*",
"//?/C:/*",
"//?/c:/x",
"*/share/*",
"**/c",
"**/*.ts",
"src/**/*.ts",
"src/**/!(*.test).ts",
"**/node_modules/**",
"**/*.{js,ts}",
"**/*.+(js|ts)",
"docs/*.md",
"*.min.js",
"*.js.map",
"**/index.@(ts|js)",
"x/**/z.ts",
"x/*/z.ts",
"x/**/.z.ts",
"a/b/**/c/**/d",
"**/b/**",
"a/**/b/**/c",
"?/?",
"??/*",
"*.?",
"a.?",
"a??",
"a/?/c",
"é",
"É",
"日*",
"😀",
"?😀?",
"a?b",
"a*b*c",
"a**b",
"**a",
"a**",
"*/",
"**/",
"/",
"//",
".",
"..",
"./*",
"../*",
"*.tar.*",
"file.*",
"*.",
"[.]",
"\\*",
"a{b,c",
"a{b..c}",
"{a,b,c,d,e}{f,g}",
"*{.js,.ts}",
"+(a|b)+(c|d)",
"!(a).!(b)",
"@(a|b|c|d|e|f|g)",
"*(?)",
"+(*)",
"!(*)",
"@(*)",
"?(*)",
"\t",
"a\nb",
" ",
"*\n*",
"[\n]",
"a/../../b",
"../../*",
"**/../../a",
"a/b/../../../c",
];
for (const path of paths) lines.push(`p\t${escape(path)}`);
for (const pattern of patterns) {
const family = (windows) => paths.map((path) => answers(path, pattern, windows)).join("");
lines.push(`c\t${escape(pattern)}\t${family(false)}\t${family(true)}`);
}
// ── seeded fuzz over glob syntax ────────────────────────────────────
let seed = 20261014;
const random = () => {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
};
const pick = (list) => list[Math.floor(random() * list.length)];
const patternTokens = [
"a",
"b",
"c",
".",
"/",
"*",
"**",
"?",
"[",
"]",
"!",
"^",
"-",
"(",
")",
"|",
"+",
"@",
"{",
"}",
",",
"..",
"\\",
":",
"A",
"B",
"é",
"1",
"2",
"[:alpha:]",
"[:digit:]",
"!(",
"+(",
"*(",
"?(",
"@(",
"{a,b}",
"{1..3}",
"[a-c]",
"[!b]",
"C:",
];
const pathTokens = [
"a",
"b",
"c",
".",
"..",
"/",
"\\",
"A",
"B",
"ab",
"é",
"1",
"2",
"-",
"C:",
"+",
"(",
")",
];
const make = (tokens, max) => {
const n = 1 + Math.floor(random() * max);
let s = "";
for (let i = 0; i < n; i++) s += pick(tokens);
return s;
};
// A zero step never ends: Node's expansion fails with RangeError (slowly).
const fuzz = [["0", "{0..1..0}"]];
for (let i = 0; i < 3000; i++) fuzz.push([make(pathTokens, 6), make(patternTokens, 7)]);
for (const [path, pattern] of fuzz) {
const results = [false, true].map((windows) => answers(path, pattern, windows));
lines.push(`f\t${escape(path)}\t${escape(pattern)}\t${results.join("\t")}`);
}
stdout.write(lines.join("\n") + "\n");
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
import { execFile } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { beforeAll, expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const testDir = import.meta.dirname;
const bin = join(testDir, "build", "test_glob");
const vendorDir = join(testDir, "../vendor/quickjs-ng");
// Compiled once with ASan + the RC audit: the oracle run also proves the
// glob port neither leaks nor double-frees across the case file's calls.
// Segment regexes compile through the vendored libregexp, as in programs.
beforeAll(async () => {
await mkdir(join(testDir, "build"), { recursive: true });
await execFileAsync("clang", [
// no -Wall/-Wextra: the vendored libunicode.c is not warning-clean
"-std=c11",
"-O1",
"-fsanitize=address",
"-DSCR_RC_AUDIT",
"-I",
vendorDir,
"-o",
bin,
join(testDir, "test_glob.c"),
join(testDir, "../src/scr_glob.c"),
join(testDir, "../src/scr_regex.c"),
// scr_regex.c hosts assert.match and calls the assert throw/inspect
// helpers — regex-linking binaries always carry scr_assert.c (native-toolchain.ts).
join(testDir, "../src/scr_assert.c"),
join(vendorDir, "libregexp.c"),
join(vendorDir, "libunicode.c"),
join(testDir, "../src/scr_string.c"),
join(testDir, "../src/scr_map.c"),
join(testDir, "../src/scr_array.c"),
join(testDir, "../src/scr_json.c"),
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_number.c"),
join(testDir, "../src/scr_console.c"),
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_object.c"),
join(testDir, "../src/scr_union.c"),
join(testDir, "../src/scr_cycle.c"),
join(testDir, "../src/scr_alloc.c"),
join(testDir, "../src/scr_lib.c"),
join(testDir, "../src/scr_url.c"),
join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_path.c"),
join(testDir, "../src/scr_bytes.c"),
join(testDir, "../src/scr_shared.c"),
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
]);
}, 60_000);
// Runs against the committed case file, generated from Node v24's bundled
// minimatch with matchGlobPattern's options (gen-glob-cases.mjs): curated
// brace, extglob, class, globstar, dot, drive and UNC patterns over a path
// vocabulary, plus a seeded fuzz corpus, for both path families and both
// case modes.
test("path.matchesGlob matches Node on committed oracle cases", async () => {
const { stderr } = await execFileAsync(bin, [join(testDir, "glob-cases.txt")], {
maxBuffer: 16 * 1024 * 1024,
});
expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
}, 120_000);
+183 -15
View File
@@ -388,7 +388,7 @@ static void mock_cyc_release(void *p) {
}
static MockRec *mock_cyc_new(int value) {
static const ScrCycType cyc_type = { &mock_trace, &mock_gcfree, 0 };
static ScrCycType cyc_type = { &mock_trace, &mock_gcfree, 0 };
MockRec *r = scr_cyc_alloc(sizeof *r, &cyc_type);
r->rc = 1;
r->value = value;
@@ -1029,7 +1029,7 @@ static void test_comparator_sort(void) {
const size_t n = sizeof(input) / sizeof(*input);
for (size_t i = 0; i < n; i++) scr_arr_push_f64(nums, input[i]);
scr_arr_push_f64(nums, -1); /* beyond the sorted count */
scr_arr_sort_values(nums, (double)n, &floor_fn, 2);
scr_arr_sort_values(nums, (double)n, &floor_fn, 2, 0, NULL);
bool ordered = true;
for (size_t i = 1; i < n; i++) {
double a = scr_arr_get_f64(nums, (double)(i - 1)), b = scr_arr_get_f64(nums, (double)i);
@@ -1043,13 +1043,13 @@ static void test_comparator_sort(void) {
ScrArr *few = scr_arr_new(SCR_ELEM_F64, 0);
for (int i = 0; i < 5; i++) scr_arr_push_f64(few, 5 - i);
sort_calls = 0;
scr_arr_sort_values(few, 5, &nan_fn, 0);
scr_arr_sort_values(few, 5, &nan_fn, 0, 0, NULL);
check(sort_calls > 0 && scr_arr_get_f64(few, 0) == 5 && scr_arr_get_f64(few, 4) == 1,
"a NaN comparator result keeps the input order");
scr_arr_sort_values(few, 5, &one_fn, 1);
scr_arr_sort_values(few, 5, &one_fn, 1, 0, NULL);
check_f64(scr_arr_len(few), 5, "a one-parameter comparator sort keeps every element");
sort_calls = 0;
scr_arr_sort_values(few, 1, &nan_fn, 0);
scr_arr_sort_values(few, 1, &nan_fn, 0, 0, NULL);
check(sort_calls == 0, "a single value needs no comparator call");
scr_arr_release(few);
scr_arr_release(nums);
@@ -1068,7 +1068,7 @@ static void test_comparator_sort(void) {
}
sort_calls = 0;
sort_throw_at = 7;
scr_arr_sort_values(words, (double)word_count, &length_fn, 2);
scr_arr_sort_values(words, (double)word_count, &length_fn, 2, 0, NULL);
check(scr_exc_pending(), "a throwing comparator leaves the exception pending");
scr_exc_clear();
check(sort_calls == 7, "the sort stops at the first comparator exception");
@@ -1077,7 +1077,7 @@ static void test_comparator_sort(void) {
if (scr_arr_peek_ref(words, (double)i) != originals[i]) unchanged = false;
check(unchanged, "a comparator exception leaves the snapshot unchanged");
sort_throw_at = -1;
scr_arr_sort_values(words, (double)word_count, &length_fn, 2);
scr_arr_sort_values(words, (double)word_count, &length_fn, 2, 0, NULL);
check(!scr_exc_pending(), "a successful comparator sort leaves no exception");
bool stable = true;
size_t last_index = 0, last_len = 0;
@@ -1102,7 +1102,7 @@ static void test_comparator_sort(void) {
const size_t big = ((size_t)1 << 20) + 300;
ScrArr *wide = scr_arr_new(SCR_ELEM_F64, 0);
for (size_t i = 0; i < big; i++) scr_arr_push_f64(wide, (double)(big - i) + 0.5);
scr_arr_sort_values(wide, (double)big, &floor_fn, 2);
scr_arr_sort_values(wide, (double)big, &floor_fn, 2, 0, NULL);
bool wide_ordered = true;
for (size_t i = 0; i < big; i++)
if (scr_arr_get_f64(wide, (double)i) != (double)(i + 1) + 0.5) wide_ordered = false;
@@ -1147,7 +1147,7 @@ static void test_dense_comparator_sort(void) {
dense_target = nums;
sort_calls = 0;
sort_throw_at = -1;
check(scr_arr_sort_dense(nums, &observe_fn, 2, false), "a dense number array sorts in place");
check(scr_arr_sort_dense(nums, &observe_fn, 2, false, 0, NULL), "a dense number array sorts in place");
check(sort_throw_at == -1, "the comparator sees the unsorted receiver");
bool ordered = true;
for (size_t i = 1; i < 101; i++)
@@ -1158,21 +1158,21 @@ static void test_dense_comparator_sort(void) {
scr_arr_push_f64(holes, 3);
scr_arr_set_f64(holes, 2, 1);
sort_calls = 0;
check(!scr_arr_sort_dense(holes, &floor_fn, 2, false) && sort_calls == 0,
check(!scr_arr_sort_dense(holes, &floor_fn, 2, false, 0, NULL) && sort_calls == 0,
"an array with holes takes the snapshot path without comparator calls");
ScrArr *undef = scr_arr_new(SCR_ELEM_F64, 0);
scr_arr_push_f64(undef, 3);
scr_arr_set_undefined(undef, 1);
scr_arr_push_f64(undef, 1);
check(!scr_arr_sort_dense(undef, &floor_fn, 2, true) && sort_calls == 0,
check(!scr_arr_sort_dense(undef, &floor_fn, 2, true, 0, NULL) && sort_calls == 0,
"storage-level undefined takes the snapshot path");
ScrArr *one = scr_arr_new(SCR_ELEM_F64, 0);
scr_arr_push_f64(one, 5);
check(scr_arr_sort_dense(one, &floor_fn, 2, false) && sort_calls == 0,
check(scr_arr_sort_dense(one, &floor_fn, 2, false, 0, NULL) && sort_calls == 0,
"a single value needs no comparator call");
size_t rc = one->rc;
one->rc = SIZE_MAX; /* as publication marks it */
check(!scr_arr_sort_dense(one, &floor_fn, 2, false), "a published array takes the general path");
check(!scr_arr_sort_dense(one, &floor_fn, 2, false, 0, NULL), "a published array takes the general path");
one->rc = rc;
scr_arr_release(one);
scr_arr_release(undef);
@@ -1193,7 +1193,7 @@ static void test_dense_comparator_sort(void) {
sort_calls = 0;
sort_throw_at = at;
dense_edit_at = -1;
check(scr_arr_sort_dense(words, &length_fn, 2, owned) && scr_exc_pending(),
check(scr_arr_sort_dense(words, &length_fn, 2, owned, 0, NULL) && scr_exc_pending(),
"a throwing comparator leaves the exception pending");
scr_exc_clear();
check(sort_calls == at && scr_arr_len(words) == count,
@@ -1225,7 +1225,7 @@ static void test_dense_comparator_sort(void) {
sort_calls = 0;
sort_throw_at = -1;
dense_edit_at = 3;
check(scr_arr_sort_dense(words, &length_fn, 2, false) && !scr_exc_pending(),
check(scr_arr_sort_dense(words, &length_fn, 2, false, 0, NULL) && !scr_exc_pending(),
"a comparator that truncates the receiver still completes");
dense_edit_at = -1;
bool lengths = scr_arr_len(words) == count;
@@ -1539,6 +1539,173 @@ static void test_inline_slots(void) {
#endif
}
/* Keyed sorts for recognized numeric comparators: the same stable result as
* the comparator sort, without comparator calls unless a key is NaN or -0. */
static double keyed_ascending(ScrClosure *self, double a, double b) {
(void)self;
sort_calls++;
return a - b;
}
static double keyed_descending(ScrClosure *self, double a, double b) {
(void)self;
sort_calls++;
return b - a;
}
static double keyed_rec_ascending(ScrClosure *self, void *a, void *b) {
(void)self;
sort_calls++;
double d = (double)((MockRec *)a)->value - (double)((MockRec *)b)->value;
mock_release(a);
mock_release(b);
return d;
}
static double keyed_rec_descending(ScrClosure *self, void *a, void *b) {
(void)self;
sort_calls++;
double d = (double)((MockRec *)b)->value - (double)((MockRec *)a)->value;
mock_release(a);
mock_release(b);
return d;
}
/* Reads value 0 as -0, which the comparator also treats as equal to 0. A
* first scan reads keys without storing them, so a key may be read twice,
* and placing a short tail reads head keys again in binary searches. */
static long key_reads = 0;
static double keyed_rec_key(ScrClosure *self, void *a) {
(void)self;
key_reads++;
int value = ((MockRec *)a)->value;
mock_release(a);
return value == 0 ? -0.0 : (double)value;
}
static uint32_t keyed_seed = 7;
static uint32_t keyed_random(void) {
keyed_seed = keyed_seed * 1103515245u + 12345u;
return keyed_seed >> 8;
}
static void keyed_fill(ScrArr *a, ScrArr *b, size_t n, int shape) {
for (size_t i = 0; i < n; i++) {
double v;
if (shape == 0) v = (double)(keyed_random() % 64) - 32; /* small integers with ties */
else if (shape == 1) v = ((double)keyed_random() - 4e6) / 977.0 * 1e10; /* wide doubles */
else if (shape == 2) v = (double)i; /* presorted */
else if (shape == 3) v = (double)(n - i) * 0.25; /* strictly reversed */
else if (shape == 4) v = (double)((i % (n / 3 + 1)) / 2); /* three runs with ties */
else if (shape == 5) v = i + 3 < n ? (double)i : (double)(keyed_random() % 64); /* sorted, then appended */
else v = i < 3 ? (double)(keyed_random() % 64) : (double)(i / 2); /* a few, then sorted */
if (shape == 1 && i == 3) v = INFINITY;
if (shape == 1 && i == 5) v = -INFINITY;
scr_arr_push_f64(a, v);
scr_arr_push_f64(b, v);
}
}
static bool keyed_same_numbers(ScrArr *a, ScrArr *b) {
if (scr_arr_len(a) != scr_arr_len(b)) return false;
for (size_t i = 0; i < (size_t)scr_arr_len(a); i++) {
double x = scr_arr_get_f64(a, (double)i), y = scr_arr_get_f64(b, (double)i);
if (memcmp(&x, &y, sizeof x) != 0) return false;
}
return true;
}
static void test_keyed_sort(void) {
ScrClosure ascending = {SIZE_MAX, (void *)keyed_ascending, 0, NULL, 0};
ScrClosure descending = {SIZE_MAX, (void *)keyed_descending, 0, NULL, 0};
const size_t sizes[] = {2, 5, 40, 49, 1000, 70000, 140000}; /* the last takes 11-bit digits */
const size_t size_count = sizeof(sizes) / sizeof(*sizes);
bool same = true, uncalled = true;
for (size_t s = 0; s < size_count; s++) {
for (int shape = 0; shape < 7; shape++) {
for (int order = -1; order <= 1; order += 2) {
ScrClosure *cmp = order > 0 ? &ascending : &descending;
for (int path = 0; path < 3; path++) {
ScrArr *expected = scr_arr_new(SCR_ELEM_F64, 0);
ScrArr *actual = scr_arr_new(SCR_ELEM_F64, 0);
keyed_fill(expected, actual, sizes[s], shape);
scr_arr_sort_values(expected, (double)sizes[s], cmp, 2, 0, NULL);
sort_calls = 0;
if (path == 0) scr_arr_sort_values(actual, (double)sizes[s], cmp, 2, order, NULL);
else scr_arr_sort_dense(actual, cmp, 2, path == 2, order, NULL);
uncalled &= sort_calls == 0;
same &= keyed_same_numbers(expected, actual);
scr_arr_release(expected);
scr_arr_release(actual);
}
}
}
}
check(same, "keyed number sorts match the comparator sort");
check(uncalled, "keyed number sorts call no comparator");
const double specials[] = {NAN, -0.0};
for (size_t k = 0; k < 2; k++) {
ScrArr *expected = scr_arr_new(SCR_ELEM_F64, 0);
ScrArr *actual = scr_arr_new(SCR_ELEM_F64, 0);
keyed_fill(expected, actual, 300, 0);
scr_arr_set_f64(expected, 150, specials[k]);
scr_arr_set_f64(actual, 150, specials[k]);
scr_arr_sort_values(expected, 300, &ascending, 2, 0, NULL);
sort_calls = 0;
scr_arr_sort_dense(actual, &ascending, 2, true, 1, NULL);
check(sort_calls > 0 && keyed_same_numbers(expected, actual),
k == 0 ? "a NaN key sorts through the comparator" : "a -0 key sorts through the comparator");
scr_arr_release(expected);
scr_arr_release(actual);
}
ScrClosure rec_ascending = {SIZE_MAX, (void *)keyed_rec_ascending, 0, NULL, 0};
ScrClosure rec_descending = {SIZE_MAX, (void *)keyed_rec_descending, 0, NULL, 0};
ScrClosure rec_key = {SIZE_MAX, (void *)keyed_rec_key, 0, NULL, 0};
long live0 = mock_live;
bool identical = true, direct = true, fallback = true;
for (size_t s = 0; s < size_count; s++) {
for (int order = -1; order <= 1; order += 2) {
ScrClosure *cmp = order > 0 ? &rec_ascending : &rec_descending;
for (int variant = 0; variant < 8; variant++) {
/* Random keys, three ascending runs, ascending keys with a few or an
* eighth appended, each without and with a -0 key. */
bool zero = variant & 1, runs = variant / 2 == 1, appended = variant / 2 >= 2;
size_t tail = variant / 2 == 3 ? sizes[s] / 8 : 4;
size_t n = sizes[s];
ScrArr *expected = scr_arr_new_ops(&mock_ops, 0);
ScrArr *actual = scr_arr_new_ops(&mock_ops, 0);
for (size_t i = 0; i < n; i++) {
int value = runs ? (int)((i % (n / 3 + 1)) * 40 / (n / 3 + 1)) + 1
: appended ? (i + tail < n ? (int)(i * 40 / n) + 1 : (int)(keyed_random() % 45))
: (int)(keyed_random() % 50) - 25;
if (value == 0) value = 25;
if (zero && i == n / 2) value = 0;
MockRec *r = mock_new(value);
scr_arr_push_ref(expected, r);
scr_arr_push_ref(actual, mock_retain(r));
}
scr_arr_sort_values(expected, (double)n, cmp, 2, 0, NULL);
sort_calls = 0;
key_reads = 0;
if (s % 2) scr_arr_sort_values(actual, (double)n, cmp, 2, order, &rec_key);
else scr_arr_sort_dense(actual, cmp, 2, s % 4 == 2, order, &rec_key);
for (size_t i = 0; i < n; i++)
identical &= scr_arr_peek_ref(expected, (double)i) == scr_arr_peek_ref(actual, (double)i);
if (zero) fallback &= sort_calls > 0;
else direct &= sort_calls == 0 && key_reads <= 2 * (long)n + (long)tail * 64;
scr_arr_release(expected);
scr_arr_release(actual);
}
}
}
check(identical, "keyed record sorts keep the comparator sort's stable permutation");
check(direct, "keyed record sorts read keys a bounded number of times and call no comparator");
check(fallback, "a -0 record key sorts through the comparator");
check(mock_live == live0, "keyed record sorts balance every key read");
}
int main(int argc, char **argv) {
if (argc > 1) {
ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0);
@@ -1563,6 +1730,7 @@ int main(int argc, char **argv) {
test_primitive_sort();
test_comparator_sort();
test_keyed_sort();
test_dense_comparator_sort();
test_f64_basics();
test_numeric_read();
+3 -3
View File
@@ -95,20 +95,20 @@ int main(void) {
&scr_str_release_v, NULL);
scr_union_retain(su);
scr_union_release(su);
CHECK(!scr_cyc_hdr(su)->buffered);
CHECK(!scr_cyc_is_buffered(scr_cyc_hdr(su)));
scr_union_release(su);
CHECK(scr_str_live_count() == 0);
ScrUnion *fu = scr_union_new_f64(1, 2.5);
scr_union_retain(fu);
scr_union_release(fu);
CHECK(!scr_cyc_hdr(fu)->buffered);
CHECK(!scr_cyc_is_buffered(scr_cyc_hdr(fu)));
scr_union_release(fu);
ScrUnion *cu = scr_union_new_ref(0, scr_closure_new((void *)&dummy_fn, 0),
&scr_closure_retain_v, &scr_closure_release_v,
&scr_closure_trace_v);
scr_union_retain(cu);
scr_union_release(cu);
CHECK(scr_cyc_hdr(cu)->buffered);
CHECK(scr_cyc_is_buffered(scr_cyc_hdr(cu)));
scr_union_release(cu); /* dies while buffered: leaves the buffer, frees */
CHECK(scr_closure_live_count() == 0);
+16 -16
View File
@@ -63,7 +63,7 @@ static Node *make_ring(size_t count) {
Node **nodes = calloc(count, sizeof(*nodes));
if (!nodes) scr_trap("cycle test: out of memory\n");
for (size_t i = 0; i < count; i++) {
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
nodes[i] = scr_cyc_alloc(sizeof(*nodes[i]), &cyc_type);
nodes[i]->rc = 1; /* the ring edge that will point at this node */
}
@@ -119,7 +119,7 @@ static void check_age_reset_when_last_mature_root_dies(void) {
ring = make_ring(32);
ring->rc++;
release_live(ring);
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
leaf = scr_cyc_alloc(sizeof(*leaf), &cyc_type);
leaf->rc = 1;
leaf->rc++; /* temporary release makes it a nursery candidate */
@@ -155,7 +155,7 @@ static void check_dead_backlog_rearms_release_trigger(void) {
Node *leaves[BACKLOG];
for (size_t i = 0; i < BACKLOG; i++) {
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
leaves[i] = scr_cyc_alloc(sizeof(*leaves[i]), &cyc_type);
leaves[i]->rc = 2; /* external owner plus a temporary reference */
release_live(leaves[i]);
@@ -183,7 +183,7 @@ static void check_dead_backlog_rearms_release_trigger(void) {
}
static Node *make_leaf(void) {
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
Node *leaf = scr_cyc_alloc(sizeof(*leaf), &cyc_type);
leaf->rc = 1;
return leaf;
@@ -267,7 +267,7 @@ static void age_to_old(Node *node) {
read_live(node);
scr_collect_cycles();
}
check(scr_cyc_hdr(node)->gen == SCR_CYC_OLD, "survivor did not reach the old generation");
check(scr_cyc_gen(scr_cyc_hdr(node)) == SCR_CYC_OLD, "survivor did not reach the old generation");
}
/* Fresh live leaves, each released once so the release path keeps running
@@ -328,7 +328,7 @@ static void check_older_rings_into_old(void) {
}
scr_cyc_collect_scheduled(); /* every ring survives at least one pass */
for (size_t i = 0; i < RINGS; i++) {
check(scr_cyc_hdr(rings[i])->gen != SCR_CYC_NURSERY, "ring was not promoted");
check(scr_cyc_gen(scr_cyc_hdr(rings[i])) != SCR_CYC_NURSERY, "ring was not promoted");
release_live(rings[i]);
}
size_t grown = grow_until(keep, LIMIT, before + RINGS * RING);
@@ -435,8 +435,8 @@ static size_t grow_dead_cycle_until_full_pass(size_t old_garbage, bool mixed) {
age_to_old(tie);
garbage = make_ring(old_garbage);
read_live(garbage); /* a release-triggered pass may promote it at once */
if (scr_cyc_hdr(garbage)->gen == SCR_CYC_NURSERY) scr_collect_cycles();
check(scr_cyc_hdr(garbage)->gen == SCR_CYC_MATURE, "survivor did not reach the mature generation");
if (scr_cyc_gen(scr_cyc_hdr(garbage)) == SCR_CYC_NURSERY) scr_collect_cycles();
check(scr_cyc_gen(scr_cyc_hdr(garbage)) == SCR_CYC_MATURE, "survivor did not reach the mature generation");
tie->other = garbage;
garbage->rc++;
garbage->other = tie;
@@ -648,7 +648,7 @@ static void check_deep_cross_generation_chain(void) {
size_t before = freed;
Node *chain = NULL;
for (size_t i = 0; i < DEPTH; i++) {
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
Node *node = scr_cyc_alloc(sizeof(*node), &cyc_type);
node->rc = 1;
node->next = chain;
@@ -719,7 +719,7 @@ static void bypass_free(void *obj) {
bypass_freed++;
node_free(obj);
}
static const ScrCycType bypass_type = { &bypass_trace, &bypass_free, 7 };
static ScrCycType bypass_type = { &bypass_trace, &bypass_free, 7 };
static Node *make_bypass_node(void) {
Node *node = scr_cyc_alloc(sizeof(*node), &bypass_type);
@@ -818,7 +818,7 @@ static void mature_kind_free(void *obj) {
mature_kind_freed++;
node_free(obj);
}
static const ScrCycType mature_kind_type = { &node_trace, &mature_kind_free, 8 };
static ScrCycType mature_kind_type = { &node_trace, &mature_kind_free, 8 };
static Node *make_mature_kind_node(void) {
Node *node = scr_cyc_alloc(sizeof(*node), &mature_kind_type);
@@ -834,7 +834,7 @@ static size_t probe_old_on_release(Node **owned, size_t probe) {
for (size_t i = 0; i < probe; i++) {
owned[i] = make_mature_kind_node();
read_live(owned[i]);
old += scr_cyc_hdr(owned[i])->gen == SCR_CYC_OLD;
old += scr_cyc_gen(scr_cyc_hdr(owned[i])) == SCR_CYC_OLD;
}
return old;
}
@@ -855,7 +855,7 @@ static void check_mature_bypass(void) {
* at that level: none dies. */
scr_collect_cycles();
for (size_t i = 0; i < nlive; i++) {
scr_cyc_hdr(live[i])->gen = SCR_CYC_MATURE;
scr_cyc_set_gen(scr_cyc_hdr(live[i]), SCR_CYC_MATURE);
read_live(live[i]);
}
scr_cyc_pass_at(SCR_CYC_MATURE);
@@ -924,7 +924,7 @@ static void paired_free(void *obj) {
paired_freed++;
node_free(obj);
}
static const ScrCycType paired_type = { &paired_trace, &paired_free, 10 };
static ScrCycType paired_type = { &paired_trace, &paired_free, 10 };
static void check_probe_sees_candidate_rings(void) {
/* Both members of a ring must be buffered before a pass walks either: with
@@ -978,7 +978,7 @@ static void check_probe_sees_candidate_rings(void) {
/* A nursery pass re-buffers its survivors past the mature level when
* their type bypasses it, even if the type's nursery candidates are
* walked (here half of them die young, in rings). */
static const ScrCycType young_dying_type = { &node_trace, &node_free, 9 };
static ScrCycType young_dying_type = { &node_trace, &node_free, 9 };
static void check_rebuffer_past_mature(void) {
enum { BATCH = 1024, ROUNDS = 24, PROBE = 1024 };
@@ -1011,7 +1011,7 @@ static void check_rebuffer_past_mature(void) {
}
scr_cyc_pass_at(SCR_CYC_NURSERY);
size_t old = 0;
for (size_t i = nlive - PROBE; i < nlive; i++) old += scr_cyc_hdr(live[i])->gen == SCR_CYC_OLD;
for (size_t i = nlive - PROBE; i < nlive; i++) old += scr_cyc_gen(scr_cyc_hdr(live[i])) == SCR_CYC_OLD;
check(old >= PROBE / 2, "nursery survivors were re-buffered into a level their type bypasses");
for (size_t i = 0; i < nlive; i++) {
live[i]->rc = 0;
+162
View File
@@ -0,0 +1,162 @@
/* Oracle test for path.matchesGlob.
* Reads the case file given as argv[1] (line format in gen-glob-cases.mjs:
* curated paths, per-pattern answer rows over them, and fuzz cases) and
* runs every case for both path families with case-sensitive and
* case-insensitive matching, each answer "1", "0", or "E" for a thrown
* error.
*
* Exit 0 = all pass; prints each mismatch (capped) and exits 1 otherwise.
*/
#include "../src/scr_runtime.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
long scr_str_live_count(void); /* provided by scr_string.c */
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
/* An escaped case string (printable ASCII, \uXXXX code units) to a string
* in the runtime's WTF-8 storage. */
static ScrStr *decode(const char *text) {
size_t len = strlen(text);
uint16_t *units = malloc((len ? len : 1) * sizeof *units);
size_t n = 0;
for (size_t i = 0; i < len; i++) {
if (text[i] == '\\' && text[i + 1] == 'u') {
const char *h = text + i + 2;
units[n++] = (uint16_t)(hex_val(h[0]) << 12 | hex_val(h[1]) << 8 | hex_val(h[2]) << 4 | hex_val(h[3]));
i += 5;
} else {
units[n++] = (uint8_t)text[i];
}
}
char *out = malloc(3 * n + 1);
size_t w = 0;
bool lone = false;
for (size_t i = 0; i < n; i++) {
uint32_t cp = units[i];
if (cp >= 0xD800 && cp < 0xDC00 && i + 1 < n && units[i + 1] >= 0xDC00 && units[i + 1] < 0xE000) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (units[i + 1] - 0xDC00);
i++;
} else if (cp >= 0xD800 && cp < 0xE000) {
lone = true;
}
if (cp < 0x80) {
out[w++] = (char)cp;
} else if (cp < 0x800) {
out[w++] = (char)(0xC0 | (cp >> 6));
out[w++] = (char)(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
out[w++] = (char)(0xE0 | (cp >> 12));
out[w++] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[w++] = (char)(0x80 | (cp & 0x3F));
} else {
out[w++] = (char)(0xF0 | (cp >> 18));
out[w++] = (char)(0x80 | ((cp >> 12) & 0x3F));
out[w++] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[w++] = (char)(0x80 | (cp & 0x3F));
}
}
if (lone) scr_wtf8_note();
ScrStr *s = scr_str_new(out, w);
free(out);
free(units);
return s;
}
/* Splits a line at tabs in place; empty fields are kept. */
static int split(char *line, char **fields, int max) {
int count = 0;
fields[count++] = line;
for (char *c = line; *c; c++)
if (*c == '\t') {
*c = 0;
if (count == max) return -1;
fields[count++] = c + 1;
}
return count;
}
static long total, failed;
/* Checks both families and both case modes against two answer strings. */
static void check(ScrStr *path, ScrStr *pattern, const char *posix, const char *win32, const char *path_text,
const char *pattern_text) {
for (int windows = 0; windows < 2; windows++) {
const char *expected = windows ? win32 : posix;
for (int nocase = 0; nocase < 2; nocase++) {
bool hit = scr_glob_match(path, pattern, windows, nocase);
char got = scr_exc_pending() ? 'E' : hit ? '1' : '0';
if (scr_exc_pending()) scr_exc_clear();
total++;
if (got != expected[nocase]) {
failed++;
if (failed <= 40)
fprintf(stderr, "MISMATCH %s nocase=%d path=\"%s\" pattern=\"%s\" got %c want %c\n",
windows ? "win32" : "posix", nocase, path_text, pattern_text, got, expected[nocase]);
}
}
}
}
int main(int argc, char **argv) {
if (argc < 2) return 2;
FILE *f = fopen(argv[1], "r");
if (!f) return 2;
static char line[1 << 20];
size_t npaths = 0, cap = 0;
ScrStr **paths = NULL;
char **path_texts = NULL;
while (fgets(line, sizeof line, f)) {
size_t len = strlen(line);
if (len && line[len - 1] == '\n') line[--len] = 0;
if (!len) continue;
char *fields[5];
int count = split(line, fields, 5);
if (strcmp(fields[0], "p") == 0 && count == 2) {
if (npaths == cap) {
cap = cap ? 2 * cap : 64;
paths = realloc(paths, cap * sizeof *paths);
path_texts = realloc(path_texts, cap * sizeof *path_texts);
}
path_texts[npaths] = strdup(fields[1]);
paths[npaths++] = decode(fields[1]);
} else if (strcmp(fields[0], "c") == 0 && count == 4 && strlen(fields[2]) == 2 * npaths &&
strlen(fields[3]) == 2 * npaths) {
ScrStr *pattern = decode(fields[1]);
for (size_t i = 0; i < npaths; i++)
check(paths[i], pattern, fields[2] + 2 * i, fields[3] + 2 * i, path_texts[i], fields[1]);
scr_str_release(pattern);
} else if (strcmp(fields[0], "f") == 0 && count == 5 && strlen(fields[3]) == 2 && strlen(fields[4]) == 2) {
ScrStr *path = decode(fields[1]);
ScrStr *pattern = decode(fields[2]);
check(path, pattern, fields[3], fields[4], fields[1], fields[2]);
scr_str_release(path);
scr_str_release(pattern);
} else {
fprintf(stderr, "bad line\n");
return 2;
}
}
fclose(f);
for (size_t i = 0; i < npaths; i++) {
scr_str_release(paths[i]);
free(path_texts[i]);
}
free(paths);
free(path_texts);
#ifdef SCR_RC_AUDIT
if (scr_str_live_count() != 0) {
fprintf(stderr, "live strings at exit: %ld\n", scr_str_live_count());
failed++;
}
#endif
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
return failed == 0 ? 0 : 1;
}
+54
View File
@@ -0,0 +1,54 @@
// Number and boolean records that only flow into field reads keep their
// values; records that are stored, returned, captured, compared by
// identity or printed stay observable exactly like Node.
interface Point {
x: number;
y: number;
}
interface Flagged {
on: boolean;
weight: number;
}
function length2(p: Point): number {
return p.x * p.x + p.y * p.y;
}
function dot(a: Point, b: Point): number {
return a.x * b.x + a.y * b.y;
}
function weigh(f: Flagged): number {
return f.on ? f.weight : -f.weight;
}
const stored: Point[] = [];
function keep(p: Point): number {
stored.push(p);
return p.x;
}
function same(p: Point): Point {
return p;
}
function depth(p: Point, n: number): number {
return n === 0 ? length2(p) : depth({ x: p.x + 1, y: p.y - 1 }, n - 1) + p.x;
}
let total = 0;
const readers: (() => number)[] = [];
for (let i = 0; i < 6; i++) {
const p = { x: i, y: i * 2 };
const q: Point = { x: 1, y: -1 };
total += length2(p) + dot(p, q) + dot({ x: 3, y: 4 }, { x: i, y: 1 });
total += weigh({ on: i % 2 === 0, weight: i });
if (i % 3 === 0) total += keep(p);
const r = { x: i, y: 0 };
readers.push(() => r.x * 10);
const s = same({ x: i, y: i });
stored.push(s);
const a = { x: 1, y: 2 };
const b = { x: 1, y: 2 };
if (a === b) total += 1000;
total += depth({ x: i, y: 5 }, 4);
}
console.log(total);
console.log(stored.map((p) => `${p.x}:${p.y}`).join(" "));
console.log(readers.map((read) => read()).join(" "));
const shown = { x: 7, y: 8 };
console.log(shown, JSON.stringify({ x: 1, y: 2 }));
+86
View File
@@ -0,0 +1,86 @@
// Instances of small classes with number fields that are only read keep
// their values; constructors that register `this`, methods returning
// `this`, subclasses and throwing constructors behave as in Node.
class Vec {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
dot(o: Vec): number {
return this.x * o.x + this.y * o.y;
}
self(): Vec {
return this;
}
}
const registry: Tracked[] = [];
class Tracked {
id: number;
constructor(id: number) {
this.id = id;
registry.push(this);
}
twice(): number {
return this.id * 2;
}
}
class Base {
v: number;
constructor(v: number) {
this.v = v;
}
get(): number {
return this.v;
}
}
class Derived extends Base {
get(): number {
return this.v * 100;
}
}
class Checked {
n: number;
half: number;
constructor(n: number) {
this.n = n;
if (n < 0) throw new RangeError(`negative ${n}`);
this.half = n / 2;
}
sum(): number {
return this.n + this.half;
}
}
class Defaults {
a = 1;
b = 2;
c: number;
constructor(c: number) {
this.c = c + this.a + this.b;
}
total(): number {
return this.a + this.b + this.c;
}
}
let total = 0;
const selves: Vec[] = [];
for (let i = 0; i < 5; i++) {
const v = new Vec(i, i + 1);
total += v.dot(new Vec(2, 3)) + new Vec(i, i).dot(v);
const t = new Tracked(i);
total += t.twice();
selves.push(new Vec(i, -i).self());
const b: Base = i % 2 === 0 ? new Base(i) : new Derived(i);
total += b.get();
try {
const c = new Checked(i - 2);
total += c.sum();
} catch (error) {
console.log("caught", (error as Error).message);
}
total += new Defaults(i).total();
}
console.log(total);
console.log(registry.map((t) => t.id).join(","), selves.map((v) => `${v.x}/${v.y}`).join(","));
+57
View File
@@ -0,0 +1,57 @@
// Functions that return fresh number records or objects on every path,
// including early returns in loops and conditional results, give the same
// values whether their result is only read or kept.
interface Pair {
a: number;
b: number;
}
class Money {
cents: number;
constructor(cents: number) {
this.cents = cents;
}
plus(o: Money): Money {
return new Money(this.cents + o.cents);
}
scaled(k: number): Money {
return new Money(Math.round(this.cents * k));
}
}
function pair(a: number, b: number): Pair {
return { a, b };
}
function swap(p: Pair): Pair {
return { a: p.b, b: p.a };
}
function pick(n: number): Pair {
if (n > 4) return { a: n, b: -n };
for (let i = 0; i < 3; i++) if (i === n) return { a: i, b: i * i };
return n < 0 ? { a: 0, b: 0 } : { a: 1, b: 1 };
}
function risky(n: number): Pair {
if (n === 2) throw new Error(`risky ${n}`);
return { a: n, b: n + 0.5 };
}
function sum(p: Pair): number {
return p.a + p.b;
}
function countdown(n: number): Pair {
return n === 0 ? { a: 0, b: 0 } : { a: sum(countdown(n - 1)) + n, b: n };
}
const kept: Pair[] = [];
let total = 0;
for (let i = -1; i < 7; i++) {
total += sum(swap(pair(i, 2 * i))) + sum(pick(i));
const p = pick(i);
total += p.a * 3 + p.b;
kept.push(pick(i));
try {
total += sum(risky(i));
} catch (error) {
console.log((error as Error).message);
}
const m = new Money(100 + i).plus(new Money(5)).scaled(1.5);
total += m.cents;
total += sum(countdown(i < 0 ? 0 : i));
}
console.log(total, kept.map((p) => `${p.a},${p.b}`).join(" "));

Some files were not shown because too many files have changed in this diff Show More