diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index eb5277533..64abc94a8 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -54,7 +54,7 @@ fib.s Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file. -The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v12 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime. +The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v13 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime. ## scriptc run diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index f0d5efabb..5fcd664a8 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -68,7 +68,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_v12` checks runtime compatibility at link time. +Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v13` 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/docs/content/docs/native-objects.mdx b/docs/content/docs/native-objects.mdx index 609065160..caa5baefd 100644 --- a/docs/content/docs/native-objects.mdx +++ b/docs/content/docs/native-objects.mdx @@ -8,7 +8,7 @@ title: "Native Program Objects" The external object ABI is **experimental**. Its `scr_*` function and data surface may change before 1.0, so consumers must use the exact runtime-pack version reported by the same compiler installation. This is stricter than semver compatibility. -The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v12`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. +The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v13`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. ## Machine-readable link information diff --git a/examples/native-object/README.md b/examples/native-object/README.md index 486612a0e..3086e995c 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_v12`, 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_v13`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI. diff --git a/packages/cli/README.md b/packages/cli/README.md index b4876f9b0..a8dfbc798 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -34,7 +34,7 @@ Builds use a bounded persistent cache. Unchanged source can reuse the validated - `scriptc run ` — compile and run - `scriptc coverage ` — what compiles statically, and why the rest doesn't -`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v12` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. +`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v13` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. For embedder-hosted modules that are not installed npm packages, coverage can map an exact bare specifier to a local declaration with repeatable `--external-types ` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index ad7791c66..4e1616a8c 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: 12, marker: "scr_runtime_abi_v12" }, + runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" }, 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_v12"); + expect(error?.stderr).toContain("scr_runtime_abi_v13"); }); }); diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index 25a735356..c409fb87b 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -9,6 +9,20 @@ import type { } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; +/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for + * field (its static assertions pin `cyc` and `data`). */ +export const SCR_ARR_FIELD = { + rc: 0, + len: 1, + cap: 2, + elem: 3, + cyc: 4, + ops: 5, + data: 6, + present: 7, + ext: 8, +} as const; + /** Lines that decide whether the double `index` is an integer in * `[0, length)`, where `length` is an unsigned `sizeType` count, and give * that integer as an i64. `%{ok}` is the answer; `%{wide}` is meaningful diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 34428db8b..dd0c20588 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -8,9 +8,10 @@ * `undefined`, sparse indices, noncanonical numeric properties (negative, * fractional, NaN, infinite, 2^32-1), and out-of-range reads branch to the * unchanged runtime call, which is marked cold. The ScrArr field indices - * match the C layout in scr_runtime.h: len 1, cap 2, data 7, present 8. */ + * (SCR_ARR_FIELD) match the C layout in scr_runtime.h. */ import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; +import { SCR_ARR_FIELD } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -85,10 +86,14 @@ function emitDenseGuard( data = B.tmp(), statesPtr = B.tmp(), states = B.tmp(); - B.line(`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 7`); + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); - B.line(`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 8`); + B.line( + `${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.present}`, + ); host.markMemoryPointer(statesPtr, "array:header"); B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); const widen = (value: string): string => { @@ -389,9 +394,9 @@ export function emitDenseArrayPush( join = B.newLabel("arr.push.join"); B.condBr(room, fast, slow); B.startBlock(fast); - const dataPtr = header(7), + const dataPtr = header(SCR_ARR_FIELD.data), data = B.tmp(), - statesPtr = header(8), + statesPtr = header(SCR_ARR_FIELD.present), states = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index b474068cc..56b18e2b4 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,6 +1,6 @@ import { typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; -import { BYTES_ELEM_NUM, f64Lit } from "./common.js"; +import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit } from "./common.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { InternalCompilerError } from "../../errors.js"; /* ScrDyn helpers for the LLVM backend: per-type match predicates @@ -1199,8 +1199,8 @@ export class LlDyn { : [ { index: 1, type: host.sizeType, name: "length" }, { index: 2, type: host.sizeType, name: "capacity" }, - { index: 7, type: "ptr" as const, name: "data" }, - { index: 8, type: "ptr" as const, name: "presence" }, + { index: SCR_ARR_FIELD.data, type: "ptr" as const, name: "data" }, + { index: SCR_ARR_FIELD.present, type: "ptr" as const, name: "presence" }, ]; members.forEach((member) => { const cachedPtr = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 11f02805d..648a55fe4 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1912,12 +1912,14 @@ export class LlEmitter { `%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`, `%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`, // ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem, - // elem_retain, elem_release, elem_trace, data, present, ext }, where - // ext points at the out-of-line sparse, property and metadata storage - // (NULL until first used). Live dynamic stream commits swap its dense, - // presence and out-of-line storage while preserving the target - // object's identity. - `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, ptr, ptr, ptr, ptr }`, + // cyc, ops, data, present, ext } (field numbers in SCR_ARR_FIELD), + // where ops points at the element type's constant RC descriptor + // (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse, + // property and metadata storage (NULL until first used). Live dynamic + // stream commits swap its dense, presence and out-of-line storage while + // preserving the target object's identity. + `%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr }`, + `%ScrArrOps = type { ptr, ptr, ptr }`, // The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release, // val_trace, key_retain, key_release, key_trace } — the inline RC // fast paths test the two trace slots (shapes.ts). @@ -2102,7 +2104,7 @@ export class LlEmitter { out.push( `@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`, `@${inst.sym}_present = internal constant [${n} x i8] ${present}`, - `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, ptr null, ptr null, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`, + `@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, i32 0, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`, ); } if (this.templateStringsInstances.size > 0) out.push(``); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 5a2366d50..c452240ac 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -4,6 +4,7 @@ import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js"; import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; import { mangleResolveThunk } from "../mangle.js"; import { + arrOps, elemAccess, FN_ATTRS, mapKeyAccess, @@ -1299,10 +1300,18 @@ export function emitMapLikeIntrinsic( return { name: "", type: e.type }; case "toArray": { if (receiverType.kind !== "set") throw new InternalCompilerError("unreachable"); - // Fresh +1 elem[] of the live entries in insertion order. - host.declare(`declare ptr @scr_set_to_arr_${kAcc}(ptr)`); + // Fresh +1 elem[] of the live entries in insertion order; a REF result + // takes its element type's array descriptor. const t = B.tmp(); - B.line(`${t} = call ptr @scr_set_to_arr_${kAcc}(ptr ${r.name})`); + if (kAcc === "ref") { + host.declare(`declare ptr @scr_set_to_arr_ref(ptr, ptr)`); + B.line( + `${t} = call ptr @scr_set_to_arr_ref(ptr ${r.name}, ptr ${arrOps(host.shapeHost, receiverType.elem)})`, + ); + } else { + host.declare(`declare ptr @scr_set_to_arr_${kAcc}(ptr)`); + B.line(`${t} = call ptr @scr_set_to_arr_${kAcc}(ptr ${r.name})`); + } return host.own({ name: t, type: e.type }); } default: { diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index d6cd7ff4c..cdce1000c 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -16,7 +16,7 @@ import { import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; import { classFieldIndex, classStructSym } from "./classes.js"; -import { llvmCommentText } from "./common.js"; +import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import { FN_ATTRS, elemAccess, @@ -176,9 +176,9 @@ export function streamTypedRefCommitAdapter( const storageMembers = [ { name: "len", index: 1, type: host.sizeType }, { name: "cap", index: 2, type: host.sizeType }, - { name: "data", index: 7, type: "ptr" }, - { name: "present", index: 8, type: "ptr" }, - { name: "ext", index: 9, type: "ptr" }, + { name: "data", index: SCR_ARR_FIELD.data, type: "ptr" }, + { name: "present", index: SCR_ARR_FIELD.present, type: "ptr" }, + { name: "ext", index: SCR_ARR_FIELD.ext, type: "ptr" }, ]; for (const member of storageMembers) { lines.push( diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index abe88c9d1..73b79b741 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -12,7 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js"; import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { ReferenceEffects } from "./reference-effects.js"; -import { exactIndexLines } from "./common.js"; +import { SCR_ARR_FIELD, exactIndexLines } from "./common.js"; export interface LocalArrayRead { type: IrType; @@ -513,7 +513,9 @@ function emitDenseReferenceArrayRead( statePtr = B.tmp(), denseState = B.tmp(), densePresent = B.tmp(); - B.line(`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 8`); + B.line( + `${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.present}`, + ); host.markMemoryPointer(statesPtr, "array:header"); B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); B.line(`${statePtr} = getelementptr inbounds i8, ptr ${states}, ${host.sizeType} ${offset}`); @@ -526,7 +528,9 @@ function emitDenseReferenceArrayRead( data = B.tmp(), valuePtr = B.tmp(), raw = B.tmp(); - B.line(`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 7`); + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${offset}`); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 7f3e28d5d..2a3b983e7 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -27,7 +27,7 @@ import { mangleRecordTrace, } from "../mangle.js"; import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js"; -import { llvmCommentText } from "./common.js"; +import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import type { NullableUnions } from "./nullable-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -197,7 +197,7 @@ export function releaseSym(host: ShapeHost, t: IrType): string { /** How a runtime family decides whether an object carries a ScrCycHdr: * never, always, or per object through the trace slots its C release - * tests (`a->elem_trace`, `m->key_trace || m->val_trace`). */ + * tests (`a->cyc`, `m->key_trace || m->val_trace`). */ type RcCycle = "none" | "always" | "arr" | "map"; interface InlineRcFamily { @@ -318,9 +318,9 @@ export function inlineRcDecls(host: ShapeHost): string[] { function rcHeaderTest(cycle: "arr" | "map", yes: string, no: string): string[] { if (cycle === "arr") { return [ - ` %tracep = getelementptr inbounds %ScrArr, ptr %o, i32 0, i32 6 ; elem_trace`, - ` %trace = load ptr, ptr %tracep`, - ` %headered = icmp ne ptr %trace, null`, + ` %cycp = getelementptr inbounds %ScrArr, ptr %o, i32 0, i32 ${SCR_ARR_FIELD.cyc} ; cyc`, + ` %cyc = load i32, ptr %cycp`, + ` %headered = icmp ne i32 %cyc, 0`, ` br i1 %headered, label %${yes}, label %${no}`, ]; } @@ -566,7 +566,7 @@ function elemKindNum(elem: IrType): number { /** Array construction call text: ref elements * (records, unions, closures — and cycle-capable inner arrays, whose * SCR_ELEM_ARR spelling would hide them from the outer array's trace) - * construct through scr_arr_new_ref with the element type's `_v` RC entry + * construct through scr_arr_new_ops with the element type's `_v` RC entry * points; every other element kind keeps the plain scr_arr_new call. */ export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): string { const useRef = @@ -590,9 +590,21 @@ export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): stri host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`); return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`; } + host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`); + return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`; +} + +/** The constant %ScrArrOps descriptor (retain, release, trace) of a REF + * element type, defined once per module and shared by all its arrays. */ +export function arrOps(host: ShapeHost, elem: IrType): string { const v = vAdapters(host, elem); - host.declare(`declare ptr @scr_arr_new_ref(ptr, ptr, ptr, ${host.sizeType})`); - return `call ptr @scr_arr_new_ref(ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, elem)}, ${host.sizeType} ${capText})`; + const trace = traceArg(host, elem); + const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_"); + const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`; + host.declare( + `${name} = internal constant %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace} }`, + ); + return name; } /* ── capture boxes ────────────────────────────────────────────────────── */ diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index cdbfbacc5..a8b30c680 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 = 12 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v12" as const; +export const RUNTIME_ABI_VERSION = 13 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v13" 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 8939fd4cc..7fd8dc415 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: 12, marker: "scr_runtime_abi_v12" }, + runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" }, 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 e71cacd8a..42a30cf2c 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -113,7 +113,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" }, + runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" }, 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 d85579558..49fd801b6 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_v12"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v13"); 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_v12"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v13"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index b062b5980..45a96f84e 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -316,7 +316,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" }, + runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index e0752938e..74f1a12ee 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -403,7 +403,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" }, + runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 78854145a..6cf69bed8 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -132,7 +132,7 @@ static void scr_elem_release(const ScrArr *a, uint64_t slot) { if (a->elem == SCR_ELEM_STR) scr_str_release((ScrStr *)p); else if (a->elem == SCR_ELEM_ARR) scr_arr_release((ScrArr *)p); else if (a->elem == SCR_ELEM_BYTES) scr_bytes_release((ScrBytes *)p); - else if (a->elem == SCR_ELEM_REF) a->elem_release(p); + else if (a->elem == SCR_ELEM_REF) a->ops->release(p); } static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) { @@ -142,7 +142,7 @@ static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) { if (a->elem == SCR_ELEM_STR) p = scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_ARR) p = scr_arr_retain((ScrArr *)p); else if (a->elem == SCR_ELEM_BYTES) p = scr_bytes_retain((ScrBytes *)p); - else p = a->elem_retain(p); + else p = a->ops->retain(p); return scr_slot_from_ptr(p); } @@ -661,9 +661,8 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { a->len = 0; a->cap = 0; a->elem = elem; - a->elem_retain = NULL; - a->elem_release = NULL; - a->elem_trace = NULL; + a->cyc = 0; + a->ops = NULL; a->data = NULL; a->present = NULL; a->ext = NULL; @@ -693,7 +692,7 @@ void scr_arr_swap_indexed_ext(ScrArr *a, ScrArr *b) { } /* Collector trace of a cycle-capable array: every element is a headered - * child (elem_trace non-NULL means the element TYPE carries a header, and + * child (`cyc` means the element TYPE carries a header, and * arrays are monomorphic), so trace visits all of them and the teardown * below releases none — the complement contract in scr_runtime.h. */ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { @@ -737,11 +736,10 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } -ScrArr *scr_arr_new_ref(void *(*elem_retain)(void *), - void (*elem_release)(void *), - ScrTraceFn elem_trace, size_t initial_cap) { +ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { ScrArr *a; - if (elem_trace) { + bool cyc = ops->trace != NULL; + if (cyc) { static const ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; a = scr_cyc_alloc_inline(sizeof(ScrArr), &cyc_type); } else { @@ -752,9 +750,8 @@ ScrArr *scr_arr_new_ref(void *(*elem_retain)(void *), a->len = 0; a->cap = 0; a->elem = SCR_ELEM_REF; - a->elem_retain = elem_retain; - a->elem_release = elem_release; - a->elem_trace = elem_trace; + a->cyc = cyc; + a->ops = ops; a->data = NULL; a->present = NULL; a->ext = NULL; @@ -799,7 +796,7 @@ static void scr_arr_destroy(void *object) { } } } - if (a->elem_trace) { + if (a->cyc) { scr_arr_gc_free(a); } else { scr_mem_free(a->data); @@ -815,9 +812,9 @@ static void scr_arr_destroy(void *object) { void scr_arr_release(ScrArr *a) { if (!a || a->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--a->rc == 0) { - if (a->elem_trace) scr_cyc_dead(a); + if (a->cyc) scr_cyc_dead(a); scr_rc_destroy(a, scr_arr_destroy); - } else if (a->elem_trace) { + } else if (a->cyc) { scr_cyc_released(a); /* possible cycle root; may collect */ } } @@ -1213,7 +1210,7 @@ void *scr_arr_get_ref(ScrArr *a, double i) { if (!p) return NULL; if (a->elem == SCR_ELEM_STR) scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_BYTES) scr_bytes_retain((ScrBytes *)p); - else if (a->elem == SCR_ELEM_REF) p = a->elem_retain(p); + else if (a->elem == SCR_ELEM_REF) p = a->ops->retain(p); else scr_arr_retain((ScrArr *)p); return p; } @@ -1630,7 +1627,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { size_t hint = scr_arr_is_dense(a) || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = a->elem == SCR_ELEM_REF - ? scr_arr_new_ref(a->elem_retain, a->elem_release, a->elem_trace, hint ? hint : 1) + ? scr_arr_new_ops(a->ops, hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; size_t old_len = a->len; @@ -1699,7 +1696,7 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, ScrArr *snapshot = items == a ? scr_arr_slice(a, 0, INFINITY) : NULL; if (snapshot) items = snapshot; ScrArr *removed = a->elem == SCR_ELEM_REF - ? scr_arr_new_ref(a->elem_retain, a->elem_release, a->elem_trace, n) + ? scr_arr_new_ops(a->ops, n) : scr_arr_new(a->elem, n); if (n) { memcpy(removed->data, a->data + from, n * sizeof(*a->data)); @@ -1743,7 +1740,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou bool refs = scr_elem_is_ref(a->elem); if (!scr_arr_is_dense(a) || !scr_arr_dense_ok(a, next_len, count)) { ScrArr *items = a->elem == SCR_ELEM_REF - ? scr_arr_new_ref(a->elem_retain, a->elem_release, a->elem_trace, count) + ? scr_arr_new_ops(a->ops, count) : scr_arr_new(a->elem, count); scr_arr_push_many(items, count, slots); scr_arr_release(scr_arr_splice_insert(a, start, deleteCount, items)); @@ -1812,10 +1809,10 @@ ScrArr *scr_arr_flat_copy(const ScrArr *a, ScrArr *out, bool flatten) { static bool scr_arr_ref_eq(const ScrArr *a, uint64_t slot, void *v) { void *p = scr_slot_to_ptr(slot); if (a->elem == SCR_ELEM_STR) return scr_str_eq((ScrStr *)p, (ScrStr *)v); - if (a->elem == SCR_ELEM_REF && a->elem_retain == scr_dyn_retain_v) { + if (a->elem == SCR_ELEM_REF && a->ops->retain == scr_dyn_retain_v) { return scr_dyn_strict_eq((ScrDyn *)p, (ScrDyn *)v); } - if (a->elem == SCR_ELEM_REF && a->elem_retain == scr_closure_retain_v) + if (a->elem == SCR_ELEM_REF && a->ops->retain == scr_closure_retain_v) return scr_closure_identity_equal((ScrClosure *)p, (ScrClosure *)v); return p == v; } @@ -1869,7 +1866,7 @@ bool scr_arr_includes_bool(ScrArr *a, bool v) { } bool scr_arr_includes_ref(ScrArr *a, void *v) { - if (a->elem == SCR_ELEM_REF && a->elem_retain == scr_dyn_retain_v) { + if (a->elem == SCR_ELEM_REF && a->ops->retain == scr_dyn_retain_v) { const ScrDyn *needle = (const ScrDyn *)v; if (needle->kind == SCR_DYN_NUM && needle->v.num != needle->v.num) { for (size_t i = 0; i < a->cap && i < a->len; i++) { @@ -1932,7 +1929,7 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { size_t hint = to <= a->cap || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = a->elem == SCR_ELEM_REF - ? scr_arr_new_ref(a->elem_retain, a->elem_release, a->elem_trace, hint ? hint : 1) + ? scr_arr_new_ops(a->ops, hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; if (to <= a->cap) { @@ -2202,7 +2199,7 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity) if (a->elem == SCR_ELEM_STR) c.retain = scr_sort_retain_str; else if (a->elem == SCR_ELEM_ARR) c.retain = scr_sort_retain_arr; else if (a->elem == SCR_ELEM_BYTES) c.retain = scr_sort_retain_bytes; - else if (a->elem == SCR_ELEM_REF) c.retain = a->elem_retain; + else if (a->elem == SCR_ELEM_REF) c.retain = a->ops->retain; else scr_trap("scriptc: invalid comparator sort element kind\n"); } uint64_t *src = malloc(n * sizeof(*src)); diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 37644f705..c445711cc 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1342,7 +1342,7 @@ static void scr_promise_all_settle(ScrAllState *st, ScrPromise *result, size_t i if (st->values) { scr_promise_fulfill_ref(result, scr_arr_retain(st->values), scr_arr_retain_v, scr_arr_release_v, - st->values->elem_trace ? scr_arr_trace_v : NULL); + st->values->cyc ? scr_arr_trace_v : NULL); } else { scr_promise_fulfill_void(result); } @@ -1460,7 +1460,7 @@ ScrPromise *scr_promise_all(ScrArr *ps, ScrArr *values, if (st->values) { scr_promise_fulfill_ref(result, scr_arr_retain(st->values), scr_arr_retain_v, scr_arr_release_v, - st->values->elem_trace ? scr_arr_trace_v : NULL); + st->values->cyc ? scr_arr_trace_v : NULL); } else { scr_promise_fulfill_void(result); } diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 675c2ae3b..e63b6e1a7 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -888,9 +888,12 @@ static void scr_dyn_all_result(ScrPromise *destination, ScrPromise *source) { scr_promise_fulfill_ref(destination, result, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); } +static const ScrArrOps scr_promise_arr_ops = { scr_promise_retain_v, scr_promise_release_v, scr_promise_trace_v }; +static const ScrArrOps scr_dyn_arr_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v }; + ScrPromise *scr_dyn_promise_all(const ScrDyn *entries) { - ScrArr *promises = scr_arr_new_ref(scr_promise_retain_v, scr_promise_release_v, scr_promise_trace_v, entries->v.arr.len); - ScrArr *values = scr_arr_new_ref(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, entries->v.arr.len); + ScrArr *promises = scr_arr_new_ops(&scr_promise_arr_ops, entries->v.arr.len); + ScrArr *values = scr_arr_new_ops(&scr_dyn_arr_ops, entries->v.arr.len); for (size_t i = 0; i < entries->v.arr.len; i++) { ScrDyn *value = entries->v.arr.items[i]; ScrPromise *source; diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 1454d3086..1e2cc8a13 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -6,6 +6,9 @@ * exact source lists). */ #include "scr_runtime.h" +static const ScrArrOps scr_bytes_io_str_ops = { scr_str_retain_v, scr_str_release_v, NULL }; +static const ScrArrOps scr_bytes_io_bytes_ops = { scr_bytes_retain_v, scr_bytes_release_v, NULL }; + #include #include #include @@ -337,7 +340,7 @@ static void scr_fs_symlink_checked_core(const ScrDyn *target, const ScrDyn *dest ScrStr *source = !scr_exc_pending() ? scr_dyn_string_coerce_js(target) : NULL; ScrStr *absolute = NULL; if (link && source && !scr_exc_pending()) { - ScrArr *parts = scr_arr_new_ref(scr_str_retain_v, scr_str_release_v, NULL, 3); + ScrArr *parts = scr_arr_new_ops(&scr_bytes_io_str_ops, 3); scr_arr_push_ref(parts, scr_str_retain(link)); scr_arr_push_ref(parts, scr_str_new("..", 2)); scr_arr_push_ref(parts, scr_str_retain(source)); @@ -658,7 +661,7 @@ static ScrArr *scr_fs_cb_vectors(const ScrDyn *buffers) { scr_dyn_release(snapshot); return NULL; } - ScrArr *out = scr_arr_new_ref(scr_bytes_retain_v, scr_bytes_release_v, NULL, array->v.arr.len); + ScrArr *out = scr_arr_new_ops(&scr_bytes_io_bytes_ops, array->v.arr.len); for (size_t i = 0; i < array->v.arr.len; i++) { const ScrDyn *buffer = array->v.arr.items[i]; if (!scr_dyn_bytes_is(buffer, SCR_BYTES_U8)) { diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index bc558318d..360f4f2ad 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -125,7 +125,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v12(void) {} +void scr_runtime_abi_v13(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index 3ff0b5a68..063536b8f 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -23,8 +23,7 @@ static bool copying_elem_is_ref(ScrElemKind kind) { static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { ScrArr *out = a->elem == SCR_ELEM_REF - ? scr_arr_new_ref(a->elem_retain, a->elem_release, - a->elem_trace, cap ? cap : 1) + ? scr_arr_new_ops(a->ops, cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); out->len = cap; return out; @@ -36,7 +35,7 @@ static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { if (a->elem == SCR_ELEM_STR) p = scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_ARR) p = scr_arr_retain((ScrArr *)p); else if (a->elem == SCR_ELEM_BYTES) p = scr_bytes_retain((ScrBytes *)p); - else p = a->elem_retain(p); + else p = a->ops->retain(p); return copying_slot_from_ptr(p); } diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index c95c1722d..ae9cdb4dd 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -791,9 +791,11 @@ ScrArr *scr_emitter_event_names(ScrEmitter *em) { * The once wrapper is runtime-internal here, so BOTH forms answer the * original listeners (SEMANTICS.md documents the rawListeners * divergence). */ +static const ScrArrOps scr_ee_closure_arr_ops = { &scr_closure_retain_v, &scr_closure_release_v, + &scr_closure_trace_v }; + ScrArr *scr_emitter_listeners(ScrEmitter *em, ScrStr *name) { - ScrArr *out = scr_arr_new_ref(&scr_closure_retain_v, &scr_closure_release_v, - &scr_closure_trace_v, 0); + ScrArr *out = scr_arr_new_ops(&scr_ee_closure_arr_ops, 0); ScrEeBucket *b = em->reg ? scr_ee_bucket_find(em->reg, name->data, name->len) : NULL; if (b) { for (size_t i = 0; i < b->n; i++) { diff --git a/packages/runtime/src/scr_island.c b/packages/runtime/src/scr_island.c index d32dec4df..4d23370e8 100644 --- a/packages/runtime/src/scr_island.c +++ b/packages/runtime/src/scr_island.c @@ -11095,8 +11095,8 @@ ScrArr *scr_jsval_exit_jsval_arr(ScrJsval *v) { int64_t len = 0; JS_ToInt64(isl_ctx, &len, lv); JS_FreeValue(isl_ctx, lv); - ScrArr *out = scr_arr_new_ref(&scr_jsval_retain_v, &scr_jsval_release_v, NULL, - len > 0 ? (size_t)len : 0); + static const ScrArrOps jsval_ops = { &scr_jsval_retain_v, &scr_jsval_release_v, NULL }; + ScrArr *out = scr_arr_new_ops(&jsval_ops, len > 0 ? (size_t)len : 0); for (int64_t i = 0; i < len; i++) { JSValue e = JS_GetPropertyUint32(isl_ctx, v->v, (uint32_t)i); /* getters run */ if (JS_IsException(e)) { diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 4887489f3..463d6d2d1 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -391,7 +391,8 @@ static void test_item_release(void *ptr) { static ScrArr *test_tags_new(size_t cap) { return scr_arr_new(SCR_ELEM_STR, cap); } static ScrArr *test_list_new(size_t cap) { - return scr_arr_new_ref(test_item_retain, test_item_release, NULL, cap); + static const ScrArrOps item_ops = { test_item_retain, test_item_release, NULL }; + return scr_arr_new_ops(&item_ops, cap); } static void test_arr_release(void *ptr) { scr_arr_release(ptr); } diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 65165ed67..0e4ce71ce 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -4702,7 +4702,8 @@ int scr_fs_symlink_infer(ScrStr *absolute) { void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind) { #ifdef _WIN32 - ScrArr *parts = scr_arr_new_ref(scr_str_retain_v, scr_str_release_v, NULL, 3); + static const ScrArrOps str_ops = { scr_str_retain_v, scr_str_release_v, NULL }; + ScrArr *parts = scr_arr_new_ops(&str_ops, 3); scr_arr_push_ref(parts, scr_str_retain(destination)); scr_arr_push_ref(parts, scr_str_new("..", 2)); scr_arr_push_ref(parts, scr_str_retain(target)); @@ -7545,12 +7546,12 @@ ScrArr *scr_set_to_arr_str(const ScrMap *s) { return out; } -ScrArr *scr_set_to_arr_ref(const ScrMap *s) { - ScrArr *out = scr_arr_new_ref(s->key_retain, s->key_release, s->key_trace, s->nlive); +ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { + ScrArr *out = scr_arr_new_ops(ops, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; for (size_t i = 0; i < s->nentries; i++) { if (!s->entries[i].hash) continue; - s->key_retain((void *)(uintptr_t)s->entries[i].key); + ops->retain((void *)(uintptr_t)s->entries[i].key); out->data[out->len] = s->entries[i].key; out->present[out->len++] = SCR_ARR_VALUE; } diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 481b0d241..84adf177f 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1324,7 +1324,8 @@ void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked) { } ScrArr *scr_set_to_arr_dyn(const ScrMap *map) { - ScrArr *out = scr_arr_new_ref(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, map->nlive); + static const ScrArrOps dyn_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v }; + ScrArr *out = scr_arr_new_ops(&dyn_ops, map->nlive); for (size_t i = 0; i < map->nentries; i++) { if (scr_map_iter_live(map, (double)i)) scr_arr_push_ref(out, scr_map_dyn_key(map, (double)i)); } diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index d48a348f5..265e08875 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -118,7 +118,7 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { scr_pub_refuse(ctx, "Cannot publish an array with non-index properties"); return; } - if (!scr_pub_mark(ctx, a, a->elem_trace != NULL, SCR_PUB_KIND_ARRAY)) return; + if (!scr_pub_mark(ctx, a, a->cyc != 0, SCR_PUB_KIND_ARRAY)) return; ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; size_t dense = a->len < a->cap ? a->len : a->cap; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index fc4ab7ef5..7a52b2699 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -62,7 +62,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_v12(void); +void scr_runtime_abi_v13(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── @@ -1564,8 +1564,8 @@ typedef enum { SCR_ELEM_BYTES, /* slots are ScrBytes* — released with the array */ /* Slots are void* to values the runtime cannot lay out itself — records, * class instances, unions, and cycle-capable inner arrays. The element - * type's RC entry points arrive at construction (scr_arr_new_ref) and are - * stored ONCE per array, the SCR_BOX_OBJ / map-value technique; every + * type's RC entry points arrive at construction (scr_arr_new_ops) as one + * constant descriptor per element type, stored once per array; every * array is still monomorphic (one element type, one set of entry points). */ SCR_ELEM_REF, } ScrElemKind; @@ -1576,21 +1576,32 @@ typedef enum { SCR_ARR_UNDEFINED = 2, } ScrArrState; +/* The RC entry points of an SCR_ELEM_REF array's element type: one constant + * per element type (emitted by the compiler, or static in the runtime), + * shared by every array of that type. retain/release are the type's `_v` + * adapters; trace is non-NULL exactly when the element type carries a cycle + * header. */ +typedef struct ScrArrOps { + void *(*retain)(void *); + void (*release)(void *); + ScrTraceFn trace; +} ScrArrOps; + typedef struct ScrArr { size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */ size_t len; size_t cap; /* dense capacity; sparse entries cover indices >= cap */ ScrElemKind elem; - /* SCR_ELEM_REF only; NULL for every other element kind. elem_trace is - * non-NULL exactly when the element type carries a cycle header: such - * arrays are CYCLE-CAPABLE (an element can point back at the array that - * owns it) and allocate with the hidden collector header — their trace - * visits every element, and the collector teardown frees only the slot - * storage (all elements are traced children). Arrays of acyclic ref - * elements and the historic element kinds keep the lean 1-word header. */ - void *(*elem_retain)(void *); - void (*elem_release)(void *); - ScrTraceFn elem_trace; + /* 1 exactly when ops->trace is non-NULL: the element type carries a cycle + * header, so the array is CYCLE-CAPABLE (an element can point back at the + * array that owns it) and allocates with the hidden collector header — + * its trace visits every element, and the collector teardown frees only + * the slot storage (all elements are traced children). Arrays of acyclic + * ref elements and the historic element kinds keep the lean 1-word + * header. Kept beside `elem` so the RC fast paths test it with one load. */ + uint32_t cyc; + /* SCR_ELEM_REF only; NULL for every other element kind. */ + const ScrArrOps *ops; uint64_t *data; uint8_t *present; /* one state per dense slot; heap arrays share data allocation */ /* Rarely used storage (sparse indices, noncanonical numeric properties, @@ -1608,11 +1619,15 @@ typedef struct ScrArrExt { size_t prop_cap; struct ScrDyn *metadata; /* heterogeneous named properties of match results */ } ScrArrExt; -/* The LLVM backend's inline retain/release read elem_trace through its - * mirrored %ScrArr type (field 6) to decide mark-live / candidate buffering - * exactly like scr_arr_retain and scr_arr_release. */ -_Static_assert(offsetof(ScrArr, elem_trace) == 3 * sizeof(size_t) + 3 * sizeof(void *), - "inline RC fast paths read ScrArr.elem_trace as %ScrArr field 6"); +/* The LLVM backend's inline retain/release read `cyc` through its mirrored + * %ScrArr type (field 4, i32) to decide mark-live / candidate buffering + * exactly like scr_arr_retain and scr_arr_release; its inline element reads + * use data (field 6) and present (field 7). */ +_Static_assert(sizeof(ScrElemKind) == sizeof(uint32_t), "ScrArr.elem is an i32 (%ScrArr field 3)"); +_Static_assert(offsetof(ScrArr, cyc) == 3 * sizeof(size_t) + sizeof(uint32_t), + "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); +_Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 2 * sizeof(uint32_t) + sizeof(void *), + "inline element reads use ScrArr.data as %ScrArr field 6"); typedef struct ScrArrSparseSlot { size_t index; @@ -1629,13 +1644,12 @@ typedef struct ScrArrProp { ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap); /* returns +1 */ -/* SCR_ELEM_REF construction: retain/release must be non-NULL (the compiler - * passes the element type's `_v` adapters); trace is non-NULL iff the - * element type carries a cycle header (the array then allocates with the - * collector header — see the struct comment). Returns +1. */ -ScrArr *scr_arr_new_ref(void *(*elem_retain)(void *), - void (*elem_release)(void *), - ScrTraceFn elem_trace, size_t initial_cap); +/* SCR_ELEM_REF construction: `ops` must outlive the array (a constant; the + * compiler emits one per element type) and its retain/release must be + * non-NULL; ops->trace is non-NULL iff the element type carries a cycle + * header (the array then allocates with the collector header — see the + * struct comment). Returns +1. */ +ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap); static inline ScrArr *scr_arr_retain(ScrArr *a) { if (a->rc != SIZE_MAX) { @@ -1645,8 +1659,8 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { } /* Releases ref elements (SCR_ELEM_STR/ARR/BYTES/REF) recursively at rc == 0. - * NULL-tolerant (uninitialized locals). Cycle-capable arrays (elem_trace - * non-NULL) feed the candidate-root buffer like every headered type. */ + * NULL-tolerant (uninitialized locals). Cycle-capable arrays (`cyc`) feed + * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); /* The array's out-of-line storage, allocated (zeroed) on first use. */ @@ -2152,12 +2166,12 @@ void scr_set_add_all(ScrMap *set, ScrArr *values); ScrArr *scr_set_to_arr_f64(const ScrMap *s); ScrArr *scr_set_to_arr_bool(const ScrMap *s); ScrArr *scr_set_to_arr_str(const ScrMap *s); -ScrArr *scr_set_to_arr_ref(const ScrMap *s); +ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops); ScrArr *scr_set_to_arr_dyn(const ScrMap *s); /* REF-element Set construction: a set-shaped map whose keys are refcounted * pointers under identity hashing (see SCR_MAP_KEY_REF above). The element - * type's `_v` adapters arrive once, the scr_arr_new_ref technique. */ + * type's `_v` adapters arrive once, as for arrays (scr_arr_new_ops). */ ScrMap *scr_set_new_ref(void *(*elem_retain)(void *), void (*elem_release)(void *)); /* The live STRING keys of a map in JS OWN-KEY ORDER (Object.keys/values/ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index dc7647090..72eae91de 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2329,7 +2329,8 @@ static ScrPromise *scr_sc_consume(ScrStream *s, int kind) { scr_promise_reject_pending(p); return p; } - ScrArr *chunks = scr_arr_new_ref(scr_bytes_retain_v, scr_bytes_release_v, NULL, 4); + static const ScrArrOps bytes_ops = { scr_bytes_retain_v, scr_bytes_release_v, NULL }; + ScrArr *chunks = scr_arr_new_ops(&bytes_ops, 4); ScrStreamErrInv fin_inv = kind == SCR_SC_TEXT ? &scr_sc_fin_text : kind == SCR_SC_JSON ? &scr_sc_fin_json : &scr_sc_fin_buffer; diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 19f1d4fee..0b3b0a576 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -396,11 +396,14 @@ static MockRec *mock_cyc_new(int value) { return r; } +static const ScrArrOps mock_ops = { &mock_retain, &mock_release, NULL }; +static const ScrArrOps mock_cyc_ops = { &mock_cyc_retain, &mock_cyc_release, &mock_trace }; + static void test_ref_elements(void) { #ifdef SCR_RC_AUDIT long arrays0 = scr_arr_live_count(); #endif - ScrArr *a = scr_arr_new_ref(&mock_retain, &mock_release, NULL, 0); + ScrArr *a = scr_arr_new_ops(&mock_ops, 0); scr_arr_push_ref(a, mock_new(1)); /* ownership moves in */ scr_arr_push_ref(a, mock_new(2)); check_f64(scr_arr_len(a), 2, "ref push grows"); @@ -446,7 +449,7 @@ static void test_ref_cycle(void) { long arrays0 = scr_arr_live_count(); #endif /* arr -> rec -> arr: drop the external references, then collect. */ - ScrArr *arr = scr_arr_new_ref(&mock_cyc_retain, &mock_cyc_release, &mock_trace, 0); + ScrArr *arr = scr_arr_new_ops(&mock_cyc_ops, 0); MockRec *rec = mock_cyc_new(7); rec->owner = (ScrArr *)scr_arr_retain(arr); /* rec points back at arr */ scr_arr_push_ref(arr, rec); /* arr owns rec */ @@ -466,7 +469,7 @@ static void test_ref_truncate_cycle(void) { /* The truncation release runs while the record points back at the array. * The array edge must be detached before mock_cyc_release can trigger the * cycle collector. */ - ScrArr *arr = scr_arr_new_ref(&mock_cyc_retain, &mock_cyc_release, &mock_trace, 0); + ScrArr *arr = scr_arr_new_ops(&mock_cyc_ops, 0); MockRec *rec = mock_cyc_new(8); rec->owner = (ScrArr *)scr_arr_retain(arr); scr_arr_push_ref(arr, rec); @@ -481,7 +484,7 @@ static void test_ref_truncate_cycle(void) { static void test_ref_trace_storage_boundaries(void) { long before = mock_live; - ScrArr *arr = scr_arr_new_ref(&mock_cyc_retain, &mock_cyc_release, &mock_trace, 4096); + ScrArr *arr = scr_arr_new_ops(&mock_cyc_ops, 4096); MockRec *dense = mock_cyc_new(1); MockRec *sparse = mock_cyc_new(2); MockRec *property = mock_cyc_new(3); diff --git a/packages/runtime/test/test_map.c b/packages/runtime/test/test_map.c index 09630fb31..f9faffa59 100644 --- a/packages/runtime/test/test_map.c +++ b/packages/runtime/test/test_map.c @@ -588,6 +588,8 @@ static void test_value_keys_and_copies(void) { } #ifdef SCR_RC_AUDIT +static const ScrArrOps map_arr_ops = { scr_map_retain_v, scr_map_release_v, scr_map_trace_v }; + static void test_identity_cycles(void) { scr_collect_cycles(); long maps = scr_map_live_count(); @@ -596,7 +598,7 @@ static void test_identity_cycles(void) { long unions = scr_union_live_count(); for (int mode = 0; mode < 5; mode++) { for (int i = 0; i < 100; i++) { - ScrArr *key = scr_arr_new_ref(scr_map_retain_v, scr_map_release_v, scr_map_trace_v, 0); + ScrArr *key = scr_arr_new_ops(&map_arr_ops, 0); bool wrapped = mode >= 3; ScrMapValKind value_kind = mode == 0 ? SCR_MAP_VAL_F64 : SCR_MAP_VAL_REF; ScrTraceFn value_trace = mode == 2 ? scr_arr_trace_v : NULL; diff --git a/tests/harness/large-artifacts.test.ts b/tests/harness/large-artifacts.test.ts index 207ce80a2..a8bb95fb4 100644 --- a/tests/harness/large-artifacts.test.ts +++ b/tests/harness/large-artifacts.test.ts @@ -4,8 +4,8 @@ import { occurrences, withoutFirstLine, withoutRange } from "./large-artifacts.j test("byte-level artifact edits match their string equivalents", () => { const text = "x declare void @scr_runtime_abi_v3()\ndeclare void @scr_runtime_abi_vX()\n" + - "declare void @scr_runtime_abi_v12()\n call void @scr_runtime_abi_v12()\n" + - "é\n call void @scr_runtime_abi_v12()\n"; + "declare void @scr_runtime_abi_v13()\n call void @scr_runtime_abi_v13()\n" + + "é\n call void @scr_runtime_abi_v13()\n"; const bytes = Buffer.from(text); const declaration = /^declare void @scr_runtime_abi_v\d+\(\)\n/m; const call = /^ {2}call void @scr_runtime_abi_v\d+\(\)\n/m; @@ -21,7 +21,7 @@ test("byte-level artifact edits match their string equivalents", () => { expect(edited.toString()).toBe(text.replace(declaration, "").replace(call, "")); expect(withoutFirstLine(bytes, "missing", /^missing\n$/)).toBe(bytes); - const line = " call void @scr_runtime_abi_v12()\n"; + const line = " call void @scr_runtime_abi_v13()\n"; const found = occurrences(bytes, line); expect(found).toHaveLength(text.split(line).length - 1); expect(withoutRange(bytes, found[0]!, Buffer.byteLength(line)).toString()).toBe( diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index b00a46da7..09caf2f0f 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -100,7 +100,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 12, marker: "scr_runtime_abi_v12" }, + runtime_abi: { version: 13, marker: "scr_runtime_abi_v13" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: {