mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge main into EventTarget
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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));
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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,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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5120-frame-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5121-frame-objects.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5122-frame-result-slots.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5123-frame-reassigned.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5124-frame-objects-control.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5125-frame-mutating-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5126-frame-returned-params.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5127-frame-loop-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5145-path-matches-glob-basics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5146-path-matches-glob-braces.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5147-path-matches-glob-extglob.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5148-path-matches-glob-classes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5149-path-matches-glob-win32.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5155-cycle-header-types.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5160-field-callback-receiver.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5161-field-callback-receiver-escape.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5162-field-method-reference-receiver.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5163-field-callback-receiver.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5164-worker-field-callback-receiver.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5200-array-sort-keyed-numbers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5201-array-sort-keyed-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 }));
|
||||
@@ -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(","));
|
||||
@@ -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
Reference in New Issue
Block a user