From ebce6f83d1cd2c9606986b44d6ae749c83331641 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 01:34:53 -0500 Subject: [PATCH 1/9] Box function-field receivers only when the callee reads this --- packages/compiler/src/backend/llvm/emitter.ts | 2 + .../compiler/src/backend/llvm/expr-calls.ts | 42 ++++++++++- .../5160-field-callback-receiver.ts.json | 6 ++ ...161-field-callback-receiver-escape.ts.json | 6 ++ ...62-field-method-reference-receiver.ts.json | 6 ++ .../5163-field-callback-receiver.cjs.json | 6 ++ ...164-worker-field-callback-receiver.ts.json | 6 ++ packages/runtime/src/scr_json.c | 25 +++++++ packages/runtime/src/scr_runtime.h | 14 ++++ tests/corpus/5160-field-callback-receiver.ts | 73 +++++++++++++++++++ .../5161-field-callback-receiver-escape.ts | 40 ++++++++++ .../5162-field-method-reference-receiver.ts | 44 +++++++++++ tests/corpus/5163-field-callback-receiver.cjs | 24 ++++++ .../5164-worker-field-callback-receiver.ts | 44 +++++++++++ 14 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5160-field-callback-receiver.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5161-field-callback-receiver-escape.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5162-field-method-reference-receiver.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5163-field-callback-receiver.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5164-worker-field-callback-receiver.ts.json create mode 100644 tests/corpus/5160-field-callback-receiver.ts create mode 100644 tests/corpus/5161-field-callback-receiver-escape.ts create mode 100644 tests/corpus/5162-field-method-reference-receiver.ts create mode 100644 tests/corpus/5163-field-callback-receiver.cjs create mode 100644 tests/corpus/5164-worker-field-callback-receiver.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0e948a2d7..288ac8818 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -734,6 +734,8 @@ export class LlEmitter { * typeKey → thunk symbol. */ readonly resolveThunks = new Map(); readonly resolveThunkDefs: string[] = []; + /** Typed-ref recipe constants by type key (lazy receivers). */ + readonly typedRefRecipes = new Map(); /** ReadableStream.from adapters keep typed arrays by reference and box * one current element per pull. */ readonly streamFromArrayAdapters = new Map(); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b38ae0bb1..d1d86eb57 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -2,13 +2,16 @@ 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 { typedRefConstructor, vAdapters } from "./shapes.js"; import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js"; import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js"; import { @@ -723,7 +726,41 @@ 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; + 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 } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind} }`, + ); + } + 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 +836,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( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5160-field-callback-receiver.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5160-field-callback-receiver.ts.json new file mode 100644 index 000000000..f02de83d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5160-field-callback-receiver.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5160-field-callback-receiver.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5161-field-callback-receiver-escape.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5161-field-callback-receiver-escape.ts.json new file mode 100644 index 000000000..230182027 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5161-field-callback-receiver-escape.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5161-field-callback-receiver-escape.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5162-field-method-reference-receiver.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5162-field-method-reference-receiver.ts.json new file mode 100644 index 000000000..0cafc4bec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5162-field-method-reference-receiver.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5162-field-method-reference-receiver.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5163-field-callback-receiver.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5163-field-callback-receiver.cjs.json new file mode 100644 index 000000000..c8d6761c3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5163-field-callback-receiver.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5163-field-callback-receiver.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5164-worker-field-callback-receiver.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5164-worker-field-callback-receiver.ts.json new file mode 100644 index 000000000..ad129696d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5164-worker-field-callback-receiver.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5164-worker-field-callback-receiver.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3d81e84f9..d3b1546f3 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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,17 @@ 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 (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. */ diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index fcd787655..669758c01 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -5503,6 +5503,20 @@ 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). */ +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; +} 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 diff --git a/tests/corpus/5160-field-callback-receiver.ts b/tests/corpus/5160-field-callback-receiver.ts new file mode 100644 index 000000000..02570b9d8 --- /dev/null +++ b/tests/corpus/5160-field-callback-receiver.ts @@ -0,0 +1,73 @@ +// Function-typed class fields called as methods (`h.fn(x)`): JS binds the +// instance as `this`. Function expressions read it, store it, compare it +// with the instance and with itself, and mutate the instance through it; +// arrow fields keep their lexical `this`. Each call gets its own binding, +// nested field calls bind their own instances, and a callee that throws +// still leaves the outer binding in place. +class Counter { + count = 0; + label: string; + step: (this: Counter, by: number) => number; + describe: (this: Counter) => string; + arrow: (x: number) => number; + inner: Counter | null = null; + constructor(label: string) { + this.label = label; + this.step = function (this: Counter, by: number): number { + this.count += by; + return this.count; + }; + this.describe = function (this: Counter): string { + return `${this.label}=${this.count}`; + }; + this.arrow = (x: number) => x * 2; + } +} + +const a = new Counter("a"); +const b = new Counter("b"); +console.log(a.step(2), a.step(3), b.step(10), a.describe(), b.describe()); +console.log(a.arrow(21), b.arrow(1)); + +// The bound receiver is the instance itself, and stays one value within a call. +let seen: Counter | null = null; +let same = false; +a.describe = function (this: Counter): string { + seen = this; + same = this === this; + return `${this.label}:${this === a}:${this === b}`; +}; +console.log(a.describe(), seen === a, same); + +// Nested calls: the callee calls another instance's field. +a.inner = b; +a.step = function (this: Counter, by: number): number { + const before = this.inner === null ? -1 : this.inner.step(by * 100); + this.count += by; + return before + this.count; +}; +console.log(a.step(1), a.describe(), b.describe()); + +// A throwing callee: the receiver of the surrounding call is unaffected. +const thrower = new Counter("thrower"); +thrower.step = function (this: Counter, by: number): number { + if (by > 0) throw new Error(`${this.label} refused ${by}`); + return 0; +}; +b.describe = function (this: Counter): string { + let message = ""; + try { + thrower.step(5); + } catch (error) { + message = (error as Error).message; + } + return `${this.label} after ${message}`; +}; +console.log(b.describe()); + +// Many calls in a loop, alternating receivers. +let total = 0; +const pair = [new Counter("x"), new Counter("y")]; +pair[0]!.step = pair[1]!.step; +for (let i = 0; i < 1000; i++) total += pair[i % 2]!.step(1); +console.log(total, pair[0]!.count, pair[1]!.count); diff --git a/tests/corpus/5161-field-callback-receiver-escape.ts b/tests/corpus/5161-field-callback-receiver-escape.ts new file mode 100644 index 000000000..615eaf442 --- /dev/null +++ b/tests/corpus/5161-field-callback-receiver-escape.ts @@ -0,0 +1,40 @@ +// A function-typed field's `this` escaping its call: stored in arrays and +// closures, returned, and compared after the call; the escaped binding +// keeps the instance alive after every other reference is dropped. Calls +// whose callee never reads `this` (arrow fields, declared functions +// stored in fields) leave no trace. +class Node { + name: string; + keep: (this: Node) => Node; + later: (this: Node) => () => string; + plain: (n: number) => number; + constructor(name: string) { + this.name = name; + this.keep = function (this: Node): Node { + return this; + }; + this.later = function (this: Node): () => string { + const self = this; + return () => `later ${self.name}`; + }; + this.plain = double; + } +} + +function double(n: number): number { + return n * 2; +} + +const kept: Node[] = []; +let n: Node | null = new Node("first"); +kept.push(n.keep()); +const report = n.later(); +console.log(kept[0] === n, kept[0]!.name, report()); +n = null; +console.log(kept[0]!.name, report()); + +const fresh = new Node("fresh"); +console.log(fresh.plain(21), fresh.keep().keep() === fresh); +const callbacks: (() => string)[] = []; +for (const name of ["p", "q", "r"]) callbacks.push(new Node(name).later()); +console.log(callbacks.map((cb) => cb()).join(",")); diff --git a/tests/corpus/5162-field-method-reference-receiver.ts b/tests/corpus/5162-field-method-reference-receiver.ts new file mode 100644 index 000000000..3e70a7738 --- /dev/null +++ b/tests/corpus/5162-field-method-reference-receiver.ts @@ -0,0 +1,44 @@ +// Class methods stored in function-typed fields and called through +// another instance: the call binds the instance it is read from, so a +// method reference moved between objects runs against its new receiver, +// including subclass instances and methods inherited from a base class. +class Account { + owner: string; + balance: number; + action: (amount: number) => string; + constructor(owner: string, balance: number) { + this.owner = owner; + this.balance = balance; + this.action = this.deposit; + } + deposit(amount: number): string { + this.balance += amount; + return `${this.owner} has ${this.balance}`; + } + withdraw(amount: number): string { + this.balance -= amount; + return `${this.owner} has ${this.balance}`; + } +} + +class Savings extends Account { + rate = 2; + override deposit(amount: number): string { + this.balance += amount * this.rate; + return `${this.owner} saved ${this.balance}`; + } +} + +const alice = new Account("alice", 10); +const bob = new Account("bob", 100); +const carol = new Savings("carol", 0); +console.log(alice.action(5), bob.action(1), carol.action(7)); +bob.action = alice.withdraw; +console.log(bob.action(30), alice.balance, bob.balance); +const dave = new Savings("dave", 1); +dave.action = carol.deposit; +console.log(dave.action(1), carol.balance, dave.balance); +carol.action = alice.withdraw; +console.log(carol.action(3), carol.balance); +const accounts: Account[] = [alice, bob, carol, dave]; +for (const account of accounts) console.log(account.action(2)); diff --git a/tests/corpus/5163-field-callback-receiver.cjs b/tests/corpus/5163-field-callback-receiver.cjs new file mode 100644 index 000000000..0ae11c6c6 --- /dev/null +++ b/tests/corpus/5163-field-callback-receiver.cjs @@ -0,0 +1,24 @@ +// Function-valued properties of class instances in a JavaScript program: +// methods assigned in the constructor, replaced later, and called through +// the instance read `this` for the instance they were called on. +'use strict'; +class Greeter { + constructor(name) { + this.name = name; + this.greet = function (other) { + return `${this.name} greets ${other}`; + }; + this.shout = (other) => `${other.toUpperCase()}!`; + } +} +const g = new Greeter('ann'); +const h = new Greeter('ben'); +console.log(g.greet('you'), h.greet('me'), g.shout('hey')); +h.greet = g.greet; +console.log(h.greet('again')); +let calls = 0; +g.greet = function (other) { + calls++; + return `${this === g} ${this === h} ${other}`; +}; +console.log(g.greet('x'), calls); diff --git a/tests/corpus/5164-worker-field-callback-receiver.ts b/tests/corpus/5164-worker-field-callback-receiver.ts new file mode 100644 index 000000000..e426a03a2 --- /dev/null +++ b/tests/corpus/5164-worker-field-callback-receiver.ts @@ -0,0 +1,44 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Function-typed fields called as methods inside worker threads: each +// thread binds its own instances as `this` while the main thread does the +// same, and the results come back over the message port. +class Tally { + total = 0; + name: string; + add: (this: Tally, n: number) => number; + constructor(name: string) { + this.name = name; + this.add = function (this: Tally, n: number): number { + this.total += n; + return this.total; + }; + } +} + +function work(seed: number): string { + const tallies = [new Tally(`a${seed}`), new Tally(`b${seed}`)]; + for (let i = 0; i < 2000; i++) tallies[i % 2]!.add(i + seed); + return tallies.map((t) => `${t.name}:${t.total}`).join(" "); +} + +if (isMainThread) { + const results = await Promise.all( + [1, 2, 3].map( + (k) => + new Promise((resolve, reject) => { + let result = ""; + const worker = new Worker(new URL(import.meta.url), { workerData: k }); + worker.once("message", (value: unknown) => { + result = value as string; + }); + worker.once("error", reject); + worker.once("exit", () => resolve(result)); + }), + ), + ); + console.log(work(0)); + for (const line of results) console.log(line); +} else { + parentPort!.postMessage(work(workerData as number)); +} From 7bc8a191f029bb247b91b97faacf96ae3398741a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 02:44:11 -0500 Subject: [PATCH 2/9] Keep non-escaping number records and objects in frame storage --- benchmarks/runtime/README.md | 1 + benchmarks/runtime/workloads.json | 7 + benchmarks/runtime/workloads/value-objects.ts | 132 +++++++++++ packages/compiler/src/backend/llvm/emitter.ts | 61 ++++- .../compiler/src/backend/llvm/expr-calls.ts | 127 +++++----- .../src/backend/llvm/expr-primitives.ts | 52 ++-- .../src/backend/llvm/record-results.ts | 206 ++++++++++++++++ .../src/backend/llvm/stack-objects.test.ts | 223 ++++++++++++++++++ .../src/backend/llvm/stack-objects.ts | 160 +++++++++++++ .../tests/corpus/5120-frame-records.ts.json | 6 + .../tests/corpus/5121-frame-objects.ts.json | 6 + .../corpus/5122-frame-result-slots.ts.json | 6 + .../corpus/5123-frame-reassigned.ts.json | 6 + .../corpus/5124-frame-objects-control.ts.json | 6 + .../corpus/5127-frame-loop-bindings.ts.json | 6 + tests/corpus/5120-frame-records.ts | 54 +++++ tests/corpus/5121-frame-objects.ts | 86 +++++++ tests/corpus/5122-frame-result-slots.ts | 57 +++++ tests/corpus/5123-frame-reassigned.ts | 72 ++++++ tests/corpus/5124-frame-objects-control.ts | 46 ++++ tests/corpus/5127-frame-loop-bindings.ts | 61 +++++ 21 files changed, 1306 insertions(+), 75 deletions(-) create mode 100644 benchmarks/runtime/workloads/value-objects.ts create mode 100644 packages/compiler/src/backend/llvm/record-results.ts create mode 100644 packages/compiler/src/backend/llvm/stack-objects.test.ts create mode 100644 packages/compiler/src/backend/llvm/stack-objects.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5120-frame-records.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5121-frame-objects.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5122-frame-result-slots.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5123-frame-reassigned.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5124-frame-objects-control.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5127-frame-loop-bindings.ts.json create mode 100644 tests/corpus/5120-frame-records.ts create mode 100644 tests/corpus/5121-frame-objects.ts create mode 100644 tests/corpus/5122-frame-result-slots.ts create mode 100644 tests/corpus/5123-frame-reassigned.ts create mode 100644 tests/corpus/5124-frame-objects-control.ts create mode 100644 tests/corpus/5127-frame-loop-bindings.ts diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 9dea0a3ea..44f67b28c 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -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 | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index 5e7ebfce9..8d8c183f0 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -70,6 +70,13 @@ "0.5" ] }, + { + "name": "value-objects", + "entry": "workloads/value-objects.ts", + "args": [ + "2" + ] + }, { "name": "validate-errors", "entry": "workloads/validate-errors.ts", diff --git a/benchmarks/runtime/workloads/value-objects.ts b/benchmarks/runtime/workloads/value-objects.ts new file mode 100644 index 000000000..f917e5eb1 --- /dev/null +++ b/benchmarks/runtime/workloads/value-objects.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)); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0e948a2d7..4c2be9440 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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(); readonly stringSlices = new Map(); private localStackUnions = new Map(); + private localStackObjects = new Map(); + private reassignedStackObjects = new Set(); + /** The binding slot of each reassigned stack-object local. */ + private stackObjectSlots = new Map(); private localUnionStorageProofs = new Map(); private localUnionStorage = new Map(); integerArrayBindings = new Set(); @@ -529,6 +545,8 @@ 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; @@ -931,6 +949,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. @@ -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); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b38ae0bb1..13ff63ad2 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -10,6 +10,7 @@ import { type IrFunction, } from "../../ir/ir.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 +63,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 +129,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 +137,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. @@ -913,60 +919,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 +1192,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(); +} diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 180e8b2f0..00b22200d 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -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); + } +} diff --git a/packages/compiler/src/backend/llvm/record-results.ts b/packages/compiler/src/backend/llvm/record-results.ts new file mode 100644 index 000000000..d5de03920 --- /dev/null +++ b/packages/compiler/src/backend/llvm/record-results.ts @@ -0,0 +1,206 @@ +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 { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js"; + +/** Prefix of a function's result-slot entry: `%into.(...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(); + 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. 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), + ); + const fillable = (value: IrExpr): 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) && fillable(value.else_); + 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 "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 added: IrFunction[] = []; + for (const fn of mod.functions) { + if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures !== undefined) + continue; + const ret = fn.returnType; + if ( + !(ret.kind === "record" && scalarRecordShape(shapes.get(ret.shapeId))) && + !(ret.kind === "object" && stackClass(ret.className)) + ) + continue; + let returns = 0; + const eligible = 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); + }, + }); + if (!eligible || returns === 0) continue; + 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] }; +} diff --git a/packages/compiler/src/backend/llvm/stack-objects.test.ts b/packages/compiler/src/backend/llvm/stack-objects.test.ts new file mode 100644 index 000000000..9dfb90a84 --- /dev/null +++ b/packages/compiler/src/backend/llvm/stack-objects.test.ts @@ -0,0 +1,223 @@ +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", 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)); + expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}fresh`]); +}); + +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/); +}); diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts new file mode 100644 index 000000000..f2e570470 --- /dev/null +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -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 { + const result = new Map(); + 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 { + const result = new Set(); + 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(); + 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 }; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5120-frame-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5120-frame-records.ts.json new file mode 100644 index 000000000..e549e0083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5120-frame-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5120-frame-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5121-frame-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5121-frame-objects.ts.json new file mode 100644 index 000000000..9966443f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5121-frame-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5121-frame-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5122-frame-result-slots.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5122-frame-result-slots.ts.json new file mode 100644 index 000000000..16835f9ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5122-frame-result-slots.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5122-frame-result-slots.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5123-frame-reassigned.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5123-frame-reassigned.ts.json new file mode 100644 index 000000000..9bbcbc2b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5123-frame-reassigned.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5123-frame-reassigned.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5124-frame-objects-control.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5124-frame-objects-control.ts.json new file mode 100644 index 000000000..6328b38d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5124-frame-objects-control.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5124-frame-objects-control.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5127-frame-loop-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5127-frame-loop-bindings.ts.json new file mode 100644 index 000000000..de8dd0338 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5127-frame-loop-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5127-frame-loop-bindings.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5120-frame-records.ts b/tests/corpus/5120-frame-records.ts new file mode 100644 index 000000000..c8d3277be --- /dev/null +++ b/tests/corpus/5120-frame-records.ts @@ -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 })); diff --git a/tests/corpus/5121-frame-objects.ts b/tests/corpus/5121-frame-objects.ts new file mode 100644 index 000000000..283ee6747 --- /dev/null +++ b/tests/corpus/5121-frame-objects.ts @@ -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(",")); diff --git a/tests/corpus/5122-frame-result-slots.ts b/tests/corpus/5122-frame-result-slots.ts new file mode 100644 index 000000000..e9db7636b --- /dev/null +++ b/tests/corpus/5122-frame-result-slots.ts @@ -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(" ")); diff --git a/tests/corpus/5123-frame-reassigned.ts b/tests/corpus/5123-frame-reassigned.ts new file mode 100644 index 000000000..afc229b79 --- /dev/null +++ b/tests/corpus/5123-frame-reassigned.ts @@ -0,0 +1,72 @@ +// Reassigned bindings of number records and objects: each new value may +// be computed from the old one, aliases keep the object they were given, +// and an assignment that throws leaves the previous value in place. +class Complex { + re: number; + im: number; + constructor(re: number, im: number) { + this.re = re; + this.im = im; + } + times(o: Complex): Complex { + return new Complex(this.re * o.re - this.im * o.im, this.re * o.im + this.im * o.re); + } + plus(o: Complex): Complex { + return new Complex(this.re + o.re, this.im + o.im); + } + swapped(): Complex { + return new Complex(this.im, this.re); + } +} +interface P { + x: number; + y: number; +} +function rotate(p: P): P { + return { x: -p.y, y: p.x }; +} +function fail(p: P, n: number): P { + if (n === 2) throw new Error("fail"); + return { x: p.x + n, y: p.y }; +} + +let inside = 0; +for (let y = -4; y <= 4; y++) + for (let x = -8; x <= 4; x++) { + const c = new Complex(x / 4, y / 4); + let z = new Complex(0, 0); + let n = 0; + while (n < 50 && z.re * z.re + z.im * z.im <= 4) { + z = z.times(z).plus(c); + n++; + } + if (n === 50) inside++; + } +console.log("inside", inside); + +let out = ""; +for (let k = 0; k < 3; k++) { + let z = new Complex(k, 1); + let p: P = { x: 1, y: 2 }; + for (let i = 0; i < 4; i++) { + z = z.times(z).plus(new Complex(0.5, -0.25)); + if (i % 2 === 0) z = z.swapped(); + else p = rotate(p); + p = { x: p.y, y: p.x + i }; + try { + p = fail(p, i); + } catch { + out += "!"; + } + } + let q: P = { x: 0, y: 0 }; + if (k === 1) q = rotate(p); + else if (k === 2) q = { x: 9, y: 9 }; + out += `${z.re.toFixed(3)},${z.im.toFixed(3)};${p.x},${p.y};${q.x},${q.y}|`; +} +console.log(out); + +let w = new Complex(1, 2); +const alias = w; +w = new Complex(3, 4); +console.log(alias.re, w.re); diff --git a/tests/corpus/5124-frame-objects-control.ts b/tests/corpus/5124-frame-objects-control.ts new file mode 100644 index 000000000..d92d90f73 --- /dev/null +++ b/tests/corpus/5124-frame-objects-control.ts @@ -0,0 +1,46 @@ +// Number records and objects across try/catch/finally, labeled loops, +// generators and async functions keep the same values and order of +// effects as in Node. +interface Box { + w: number; + h: number; +} +function area(b: Box): number { + return b.w * b.h; +} +function make(w: number, h: number): Box { + return { w, h }; +} +function guarded(n: number): number { + const b = make(n, n + 1); + try { + if (n === 3) throw new Error("three"); + return area(b); + } catch (error) { + return -area({ w: (error as Error).message.length, h: 1 }); + } finally { + console.log("finally", area(b)); + } +} +function* boxes(n: number): Generator { + for (let i = 0; i < n; i++) { + const b = make(i, 2); + yield area(b); + } +} +async function later(n: number): Promise { + const b = make(n, 3); + await null; + return area(b) + area(make(1, 1)); +} + +let total = 0; +outer: for (let i = 0; i < 5; i++) { + for (let j = 0; j < 5; j++) { + const b = { w: i, h: j }; + if (area(b) > 6) continue outer; + total += area(b) + guarded(j); + } +} +console.log(total, [...boxes(4)].join(",")); +later(4).then((value) => console.log("later", value)); diff --git a/tests/corpus/5127-frame-loop-bindings.ts b/tests/corpus/5127-frame-loop-bindings.ts new file mode 100644 index 000000000..dc7180d10 --- /dev/null +++ b/tests/corpus/5127-frame-loop-bindings.ts @@ -0,0 +1,61 @@ +// Loop bindings of number records and objects that the body reassigns: +// for...of over arrays, holey arrays, Sets, Map values and generators, +// destructured bindings, for-loop headers and for...in bodies all read +// the value most recently assigned, and the iterated values are unchanged. +interface P { x: number; y: number } +class C { v: number; constructor(v: number) { this.v = v; } } +function shift(p: P): P { return { x: p.x + 100, y: p.y }; } +function read(p: P): number { return p.x * 10 + p.y; } +const points: P[] = [{ x: 1, y: 2 }, { x: 3, y: 4 }]; +const objects = [new C(1), new C(2)]; +let out = ""; +for (let p of points) { + out += read(p) + " "; + p = { x: p.x + 10, y: 0 }; + out += read(p) + " "; + p = shift(p); + out += read(p) + ";"; +} +for (let c of objects) { + c = new C(c.v * 7); + out += c.v + ";"; +} +for (let { x, y } of points) { + let q: P = { x, y }; + q = shift(q); + out += read(q) + ";"; +} +for (let i = 0, p: P = { x: 0, y: 0 }; i < 3; i++, p = { x: p.x + i, y: i }) out += read(p) + ","; +for (const key in { a: 1, b: 2 }) { + let r: P = { x: key.length, y: 0 }; + r = shift(r); + out += read(r) + ";"; +} +console.log(out, points.map(read).join(" "), objects.map((c) => c.v).join(" ")); +const set = new Set

([{ x: 1, y: 2 }, { x: 3, y: 4 }]); +const map = new Map([["a", new C(1)], ["b", new C(2)]]); +function* gen(): Generator

{ yield { x: 5, y: 6 }; yield { x: 7, y: 8 }; } +let out2 = ""; +for (let p of set) { + out2 += read(p) + " "; + p = { x: p.x + 10, y: 0 }; + out2 += read(p) + " "; + p = shift(p); + out2 += read(p) + ";"; +} +for (let c of map.values()) { + c = new C(c.v * 7); + out2 += c.v + ";"; +} +for (let p of gen()) { + p = shift(p); + out2 += read(p) + ";"; +} +const holey: P[] = [{ x: 1, y: 1 }]; +holey[3] = { x: 2, y: 2 }; +for (let p of holey) { + if (p === undefined) continue; + p = shift(p); + out2 += read(p) + ";"; +} +console.log(out2, [...set].map(read).join(" "), [...map.values()].map((c) => c.v).join(" ")); From e3d338003225495cb4b11d455460f7c77f578f09 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 03:54:52 -0500 Subject: [PATCH 3/9] Add path.matchesGlob --- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 22 +- .../generated/node-v24-internal.json | 20 +- internal/compatibility/static-support.json | 3 + .../ambient/scriptc-node-fallback.d.ts | 5 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../compiler/src/backend/native/executable.ts | 13 +- .../compiler/src/backend/native/library.ts | 2 +- .../src/frontend/lowering/surfaces.ts | 12 + packages/compiler/src/ir/builtin-effects.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/runtime-features.ts | 4 +- packages/compiler/surface-manifest.json | 18 + .../5145-path-matches-glob-basics.ts.json | 6 + .../5146-path-matches-glob-braces.ts.json | 6 + .../5147-path-matches-glob-extglob.ts.json | 6 + .../5148-path-matches-glob-classes.ts.json | 6 + .../5149-path-matches-glob-win32.ts.json | 6 + .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-common/library-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 1 + .../test/library-matrix.test.ts | 1 + packages/runtime/src/scr_glob.c | 2163 +++++++++++ packages/runtime/src/scr_runtime.h | 9 + packages/runtime/test/gen-glob-cases.mjs | 469 +++ packages/runtime/test/glob-cases.txt | 3316 +++++++++++++++++ packages/runtime/test/glob.test.ts | 69 + packages/runtime/test/test_glob.c | 162 + tests/corpus/5145-path-matches-glob-basics.ts | 39 + tests/corpus/5146-path-matches-glob-braces.ts | 25 + .../corpus/5147-path-matches-glob-extglob.ts | 23 + .../corpus/5148-path-matches-glob-classes.ts | 32 + tests/corpus/5149-path-matches-glob-win32.ts | 72 + 34 files changed, 6517 insertions(+), 21 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5145-path-matches-glob-basics.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5146-path-matches-glob-braces.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5147-path-matches-glob-extglob.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5148-path-matches-glob-classes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5149-path-matches-glob-win32.ts.json create mode 100644 packages/runtime/src/scr_glob.c create mode 100644 packages/runtime/test/gen-glob-cases.mjs create mode 100644 packages/runtime/test/glob-cases.txt create mode 100644 packages/runtime/test/glob.test.ts create mode 100644 packages/runtime/test/test_glob.c create mode 100644 tests/corpus/5145-path-matches-glob-basics.ts create mode 100644 tests/corpus/5146-path-matches-glob-braces.ts create mode 100644 tests/corpus/5147-path-matches-glob-extglob.ts create mode 100644 tests/corpus/5148-path-matches-glob-classes.ts create mode 100644 tests/corpus/5149-path-matches-glob-win32.ts diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 4276802cd..58cbcd2ec 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "d94b8161bbee649b9890", + "artifactVersion": "716a56b02ac0127c5383", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 7f62476e4..636e09051 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -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" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 55c256e1f..35f26293b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 40, - "verify-gap": 2384, - "audit-partial": 782, + "verify-gap": 2383, + "audit-partial": 783, "classify": 2, "implement": 80 } @@ -49536,12 +49536,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" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 600d7a1db..2e808bd03 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -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 @@ -69447,12 +69447,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" ] }, @@ -69616,8 +69621,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" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 8a1969d6f..772f355fd 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -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"], diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d533b2305..6622a46a9 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -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: "\\"; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 0eae483eb..4687d25a3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -154,6 +154,8 @@ export const LIB_FN_SYMS: Record = { "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", diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 69bea51bd..5ff8b50d6 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -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"))] diff --git a/packages/compiler/src/backend/native/library.ts b/packages/compiler/src/backend/native/library.ts index 83dc3bbc9..dffa4ef5b 100644 --- a/packages/compiler/src/backend/native/library.ts +++ b/packages/compiler/src/backend/native/library.ts @@ -105,7 +105,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise // 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"] : []), diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 0a1070e67..342b32dac 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -918,6 +918,12 @@ const PATH_MODULE_FNS: Record = { 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 = { 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. diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 2deee7f1f..287ef1fe8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -463,6 +463,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index c014b148f..4fe8f4883 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -406,6 +406,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. */ diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 26db4915b..8f99cce4a 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -114,7 +114,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 ( diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 1fd2b5b07..2e372fd3a 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -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", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5145-path-matches-glob-basics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5145-path-matches-glob-basics.ts.json new file mode 100644 index 000000000..564134d67 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5145-path-matches-glob-basics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5145-path-matches-glob-basics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5146-path-matches-glob-braces.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5146-path-matches-glob-braces.ts.json new file mode 100644 index 000000000..5fba1b6f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5146-path-matches-glob-braces.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5146-path-matches-glob-braces.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5147-path-matches-glob-extglob.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5147-path-matches-glob-extglob.ts.json new file mode 100644 index 000000000..8a572023b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5147-path-matches-glob-extglob.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5147-path-matches-glob-extglob.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5148-path-matches-glob-classes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5148-path-matches-glob-classes.ts.json new file mode 100644 index 000000000..e50baa2e0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5148-path-matches-glob-classes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5148-path-matches-glob-classes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5149-path-matches-glob-win32.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5149-path-matches-glob-win32.ts.json new file mode 100644 index 000000000..6812601e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5149-path-matches-glob-win32.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5149-path-matches-glob-win32.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index a0e7d554c..b56f63e76 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -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"], diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs index 9c8cb93c5..4f14d959b 100644 --- a/packages/runtime-pack-common/library-matrix.mjs +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -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", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 1c3b33462..f81d5ff6d 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -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"], diff --git a/packages/runtime-pack-common/test/library-matrix.test.ts b/packages/runtime-pack-common/test/library-matrix.test.ts index cc5d771ff..e78a2260c 100644 --- a/packages/runtime-pack-common/test/library-matrix.test.ts +++ b/packages/runtime-pack-common/test/library-matrix.test.ts @@ -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", diff --git a/packages/runtime/src/scr_glob.c b/packages/runtime/src/scr_glob.c new file mode 100644 index 000000000..b2c966fc7 --- /dev/null +++ b/packages/runtime/src/scr_glob.c @@ -0,0 +1,2163 @@ +/* path.matchesGlob — Node 24's matchGlobPattern: its bundled minimatch run + * with { nocase: macOS or Windows host, windowsPathsNoEscape, nonegate, + * nocomment, optimizationLevel: 2, platform: the path family, + * nocaseMagicOnly }. Ported function by function from that minimatch + * (brace expansion with balanced-match, the extglob AST with its flatten, + * adopt and usurp rewrites, character classes, the optimizationLevel 2 + * pattern and file rewrites, and globstar matching), so every edge case + * answers as Node does; test/glob-cases.txt is the Node-generated oracle. + * + * Strings are handled as UTF-16 code units, the unit JavaScript's string + * operations use, so indexing, comparisons and `.length` agree with the + * original. Each path segment pattern with magic becomes a JavaScript + * regular expression source that the runtime's regex engine compiles with + * minimatch's flags; matching a segment runs that regex (or minimatch's + * fast test for the simple star and question-mark shapes). + * + * Every allocation of one call comes from an arena released at the end, + * so the port can keep the original's immutable-string style. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +/* ── arena ───────────────────────────────────────────────────────────── */ + +typedef struct ArenaBlock { + struct ArenaBlock *next; + size_t used, cap; + _Alignas(16) unsigned char data[]; +} ArenaBlock; + +typedef struct { + ArenaBlock *head; + ScrRegex **regexes; /* compiled segment regexes, released with the arena */ + size_t nregex, capregex; +} Arena; + +static void *ar_alloc(Arena *a, size_t n) { + n = (n + 15) & ~(size_t)15; + if (!a->head || a->head->cap - a->head->used < n) { + size_t cap = n > 65536 ? n : 65536; + ArenaBlock *b = malloc(sizeof *b + cap); + if (!b) scr_trap("scriptc: out of memory\n"); + b->next = a->head; + b->used = 0; + b->cap = cap; + a->head = b; + } + void *p = a->head->data + a->head->used; + a->head->used += n; + return p; +} + +static void ar_free(Arena *a) { + for (size_t i = 0; i < a->nregex; i++) scr_regex_release(a->regexes[i]); + free(a->regexes); + while (a->head) { + ArenaBlock *next = a->head->next; + free(a->head); + a->head = next; + } +} + +/* ── UTF-16 strings ──────────────────────────────────────────────────── */ + +typedef struct { + const uint16_t *p; + size_t n; +} S; + +typedef struct { + Arena *a; + uint16_t *p; + size_t n, cap; +} B; + +static const S S_EMPTY = { NULL, 0 }; + +static void b_init(B *b, Arena *a) { + b->a = a; + b->p = NULL; + b->n = b->cap = 0; +} + +static void b_reserve(B *b, size_t extra) { + if (b->cap - b->n >= extra) return; + size_t cap = b->cap ? b->cap * 2 : 32; + while (cap - b->n < extra) cap *= 2; + uint16_t *p = ar_alloc(b->a, cap * sizeof *p); + if (b->n) memcpy(p, b->p, b->n * sizeof *p); + b->p = p; + b->cap = cap; +} + +static void b_unit(B *b, uint16_t u) { + b_reserve(b, 1); + b->p[b->n++] = u; +} + +static void b_s(B *b, S s) { + if (!s.n) return; + b_reserve(b, s.n); + memcpy(b->p + b->n, s.p, s.n * sizeof *s.p); + b->n += s.n; +} + +static void b_c(B *b, const char *ascii) { + for (; *ascii; ascii++) b_unit(b, (uint16_t)(unsigned char)*ascii); +} + +static S b_done(B *b) { + S s = { b->p, b->n }; + return s; +} + +static S s_c(Arena *a, const char *ascii) { + B b; + b_init(&b, a); + b_c(&b, ascii); + return b_done(&b); +} + +static S s_cat(Arena *a, S x, S y) { + B b; + b_init(&b, a); + b_s(&b, x); + b_s(&b, y); + return b_done(&b); +} + +static S s_cat3(Arena *a, S x, S y, S z) { + B b; + b_init(&b, a); + b_s(&b, x); + b_s(&b, y); + b_s(&b, z); + return b_done(&b); +} + +static S s_slice(S s, size_t from, size_t to) { + if (to > s.n) to = s.n; + if (from > to) from = to; + S r = { s.p + from, to - from }; + return r; +} + +static bool s_eq(S x, S y) { + return x.n == y.n && (x.n == 0 || memcmp(x.p, y.p, x.n * sizeof *x.p) == 0); +} + +static bool s_eq_c(S x, const char *ascii) { + size_t n = strlen(ascii); + if (x.n != n) return false; + for (size_t i = 0; i < n; i++) + if (x.p[i] != (unsigned char)ascii[i]) return false; + return true; +} + +static bool s_starts_at(S s, size_t at, S prefix) { + return at <= s.n && s.n - at >= prefix.n && + (prefix.n == 0 || memcmp(s.p + at, prefix.p, prefix.n * sizeof *s.p) == 0); +} + +static bool s_starts_c(S s, size_t at, const char *ascii) { + size_t n = strlen(ascii); + if (at > s.n || s.n - at < n) return false; + for (size_t i = 0; i < n; i++) + if (s.p[at + i] != (unsigned char)ascii[i]) return false; + return true; +} + +static bool s_ends(S s, S suffix) { + return s.n >= suffix.n && s_starts_at(s, s.n - suffix.n, suffix); +} + +/* String.prototype.indexOf(single unit, from): -1 when absent. */ +static long s_index_unit(S s, uint16_t u, long from) { + if (from < 0) from = 0; + for (size_t i = (size_t)from; i < s.n; i++) + if (s.p[i] == u) return (long)i; + return -1; +} + +static bool s_has_unit(S s, uint16_t u) { return s_index_unit(s, u, 0) >= 0; } + +/* `.` in a non-unicode regex: any unit but a line terminator. */ +static bool is_line_terminator(uint16_t c) { + return c == '\n' || c == '\r' || c == 0x2028 || c == 0x2029; +} + +/* `\s` in a regex: WhiteSpace and LineTerminator units. */ +static bool is_js_space(uint16_t c) { + return c == ' ' || (c >= 0x09 && c <= 0x0D) || c == 0xA0 || c == 0x1680 || + (c >= 0x2000 && c <= 0x200A) || c == 0x2028 || c == 0x2029 || c == 0x202F || + c == 0x205F || c == 0x3000 || c == 0xFEFF; +} + +/* WTF-8 storage to UTF-16 units (lone surrogates decode as themselves). */ +static S s_from_str(Arena *a, const ScrStr *str) { + B b; + b_init(&b, a); + b_reserve(&b, str->len + 1); + const unsigned char *p = (const unsigned char *)str->data; + size_t n = str->len, i = 0; + while (i < n) { + unsigned char c = p[i]; + uint32_t cp; + if (c < 0x80) { + cp = c; + i += 1; + } else if ((c & 0xE0) == 0xC0 && i + 1 < n) { + cp = ((uint32_t)(c & 0x1F) << 6) | (p[i + 1] & 0x3F); + i += 2; + } else if ((c & 0xF0) == 0xE0 && i + 2 < n) { + cp = ((uint32_t)(c & 0x0F) << 12) | ((uint32_t)(p[i + 1] & 0x3F) << 6) | (p[i + 2] & 0x3F); + i += 3; + } else if ((c & 0xF8) == 0xF0 && i + 3 < n) { + cp = ((uint32_t)(c & 0x07) << 18) | ((uint32_t)(p[i + 1] & 0x3F) << 12) | + ((uint32_t)(p[i + 2] & 0x3F) << 6) | (p[i + 3] & 0x3F); + i += 4; + } else { + cp = 0xFFFD; + i += 1; + } + if (cp >= 0x10000) { + cp -= 0x10000; + b_unit(&b, (uint16_t)(0xD800 | (cp >> 10))); + b_unit(&b, (uint16_t)(0xDC00 | (cp & 0x3FF))); + } else { + b_unit(&b, (uint16_t)cp); + } + } + return b_done(&b); +} + +/* UTF-16 units to a fresh string in canonical WTF-8 storage. */ +static ScrStr *s_to_str(S s) { + size_t bytes = 0; + bool lone = false; + for (size_t i = 0; i < s.n; i++) { + uint16_t u = s.p[i]; + if (u < 0x80) bytes += 1; + else if (u < 0x800) bytes += 2; + else if (u >= 0xD800 && u < 0xDC00 && i + 1 < s.n && s.p[i + 1] >= 0xDC00 && s.p[i + 1] < 0xE000) { + bytes += 4; + i++; + } else { + if (u >= 0xD800 && u < 0xE000) lone = true; + bytes += 3; + } + } + char *buf = malloc(bytes + 1); + if (!buf) scr_trap("scriptc: out of memory\n"); + size_t w = 0; + for (size_t i = 0; i < s.n; i++) { + uint32_t cp = s.p[i]; + if (cp >= 0xD800 && cp < 0xDC00 && i + 1 < s.n && s.p[i + 1] >= 0xDC00 && s.p[i + 1] < 0xE000) { + cp = 0x10000 + ((cp - 0xD800) << 10) + (s.p[i + 1] - 0xDC00); + i++; + } + if (cp < 0x80) { + buf[w++] = (char)cp; + } else if (cp < 0x800) { + buf[w++] = (char)(0xC0 | (cp >> 6)); + buf[w++] = (char)(0x80 | (cp & 0x3F)); + } else if (cp < 0x10000) { + buf[w++] = (char)(0xE0 | (cp >> 12)); + buf[w++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[w++] = (char)(0x80 | (cp & 0x3F)); + } else { + buf[w++] = (char)(0xF0 | (cp >> 18)); + buf[w++] = (char)(0x80 | ((cp >> 12) & 0x3F)); + buf[w++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[w++] = (char)(0x80 | (cp & 0x3F)); + } + } + if (lone) scr_wtf8_note(); + ScrStr *out = scr_str_new(buf, w); + free(buf); + return out; +} + +/* String.prototype.toLowerCase through the runtime's case conversion. */ +static S s_lower(Arena *a, S s) { + ScrStr *str = s_to_str(s); + ScrStr *lower = scr_str_to_lower(str); + S out = s_from_str(a, lower); + scr_str_release(lower); + scr_str_release(str); + return out; +} + +/* ── string vectors ──────────────────────────────────────────────────── */ + +typedef struct { + S *v; + size_t n, cap; +} SV; + +static void sv_push(Arena *a, SV *sv, S s) { + if (sv->n == sv->cap) { + size_t cap = sv->cap ? sv->cap * 2 : 8; + S *v = ar_alloc(a, cap * sizeof *v); + if (sv->n) memcpy(v, sv->v, sv->n * sizeof *v); + sv->v = v; + sv->cap = cap; + } + sv->v[sv->n++] = s; +} + +static SV sv_copy(Arena *a, const SV *from) { + SV sv = { NULL, 0, 0 }; + for (size_t i = 0; i < from->n; i++) sv_push(a, &sv, from->v[i]); + return sv; +} + +/* Array.prototype.splice(at, remove, ...insert) for string vectors. */ +static void sv_splice(Arena *a, SV *sv, size_t at, size_t remove, const S *insert, size_t ninsert) { + if (at > sv->n) at = sv->n; + if (remove > sv->n - at) remove = sv->n - at; + size_t n = sv->n - remove + ninsert; + S *v = ar_alloc(a, (n ? n : 1) * sizeof *v); + if (at) memcpy(v, sv->v, at * sizeof *v); + if (ninsert) memcpy(v + at, insert, ninsert * sizeof *v); + if (sv->n - at - remove) memcpy(v + at + ninsert, sv->v + at + remove, (sv->n - at - remove) * sizeof *v); + sv->v = v; + sv->n = sv->cap = n; +} + +static long sv_index_c(const SV *sv, const char *ascii, long from) { + for (size_t i = from < 0 ? 0 : (size_t)from; i < sv->n; i++) + if (s_eq_c(sv->v[i], ascii)) return (long)i; + return -1; +} + +/* `str.split(",")`. */ +static SV s_split_unit(Arena *a, S s, uint16_t sep) { + SV out = { NULL, 0, 0 }; + size_t start = 0; + for (size_t i = 0; i <= s.n; i++) { + if (i == s.n || s.p[i] == sep) { + sv_push(a, &out, s_slice(s, start, i)); + start = i + 1; + } + } + return out; +} + +/* ── brace expansion (balanced-match and brace-expansion) ────────────── */ + +#define EXPANSION_MAX 100000 + +/* The escape sentinels: unique unit strings no input contains. */ +static S esc_token(Arena *a, const char *name) { + B b; + b_init(&b, a); + b_unit(&b, 0); + b_c(&b, name); + b_unit(&b, 0xFDD0); + b_unit(&b, 0xFDD1); + b_unit(&b, 0); + return b_done(&b); +} + +typedef struct { + S slash, open, close, comma, period; +} Esc; + +static S s_replace_all(Arena *a, S s, S from, S to) { + if (!from.n) return s; + B b; + b_init(&b, a); + size_t i = 0; + while (i < s.n) { + if (s_starts_at(s, i, from)) { + b_s(&b, to); + i += from.n; + } else { + b_unit(&b, s.p[i++]); + } + } + return b_done(&b); +} + +static S escape_braces(Arena *a, const Esc *e, S s) { + s = s_replace_all(a, s, s_c(a, "\\\\"), e->slash); + s = s_replace_all(a, s, s_c(a, "\\{"), e->open); + s = s_replace_all(a, s, s_c(a, "\\}"), e->close); + s = s_replace_all(a, s, s_c(a, "\\,"), e->comma); + return s_replace_all(a, s, s_c(a, "\\."), e->period); +} + +static S unescape_braces(Arena *a, const Esc *e, S s) { + s = s_replace_all(a, s, e->slash, s_c(a, "\\")); + s = s_replace_all(a, s, e->open, s_c(a, "{")); + s = s_replace_all(a, s, e->close, s_c(a, "}")); + s = s_replace_all(a, s, e->comma, s_c(a, ",")); + return s_replace_all(a, s, e->period, s_c(a, ".")); +} + +typedef struct { + bool ok; + size_t start, end; + S pre, body, post; +} Balanced; + +/* balanced-match's range() and balanced() for "{" and "}". */ +static Balanced balanced(S str) { + Balanced r = { false, 0, 0, S_EMPTY, S_EMPTY, S_EMPTY }; + long ai = s_index_unit(str, '{', 0); + long bi = s_index_unit(str, '}', ai + 1); + long i = ai; + if (!(ai >= 0 && bi > 0)) return r; + long *begs = malloc((str.n + 1) * sizeof *begs); + if (!begs) scr_trap("scriptc: out of memory\n"); + size_t nbegs = 0; + long left = (long)str.n, right = -1; + bool have = false; + long rs = 0, re = 0; + while (i >= 0 && !have) { + if (i == ai) { + begs[nbegs++] = i; + ai = s_index_unit(str, '{', i + 1); + } else if (nbegs == 1) { + rs = begs[--nbegs]; + re = bi; + have = true; + } else { + if (nbegs > 0) { + long beg = begs[--nbegs]; + if (beg < left) { + left = beg; + right = bi; + } + } + bi = s_index_unit(str, '}', i + 1); + } + i = ai < bi && ai >= 0 ? ai : bi; + } + if (!have && nbegs && right >= 0) { + rs = left; + re = right; + have = true; + } + free(begs); + if (!have) return r; + r.ok = true; + r.start = (size_t)rs; + r.end = (size_t)re; + r.pre = s_slice(str, 0, (size_t)rs); + r.body = s_slice(str, (size_t)rs + 1, (size_t)re); + r.post = s_slice(str, (size_t)re + 1, str.n); + return r; +} + +static SV parse_comma_parts(Arena *a, S str) { + SV parts = { NULL, 0, 0 }; + if (!str.n) { + sv_push(a, &parts, S_EMPTY); + return parts; + } + Balanced m = balanced(str); + if (!m.ok) return s_split_unit(a, str, ','); + SV p = s_split_unit(a, m.pre, ','); + S brace = s_cat3(a, s_c(a, "{"), m.body, s_c(a, "}")); + p.v[p.n - 1] = s_cat(a, p.v[p.n - 1], brace); + SV post = parse_comma_parts(a, m.post); + if (m.post.n) { + p.v[p.n - 1] = s_cat(a, p.v[p.n - 1], post.v[0]); + for (size_t k = 1; k < post.n; k++) sv_push(a, &p, post.v[k]); + } + return p; +} + +/* /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/ and its alpha twin. */ +static size_t scan_int(S s, size_t i) { + size_t j = i; + if (j < s.n && s.p[j] == '-') j++; + size_t d = j; + while (j < s.n && s.p[j] >= '0' && s.p[j] <= '9') j++; + return j > d ? j : i; +} + +static bool is_alpha_unit(uint16_t c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } + +static bool is_sequence(S s, bool alpha) { + size_t i = 0, j; + if (alpha) { + if (s.n < 4 || !is_alpha_unit(s.p[0]) || s.p[1] != '.' || s.p[2] != '.' || !is_alpha_unit(s.p[3])) + return false; + i = 4; + } else { + if ((j = scan_int(s, 0)) == 0) return false; + if (!s_starts_c(s, j, "..")) return false; + size_t k = scan_int(s, j + 2); + if (k == j + 2) return false; + i = k; + } + if (i == s.n) return true; + if (!s_starts_c(s, i, "..")) return false; + size_t k = scan_int(s, i + 2); + return k != i + 2 && k == s.n; +} + +/* Number(str) for the digit forms a sequence admits, else NaN. */ +static double seq_number(S s) { + size_t end = scan_int(s, 0); + if (end == 0 || end != s.n) return NAN; + double v = 0; + size_t i = s.p[0] == '-' ? 1 : 0; + for (; i < s.n; i++) v = v * 10 + (s.p[i] - '0'); + return s.p[0] == '-' ? -v : v; +} + +static double seq_numeric(S s) { + double v = seq_number(s); + return isnan(v) ? (s.n ? (double)s.p[0] : NAN) : v; +} + +/* /^-?0\d/ */ +static bool is_padded(S s) { + size_t i = s.n && s.p[0] == '-' ? 1 : 0; + return s.n >= i + 2 && s.p[i] == '0' && s.p[i + 1] >= '0' && s.p[i + 1] <= '9'; +} + +/* /,(?!,).*\}/ */ +static bool has_comma_then_close(S s) { + for (size_t j = 0; j < s.n; j++) { + if (s.p[j] != ',' || (j + 1 < s.n && s.p[j + 1] == ',')) continue; + for (size_t k = j + 1; k < s.n && !is_line_terminator(s.p[k]); k++) + if (s.p[k] == '}') return true; + } + return false; +} + +static S s_number(Arena *a, double v) { + char buf[32]; + size_t n = scr_f64_to_str(v, buf); + buf[n] = 0; + return s_c(a, buf); +} + +typedef struct { + bool failed; /* RangeError: Invalid array length (armed) */ +} ExpandState; + +static SV expand_(Arena *a, const Esc *e, ExpandState *st, S str, size_t max, bool is_top) { + SV expansions = { NULL, 0, 0 }; + Balanced m = balanced(str); + if (!m.ok) { + sv_push(a, &expansions, str); + return expansions; + } + S pre = m.pre; + SV post; + if (m.post.n) { + post = expand_(a, e, st, m.post, max, false); + if (st->failed) return expansions; + } else { + post.v = NULL; + post.n = post.cap = 0; + sv_push(a, &post, S_EMPTY); + } + if (m.pre.n && m.pre.p[m.pre.n - 1] == '$') { + for (size_t k = 0; k < post.n && k < max; k++) + sv_push(a, &expansions, s_cat3(a, pre, s_cat3(a, s_c(a, "{"), m.body, s_c(a, "}")), post.v[k])); + return expansions; + } + bool numeric_seq = is_sequence(m.body, false); + bool alpha_seq = is_sequence(m.body, true); + bool sequence = numeric_seq || alpha_seq; + bool options = s_has_unit(m.body, ','); + if (!sequence && !options) { + if (has_comma_then_close(m.post)) { + S again = s_cat3(a, s_cat(a, m.pre, s_c(a, "{")), s_cat(a, m.body, e->close), m.post); + return expand_(a, e, st, again, max, true); + } + sv_push(a, &expansions, str); + return expansions; + } + SV n; + if (sequence) { + n.v = NULL; + n.n = n.cap = 0; + size_t start = 0; + for (size_t i = 0; i <= m.body.n; i++) { + if (i == m.body.n || s_starts_c(m.body, i, "..")) { + sv_push(a, &n, s_slice(m.body, start, i)); + if (i == m.body.n) break; + start = i + 2; + i++; + } + } + } else { + n = parse_comma_parts(a, m.body); + if (n.n == 1) { + SV inner = expand_(a, e, st, n.v[0], max, false); + if (st->failed) return expansions; + SV embraced = { NULL, 0, 0 }; + for (size_t k = 0; k < inner.n; k++) + sv_push(a, &embraced, s_cat3(a, s_c(a, "{"), inner.v[k], s_c(a, "}"))); + n = embraced; + if (n.n == 1) { + for (size_t k = 0; k < post.n; k++) + sv_push(a, &expansions, s_cat3(a, m.pre, n.v[0], post.v[k])); + return expansions; + } + } + } + SV N = { NULL, 0, 0 }; + if (sequence && n.n >= 2) { + double x = seq_numeric(n.v[0]); + double y = seq_numeric(n.v[1]); + size_t width = n.v[0].n > n.v[1].n ? n.v[0].n : n.v[1].n; + double incr = n.n == 3 ? fabs(seq_numeric(n.v[2])) : 1; + bool reverse = y < x; + if (reverse) incr = -incr; + bool pad = false; + for (size_t k = 0; k < n.n; k++) pad = pad || is_padded(n.v[k]); + /* An array never holds more than 2^32 - 1 elements. */ + double count = incr == 0 ? INFINITY : floor((y - x) / incr) + 1; + if (count > 4294967295.0) { + static const char msg[] = "Invalid array length"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + st->failed = true; + return expansions; + } + for (double i = x; reverse ? i >= y : i <= y; i += incr) { + S c; + if (alpha_seq) { + uint16_t unit = (uint16_t)(int64_t)i; + if (unit == '\\') { + c = S_EMPTY; + } else { + uint16_t *one = ar_alloc(a, sizeof *one); + *one = unit; + c.p = one; + c.n = 1; + } + } else { + c = s_number(a, i); + if (pad && width > c.n) { + size_t need = width - c.n; + B b; + b_init(&b, a); + if (i < 0) b_unit(&b, '-'); + for (size_t z = 0; z < need; z++) b_unit(&b, '0'); + b_s(&b, i < 0 ? s_slice(c, 1, c.n) : c); + c = b_done(&b); + } + } + sv_push(a, &N, c); + } + } else { + for (size_t j = 0; j < n.n; j++) { + SV sub = expand_(a, e, st, n.v[j], max, false); + if (st->failed) return expansions; + for (size_t k = 0; k < sub.n; k++) sv_push(a, &N, sub.v[k]); + } + } + for (size_t j = 0; j < N.n; j++) { + for (size_t k = 0; k < post.n && expansions.n < max; k++) { + S expansion = s_cat3(a, pre, N.v[j], post.v[k]); + if (!is_top || sequence || expansion.n) sv_push(a, &expansions, expansion); + } + } + return expansions; +} + +/* minimatch's braceExpand: patterns without a closed brace pass through. */ +static SV brace_expand(Arena *a, S pattern, bool *failed) { + SV out = { NULL, 0, 0 }; + bool closed = false; + for (size_t i = 0; i < pattern.n && !closed; i++) { + if (pattern.p[i] != '{') continue; + for (size_t k = i + 1; k < pattern.n; k++) { + uint16_t c = pattern.p[k]; + if (c == '}') { + closed = true; + break; + } + if (c == '{' || is_line_terminator(c)) break; + } + } + if (!closed) { + sv_push(a, &out, pattern); + return out; + } + Esc e = { esc_token(a, "SLASH"), esc_token(a, "OPEN"), esc_token(a, "CLOSE"), + esc_token(a, "COMMA"), esc_token(a, "PERIOD") }; + S str = pattern; + if (s_starts_c(str, 0, "{}")) str = s_cat(a, s_c(a, "\\{\\}"), s_slice(str, 2, str.n)); + ExpandState st = { false }; + SV raw = expand_(a, &e, &st, escape_braces(a, &e, str), EXPANSION_MAX, true); + if (st.failed) { + *failed = true; + return out; + } + for (size_t i = 0; i < raw.n; i++) sv_push(a, &out, unescape_braces(a, &e, raw.v[i])); + return out; +} + +/* ── character classes (brace-expressions.js) ────────────────────────── */ + +static const struct { + const char *cls, *unip; + bool u, neg; +} POSIX_CLASSES[] = { + { "[:alnum:]", "\\p{L}\\p{Nl}\\p{Nd}", true, false }, + { "[:alpha:]", "\\p{L}\\p{Nl}", true, false }, + { "[:ascii:]", "\\x00-\\x7f", false, false }, + { "[:blank:]", "\\p{Zs}\\t", true, false }, + { "[:cntrl:]", "\\p{Cc}", true, false }, + { "[:digit:]", "\\p{Nd}", true, false }, + { "[:graph:]", "\\p{Z}\\p{C}", true, true }, + { "[:lower:]", "\\p{Ll}", true, false }, + { "[:print:]", "\\p{C}", true, false }, + { "[:punct:]", "\\p{P}", true, false }, + { "[:space:]", "\\p{Z}\\t\\r\\n\\v\\f", true, false }, + { "[:upper:]", "\\p{Lu}", true, false }, + { "[:word:]", "\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}", true, false }, + { "[:xdigit:]", "A-Fa-f0-9", false, false }, +}; + +static void b_brace_escape(B *b, uint16_t c) { + if (c == '[' || c == ']' || c == '\\' || c == '-') b_unit(b, '\\'); + b_unit(b, c); +} + +static bool is_regexp_special(uint16_t c) { + return c == '-' || c == '[' || c == ']' || c == '{' || c == '}' || c == '(' || c == ')' || + c == '*' || c == '+' || c == '?' || c == '.' || c == ',' || c == '\\' || c == '^' || + c == '$' || c == '|' || c == '#' || is_js_space(c); +} + +static void b_regexp_escape(B *b, S s) { + for (size_t i = 0; i < s.n; i++) { + if (is_regexp_special(s.p[i])) b_unit(b, '\\'); + b_unit(b, s.p[i]); + } +} + +typedef struct { + S src; + bool uflag; + size_t consumed; + bool magic; +} ClassResult; + +static ClassResult parse_class(Arena *a, S glob, size_t pos) { + ClassResult none = { S_EMPTY, false, 0, false }; + ClassResult bad = { s_c(a, "$."), false, glob.n - pos, true }; + SV ranges = { NULL, 0, 0 }, negs = { NULL, 0, 0 }; + size_t i = pos + 1; + bool saw_start = false, uflag = false, escaping = false, negate = false; + size_t end_pos = pos; + bool have_start = false; + uint16_t range_start = 0; + while (i < glob.n) { + uint16_t c = glob.p[i]; + if ((c == '!' || c == '^') && i == pos + 1) { + negate = true; + i++; + continue; + } + if (c == ']' && saw_start && !escaping) { + end_pos = i + 1; + break; + } + saw_start = true; + if (c == '\\' && !escaping) { + escaping = true; + i++; + continue; + } + if (c == '[' && !escaping) { + bool matched = false; + for (size_t k = 0; k < sizeof POSIX_CLASSES / sizeof POSIX_CLASSES[0]; k++) { + if (s_starts_c(glob, i, POSIX_CLASSES[k].cls)) { + if (have_start) return bad; + i += strlen(POSIX_CLASSES[k].cls); + sv_push(a, POSIX_CLASSES[k].neg ? &negs : &ranges, s_c(a, POSIX_CLASSES[k].unip)); + uflag = uflag || POSIX_CLASSES[k].u; + matched = true; + break; + } + } + if (matched) continue; + } + escaping = false; + if (have_start) { + B b; + b_init(&b, a); + if (c > range_start) { + b_brace_escape(&b, range_start); + b_unit(&b, '-'); + b_brace_escape(&b, c); + sv_push(a, &ranges, b_done(&b)); + } else if (c == range_start) { + b_brace_escape(&b, c); + sv_push(a, &ranges, b_done(&b)); + } + have_start = false; + i++; + continue; + } + if (s_starts_c(glob, i + 1, "-]")) { + B b; + b_init(&b, a); + b_brace_escape(&b, c); + b_brace_escape(&b, '-'); + sv_push(a, &ranges, b_done(&b)); + i += 2; + continue; + } + if (s_starts_c(glob, i + 1, "-")) { + have_start = true; + range_start = c; + i += 2; + continue; + } + B b; + b_init(&b, a); + b_brace_escape(&b, c); + sv_push(a, &ranges, b_done(&b)); + i++; + } + if (end_pos < i) return none; + if (!ranges.n && !negs.n) return bad; + if (negs.n == 0 && ranges.n == 1 && !negate) { + S r0 = ranges.v[0]; + bool one = (r0.n == 1 && !is_line_terminator(r0.p[0])) || + (r0.n == 2 && r0.p[0] == '\\' && !is_line_terminator(r0.p[1])); + if (one) { + S r = r0.n == 2 ? s_slice(r0, 1, 2) : r0; + B b; + b_init(&b, a); + b_regexp_escape(&b, r); + ClassResult out = { b_done(&b), false, end_pos - pos, false }; + return out; + } + } + B sr, sn; + b_init(&sr, a); + b_unit(&sr, '['); + if (negate) b_unit(&sr, '^'); + for (size_t k = 0; k < ranges.n; k++) b_s(&sr, ranges.v[k]); + b_unit(&sr, ']'); + b_init(&sn, a); + b_unit(&sn, '['); + if (!negate) b_unit(&sn, '^'); + for (size_t k = 0; k < negs.n; k++) b_s(&sn, negs.v[k]); + b_unit(&sn, ']'); + S comb; + if (ranges.n && negs.n) { + B b; + b_init(&b, a); + b_unit(&b, '('); + b_s(&b, b_done(&sr)); + b_unit(&b, '|'); + b_s(&b, b_done(&sn)); + b_unit(&b, ')'); + comb = b_done(&b); + } else { + comb = ranges.n ? b_done(&sr) : b_done(&sn); + } + ClassResult out = { comb, uflag, end_pos - pos, true }; + return out; +} + +/* unescape.js with its defaults (magicalBraces, backslash escapes). */ +static S unescape_glob(Arena *a, S s) { + B b; + b_init(&b, a); + /* s.replace(/((?!\\).|^)\[([^\/\\])\]/g, "$1$2") */ + size_t i = 0; + while (i < s.n) { + if (s.p[i] != '\\' && !is_line_terminator(s.p[i]) && i + 3 < s.n && s.p[i + 1] == '[' && + s.p[i + 2] != '/' && s.p[i + 2] != '\\' && s.p[i + 3] == ']') { + b_unit(&b, s.p[i]); + b_unit(&b, s.p[i + 2]); + i += 4; + continue; + } + if (i == 0 && s.n >= 3 && s.p[0] == '[' && s.p[1] != '/' && s.p[1] != '\\' && s.p[2] == ']') { + b_unit(&b, s.p[1]); + i = 3; + continue; + } + b_unit(&b, s.p[i++]); + } + S first = b_done(&b); + /* .replace(/\\([^\/])/g, "$1") */ + B c; + b_init(&c, a); + for (size_t k = 0; k < first.n; k++) { + if (first.p[k] == '\\' && k + 1 < first.n && first.p[k + 1] != '/') { + b_unit(&c, first.p[k + 1]); + k++; + continue; + } + b_unit(&c, first.p[k]); + } + return b_done(&c); +} + +/* ── the extglob AST (ast.js) ────────────────────────────────────────── */ + +typedef struct Ast Ast; + +typedef struct { + bool is_str; + S str; + Ast *ast; +} Part; + +typedef struct { + Part *v; + size_t n, cap; +} PartV; + +typedef struct { + Ast **v; + size_t n, cap; +} AstV; + +struct Ast { + uint16_t type; /* 0 for a plain sequence, else ! ? + * @ */ + Ast *root, *parent; + size_t parent_index; + int has_magic; /* -1 undefined */ + bool uflag; + PartV parts; + AstV *negs; + bool filled_negs; + bool to_string_valid; + S to_string; + bool empty_ext; +}; + +typedef struct { + Arena *a; + bool nocase; +} Ctx; + +static void partv_push(Arena *a, PartV *pv, Part p) { + if (pv->n == pv->cap) { + size_t cap = pv->cap ? pv->cap * 2 : 4; + Part *v = ar_alloc(a, cap * sizeof *v); + if (pv->n) memcpy(v, pv->v, pv->n * sizeof *v); + pv->v = v; + pv->cap = cap; + } + pv->v[pv->n++] = p; +} + +static Ast *ast_new(Ctx *x, uint16_t type, Ast *parent) { + Ast *t = ar_alloc(x->a, sizeof *t); + memset(t, 0, sizeof *t); + t->type = type; + t->has_magic = type ? 1 : -1; + t->parent = parent; + t->root = parent ? parent->root : t; + if (t->root == t) { + t->negs = ar_alloc(x->a, sizeof *t->negs); + memset(t->negs, 0, sizeof *t->negs); + } else { + t->negs = t->root->negs; + } + if (type == '!' && !t->root->filled_negs) { + AstV *nv = t->negs; + if (nv->n == nv->cap) { + size_t cap = nv->cap ? nv->cap * 2 : 4; + Ast **v = ar_alloc(x->a, cap * sizeof *v); + if (nv->n) memcpy(v, nv->v, nv->n * sizeof *v); + nv->v = v; + nv->cap = cap; + } + nv->v[nv->n++] = t; + } + t->parent_index = parent ? parent->parts.n : 0; + return t; +} + +static bool is_ext_type(uint16_t c) { return c == '!' || c == '?' || c == '+' || c == '*' || c == '@'; } + +static void ast_push_str(Ctx *x, Ast *t, S s) { + if (!s.n) return; + Part p = { true, s, NULL }; + partv_push(x->a, &t->parts, p); +} + +static void ast_push_ast(Ctx *x, Ast *t, Ast *child) { + Part p = { false, S_EMPTY, child }; + partv_push(x->a, &t->parts, p); +} + +static S ast_to_string(Ctx *x, Ast *t) { + if (t->to_string_valid) return t->to_string; + B b; + b_init(&b, x->a); + if (t->type) { + b_unit(&b, t->type); + b_unit(&b, '('); + } + for (size_t i = 0; i < t->parts.n; i++) { + if (t->type && i) b_unit(&b, '|'); + b_s(&b, t->parts.v[i].is_str ? t->parts.v[i].str : ast_to_string(x, t->parts.v[i].ast)); + } + if (t->type) b_unit(&b, ')'); + t->to_string = b_done(&b); + t->to_string_valid = true; + return t->to_string; +} + +static bool ast_is_start(Ast *t) { + if (t->root == t) return true; + if (!ast_is_start(t->parent)) return false; + if (t->parent_index == 0) return true; + Ast *p = t->parent; + for (size_t i = 0; i < t->parent_index; i++) { + if (i >= p->parts.n) return false; + Part *pp = &p->parts.v[i]; + if (pp->is_str || pp->ast->type != '!') return false; + } + return true; +} + +static bool ast_is_end(Ast *t) { + if (t->root == t) return true; + if (t->parent->type == '!') return true; + if (!ast_is_end(t->parent)) return false; + if (!t->type) return ast_is_end(t->parent); + return t->parent_index == t->parent->parts.n - 1; +} + +static Ast *ast_clone(Ctx *x, Ast *t, Ast *parent); + +static void ast_copy_in(Ctx *x, Ast *t, Part part) { + if (part.is_str) ast_push_str(x, t, part.str); + else ast_push_ast(x, t, ast_clone(x, part.ast, t)); +} + +static Ast *ast_clone(Ctx *x, Ast *t, Ast *parent) { + Ast *c = ast_new(x, t->type, parent); + for (size_t i = 0; i < t->parts.n; i++) ast_copy_in(x, c, t->parts.v[i]); + return c; +} + +static void ast_fill_negs(Ctx *x, Ast *t) { + if (t->filled_negs) return; + ast_to_string(x, t); + t->filled_negs = true; + AstV *nv = t->negs; + while (nv->n) { + Ast *n = nv->v[--nv->n]; + if (n->type != '!') continue; + Ast *p = n, *pp = p->parent; + while (pp) { + for (size_t i = p->parent_index + 1; !pp->type && i < pp->parts.n; i++) { + for (size_t k = 0; k < n->parts.n; k++) { + /* Extglob alternatives are always AST parts. */ + if (!n->parts.v[k].is_str) ast_copy_in(x, n->parts.v[k].ast, pp->parts.v[i]); + } + } + p = pp; + pp = p->parent; + } + } +} + +/* The adoption tables: which child extglob types a parent type absorbs. */ +static bool type_in(const char *list, uint16_t c) { + for (; *list; list++) + if ((uint16_t)(unsigned char)*list == c) return true; + return false; +} + +static const char *adoption(uint16_t type) { + switch (type) { + case '!': return "@"; + case '?': return "?@"; + case '@': return "@"; + case '*': return "*+?@"; + case '+': return "+@"; + default: return ""; + } +} + +static const char *adoption_with_space(uint16_t type) { + switch (type) { + case '!': return "?"; + case '@': return "?"; + case '+': return "?*"; + default: return ""; + } +} + +static const char *adoption_any(uint16_t type) { + switch (type) { + case '!': return "?@"; + case '?': return "?@"; + case '@': return "?@"; + case '*': return "*+?@"; + case '+': return "+@?*"; + default: return ""; + } +} + +/* usurpMap: the type a parent takes when it absorbs a child of type c. */ +static uint16_t usurp_type(uint16_t type, uint16_t c) { + switch (type) { + case '!': return c == '!' ? '@' : 0; + case '?': return c == '*' || c == '+' ? '*' : 0; + case '@': return is_ext_type(c) ? c : 0; + case '+': return c == '?' || c == '*' ? '*' : 0; + default: return 0; + } +} + +static bool ast_can_adopt_type(Ast *t, uint16_t c, const char *(*map)(uint16_t)) { + return t->type && type_in(map(t->type), c); +} + +static Ast *only_ext_grandchild(Part *child) { + if (child->is_str) return NULL; + Ast *c = child->ast; + if (c->type || c->parts.n != 1) return NULL; + Part *gc = &c->parts.v[0]; + if (gc->is_str || !gc->ast->type) return NULL; + return gc->ast; +} + +static bool ast_can_adopt(Ast *t, Part *child, const char *(*map)(uint16_t)) { + if (!t->type) return false; + Ast *gc = only_ext_grandchild(child); + return gc && ast_can_adopt_type(t, gc->type, map); +} + +static void ast_adopt(Ctx *x, Ast *t, Part *child, size_t index) { + Ast *gc = child->ast->parts.v[0].ast; + PartV parts = { NULL, 0, 0 }; + for (size_t i = 0; i < index; i++) partv_push(x->a, &parts, t->parts.v[i]); + for (size_t i = 0; i < gc->parts.n; i++) partv_push(x->a, &parts, gc->parts.v[i]); + for (size_t i = index + 1; i < t->parts.n; i++) partv_push(x->a, &parts, t->parts.v[i]); + for (size_t i = 0; i < gc->parts.n; i++) + if (!gc->parts.v[i].is_str) gc->parts.v[i].ast->parent = t; + t->parts = parts; + t->to_string_valid = false; +} + +static void ast_adopt_with_space(Ctx *x, Ast *t, Part *child, size_t index) { + Ast *gc = child->ast->parts.v[0].ast; + Ast *blank = ast_new(x, 0, gc); + Part empty = { true, S_EMPTY, NULL }; + partv_push(x->a, &blank->parts, empty); + ast_push_ast(x, gc, blank); + ast_adopt(x, t, child, index); +} + +static bool ast_can_usurp(Ast *t, Part *child) { + if (!t->type || t->parts.n != 1) return false; + Ast *gc = only_ext_grandchild(child); + return gc && usurp_type(t->type, gc->type) != 0; +} + +static void ast_usurp(Ast *t, Part *child) { + Ast *gc = child->ast->parts.v[0].ast; + uint16_t nt = usurp_type(t->type, gc->type); + if (!nt) return; + t->parts = gc->parts; + for (size_t i = 0; i < t->parts.n; i++) + if (!t->parts.v[i].is_str) t->parts.v[i].ast->parent = t; + t->type = nt; + t->to_string_valid = false; + t->empty_ext = false; +} + +static void ast_flatten(Ctx *x, Ast *t) { + if (!is_ext_type(t->type)) { + for (size_t i = 0; i < t->parts.n; i++) + if (!t->parts.v[i].is_str) ast_flatten(x, t->parts.v[i].ast); + } else { + int iterations = 0; + bool done; + do { + done = true; + for (size_t i = 0; i < t->parts.n; i++) { + Part c = t->parts.v[i]; + if (c.is_str) continue; + ast_flatten(x, c.ast); + if (ast_can_adopt(t, &c, adoption)) { + done = false; + ast_adopt(x, t, &c, i); + } else if (ast_can_adopt(t, &c, adoption_with_space)) { + done = false; + ast_adopt_with_space(x, t, &c, i); + } else if (ast_can_usurp(t, &c)) { + done = false; + ast_usurp(t, &c); + } + } + } while (!done && ++iterations < 10); + } + t->to_string_valid = false; +} + +static size_t ast_parse(Ctx *x, S str, Ast *ast, size_t pos, int ext_depth) { + const int max_depth = 2; + bool escaping = false, in_brace = false, brace_neg = false; + size_t brace_start = (size_t)-1; + if (!ast->type) { + size_t i2 = pos; + B acc; + b_init(&acc, x->a); + while (i2 < str.n) { + uint16_t c = str.p[i2++]; + if (escaping || c == '\\') { + escaping = !escaping; + b_unit(&acc, c); + continue; + } + if (in_brace) { + if (i2 == brace_start + 1) { + if (c == '^' || c == '!') brace_neg = true; + } else if (c == ']' && !(i2 == brace_start + 2 && brace_neg)) { + in_brace = false; + } + b_unit(&acc, c); + continue; + } else if (c == '[') { + in_brace = true; + brace_start = i2; + brace_neg = false; + b_unit(&acc, c); + continue; + } + bool recurse = is_ext_type(c) && i2 < str.n && str.p[i2] == '(' && ext_depth <= max_depth; + if (recurse) { + ast_push_str(x, ast, b_done(&acc)); + b_init(&acc, x->a); + Ast *ext = ast_new(x, c, ast); + i2 = ast_parse(x, str, ext, i2, ext_depth + 1); + ast_push_ast(x, ast, ext); + continue; + } + b_unit(&acc, c); + } + ast_push_str(x, ast, b_done(&acc)); + return i2; + } + size_t i = pos + 1; + Ast *part = ast_new(x, 0, ast); + AstV parts = { NULL, 0, 0 }; + B acc; + b_init(&acc, x->a); + while (i < str.n) { + uint16_t c = str.p[i++]; + if (escaping || c == '\\') { + escaping = !escaping; + b_unit(&acc, c); + continue; + } + if (in_brace) { + if (i == brace_start + 1) { + if (c == '^' || c == '!') brace_neg = true; + } else if (c == ']' && !(i == brace_start + 2 && brace_neg)) { + in_brace = false; + } + b_unit(&acc, c); + continue; + } else if (c == '[') { + in_brace = true; + brace_start = i; + brace_neg = false; + b_unit(&acc, c); + continue; + } + bool can_adopt = ast_can_adopt_type(ast, c, adoption_any); + bool recurse = is_ext_type(c) && i < str.n && str.p[i] == '(' && (ext_depth <= max_depth || can_adopt); + if (recurse) { + int depth_add = can_adopt ? 0 : 1; + ast_push_str(x, part, b_done(&acc)); + b_init(&acc, x->a); + Ast *ext = ast_new(x, c, part); + ast_push_ast(x, part, ext); + i = ast_parse(x, str, ext, i, ext_depth + depth_add); + continue; + } + if (c == '|') { + ast_push_str(x, part, b_done(&acc)); + b_init(&acc, x->a); + if (parts.n == parts.cap) { + size_t cap = parts.cap ? parts.cap * 2 : 4; + Ast **v = ar_alloc(x->a, cap * sizeof *v); + if (parts.n) memcpy(v, parts.v, parts.n * sizeof *v); + parts.v = v; + parts.cap = cap; + } + parts.v[parts.n++] = part; + part = ast_new(x, 0, ast); + continue; + } + if (c == ')') { + if (acc.n == 0 && ast->parts.n == 0) ast->empty_ext = true; + ast_push_str(x, part, b_done(&acc)); + for (size_t k = 0; k < parts.n; k++) ast_push_ast(x, ast, parts.v[k]); + ast_push_ast(x, ast, part); + return i; + } + b_unit(&acc, c); + } + /* An unterminated extglob is literal text. */ + ast->type = 0; + ast->has_magic = -1; + ast->parts.n = 0; + Part lit = { true, s_slice(str, pos - 1, str.n), NULL }; + partv_push(x->a, &ast->parts, lit); + return i; +} + +typedef struct { + S re, body; + bool has_magic, uflag; +} Source; + +static const char *START_NO_TRAVERSAL = "(?!(?:^|/)\\.\\.?(?:$|/))"; +static const char *START_NO_DOT = "(?!\\.)"; +static const char *QMARK = "[^/]"; +static const char *STAR = "[^/]*?"; +static const char *STAR_NO_EMPTY = "[^/]+?"; + +static bool is_re_special(uint16_t c) { + const char *specials = "().*{}+?[]^$\\!"; + for (; *specials; specials++) + if ((uint16_t)(unsigned char)*specials == c) return true; + return false; +} + +static Source parse_glob(Ctx *x, S glob, int has_magic, bool no_empty) { + bool escaping = false, uflag = false, in_star = false; + B re; + b_init(&re, x->a); + bool all_stars = glob.n > 0; + for (size_t k = 0; k < glob.n; k++) all_stars = all_stars && glob.p[k] == '*'; + for (size_t i = 0; i < glob.n; i++) { + uint16_t c = glob.p[i]; + if (escaping) { + escaping = false; + if (is_re_special(c)) b_unit(&re, '\\'); + b_unit(&re, c); + continue; + } + if (c == '*') { + if (in_star) continue; + in_star = true; + b_c(&re, no_empty && all_stars ? STAR_NO_EMPTY : STAR); + has_magic = 1; + continue; + } + in_star = false; + if (c == '\\') { + if (i == glob.n - 1) b_c(&re, "\\\\"); + else escaping = true; + continue; + } + if (c == '[') { + ClassResult cls = parse_class(x->a, glob, i); + if (cls.consumed) { + b_s(&re, cls.src); + uflag = uflag || cls.uflag; + i += cls.consumed - 1; + has_magic = has_magic == 1 || cls.magic ? 1 : has_magic; + continue; + } + } + if (c == '?') { + b_c(&re, QMARK); + has_magic = 1; + continue; + } + S one = { &glob.p[i], 1 }; + b_regexp_escape(&re, one); + } + Source out = { b_done(&re), unescape_glob(x->a, glob), has_magic == 1, uflag }; + return out; +} + +/* allow_dot: -1 undefined, else a boolean. */ +static Source ast_to_regexp_source(Ctx *x, Ast *t, int allow_dot); + +static S ast_parts_to_regexp(Ctx *x, Ast *t, bool dot) { + B b; + b_init(&b, x->a); + bool first = true; + for (size_t i = 0; i < t->parts.n; i++) { + /* Extglob parts are always AST nodes. */ + if (t->parts.v[i].is_str) continue; + Source s = ast_to_regexp_source(x, t->parts.v[i].ast, dot); + t->uflag = t->uflag || s.uflag; + if (ast_is_start(t) && ast_is_end(t) && !s.re.n) continue; + if (!first) b_unit(&b, '|'); + b_s(&b, s.re); + first = false; + } + return b_done(&b); +} + +static bool is_pattern_start_unit(S s, size_t at) { + return at < s.n && (s.p[at] == '[' || s.p[at] == '.'); +} + +static Source ast_to_regexp_source(Ctx *x, Ast *t, int allow_dot) { + bool dot = allow_dot == -1 ? false : allow_dot == 1; + if (t->root == t) { + ast_flatten(x, t); + ast_fill_negs(x, t); + } + if (!is_ext_type(t->type)) { + bool no_empty = ast_is_start(t) && ast_is_end(t); + for (size_t i = 0; i < t->parts.n && no_empty; i++) + if (!t->parts.v[i].is_str) no_empty = false; + B src; + b_init(&src, x->a); + for (size_t i = 0; i < t->parts.n; i++) { + Part *p = &t->parts.v[i]; + Source s = p->is_str ? parse_glob(x, p->str, t->has_magic, no_empty) + : ast_to_regexp_source(x, p->ast, allow_dot); + t->has_magic = t->has_magic == 1 || s.has_magic ? 1 : t->has_magic; + t->uflag = t->uflag || s.uflag; + b_s(&src, s.re); + } + S srcs = b_done(&src); + const char *start = ""; + if (ast_is_start(t) && t->parts.n && t->parts.v[0].is_str) { + S p0 = t->parts.v[0].str; + bool dot_trav_allowed = t->parts.n == 1 && (s_eq_c(p0, "..") || s_eq_c(p0, ".")); + if (!dot_trav_allowed) { + bool need_no_trav = (dot && is_pattern_start_unit(srcs, 0)) || + (s_starts_c(srcs, 0, "\\.") && is_pattern_start_unit(srcs, 2)) || + (s_starts_c(srcs, 0, "\\.\\.") && is_pattern_start_unit(srcs, 4)); + bool need_no_dot = !dot && allow_dot != 1 && is_pattern_start_unit(srcs, 0); + start = need_no_trav ? START_NO_TRAVERSAL : need_no_dot ? START_NO_DOT : ""; + } + } + const char *end = ""; + if (ast_is_end(t) && t->root->filled_negs && t->parent && t->parent->type == '!') end = "(?:$|\\/)"; + B fin; + b_init(&fin, x->a); + b_c(&fin, start); + b_s(&fin, srcs); + b_c(&fin, end); + t->has_magic = t->has_magic == 1 ? 1 : 0; + Source out = { b_done(&fin), unescape_glob(x->a, srcs), t->has_magic == 1, t->uflag }; + return out; + } + bool repeated = t->type == '*' || t->type == '+'; + const char *start = t->type == '!' ? "(?:(?!(?:" : "(?:"; + S body = ast_parts_to_regexp(x, t, dot); + if (ast_is_start(t) && ast_is_end(t) && !body.n && t->type != '!') { + S s = ast_to_string(x, t); + t->parts.n = 0; + Part lit = { true, s, NULL }; + partv_push(x->a, &t->parts, lit); + t->type = 0; + t->has_magic = -1; + Source out = { s, unescape_glob(x->a, ast_to_string(x, t)), false, false }; + return out; + } + S body_dot_allowed = !repeated || allow_dot == 1 || dot ? S_EMPTY : ast_parts_to_regexp(x, t, true); + if (s_eq(body_dot_allowed, body)) body_dot_allowed = S_EMPTY; + if (body_dot_allowed.n) { + B b; + b_init(&b, x->a); + b_c(&b, "(?:"); + b_s(&b, body); + b_c(&b, ")(?:"); + b_s(&b, body_dot_allowed); + b_c(&b, ")*?"); + body = b_done(&b); + } + B fin; + b_init(&fin, x->a); + if (t->type == '!' && t->empty_ext) { + if (ast_is_start(t) && !dot) b_c(&fin, START_NO_DOT); + b_c(&fin, STAR_NO_EMPTY); + } else { + b_c(&fin, start); + b_s(&fin, body); + if (t->type == '!') { + b_c(&fin, "))"); + if (ast_is_start(t) && !dot && allow_dot != 1) b_c(&fin, START_NO_DOT); + b_c(&fin, STAR); + b_c(&fin, ")"); + } else if (t->type == '@') { + b_c(&fin, ")"); + } else if (t->type == '?') { + b_c(&fin, ")?"); + } else if (t->type == '+' && body_dot_allowed.n) { + b_c(&fin, ")"); + } else if (t->type == '*' && body_dot_allowed.n) { + b_c(&fin, ")?"); + } else { + b_unit(&fin, ')'); + b_unit(&fin, t->type); + } + } + t->has_magic = t->has_magic == 1 ? 1 : 0; + Source out = { b_done(&fin), unescape_glob(x->a, body), t->has_magic == 1, t->uflag }; + return out; +} + +/* ── segment patterns (Minimatch.parse) ──────────────────────────────── */ + +typedef enum { PAT_GLOBSTAR, PAT_STRING, PAT_REGEX } PatKind; + +typedef enum { + FAST_NONE, + FAST_STAR, /* starTest */ + FAST_STAR_EXT, /* starDotExtTest(ext) */ + FAST_QMARKS, /* qmarksTest([$0, ext]) */ + FAST_STAR_DOT_STAR, + FAST_DOT_STAR, +} FastKind; + +typedef struct { + PatKind kind; + S str; /* PAT_STRING */ + ScrRegex *regex; /* PAT_REGEX without a fast test */ + FastKind fast; + S ext; /* lowercased under nocase */ + size_t qlen; /* qmarks: the whole match's length */ +} Pat; + +typedef struct { + Pat *v; + size_t n, cap; +} PatV; + +static void patv_push(Arena *a, PatV *pv, Pat p) { + if (pv->n == pv->cap) { + size_t cap = pv->cap ? pv->cap * 2 : 8; + Pat *v = ar_alloc(a, cap * sizeof *v); + if (pv->n) memcpy(v, pv->v, pv->n * sizeof *v); + pv->v = v; + pv->cap = cap; + } + pv->v[pv->n++] = p; +} + +static FastKind fast_kind(S p, S *ext, size_t *qlen) { + size_t i = 0; + while (i < p.n && p.p[i] == '*') i++; + if (i > 0 && i == p.n) return FAST_STAR; + if (i > 0) { + /* `[^+@!?\*\[\(]*` admits every other unit, line terminators too. */ + bool plain = true; + for (size_t k = i; k < p.n; k++) { + uint16_t c = p.p[k]; + if (c == '+' || c == '@' || c == '!' || c == '?' || c == '*' || c == '[' || c == '(') plain = false; + } + if (plain) { + *ext = s_slice(p, i, p.n); + return FAST_STAR_EXT; + } + } + size_t q = 0; + while (q < p.n && p.p[q] == '?') q++; + if (q > 0) { + bool plain = true; + for (size_t k = q; k < p.n; k++) { + uint16_t c = p.p[k]; + if (c == '+' || c == '@' || c == '!' || c == '?' || c == '*' || c == '[' || c == '(') plain = false; + } + if (plain) { + *ext = s_slice(p, q, p.n); + *qlen = p.n; + return FAST_QMARKS; + } + } + /* ^\*+\.\*+$ */ + if (i > 0 && i < p.n && p.p[i] == '.') { + size_t k = i + 1, s = k; + while (k < p.n && p.p[k] == '*') k++; + if (k > s && k == p.n) return FAST_STAR_DOT_STAR; + } + /* ^\.\*+$ */ + if (p.n >= 2 && p.p[0] == '.') { + size_t k = 1; + while (k < p.n && p.p[k] == '*') k++; + if (k == p.n) return FAST_DOT_STAR; + } + return FAST_NONE; +} + +static bool throw_too_long(void) { + static const char msg[] = "pattern is too long"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; +} + +/* Minimatch.parse for one segment. False after arming an exception. */ +static bool parse_segment(Ctx *x, S pattern, Pat *out) { + memset(out, 0, sizeof *out); + if (pattern.n > 65536) return throw_too_long(); + if (s_eq_c(pattern, "**")) { + out->kind = PAT_GLOBSTAR; + return true; + } + if (!pattern.n) { + out->kind = PAT_STRING; + out->str = S_EMPTY; + return true; + } + S ext = S_EMPTY; + size_t qlen = 0; + FastKind fast = fast_kind(pattern, &ext, &qlen); + Ast *ast = ast_new(x, 0, NULL); + ast_parse(x, pattern, ast, 0, 0); + ast_to_string(x, ast); + Source src = ast_to_regexp_source(x, ast, -1); + bool any_magic = src.has_magic || ast->has_magic == 1; + if (!any_magic) { + out->kind = PAT_STRING; + out->str = src.body; + return true; + } + out->kind = PAT_REGEX; + if (fast != FAST_NONE) { + out->fast = fast; + out->ext = x->nocase && ext.n ? s_lower(x->a, ext) : ext; + out->qlen = qlen; + return true; + } + B b; + b_init(&b, x->a); + b_unit(&b, '^'); + b_s(&b, src.re); + b_unit(&b, '$'); + ScrStr *source = s_to_str(b_done(&b)); + char flags[3]; + size_t nf = 0; + if (x->nocase) flags[nf++] = 'i'; + if (src.uflag) flags[nf++] = 'u'; + ScrStr *flag_str = scr_str_new(flags, nf); + ScrRegex *re = scr_regex_new(source, flag_str); + scr_str_release(source); + scr_str_release(flag_str); + if (scr_exc_pending()) return false; + Arena *a = x->a; + if (a->nregex == a->capregex) { + a->capregex = a->capregex ? a->capregex * 2 : 8; + a->regexes = realloc(a->regexes, a->capregex * sizeof *a->regexes); + if (!a->regexes) scr_trap("scriptc: out of memory\n"); + } + a->regexes[a->nregex++] = re; + out->regex = re; + return true; +} + +static bool starts_dot(S f) { return f.n && f.p[0] == '.'; } +static bool is_dot_or_dotdot(S f) { return s_eq_c(f, ".") || s_eq_c(f, ".."); } + +static bool pat_test(Ctx *x, const Pat *p, S f) { + switch (p->fast) { + case FAST_STAR: return f.n != 0 && !starts_dot(f); + case FAST_STAR_EXT: + return !starts_dot(f) && s_ends(x->nocase ? s_lower(x->a, f) : f, p->ext); + case FAST_QMARKS: { + bool noext = f.n == p->qlen && !starts_dot(f); + if (!p->ext.n) return noext; + return noext && s_ends(x->nocase ? s_lower(x->a, f) : f, p->ext); + } + case FAST_STAR_DOT_STAR: return !starts_dot(f) && s_has_unit(f, '.'); + case FAST_DOT_STAR: return !is_dot_or_dotdot(f) && starts_dot(f); + case FAST_NONE: break; + } + ScrStr *str = s_to_str(f); + bool hit = scr_regex_test(p->regex, str); + scr_str_release(str); + return hit; +} + +/* ── pattern rewrites (optimizationLevel 2) ──────────────────────────── */ + +typedef struct { + SV *v; + size_t n, cap; +} SVV; + +static void svv_push(Arena *a, SVV *vv, SV sv) { + if (vv->n == vv->cap) { + size_t cap = vv->cap ? vv->cap * 2 : 8; + SV *v = ar_alloc(a, cap * sizeof *v); + if (vv->n) memcpy(v, vv->v, vv->n * sizeof *v); + vv->v = v; + vv->cap = cap; + } + vv->v[vv->n++] = sv; +} + +/* Remove "." and "" portions inside a path (Minimatch's shared loop). */ +static bool drop_empty_and_dot(Arena *a, SV *parts) { + bool did = false; + for (size_t i = 1; i + 1 < parts->n; i++) { + S p = parts->v[i]; + if (i == 1 && !p.n && !parts->v[0].n) continue; + if (s_eq_c(p, ".") || !p.n) { + did = true; + sv_splice(a, parts, i, 1, NULL, 0); + i--; + } + } + if (parts->n == 2 && s_eq_c(parts->v[0], ".") && (s_eq_c(parts->v[1], ".") || !parts->v[1].n)) { + did = true; + parts->n--; + } + return did; +} + +static bool is_reducible(S p) { + return p.n && !s_eq_c(p, ".") && !s_eq_c(p, "..") && !s_eq_c(p, "**"); +} + +static void first_phase(Arena *a, SVV *glob_parts) { + bool did; + do { + did = false; + for (size_t g = 0; g < glob_parts->n; g++) { + SV *parts = &glob_parts->v[g]; + long gs = -1; + while ((gs = sv_index_c(parts, "**", gs + 1)) != -1) { + size_t gss = (size_t)gs; + while (gss + 1 < parts->n && s_eq_c(parts->v[gss + 1], "**")) gss++; + if (gss > (size_t)gs) sv_splice(a, parts, (size_t)gs + 1, gss - (size_t)gs, NULL, 0); + if (!((size_t)gs + 1 < parts->n && s_eq_c(parts->v[gs + 1], ".."))) continue; + bool have_p = (size_t)gs + 2 < parts->n, have_p2 = (size_t)gs + 3 < parts->n; + S p = have_p ? parts->v[gs + 2] : S_EMPTY; + S p2 = have_p2 ? parts->v[gs + 3] : S_EMPTY; + if (!have_p || !p.n || s_eq_c(p, ".") || s_eq_c(p, "..") || !have_p2 || !p2.n || + s_eq_c(p2, ".") || s_eq_c(p2, "..")) + continue; + did = true; + sv_splice(a, parts, (size_t)gs, 1, NULL, 0); + SV other = sv_copy(a, parts); + other.v[gs] = s_c(a, "**"); + svv_push(a, glob_parts, other); + parts = &glob_parts->v[g]; + gs--; + } + if (drop_empty_and_dot(a, parts)) did = true; + long dd = 0; + while ((dd = sv_index_c(parts, "..", dd + 1)) != -1) { + /* parts[-1] is undefined: not reducible. */ + if (dd == 0) continue; + S p = parts->v[dd - 1]; + if (is_reducible(p)) { + did = true; + bool need_dot = dd == 1 && (size_t)dd + 1 < parts->n && s_eq_c(parts->v[dd + 1], "**"); + S dotp = s_c(a, "."); + sv_splice(a, parts, (size_t)dd - 1, 2, need_dot ? &dotp : NULL, need_dot ? 1 : 0); + if (parts->n == 0) sv_push(a, parts, S_EMPTY); + dd -= 2; + } + } + } + } while (did); +} + +/* partsMatch: the merged pattern, or false (*ok = false). */ +static SV parts_match(Arena *a, const SV *pa, const SV *pb, bool *ok) { + SV result = { NULL, 0, 0 }; + size_t ai = 0, bi = 0; + char which = 0; + *ok = false; + while (ai < pa->n && bi < pb->n) { + S x = pa->v[ai], y = pb->v[bi]; + if (s_eq(x, y)) { + sv_push(a, &result, which == 'b' ? y : x); + ai++; + bi++; + } else if (s_eq_c(x, "**") && ai + 1 < pa->n && s_eq(y, pa->v[ai + 1])) { + sv_push(a, &result, x); + ai++; + } else if (s_eq_c(y, "**") && bi + 1 < pb->n && s_eq(x, pb->v[bi + 1])) { + sv_push(a, &result, y); + bi++; + } else if (s_eq_c(x, "*") && y.n && !starts_dot(y) && !s_eq_c(y, "**")) { + if (which == 'b') return result; + which = 'a'; + sv_push(a, &result, x); + ai++; + bi++; + } else if (s_eq_c(y, "*") && x.n && !starts_dot(x) && !s_eq_c(x, "**")) { + if (which == 'a') return result; + which = 'b'; + sv_push(a, &result, y); + ai++; + bi++; + } else { + return result; + } + } + *ok = pa->n == pb->n; + return result; +} + +static void second_phase(Arena *a, SVV *glob_parts) { + for (size_t i = 0; i + 1 < glob_parts->n; i++) { + for (size_t j = i + 1; j < glob_parts->n; j++) { + bool ok; + SV matched = parts_match(a, &glob_parts->v[i], &glob_parts->v[j], &ok); + if (ok) { + glob_parts->v[i].n = 0; + glob_parts->v[j] = matched; + break; + } + } + } + size_t w = 0; + for (size_t i = 0; i < glob_parts->n; i++) + if (glob_parts->v[i].n) glob_parts->v[w++] = glob_parts->v[i]; + glob_parts->n = w; +} + +/* levelTwoFileOptimize: rewrites `parts` in place (the original splices + * the caller's array, so later set entries see the result) and answers + * the file to match, a fresh [""] when nothing is left. */ +static SV level_two_file_optimize(Arena *a, SV *parts) { + bool did; + do { + did = drop_empty_and_dot(a, parts); + long dd = 0; + while ((dd = sv_index_c(parts, "..", dd + 1)) != -1) { + /* parts[-1] is undefined: not reducible. */ + if (dd == 0) continue; + S p = parts->v[dd - 1]; + if (is_reducible(p)) { + did = true; + sv_splice(a, parts, (size_t)dd - 1, 2, NULL, 0); + dd -= 2; + } + } + } while (did); + if (parts->n) return *parts; + SV empty = { NULL, 0, 0 }; + sv_push(a, &empty, S_EMPTY); + return empty; +} + +/* Minimatch.slashSplit (preserveMultipleSlashes is off). */ +static SV slash_split(Arena *a, S p, bool windows) { + SV out = { NULL, 0, 0 }; + if (windows && p.n >= 3 && p.p[0] == '/' && p.p[1] == '/' && p.p[2] != '/') sv_push(a, &out, S_EMPTY); + size_t start = 0, i = 0; + while (i <= p.n) { + if (i == p.n) { + sv_push(a, &out, s_slice(p, start, i)); + break; + } + if (p.p[i] == '/') { + sv_push(a, &out, s_slice(p, start, i)); + while (i < p.n && p.p[i] == '/') i++; + start = i; + continue; + } + i++; + } + return out; +} + +/* ── matching (Minimatch.match / matchOne) ───────────────────────────── */ + +typedef struct { + Pat *v; + size_t n; +} PatSlice; + +static bool is_drive(S s) { + return s.n == 2 && s.p[1] == ':' && ((s.p[0] >= 'a' && s.p[0] <= 'z') || (s.p[0] >= 'A' && s.p[0] <= 'Z')); +} + +static uint16_t lower_ascii(uint16_t c) { return c >= 'A' && c <= 'Z' ? (uint16_t)(c + 32) : c; } + +/* #matchOne: 1 hit, 0 miss. */ +static bool match_one_plain(Ctx *x, const SV *file, PatSlice pattern, size_t fi, size_t pi) { + size_t fl = file->n, pl = pattern.n; + for (; fi < fl && pi < pl; fi++, pi++) { + const Pat *p = &pattern.v[pi]; + S f = file->v[fi]; + if (p->kind == PAT_GLOBSTAR) return false; + bool hit = p->kind == PAT_STRING ? s_eq(f, p->str) : pat_test(x, p, f); + if (!hit) return false; + } + if (fi == fl && pi == pl) return true; + if (fi == fl) return false; /* partial is off */ + return fi == fl - 1 && !file->v[fi].n; +} + +static bool dotty(S f) { return is_dot_or_dotdot(f) || starts_dot(f); } + +/* #matchGlobStarBodySections: 1 true, 0 false, -1 null. */ +typedef struct { + PatSlice body; + long after; +} BodySeg; + +static int match_body_sections(Ctx *x, const SV *file, BodySeg *segs, size_t nsegs, size_t fi, + size_t bi, int depth, bool saw_tail) { + if (bi == nsegs) { + for (size_t i = fi; i < file->n; i++) { + saw_tail = true; + if (dotty(file->v[i])) return 0; + } + return saw_tail ? 1 : 0; + } + BodySeg *bs = &segs[bi]; + while ((long)fi <= bs->after) { + SV head = { file->v, fi + bs->body.n <= file->n ? fi + bs->body.n : file->n, 0 }; + bool m = match_one_plain(x, &head, bs->body, fi, 0); + if (m && depth < 200) { + int sub = match_body_sections(x, file, segs, nsegs, fi + bs->body.n, bi + 1, depth + 1, saw_tail); + if (sub != 0) return sub; + } + if (fi < file->n && dotty(file->v[fi])) return 0; + if (fi >= file->n) { + /* file[fileIndex] is undefined: String(undefined) never starts with ".". */ + } + fi++; + } + return -1; +} + +static bool match_globstar(Ctx *x, const SV *file, PatSlice pattern, size_t fi, size_t pi) { + size_t firstgs = pi, lastgs = 0; + while (firstgs < pattern.n && pattern.v[firstgs].kind != PAT_GLOBSTAR) firstgs++; + for (size_t i = 0; i < pattern.n; i++) + if (pattern.v[i].kind == PAT_GLOBSTAR) lastgs = i; + PatSlice head = { pattern.v + pi, firstgs - pi }; + PatSlice body = { pattern.v + firstgs + 1, lastgs > firstgs ? lastgs - firstgs - 1 : 0 }; + PatSlice tail = { pattern.v + lastgs + 1, pattern.n - lastgs - 1 }; + if (head.n) { + SV file_head = { file->v + fi, fi + head.n <= file->n ? head.n : (fi < file->n ? file->n - fi : 0), 0 }; + if (!match_one_plain(x, &file_head, head, 0, 0)) return false; + fi += head.n; + } + size_t tail_match = 0; + if (tail.n) { + if (tail.n + fi > file->n) return false; + size_t tail_start = file->n - tail.n; + if (match_one_plain(x, file, tail, tail_start, 0)) { + tail_match = tail.n; + } else { + if (file->v[file->n - 1].n || fi + tail.n == file->n) return false; + tail_start--; + if (!match_one_plain(x, file, tail, tail_start, 0)) return false; + tail_match = tail.n + 1; + } + } + if (!body.n) { + bool saw_some = tail_match != 0; + for (size_t i = fi; i + tail_match < file->n; i++) { + saw_some = true; + if (dotty(file->v[i])) return false; + } + return saw_some; + } + BodySeg *segs = ar_alloc(x->a, (body.n + 1) * sizeof *segs); + size_t *sums = ar_alloc(x->a, (body.n + 2) * sizeof *sums); + size_t nsegs = 1, nsums = 1, non_gs = 0; + segs[0].body.v = body.v; + segs[0].body.n = 0; + sums[0] = 0; + for (size_t k = 0; k < body.n; k++) { + if (body.v[k].kind == PAT_GLOBSTAR) { + sums[nsums++] = non_gs; + segs[nsegs].body.v = body.v + k + 1; + segs[nsegs].body.n = 0; + nsegs++; + } else { + segs[nsegs - 1].body.n++; + non_gs++; + } + } + long file_length = (long)(file->n - tail_match); + size_t si = nsums - 1; + for (size_t k = 0; k < nsegs; k++) { + segs[k].after = file_length - (long)(sums[si] + segs[k].body.n); + si--; + } + return match_body_sections(x, file, segs, nsegs, fi, 0, 0, tail_match != 0) == 1; +} + +static bool match_pattern(Ctx *x, SV *ff, PatSlice pattern, bool windows) { + SV file = *ff; + size_t fi = 0, pi = 0; + if (windows) { + bool file_drive = file.n > 0 && is_drive(file.v[0]); + bool file_unc = !file_drive && file.n > 3 && !file.v[0].n && !file.v[1].n && s_eq_c(file.v[2], "?") && + is_drive(file.v[3]); + bool pat_drive = pattern.n > 0 && pattern.v[0].kind == PAT_STRING && is_drive(pattern.v[0].str); + bool pat_unc = !pat_drive && pattern.n > 3 && pattern.v[0].kind == PAT_STRING && !pattern.v[0].str.n && + pattern.v[1].kind == PAT_STRING && !pattern.v[1].str.n && + pattern.v[2].kind == PAT_STRING && s_eq_c(pattern.v[2].str, "?") && + pattern.v[3].kind == PAT_STRING && is_drive(pattern.v[3].str); + long fdi = file_unc ? 3 : file_drive ? 0 : -1; + long pdi = pat_unc ? 3 : pat_drive ? 0 : -1; + if (fdi >= 0 && pdi >= 0) { + S fd = file.v[fdi], pd = pattern.v[pdi].str; + if (lower_ascii(fd.p[0]) == lower_ascii(pd.p[0])) { + pattern.v[pdi].str = fd; + pi = (size_t)pdi; + fi = (size_t)fdi; + } + } + } + file = level_two_file_optimize(x->a, ff); + for (size_t k = 0; k < pattern.n; k++) + if (pattern.v[k].kind == PAT_GLOBSTAR) return match_globstar(x, &file, pattern, fi, pi); + return match_one_plain(x, &file, pattern, fi, pi); +} + +/* The whole minimatch(path, pattern, options) call. False with an armed + * exception on failure (*threw set). */ +static bool glob_match(Arena *a, S path, S pattern, bool windows, bool nocase, bool *threw) { + *threw = false; + if (pattern.n > 65536) { + *threw = true; + return throw_too_long(); + } + Ctx x = { a, nocase }; + /* windowsPathsNoEscape: backslashes are separators, never escapes. */ + B nb; + b_init(&nb, a); + for (size_t i = 0; i < pattern.n; i++) b_unit(&nb, pattern.p[i] == '\\' ? '/' : pattern.p[i]); + S pat = b_done(&nb); + if (!pat.n) return path.n == 0; + bool failed = false; + SV expanded = brace_expand(a, pat, &failed); + if (failed) { + *threw = true; + return false; + } + /* [...new Set(...)] */ + SV glob_set = { NULL, 0, 0 }; + for (size_t i = 0; i < expanded.n; i++) { + bool seen = false; + for (size_t k = 0; k < glob_set.n && !seen; k++) seen = s_eq(glob_set.v[k], expanded.v[i]); + if (!seen) sv_push(a, &glob_set, expanded.v[i]); + } + SVV glob_parts = { NULL, 0, 0 }; + for (size_t i = 0; i < glob_set.n; i++) svv_push(a, &glob_parts, slash_split(a, glob_set.v[i], windows)); + first_phase(a, &glob_parts); + second_phase(a, &glob_parts); + bool no_magic_root = windows && nocase; + PatV *set = ar_alloc(a, (glob_parts.n ? glob_parts.n : 1) * sizeof *set); + for (size_t g = 0; g < glob_parts.n; g++) { + SV *s = &glob_parts.v[g]; + PatV pv = { NULL, 0, 0 }; + size_t literal = 0; + if (no_magic_root) { + /* A missing portion reads as undefined, which globMagic tests as the + * text "undefined": no magic. */ + bool unc = s->n >= 2 && !s->v[0].n && !s->v[1].n; + if (unc) { + S s2 = s->n > 2 ? s->v[2] : S_EMPTY, s3 = s->n > 3 ? s->v[3] : S_EMPTY; + /* globMagic: /[?*]|[+@!]\(.*?\)|\[|\]/ */ + bool magic2 = false, magic3 = false; + for (int which = 0; which < 2; which++) { + S t = which ? s3 : s2; + bool m = false; + for (size_t i = 0; i < t.n && !m; i++) { + uint16_t c = t.p[i]; + if (c == '?' || c == '*' || c == '[' || c == ']') m = true; + else if ((c == '+' || c == '@' || c == '!') && i + 1 < t.n && t.p[i + 1] == '(') { + for (size_t k = i + 2; k < t.n && !is_line_terminator(t.p[k]); k++) + if (t.p[k] == ')') { + m = true; + break; + } + } + } + if (which) magic3 = m; + else magic2 = m; + } + unc = (s_eq_c(s2, "?") || !magic2) && !magic3; + } + if (unc) literal = s->n < 4 ? s->n : 4; + else if (s->n && s->v[0].n >= 2 && s->v[0].p[1] == ':' && + ((s->v[0].p[0] >= 'a' && s->v[0].p[0] <= 'z') || (s->v[0].p[0] >= 'A' && s->v[0].p[0] <= 'Z'))) + literal = 1; + } + for (size_t k = 0; k < s->n; k++) { + Pat p; + if (k < literal) { + memset(&p, 0, sizeof p); + p.kind = PAT_STRING; + p.str = s->v[k]; + } else if (!parse_segment(&x, s->v[k], &p)) { + *threw = true; + return false; + } + patv_push(a, &pv, p); + } + set[g] = pv; + } + if (windows) { + for (size_t g = 0; g < glob_parts.n; g++) { + PatV *p = &set[g]; + if (p->n > 3 && p->v[0].kind == PAT_STRING && !p->v[0].str.n && p->v[1].kind == PAT_STRING && + !p->v[1].str.n && glob_parts.v[g].n > 2 && s_eq_c(glob_parts.v[g].v[2], "?") && + p->v[3].kind == PAT_STRING && is_drive(p->v[3].str)) { + memset(&p->v[2], 0, sizeof p->v[2]); + p->v[2].kind = PAT_STRING; + p->v[2].str = s_c(a, "?"); + } + } + } + /* match(f) */ + B fb; + b_init(&fb, a); + for (size_t i = 0; i < path.n; i++) b_unit(&fb, windows && path.p[i] == '\\' ? '/' : path.p[i]); + SV ff = slash_split(a, b_done(&fb), windows); + for (size_t g = 0; g < glob_parts.n; g++) { + PatSlice pattern = { set[g].v, set[g].n }; + if (match_pattern(&x, &ff, pattern, windows)) return true; + if (scr_exc_pending()) { + *threw = true; + return false; + } + } + return false; +} + +/* ── entry points ────────────────────────────────────────────────────── */ + +static bool scr_glob_host_nocase(void) { +#if defined(__APPLE__) || defined(_WIN32) + return true; +#else + return false; +#endif +} + +bool scr_glob_match(ScrStr *path, ScrStr *pattern, bool windows, bool nocase) { + Arena a = { NULL, NULL, 0, 0 }; + S p = s_from_str(&a, path); + S g = s_from_str(&a, pattern); + bool threw; + bool hit = glob_match(&a, p, g, windows, nocase, &threw); + ar_free(&a); + return hit && !threw; +} + +bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern) { + return scr_glob_match(path, pattern, false, scr_glob_host_nocase()); +} + +bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern) { + return scr_glob_match(path, pattern, true, scr_glob_host_nocase()); +} diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index a34562e75..0c55b507d 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4206,6 +4206,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 diff --git a/packages/runtime/test/gen-glob-cases.mjs b/packages/runtime/test/gen-glob-cases.mjs new file mode 100644 index 000000000..97341524a --- /dev/null +++ b/packages/runtime/test/gen-glob-cases.mjs @@ -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 adds a path to the curated list +// c a pattern against every curated path +// f 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"); diff --git a/packages/runtime/test/glob-cases.txt b/packages/runtime/test/glob-cases.txt new file mode 100644 index 000000000..542701503 --- /dev/null +++ b/packages/runtime/test/glob-cases.txt @@ -0,0 +1,3316 @@ +p +p . +p .. +p / +p a +p A +p ab +p a.b +p a.js +p A.JS +p .a +p .js +p a/b +p a/b/c +p a/b/c/d +p a/.b +p a/.b/c +p a/../b +p ./a +p ../a +p a//b +p a/b/ +p /a +p /a/b/c.js +p //a/b +p x/y/.z.ts +p src/lib/index.test.ts +p node_modules/pkg/index.js +p a\u005cb +p C:\u005ca\u005cb +p c:\u005ca +p C:/a/b +p \u005c\u005cserver\u005cshare\u005cx +p //server/share/x +p //?/C:/x +p a b +p a,b +p a{b} +p a(b) +p [a] +p a!b +p a+b +p a*b +p file.tar.gz +p file. +p 1 +p 12 +p a10 +p \u00e9 +p \u00c9 +p \ud83d\ude00 +p a\ud83d\ude00b +p \ud83d +p abc/def/ghi.txt +p +(a) +p \u0009 +p a\u000ab +p aaa +p b +p ba +p a/../../b +p ../../a +c 1100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 1100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c * 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c ** 1100001111111111111100001111110000110000111111111100111111111111111111111111111111111111111111111111111111111111111111110000 1100001111111111111100001111110000110000111111111100111111111111111111111111111111111111111111111111111111111111111111110000 +c *** 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c ? 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 +c ?? 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000011000000110000000000000000110000 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000011000000110000000000000000110000 +c ??? 0000000000000011000000000000000000000000000000000000000011000000000000111100001111111100000000110000000000000000111100000000 0000000000000011000000000000000000000000000000000000000000000000000000111100001111111100000000110000000000000000111100000000 +c a 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c A 0000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a* 0000000011011111110100000000000000000000000000000000000011000000000000111111110011111100000000110000001100000000111100000000 0000000011011111110100000000000000000000000000000000000000000000000000111111110011111100000000110000001100000000111100000000 +c *a 0000000011010000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000001100110000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100110000 +c *a* 0000000011011111110100000000000000000000000000000000000011111100110000111111111111111111000000110000001100001100111100110000 0000000011011111110100000000000000000000000000000000000000000000000000111111111111111111000000110000001100001100111100110000 +c a? 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ?a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000 +c .* 0000000000000000000011110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000011110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *.* 0000000000000011111100000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 0000000000000011111100000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 +c *.js 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *.JS 0000000000000000011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **.js 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/*.js 0000000000000000110100000000000000000000000000110000001100000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000110000001100000000000000000000000000000000000000000000000000000000000000000000 +c **/* 0000000011111111111100001111110000110000111111111100111111111111111111111111111111111111111111111111111111111111111111110000 0000000011111111111100001111110000110000111111111100111111111111111111111111111111111111111111111111111111111111111111110000 +c */* 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011001100000000000000000000000000000000000000000000000000000000000000 +c */** 0000000000000000000000001111110000000000111100000000111100000011000000000000000000000000000000000000000000110000000000000000 0000000000000000000000001111110000000000111100000000111111111111000000000000000000000000000000000000000000110000000000000000 +c **/** 1100001111111111111100001111110000110000111111111100111111111111111111111111111111111111111111111111111111111111111111110000 1100001111111111111100001111110000110000111111111100111111111111111111111111111111111111111111111111111111111111111111110000 +c a/** 0000000000000000000000001111110000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001111110000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c a/**/b 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c a/**/* 0000000000000000000000001111110000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001111110000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c **/a 0000000011000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000011000000000000001100000000000000000000000000000000000000000000000000000000000000 +c a/*/c 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/*/* 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c /a 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 +c /** 0000001100000000000000000000000000000000000011111100000000000000001111000000000000000000000000000000000000000000000000000000 0000001100000000000000000000000000000000000011111100000000000000111111000000000000000000000000000000000000000000000000000000 +c /* 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 +c //a 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/b/ 0000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/b 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c ./a 0000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ../a 0000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/../b 0000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c a/./b 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c a/.b 0000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/.* 0000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c .a 0000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c .a/* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/.b 0000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/.* 0000000000000000000011110000001100000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000011110000001100000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000 +c **/../a 0000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/**/../b 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/**/../b/c 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a,b} 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c {a,b}/c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {,a} 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a,} 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {}a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a{} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a,b{c,d}} 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a..c} 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c {c..a} 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c {1..3} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 +c {01..3} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {3..1} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 +c {1..10..3} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 +c {a..e..2} 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {-2..2} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 +c {a,b,c}{1,2} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c x{a,b}y 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a/b,c} 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c ${a,b} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a,b}} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {{a,b} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a,{b,c}} 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c {a\u005c,b} 0000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c {1..1} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000 +c {A..c} 1100001111110000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 1100001111110000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c {x..z,a} 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [abc] 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c [a-c] 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c [!a] 0000000000100000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 0000000000100000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 +c [^a] 0000000000100000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 0000000000100000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 +c [a-] 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [-a] 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c []] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c []a] 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [!]] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011000000 +c [a-z0-9] 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000001100000000000000000000000011000000 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000001100000000000000000000000011000000 +c [z-a] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [[] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [\u005c]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [.]* 0000000000000000000011110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000011110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [[:alpha:]] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000000000001111000000000000000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000000000001111000000000000000011000000 +c [[:digit:]]* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111000000000000000000000000000000 +c [[:upper:]] 0000000001110000000000000000000000010000000000000000000000000000000000000000000000000000000000000111000000000000000001000000 0000000001110000000000000000000000010000000000000000000000000000000000000000000000000000000000000111000000000000000001000000 +c [[:lower:]] 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000001101000000000000000011000000 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000001101000000000000000011000000 +c [[:alnum:]_] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000000000000000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000000000000000011000000 +c [[:space:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 +c [[:punct:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [[:xdigit:]] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100000000000000000000000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100000000000000000000000011000000 +c [[:word:]] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000000000000000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111000000000000000011000000 +c [[:ascii:]] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100000000000000000011000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100000000000000000011000011000000 +c [![:alpha:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000110011000011000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000110011000011000000000000 +c [[:graph:]] 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111110000000000000011000000 0000000011110000000000000000000000110000000000000000000000000000000000000000000000000000001100001111110000000000000011000000 +c [[:print:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000011000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000011000000000000 +c [[:cntrl:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 +c [[:blank:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 +c [a-[:digit:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [[:bogus:]] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c [\u00e9] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000 +c [\ud83d\ude00] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000 +c [\u00e0-\u00fc] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001101000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001101000000000000000000000000 +c +(a) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c *(a) 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c ?(a) 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c @(a) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !(a) 1100001100101111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001100101111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c !(a|b) 1100001100101111111100000000000000000000000000000000000011111100110000111111111111111111111111111111111111001111111100110000 1100001100101111111100000000000000000000000000000000000000000000000000111111111111111111111111111111111111001111111100110000 +c +(a|b) 0000000011011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 0000000011011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 +c *(a|b)c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a+(b) 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a*(b) 0000000011011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a?(b) 0000000011011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a@(b|c) 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a!(b) 0000000011010011110100000000000000000000000000000000000011000000000000111111110011111100000000110000001100000000111100000000 0000000011010011110100000000000000000000000000000000000000000000000000111111110011111100000000110000001100000000111100000000 +c !(*.js) 1100001111111111001000000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001111111111001000000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c !(.*) 1100001111111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001111111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c *.!(js) 0000000000000011001000000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 0000000000000011001000000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 +c !(a)/b 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/!(b) 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +(a|*.js) 0000000011010000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 0000000011010000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c @(a|b)/** 0000000000000000000000001111110000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001111110000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c !(!(a)) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +(+(a)) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c *(+(a)) 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c ?(*(a)) 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 1100001111010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c @(+(a)) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c +(?(a)) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c !(@(a)) 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c @(!(a)) 1100001100101111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001100101111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c +(*(a)|b) 0000000011011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 0000000011011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 +c *(a|+(b)) 1100001111011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 1100001111011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 +c !() 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c +() 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c @() 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ?() 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *() 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a!() 0000000000001111110100000000000000000000000000000000000011000000000000111111110011111100000000110000001100000000111100000000 0000000000001111110100000000000000000000000000000000000000000000000000111111110011111100000000110000001100000000111100000000 +c !(a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +(a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a) 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c (a) 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +(a)b) 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !(a)* 1100001111110000001000000000000000110000000000000000000000111100110000000000001100000011111111001111110011001111000011110000 1100001111110000001000000000000000110000000000000000000000000000000000000000001100000011111111001111110011001111000011110000 +c *!(a) 1100001111111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001111111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c +(a|) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000 +c @(|a) 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !(|a) 0000000000101111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000000101111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c x/+(a|b)/y 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +([a-c]) 0000000011011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 0000000011011100000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000001111110000 +c !([a-c]) 1100001100101111111100000000000000000000000000000000000011111100110000111111111111111111111111111111111111001111111100110000 1100001100101111111100000000000000000000000000000000000000000000000000111111111111111111111111111111111111001111111100110000 +c @(*.ts|*.js) 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/@(a|b).js 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ?(a|b|c)? 0011000011111100000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011110000 0011000011111100000000000000000000110000000000000000000000000000000000000000000000000000001100001111000011000011000011110000 +c !(foo).js 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !(foo)*.js 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +(!(a)) 1100001100101111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001100101111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c *(!(a)) 1100001100101111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001100101111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c !(+(a)) 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c !(*(a)) 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c !(?(a)) 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c #a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !!a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a\u005cb 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c \u005ca 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a\u005c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c C:/a/* 0000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000110011000000000000000000000000000000000000000000000000000000000000 +c c:/a/* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000110011000000000000000000000000000000000000000000000000000000000000 +c C:\u005ca\u005c* 0000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000110011000000000000000000000000000000000000000000000000000000000000 +c C:/** 0000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000111111000011000000000000000000000000000000000000000000000000000000 +c //server/share/* 0000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000111100000000000000000000000000000000000000000000000000000000 +c //?/C:/* 0000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000001100000011000000000000000000000000000000000000000000000000000000 +c //?/c:/x 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000 +c */share/* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/c 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/*.ts 0000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000 +c src/**/*.ts 0000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000 +c src/**/!(*.test).ts 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/node_modules/** 0000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000 +c **/*.{js,ts} 0000000000000000110100000000000000000000000000110000111100000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000110000111100000000000000000000000000000000000000000000000000000000000000000000 +c **/*.+(js|ts) 0000000000000000110100000000000000000000000000110000111100000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000110000111100000000000000000000000000000000000000000000000000000000000000000000 +c docs/*.md 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *.min.js 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *.js.map 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/index.@(ts|js) 0000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000 +c x/**/z.ts 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c x/*/z.ts 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c x/**/.z.ts 0000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000 +c a/b/**/c/**/d 0000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/b/** 0000000000000000000000000011110000000000001100110000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011110000000000001100110000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/**/b/**/c 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ?/? 0000000000000000000000001100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000001100000000000000111100000000000011000000000000000000000000000000000000000000000000000000000000000000 +c ??/* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000 +c *.? 0000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a.? 0000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a?? 0000000000000011000000000000000000000000000000000000000011000000000000111100000011111100000000110000000000000000111100000000 0000000000000011000000000000000000000000000000000000000000000000000000111100000011111100000000110000000000000000111100000000 +c a/?/c 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c \u00e9 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000 +c \u00c9 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000 +c \u65e5* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c \ud83d\ude00 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000 +c ?\ud83d\ude00? 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000 +c a?b 0000000000000011000000000000000000000000000000000000000011000000000000111100000011111100000000000000000000000000110000000000 0000000000000011000000000000000000000000000000000000000000000000000000111100000011111100000000000000000000000000110000000000 +c a*b*c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a**b 0000000000001111000000000000000000000000000000000000000011000000000000111100000011111100000000000000001100000000110000000000 0000000000001111000000000000000000000000000000000000000000000000000000111100000011111100000000000000001100000000110000000000 +c **a 0000000011010000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000001100110000 0000000011010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100110000 +c a** 0000000011011111110100000000000000000000000000000000000011000000000000111111110011111100000000110000001100000000111100000000 0000000011011111110100000000000000000000000000000000000000000000000000111111110011111100000000110000001100000000111100000000 +c */ 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c **/ 1100001100000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000 1100001100000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000 +c / 0000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c // 0000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c . 0011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c .. 0000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ./* 0000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c ../* 0000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000001100 0000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000001100 +c *.tar.* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000 +c file.* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 +c *. 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000 +c [.] 0011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c \u005c* 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a{b,c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a{b..c} 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c {a,b,c,d,e}{f,g} 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *{.js,.ts} 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000110100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c +(a|b)+(c|d) 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c !(a).!(b) 0000000000000000001000000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 0000000000000000001000000000000000000000000000000000000000000000000000000000000000000011110000000000000000000000000000000000 +c @(a|b|c|d|e|f|g) 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 0000000011010000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000011000000 +c *(?) 1100001111111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001111111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c +(*) 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c !(*) 1100001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 1100001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c @(*) 0000000011111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 0000000011111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c ?(*) 1100001111111111111100000000000000110000000000000000000011111100110000111111111111111111111111111111111111001111111111110000 1100001111111111111100000000000000110000000000000000000000000000000000111111111111111111111111111111111111001111111111110000 +c \u0009 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000 +c a\u000ab 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000 +c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c *\u000a* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000000000 +c [\u000a] 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +c a/../../b 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100 +c ../../* 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011 +c **/../../a 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011 +c a/b/../../../c 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +f 0 {0..1..0} EE EE +f 1\u005c ,,[:alpha:]A^ 00 00 +f 1+cbC: [{a,b}b-[!b]{a,b} 00 00 +f .. {\u005c{:a} 00 00 +f /C:b((A ?(@(b* 00 00 +f (-bbAab }[[:digit:]([ 00 00 +f b221-2 A 00 00 +f C:+(A C:,@(@(?{1..3} 00 00 +f C:b(2 [:alpha:]!+!( 00 00 +f )2.. , 00 00 +f 2+B - 00 00 +f \u00e9 ) 00 00 +f -)\u00e91- {a,b}**(C: 00 00 +f .. )b 00 00 +f /\u005c\u00e9( @[:alpha:],-? 00 00 +f )ab....\u005c ?{1..3}[:alpha:]!- 00 00 +f -c-.. !(B*(,{1..3}^ 00 00 +f (BB2(/ ** 11 11 +f b )aB, 00 00 +f 2 \u00e9[!b] 00 00 +f C:a\u005cB/ab \u00e9.[:digit:]*:?( 00 00 +f ))Aa..C: 1: 00 00 +f ..ca [:alpha:]?*A}{2 00 00 +f C:+ }@-?( 00 00 +f Bc/c 22[!b][[!b] 00 00 +f \u00e9\u00e9Bab+ ,C:..!)*( 00 00 +f -(1+ 1)2^-b?( 00 00 +f abab)\u005cb \u00e9*C:*{a,b}B 00 00 +f + *+( 00 00 +f c\u005c/C: C:2*(..!2[a-c] 00 00 +f A\u00e9 2|/,!B 00 00 +f )1ab @{1..3} 00 00 +f Acab\u005caab !+ 00 00 +f ( +(]C: 00 00 +f .1ab1- [\u00e9 00 00 +f ab)/bC:. [:digit:]!(?2 00 00 +f \u005cC:bab -@[a-c]B{a,b} 00 00 +f .(C: )2+[:alpha:] 00 00 +f /B1B- ([a-c](*( 00 00 +f .. *a\u00e9 00 00 +f (-))\u005c +(a2]. 00 00 +f a [!b],A({1..3}.. 00 00 +f \u005cB\u005c 1]+([!b]*(| 00 00 +f C:...1A. c 00 00 +f B-.. @..@( 00 00 +f Bb {1..3}c{.. 00 00 +f bab ],-: 00 00 +f (-)1 !(2\u005c* 00 00 +f ) ) 11 11 +f a ? 11 11 +f .. cC:..A 00 00 +f .c , 00 00 +f C:-1/(b -[a-c]a[!b][!b] 00 00 +f \u005cbcca ++!!(}{[:digit:] 00 00 +f A+ AA([2 00 00 +f -..BBb) \u005c[!b]+/BA] 00 00 +f .c( ^-\u005c 00 00 +f .C:+ [:alpha:]a..1!c 00 00 +f +\u00e9C:(( )?@..\u005c{ 00 00 +f /c\u005c !2}..} 00 00 +f /-/+.. :[A!@! 00 00 +f ab2((( ^)aca} 00 00 +f C: |*(!-B{1..3}\u00e9 00 00 +f 2abaA2. !2+c 00 00 +f c | 00 00 +f 1C: .:!(!(?(\u005c!( 00 00 +f b {{1..3}@! 00 00 +f \u00e9 }@1]B 00 00 +f \u00e9\u005c ]([a-c] 00 00 +f )-1-\u00e9 {1..3}2+(}A 00 00 +f ..B2...( ** 00 00 +f ab 1?( 00 00 +f b/\u005c [!b]**b:?( 00 00 +f /\u00e9..(/ {1..3}@([!b]:[ 00 00 +f +\u00e9\u005cab a?( 00 00 +f 2+1 ^ 00 00 +f b/ca c*a,*( 00 00 +f -A-)2 [:alpha:]A{a,b} 00 00 +f c !()?(} 00 00 +f C:\u00e9A.b +(B?(1 00 00 +f ba/ ,*([a-c] 00 00 +f ..aA [!b]C:/\u00e9 00 00 +f 1AC:)-.. [:alpha:] 00 00 +f + \u005c 00 00 +f A1 [:digit:]@(,[:digit:] 00 00 +f b( !(2c1!( 00 00 +f ...a.. b}b@?(\u00e9} 00 00 +f ..2-/c.. 2*C:(\u00e9 00 00 +f A\u005cC:. +*(b 00 00 +f Bc\u00e9(c\u00e9 {a,b}.\u005c|A!( 00 00 +f \u005c1b )A*(:]){a,b} 00 00 +f (/2ab+ [!b]A\u005c|*(B 00 00 +f \u00e9 *{a,b}[a-c] 00 00 +f ..+baa { 00 00 +f a. [!b](ca\u00e9 00 00 +f 1 +A*( 00 00 +f + \u00e9! 00 00 +f ) [!b]| 00 00 +f +(..-1a c 00 00 +f cB/. .|] 00 00 +f b ).*(..{1..3}- 00 00 +f abAA/\u00e9 [a-c][!b] 00 00 +f +)b .!*}{a,b} 00 00 +f ..BA2A -] 00 00 +f -cAaBB c2[:alpha:] 00 00 +f C:.\u005c {a,b}[*..1 00 00 +f b\u00e9b B[:alpha:]{ 00 00 +f .(-\u00e9a @( 00 00 +f C:baA @*(+([a-c][!b]-! 00 00 +f C: {],.[!b] 00 00 +f 1\u00e9A1 {a,b}+(* 00 00 +f + ..[!b] 00 00 +f 1A [*\u00e9 00 00 +f .A\u005cab [a-c]B 00 00 +f \u005cA [a-c]\u005cA..[ 00 00 +f \u005caaBaB @\u00e9}@AB 00 00 +f cabBc \u005c{1..3}*/\u005c 00 00 +f /b..bC: \u00e9) 00 00 +f 2 ?/|\u005c..[:digit:]+( 00 00 +f +c1...\u00e9 \u005c))** 00 00 +f \u00e9C:B)( }2{a,b}!(} 00 00 +f (\u00e9 ) 00 00 +f .1baC:b \u00e9[:alpha:] 00 00 +f C:caba1a A^2} 00 00 +f +.. ]-? 00 00 +f bb+/c +!cB+(A 00 00 +f ((ab }{1..3}++( 00 00 +f . c[!b]!([a-c]- 00 00 +f +c.B C:@*(*( 00 00 +f aab+ \u005cc 00 00 +f (B : 00 00 +f abA1b1 C:!(,!([:digit:]{a,b}[ 00 00 +f )B2bA a 00 00 +f c @(( 00 00 +f b/+a /@(\u005c{ 00 00 +f ab @(?*() 00 00 +f ..a\u00e9\u00e9a 2 00 00 +f )Baabc /+(}\u005c[a-c] 00 00 +f )c(+A {a,b}]/@(*+ 00 00 +f ab+\u005cB/ 1@{a,b}C: 00 00 +f \u00e9aba-1 }{a,b}+^[!b] 00 00 +f \u005c+b2 2@[a-c]*] 00 00 +f A ?(a 00 00 +f 22 Bb:![ 00 00 +f ..B.C:1 !((\u00e9 00 00 +f .\u005c(A./ !!()@(?( 00 00 +f (cC: [:digit:]C:[:alpha:]. 00 00 +f - .+@( 00 00 +f . ^A)2 00 00 +f a-/(-C: {1..3}@(a+(\u00e9] 00 00 +f A .a?(!]) 00 00 +f (\u00e9ab- .{a,b}-a-!() 00 00 +f 2/C: {C:{a,b}\u00e9{a,b}@( 00 00 +f (..+A2\u005c [!b]{A!( 00 00 +f 2ABb+ bB 00 00 +f \u00e9.+.\u00e9 :+(c** 00 00 +f 1 {1..3}(c!(1B 00 00 +f ab(b-\u005c {1..3}|{.[!b]| 00 00 +f b/ - 00 00 +f )cc , 00 00 +f /.C:. *1/\u005c(/ 00 00 +f .\u00e9./+ !(@A} 00 00 +f ab1-\u005c.- :?(aab\u005c?( 00 00 +f ..c +( 00 00 +f ../ [{1..3}^ 00 00 +f C:A( [:digit:]]) 00 00 +f A\u005cb1 @[a-c] 00 00 +f +..abC:b [!b]+({1..3}[!b]} 00 00 +f B.. {..! 00 00 +f b/\u005cA cC:[!b],[[:alpha:] 00 00 +f 2)-)aA /!A-@[[a-c] 00 00 +f ) ..[!b][B 00 00 +f - *[!b]{c*({a,b}\u00e9 00 00 +f /bC:c) * 00 00 +f b. |{1..3}^\u005c 00 00 +f B- **[A*(a] 00 00 +f /ab2b- !,+1[a-c]**/ 00 00 +f C:1 [a-c][a-c]]( 00 00 +f /.. ) 00 00 +f C: {a,b}@(]],|] 00 00 +f (ab2..- +?BC: 00 00 +f B \u00e9+ 00 00 +f Aabab. !()[:digit:]!({1..3} 00 00 +f )abC: [:digit:],A{a,b}{ 00 00 +f 12C:ab [a-c]. 00 00 +f ./AA. @acba.. 00 00 +f cb {1..3}+C:B\u00e9 00 00 +f B.ab\u005c1 ^ 00 00 +f ..2 ^[b{1..3}{a,b} 00 00 +f .. * 00 00 +f -C:-bB B. 00 00 +f ..2/- .)@([:digit:][:digit:] 00 00 +f B1B/ @([- 00 00 +f c.. +(..@@c[* 00 00 +f 212 +?( 00 00 +f )ABC:(. C:1+([:digit:]*(** 00 00 +f b\u005c(2ab }@(b1 00 00 +f C:B/ccc 1[a-c] 00 00 +f c2 @[:digit:]@( 00 00 +f C:/BC:C:- -?( 00 00 +f ab(-+ [:digit:]/ 00 00 +f bca11 !(..@ 00 00 +f - [-@({a,b} 00 00 +f a1c **B/ 00 00 +f -bc(- *(*(( 00 00 +f A ]**@(.. 00 00 +f -(1. ,A 00 00 +f cab ..a*( 00 00 +f /- {a,b}**..@(-*( 00 00 +f a2b ?) 00 00 +f b !(*({a,b}\u005c 00 00 +f /../)A.. ccc. 00 00 +f 1\u00e9)a1.. **[a-c] 00 00 +f ab *(( 00 00 +f +1. [a-c] 00 00 +f b..Bab\u00e9b [!b]! 00 00 +f /aa-(- ^{a,b} 00 00 +f 2cb(C: C:{ 00 00 +f ( +(?.. 00 00 +f A \u00e9\u00e9@ 00 00 +f \u005cc ?(|]* 00 00 +f 1 c 00 00 +f A [:digit:][:digit:]!( 00 00 +f c\u005c.1- \u005c} 00 00 +f +. ) 00 00 +f a...\u00e9C:2 b-{a,b}[..1 00 00 +f .aa/) @((@}?** 00 00 +f \u00e9 ! 00 00 +f +\u00e9 C:)+,[:digit:]\u00e9 00 00 +f )..b ,\u00e9(!( 00 00 +f /.Bab\u00e9c C:)C:B@( 00 00 +f AabbBC: [*( 00 00 +f C:)ab -a!2{1..3} 00 00 +f 2b+++\u005c 2 00 00 +f ..Bab- [:alpha:] 00 00 +f cbC:ab [:alpha:]a2 00 00 +f )\u00e9c/+ [!b]@![a-c][:digit:] 00 00 +f A.- \u005c]*(: 00 00 +f ) / 00 00 +f .\u005c....ab \u00e9!- 00 00 +f ( B){a,b}c,[!b] 00 00 +f ab-...\u00e9.. [!b]}+(a 00 00 +f -/ | 00 00 +f ab..B-A ?(/!\u00e9|.** 00 00 +f 2 [!b]1[a-c]?({?[ 00 00 +f (2a ?[ 00 00 +f abC:a.a ,/+*[{ 00 00 +f ab)1. [:digit:]!( 00 00 +f bAaB ..,*( 00 00 +f aC:.C:) \u005c 00 00 +f /2\u00e9c) **]!( 00 00 +f / **B?[!b] 00 00 +f c1 {@ 00 00 +f -b/A 2}[:digit:]{a,b}B@ 00 00 +f ..\u005c2 ?([a-c]2!+(2B 00 00 +f \u005c2(- [!b]*/*[|/ 00 00 +f 22(2 [a-c]*{a,b}| 00 00 +f /)..1/ab C:[:digit:] 00 00 +f ab ^(.:^\u00e9 00 00 +f \u00e9..C:\u00e9 -*(b 00 00 +f +ab1./ \u00e9 00 00 +f C:b .[:digit:][![a-c] 00 00 +f ..\u005c ../ 00 11 +f 2cbb . 00 00 +f -/..) [a-c]? 00 00 +f \u00e9B/A-\u005c B!{A 00 00 +f )\u005c1/) /{!(,(,] 00 00 +f 2/1A2 B-..^A^ 00 00 +f ab1.1 ..1!( 00 00 +f C: [:digit:] 00 00 +f B1a)2 ^B! 00 00 +f a..- +!] 00 00 +f aabAa \u00e9^\u005cB 00 00 +f A !([:alpha:][a-c]|?\u00e9 00 00 +f +1 [:digit:]@*({a,b} 00 00 +f \u005c ,1.^{1..3}^\u005c 00 00 +f A [!b]]C:|{*: 00 00 +f C: 1[!b]!] 00 00 +f \u005cBac\u005c ..ab:{1..3} 00 00 +f -/ !([!b]\u005c.12+( 00 00 +f A1c)2/ .* 00 00 +f . ,[ 00 00 +f ABabbB ]:[a-c]bb} 00 00 +f C: !(!/1 00 00 +f c(C:. [a-c]c*(a|*(!( 00 00 +f /.. ^/+(1/ 00 00 +f 1\u00e9abbA [:alpha:]?/1\u00e9@( 00 00 +f 1.\u00e9A.. [B+({1..3}1\u005cA 00 00 +f /A1+C: ^[/[:?( 00 00 +f )+a2\u005c+ [:alpha:],C:\u005c@-B 00 00 +f ab **.C:[:digit:]| 00 00 +f a-\u00e9 B[!b] 00 00 +f A/ 2+(c 00 00 +f (( +. 00 00 +f A [?{a+(}^ 00 00 +f -\u00e9+ ?^!**( 00 00 +f \u005c ! 00 00 +f \u005cC:ab..2.. B 00 00 +f ).+(a [:alpha:] 00 00 +f A+2B ^B 00 00 +f .-+ B!( 00 00 +f )A(bAC: )(\u00e9 00 00 +f C:C:ab2\u005c @(@!{1..3}!( 00 00 +f /-ab(- {..?@(!(+ 00 00 +f 1B 1()*]a, 00 00 +f .Bc\u00e9- + 00 00 +f ab2\u005c-b\u00e9 /[:digit:][Aa 00 00 +f ...+( @({2| 00 00 +f )b }[:digit:]]| 00 00 +f ./\u00e9\u00e9 2[|[:digit:] 00 00 +f BB((+ ^A[a-c]]c!( 00 00 +f -B+a ^?!*} 00 00 +f Aabab..abab ^-**,1 00 00 +f + }@(AC:c 00 00 +f 1..C:a1 @(.a@[:digit:]?( 00 00 +f + bb 00 00 +f \u00e9(c.. ,:.\u00e9+( 00 00 +f a/ \u00e9[:digit:]C:(?([[ 00 00 +f C:\u00e9bC:\u005c +(**+(+ 00 00 +f / {a,b})?() 00 00 +f B 11**\u00e9! 00 00 +f /\u00e9ab1\u00e92 +!(\u00e9[:alpha:] 00 00 +f 2abab-\u00e9/ } 00 00 +f -c22 ..**+**[!b] 00 00 +f ab/ab a**|{ 00 00 +f a2-A+( ?\u00e9 00 00 +f ..)B -!{ 00 00 +f B+ac a{1..3}}-[!b]*(+ 00 00 +f +a ?(..[!b]B1| 00 00 +f A1A21b aC:)@ 00 00 +f b(.1ab) b[:alpha:]*- 00 00 +f b \u00e9 00 00 +f bab1Bb B{1..3}! 00 00 +f C:1 ][!b]}.(a 00 00 +f \u00e9B..- [:alpha:][!()?,| 00 00 +f 1.2+/ .?(/]/ 00 00 +f /A ]\u005c}{a,b}^ 00 00 +f \u005cab )@(@ 00 00 +f ..-(bAc {1..3}[:alpha:]**?(+ 00 00 +f (AaA+ \u00e9@? 00 00 +f C:2 A*,*( 00 00 +f )A.. ^!(?{a 00 00 +f AAA/..) (]\u00e9(( 00 00 +f .a2) @\u005c{1..3}!(* 00 00 +f -+\u00e9 b{a,b}[:alpha:][:digit:]?B[:alpha:] 00 00 +f bab.\u005c2- .** 00 00 +f 1.abc {1..3})}/ 00 00 +f ccb+b **?(!({1..3}1{a,b} 00 00 +f /ab1 {1..3}cC: 00 00 +f \u00e9C:Babb [!b] 00 00 +f .--\u005c [a-c] 00 00 +f \u00e9\u00e9.Ab [a-c]?B 00 00 +f +aabab- ^B}[!b]@(b, 00 00 +f \u005c b](^b.. 00 00 +f +\u005cC: 12[!b]( 00 00 +f \u00e9c/c c{[:digit:]^|[!b]! 00 00 +f \u00e92 [a.1^ 00 00 +f 1+C: {a,b}b..( 00 00 +f ......) *^2 00 00 +f B2C:ab..C: !1,2*\u005cc 00 00 +f C:\u005cB(A -@[ 00 00 +f A)C:) ^*C:.{a,b}| 00 00 +f bC: {1..3}@(!1..?( 00 00 +f 1 2..]1a 00 00 +f \u005c\u005cBab [a-c])][a-c] 00 00 +f )\u005c- [:digit:]*+[:alpha:]a{1..3} 00 00 +f (ab- +(@) 00 00 +f ..B+a {1..3}[?-?{ 00 00 +f .)-A/a a[a-c] 00 00 +f +1... A 00 00 +f .\u00e9\u005cA2 ?([!b]a 00 00 +f bA) \u00e9c[a-c](.. 00 00 +f ..1\u00e91 -[:digit:]*2,b{a,b} 00 00 +f C:1-2ab |[:digit:]B]..[!b] 00 00 +f .. c[:digit:]b2:** 00 00 +f b/\u00e9c **@(*Bb|, 00 00 +f Bb\u005c {a,b}}C:[:alpha:]*( 00 00 +f ( \u00e9^+?( 00 00 +f +2Ab\u005c) }** 00 00 +f /aA+a * 00 00 +f +ab2 2 00 00 +f 1\u00e9/-2 [:alpha:] 00 00 +f B)C: ? 00 00 +f bab\u005c+ c 00 00 +f c/C:.. @(,?(B 00 00 +f -\u005c ?(,2 00 00 +f (.)-( ) 00 00 +f A)-c(A \u005c-@(**!@( 00 00 +f a B[a-c] 00 00 +f b/B2) @! 00 00 +f -B-b..1 ..?( 00 00 +f /+( 1+( 00 00 +f ()2 \u00e9^{a,b} 00 00 +f aabb1\u005c\u00e9 c{a,b}}** 00 00 +f b\u00e9 [!b]]**(a 00 00 +f ( **{a,b},!( 00 00 +f babBA.. 2{2{[!b]+ 00 00 +f \u00e9b\u00e91\u005c1 [[!b]\u005cAa 00 00 +f ca+ [**!+( 00 00 +f / [:alpha:]{1..3}!(!{1..3} 00 00 +f a)/C: \u005c*}:{ 00 00 +f \u00e9B2./( c..!(b[[:alpha:] 00 00 +f /(..-/ C:{a,b} 00 00 +f )a2-.C: B[:digit:]{a,b}: 00 00 +f /a |-^* 00 00 +f -c+(C:1 !.C:[:alpha:] 00 00 +f c\u00e9. !(+([:alpha:]?([a-c]|[:alpha:] 00 00 +f AB ?(}{a,b}!:)B 00 00 +f \u00e9 }ba 00 00 +f aba.. a** 11 11 +f +B\u005cC:ab\u005c !..?(A*[!b] 00 00 +f 2)-../ ** 11 11 +f ab1+ {1..3}?C:[a-c]*[:alpha:] 00 00 +f \u005cbC: : 00 00 +f ..(1b [b} 00 00 +f ab1 [:alpha:]| 00 00 +f aB))\u005cb @(. 00 00 +f /C:11) *(.. 00 00 +f ab) ?(cbc 00 00 +f .. ^?( 00 00 +f \u00e9.. Bb?(1?\u005c[:digit:] 00 00 +f 2 1 00 00 +f C:A-1..ab ,{1..3}+**@(b} 00 00 +f ( !C:c|( 00 00 +f B..B *[:alpha:]B!(@ 00 00 +f c2A *(?(b1a{a,b}[a-c] 00 00 +f -.11a |a(+([:digit:]-^ 00 00 +f )c+\u005c\u00e9/ 1(}..|) 00 00 +f C:B !({a,b}C: 00 00 +f a+2)ab a\u00e9\u005c[a-c]{a,b}!({1..3} 00 00 +f Bb\u00e9 c..|?[a-c]@: 00 00 +f C:b [!b]{ 00 00 +f b+ { 00 00 +f 2 ,c 00 00 +f -\u00e92\u005c\u00e9c }2, 00 00 +f ) {@?*(+(A) 00 00 +f c1 )1.a|!(@( 00 00 +f A\u005cC:++- .Bc 00 00 +f +\u00e91 .C:/..: 00 00 +f +- [a-c] 00 00 +f ((+)(2 {a,b}[(@(| 00 00 +f A.A+ ..(1*( 00 00 +f ab)BAB . 00 00 +f -\u005c.C: /+(@(?(:{ 00 00 +f cC:..cb -?C:-[:alpha:]] 00 00 +f -b- ,c{^* 00 00 +f B). ,2]/@+( 00 00 +f c\u005cacB ? 00 00 +f 2B+.1b \u005c?(+(!() 00 00 +f ..\u005c(/1 *,[a-c]\u00e9a*( 00 00 +f abA [!b]AaC:?a 00 00 +f 22/ ) 00 00 +f (. @a?( 00 00 +f ..Ba).1 {a,b}. 00 00 +f b - 00 00 +f + C:[:digit:]**C: 00 00 +f ...A *(C:?(,[!b]A! 00 00 +f - .A 00 00 +f 1)) +(..@((+(:] 00 00 +f +B\u00e9\u00e92 c{a,b}-[:digit:]@(@B 00 00 +f A\u00e91-) [!b][:alpha:]C:!,]) 00 00 +f ((b+ (?[a-c][:alpha:]?( 00 00 +f + !.[:alpha:])a?( 00 00 +f b)+.. A!([b: 00 00 +f BC:B\u005c/ [:alpha:] 00 00 +f -C:C:)A 1!c 00 00 +f \u005c)( [ 00 00 +f (AB\u00e9 .. 00 00 +f /\u005ca [:alpha:][ 00 00 +f .C: !(\u005cB?:! 00 00 +f aC:+- !( 00 00 +f C:(ab- 1 00 00 +f ab\u005c\u00e9+C:b b?!(a 00 00 +f 1-.. {^|^ 00 00 +f B B*!( 00 00 +f \u00e9-c\u005c .@C: 00 00 +f \u00e9ab)/C:C: 2*(,* 00 00 +f ) ]A 00 00 +f Ab- :2C:c 00 00 +f (\u00e9)bA +([!b]/A 00 00 +f 1...cab }Bc 00 00 +f )/\u00e9/ [:alpha:]b@ 00 00 +f 11.. ? 00 00 +f \u00e9\u005c/.aB B{1..3} 00 00 +f )2abbb( .!(. 00 00 +f (21/. ,,{a,b}+({1..3}* 00 00 +f +(ab- +(1^@((2^ 00 00 +f +B [:alpha:] 00 00 +f C:..2\u005c {1..3}@(B*( 00 00 +f (2 ?(b\u005cb\u005c 00 00 +f -( +(/ 00 00 +f .bb\u00e92+ ..?,?(!b?( 00 00 +f 11-\u005c -*,!( 00 00 +f b2 +C:b@: 00 00 +f C:B) BB[!b]\u005c+ 00 00 +f )-\u005c\u00e9 @b?B- 00 00 +f ab..-\u005caab }1a**{[:digit:]: 00 00 +f )ab [:alpha:] 00 00 +f 2.b.AA @(b 00 00 +f Bb-ab./ !}**[*(}: 00 00 +f 1a/\u005c)B (*..+(c+( 00 00 +f ./2 22[!b] 00 00 +f Bab..2.+ +!({[:digit:]A!(!( 00 00 +f (c1cb :@ 00 00 +f BccA *@(/}( 00 00 +f b , 00 00 +f aC: [!b][:alpha:]^]{\u005c 00 00 +f c1a2/ 2]A+(/ 00 00 +f \u00e9-c1A [}^. 00 00 +f Bb++ @(@}[:digit:]!( 00 00 +f ac1-Bb [:alpha:]}^[^- 00 00 +f cc.2B- [:digit:] 00 00 +f ab/-\u005c/ [C:, 00 00 +f \u00e9.\u005c/+\u00e9 @[:digit:]{a,b}{ 00 00 +f BB ^!:[ 00 00 +f 2 +/C:]{a,b}, 00 00 +f \u00e9\u00e9aB .*((@(c1 00 00 +f cBb A[.a* 00 00 +f 1 +:/*( 00 00 +f ..abaC: [:alpha:]1}- 00 00 +f abA)- /[:digit:] 00 00 +f c..C:B.. [a-c] 00 00 +f \u005cB [!b],.B 00 00 +f /\u005c2C:/ bb 00 00 +f (..( [:alpha:][a-c]\u005c 00 00 +f ab 2,**+(!!( 00 00 +f ..c./1. [-@C:{b 00 00 +f B - 00 00 +f AB -,a{a,b}[+([:alpha:] 00 00 +f bc2. {a,b}(C:.-[!b]1 00 00 +f ab\u00e9C:/2.. +( 00 00 +f 1) @(\u00e9{!( 00 00 +f a)/ }- 00 00 +f B) |:,cC:^ 00 00 +f 1ab .[.]a 00 00 +f (\u005c a}?,^@ 00 00 +f (b)\u00e9 bB2?[:alpha:] 00 00 +f -ABab [a-c]@2{a,b}[}- 00 00 +f \u005cb-) BC:C:\u00e9|{a,b}?( 00 00 +f AC:ab- cb{1..3}c]{1..3}. 00 00 +f +A {A. 00 00 +f c./ *(1\u005c?\u00e9-[:alpha:] 00 00 +f ababA [:digit:]{1..3}|C: 00 00 +f B\u00e9C:B- \u005c!@1*(\u00e9*( 00 00 +f -Ac..)- {@(){1..3} 00 00 +f +aba [a-c]{1..3}!\u00e9 00 00 +f ab. ??[!b][!b] 00 00 +f a(+ +(*! 00 00 +f bB. c\u00e9bb|A{ 00 00 +f /\u00e9 | 00 00 +f C:-ab @(/{1..3}2C:2 00 00 +f +A !(,cC:}b 00 00 +f 1Aabb2- {/)]? 00 00 +f abC:A+ / 00 00 +f ..(abC:.... {1..3}2a 00 00 +f 1)(a..( ] 00 00 +f 2aaab\u005cC: @[:alpha:]( 00 00 +f )-). /[!b] 00 00 +f Aa2 [a-c]\u005c[:digit:]**/ 00 00 +f ab.. !]c 00 00 +f 1abB.\u00e9A {2)/, 00 00 +f abC:\u00e9\u005cc +[:alpha:][a-c]c)b 00 00 +f 2c-b \u00e9\u00e9 00 00 +f 1bBB. b 00 00 +f 1ab ***?( 00 00 +f )\u00e9.) [:digit:]{1..3}.*( 00 00 +f +/ *,+(C: 00 00 +f ab \u005c[ 00 00 +f .. \u00e9B{1..3} 00 00 +f cb\u005c\u00e9... [a-c] 00 00 +f . ]+ 00 00 +f B c?]a!(A, 00 00 +f + +{1..3}A/ 00 00 +f (BB *((^{a,b}(+ 00 00 +f c(\u005cA- -:{a,b}bC:?( 00 00 +f 1C:..a)\u00e9 [:digit:] 00 00 +f aba..-+ C:[:alpha:]**,!(B.. 00 00 +f 12(/A {a,b}^-! 00 00 +f 1 ,[:alpha:]A}@(.. 00 00 +f a\u00e9A/c ][:digit:] 00 00 +f ( .,[!b] 00 00 +f aa(bab -\u00e9}\u00e9**]C: 00 00 +f 2B [a-c] 00 00 +f .c12-c b-+ 00 00 +f ..\u005ccC: !([a-c]: 00 00 +f / \u00e9-|:{+( 00 00 +f ) }{a,b}Bc 00 00 +f c- C: 00 00 +f (..-/(.. \u00e9(-?(!(C: 00 00 +f a ?(B@c 00 00 +f b..2C:1 * 11 11 +f 1ab1+B +(.. 00 00 +f 2).2C: {1..3}? 00 00 +f -\u005c ! 00 00 +f 2AAA. *(!(?[a-c]c] 00 00 +f A-+( ..: 00 00 +f -.a- {1!(**+ 00 00 +f ac |{a,b}{\u00e9 00 00 +f +C:C: 2b 00 00 +f /a\u005c.C:\u005c [[a-c][a-c])a\u00e9 00 00 +f )\u00e9-C:Aa [:alpha:]c{.. 00 00 +f \u005c- 1].?( 00 00 +f B2ab2 cC:+b[!b]? 00 00 +f 1C:+).. * 11 11 +f \u005c ?| 00 00 +f B1/+2C: ..|!..{a,b}{1..3}} 00 00 +f +bC: ^ 00 00 +f ..cC:/ :[:digit:]\u005c?[:alpha:]@ 00 00 +f \u005c1+ @([[:digit:][a-c] 00 00 +f C:)a .[a-c]b 00 00 +f C:\u00e9 /) 00 00 +f \u00e91/ * 11 11 +f \u00e9- [:digit:]][[a-c],?(, 00 00 +f ab -,..** 00 00 +f abBbC:( *(@( 00 00 +f 21a-B. +(: 00 00 +f C:( @ 00 00 +f )ab2b. 2{{1..3}a{a,b}{| 00 00 +f b2aC:aB *,\u00e9c 00 00 +f ...aA1ab @( 00 00 +f Bab {?([!b]( 00 00 +f b (..? 00 00 +f A ^+(!( 00 00 +f .2b1 2\u00e9{a,b} 00 00 +f abB\u005c (a@) 00 00 +f )cC: \u005c[!b]\u005cA 00 00 +f )// @([!b]+( 00 00 +f 2). /[[ 00 00 +f ).2a-c 1 00 00 +f b)BbA 1 00 00 +f /a..2 ){?([a-c]{[ 00 00 +f 2 }^!( 00 00 +f . ?[:alpha:]*)@( 00 00 +f + \u005c 00 00 +f ..\u005c (c 00 00 +f ..ab1.- Bcb 00 00 +f C:+.a2 /)-|[:alpha:] 00 00 +f A)a/a 12{a,b}+(\u005c..+( 00 00 +f A\u005c).22 [:digit:]}2a 00 00 +f /+c :[:alpha:] 00 00 +f Aa\u005cB :. 00 00 +f + ,1b|C:( 00 00 +f .. ({ 00 00 +f ..(2(/ a{a,b},[:digit:]A* 00 00 +f -111ab .. 00 00 +f .ab\u00e9AC: A@( 00 00 +f C: (A\u005c?(\u00e9{ 00 00 +f 1 .:b{ 00 00 +f c1+ [:alpha:]@ 00 00 +f /..B1 B*(, 00 00 +f c +(B.Ab@) 00 00 +f -2 [:alpha:]b*([a-c]^,A 00 00 +f 1B/ [a-c] 00 00 +f \u00e9)/ {1..3}]*(*({1..3}\u00e9 00 00 +f ab.aB ,b| 00 00 +f +-/1 \u00e9**\u00e9!2 00 00 +f \u005c.2 1!?A 00 00 +f 2-C:A..( {a,b}-?(@[:digit:]aB 00 00 +f 2\u005c\u005c BB+(!(+( 00 00 +f +\u005c)b **[:alpha:]Bc 00 00 +f C: C:^[!b] 00 00 +f )\u00e91Bc -} 00 00 +f A)C:( C:]] 00 00 +f +b\u005cAA +[!b]..[!b] 00 00 +f ( +([:alpha:]c 00 00 +f ..\u005cAC:B- @ 00 00 +f a+.. a\u005c 00 00 +f / [*([:alpha:] 00 00 +f B\u00e9a ,-C:]B| 00 00 +f ..2AB- ] 00 00 +f .\u00e9 C:+(){2] 00 00 +f B\u005cAABab +*[:digit:][!b]\u00e9 00 00 +f 2B ?([..1?\u005c 00 00 +f /+(a +( 00 00 +f (C:C:\u00e9 .[+ 00 00 +f c B1,**a 00 00 +f (ab/b)2 -/!(C: 00 00 +f 1 \u005c[:alpha:]|!{1..3}A 00 00 +f ..(\u00e92 {[(2-AB 00 00 +f (c C: 00 00 +f 111\u005c+ ..[:alpha:]*|\u005c..+( 00 00 +f )C: @(\u005c(c[!b] 00 00 +f bcab..C:B ** 11 11 +f B\u005c/\u00e9( \u005c*( 00 00 +f ) (}1[:digit:]{ 00 00 +f / ..][:alpha:]|C:**1 00 00 +f abac.. B1.[!,*( 00 00 +f 1\u00e9Ab. [:digit:] 00 00 +f cabb\u00e9 \u00e9[a-c] 00 00 +f C:ab1A 2**{1..3}: 00 00 +f ..A..1 - 00 00 +f ..b |[!-[a-c] 00 00 +f \u005c//(/ {1..3}a@ 00 00 +f c ?[:alpha:]A)[:digit:]A 00 00 +f (+C:(ab \u005c**?+* 00 00 +f .C:()c +[a-c]@|+C: 00 00 +f a ]. 00 00 +f Aab !(*|[:alpha:]@A 00 00 +f 2c ?A**||2\u005c 00 00 +f c--2)B [!b] 00 00 +f Bcb @(.. 00 00 +f ab\u00e9.\u00e9 B?[:digit:].A[a-c] 00 00 +f a..a (/ 00 00 +f aC:C:2- ({)A!{a,b} 00 00 +f 1c..\u005c-+ ,(B?(? 00 00 +f C:---2A @:) 00 00 +f C: [A 00 00 +f B\u00e9a (\u00e9?( 00 00 +f ../ \u005c1?(1[:digit:]!(* 00 00 +f a)\u005c !( 00 00 +f ../ c!(A*(((c 00 00 +f B..(C:\u005c +) 00 00 +f b.-a /.,b? 00 00 +f \u005cb\u00e9/(1 B]+(:c[!b] 00 00 +f abab+) /Bc 00 00 +f -(C:B.. \u00e9 00 00 +f .. @( 00 00 +f ac\u00e9c.. 2|**{[!b]+(** 00 00 +f -1 [!b] 00 00 +f 2/a **/\u005cb@(?(!( 00 00 +f 1a [:alpha:]*!({\u00e9A/ 00 00 +f \u005c-.c ) 00 00 +f \u005c(c A+**!( 00 00 +f ( ^ 00 00 +f A-.c *(!( 00 00 +f /1\u005cab }+!(** 00 00 +f a +(A 00 00 +f c2C:\u00e9)\u00e9 b 00 00 +f C:\u005cb \u00e9!( 00 00 +f ./.2 b** 00 00 +f BB..C: +][a-c]B 00 00 +f 1 \u00e9? 00 00 +f C:1 B[}\u00e9 00 00 +f + -!-{a,b}C:b 00 00 +f Ba.B./ ..{c1C: 00 00 +f aba..)b bB[B 00 00 +f .+c *@)*[!b]| 00 00 +f ab1\u005cccA b*[a-c]]b)^ 00 00 +f ab-..ab [!b]2.. 00 00 +f +ab) :[!b]A 00 00 +f A+1 [!b]+(?( 00 00 +f \u00e9/ ?(b|+ 00 00 +f ABb @C:{1..3} 00 00 +f ( /....}**{ 00 00 +f \u005c22./ { 00 00 +f \u00e9 ) 00 00 +f 1..b..B2 ] 00 00 +f /abc) ({ 00 00 +f 1--c(- ::(^b| 00 00 +f 1-\u005cab A 00 00 +f -( } 00 00 +f AB-B2 ?]{a,b}[:alpha:], 00 00 +f )+.c1B {a,b}a[:alpha:], 00 00 +f .12 a 00 00 +f A2c.- [a-c]b*(?(c 00 00 +f \u005c !( 00 00 +f 1a2c *(.. 00 00 +f A.B(C:) |[]2:[:alpha:]} 00 00 +f \u00e9/..2B) {[:alpha:] 00 00 +f -/a..C: |B|+ 00 00 +f b ,^! 00 00 +f -1.. [^@.{1..3}- 00 00 +f aC:2 ? 00 00 +f -\u00e9ab @|[ 00 00 +f C:1 {1..3}[!b][,[ 00 00 +f a1 {}[:alpha:] 00 00 +f \u005cbbb {a,b}[:alpha:]({a,b}c[:digit:] 00 00 +f )- [!b]|b:*({a,b} 00 00 +f \u005c [a-c]BC:[:digit:]b+(} 00 00 +f ab )?([a-c] 00 00 +f a.. \u00e9]{} 00 00 +f abaB -,c**-c 00 00 +f )C:A\u005cab2 ?( 00 00 +f b\u00e9. 2\u00e9 00 00 +f a(2 ?(!(]1: 00 00 +f .C:/B\u00e9B +(+[!b]*(**[ 00 00 +f \u00e9\u005cC:/ab @([* 00 00 +f bB(a ?(:{{/B 00 00 +f \u005c a[!b] 00 00 +f /..a !(:}2^[a-c] 00 00 +f \u00e9/ \u00e9@**[:alpha:]@!( 00 00 +f ..\u005c1.A ?(/[a-c]|?+( 00 00 +f AB/( }?}.{a,b}?( 00 00 +f 2a [:digit:][)+(\u005c). 00 00 +f c/.. !**/A+[.. 00 00 +f 1( +1 00 00 +f 2( A[:digit:]{[^a 00 00 +f c1.1- A!(B![a-c]+(* 00 00 +f ..(Acbab [..?( 00 00 +f aC: ^^}-[ 00 00 +f \u005c+\u00e92ab {a,b}\u005cC:* 00 00 +f 2 -.. 00 00 +f c\u00e9-\u005c-+ :c[:alpha:][a-c]}2 00 00 +f 1( {[a:-{1..3} 00 00 +f a..AA..) (+!(+@+( 00 00 +f C:2ca ?(a!()[!b]a 00 00 +f B+.C:a A? 00 00 +f A+(- B 00 00 +f // ***,*(] 00 00 +f /+-aa -[:alpha:]!( 00 00 +f \u00e9A/.. {1..3}[:alpha:][:digit:][a-c] 00 00 +f )1b(b\u00e9 12**{1..3}!()** 00 00 +f A. 2}[a-c]1?*( 00 00 +f A(/ /!-a 00 00 +f ) ^?(A[a-c]| 00 00 +f )b ([:digit:]!A) 00 00 +f +.... c[:alpha:][ 00 00 +f \u005cc/ b*+( 00 00 +f ..2. ,( 00 00 +f 2ABb {1..3}..^^, 00 00 +f (A\u005cC: - 00 00 +f 2 !**. 00 00 +f C:1(C:aa \u00e9-c/C:| 00 00 +f cAab/ ***( 00 00 +f aab\u005c aB\u005c1@( 00 00 +f c..B b 00 00 +f -.+1\u005c . 00 00 +f C:(C: A[:alpha:]*(+B.. 00 00 +f A +?.. 00 00 +f c...c1 ** 11 11 +f (( a*!() 00 00 +f C:/ ) 00 00 +f (ab.) [:digit:]+ 00 00 +f 1ab1 A 00 00 +f (\u00e9abB ?(a: 00 00 +f /- @,[a-c]]C: 00 00 +f b\u00e9 2[a-c],a 00 00 +f a) b[a-c]{2*(+ 00 00 +f .1 {1..3}, 00 00 +f ab.\u00e9a\u005cab [!b],)c@( 00 00 +f 1.-ab\u005c ..1\u00e9!([:digit:] 00 00 +f -..((\u00e9B ^{^\u005c 00 00 +f abA (?B+( 00 00 +f 1ababbc2 [!b]a 00 00 +f a {a,b}[:alpha:].c/ 00 00 +f \u005cc ..A2 00 00 +f ab+b..1\u005c :] 00 00 +f \u00e9.)2 @1-!(.. 00 00 +f cab\u00e91C:.. B[:digit:] 00 00 +f ..\u005cC:+ab **,/ 00 00 +f \u00e9 \u005cAc(*(\u00e9a 00 00 +f /12\u00e9\u00e9 {a,b}[:digit:]+!+([- 00 00 +f b.C: }[:digit:] 00 00 +f \u00e9..+ ]+|?( 00 00 +f B\u00e9(c @c{1..3}2!( 00 00 +f 2A+ ^ 00 00 +f \u00e9 [).BC:)! 00 00 +f a+abaab \u00e9,},[:digit:] 00 00 +f 2.Aabab 2[c?^ 00 00 +f a+B(B !2 00 00 +f ab).. )@[a-c]@[!b]*(a 00 00 +f /-\u005c [:digit:]1}@(( 00 00 +f .. / 00 00 +f 2(b ,cbA:**{1..3} 00 00 +f 2\u005c..abC: c}[!b]\u005c@ 00 00 +f C:cbC:b .*@({1..3} 00 00 +f a/(ab12 |^ 00 00 +f / [@}....@( 00 00 +f b2c.\u005c *(*(c 00 00 +f C:A\u00e9/ [a-c]*!| 00 00 +f .\u005c [a-c]/ 00 00 +f )A2.+ *(\u00e9 00 00 +f ..2c-/ ^A@(B 00 00 +f +..ab-\u00e9b **-1 00 00 +f 2a A/ 00 00 +f ( BA:a 00 00 +f c)..c *{1..3}..}??+ 00 00 +f -c .:ca+( 00 00 +f ..c. [:digit:][:alpha:][?1*(c 00 00 +f .2. *2?([ 00 00 +f \u00e92a !()(.] 00 00 +f Bab c:?( 00 00 +f ) B!( 00 00 +f aaba \u005cc 00 00 +f -a)- A?[!b][:digit:])?(.. 00 00 +f -BA( {1..3}+(+ 00 00 +f ...) 1*(|..( 00 00 +f BB2/ ?1\u00e9[:alpha:] 00 00 +f cB.-a +*[:alpha:][**{a,b}{ 00 00 +f Bb\u005c. {1..3}\u00e9(,\u005ca!( 00 00 +f \u005cAa\u00e9. |\u005c@.c}.. 00 00 +f aba..\u00e9/ A{{1..3} 00 00 +f 22C:-B 2!\u005c)a:** 00 00 +f ()..\u00e9 .. 00 00 +f Ab+/ {[!b]? 00 00 +f . ?/!:,\u005c 00 00 +f 2..C:B 1\u00e9))b 00 00 +f -..1 !11|[!b] 00 00 +f B2(\u005c \u005c! 00 00 +f ..// +(,@({1..3}\u005c[:digit:]: 00 00 +f b\u005c 1[!b]/{..@(] 00 00 +f \u005c(C:A cA\u00e9:|\u00e9 00 00 +f -A 1[a-c]**{a,b}C:\u00e9 00 00 +f A ! 00 00 +f / B+ 00 00 +f -c (B@ 00 00 +f (\u00e9 [:digit:] 00 00 +f B.\u005c\u00e9 {\u00e9-}|a+ 00 00 +f -1ab ^\u00e9(*([:digit:][:alpha:] 00 00 +f )( c 00 00 +f (Abab 1a!( 00 00 +f -A\u00e9-abc (BB.{a,b}[!b]A 00 00 +f +.aB2- [{1..3}\u00e9..^ 00 00 +f A+C:1 [a-c]!1:@{1..3}} 00 00 +f / b\u005c{a,b}[a-c] 00 00 +f C: 1 00 00 +f \u005cb21- \u00e9*\u005c 00 00 +f 1(\u005c **[:alpha:]}c\u00e9b 00 00 +f ) (A+:1 00 00 +f ..2 ?*!{a,b}- 00 00 +f abA( B[:alpha:][:alpha:] 00 00 +f \u005c b 00 00 +f (....C:AB {a,b}( 00 00 +f bBcab). [:digit:]b\u00e9{1..3}C: 00 00 +f )+a *..+, 00 00 +f -2)\u00e9 @-, 00 00 +f acb.. {1..3}+(1!( 00 00 +f BC:ab\u00e9+A {a,b}1{a,b}?} 00 00 +f B..baAb C:[@(?:[:alpha:]\u00e9 00 00 +f A1C:C: ^*( 00 00 +f ..+ \u00e9.. 00 00 +f \u005ca [:alpha:]-+@^.. 00 00 +f B/..1\u00e9+ bB 00 00 +f a A{ 00 00 +f C:-+c.. *(\u005c\u00e9[:digit:]@{ 00 00 +f + [a-c]{1..3}\u00e9{{1..3}:* 00 00 +f +2C:- + 00 00 +f B..(1 {a,b}@(1\u005c[a-c][ 00 00 +f C:-\u005c. B!( 00 00 +f .\u005c2\u005c !( 00 00 +f AC:) + 00 00 +f ab+A\u00e9 ^@(*()c 00 00 +f \u005cab1(..a c 00 00 +f aA-BC: {1..3}[:alpha:]{1..3}+( 00 00 +f +2) )..C: 00 00 +f ab [!b]@ 00 00 +f \u00e9ab\u005c(\u00e9.. - 00 00 +f + ...1[:alpha:]*A\u00e9 00 00 +f C:b ?(!(@? 00 00 +f ..aba A{1..3}*(..+[a-c] 00 00 +f .1C:11a a[/b[:alpha:]@([:alpha:] 00 00 +f - ( 00 00 +f 1) a[a-c]+(*(c+* 00 00 +f +\u005cabAa !@(!( 00 00 +f C: B^{[!b]@1[:digit:] 00 00 +f A)A ?(B{a,b} 00 00 +f C:A *?{a,b}2?2 00 00 +f )2/(+c [:digit:] 00 00 +f ) ([:alpha:]b1 00 00 +f \u005cA \u005c!([!b]^**}+( 00 00 +f -2cB..B .. 00 00 +f 2-ab-- ^\u00e9 00 00 +f .\u005c\u005c+ \u00e9:C:?.c| 00 00 +f (ca\u005c+- {{1..3}\u00e9[:alpha:] 00 00 +f Ac+/BB A\u00e9?(a 00 00 +f .) \u005c. 00 00 +f b)abbC: )@**+: 00 00 +f ..c\u00e9C:C: *\u00e9!([:alpha:]1@( 00 00 +f C:. @([a-c].. 00 00 +f a a.. 00 00 +f C:.c !-....**] 00 00 +f \u00e9./C:ab+ -)] 00 00 +f b \u005cC:[a-c]@{1..3} 00 00 +f B(.ab\u005c @(+^{a,b}*?( 00 00 +f a...\u00e9. B2 00 00 +f / }[!b]@(, 00 00 +f b2c2 b)?c\u00e9{a,b} 00 00 +f 1A\u005c \u00e9!(**A[:digit:], 00 00 +f .. @*\u005c{1 00 00 +f c2a [!b][a-c]@..|\u005c.. 00 00 +f )a C:2\u00e9@.. 00 00 +f 1ab {a,b}@ 00 00 +f c-abb ]A 00 00 +f C:)C: [:digit:]@ 00 00 +f )+c [a-c]| 00 00 +f ab {1..3}!(*(+.[a-c] 00 00 +f .C: +**,{a,b}|\u005c 00 00 +f \u005c) ,(.!*( 00 00 +f Aab1B !([!b]AB 00 00 +f c A 00 00 +f a\u00e9\u005c\u00e9 +1/..[!b]? 00 00 +f \u005cC:Aa ! 00 00 +f -C:A-a C:C:/@({a,b} 00 00 +f A @[a-c]^.2[a-c].. 00 00 +f ab *( 00 00 +f +bb2 +([:alpha:]*** 00 00 +f Aa\u005c+ {1..3}2C:?*( 00 00 +f \u005c\u005cabb+ ]@[:alpha:]A?A[:digit:] 00 00 +f \u00e9 1..[ 00 00 +f ..). @(},b 00 00 +f aaa../ab -[:alpha:]|,{a,b} 00 00 +f bB1..c \u005c\u00e9/:!(!@( 00 00 +f ..ab2. }B+{1..3}\u005c[!b] 00 00 +f b2c..\u00e9- A 00 00 +f B(. (@[:digit:]{1..3}c 00 00 +f a\u00e9 B\u005c[:alpha:].. 00 00 +f aB/-+1 /**@ 00 00 +f B+) @(\u00e9+(^, 00 00 +f -B)\u00e9++ ) 00 00 +f )\u005c\u00e9B1c @()!({a,b}[!b]+( 00 00 +f \u005cb\u00e9b B2]]B 00 00 +f C:( +A} 00 00 +f ) !(?( 00 00 +f c-\u00e92 {1..1+|] 00 00 +f -().) a 00 00 +f C: [:digit:] 00 00 +f AAc(2( {[:digit:],! 00 00 +f 1+2abb ^ 00 00 +f ()\u00e9-A- AB]? 00 00 +f b\u00e9BC:1 },?( 00 00 +f (..b1 *(!( 00 00 +f 2 ,]^{a,b} 00 00 +f /cab2ab1 a**{. 00 00 +f )..B @( 00 00 +f Bc 2 00 00 +f 1a++) [!b]{[:alpha:]B@ 00 00 +f a2-ab/ [a-c]** 11 11 +f \u005cB..C:B ?/ 00 00 +f . ?@(1 00 00 +f c21(\u005c [:alpha:]-{^!( 00 00 +f \u00e9.... {a,b}// 00 00 +f )2.1 }\u00e9-B--{ 00 00 +f -1 (|)c2 00 00 +f 2..)2b \u005c(?\u00e9C:+( 00 00 +f ab/2\u00e9\u005c [:alpha:]}+(..a 00 00 +f Bb .. 00 00 +f c /{a,b}^ 00 00 +f abC:A {1..3}[:digit:]b( 00 00 +f aa ],@ 00 00 +f / {1..3}!(/^[@( 00 00 +f /a/ *(2[:alpha:] 00 00 +f 12 [:digit:]B\u005c 00 00 +f aC:/\u00e9ab *(+(!:([:digit:][a-c] 00 00 +f /..) {\u005c@:c 00 00 +f babC:).( C:...@[:digit:]{1..3} 00 00 +f c ( 00 00 +f ca1.b2 [:digit:]? 00 00 +f (a21\u00e91 c/{a,b} 00 00 +f 1C:A ?([:digit:]\u005c 00 00 +f C:+ [!b]*|{*[:digit:]? 00 00 +f (...abc ]..)) 00 00 +f \u00e9\u00e9a) [:digit:]@(@:?A{ 00 00 +f -/(/. \u005ca[!b][( 00 00 +f A @ 00 00 +f ca(b ?+..]?(+(/ 00 00 +f C:A *[a-c][a-c]+ 00 00 +f .)-11 ** 00 00 +f )AaaA.. *(^[:alpha:] 00 00 +f ()a\u005c1 2 00 00 +f C: 1[,1\u00e9. 00 00 +f +aB (^{1..3}[a-c]{1..3}{a,b}c 00 00 +f .\u00e91 *!( 00 00 +f Bab.. \u005c!(a\u00e9(B1 00 00 +f -C:..bc\u00e9 )? 00 00 +f ++ [:digit:] 00 00 +f .2(+(\u00e9 {1..3},[:alpha:][:alpha:] 00 00 +f 1B\u00e91 )**aB{a,b}!( 00 00 +f (A) }^B}[- 00 00 +f B)Ab [!b]@(+(: 00 00 +f c) !!([:digit:] 00 00 +f bc |[:digit:]^!\u00e9\u005c 00 00 +f c... +(! 00 00 +f A)b).... [:alpha:]*?( 00 00 +f c/c @\u005c}*(}[ 00 00 +f .\u00e9( A^?} 00 00 +f ( c?(- 00 00 +f /c1\u005c ?1+(/- 00 00 +f c \u00e9+cb[!b] 00 00 +f A }){a,b}- 00 00 +f A) \u005cB}-[:digit:] 00 00 +f (Bab.. *( 00 00 +f 2\u00e9..ab1 C:?(/ 00 00 +f \u00e9+a -* 00 00 +f +/C: \u00e9\u005c(***(!C: 00 00 +f \u005c2 @(@(),A 00 00 +f \u00e9C:ab. +?([!b][:alpha:]:-c 00 00 +f B /bc? 00 00 +f A) ]([:alpha:]\u005c, 00 00 +f C: 1{1..3} 00 00 +f Aa2-b .b 00 00 +f a1 |b,^A[!b] 00 00 +f 1C:b1B\u005c ?c.. 00 00 +f ((bc ** 11 11 +f b .. 00 00 +f ab-bA- ]|b! 00 00 +f \u005c 1[a-c]..!({a,b}/[ 00 00 +f c..+\u00e9 ?([:alpha:]* 00 00 +f ab/(b.. 1c 00 00 +f 2.. {a,b}? 00 00 +f -2(A * 11 11 +f + {a,b}.|}[:alpha:].2 00 00 +f .2 |+A\u005c^[!b] 00 00 +f /\u005c++ab *(C:,2 00 00 +f ab)/a ..?(, 00 00 +f -abA\u005c\u005c [:digit:]??(:C:{a,b} 00 00 +f ..+\u00e9 [!b]!(:[:alpha:](c 00 00 +f c(\u005c+ [:alpha:][a-c][!b] 00 00 +f B2b2.. B/ 00 00 +f -ab)/B {)[!b]\u00e9c 00 00 +f 2B !(?(*![:alpha:]\u005cB 00 00 +f ). cA@(a]*(1 00 00 +f 1()b( ?(/\u00e9C:!(*( 00 00 +f )1bB1. ?({1..3}]|./ 00 00 +f 2 A+ 00 00 +f C: )b1)(*() 00 00 +f a2c(-A ).. 00 00 +f bC:cBA ]@1 00 00 +f 2A)ab. /@(b 00 00 +f . }){1..3}- 00 00 +f -\u00e9/+ {1..3}++[:digit:]- 00 00 +f /B2\u00e9(1 -a 00 00 +f \u005c @B[a-c]\u005c2 00 00 +f abBb+(C: -! 00 00 +f +\u00e9 +(+(-+{a,b}C:( 00 00 +f / .c^**{1..3} 00 00 +f ab/.ab ,A+ 00 00 +f +\u00e9b\u00e9 *([a-c][:alpha:]*(A 00 00 +f )-/(21 [2[:digit:],/ 00 00 +f cab)2 [:digit:][:digit:]\u00e9\u00e9{a,b}(. 00 00 +f 1/ ,/1 00 00 +f 2-1C:\u00e9.. C:[!b]- 00 00 +f a/ ^ 00 00 +f )..\u005c **[:digit:] 00 00 +f ...BA(2 1[:digit:])[a-c]AC: 00 00 +f /cB\u005c1 ?(/ 00 00 +f bab-+ *[!\u005c) 00 00 +f \u00e9a.A1.. ,!@( 00 00 +f . ?( 00 00 +f 2.B\u005c21 [b 00 00 +f ( +(b)]/ 00 00 +f .-Aabb ^*(|{ 00 00 +f b..A C:)/aa| 00 00 +f ac. ?-?b.[:digit:]\u005c 00 00 +f ..B2a :{a,b}-. 00 00 +f 1-..A @(B..\u00e9!}? 00 00 +f BC:C: )+{1..3}{a,b} 00 00 +f a1(B ^. 00 00 +f )abb b/+\u005c{a,b}a. 00 00 +f BbB }!(![\u005c[:alpha:]} 00 00 +f C:b |*{{( 00 00 +f a } 00 00 +f (B.+/ \u00e91..b[c/ 00 00 +f aa {a,b} 00 00 +f ..(+/AB /[!b][!b] 00 00 +f .. * 00 00 +f bA -[:alpha:]! 00 00 +f 1 !( 00 00 +f ab- [a-c]b 00 00 +f - \u00e9A[:digit:] 00 00 +f a1) \u005c:!(!{1..3}\u00e9 00 00 +f .. ]?(+(1[!b]b 00 00 +f bab\u00e9+..1 +([B?[ 00 00 +f /(\u005cc ,. 00 00 +f Ba( [:digit:]\u005cC:+[a-c], 00 00 +f B.\u00e9ab.. \u005c){a,b} 00 00 +f /1C:\u005c (+(({a,b}[:digit:]B( 00 00 +f Aa2 @[!b][:alpha:]c\u00e9\u00e9*( 00 00 +f )(B\u005c /!(^? 00 00 +f 1b1.B \u00e9***[!b]?( 00 00 +f Bc ([:digit:]?((|[!b] 00 00 +f AC:./BA A** 00 00 +f 1b+\u00e9- ,+( 00 00 +f c c)\u00e9A 00 00 +f b.C: :*\u005c..[!b]\u005c?( 00 00 +f C:\u005c2 },?@(, 00 00 +f /2/. \u00e9A\u005c 00 00 +f 2 [:alpha:]*(^. 00 00 +f ....B c{a,b}A/[:digit:]1c 00 00 +f /A1 ())c{1..3}a| 00 00 +f ..)\u00e9 ,{a,b} 00 00 +f c2 @C:\u00e9 00 00 +f B+)b( |++/][:alpha:] 00 00 +f A\u005c/2.a [*(B{1..3} 00 00 +f 1AC: b[?(?.@( 00 00 +f .\u005c..Bab\u00e9 aB[:alpha:].?(B@ 00 00 +f 2 [:digit:],/ 00 00 +f ..c ?(*)|@ 00 00 +f -..C:1.. /[!b]@?(B/} 00 00 +f ab-1..\u00e9 *(1^c,[:digit:]c 00 00 +f B+ab-C: }] 00 00 +f 22A. }C:?[:alpha:]B+! 00 00 +f aB - 00 00 +f B.a/cB [[:digit:]!*(.B[ 00 00 +f A\u00e9 [!b]{. 00 00 +f ab b 00 00 +f (c+ c: 00 00 +f a *!*( 00 00 +f ..A ^+( 00 00 +f 2.ab . 00 00 +f A }c[:alpha:] 00 00 +f a(c- !){1..3}\u00e9+, 00 00 +f )b\u00e9/).. [!b]{a,b} 00 00 +f \u00e9. /+- 00 00 +f ) ,+(}b 00 00 +f 2ab C:| 00 00 +f a ?(1B@( 00 00 +f .abb-a2 [:digit:](1{a,b}!( 00 00 +f \u005c [a-c] 00 00 +f Ac\u00e9\u005cab\u005c ,{) 00 00 +f Aab)..- !(^{A 00 00 +f 2 [a-c] 00 00 +f .AA....( }a[:digit:]\u005c 00 00 +f -..+ ?( 00 00 +f -.. C:a@(*( 00 00 +f c+.. 2/A 00 00 +f AC:/ }1b..*(?(** 00 00 +f / ^B2]B+ 00 00 +f ./ @+([!b]@, 00 00 +f C:/..+C: [!b],@([:alpha:], 00 00 +f B)c -1[!b]+} 00 00 +f c\u00e9+ + 00 00 +f bab \u005c\u00e9[:digit:]] 00 00 +f c\u005c 1a[a-c]A!(]** 00 00 +f ab. ***(!(**{a,b}:.. 00 00 +f -+Ac+.. \u005cA\u005cB 00 00 +f ab/ab..b \u005c 00 00 +f abC:a)ab2 @(!{*(2) 00 00 +f (C:\u00e9c2 */** 00 00 +f \u00e9ab2... B,B 00 00 +f 2( \u005c 00 00 +f bB\u00e9ab ]{1..3}.. 00 00 +f 1b , 00 00 +f .ca {- 00 00 +f /(2\u00e9/ !([!b][ 00 00 +f A . 00 00 +f 1c\u00e9 ! 00 00 +f )\u005c1 ,+.. 00 00 +f a..C:(.. [:digit:] 00 00 +f \u00e9C: | 00 00 +f \u005cab c, 00 00 +f C:1.bA {a,b}**),+( 00 00 +f )- [[:digit:]|C:1 00 00 +f \u005caaBb b[b*([+*( 00 00 +f /2\u005c ?(, 00 00 +f aAA.. +(2 00 00 +f ( ,| 00 00 +f 1cc :a 00 00 +f BC:cab ? 00 00 +f b(/ @( 00 00 +f 2/22++ !-+({a,b}C:{: 00 00 +f - {a,b}C:[:alpha:]^{a,b}( 00 00 +f a..c (.[a-c]|[:alpha:]!(** 00 00 +f aA { 00 00 +f ( !@(?([:digit:]+( 00 00 +f ab /[:alpha:][:digit:]2 00 00 +f c1c) [a-c],^?( 00 00 +f 1/\u005c- c}/[:digit:]2** 00 00 +f ..aac }\u005c{1..3}2 00 00 +f bb\u005c-..( *(C:!(. 00 00 +f .BA\u00e9 , 00 00 +f \u00e9ab()-A a{1..3} 00 00 +f C:.2/ |A[!b]{a,b}(+( 00 00 +f (ab... B[:alpha:][a-c] 00 00 +f A {a,b}C:@[:alpha:]]+(+ 00 00 +f )a-A\u00e9.. !(!(+] 00 00 +f a+)\u005c !Bc+(b 00 00 +f ab.AB/ [?( 00 00 +f . 2{ 00 00 +f abc.-- {1..3} 00 00 +f b..--// ?/* 00 00 +f b [}:( 00 00 +f ab.abb \u005c[:digit:]B 00 00 +f C:-ab\u005c )+!@2 00 00 +f /-c C:] 00 00 +f +\u00e9 +B\u005c 00 00 +f + {*[a-c]}2 00 00 +f b)C:b -/?([a-c]?[:alpha:] 00 00 +f ((B ) 00 00 +f 2)b/-2 b,A.*( 00 00 +f )+-+) @( 00 00 +f ab\u00e9(Ba a 00 00 +f -b/\u005c {[:digit:]c[:digit:]c 00 00 +f bcb a[ 00 00 +f A {)[!b]? 00 00 +f B\u005c B!... 00 00 +f A+\u00e9. b 00 00 +f 1)1C: [:alpha:] 00 00 +f \u005c2B(A [!b]}( 00 00 +f B.ab [a-c]C:\u00e9 00 00 +f cC:bC: ^.bb1} 00 00 +f -(12a.. b*(!B\u00e9!( 00 00 +f C:ab\u00e9A2 ..2bBb 00 00 +f b)..C: ([:digit:]?(|?{a,b} 00 00 +f c\u00e9/.C:- /]}..@ 00 00 +f \u005c1c /: 00 00 +f C: [:digit:](,!()[!b] 00 00 +f c\u00e9.(\u00e9b *( 00 00 +f B {1..3} 00 00 +f b)2B / 00 00 +f ab\u00e9-1\u005c .\u005c*c^@ 00 00 +f \u005cb )|BC:)C:. 00 00 +f 2Aabab\u005c\u005c :A1[:digit:]]1 00 00 +f C:C:..../+ !B 00 00 +f 12- BC:\u005c:[2c 00 00 +f \u005cAab +( 00 00 +f a\u00e9..2C:.. !b 00 00 +f 1 a!a*[} 00 00 +f AA .[a-c].?(1a[a-c] 00 00 +f b Bb{1..3}[a-c]^+( 00 00 +f \u005c |2+[:digit:]a}. 00 00 +f +aAB\u005c /.. 00 00 +f /B)..c /?(2c 00 00 +f (-C:/ [a-c]:A[:digit:]) 00 00 +f ab | 00 00 +f cab +( 00 00 +f .(1\u005c (?( 00 00 +f \u00e9b-\u00e9/ [{1..3}\u00e9. 00 00 +f \u00e9b.. ] 00 00 +f --1/- +(bC:(./ 00 00 +f A c 00 00 +f 2aab a?\u005c/\u00e9 00 00 +f A !/.** 00 00 +f .. *(aC:}* 00 00 +f \u00e9/A. ..({1..3}c 00 00 +f c\u00e9 ?c,:]. 00 00 +f Bcab +c@(!AC:@ 00 00 +f \u005cC: ] 00 00 +f 1\u005c..((C: |?\u005c[:digit:]|\u00e9 00 00 +f /aabC:2 @(\u005c{a,b}*({a,b} 00 00 +f /C:\u005ca ]^( 00 00 +f +B /**c{a,b} 00 00 +f .A }!( 00 00 +f c \u005c1)+( 00 00 +f \u005cabB B.. 00 00 +f (\u005cc..-( -[ 00 00 +f . ?(a\u00e9 00 00 +f .a..1.2 !(..{a,b} 00 00 +f C: [:digit:]B 00 00 +f .ab(\u005c2 A[:alpha:]?({1..3}{1..3} 00 00 +f ((A @?(\u005cA**{a,b}[ 00 00 +f .+)\u00e9) ....*() 00 00 +f A/)ca b.**** 00 00 +f +C:\u005c .}]@(*? 00 00 +f C:.\u00e9 / 00 00 +f ...( **(B.!(* 00 00 +f b-\u005c [:alpha:]B@ 00 00 +f (.(a(c [:digit:]:1[!b] 00 00 +f .... +({a,b}?*|@(?( 00 00 +f b+ :\u00e9aB 00 00 +f a +c[!b]/..** 00 00 +f .1 1 00 00 +f /..(C: a}\u005c{a,b}||.. 00 00 +f /)B(\u00e9 ,-1{ 00 00 +f 21a | 00 00 +f / {1..3})bb@(! 00 00 +f bBA+ab ,{a,b}* 00 00 +f ..\u00e9a..2- *(/[!b]{1..3} 00 00 +f \u00e9(..( }![:alpha:] 00 00 +f ...)c/a {{1..3} 00 00 +f ..1 ^[:alpha:]c\u005c 00 00 +f a\u005c.2 ( 00 00 +f \u00e9\u00e9 +**?^{a,b}- 00 00 +f )-1A A[:alpha:]-:[:alpha:]**] 00 00 +f ba.. */b}?{a,b} 00 00 +f b2B ?!( 00 00 +f \u00e9C:\u005c\u00e92 @ 00 00 +f B+ [:digit:]@!(b..?2 00 00 +f \u005c\u00e92 ..{b 00 00 +f )\u00e9112- 2\u005c.... 00 00 +f b2\u00e9Abb A[a-c]\u005c[:alpha:] 00 00 +f /-a\u00e9c ..:\u005c..{A{a,b} 00 00 +f AB+)/ab cb[!b]!*] 00 00 +f C:C:A **[a 00 00 +f \u00e9A1\u00e9 ? 00 00 +f \u005cc) :|[:alpha:]*( 00 00 +f ./\u00e9AB ,2+| 00 00 +f \u00e9C:bB(c 2 00 00 +f A+.b+ 2^ 00 00 +f ))b !?(*{a,b} 00 00 +f 1)( [a-c]{1..3}1[ 00 00 +f B/.a+ {1..3},-(/ 00 00 +f () C:?({a,b}!\u005c 00 00 +f C:+ } 00 00 +f bc 1 00 00 +f 1+(\u00e9..( {1..3}!([1C: 00 00 +f ..B \u00e9 00 00 +f /a2C: -C:[:digit:]! 00 00 +f \u00e9/ ..![:alpha:] 00 00 +f .. c[:alpha:] 00 00 +f ab\u005c- }c[!b]:b..1 00 00 +f //... @(^[:digit:]/A 00 00 +f \u005c.2c2 b!(-/@([ 00 00 +f \u005cab)2-. *1** 00 00 +f )(A2 !A 00 00 +f +ab @(2C:) 00 00 +f .. !..+(C:a1!( 00 00 +f )C:1/B )b{\u00e9?? 00 00 +f \u005cC:1\u00e9..B ?!( 00 00 +f )1 ]..1[:alpha:]-** 00 00 +f b /{.@(..b 00 00 +f - \u005c1)2\u00e9.. 00 00 +f ab.B1A1 *A].|/ 00 00 +f )C:\u00e9 a 00 00 +f 1\u00e9 } 00 00 +f a a)\u005c!( 00 00 +f a\u00e9)(- a]A 00 00 +f ) *c 00 00 +f ab !* 00 00 +f / ]..(1**^ 00 00 +f ( \u005c*) 00 00 +f - [:digit:] 00 00 +f c {*) 00 00 +f a-(c ,@(\u005c{a,b}A 00 00 +f ..-C:/\u00e9 /*b@(b/| 00 00 +f )\u005c -[a-c]C:[:alpha:] 00 00 +f /\u005ca )|*(.[!b]?( 00 00 +f c/\u00e9..b [:digit:]::..*(* 00 00 +f (( C:(A?({ 00 00 +f \u005cB\u00e9(b ! 00 00 +f ++....c2 *(\u00e9 00 00 +f )b1 2[:alpha:]2 00 00 +f A(b.(/ B\u00e92\u005c 00 00 +f b.2bb --[?(|{1..3} 00 00 +f B./A * 00 00 +f cC:C: *( 00 00 +f 1B+. @( 00 00 +f a {a,b}.:{1..3}1B[a-c] 00 00 +f cbBc c 00 00 +f \u005c-. ..:)B[:digit:]?(* 00 00 +f a2\u005c--. a[] 00 00 +f ab C: 00 00 +f aaabB.. .}{b 00 00 +f 2 c 00 00 +f C:\u00e9 ^| 00 00 +f a b*({B( 00 00 +f )ac..a- **+( 00 00 +f 1 \u00e9. 00 00 +f -\u005c/\u00e9 @(:[!b]A[:alpha:]*(! 00 00 +f -ABb\u005c( [ 00 00 +f ab\u005c B[a-c]!(\u005c^ 00 00 +f ab2 [] 00 00 +f )/\u005c +(!(!(Bb, 00 00 +f ..B ?][:digit:][a-c]B+** 00 00 +f 1)..a) C:!()]@( 00 00 +f A [:alpha:] 01 01 +f abAA B| 00 00 +f /\u005c1 B{?(|B 00 00 +f )2()\u00e9 }^?B{1..3}] 00 00 +f B\u00e9A \u00e9!(c! 00 00 +f B(ab() +( 00 00 +f ../C:- ,[:alpha:]!(:?(A^ 00 00 +f c -] 00 00 +f /.. +*)*?(? 00 00 +f B.\u005c21b [:alpha:],+(@( 00 00 +f B !1..{a,b}** 00 00 +f c {a,b}] 00 00 +f )c(c *(A{@( 00 00 +f A ]! 00 00 +f .\u00e9)2 {..*(B**@** 00 00 +f ab2/aB [:digit:] 00 00 +f ab.2)( @([b-( 00 00 +f ..- 1[!b]]|c[!b] 00 00 +f B B 11 11 +f 2)a\u005cAab [:alpha:][a-c]1 00 00 +f a( C:* 00 00 +f C:+2 1c/:- 00 00 +f C:\u005c1 +){a,b}{1..3}ab 00 00 +f ) (b.?C:@ 00 00 +f /ab !?@(| 00 00 +f cc) .. 00 00 +f ../(21) [:alpha:]** 00 00 +f 2- \u005c[:alpha:] 00 00 +f +1 [B[a-c]2\u005c 00 00 +f A B| 00 00 +f +C:.ab )a{a,b}b2 00 00 +f /\u005c) ?*( 00 00 +f 1A. !**\u00e9[:alpha:]^B. 00 00 +f B1a [\u00e9( 00 00 +f c)+\u005c [!b] 00 00 +f abab-c :,}{a,b}@( 00 00 +f \u00e9ab)/a) }[:alpha:]]@[!b]\u00e9\u005c 00 00 +f A) [!b]( 00 00 +f )/..(c1 ]-[!b]ac]@ 00 00 +f \u005c {1..3}{ 00 00 +f bA [:digit:]^[:alpha:]a\u00e9 00 00 +f .c(\u00e9 @(C:!@ 00 00 +f B(\u005cC: ]\u00e9?/! 00 00 +f c/ac . 00 00 +f c\u005c/\u00e9aB :B.\u00e9? 00 00 +f abB+(\u00e9c \u00e9 00 00 +f b) (+[:alpha:] 00 00 +f / !(-!( 00 00 +f 1C:a [a-c] 00 00 +f \u00e9-2/1) +(?/? 00 00 +f \u005ca ?( 00 00 +f \u005c ?(BB[a-c] 00 00 +f / @(,{1..3} 00 00 +f 1A..C:.ab [:digit:]!(?(({} 00 00 +f .a !(B?{1..3}**** 00 00 +f \u00e921c [:alpha:]{a,b} 00 00 +f B/2 [:alpha:]^.[:alpha:]\u005c 00 00 +f \u00e9\u00e9( C:}b+(?@ 00 00 +f b\u00e9..cab) [a-c]A(*(.. 00 00 +f a ::,{1..3} 00 00 +f \u00e9C:\u00e9 *(- 00 00 +f -)1/ ,?([!b].c{1..3} 00 00 +f abbAAA !(/ 00 00 +f - ]!([:alpha:]+(}** 00 00 +f aab - 00 00 +f 1 B/ 00 00 +f b.cab-. \u005c 00 00 +f ab1..-\u00e9 {1..3} 00 00 +f Aab\u005c !( 00 00 +f A\u00e9)a- ,@(a[a-c]!(^: 00 00 +f \u00e9b\u005c [2(,c 00 00 +f \u00e9\u005c))ab (?(C:2 00 00 +f -\u00e9\u00e9.. .:*?{b[ 00 00 +f ) ..[:alpha:]| 00 00 +f (Acb [([:alpha:]{ 00 00 +f bAaba [:digit:]C:{\u005c/.. 00 00 +f C:/1.. [b 00 00 +f a-a.a) !!c**C:! 00 00 +f abB [:alpha:]!([{a,b}{a,b} 00 00 +f B2C: [a-c]A..?}[[ 00 00 +f -1 !@([:digit:] 00 00 +f C:)cb-B \u005c:*)?(B! 00 00 +f ..)-B +( 00 00 +f B.Aab \u005c@@(@(*(1\u005c 00 00 +f .A. }@(*(*+( 00 00 +f . {1..3}*? 00 00 +f (1) @(|A*() 00 00 +f bB.)./ [!b]B!\u005c{1..3}cc 00 00 +f 1A....a\u00e9 ..**,c{a,b}) 00 00 +f BBb\u005c\u00e9 {1..3} 00 00 +f +c(B\u00e9- c*[!b] 00 00 +f bA..( .. 00 00 +f \u005c)\u00e9bC: *@* 00 00 +f )-Bab/ [{a,b}b/[!b]+, 00 00 +f -\u00e9).. @(]-*(A... 00 00 +f BAAab A(@{1..3} 00 00 +f -B -..a[a-c]}[ 00 00 +f /).c ^\u00e9:)\u005c2 00 00 +f ..+A\u00e92 . 00 00 +f 1..a \u005c..b[a-c][!b]: 00 00 +f (+/abc A/![:alpha:]!( 00 00 +f ab2Aa [:alpha:]**[a-c][:digit:]?] 00 00 +f -/..ab .}*! 00 00 +f +bab.+ b*([:alpha:] 00 00 +f -aba [:alpha:]c)^B?( 00 00 +f \u00e9 2\u005c[!b]!(- 00 00 +f C: [a-c]{**A@(+ 00 00 +f 1\u005c\u005c (..[a-c][:digit:]*(^? 00 00 +f b+)c.a [*[a-c]].+( 00 00 +f C:2 a[**| 00 00 +f b-abaA [:digit:]b.[+([ 00 00 +f +2 ?( 00 00 +f .- {1..3}1 00 00 +f B)c [!b][a!(\u00e9{{a,b} 00 00 +f a1bc [!b]@@( 00 00 +f Aa\u00e9 ?(C:[:alpha:]\u005c.?({a,b} 00 00 +f + \u005cC: 00 00 +f ) ([:alpha:] 00 00 +f \u005cab A,(,^ 00 00 +f .C:BB {1..3}2(:c[:digit:] 00 00 +f cab/a/ .C:[:digit:]*@( 00 00 +f 12)b B\u005c 00 00 +f B b,@ 00 00 +f (\u005ca {1..3} 00 00 +f ( \u00e9b\u00e9{1..3}{a,b}.[!b] 00 00 +f b) +(C:b[a-c]: 00 00 +f 1b [a 00 00 +f (B( [!b]{a,b}\u00e9@(?+ 00 00 +f /.. +(}!( 00 00 +f \u00e9ABBab *[a-c][:digit:]{+C: 00 00 +f +BabBaab +(*** 00 00 +f \u005c\u005c. / 00 00 +f c2 \u00e9a)C:}\u005c) 00 00 +f 21)+ +(}C:*(.!(? 00 00 +f )ab\u00e9Aab) {{1..3}@2[:digit:] 00 00 +f abC:a *(]\u00e9/ 00 00 +f +B .BA*(C: 00 00 +f AAb. ^[a-c]Ba\u00e9A. 00 00 +f B(B ^{ 00 00 +f -.\u00e9C: [a-c]@[a-c]{1..3}A[:digit:] 00 00 +f B2A/a a@(b?(. 00 00 +f -2\u00e9- ..],.\u005cc 00 00 +f /..-(aba *(b}1{a,b}^ 00 00 +f aC: a 00 00 +f b !:|-@ 00 00 +f abB)abA/ +(!(| 00 00 +f (-a( !(*:**{ 00 00 +f \u00e9.. )**b]|\u00e9 00 00 +f .Bab..-. | 00 00 +f Bb +( 00 00 +f -. \u00e9[:digit:])}a 00 00 +f .. (.. 00 00 +f B\u005c {1..3}.\u00e9/. 00 00 +f b-\u005ccab.. !b 00 00 +f aab((c C: 00 00 +f \u00e9c [a-c]1a? 00 00 +f +.. ..-AC:,+ 00 00 +f A/\u005cC:A )]} 00 00 +f /+1C:( ,@(:([ 00 00 +f cA-A+ ..) 00 00 +f (b @()c 00 00 +f +C:b }b..} 00 00 +f \u00e9ac/1b 2[:digit:]\u005c^2 00 00 +f \u005cB-AB\u005c +} 00 00 +f 1)2) 2a^[:alpha:]]!( 00 00 +f / @(\u00e9 00 00 +f aB ../ 00 00 +f .. **|..^- 00 00 +f \u00e9-..\u005c+ @,AB+(a 00 00 +f c) a\u00e9 00 00 +f )-\u00e9)B\u005c ^^a*( 00 00 +f \u00e9)) }]*([| 00 00 +f \u005c2(C:Ab ?(]|?(a2{1..3} 00 00 +f +2bAaC: ?([a-c]c?(A[:digit:]{a,b} 00 00 +f )\u005cb ^+@(@@( 00 00 +f A C:[:digit:]| 00 00 +f ..b\u00e9)2 ^[!b]**\u005c,{ 00 00 +f 1B+1 !([:alpha:]:+@(@( 00 00 +f abab1 2+( 00 00 +f ab.1+1A ..1[:digit:]}, 00 00 +f -C:2c1 ,{a,b}a?,@ 00 00 +f +ca\u005c ,*[:alpha:]*(-[a-c] 00 00 +f (A)b A/|{1..3}{*( 00 00 +f b2( .[:alpha:]-}^! 00 00 +f 11\u00e9b ..[.[a-c][:digit:]{a,b} 00 00 +f 12BC:-.. .! 00 00 +f aAC:. [ 00 00 +f \u005c [a-c]+**\u00e9{1..3}- 00 00 +f Bab.(\u005c1 +?([ 00 00 +f /../(C:b } 00 00 +f A ! 00 00 +f a/.. !( 00 00 +f .12( [:digit:]( 00 00 +f 22 ^a 00 00 +f c2/b1. !{ 00 00 +f a. ^.{1..3}}.. 00 00 +f ...C:\u005c\u00e9C: .(-*[:digit:]] 00 00 +f .()( ?(\u005cC:1 00 00 +f ( [[:alpha:]/2[a-c] 00 00 +f c12(21 1*..!!B 00 00 +f A2abC:(B {1..3}B\u005cb] 00 00 +f //2 [:alpha:]|B 00 00 +f \u005c\u00e9/ !@(}b[:digit:] 00 00 +f 21 /**(^..( 00 00 +f B1 +( 00 00 +f (AA {+(][ 00 00 +f b ({^} 00 00 +f /C:2B) ..+...\u00e9|a 00 00 +f ) }/+ 00 00 +f 1(c \u005c{1..3}..bbB 00 00 +f Ba ,)a 00 00 +f A( {a,b},..[:alpha:] 00 00 +f bA\u005c 2*([a-c] 00 00 +f \u005c\u005c+2+ c2 00 00 +f ..Ba b@(c? 00 00 +f C:.\u00e9C:B( ..C:/??[? 00 00 +f ..Aa +*- 00 00 +f ) -\u005c+[a-c]{{a,b}c 00 00 +f /\u005c )A[a-c].[:digit:]@ 00 00 +f . C:1}C:(B2 00 00 +f - [!b]||1 00 00 +f /\u00e9+ (!)!( 00 00 +f 2bC:a @(+(!*!(* 00 00 +f C:\u005c).(- cA@,{1..3}B\u005c 00 00 +f 1(-1\u00e9 {1..3}[a-c] 00 00 +f (ab ]:+][!b]..C: 00 00 +f 1.abaa B!(^?(| 00 00 +f aba-ac [:alpha:]c: 00 00 +f \u005c+\u00e9/\u005c\u00e9 ]*(]c 00 00 +f bC:A/ ?([:alpha:],C: 00 00 +f C:(...c\u005c **../ 00 00 +f )2\u005c\u00e9cc a]]{ 00 00 +f ../a }([a-c]*{a,b} 00 00 +f )cbC:1 [:digit:]) 00 00 +f \u005c !([a-c]*,[[a-c] 00 00 +f /..aC: 2^[a-c]..{a,b}\u005c 00 00 +f a)cc }-! 00 00 +f \u005c)() ..@,\u005c{ 00 00 +f /C:B/ {a,b}2b{1..3}2 00 00 +f C:+/ ?{a,b}[a-c]-??( 00 00 +f \u005cB.a- ?|)** 00 00 +f ) [!b]! 00 00 +f \u00e9..2(abA ** 11 11 +f \u00e9+2ab b 00 00 +f 2A c+**[!b] 00 00 +f +\u005c [ 00 00 +f BB : 00 00 +f .2cc /!(!:|@( 00 00 +f ..(B {a,b}+:{a,b}b[:digit:]} 00 00 +f ab-ab ![:alpha:]b:*B\u005c 00 00 +f 1 {@( 00 00 +f - 2]|,.@ 00 00 +f aa..ab\u005cab \u005c{a,b}\u00e9 00 00 +f - C:!/@@(.1 00 00 +f 2 ^**:{\u00e912 00 00 +f ++.) :\u005c 00 00 +f ab..1 [:alpha:][:digit:]C:- 00 00 +f ac !{a,b}\u005c[a-c][[!b]* 00 00 +f -. . 00 00 +f \u00e9b+ @!?( 00 00 +f Ab {{1..3}b@ 00 00 +f C:B2)c ?+(b\u005c{a,b}:C: 00 00 +f 1B {a,b}2! 00 00 +f \u00e9)aba [:digit:] 00 00 +f C:A.11 +@(b.C:: 00 00 +f A\u005c.)2a ** 11 00 +f \u005c [:alpha:]+( 00 00 +f \u00e9/A-- c+]A] 00 00 +f 2B1//1 [:alpha:]b*(+(* 00 00 +f aAB()+ @?( 00 00 +f ) *([^^ 00 00 +f / [!b]b|[a-c]]:^ 00 00 +f ))\u00e9b .+({1..3}@( 00 00 +f .. [:digit:]\u005c[:digit:]- 00 00 +f b-(\u005c) ,1\u00e9+A 00 00 +f a2..\u00e9ab- *() 00 00 +f c1 ,+a^\u00e9C: 00 00 +f b\u00e9A a 00 00 +f 2 \u005c|B| 00 00 +f / 2/[a-c] 00 00 +f \u00e9Aab/\u00e9.. )c{a,b}/1\u005c* 00 00 +f b2cC:A ?{b*([:digit:][!b]/ 00 00 +f .) |^?,,*(a 00 00 +f ....+ B( 00 00 +f ..2 /(2-||/ 00 00 +f -2 C:A[:alpha:] 00 00 +f B\u00e9\u00e9b) *) 11 11 +f ( a 00 00 +f cA\u005c b.[:digit:] 00 00 +f A+ABa (@[:digit:]*+C:{a,b} 00 00 +f \u00e9\u00e9. })\u005c*()* 00 00 +f c++\u005c ( 00 00 +f A..A2ab \u00e9*!...(b 00 00 +f (1A/ [:digit:]( 00 00 +f BabC:b ,1!( 00 00 +f -2aa ]![!b]../: 00 00 +f A-abC: 2/\u005c 00 00 +f . ([:digit:][!b]**( 00 00 +f \u005c.a.... @( 00 00 +f A\u00e9a-.. \u00e9+(1[:alpha:]@ 00 00 +f ab..) ! 00 00 +f bB/c/ 2{[[ 00 00 +f .--1 {1..3}B 00 00 +f 1b\u005ca/ |@(|!\u005cb 00 00 +f a. **\u00e9 00 00 +f \u00e9A 2, 00 00 +f (2+\u00e9 [:digit:][:digit:][:digit:]C:}c 00 00 +f 1.... [:alpha:]?((1/ 00 00 +f ..2-++\u00e9 [:digit:] 00 00 +f ..Bc2C:B {^/ 00 00 +f C:c..C: b2**a 00 00 +f .a-.\u00e9 ( 00 00 +f +\u005cC:-c !( 00 00 +f (1\u005c) ,,)*(/ 00 00 +f \u005cC:(A)c [!b][:alpha:]:B|b 00 00 +f b..c1/B !([a-c]a[a-c][!b]?a 00 00 +f 1\u005c.c./ @) 00 00 +f \u005c(b2/( C:a 00 00 +f +) \u005c\u00e9,![a-c] 00 00 +f a(c -a+, 00 00 +f b/ 1}-[!b]^-@( 00 00 +f + \u00e9[:alpha:]+ 00 00 +f B-1)2C: :a 00 00 +f c-abAa [a-c]{1..3}{1..3}a 00 00 +f abc aC:ca 00 00 +f c 2@(? 00 00 +f b+abab. B+( 00 00 +f /ab. @([:digit:]?[:digit:]**\u00e9+( 00 00 +f b2\u005c1 [a-c]A[*A^[:alpha:] 00 00 +f AC:+c-ab {1..3}..B 00 00 +f -+c\u00e9 A1 00 00 +f B..\u005c b\u005c?(+(} 00 00 +f bB2B [:digit:][!b]-[a-c] 00 00 +f ) [:alpha:]..) 00 00 +f \u00e92.( |2{*(:!2 00 00 +f \u00e9a.. +(*[a-c]b 00 00 +f \u005c\u00e9(-B [!b] 00 00 +f a [:digit:]B\u00e9 00 00 +f ..B\u00e9 |*{{1..3}{a,b} 00 00 +f ..C: !. 00 00 +f aB ): 00 00 +f \u00e9..B( b|/!( 00 00 +f C: B+( 00 00 +f /A/)b- c/]-a[!b]** 00 00 +f a/ /B**{a,b}[:digit:]+(b 00 00 +f B+C:2B +C: 00 00 +f Bb( } 00 00 +f ++A) **!(|[!b][a-c]!{ 00 00 +f /A\u005c [:digit:]{[a-c]{a,b} 00 00 +f b/(-ab C:?!:B( 00 00 +f -B( .. 00 00 +f \u005cB !([:alpha:]?(+( 00 00 +f .abb1Bab C: 00 00 +f C:1-C: !!( 00 00 +f .. /1!({1..3} 00 00 +f /+ +C: 00 00 +f b)2C:A 1?(B[!b]* 00 00 +f A( @,a?!(+(- 00 00 +f .. \u00e9 00 00 +f c B**[:digit:]{1..3}2a 00 00 +f - ?b[:digit:]\u005c 00 00 +f b 2 00 00 +f + {\u005c 00 00 +f bC:1\u00e9.. c2^(|{1..3} 00 00 +f C:21B\u00e9 [:alpha:]b 00 00 +f /)2/ !(c*(*( 00 00 +f B a{Aa 00 00 +f a. !(* 00 00 +f Bc +(b@(c+..b 00 00 +f B {a,b}+?([ 00 00 +f c11\u00e9 ** 11 11 +f \u00e9bbC:\u00e9\u00e9 ^()\u005c} 00 00 +f (c *(,?( 00 00 +f /.c..( [!b]B- 00 00 +f ca ][:digit:][a| 00 00 +f \u005c/- 1A 00 00 +f )abA. {[:digit:]\u00e9 00 00 +f bb) .1:(: 00 00 +f a ^ 00 00 +f (\u00e9\u005c ?{a,b}1[(* 00 00 +f a.)\u00e9/ -^{a,b} 00 00 +f ..) c!(c+(A 00 00 +f -b :(!+ 00 00 +f 2c1 @(C: 00 00 +f B(bba( {a,b} 00 00 +f /( c@(!! 00 00 +f +A+ *(c/ 00 00 +f ..b- ^ 00 00 +f .. [!b]^[:alpha:]C::{1..3}C: 00 00 +f abBbc/a !?({a,b}..{1..3}{1..3} 00 00 +f C:a / 00 00 +f \u005c2c.. @(c*|!{a,b} 00 00 +f + a!( 00 00 +f C:).. } 00 00 +f ....c(+b {a,b} 00 00 +f C:11\u00e9 B2+ 00 00 +f \u00e9) }?!(/!([:alpha:]( 00 00 +f ) **{1..3}] 00 00 +f \u00e9aba\u00e9 +([:digit:]+(/b 00 00 +f (ab {1..3}{a!([!b], 00 00 +f .\u005c/ -1[:alpha:]. 00 00 +f \u00e9\u00e9+ *?(-C:*(1} 00 00 +f B.+\u005ccB [a-c]c{{?( 00 00 +f (B+A2\u005c . 00 00 +f )( \u00e9a 00 00 +f .++ !(B*2[a-c]: 00 00 +f ab..\u00e9 (|+(? 00 00 +f Ba1 -?c+{1..3}?( 00 00 +f )\u005cC:+2 c!(! 00 00 +f . BA1* 00 00 +f A..a- !\u00e9. 00 00 +f caa/\u00e9 ]|****{a,b}A 00 00 +f .. {+C:a[!b] 00 00 +f c\u005c+\u00e92 [:alpha:]: 00 00 +f ..a B|+?( 00 00 +f ab/ .[a-c][**,A.. 00 00 +f A-+c2 a 00 00 +f A\u005c a?(+1]+(\u00e9 00 00 +f 2C: *(+(b+ 00 00 +f - ..] 00 00 +f .1b {1..3}!(?( 00 00 +f /. , 00 00 +f \u00e9. (!(a! 00 00 +f c b[:digit:]\u005c 00 00 +f /-C:a C:*(:*( 00 00 +f ab/ 1/[!b]!(! 00 00 +f -/1+ ([:alpha:]*([!b] 00 00 +f . **1^ 00 00 +f +2.. c)c{1..3}[!b]*( 00 00 +f ..bcc |?[ 00 00 +f .aba [.B!,|^ 00 00 +f 2Bb 2\u00e9a 00 00 +f B..+ !(+b. 00 00 +f ca +[@+ 00 00 +f ..( B*[:alpha:]\u00e9cc?( 00 00 +f )C:a **[:alpha:] 11 11 +f 2 B( 00 00 +f a. .C:[+[!b]. 00 00 +f C: ?(? 00 00 +f )-ba(- (!(*(C:!( 00 00 +f A @(])?(2 00 00 +f / -*([:alpha:]]!(a2 00 00 +f 1-\u00e9\u00e9 /+:C:+ 00 00 +f c((- [!b]a: 00 00 +f C:a\u005c \u005c 00 00 +f C:\u00e9c [:digit:], 00 00 +f ca 1*(b] 00 00 +f \u005c -{]2A@} 00 00 +f 21\u00e9-(B ]/!(*(1 00 00 +f 2..1/ab -+([a-c]( 00 00 +f ()..1( ^\u00e9@[!b] 00 00 +f -a !-. 00 00 +f C:\u005cB !( 00 00 +f .C:babC: ](, 00 00 +f \u005c..ab+ ?(1b**+ 00 00 +f +1+\u00e9. ^.@)a 00 00 +f bab 1(. 00 00 +f C:C:..A/ [:digit:]{a,b}[a-c]@ 00 00 +f ab2a [!b]|? 00 00 +f ).ab1 C:..+( 00 00 +f -.)\u00e9 [!b] 00 00 +f B(\u005c **[..\u005c[ 00 00 +f 1\u00e9.2C: !C:| 00 00 +f /..b 2[:digit:]:) 00 00 +f ( ?([:alpha:]! 00 00 +f b .[a-c]]*([!b]a] 00 00 +f ..\u005cb [:digit:]{1..3}..+(.. 00 00 +f ..212C: 2^+ 00 00 +f (ab/ ,?( 00 00 +f a-1/ab c^[:alpha:] 00 00 +f ( +B{, 00 00 +f B) {a,b}++ 00 00 +f 1c) A\u005c[!b][!b]a 00 00 +f \u005c..) ** 11 00 +f +Aa -}!.1? 00 00 +f B [:digit:]] 00 00 +f 112..A\u00e9 )^C:( 00 00 +f c\u005ca+Bc 2+(C:: 00 00 +f C:c [?|A, 00 00 +f C:b\u00e9 +a 00 00 +f +(/abC:a !(A*(, 00 00 +f 1) c 00 00 +f \u005c.. {1..3}..[:alpha:]?@ 00 00 +f b *{1..3}1/]{1..3} 00 00 +f ( \u00e9c[!b])2 00 00 +f cb , 00 00 +f \u00e9Bab @()C:A: 00 00 +f ) ..[!b]B{a,b} 00 00 +f ..c {1..3}- 00 00 +f .ab) @(:?(b. 00 00 +f \u005c\u005cB- c\u005c,}! 00 00 +f 2)C: /(A 00 00 +f Bab2B -\u00e9c!.[:digit:] 00 00 +f \u005c. A{1..3}/!(*()^ 00 00 +f /ba +[!b][:alpha:].^ 00 00 +f ((C:.) C:\u005c@[21 00 00 +f 1\u00e9ab cc[:alpha:]\u005c+( 00 00 +f B\u00e9ab+ (A1?c(a 00 00 +f .2 .{a,b}B( 00 00 +f cab. ][!b]@( 00 00 +f cB..+ C:+]|@@ 00 00 +f 1/(. ..|1/!(\u00e9.. 00 00 +f - ]2B 00 00 +f 21ab(B }{1..3} 00 00 +f \u005c./2A.. @C:\u005c 00 00 +f B..b\u00e9 [a-c]{a,b})-c^ 00 00 +f +B C:[:digit:].[a-c][:alpha:]a 00 00 +f a ab 00 00 +f -a+C:A C:B|.!C: 00 00 +f 2\u005c }!({a,b} 00 00 +f B **[!b]\u00e9 00 00 +f 1..B2A {a,b},[a-c]! 00 00 +f b [!b]@(!c- 00 00 +f .1.(\u005c [a-c]a+2[:alpha:]@( 00 00 +f A *2(**** 00 00 +f \u005c1....(A c 00 00 +f ab.Ab !(****( 00 00 +f + ca.* 00 00 +f C:2 !(C:A*B 00 00 +f /..-C: B[:alpha:]B 00 00 +f C:(bc b..** 00 00 +f /c+-1c .. 00 00 +f 22 1?(:}}{a,b}@( 00 00 +f ..(1b/) b!(![!b]a 00 00 +f 1\u005c\u00e9ab ?([:alpha:]!(B..(* 00 00 +f Bb !()..{1..3}^ 00 00 +f cab\u005c ]c\u005c*/@.. 00 00 +f c.ba }?(* 00 00 +f ..+2ba B*!( 00 00 +f A/\u00e9ca) ,!(]/a\u005c 00 00 +f .C:c [:alpha:]..{a,b}* 00 00 +f .. {1..3}+(?( 00 00 +f B\u005cab C:\u00e9//a!( 00 00 +f .)B+.. ),.. 00 00 +f (22 B!(,[:digit:]{1..3} 00 00 +f ..abB1BB b..C:\u005c{a,b}a} 00 00 +f .\u00e9C:)1.. 1 00 00 +f )+ )1?@( 00 00 +f b-/b..\u005c A*^B*(C: 00 00 +f 21 [[:alpha:]*(** 00 00 +f \u00e9bab\u005c\u00e92 +!(C:{ 00 00 +f A(Aa- (b- 00 00 +f .- **.!( 00 00 +f )2bA *(..^.{a,b}*| 00 00 +f b\u00e9- ]/ 00 00 +f -( *(!( 00 00 +f aba.aC:- ] 00 00 +f b11A(ab \u00e9a+B}:\u005c 00 00 +f cb.. 2?- 00 00 +f +C:ab a.- 00 00 +f 2ab-/) [:digit:]**[:digit:]B?(2 00 00 +f B..+C:(.. {1..3}( 00 00 +f +2.ab (Ac*(b?( 00 00 +f 1+(aa !({1..3}{1..3}*(../ 00 00 +f C:C:(c/ ^ 00 00 +f \u005c.-Ba- *!!(C:[:alpha:]( 00 00 +f A-\u00e9 {a,b}( 00 00 +f )aC:1B1 @( 00 00 +f )C: +C:B!@ 00 00 +f (b2b- :@C:{1..3}C:] 00 00 +f 1+-2abb . 00 00 +f \u005c/ab [!b]-C:^A 00 00 +f \u00e92.cb @( 00 00 +f \u00e9bab !}C:@ 00 00 +f C:- **[a-c]a 00 00 +f \u00e9BA... {2+( 00 00 +f ca2B [:digit:]c* 00 00 +f ..Ba-/. +{*(2) 00 00 +f A c,@( 00 00 +f .a AB 00 00 +f .ab2\u005cab\u00e9 c*( 00 00 +f -A21(/ [:alpha:]?..- 00 00 +f -)\u00e9\u005c.. 1^!(../..! 00 00 +f 1- @a\u005c1 00 00 +f \u00e9..ca [!b][a-c] 00 00 +f +.- |*** 00 00 +f -ab c[:digit:],*\u00e9 00 00 +f A2+) +* 00 00 +f .2/(.- ]?:} 00 00 +f B1B/ a[[ 00 00 +f aaa :?(@({a,b}b}*( 00 00 +f /A c{{^ 00 00 +f \u005caa- ****({a,b}** 00 00 +f c/c }c{a,b} 00 00 +f -C: ^**..{1..3} 00 00 +f bB ^?:[:alpha:] 00 00 +f 1... .[!b] 00 00 +f (\u005ca( BB{ 00 00 +f A [a-c] 01 01 +f bC:a+ C:+(ca/ 00 00 +f (b B){[!b]\u005c 00 00 +f ab(ab \u00e9*C:] 00 00 +f 1.-a\u00e9 ??(*( 00 00 +f b -@^}^+(B 00 00 +f ab-) @ 00 00 +f abA+ -?a+]^[ 00 00 +f .+Aa..A +(**A+(.a! 00 00 +f / +A[c).. 00 00 +f (/..ab. @( 00 00 +f 22a2 { 00 00 +f b ,{1..3}}A*(^ 00 00 +f -B.C:b !1?/@ 00 00 +f b+ [**b|-: 00 00 +f A(\u005c11 ,!(([@- 00 00 +f \u005c.(\u00e9 -c 00 00 +f 2..)A Ac++|{a,b}{a,b} 00 00 +f +)\u005c+( ]|[:digit:]C:] 00 00 +f abab+ ?({a,b}!b[:digit:]\u005c^ 00 00 +f ( 1\u00e9 00 00 +f )2a -: 00 00 +f /\u005c++aba ,)+!(C::] 00 00 +f b.A\u005c -2/a 00 00 +f ab+ {b- 00 00 +f +21(c.. A[a-c]/ 00 00 +f 1-2 *(}+B 00 00 +f aa+2++ )-*(^]@([:digit:] 00 00 +f \u005cb21 +?([!b]+ 00 00 +f c ?(\u005c(Ba!+ 00 00 +f abb\u00e9 [a-c]b@@(/ 00 00 +f A.(\u00e9 [a-c] 00 00 +f ab..+a +([*(}{a,b}){a,b} 00 00 +f 2A+.+A ^?(^ 00 00 +f /1Aa-a {:}+1B 00 00 +f aAB2..2 +2{b^ 00 00 +f )a..A\u005c (+([a-c]\u00e9.. 00 00 +f - [!b]*(\u00e9[b 00 00 +f /C:/\u005c\u005c\u005c }*(**[:digit:])B 00 00 +f ++1c [1 00 00 +f A (**\u00e9+2{a,b} 00 00 +f a/(.a ,[:alpha:]c@( 00 00 +f 1abA\u00e9.. @..@(**.+(? 00 00 +f 1A(\u005c-/ {?(!(][:digit:]} 00 00 +f bcA++ *c{1..3}^\u00e9a 00 00 +f C:\u005c(/ **}[a-c]**+} 00 00 +f C:2/A..ab | 00 00 +f \u00e9abC:..1 { 00 00 +f Ac-/.B )+(: 00 00 +f Aab1 **1b+? 00 00 +f 1)..\u005c {a,b}@(+***({1..3} 00 00 +f 2C:\u00e9/c (*(@( 00 00 +f A-. [ 00 00 +f 1 /[a-c]! 00 00 +f +A\u005c)( | 00 00 +f ..a2/2\u00e9 2+[:). 00 00 +f c2 2C: 00 00 +f ab/2( **+(b(A 00 00 +f ( @[:alpha:]B\u005c* 00 00 +f \u005c...12 C:({1..3}[:digit:]1+(* 00 00 +f \u00e9a.B A[a-c]@() 00 00 +f \u00e9 *b{C:{ 00 00 +f ..\u00e9-/ [:digit:] 00 00 +f b()a {@(||?A[a-c] 00 00 +f +C: {a,b} 00 00 +f \u00e9/abcC:2 1b 00 00 +f 1 2(..@(!B 00 00 +f abC:)( /c?( 00 00 +f 12 1c{a,b}@C:, 00 00 +f -B\u00e9(C: [ 00 00 +f ab( ?1!2]/, 00 00 +f ba+/ \u005c 00 00 +f C:)\u00e9)\u00e9b a?(}{a,b}@ 00 00 +f 21 ,+ 00 00 +f /\u00e9C: /!1b!( 00 00 +f 1 B 00 00 +f A..+\u005c ?(**![!b]a*( 00 00 +f ... {a,b}+(/\u005c[!b] 00 00 +f c)C:.. !(: 00 00 +f +B)../ b{{a,b}|!/a 00 00 +f 1/.). [a-c]|\u00e9C:*** 00 00 +f (acab2- ..*(1 00 00 +f a\u005c- {a,b} 00 00 +f (2- {+[:digit:](?( 00 00 +f B/.C:) C:\u00e9] 00 00 +f A\u005c11..2 ^ 00 00 +f .a+1 \u00e9{a,b} 00 00 +f +)\u00e9 )2?{1..3}\u00e9 00 00 +f ( a..a 00 00 +f C: [!b][a-c]}**\u00e9)\u005c 00 00 +f \u00e9/2ab.2 .[/ 00 00 +f /BA [+(!(-,){a,b} 00 00 +f + A)1] 00 00 +f Bbab/\u005cC: \u00e9?|}: 00 00 +f (\u005cabab ^[!b][!b] 00 00 +f a..B {+(*({a,b}b}{a,b} 00 00 +f 1\u005cabb+ }[a-c]b^..@(? 00 00 +f ()\u00e9 !( 00 00 +f (B2A [a-c]?[:alpha:]! 00 00 +f /\u00e9aabC: ) 00 00 +f 1 c-\u005c.}@([ 00 00 +f )\u00e9 [:digit:]B{1..3}}12[ 00 00 +f 2\u005c.( ..**@ 00 00 +f /C:.c {{ 00 00 +f b\u005c + 00 00 +f c(abA .. 00 00 +f C:\u00e9.aa\u00e9 )**?(!( 00 00 +f -+-2\u00e9\u005c 2c1{a,b}++(+( 00 00 +f +\u005c/C: {a,b} 00 00 +f C:2cA/ ?(}\u00e9- 00 00 +f +a *(, 00 00 +f \u005c2\u005cab :{a,b}[{1..3} 00 00 +f ... A..[ 00 00 +f (\u00e9 C:..- 00 00 +f ac [:digit:][:alpha:]bc[!b][a-c] 00 00 +f \u005cabc C: 00 00 +f (\u00e9\u005c !( 00 00 +f b ?( 00 00 +f 2\u005cAbC: +([:alpha:]![a-c]\u00e9*(c 00 00 +f -.. .2 00 00 +f aa..1 +c1 00 00 +f -ab.- *(|@({a,b} 00 00 +f -/ \u00e9[(}?( 00 00 +f a+ab....B |{1..3}2.2 00 00 +f 2C: +) 00 00 +f /abc1( @(^!(,{a,b}?([:digit:] 00 00 +f /1a @ 00 00 +f ab2/\u00e91 cC:[:digit:]+( 00 00 +f )-c/ }[!b]b[!b]\u005c[!b] 00 00 +f c. -^/|@( 00 00 +f \u00e9A+. c{1..3}a! 00 00 +f b(abab c\u005c.. 00 00 +f ab( 2{1..3}| 00 00 +f \u005c\u00e9.. \u00e9aa 00 00 +f 2/. !]..^ 00 00 +f \u00e92) 2\u005c*(|+*(! 00 00 +f (b)B). [:digit:]].. 00 00 +f A\u005c11ab/ ]( 00 00 +f (cc\u00e9 }{1..3}[a[^b 00 00 +f C:C:A(+ C:^C:c 00 00 +f )ab ] 00 00 +f \u005c)/b\u005cab )**1 00 00 +f /abB.b+ @{a,b}1]!- 00 00 +f \u00e9\u00e9\u00e91/) B 00 00 +f A\u00e9 a^\u005c{a,b}/ 00 00 +f +)a.)1 ){a,b} 00 00 +f .a2c a?(|A 00 00 +f +ab2..b :}1 00 00 +f ab2 [a-c]B/[!b][!(** 00 00 +f c/11 @(a\u00e91[a-c] 00 00 +f C:..c)bc /|-A 00 00 +f \u005c..A2.. ..?!([:digit:][.. 00 00 +f B..cc !({@(\u005c 00 00 +f abaB\u00e9\u00e9C: [a-c][:digit:]c 00 00 +f .ab *( 00 00 +f (\u00e9/+-b /?/||,- 00 00 +f -ab2 c?[:alpha:] 00 00 +f ./B\u00e9ac )**B@@(} 00 00 +f aA.(\u00e91 ,!+[a-c] 00 00 +f C:\u005c ][:digit:]\u00e91 00 00 +f \u00e9)ab\u00e9C:/ {a,b}a 00 00 +f ..B *(c 00 00 +f .)c\u00e9../ |, 00 00 +f / ,a 00 00 +f C:cab) [A[[:alpha:] 00 00 +f aA \u005c@C: 00 00 +f ..\u005c.B/- B@({ 00 00 +f 1 @([!b](B 00 00 +f b..\u005c )} 00 00 +f A \u005c\u00e9* 00 00 +f a *(*(,2+!( 00 00 +f /\u00e9B b.. 00 00 +f Bb\u005cc *?(|?(A 00 00 +f BC:. |B 00 00 +f C:(1 {a,b}a** 00 00 +f )b A?(*(.]{1..3} 00 00 +f cC:\u00e9.(\u005c /([:digit:]A}!( 00 00 +f (-A/ {1..3}cA[!b]... 00 00 +f b..a .aC:a 00 00 +f ca a!(-[:digit:]}[a-c] 00 00 +f /a\u005c +@(\u00e9+b2B 00 00 +f \u00e9b ?(//***(@(\u005c 00 00 +f a1/(a \u00e9c)*/ 00 00 +f . \u005cB{1..3}\u00e9C: 00 00 +f A b[a-c]. 00 00 +f A())B .!(..*[!b] 00 00 +f A)..ab\u00e9 ( 00 00 +f B11 |@(+(2+( 00 00 +f b-2..Ac (\u00e9[@bA 00 00 +f ab\u00e9 {[a-c]\u00e9{+( 00 00 +f 1a ?[:digit:]\u00e9\u005c 00 00 +f AaA+ 1{a,b}-a:[ 00 00 +f ..\u00e9.. ..[!b] 00 00 +f )bc) .@( 00 00 +f A1BC: {a,b} 00 00 +f Aa-((C: /*:, 00 00 +f abA\u005c 1a\u00e9*( 00 00 +f ab {1..3}+( 00 00 +f C:..abA ^B\u00e9@(}b*( 00 00 +f \u00e9C:B2B @bb( 00 00 +f -AA) /@(,-AB 00 00 +f \u00e9b..-). -\u00e9]+(c\u00e9 00 00 +f c-+) :@(@ 00 00 +f \u00e9..\u005cC: [:digit:]....** 00 00 +f 1-+. !( 00 00 +f +aC:a /c]!(2 00 00 +f ..bC:c [a-c][a-c]1!(a\u005c[a-c] 00 00 +f abB..b [:digit:]:?/[a-c]] 00 00 +f ()\u00e9\u005c/ {ac}[:digit:]+[ 00 00 +f \u005c ? 11 00 +f +21(2( )}\u005cBb1( 00 00 +f b...B ..[!b]b. 00 00 +f \u00e9- \u005c*?(- 00 00 +f \u005cbab-B )@( 00 00 +f b1.. [a-c][!b]\u005c2B+( 00 00 +f -)A +(|/..!*(b 00 00 +f abc2 **C:+ 00 00 +f Aa)b (2...?( 00 00 +f 1+1 !1C:!(B?(+ 00 00 +f \u005c..cab+ 1 00 00 +f ab [/[B^{1..3} 00 00 +f ab -[@**?({^ 00 00 +f c./-ab [!(^ 00 00 +f 2C:.. !(B*( 00 00 +f ...2+) :B 00 00 +f a [:digit:]a[:digit:]::* 00 00 +f )Bc1A1 )-[:alpha:]) 00 00 +f abC:ab A|- 00 00 +f abb./( \u005cB?^b[!b] 00 00 +f a ?(\u00e9\u00e9? 00 00 +f )ab22ab -[a-c]\u00e9?(** 00 00 +f C:c1 [:digit:]+ 00 00 +f ) [:digit:]!(?(,?(!( 00 00 +f )b [a-c]?(? 00 00 +f b- 2**+) 00 00 +f - *C: 00 00 +f \u00e9+(Baab |** 00 00 +f cB\u00e9. *}][a-c]??([:digit:] 00 00 +f b(-1\u00e9( -[2Bc] 00 00 +f . )A 00 00 +f 11+-1a \u005cb!(^ 00 00 +f b2\u00e92) b!|c@}: 00 00 +f ..\u005c ,*(.(A../ 00 00 +f -/\u005ccC:. [:digit:][a-c]}\u00e9] 00 00 +f . :{...b 00 00 +f a-1A/b {a,b}@*..?(A? 00 00 +f Aab (A 00 00 +f + [?c 00 00 +f ((c\u005cC: b 00 00 +f 1)(. [:digit:]]c^@([:alpha:] 00 00 +f )C:+B ])@* 00 00 +f 22\u005c..b- ,C:** 00 00 +f /.1aC: B{\u005c 00 00 +f ) ,a{a,b}?}a[a-c] 00 00 +f -( /![ 00 00 +f abAA((.. \u00e9-]. 00 00 +f c.-\u005c)1 ||[:alpha:] 00 00 +f ab(a 2[!b]?@(\u00e9! 00 00 +f .A1c } 00 00 +f +A\u005c1.. **?( 00 00 +f B :(c\u00e91..?( 00 00 +f c A} 00 00 +f ....Bb) ] 00 00 +f -)+ [A[\u00e9 00 00 +f bB.. **\u005c 00 00 +f c (?[:alpha:] 00 00 +f a/ ..{1..3}{/**{a,b} 00 00 +f .+)(B : 00 00 +f +(/2 [:alpha:]{}.+(- 00 00 +f 2c {C:C: 00 00 +f -\u005c../+ {a,b}{a,b}-}-[:digit:] 00 00 +f ab2\u00e9..( -{[:digit:]..** 00 00 +f \u005c\u00e9++ [:alpha:]{a,b}b** 00 00 +f c(c.A Bc:C:** 00 00 +f -c @aA[:alpha:]-2! 00 00 +f \u005c\u005cB(C:2 ^{1..3} 00 00 +f \u005cB2+c\u005c !*(^\u005c 00 00 +f Bab ^c[a-c]@+ 00 00 +f A\u00e9(c | 00 00 +f cb /,{a,b}A(2 00 00 +f .\u005ccbA2 ?)[:digit:] 00 00 +f B-/ba .]b1 00 00 +f ab\u005c [!b]]{1..3} 00 00 +f \u005cA(C:C:) *(!(\u00e91 00 00 +f C:b2-b {a,b} 00 00 +f 2a/+)c .A** 00 00 +f (.. ,|{1..3}..@!(- 00 00 +f .AC: ?^a 00 00 +f a [a-c].?!c 00 00 +f (\u00e9)\u00e9 [:digit:][:digit:]|b^! 00 00 +f c\u005c ]B 00 00 +f + [:digit:]{1..3} 00 00 +f c\u00e9bAc- !^ 00 00 +f ba {a,b}?(- 00 00 +f + @(} 00 00 +f 1Bbc/ .. 00 00 +f 2Bab Abb*(b 00 00 +f C:c\u00e9 ]!(( 00 00 +f + [!b]/* 00 00 +f C:..ab .![@ 00 00 +f \u00e9.. / 00 00 +f + [}}*(b\u00e9+ 00 00 +f C: /\u00e9{1..3}2 00 00 +f ab ?( 00 00 +f ( [!)A 00 00 +f A !( 00 00 +f C:.. }A 00 00 +f )..++Bb *..2( 00 00 +f ab\u00e9abb ..? 00 00 +f \u00e9.-1)B ?(,: 00 00 +f -(A .C: 00 00 +f ..a2\u00e9 [:digit:]1 00 00 +f ).b1.. {1..3}|2 00 00 +f .)B2 b 00 00 +f c\u005cC:c1A ..}.[!b][:digit:][:digit:] 00 00 +f ((/ ]^!(!(+(!) 00 00 +f B )-\u005c\u005c 00 00 +f a\u00e91/ ]{1..3}(+-[ 00 00 +f C: }...*(1 00 00 +f ( -*(^!(][+( 00 00 +f Ab\u005c.A. @(+C:c 00 00 +f B..+/AA ?c!* 00 00 +f C:\u00e9 [a-c]: 00 00 +f cbab\u005c)( )\u00e9A* 00 00 +f (+ab {b[a-c]*[:digit:] 00 00 +f \u005c1 (B,:[* 00 00 +f aC: +(:?(c** 00 00 +f \u00e9/ [ 00 00 +f .. B{a,b}A!(b 00 00 +f -/C:\u00e9 *(2c|{1..3}- 00 00 +f 11ababC:1 A/+\u005c@(. 00 00 +f ab ,2C:+\u005c\u00e9 00 00 +f abaa)b @({a,b}a 00 00 +f +bA -A1 00 00 +f \u005c+2/ (1!. 00 00 +f 1 *([!b]]| 00 00 +f Abb)C:- -..|@(^ 00 00 +f /b {? 00 00 +f BA/((\u00e9 -+*[1!(** 00 00 +f b...\u00e9/\u00e9 [+?c}, 00 00 +f ab\u00e9 !+C:[!b]?(\u005c 00 00 +f \u00e92 !([:digit:]?( 00 00 +f 1A\u005ca. !([ 00 00 +f .cab !( 00 00 +f A+.\u005c \u005c!(+(|@ 00 00 +f 2 ,1@[a-c]{a,b}+(\u00e9 00 00 +f +... +(ca 00 00 +f 2A [?({@ 00 00 +f c2Aa b+(B2, 00 00 +f +1 }C:- 00 00 +f -ab..b+ \u00e91\u00e9*(..{ 00 00 +f -1 {*({+**[:digit:]** 00 00 +f (.A. [!b]...[:digit:] 00 00 +f baab-+.. [:alpha:],.\u00e9..2( 00 00 +f 1bB2 2)\u005c[:digit:],.. 00 00 +f Bc. ,B((! 00 00 +f .aab (2 00 00 +f .- ?2 00 00 +f + .{a,b}..}{a,b}\u005c 00 00 +f 1(cBcab |[:digit:])!(\u00e9/!( 00 00 +f B \u005c2 00 00 +f 2+C:2c ! 00 00 +f ab\u005caba @A) 00 00 +f 1b .B 00 00 +f )Aaab }!( 00 00 +f abc..ab [:digit:]+C:@(!)*( 00 00 +f AaB.1 . 00 00 +f 1.. A:A 00 00 +f .....\u005cB ?(+( 00 00 +f C:++C:cc *+(\u00e9\u00e9[:alpha:]^ 00 00 +f \u005c c 00 00 +f AA *(\u00e9{a,b}:{a,b}*.. 00 00 +f +- cc{1..3} 00 00 +f C:\u00e9 \u005c?2\u00e9\u005c} 00 00 +f a/ab ?\u00e9. 00 00 +f 1+\u005cc\u00e9 b[:alpha:][a-c]?( 00 00 +f C:ababc ?![c 00 00 +f c/C:2)2 {[!b]:*{a,b}[:alpha:] 00 00 +f -..c /AA 00 00 +f 2\u005c).b2 }/}{a,b},! 00 00 +f -AA/ (A\u00e9 00 00 +f 1. +(1{1..3}@ 00 00 +f bB \u00e9c22 00 00 +f B2B( aA{1..3}] 00 00 +f a - 00 00 +f ..\u005c2 )[a-c]+( 00 00 +f 2. [:digit:][!b][:digit:] 00 00 +f c @A**B!-B 00 00 +f \u005c(1\u005ca @.[a-c]?|*( 00 00 +f B\u00e9..\u005c2 ,[!b] 00 00 +f a+b../B !(*([B[:digit:].] 00 00 +f a ..**2[ 00 00 +f A)BA ?) 00 00 +f ..c-/B1 @..^A 00 00 +f - ]{a,b}[:digit:] 00 00 +f .b.2 ?!( 00 00 +f 2cb {1..3}(C:2 00 00 +f )aba B?([!b] 00 00 +f A b{a,b}@([:digit:] 00 00 +f ab. A[a-c]{a,b} 00 00 +f AA(12 !@(\u005c 00 00 +f 1AAAa \u005c{a,b}2 00 00 +f b( ^ 00 00 +f ababb2+ +(|{1..3}B^! 00 00 +f \u005c)) *|[a-c] 00 00 +f A.. \u005c{1..3}?(A 00 00 +f abC: {a,b}:{a,b} 00 00 +f 2) ^[:digit:]{1..3}a** 00 00 +f -2/\u00e9 [:alpha:],/ 00 00 +f (( 2 00 00 +f +AC:..a) + 00 00 +f \u00e9(1 1@(@1{[:digit:] 00 00 +f .\u005c.+)C: - 00 00 +f Acab- A[:digit:])[!b] 00 00 +f \u005c/\u005c. {[:alpha:]/[:digit:]A@[!b] 00 00 +f ..ab\u00e9A }{a,b} 00 00 +f b.( |b{a,b}{1..3} 00 00 +f -b2(c ?( 00 00 +f c +(c.|,{ 00 00 +f 1)C:+ a|{a,b}[] 00 00 +f (b2 @(* 00 00 +f c2 /\u00e9|?(C:2 00 00 +f c/+2\u005c\u005c /*[:alpha:]( 00 00 +f +2b +(|([:digit:]1[ 00 00 +f cb1\u005c.\u005c ,+{a,b} 00 00 +f 2+ }@({1..3}?(b@ 00 00 +f bB1\u00e9( )+:[:alpha:]{a,b} 00 00 +f 2 ^: 00 00 +f 1( ) 00 00 +f \u005c -])+*) 00 00 +f BC:C:a- @,2!(c 00 00 +f C:cc.1C: ?(]2\u005c,@({a,b} 00 00 +f \u005c..c(/ @@1/A? 00 00 +f ... +*(Ba.. 00 00 +f b/abB\u005c ?( 00 00 +f (\u005c ,+( 00 00 +f \u005c-++.. ,B. 00 00 +f a c 00 00 +f ..b +({/ 00 00 +f b)../) ?]*(+(+( 00 00 +f /1 [!b] 00 00 +f ab ?(,*[:digit:](!( 00 00 +f \u005c/AaAB B/b: 00 00 +f C:B.. {a,b}BB!(2 00 00 +f +a/ /!(A\u005c 00 00 +f /)) ^a+!:/^ 00 00 +f ab1c *(1**^ 00 00 +f aBab--b b@bB 00 00 +f 1/BAcB }{1..3}}A 00 00 +f A.\u005c.. [!b]C:.[a-c][:digit:] 00 00 +f 2B)- **a 00 00 +f C:b.C: 2 00 00 +f C: 1! 00 00 +f a(\u00e9\u00e9)/ 2c@(a\u00e9**{1..3} 00 00 +f (A+1 -{1..3}[!b][:alpha:])!a 00 00 +f )C: *({1..3} 00 00 +f c [:alpha:]{B.{a,b} 00 00 +f ..-)A .2a|* 00 00 +f -/c( }[**{1..3} 00 00 +f cA2 1c@(** 00 00 +f 2ca/)B \u005c[a-c]c1 00 00 +f cA-2B/ +({1..3}{a,b}[:digit:] 00 00 +f ab [:alpha:]..-C:a*(A 00 00 +f abbA\u00e9\u005c a[:digit:] 00 00 +f ..)/2( |*( 00 00 +f \u00e91)2B ]!! 00 00 +f a-ab [ 00 00 +f \u005c)\u005cB {a,b}[!b]^ 00 00 +f \u00e9+b./1 /1\u00e9?( 00 00 +f -a2 **|/[a-c]C: 00 00 +f Ba)a+ !+cbc 00 00 +f ..( ***([:alpha:]A[!b]!a 00 00 +f ab1C: @*(C: 00 00 +f A(..)-( ..}@?(? 00 00 +f C: [:alpha:]2..b 00 00 +f (- @(\u00e9..| 00 00 +f -BA @(([a-c]1B 00 00 +f )/ b 00 00 +f b(((\u005cB b\u00e9[**!(^@( 00 00 +f Bab.a +\u005c)A[a-c] 00 00 +f A-b (@(B(b 00 00 +f b-2 {1..3} 00 00 +f A\u005c a.!1*(**} 00 00 +f (((-+c (a\u005c*(]-| 00 00 +f B A!(@), 00 00 +f aab 2+[!b]a:/ 00 00 +f \u005cb.. [ 00 00 +f A12)b : 00 00 +f \u00e9\u005c+ 2- 00 00 +f /1. A[!b] 00 00 +f / A 00 00 +f .abC: -| 00 00 +f /\u00e9\u005c) @(*(@(.{a,b}[a-c]| 00 00 +f b12Bc !/[+(^ 00 00 +f /.) -][+{1..3}+({ 00 00 +f a). ,:(][:digit:]{? 00 00 +f 1 @({a,b}A/ 00 00 +f C:+c {a,b}c*( 00 00 +f -a \u00e9 00 00 +f \u005cB+.- A2} 00 00 +f (ab+Aa- aC:{1..3}-a[!b] 00 00 +f B2 \u00e9[[:digit:]{*(+( 00 00 +f 1C: A).,1[:alpha:]{ 00 00 +f -2b1 [a-c] 00 00 +f \u005c 1?(\u005cA{a,b}{1..3} 00 00 +f //) bA[a-c]?() 00 00 +f )a)..1 ,[:alpha:]\u00e91C: 00 00 +f c\u00e9-ab.. )?( 00 00 +f )BC:( ) 00 00 +f ..2a1/ a,([!b]\u00e9 00 00 +f -Bcc **B[:digit:]\u00e9[:alpha:]b** 00 00 +f a **!{a,b}*.. 00 00 +f ... *11[!b][:alpha:]/ 00 00 +f B)C:2 ..[:digit:]\u00e9@( 00 00 +f 2cBA2a /B( 00 00 +f a @1.. 00 00 +f A1abab {1..3}[[a-c]A: 00 00 +f (ca))ab ...2c2)( 00 00 +f .1+ab2/ /[!b]/[a-c]2@(!( 00 00 +f ab/ ?((]},..[:alpha:] 00 00 +f 2 [:digit:]|*** 00 00 +f .-- {1..3}/*}{1..3} 00 00 +f ).a- ,a**bC:2+( 00 00 +f )(C:A\u00e9\u005c .!(}c{1..3}\u005c 00 00 +f (b\u005cab2 b@(\u005c.. 00 00 +f .. +2 00 00 +f 1 !^C:C:*)^ 00 00 +f \u00e9)abab [a-c]?[2].. 00 00 +f (+- +(**+(*( 00 00 +f )\u00e92)aA [a-c][a-c]:......1 00 00 +f abab :\u005cb[:alpha:]\u00e9[!b]{1..3} 00 00 +f ..C:..ab {\u00e9[a-c]}[ 00 00 +f 12ca1 !(C:\u00e9 00 00 +f BBa) :...?(,@ 00 00 +f 1..- C:^ 00 00 +f /b ]*..[!b] 00 00 +f 22B ...[:digit:] 00 00 +f bbc C:?({^! 00 00 +f .B @b 00 00 +f )Bab. {A[a-c]+** 00 00 +f \u00e9\u00e9 /!( 00 00 +f aC:C: [a-c][!b]| 00 00 +f 1bBa+( a**\u00e9?@( 00 00 +f ) -[:alpha:]A1}| 00 00 +f (b C: 00 00 +f 12+(1. ]-@( 00 00 +f C:22)/ b..** 00 00 +f ab\u00e9\u00e9( -{a,b} 00 00 +f B(+b [!b]).C:]} 00 00 +f C: \u005c 00 00 +f bC:b. @...+ 00 00 +f .1c b 00 00 +f (2Ba [:alpha:]((.[+ 00 00 +f abC:./ b.*/:{1..3}?( 00 00 +f -ab [:alpha:]!C:[c 00 00 +f \u005cC:ab +b 00 00 +f (- !( 00 00 +f B(\u005c2 A\u005c?([:digit:] 00 00 +f AC:B/ [:alpha:]**^ 00 00 +f 12 @[:alpha:](] 00 00 +f (... )C: 00 00 +f B.b ]b2/[a-c] 00 00 +f c)aba- Bb[,] 00 00 +f . @(-2 00 00 +f /2b(C: c 00 00 +f (aC:(\u005c \u005ca@ 00 00 +f C:2 ..?*?\u00e9} 00 00 +f 2 C: 00 00 +f 1\u00e9 b2C:[|1\u005c 00 00 +f \u00e9\u005c ?(* 00 00 +f 2b C: 00 00 +f B. ]+({{+(a 00 00 +f 1abBBac !**}*(?[:alpha:]} 00 00 +f )c ][a-c]{\u00e9 00 00 +f a/( / 00 00 +f (2A\u00e9A 2@[:alpha:]C: 00 00 +f \u005cab.cA.. (*[AA*(( 00 00 +f -aC:C:C:ab c.[:digit:]*(+ 00 00 +f .. [!b]- 00 00 +f ab2c\u00e9/ * 11 11 +f \u00e9A { 00 00 +f ab+ @(+[:alpha:]A1/c 00 00 +f \u00e92+ @+(] 00 00 +f c |\u005c.?@! 00 00 +f 1 @! 00 00 +f 2. !( 00 00 +f ..2) {[}c\u005c 00 00 +f a( ***^*(+( 00 00 +f /bC: [:digit:] 00 00 +f (Aa |.!([:digit:]. 00 00 +f Abab11 [!b]!((]-[a-c]2 00 00 +f \u005c C:@[a-c]) 00 00 +f )-A }!(?( 00 00 +f \u00e9 B[:alpha:][[!b]C:@ 00 00 +f (\u00e9(+.. ?(/..} 00 00 +f )AC:Ba\u00e9 +(??+([a-c], 00 00 +f +/2A) + 00 00 +f -C:b1ab.. \u00e9.|]1:{ 00 00 +f \u005c- *(:a)^+1 00 00 +f /.))/\u00e9 !(?(][a-c]+(?({1..3} 00 00 +f c-B2B/ cb[:digit:] 00 00 +f +2)aA !(**/A 00 00 +f \u005c/b\u00e9 b*(c[) 00 00 +f )..)\u00e9a C:. 00 00 +f C:a\u00e9-2B [:alpha:]1^?1,*( 00 00 +f aB\u005c(c ] 00 00 +f . {a,b}2-A|.. 00 00 +f AA. a 00 00 +f /2 [+(cA 00 00 +f /AA-c b?{1..3}[:alpha:]^@( 00 00 +f abB\u00e9B : 00 00 +f +.. [[a-c][a 00 00 +f A-ab {a,b}{ 00 00 +f A-)a(\u005c 2c@(!(a 00 00 +f 2+a. **,@( 00 00 +f \u00e9 ]!({)|-** 00 00 +f -B\u00e9)..ab [@ 00 00 +f + [a-c]{@[:alpha:]b{a,b} 00 00 +f C:-a.. [:digit:]A{+ 00 00 +f (a\u00e9 @@(-C: 00 00 +f B\u00e92A !.. 00 00 +f 2\u005c((- } 00 00 +f \u005cab+A ?{a,b}!([a-c]1[:digit:] 00 00 +f .. b@(]. 00 00 +f /(ab b 00 00 +f + -[:digit:] 00 00 +f 2ac... ^,*(+^ 00 00 +f C:b/\u005c.. @@.\u00e9?(1} 00 00 +f .(C:/ */@\u00e9-,+ 00 00 +f \u005c c!(!^12+( 00 00 +f c++1A /B. 00 00 +f /1A {a,b}{,{a,b}C:?({1..3} 00 00 +f a/\u00e9\u00e9 **)|^(^ 00 00 +f a. \u00e9@B[!b] 00 00 +f +a1 2..[+( 00 00 +f \u005ccb)C:/ +(\u00e9C: 00 00 +f a.b /)( 00 00 +f B/. ]B 00 00 +f B( 1[ 00 00 +f )C:.. \u00e9+.!(^+(a 00 00 +f B }+([:digit:]: 00 00 +f /(/ [ 00 00 +f ))\u00e9 b{1..3} 00 00 +f \u005c2aC:1B **@( 00 00 +f 1(/ -!1\u005c 00 00 +f \u005c\u005c1 @} 00 00 +f ..121\u00e9 b)[@(^ 00 00 +f \u00e9Bab1+ *(b 00 00 +f b.. ,ba@( 00 00 +f 1B+2Bab {1..3}|[*?{ 00 00 +f bBa [!b]+C:(, 00 00 +f ABA [ 00 00 +f ab2abc..a [:digit:]*)b 00 00 +f / ! 00 00 +f a2\u005c {1..3} 00 00 +f .c ) 00 00 +f )\u005c..(/\u00e9 [:alpha:]B@(?(B+[ 00 00 +f //) **..\u00e9[ 00 00 +f 1\u00e9abab\u00e9\u005c { 00 00 +f b)\u005c 1.?(( 00 00 +f .2 +({[!(? 00 00 +f /c\u00e92+C: +]b 00 00 +f 2+(..2 b 00 00 +f .. [a-c].?*A 00 00 +f C:\u005c+2\u00e9 { 00 00 +f b- [a-c] 00 00 +f B)( b!( 00 00 +f ab2) @(..,1c 00 00 +f ab+ !(?(^.{1..3} 00 00 +f A {a,b}|@?*?( 00 00 +f \u00e9\u00e9C:C: -a{a,b}[:digit:] 00 00 +f ..B ?(B) 00 00 +f a- {1..3}*(^[:digit:] 00 00 +f 1(/B [!(1..!(?( 00 00 +f aA+.\u005c+ ,[:digit:]!) 00 00 +f aab1 !(!( 00 00 +f -/ @] 00 00 +f \u00e9....\u005c ... 00 00 +f ).( ?!(,( 00 00 +f cBA-b /@(+({1..3}[ 00 00 +f ( [:digit:][:alpha:]]cc 00 00 +f ++ C:![!b]a+.! 00 00 +f abcbc( +( 00 00 +f A1 B@ 00 00 +f () @(2 00 00 +f c +(,([a-c][!b] 00 00 +f a\u005c(( !( 00 00 +f \u00e9.. [:alpha:][+( 00 00 +f b++ab1 2{2,+(, 00 00 +f A !(@(+( 00 00 +f (C:B22 +(!([a-c]][:alpha:] 00 00 +f +Bc)+A .@.. 00 00 +f +a\u00e9( ?(b 00 00 +f ( )!(** 00 00 +f (c\u005cab *{[a-c]!([:alpha:][:alpha:] 00 00 +f b+c/\u00e9ab B{a,b}B!{1..3}^ 00 00 +f \u00e9)..+( [:digit:]B1 00 00 +f ab(..+2 @:\u00e9@(22 00 00 +f B) [+A( 00 00 +f 1\u00e9(\u005c11 BA/{a,b}-[:digit:]{1..3} 00 00 +f B \u00e9^]*(2) 00 00 +f BAC:A *(b| 00 00 +f (b2\u00e9b. [!b][a-c])..:\u00e9} 00 00 +f \u00e9..2.( * 11 11 +f +aabc1b [!b]**!(..( 00 00 +f C: [/**^ 00 00 +f \u00e9bC: A+?( 00 00 +f c C:^}}{.. 00 00 +f c..b--- **[:digit:][!b]:@( 00 00 +f 1-cA c{a,b} 00 00 +f .Baa \u00e9[:digit:]!^:[a-c]** 00 00 +f 1b22 \u005c\u00e9b 00 00 +f \u00e9( b{a,b}} 00 00 +f ab2abB b[:digit:]( 00 00 +f A1\u00e9C:2) aA*( 00 00 +f +-\u005c \u005c* 00 00 +f cB-ab *b 11 11 +f +1 *!b 00 00 +f 1-\u00e9B(c \u00e9..:*b{a,b}b 00 00 +f C:\u00e9ab +(,@A*^+( 00 00 +f 1A.2 ?c21{1..3}?(A 00 00 +f \u00e9a..\u005c ] 00 00 +f \u005c.bC: !(2b 00 00 +f b :B 00 00 +f 1ab((1 b):B*(b 00 00 +f \u005c bB| 00 00 +f b )c^)b{1..3}[:alpha:] 00 00 +f b {[!,( 00 00 +f .ab. bA 00 00 +f B( +(.|@([a-c] 00 00 +f B2ab A[:digit:] 00 00 +f \u005c(2 B)C:B@*(- 00 00 +f b ^@( 00 00 +f A +2 00 00 +f C:c\u00e9 !\u00e9C:/\u00e9 00 00 +f 2(\u00e9A ^/{a,b}?!+( 00 00 +f \u005c..C:..ab c 00 00 +f )2+. . 00 00 +f ab \u00e9[{ 00 00 +f C: }+/ 00 00 +f /C:1B1b (!(]@([!b] 00 00 +f +C:ba12 1!( 00 00 +f A/--A b2..^@2{a,b} 00 00 +f 22 2\u005c{1..3}(]{[:alpha:] 00 00 +f 2(C:B2 b:[[!{a,b} 00 00 +f A [!b] 11 11 +f -c C:**{a,b}..}[:digit:] 00 00 +f BC:.. [\u00e9c+^: 00 00 +f AB\u00e9bc ,B{a,b}: 00 00 +f c..\u00e9A - 00 00 +f ( .[:alpha:] 00 00 +f \u00e9Ba C:[:alpha:] 00 00 +f C:)) / 00 00 +f +1 !()) 00 00 +f B [a-c]\u005c^?^ 00 00 +f ab }{2.+ 00 00 +f /.a\u00e9ab *({a,b}-: 00 00 +f .C: ** 00 00 +f Ac/c/ BB@^*[!b] 00 00 +f ....).ab B1 00 00 +f .A ][:digit:]\u00e92] 00 00 +f c\u005cC:ab [!b]][( 00 00 +f c.-ba/ \u005c\u00e9^(!+(? 00 00 +f \u005cabb ]+** 00 00 +f .)c\u00e9B- {{1..3}C: 00 00 +f (B ^ 00 00 +f )2..1 -a.[!b] 00 00 +f a.c2a 1bb@(, 00 00 +f \u00e9\u00e9+ \u00e9{C: 00 00 +f A.2a [C::*[!b] 00 00 +f / .*[:alpha:] 00 00 +f abb *(,*:[!b]2 00 00 +f A 2, 00 00 +f aab2/ab. \u005c@{1..3}(!(a 00 00 +f C: ..1@) 00 00 +f ..(c\u00e9 ) 00 00 +f 1( ){1..3}+(}+} 00 00 +f Aa) *({a,b} 00 00 +f .111cA [a-c]..[:digit:](a@ 00 00 +f abB 1-[!b]]| 00 00 +f 2.- /|B[!b]/?(. 00 00 +f (+ 2/[ 00 00 +f ..a-(2\u00e9 [:alpha:]*(2 00 00 +f /\u00e9+2ac {a.\u005c( 00 00 +f 1\u00e9B/ +([a-c]@{1..3}2 00 00 +f \u005c)C: ,{}1?(: 00 00 +f Bb }[:digit:]} 00 00 +f / ?C: 00 00 +f 1(1.. ^{@^{1..3}-@( 00 00 +f ab.B\u005c *2+( 00 00 +f B {a,b}B@A@((!( 00 00 +f C:\u00e9C:\u005cC:c [a-c](b-!({a,b}] 00 00 +f \u00e911/ ,{-\u00e9 00 00 +f /Babcb( ^ 00 00 +f \u00e9B. ! 00 00 +f b\u005c @}[a-c] 00 00 +f 1(A \u005c.| 00 00 +f BAAc @(@{1..3} 00 00 +f ..) ,b,A 00 00 +f \u005c-2C: @(^*B 00 00 +f - A[a 00 00 +f +./.. +[:alpha:]C:!([:alpha:]A{1..3} 00 00 +f \u005c(A\u005c.a ^ 00 00 +f cA )!+([:alpha:]*(@+ 00 00 +f (/\u00e9. [:digit:]c2[:alpha:]?([:alpha:]* 00 00 +f .C:\u00e9-bb ? 00 00 +f 1(b }[:digit:]{\u005c*({a,b} 00 00 +f A a\u00e9{a,b}-+(1 00 00 +f BB(\u005c b@(.**]\u00e9?( 00 00 +f \u00e9ab2ab\u00e9 .?([:digit:]+\u00e9] 00 00 +f B\u00e9/2 @({:{*{a,b} 00 00 +f .AA1aA \u00e9!}[:alpha:]a 00 00 +f +/1\u00e9/ C: 00 00 +f BC:2c+ | 00 00 +f ..C:- * 00 00 +f )2 [:alpha:]. 00 00 +f \u00e9/ ?..*{a,b}[:alpha:] 00 00 +f cC:1) B*()b 00 00 +f a\u005cC: } 00 00 +f BB\u005cB [@, 00 00 +f B *( 00 00 +f .. A 00 00 +f \u005c. ..1}\u00e9[a-c] 00 00 +f C:c) [a-c][!b]{a,b}[:alpha:] 00 00 +f b)b ]/.C: 00 00 +f c-(c [:digit:] 00 00 +f A....\u00e9 [!b]]^:B** 00 00 +f 2C:c-+ .. 00 00 +f (..+1 a 00 00 +f .\u00e9 ..\u005c*( 00 00 +f B( :1?)] 00 00 +f C:- )C:{a,b}| 00 00 +f \u00e9baab11 *}:\u005c:** 00 00 +f 2\u00e9 !(] 00 00 +f \u005c- ?, 00 00 +f a2 )}+ 00 00 +f \u005cc( ?(2{+(2 00 00 +f ..(\u00e9-C:/ a1@( 00 00 +f ..C:a \u005c.\u00e9[{1..3} 00 00 +f .a+c 11[a-c]1{1..3}@ 00 00 +f / {1..3}BB{1..3}+( 00 00 +f (\u005c/C: |-\u00e92!(C: 00 00 +f /22 c 00 00 +f ..A/ @([:digit:]1 00 00 +f +bbBA\u005c Ac1}\u005cb^ 00 00 +f c..\u00e9-c.. a-*(+(/ 00 00 +f B+.AA ?)A-+(^ 00 00 +f cabAa1+ {C:.+(c]+ 00 00 +f / {?a*(aa+( 00 00 +f b\u005c.c- b{a,b}*(b[a-c](: 00 00 +f 1A\u00e9\u005c }(!\u005c{a,b} 00 00 +f A12)cA \u00e9[:alpha:][a-c][!b]({a,b} 00 00 +f 2c C:\u005c! 00 00 +f b b.\u005c(){1..3}C: 00 00 +f cbbB {a,b}+(C:**cAc 00 00 +f c....ab {1..3}{{a,b}[ 00 00 +f B+ ]a(. 00 00 +f .(+ab+B @( 00 00 +f 1A /A@(,[!b]c 00 00 +f +../-c A** 00 00 +f B ?2*|B+* 00 00 +f 22 (\u00e9C:[{1..3} 00 00 +f ..2)A c,[:alpha:]*( 00 00 +f (+..\u005c : 00 00 +f -aab !\u005cA) 00 00 +f 1A( *(!(, 00 00 +f ( [a-c]@(@][a-c][ 00 00 +f 22 +(..-{a,b}-@(** 00 00 +f 2 : 00 00 +f -aBB\u00e9 **(}B 00 00 +f a\u00e9 |/{a,b}[{1..3}.. 00 00 +f . !({[:digit:]+1] 00 00 +f / ^ 00 00 +f A +(. 00 00 +f c. +(*([!b]@(\u00e9[:alpha:]\u005c 00 00 +f ab-C:A.. !(+([!b]C:( 00 00 +f )abab @(+( 00 00 +f B) \u005c 00 00 +f .A\u005cab .|2A[:alpha:]. 00 00 +f (bAb/+ @[:digit:]AC:/! 00 00 +f C:B/C: !]2[@([[a-c] 00 00 +f BA-cbc }?+(?( 00 00 +f 1c [:alpha:]@^(B 00 00 +f B..\u00e9- A:? 00 00 +f \u00e9c \u005cA,{1..3} 00 00 +f B)c :B+(- 00 00 +f c-( a{a,b}1 00 00 +f +c+ ]*){a,b}+c 00 00 +f \u005c2/..)( {|C:+( 00 00 +f Ac2ab)A ??!(*( 00 00 +f -\u00e9ab(A ..+(,A*( 00 00 +f 2 ) 00 00 +f BC: ]!([!b]+(\u00e9- 00 00 +f a..+ {1..3}[a-c]b+( 00 00 +f \u00e9/+ :?( 00 00 +f C: 1?/|a 00 00 +f Aa (\u00e9 00 00 +f B( -2{a,b}{a,b}c?(B 00 00 +f 2 C:@/ 00 00 +f A..Ba *( 00 00 +f (. A 00 00 +f \u005cC:\u005ccC:A .a[:alpha:]\u005c- 00 00 +f bcC:( {?+([a-c] 00 00 +f +.. ?(C:[?(@(?(\u005c 00 00 +f 2.....(ab {a,b}1{:* 00 00 +f 1/ a\u00e9[:digit:]+\u00e9 00 00 +f AAc( !(?\u005c{1..3} 00 00 +f +c ?|:.. 00 00 +f Bc1 [a-c]..[a-c] 00 00 +f B @({a,b} 00 00 +f B+-C: ?(! 00 00 +f cC:B) .,2a 00 00 +f abC:a !( 00 00 +f ab../C:\u005c ) 00 00 +f ccC:\u005c {a,b}{a,b}.. 00 00 +f bbB [:alpha:]({ 00 00 +f -\u005c/- b!( 00 00 +f (../.. ..*( 00 00 +f Aa : 00 00 +f Bcab)ab) ? 00 00 +f ..A... 22+( 00 00 +f (../B B?( 00 00 +f A ) 00 00 +f ( *[a-c]Ac! 00 00 +f \u00e9B\u00e9C: **.) 00 00 +f b) B 00 00 +f 1/-abab \u00e9/ 00 00 +f c +(*** 00 00 +f B -?(+(?(!(: 00 00 +f .. )} 00 00 +f ..\u005c1\u005c+ , 00 00 +f \u005c-- C:*( 00 00 +f bA\u005c2 {1..3}+{ 00 00 +f aAa \u00e9![:digit:],** 00 00 +f c- :?]\u005c[a-c] 00 00 +f (c(+1a 1\u005c].a?(- 00 00 +f BB 2){a,b}A 00 00 +f /( a@)?( 00 00 +f ab. ?^{a,b}+(}22 00 00 +f bc+ @+[:alpha:]C: 00 00 +f abC:( ([a-c]!..^@( 00 00 +f bA.. /c 00 00 +f a :[{a,b}a1B 00 00 +f 2(1B c 00 00 +f (C:) , 00 00 +f (1\u005cC: [!b]b{a,b} 00 00 +f \u00e9 ..{+@[:digit:] 00 00 +f bB-)a /}@(..+] 00 00 +f c-..a+ Aa 00 00 +f b..)() .A)a** 00 00 +f Bc [:digit:][!b] 00 00 +f /+c2a.. .1 00 00 +f /(\u00e9ab..( ?( 00 00 +f \u005c ?c 00 00 +f -/..C:+ +( 00 00 +f a. :\u005c] 00 00 +f \u00e9 (]c**, 00 00 +f \u00e91\u005c2A |\u005c^?( 00 00 +f . ..^* 00 00 +f baab( , 00 00 +f .a ]@|}| 00 00 +f B\u00e9b {1..3}a-[a-c]C: 00 00 +f c-\u00e9+ +.+a?( 00 00 +f cC:++C:c 2}?!{1..3} 00 00 +f - **?( 00 00 +f (a..- .[a-c] 00 00 +f abC: [a-c]\u005c+((] 00 00 +f . [:alpha:]?:{?!^ 00 00 +f )1/) {-[:digit:]*(@1/ 00 00 +f -ab//BC: {{-( 00 00 +f 1 a{** 00 00 +f ca1 :-@(^{, 00 00 +f C: @^]\u005c 00 00 +f / (] 00 00 +f \u005cba C:, 00 00 +f .C:A {+ 00 00 +f 1 \u005c2}{ 00 00 +f ..b)A)A ?( 00 00 +f )1C: \u00e9{a,b}1 00 00 diff --git a/packages/runtime/test/glob.test.ts b/packages/runtime/test/glob.test.ts new file mode 100644 index 000000000..3ee056e74 --- /dev/null +++ b/packages/runtime/test/glob.test.ts @@ -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); diff --git a/packages/runtime/test/test_glob.c b/packages/runtime/test/test_glob.c new file mode 100644 index 000000000..8c9fa62b0 --- /dev/null +++ b/packages/runtime/test/test_glob.c @@ -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 +#include +#include + +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; +} diff --git a/tests/corpus/5145-path-matches-glob-basics.ts b/tests/corpus/5145-path-matches-glob-basics.ts new file mode 100644 index 000000000..f7b85c168 --- /dev/null +++ b/tests/corpus/5145-path-matches-glob-basics.ts @@ -0,0 +1,39 @@ +// path.matchesGlob: *, ?, ** and the dot rules for the posix family, +// through the bare module, the posix namespace and named imports. Every +// row prints one 0/1 answer per path. +import * as path from "node:path"; +import { matchesGlob } from "node:path/posix"; +import { posix } from "node:path"; + +const paths = [ + "", ".", "..", "/", "a", "ab", "a.b", "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", "file.tar.gz", "file.", "1", "a10", +]; +const patterns = [ + "", "*", "**", "***", "?", "??", "???", "a", "a*", "*a", "*a*", "a?", "?a", ".*", "*.*", "*.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", "?/?", "??/*", "*.?", "a??", + "a/?/c", "a*b*c", "a**b", "**a", "a**", "*/", "**/", "/", "//", ".", "..", "./*", "../*", + "*.tar.*", "file.*", "*.", "src/**/*.ts", "**/node_modules/**", "x/**/.z.ts", "x/*/.z.ts", + "a/b/**/c/**/d", "**/b/**", "#a", "!a", "a\\b", +]; +for (const pattern of patterns) { + const row = paths.map((p) => (path.matchesGlob(p, pattern) ? "1" : "0")).join(""); + console.log(JSON.stringify(pattern).padEnd(24), row); +} + +// The three spellings are one function on posix hosts. +let agree = 0; +for (const pattern of patterns) + for (const p of paths) + if (matchesGlob(p, pattern) === posix.matchesGlob(p, pattern)) agree++; +console.log("agree", agree, patterns.length * paths.length); + +// As a predicate over a file list. +const files = ["src/main.ts", "src/util/strings.ts", "src/util/strings.test.ts", "README.md", ".env", "dist/main.js"]; +console.log(files.filter((f) => path.matchesGlob(f, "src/**/*.ts")).join(" ")); +console.log(files.filter((f) => !path.matchesGlob(f, "**/*.test.ts")).length); +const hidden: boolean = path.matchesGlob(".env", "*"); +console.log(hidden, path.matchesGlob(".env", ".*"), path.matchesGlob("dist/main.js", "*/*.js")); diff --git a/tests/corpus/5146-path-matches-glob-braces.ts b/tests/corpus/5146-path-matches-glob-braces.ts new file mode 100644 index 000000000..d0c39a702 --- /dev/null +++ b/tests/corpus/5146-path-matches-glob-braces.ts @@ -0,0 +1,25 @@ +// path.matchesGlob brace expansion: lists, nesting, numeric and letter +// sequences with steps and padding, and empty and unbalanced braces. +import { matchesGlob } from "node:path"; + +const paths = [ + "", "a", "b", "c", "d", "ab", "ac", "abc", "a1", "b2", "c3", "1", "2", "3", "4", "7", "10", "01", + "02", "03", "-1", "-2", "0", "e", "a/c", "b/c", "a/b", "xay", "xby", "xy", "{}", "{a}", "a{}", + "{a,b}", "${a,b}", "$a", "a.js", "a.ts", "a.mjs", "b.ts", "z", "y", +]; +const patterns = [ + "{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,3}", "x{a,b}y", + "x{,a}y", "{a/b,c}", "${a,b}", "{a,b}}", "{{a,b}", "{a,{b,c}}", "{a\\,b}", "{1..1}", "{x..z,a}", + "a{b,c}", "a{b..c}", "a{b,c", "{a,b,c,d,e}{,c}", "*{.js,.ts}", "{a,b}.{js,ts,mjs}", "{0..10..5}", + "{10..0..5}", "{1..3..-1}", "{a..a}", "{1..a}", "{a..1}", "{..}", "{1..}", "{,}", "{,,}", "{a,,b}", + "{{}}", "{a,b}{}", "?{b,c}", "*{1,2}", "{[ab],c}", "{!(a),b}", "{@(a|b),c}", +]; +for (const pattern of patterns) { + const row = paths.map((p) => (matchesGlob(p, pattern) ? "1" : "0")).join(""); + console.log(JSON.stringify(pattern).padEnd(20), row); +} + +// Long sequences, padded and unpadded. +console.log(matchesGlob("4999", "{1..5000}"), matchesGlob("5001", "{1..5000}"), matchesGlob("0042", "{0001..0100}")); + diff --git a/tests/corpus/5147-path-matches-glob-extglob.ts b/tests/corpus/5147-path-matches-glob-extglob.ts new file mode 100644 index 000000000..1553e7d9d --- /dev/null +++ b/tests/corpus/5147-path-matches-glob-extglob.ts @@ -0,0 +1,23 @@ +// path.matchesGlob extglobs: +(…), *(…), ?(…), @(…) and !(…), nested, +// empty, unterminated, mixed with stars and classes, and next to dots. +import * as path from "node:path"; + +const paths = [ + "", "a", "aa", "aaa", "b", "ab", "ba", "abab", "c", "ac", "abc", "bc", "x.js", "x.ts", "a.js", + "foo.js", "foobar.js", "bar.js", ".js", ".a", "a/b", "x/a/y", "x/b/y", "x/c/y", "+(a)", "a)", + "(a)", "!a", "a!b", "a.b", "foo", "foo.min.js", "index.ts", "b/c", "a/c", +]; +const patterns = [ + "+(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", "x/!(a)/y", "+([a-c])", + "!([a-c])", "@(*.ts|*.js)", "**/@(a|b).js", "?(a|b|c)?", "!(foo).js", "!(foo)*.js", "+(!(a))", + "*(!(a))", "!(+(a))", "!(*(a))", "!(?(a))", "+(a|b)+(c|d)", "!(a).!(b)", "@(a|b|c|d|e|f|g)", + "*(?)", "+(*)", "!(*)", "@(*)", "?(*)", "*.+(js|ts)", "!(*.min).js", "@(a)/*", "!(x)/**", +]; +for (const pattern of patterns) { + const row = paths.map((p) => (path.posix.matchesGlob(p, pattern) ? "1" : "0")).join(""); + console.log(JSON.stringify(pattern).padEnd(18), row); +} diff --git a/tests/corpus/5148-path-matches-glob-classes.ts b/tests/corpus/5148-path-matches-glob-classes.ts new file mode 100644 index 000000000..255a025cf --- /dev/null +++ b/tests/corpus/5148-path-matches-glob-classes.ts @@ -0,0 +1,32 @@ +// path.matchesGlob bracket expressions: ranges, negation, POSIX classes, +// unterminated and reversed brackets, escapes, non-ASCII members, and the +// TypeError for an over-long pattern. +import { posix } from "node:path"; + +const paths = [ + "", "a", "b", "c", "z", "0", "5", "9", "_", "-", "]", "[", "!", "^", ".", " ", "\t", "\n", "\x01", + "é", "ü", "ß", "日", "😀", "\ud83d", "a]", "[a]", "ab", "a5", "5a", "*", "?", "\\", "a/b", "f", + "x", "{", ",", "+", "~", "\x7f", +]; +const patterns = [ + "[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:]]", "[é]", "[😀]", "[à-ü]", "[^[:ascii:]]", "[[:alpha:][:digit:]]", + "[a-c][0-9]", "?[a-c]", "[a/b]", "[*]", "[?]", "\\*", "\\?", "[\\\\]", "[{,}]", "[+~]", + "[[:digit:]-]", "[!-]", "[ -~]", "[\\x00-\\x1f]", "*[!a-z]*", +]; +for (const pattern of patterns) { + const row = paths.map((p) => (posix.matchesGlob(p, pattern) ? "1" : "0")).join(""); + console.log(JSON.stringify(pattern).padEnd(24), row); +} + +// Node caps patterns at 64 Ki UTF-16 code units. +const long = "a".repeat(65536); +console.log(posix.matchesGlob(long, long)); +try { + console.log(posix.matchesGlob("a", long + "a")); +} catch (e) { + console.log(e instanceof TypeError, (e as Error).message); +} diff --git a/tests/corpus/5149-path-matches-glob-win32.ts b/tests/corpus/5149-path-matches-glob-win32.ts new file mode 100644 index 000000000..8812ffb8c --- /dev/null +++ b/tests/corpus/5149-path-matches-glob-win32.ts @@ -0,0 +1,72 @@ +// path.win32.matchesGlob: both separators, drive letters, UNC and \\?\ +// roots, backslashes never escaping; then a seeded grid comparing the +// posix and win32 families over generated paths and patterns. +import * as path from "node:path"; +import { matchesGlob } from "node:path/win32"; + +const paths = [ + "a\\b", "a/b", "c:\\a\\b", "c:/a/b", "c:\\a", "d:\\a\\b", "c:", "\\a", "/a", "\\\\server\\share\\x", + "//server/share/x", "\\\\server\\share", "//?/c:/x", "\\\\?\\c:\\x", "a\\b\\c.js", "a/b\\c.js", + "x", ".x", "a\\.b", "a*b", "a?b", "a\\*b", "\\\\a\\b\\c\\d", +]; +const patterns = [ + "a/b", "a\\b", "a/*", "a\\*", "*/*", "**", "**/*.js", "**\\*.js", "c:/a/*", "c:\\a\\*", "c:/**", + "c:/a", "?:/a/b", "*:/a/b", "[cd]:/a/b", "//server/share/*", "\\\\server\\share\\*", + "//server/share", "//*/share/x", "//server/*/x", "//?/c:/*", "//?/c:/x", "*/share/*", "**/x", + "**/.x", "a/.*", "a\\*b", "a\\?b", "a*b", "\\a", "/a", "{a,c:}/**", "+(a|b)/*", "a/**/*.js", +]; +for (const pattern of patterns) { + const row = paths.map((p) => (matchesGlob(p, pattern) ? "1" : "0")).join(""); + console.log(JSON.stringify(pattern).padEnd(24), row); +} +console.log(path.win32.matchesGlob("c:\\work\\src\\app.ts", "c:/work/**/*.ts")); + +// Seeded grid: answers for both families, packed per pattern. +let seed = 5149; +const random = (): number => { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +}; +const pick = (list: string[]): string => list[Math.floor(random() * list.length)]; +const make = (tokens: string[], max: number): string => { + let s = ""; + const n = 1 + Math.floor(random() * max); + for (let i = 0; i < n; i++) s += pick(tokens); + return s; +}; +const pathTokens = ["a", "b", ".", "..", "/", "\\", "ab", "1", "c:", "-", "é"]; +const patternTokens = [ + "a", "b", ".", "/", "\\", "*", "**", "?", "[a-b]", "[!a]", "{a,b}", "{1..2}", "+(a)", "!(b)", + "@(a|b)", "c:", "[[:alpha:]]", "..", "(", ")", "|", "{", "}", ",", +]; +const gridPaths: string[] = []; +for (let i = 0; i < 40; i++) gridPaths.push(make(pathTokens, 5)); +// A class next to an escaped character can make an invalid u-flag regex: +// Node throws its SyntaxError, recorded here as "E". +const answer = (match: (p: string, pattern: string) => boolean, p: string, pattern: string): string => { + try { + return match(p, pattern) ? "1" : "0"; + } catch (e) { + return e instanceof SyntaxError ? "E" : "?"; + } +}; +let errors = 0; +for (let i = 0; i < 60; i++) { + const pattern = make(patternTokens, 6); + let row = ""; + for (const p of gridPaths) { + const a = answer(path.posix.matchesGlob, p, pattern); + const b = answer(path.win32.matchesGlob, p, pattern); + if (a === "E") errors++; + row += a + b + " "; + } + console.log(JSON.stringify(pattern).padEnd(28), row.trimEnd()); +} +console.log("errors", errors); +try { + path.win32.matchesGlob("a", "\\,[[:alpha:]]"); +} catch (e) { + // The detail after the last ": " is the regex engine's own wording. + const message = (e as Error).message; + console.log((e as Error).name, message.slice(0, message.lastIndexOf(": "))); +} From 4aba593f288e53b4a7f5d9ecd392ae16231b6c1a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 04:50:50 -0500 Subject: [PATCH 4/9] Keep updated and passed-through number objects in frame storage --- .../src/backend/llvm/call-lifetimes.ts | 43 +++++++ .../src/backend/llvm/record-results.ts | 114 +++++++++++++++--- .../src/backend/llvm/stack-objects.test.ts | 55 ++++++++- .../5125-frame-mutating-methods.ts.json | 6 + .../corpus/5126-frame-returned-params.ts.json | 6 + tests/corpus/5125-frame-mutating-methods.ts | 67 ++++++++++ tests/corpus/5126-frame-returned-params.ts | 45 +++++++ 7 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5125-frame-mutating-methods.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5126-frame-returned-params.ts.json create mode 100644 tests/corpus/5125-frame-mutating-methods.ts create mode 100644 tests/corpus/5126-frame-returned-params.ts diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.ts b/packages/compiler/src/backend/llvm/call-lifetimes.ts index 555e96d7f..7dbd3ca32 100644 --- a/packages/compiler/src/backend/llvm/call-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/call-lifetimes.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/record-results.ts b/packages/compiler/src/backend/llvm/record-results.ts index d5de03920..6482bd462 100644 --- a/packages/compiler/src/backend/llvm/record-results.ts +++ b/packages/compiler/src/backend/llvm/record-results.ts @@ -9,6 +9,7 @@ import { 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.(...params, dst)` @@ -93,9 +94,11 @@ export function constructorInitializesFields(cls: IrClassDef, ctor: IrFunction): } /** Add a result-slot entry for every function whose returns all build a - * fresh scalar record or standalone scalar object. 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. */ + * 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])); @@ -109,7 +112,34 @@ export function addRecordResultEntries(mod: IrModule): IrModule { bases.has(name), lifetime.has(name), ); - const fillable = (value: IrExpr): boolean => { + // 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(); + 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(); + const fillable = (value: IrExpr, params: ReadonlySet): boolean => { switch (value.kind) { case "recordLit": return completeRecordLiteral( @@ -119,7 +149,11 @@ export function addRecordResultEntries(mod: IrModule): IrModule { case "new": return stackClass(value.className); case "ternary": - return fillable(value.then) && fillable(value.else_); + 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; } @@ -135,6 +169,38 @@ export function addRecordResultEntries(mod: IrModule): IrModule { 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 [ { @@ -163,26 +229,42 @@ export function addRecordResultEntries(mod: IrModule): IrModule { throw new Error("unreachable"); } }; - const added: IrFunction[] = []; - for (const fn of mod.functions) { + const candidates = mod.functions.filter((fn) => { if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures !== undefined) - continue; + return false; const ret = fn.returnType; - if ( - !(ret.kind === "record" && scalarRecordShape(shapes.get(ret.shapeId))) && - !(ret.kind === "object" && stackClass(ret.className)) - ) - continue; + 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 eligible = everyStmtList(fn.body, { + 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); + return s.value !== null && fillable(s.value, params); }, }); - if (!eligible || returns === 0) continue; + 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, { diff --git a/packages/compiler/src/backend/llvm/stack-objects.test.ts b/packages/compiler/src/backend/llvm/stack-objects.test.ts index 9dfb90a84..5869ac3de 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.test.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.test.ts @@ -89,7 +89,7 @@ test("reassigned bindings build each value apart and copy it into their slot", a 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", async () => { +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 }; } @@ -100,7 +100,9 @@ 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)); - expect(entries).toEqual([`${RESULT_ENTRY_PREFIX}fresh`]); + // 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 () => { @@ -221,3 +223,52 @@ test("loop bindings are never reassigned into a fixed slot", () => { 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`]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5125-frame-mutating-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5125-frame-mutating-methods.ts.json new file mode 100644 index 000000000..4b04f31a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5125-frame-mutating-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5125-frame-mutating-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5126-frame-returned-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5126-frame-returned-params.ts.json new file mode 100644 index 000000000..29427579d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5126-frame-returned-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5126-frame-returned-params.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5125-frame-mutating-methods.ts b/tests/corpus/5125-frame-mutating-methods.ts new file mode 100644 index 000000000..7b81bb029 --- /dev/null +++ b/tests/corpus/5125-frame-mutating-methods.ts @@ -0,0 +1,67 @@ +// Small objects and records whose number and boolean fields are updated +// through methods, parameters and compound assignments: every update is +// visible to later reads, aliases see the same object, and updates made +// before an object escapes are kept. +class Stats { + n = 0; + sum = 0; + max = -Infinity; + seen = false; + add(v: number): void { + this.n++; + this.sum += v; + if (v > this.max) this.max = v; + this.seen = true; + } + mean(): number { + return this.n === 0 ? 0 : this.sum / this.n; + } + merge(other: Stats): void { + this.n += other.n; + this.sum += other.sum; + if (other.max > this.max) this.max = other.max; + } +} +interface Cursor { + pos: number; + done: boolean; +} +function advance(c: Cursor, by: number): void { + c.pos += by; + if (c.pos >= 10) c.done = true; +} +function both(a: Cursor, b: Cursor): number { + a.pos += 1; + b.pos *= 2; + return a.pos + b.pos; +} +const kept: Stats[] = []; + +let out = ""; +for (let k = 0; k < 4; k++) { + const s = new Stats(); + for (let i = 0; i < 6; i++) s.add(((k * 7 + i * 3) % 11) - 2); + const t = new Stats(); + t.add(k * 10); + s.merge(t); + out += `${s.n}:${s.mean().toFixed(3)}:${s.max}:${s.seen} `; + const c: Cursor = { pos: k, done: false }; + while (!c.done) advance(c, 3); + const d: Cursor = { pos: 1, done: false }; + out += `${c.pos},${c.done},${both(d, d)},${d.pos} `; + const late = new Stats(); + late.add(k); + late.add(k + 1); + if (k % 2 === 0) kept.push(late); + try { + const e = new Stats(); + e.add(5); + if (k === 1) throw new Error("stop"); + e.add(6); + out += `${e.sum} `; + } catch (error) { + out += `${(error as Error).message} `; + } +} +console.log(out); +console.log(kept.map((s) => `${s.n}/${s.sum}`).join(" ")); diff --git a/tests/corpus/5126-frame-returned-params.ts b/tests/corpus/5126-frame-returned-params.ts new file mode 100644 index 000000000..c1cd7705d --- /dev/null +++ b/tests/corpus/5126-frame-returned-params.ts @@ -0,0 +1,45 @@ +// Functions that return one of their parameters, or another function's +// result, give the same values as Node whether the result is only read or +// kept, and objects that can be changed after construction keep sharing +// one identity. +interface Vec { + x: number; + y: number; +} +function scale(v: Vec, k: number): Vec { + return { x: v.x * k, y: v.y * k }; +} +function normalize(v: Vec): Vec { + const len = Math.sqrt(v.x * v.x + v.y * v.y); + return len === 0 ? v : scale(v, 1 / len); +} +function either(a: Vec, b: Vec, first: boolean): Vec { + return first ? a : b; +} +function halve(v: Vec, times: number): Vec { + return times === 0 ? v : halve(scale(v, 0.5), times - 1); +} +function dot(a: Vec, b: Vec): number { + return a.x * b.x + a.y * b.y; +} +interface Cell { + value: number; +} +function same(c: Cell): Cell { + return c; +} +const kept: Vec[] = []; + +let total = 0; +for (let i = 0; i < 6; i++) { + const v = { x: i, y: i % 3 }; + const n = normalize(v); + total += dot(n, v) + dot(either(v, n, i % 2 === 0), { x: 1, y: 1 }); + total += dot(halve({ x: 8, y: i }, 3), v); + kept.push(normalize({ x: 0, y: 0 })); + const c: Cell = { value: i }; + const alias = same(c); + alias.value += 10; + total += c.value; +} +console.log(total.toFixed(6), kept.map((v) => `${v.x},${v.y}`).join(" ")); From e932714bfab35533592938c07dbd16e24149a683 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 04:53:07 -0500 Subject: [PATCH 5/9] Shrink the cycle-collector object header to 8 bytes --- docs/content/docs/how-it-works.mdx | 2 +- examples/native-object/README.md | 2 +- packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/alloc.test.ts | 17 +- packages/compiler/src/backend/llvm/alloc.ts | 51 ++++-- .../compiler/src/backend/llvm/classes.test.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 12 +- .../compiler/src/backend/llvm/shapes.test.ts | 13 +- packages/compiler/src/backend/llvm/shapes.ts | 28 ++-- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../corpus/5155-cycle-header-types.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_array.c | 2 +- packages/runtime/src/scr_async.c | 2 +- packages/runtime/src/scr_closure.c | 4 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_cycle.c | 116 ++++++++------ packages/runtime/src/scr_error.c | 6 +- packages/runtime/src/scr_events_emitter.c | 2 +- packages/runtime/src/scr_exception.c | 2 +- packages/runtime/src/scr_json.c | 10 +- packages/runtime/src/scr_json.test.c | 2 +- packages/runtime/src/scr_map.c | 2 +- packages/runtime/src/scr_object.c | 2 +- packages/runtime/src/scr_runtime.h | 119 ++++++++------ packages/runtime/src/scr_stream.c | 2 +- packages/runtime/src/scr_union.c | 2 +- packages/runtime/src/scr_worker_events.c | 2 +- packages/runtime/test/test_array.c | 2 +- packages/runtime/test/test_closure.c | 6 +- packages/runtime/test/test_cycle.c | 32 ++-- tests/corpus/5155-cycle-header-types.ts | 146 ++++++++++++++++++ tests/fixtures/self-hosting/llvm-layouts.ts | 2 +- .../self-hosting-native-toolchain.test.ts | 2 +- 38 files changed, 429 insertions(+), 193 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5155-cycle-header-types.ts.json create mode 100644 tests/corpus/5155-cycle-header-types.ts diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index ab85d515b..c899476ab 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -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. diff --git a/examples/native-object/README.md b/examples/native-object/README.md index 1ec17fe16..26cde6853 100644 --- a/examples/native-object/README.md +++ b/examples/native-object/README.md @@ -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. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 2edc2ff8b..cb30fbb17 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -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"); }); }); diff --git a/packages/compiler/src/backend/llvm/alloc.test.ts b/packages/compiler/src/backend/llvm/alloc.test.ts index 8f2458e14..fa3b9306d 100644 --- a/packages/compiler/src/backend/llvm/alloc.test.ts +++ b/packages/compiler/src/backend/llvm/alloc.test.ts @@ -85,7 +85,16 @@ 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.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 +113,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:"); } }); diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index 08a7e07ae..acfe17d7a 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -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 i32, ptr %sa.tidp`, + ` %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:`, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 99a655133..309d8f071 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -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", () => { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 4c2be9440..60b0a876a 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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"; @@ -550,7 +550,7 @@ export class LlEmitter { 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; @@ -976,10 +976,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 ?? []); @@ -1097,7 +1095,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, diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index f8199d555..563fdd5b5 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -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)"), diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 4d0440b4c..7d41e7717 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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; readonly tracedUnions: Set; /** 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)`, diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index fe13cde8c..60ca3b5e9 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -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; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 655f2dfc4..473da193a 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -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: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 4e6b5c6f8..bd9cffa1b 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -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", diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0390ce658..260a1f87c 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -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 () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5155-cycle-header-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5155-cycle-header-types.ts.json new file mode 100644 index 000000000..06b50c312 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5155-cycle-header-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5155-cycle-header-types.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 25da9a6a2..512785d2b 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -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, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 6c487bb13..c52279ac1 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -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, diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index f54c4494c..090071c3d 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.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); diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index f78ab41fb..087a68ce0 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -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 diff --git a/packages/runtime/src/scr_closure.c b/packages/runtime/src/scr_closure.c index 66ee0efc1..ca5e29ced 100644 --- a/packages/runtime/src/scr_closure.c +++ b/packages/runtime/src/scr_closure.c @@ -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; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 71c6f7fe8..1b7f485b8 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -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 diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index eef8f07b3..d8664fb52 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -266,15 +266,37 @@ 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"); + scr_cyc_types[fresh] = *type; + scr_cyc_types[fresh].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 +745,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 +769,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 +791,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 +803,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 +837,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 +854,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 +891,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 +911,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 +940,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 +955,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 +964,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 +987,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 +1005,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 +1053,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 +1062,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 +1071,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); diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 672eb3681..18b5a9573 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -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 diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index ae9cdb4dd..219c3d8ab 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -284,7 +284,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; diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c index ffc88b53e..a55cc8e99 100644 --- a/packages/runtime/src/scr_exception.c +++ b/packages/runtime/src/scr_exception.c @@ -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; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 89cc33eb2..44c64740f 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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; @@ -9957,7 +9957,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) { @@ -10157,7 +10157,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; diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 3c99eaaba..b123cf97e 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -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); diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 5b6e333e8..58dd506c8 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -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(); diff --git a/packages/runtime/src/scr_object.c b/packages/runtime/src/scr_object.c index 7455dbd4a..a533a409c 100644 --- a/packages/runtime/src/scr_object.c +++ b/packages/runtime/src/scr_object.c @@ -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; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 0c55b507d..79c17f5df 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -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,68 @@ 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) { + return type->id ? type->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 +742,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 +790,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. diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 86a9dfecb..7a502ec81 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -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; diff --git a/packages/runtime/src/scr_union.c b/packages/runtime/src/scr_union.c index c4f3b41f6..9cdbe1b62 100644 --- a/packages/runtime/src/scr_union.c +++ b/packages/runtime/src/scr_union.c @@ -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; diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 5cd68b558..ab4b4878a 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -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; diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 150f4de6d..536aeaa52 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -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; diff --git a/packages/runtime/test/test_closure.c b/packages/runtime/test/test_closure.c index 74f760452..792312db1 100644 --- a/packages/runtime/test/test_closure.c +++ b/packages/runtime/test/test_closure.c @@ -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); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 2656d529a..7f7f249d4 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -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; diff --git a/tests/corpus/5155-cycle-header-types.ts b/tests/corpus/5155-cycle-header-types.ts new file mode 100644 index 000000000..16eb83ed5 --- /dev/null +++ b/tests/corpus/5155-cycle-header-types.ts @@ -0,0 +1,146 @@ +// Cycle-capable objects of many types share one compact header that names +// each type by an id assigned on its first allocation. Several worker +// threads allocate their first objects of the same types at the same time; +// cycles of every shape are built, kept, broken and dropped across the +// collector's generations; and the survivors are checked on every thread. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +class Ring { + next: Ring | null = null; + readonly id: number; + constructor(id: number) { + this.id = id; + } +} +class Pair { + other: Pair | null = null; + value: number; + constructor(value: number) { + this.value = value; + } +} +class Graph { + readonly nodes: GraphNode[] = []; +} +class GraphNode { + readonly edges: GraphEdge[] = []; + owner: Graph | null = null; + readonly label: string; + constructor(label: string) { + this.label = label; + } +} +class GraphEdge { + readonly to: GraphNode; + readonly weight: number; + constructor(to: GraphNode, weight: number) { + this.to = to; + this.weight = weight; + } +} +interface Cell { + value: number; + next: Cell | null; + link: (() => number) | null; +} + +function ring(size: number, base: number): Ring { + const first = new Ring(base); + let last = first; + for (let i = 1; i < size; i++) { + const node = new Ring(base + i); + last.next = node; + last = node; + } + last.next = first; + return first; +} + +function ringSum(start: Ring): number { + let sum = start.id; + for (let at = start.next; at !== null && at !== start; at = at.next) sum += at.id; + return sum; +} + +function graph(n: number, seed: number): Graph { + const g = new Graph(); + for (let i = 0; i < n; i++) { + const node = new GraphNode(`n${i}`); + node.owner = g; + g.nodes.push(node); + } + let x = seed; + for (const node of g.nodes) { + for (let k = 0; k < 3; k++) { + x = (Math.imul(x, 1103515245) + 12345) >>> 0; + node.edges.push(new GraphEdge(g.nodes[x % n]!, x % 17)); + } + } + return g; +} + +function graphWeight(g: Graph): number { + let total = 0; + for (const node of g.nodes) for (const edge of node.edges) total += edge.weight + edge.to.label.length; + return total; +} + +function cells(n: number): Cell { + const head: Cell = { value: 0, next: null, link: null }; + let at = head; + for (let i = 1; i < n; i++) { + const cell: Cell = { value: i, next: null, link: null }; + cell.link = () => cell.value + head.value; + at.next = cell; + at = cell; + } + at.next = head; + return head; +} + +function work(tag: number): string { + const kept: Ring[] = []; + const pairs: Pair[] = []; + let dropped = 0; + for (let round = 0; round < 40; round++) { + // Garbage rings, pairs and graphs; every few rounds one survives. + const r = ring(5 + (round % 7), round * 100 + tag); + dropped += ringSum(r); + if (round % 9 === 0) kept.push(r); + const a = new Pair(round); + const b = new Pair(-round); + a.other = b; + b.other = a; + if (round % 13 === 0) pairs.push(a); + dropped += graphWeight(graph(30 + round, round + tag)); + const c = cells(8); + let links = 0; + for (let at = c.next; at !== c && at !== null; at = at.next) links += at.link!(); + dropped += links; + } + // Break one kept ring and drop it; the rest must survive the passes. + kept[0]!.next = null; + kept.shift(); + let survivors = 0; + for (const r of kept) survivors += ringSum(r); + let pairSum = 0; + for (const p of pairs) pairSum += p.value + p.other!.value + p.other!.other!.value; + return `tag ${tag} dropped ${dropped} kept ${kept.length} survivors ${survivors} pairs ${pairSum}`; +} + +if (isMainThread) { + const results: Promise[] = []; + for (let t = 1; t <= 3; t++) { + results.push( + new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: t }); + worker.on("message", (text: string) => resolve(text)); + }), + ); + } + console.log("main", work(0)); + for (const text of await Promise.all(results)) console.log("worker", text); + console.log("main again", work(9)); +} else { + parentPort!.postMessage(work(workerData as number)); +} diff --git a/tests/fixtures/self-hosting/llvm-layouts.ts b/tests/fixtures/self-hosting/llvm-layouts.ts index d767defab..2c0f1f440 100644 --- a/tests/fixtures/self-hosting/llvm-layouts.ts +++ b/tests/fixtures/self-hosting/llvm-layouts.ts @@ -52,7 +52,7 @@ try { return `@native_literal_${index}`; }, sizeType: request.bits === 32 ? "i32" : "i64", - cycleBufferedOffset: 6, + cycleBufOffset: 4, // Collector statistics indices, numbered as LlEmitter numbers them. nextCycKind: () => FIRST_EMITTED_CYC_KIND + cycKinds++, tracedShapes: traced.shapes, diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index e1334197a..eb7b5ef16 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -96,7 +96,7 @@ for (const backend of ["llvm"] as const) { 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: [], sanitizer: "external-toolchain-required" }, flavors: { From 6eac0fee3bc912d15b53b43b850da2ac98e76a36 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 05:30:17 -0500 Subject: [PATCH 6/9] Read cycle descriptor ids with acquire loads --- packages/compiler/src/backend/llvm/alloc.test.ts | 1 + packages/compiler/src/backend/llvm/alloc.ts | 2 +- packages/runtime/src/scr_runtime.h | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/alloc.test.ts b/packages/compiler/src/backend/llvm/alloc.test.ts index fa3b9306d..771d3b09e 100644 --- a/packages/compiler/src/backend/llvm/alloc.test.ts +++ b/packages/compiler/src/backend/llvm/alloc.test.ts @@ -92,6 +92,7 @@ describe("emitted object allocation", () => { 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"); diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index acfe17d7a..f221af74e 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -215,7 +215,7 @@ function allocLines( const registered = cyc ? [ ` %sa.tidp = getelementptr inbounds ${CYC_TYPE}, ptr ${cyc.type}, i32 0, i32 3`, - ` %sa.tid = load i32, ptr %sa.tidp`, + ` %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`, diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 79c17f5df..0a6683c70 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -528,7 +528,10 @@ static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; } 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) { - return type->id ? type->id : scr_cyc_register(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]; } From f2d0aa6bb65d4c251dc66946eafa0e34ef62c05d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 06:43:46 -0500 Subject: [PATCH 7/9] Bind the class view of lazily boxed field-call receivers --- .../compiler/src/backend/llvm/expr-calls.ts | 6 +++- .../src/backend/llvm/expr-stream-bridges.ts | 30 ++++++++++------- packages/runtime/src/scr_json.c | 6 ++++ packages/runtime/src/scr_runtime.h | 6 +++- tests/corpus/5160-field-callback-receiver.ts | 33 +++++++++++++++++++ tests/corpus/5163-field-callback-receiver.cjs | 28 +++++++++++++++- 6 files changed, 95 insertions(+), 14 deletions(-) diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index d1d86eb57..0bbdde97e 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -11,6 +11,7 @@ import { 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 { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js"; @@ -748,10 +749,13 @@ export function emitCallExpr( 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 } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind} }`, + `${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)"); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a72c0b246..cb2f81a4e 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -1319,23 +1319,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( diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d3b1546f3..8e2ab7a01 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3378,6 +3378,12 @@ ScrDyn *scr_dyn_this_get(void) { 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 diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 669758c01..9840599ae 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -5504,7 +5504,9 @@ bool scr_dyn_is_nullish(const ScrDyn *d); 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). */ + * 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 *); @@ -5513,6 +5515,8 @@ typedef struct { 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. */ diff --git a/tests/corpus/5160-field-callback-receiver.ts b/tests/corpus/5160-field-callback-receiver.ts index 02570b9d8..834c3ff8e 100644 --- a/tests/corpus/5160-field-callback-receiver.ts +++ b/tests/corpus/5160-field-callback-receiver.ts @@ -71,3 +71,36 @@ const pair = [new Counter("x"), new Counter("y")]; pair[0]!.step = pair[1]!.step; for (let i = 0; i < 1000; i++) total += pair[i % 2]!.step(1); console.log(total, pair[0]!.count, pair[1]!.count); + +// Dynamic views of the bound receiver: `in` sees the prototype members of +// the instance's own class (subclasses included), and for-in and +// Object.keys list its own fields. +class Shape { + w = 2; + h = 3; + area(): number { + return this.w * this.h; + } + probe: (this: Shape) => string; + constructor() { + this.probe = function (this: Shape): string { + const keys: string[] = []; + for (const k in this) keys.push(k); + return `${"area" in this} ${"constructor" in this} ${"missing" in this} ${Object.keys(this).join(",")} ${keys.join(",")}`; + }; + } +} +class Square extends Shape { + side(): number { + return this.w; + } +} +const shape = new Shape(); +const square = new Square(); +console.log(shape.probe(), "|", square.probe()); +const members = function (this: Shape): string { + return `${"side" in this} ${"area" in this} ${this.area()}`; +}; +shape.probe = members; +square.probe = members; +console.log(shape.probe(), "|", square.probe()); diff --git a/tests/corpus/5163-field-callback-receiver.cjs b/tests/corpus/5163-field-callback-receiver.cjs index 0ae11c6c6..359a49246 100644 --- a/tests/corpus/5163-field-callback-receiver.cjs +++ b/tests/corpus/5163-field-callback-receiver.cjs @@ -1,8 +1,12 @@ // Function-valued properties of class instances in a JavaScript program: // methods assigned in the constructor, replaced later, and called through -// the instance read `this` for the instance they were called on. +// the instance read `this` for the instance they were called on. Dynamic +// views of the bound instance, record and array see their members. 'use strict'; class Greeter { + hello() { + return 'hi'; + } constructor(name) { this.name = name; this.greet = function (other) { @@ -22,3 +26,25 @@ g.greet = function (other) { return `${this === g} ${this === h} ${other}`; }; console.log(g.greet('x'), calls); +g.probe = function () { + const keys = []; + for (const k in this) keys.push(k); + return `${'hello' in this} ${'constructor' in this} ${'missing' in this} ${Object.keys(this).join(',')} ${keys.join(',')}`; +}; +console.log(g.probe()); +const rec = { + n: 1, + tag: 'r', + show() { + const keys = []; + for (const k in this) keys.push(k); + return `${'n' in this} ${'missing' in this} ${Object.keys(this).join(',')} ${keys.join(',')}`; + }, +}; +console.log(rec.show()); +const fns = [ + function () { + return `${Array.isArray(this)} ${this.length} ${0 in this} ${1 in this} ${Object.keys(this).join(',')}`; + }, +]; +console.log(fns[0]()); From 6fe81f0ce6bd59b9bff2bfdd7926969f95ebd8ca Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 06:48:48 -0500 Subject: [PATCH 8/9] Sort by key for recognized numeric comparators --- .../src/backend/llvm/expr-containers.ts | 22 +- .../src/backend/llvm/reference-writes.ts | 15 +- .../lowering/containers/array-callbacks.ts | 45 +- .../lowering/lower-array-sort.test.ts | 145 ++++++- .../src/frontend/lowering/lower-array-sort.ts | 119 +++++- packages/compiler/src/ir/ir.ts | 11 +- packages/compiler/src/ir/validate.test.ts | 74 ++++ packages/compiler/src/ir/validate.ts | 42 +- .../5200-array-sort-keyed-numbers.ts.json | 6 + .../5201-array-sort-keyed-records.ts.json | 6 + .../5202-array-sort-keyed-lookalikes.ts.json | 6 + packages/runtime/src/scr_array.c | 403 +++++++++++++++++- packages/runtime/src/scr_runtime.h | 11 +- packages/runtime/test/test_array.c | 196 ++++++++- tests/corpus/5200-array-sort-keyed-numbers.ts | 90 ++++ tests/corpus/5201-array-sort-keyed-records.ts | 118 +++++ .../5202-array-sort-keyed-lookalikes.ts | 73 ++++ 17 files changed, 1335 insertions(+), 47 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5200-array-sort-keyed-numbers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5201-array-sort-keyed-records.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5202-array-sort-keyed-lookalikes.ts.json create mode 100644 tests/corpus/5200-array-sort-keyed-numbers.ts create mode 100644 tests/corpus/5201-array-sort-keyed-records.ts create mode 100644 tests/corpus/5202-array-sort-keyed-lookalikes.ts diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index e41043350..3c4cf4ff5 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -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 }; diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 3a05ce0dd..4a6173f19 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -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); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 4d977ce0e..27a55d35e 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -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, + 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 diff --git a/packages/compiler/src/frontend/lowering/lower-array-sort.test.ts b/packages/compiler/src/frontend/lowering/lower-array-sort.test.ts index 6e65e55f9..54f3985a7 100644 --- a/packages/compiler/src/frontend/lowering/lower-array-sort.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-array-sort.test.ts @@ -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 = {}) => ({ + 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(); @@ -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(); +}); diff --git a/packages/compiler/src/frontend/lowering/lower-array-sort.ts b/packages/compiler/src/frontend/lowering/lower-array-sort.ts index cba3f9472..e5546f517 100644 --- a/packages/compiler/src/frontend/lowering/lower-array-sort.ts +++ b/packages/compiler/src/frontend/lowering/lower-array-sort.ts @@ -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, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index d76817099..ee3568d01 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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, diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index 9c1207067..df76d9143 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -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"; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 22a7bc849..44d8436c0 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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}`, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5200-array-sort-keyed-numbers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5200-array-sort-keyed-numbers.ts.json new file mode 100644 index 000000000..3548d41a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5200-array-sort-keyed-numbers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5200-array-sort-keyed-numbers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5201-array-sort-keyed-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5201-array-sort-keyed-records.ts.json new file mode 100644 index 000000000..5aee8b2b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5201-array-sort-keyed-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5201-array-sort-keyed-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5202-array-sort-keyed-lookalikes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5202-array-sort-keyed-lookalikes.ts.json new file mode 100644 index 000000000..b661e69ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5202-array-sort-keyed-lookalikes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5202-array-sort-keyed-lookalikes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 090071c3d..dc68639db 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -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; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 0a6683c70..74971ffb5 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -2020,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). */ diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 536aeaa52..d4beea40f 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -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(); diff --git a/tests/corpus/5200-array-sort-keyed-numbers.ts b/tests/corpus/5200-array-sort-keyed-numbers.ts new file mode 100644 index 000000000..0be426770 --- /dev/null +++ b/tests/corpus/5200-array-sort-keyed-numbers.ts @@ -0,0 +1,90 @@ +// Numeric comparators the runtime orders by key: `a - b`, `b - a` and the +// three-way ternaries. Results, stability-sensitive values (-0 next to 0) +// and NaN placement must match the comparator sort. +let seed = 99; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +function digest(values: number[]): string { + let hash = 0; + for (let i = 0; i < values.length; i++) { + const v = values[i]!; + const code = Object.is(v, -0) ? -7 : Number.isNaN(v) ? -9 : v; + hash = (Math.imul(hash, 31) + Math.floor(code * 1000)) | 0; + } + return values.length + ":" + hash; +} + +function show(values: number[]): string { + return values.map((v) => (Object.is(v, -0) ? "-0" : String(v))).join(","); +} + +const sizes = [0, 1, 2, 3, 16, 17, 47, 48, 49, 50, 64, 100, 1000, 20000]; +const inputs: [string, (n: number, i: number) => number][] = [ + ["ints", () => Math.floor(random() * 200) - 100], + ["wide", () => Math.floor(random() * 4294967296) - 2147483648], + ["doubles", () => (random() - 0.5) * 1e6], + ["tiny", () => (random() - 0.5) * 1e-300], + ["presorted", (n, i) => i * 3], + ["reversed", (n, i) => (n - i) * 0.5], + ["ties-reversed", (n, i) => Math.floor((n - i) / 3)], + ["two-runs", (n, i) => (i < n / 2 ? i : i - n / 2)], + ["appended", (n, i) => (i + 5 < n ? i : Math.floor(random() * n))], + ["infinities", (n, i) => (i % 7 === 0 ? Infinity : i % 5 === 0 ? -Infinity : i % 11)], +]; + +for (const [label, make] of inputs) { + const lines: string[] = []; + for (const n of sizes) { + const base: number[] = []; + for (let i = 0; i < n; i++) base.push(make(n, i)); + const ascending = base.slice().sort((a, b) => a - b); + const descending = base.slice().sort((a, b) => b - a); + const ternary = base.toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + const flipped = base.toSorted((x, y) => (x > y ? -1 : x < y ? 1 : 0)); + lines.push(n + "=" + digest(ascending) + "/" + digest(descending) + "/" + digest(ternary) + "/" + digest(flipped)); + } + console.log(label, lines.join(" ")); +} + +// Small inputs printed in full. +const small = [5, -3, 2.5, 0, 9, -3, 1e21, -1e-7, 2.5, 7]; +console.log(show(small.toSorted((a, b) => a - b))); +console.log(show(small.toSorted((a, b) => b - a))); +console.log(show(small.toSorted((a, b) => (b < a ? -1 : b > a ? 1 : 0)))); +console.log(show(small), "unchanged by toSorted"); + +// -0 and 0 compare equal, so their original order survives. +const zeros = [0, -0, 3, -0, 0, -1, 0, -0]; +console.log(show(zeros.toSorted((a, b) => a - b))); +console.log(show(zeros.toSorted((a, b) => b - a))); +console.log(show(zeros.toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0)))); +const manyZeros: number[] = []; +for (let i = 0; i < 300; i++) manyZeros.push(i % 3 === 0 ? -0 : i % 3 === 1 ? 0 : (i % 17) - 8); +console.log(digest(manyZeros.toSorted((a, b) => a - b)), show(manyZeros.toSorted((a, b) => a - b).slice(100, 140))); + +// NaN makes the comparator inconsistent; the comparator sort decides. +const withNaN = [3, NaN, 1, 2, NaN, 0, -1, 5, 4]; +console.log(show(withNaN.toSorted((a, b) => a - b))); +console.log(show(withNaN.toSorted((a, b) => b - a))); +console.log(show(withNaN.toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0)))); + +// sort returns its receiver, sorted in place. +const inPlace = [4, 1, 3, 2]; +const same = inPlace.sort((a, b) => a - b); +console.log(same === inPlace, show(inPlace)); + +// Holes and appended elements take the snapshot path. +const sparse: number[] = [5, 3]; +sparse[6] = 1; +sparse[4] = 9; +sparse.sort((a, b) => b - a); +console.log(sparse.length, show(sparse.filter(() => true)), 4 in sparse, 5 in sparse); +const holey: number[] = []; +holey[3] = 2; +holey[1] = 8; +holey[0] = -1; +const sortedHoley = holey.toSorted((a, b) => a - b); +console.log(sortedHoley.length, String(sortedHoley), 3 in holey); diff --git a/tests/corpus/5201-array-sort-keyed-records.ts b/tests/corpus/5201-array-sort-keyed-records.ts new file mode 100644 index 000000000..ef8c6cbc2 --- /dev/null +++ b/tests/corpus/5201-array-sort-keyed-records.ts @@ -0,0 +1,118 @@ +// Comparators over one number field of records and class instances +// (`a.k - b.k`, `b.k - a.k`, three-way ternaries) sort by key: the stable +// order of equal keys, NaN and -0 keys, aliases and holes match Node. +interface Row { + id: number; + key: number; + label: string; +} + +class Item { + id: number; + rank: number; + constructor(id: number, rank: number) { + this.id = id; + this.rank = rank; + } +} + +class Special extends Item { + note: string; + constructor(id: number, rank: number, note: string) { + super(id, rank); + this.note = note; + } +} + +let seed = 7; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +function ids(rows: { id: number }[]): string { + let hash = 0; + for (const row of rows) hash = (Math.imul(hash, 31) + row.id) | 0; + return rows.length + ":" + hash; +} + +function makeRows(n: number, key: (i: number) => number): Row[] { + const rows: Row[] = []; + for (let i = 0; i < n; i++) rows.push({ id: i, key: key(i), label: "r" + i }); + return rows; +} + +const keys: [string, (n: number, i: number) => number][] = [ + ["ties", () => Math.floor(random() * 10)], + ["ints", () => Math.floor(random() * 100000) - 50000], + ["doubles", () => (random() - 0.5) * 1e9], + ["presorted", (n, i) => i], + ["reversed", (n, i) => n - i], + ["ties-reversed", (n, i) => Math.floor((n - i) / 4)], + ["runs", (n, i) => i % Math.ceil(n / 3)], + ["infinities", (n, i) => (i % 9 === 0 ? Infinity : i % 4 === 0 ? -Infinity : i % 6)], +]; +for (const [label, keyFor] of keys) { + const parts: string[] = []; + for (const n of [0, 1, 2, 5, 16, 33, 48, 49, 200, 5000]) { + const rows = makeRows(n, (i) => keyFor(n, i)); + const ascending = rows.toSorted((a, b) => a.key - b.key); + const descending = rows.toSorted((a, b) => b.key - a.key); + const ternary = rows.toSorted((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const reversed = rows.toSorted((a, b) => (a.key > b.key ? -1 : a.key < b.key ? 1 : 0)); + const inPlace = rows.slice(); + inPlace.sort((p, q) => q.key - p.key); + parts.push([ids(ascending), ids(descending), ids(ternary), ids(reversed), ids(inPlace)].join("/")); + } + console.log(label, parts.join(" ")); +} + +// Equal keys keep their original order in both directions. +const small = makeRows(12, (i) => [3, 1, 2, 1, 3, 0, 2, 1, 0, 3, 2, 1][i]!); +console.log(small.toSorted((a, b) => a.key - b.key).map((r) => r.label).join(",")); +console.log(small.toSorted((a, b) => b.key - a.key).map((r) => r.label).join(",")); + +// -0 and NaN keys sort through the comparator. +const zeroRows = makeRows(60, (i) => (i % 3 === 0 ? -0 : i % 3 === 1 ? 0 : i % 5)); +console.log(zeroRows.toSorted((a, b) => a.key - b.key).map((r) => r.id).join(",")); +const nanRows = makeRows(9, (i) => [4, NaN, 2, 7, NaN, 1, 0, 3, 5][i]!); +console.log(nanRows.toSorted((a, b) => a.key - b.key).map((r) => r.id).join(",")); +console.log(nanRows.toSorted((a, b) => b.key - a.key).map((r) => r.id).join(",")); + +// sort keeps the receiver's identity; aliases observe the order. +const shared = makeRows(40, () => Math.floor(random() * 6)); +const alias = shared; +const result = shared.sort((a, b) => a.key - b.key); +console.log(result === shared, ids(alias), alias[0]!.label, alias[39]!.label); +shared[0]!.label = "changed"; +console.log(alias[0]!.label); + +// Class instances, including subclass instances in a base-class array. +const items: Item[] = []; +const specials: Special[] = []; +for (let i = 0; i < 300; i++) { + const rank = Math.floor(random() * 20); + if (i % 4 === 0) { + const special = new Special(i, rank, "s" + i); + specials.push(special); + items.push(special); + } else items.push(new Item(i, rank)); +} +console.log(ids(items.toSorted((a, b) => a.rank - b.rank)), ids(items.toSorted((a, b) => b.rank - a.rank))); +specials.sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0)); +console.log(ids(specials), specials[0]!.note, specials[specials.length - 1]!.note); +items.sort((a, b) => b.rank - a.rank); +console.log(ids(items), items[0]!.rank, items[299]!.rank); + +// Holes take the snapshot path. +const holes: Row[] = makeRows(6, (i) => 6 - i); +holes[9] = { id: 99, key: -5, label: "late" }; +holes[7] = { id: 77, key: 3, label: "mid" }; +holes.sort((a, b) => a.key - b.key); +console.log(holes.length, holes.map((r) => r.id).join(","), 7 in holes, 8 in holes); +const holeCopy: Row[] = []; +holeCopy[4] = { id: 4, key: 1, label: "d" }; +holeCopy[0] = { id: 0, key: 0, label: "a" }; +holeCopy[2] = { id: 2, key: 1, label: "c" }; +const holeSorted = holeCopy.toSorted((a, b) => b.key - a.key); +console.log(holeSorted.length, holeSorted.map((r) => (r === undefined ? "u" : r.id)).join(",")); diff --git a/tests/corpus/5202-array-sort-keyed-lookalikes.ts b/tests/corpus/5202-array-sort-keyed-lookalikes.ts new file mode 100644 index 000000000..2111a0337 --- /dev/null +++ b/tests/corpus/5202-array-sort-keyed-lookalikes.ts @@ -0,0 +1,73 @@ +// Consistent comparators close to the keyed forms still sort as Node sorts +// them: accessors with effects, several fields, optional fields, captured +// state, scaled or negated results, declared and stored functions. +interface Pair { + id: number; + left: number; + right: number; + weight?: number; +} + +class Score { + id: number; + base: number; + reads = 0; + constructor(id: number, base: number) { + this.id = id; + this.base = base; + } + get value(): number { + this.reads++; + return this.base * 2; + } +} + +let seed = 3; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +const pairs: Pair[] = []; +for (let i = 0; i < 400; i++) { + const pair: Pair = { id: i, left: Math.floor(random() * 30), right: Math.floor(random() * 30) }; + if (i % 3 === 0) pair.weight = Math.floor(random() * 5); + pairs.push(pair); +} +const order = (rows: { id: number }[]): string => rows.map((row) => row.id).join(","); +const hash = (rows: { id: number }[]): number => { + let h = 0; + for (const row of rows) h = (Math.imul(h, 31) + row.id) | 0; + return h; +}; + +console.log("mixed", hash(pairs.toSorted((a, b) => a.left - b.left || a.right - b.right))); +console.log("scaled", hash(pairs.toSorted((a, b) => (a.left - b.left) * 2))); +console.log("optional", hash(pairs.toSorted((a, b) => (a.weight ?? 0) - (b.weight ?? 0)))); +let direction = -1; +console.log("captured", hash(pairs.toSorted((a, b) => direction * (a.left - b.left)))); +direction = 1; +console.log("captured-again", hash(pairs.toSorted((a, b) => direction * (a.left - b.left)))); +console.log("ternary-two", hash(pairs.toSorted((a, b) => (a.left < b.left ? -2 : a.left > b.left ? 2 : 0)))); + +function byLeft(a: Pair, b: Pair): number { + return a.left - b.left; +} +console.log("declared", hash(pairs.toSorted(byLeft)), order(pairs.toSorted(byLeft).slice(0, 12))); + +const scores: Score[] = []; +for (let i = 0; i < 64; i++) scores.push(new Score(i, Math.floor(random() * 8))); +const byValue = scores.toSorted((a, b) => a.value - b.value); +let reads = 0; +for (const score of scores) reads += score.reads; +console.log("getter", hash(byValue), reads > 0); + +const numbers: number[] = []; +for (let i = 0; i < 300; i++) numbers.push(Math.floor(random() * 40) - 20); +const show = (values: number[]): string => values.slice(0, 20).join(","); +console.log("no-param", show(numbers.toSorted(() => 0))); +console.log("negated", show(numbers.toSorted((a, b) => -(a - b)))); +console.log("swapped-sub", show(numbers.toSorted((a, b) => -b + a))); +const reversedSub = (a: number, b: number): number => b - a; +console.log("stored", show(numbers.toSorted(reversedSub))); +console.log("abs", show(numbers.toSorted((a, b) => Math.abs(a) - Math.abs(b)))); From 5cfcc0d565509665914948b13eb7ca8622b0b9ee Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 07:08:55 -0500 Subject: [PATCH 9/9] Copy only stable descriptor fields when registering a cycle type --- packages/runtime/src/scr_cycle.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index d8664fb52..c5ea9e26e 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -278,8 +278,10 @@ uint32_t scr_cyc_register(ScrCycType *type) { 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"); - scr_cyc_types[fresh] = *type; - scr_cyc_types[fresh].id = fresh; + /* 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,