Keep the RC entry points of array elements in one shared descriptor (#882)

This commit is contained in:
Chris Tate
2026-10-12 01:59:44 -05:00
committed by GitHub
38 changed files with 206 additions and 133 deletions
+1 -1
View File
@@ -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 <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v12</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> 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 <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v13</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> 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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -15,4 +15,4 @@ $ ./app-ld
`cc` uses the C compiler as a linker driver. `ld` links the same precompiled runtime objects directly with the selected Apple SDK. The object defines `main`; it is a complete program object, not a library to load into another process. Use `scriptc build --lib --profile ...` for a host-callable static library.
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_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.
+1 -1
View File
@@ -34,7 +34,7 @@ Builds use a bounded persistent cache. Unchanged source can reuse the validated
- `scriptc run <file.ts>` — compile and run
- `scriptc coverage <file.ts>` — 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 <specifier=file.d.ts>` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers.
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 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");
});
});
@@ -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
@@ -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)}`);
+3 -3
View File
@@ -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();
@@ -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(``);
@@ -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: {
@@ -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(
@@ -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}`);
+20 -8
View File
@@ -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 ────────────────────────────────────────────────────── */
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
export const RUNTIME_ABI_VERSION = 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;
@@ -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: {
@@ -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",
@@ -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 () => {
@@ -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,
@@ -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,
+22 -25
View File
@@ -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));
+2 -2
View File
@@ -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);
}
+5 -2
View File
@@ -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;
+5 -2
View File
@@ -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 <errno.h>
#include <math.h>
#include <stdio.h>
@@ -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)) {
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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);
}
+4 -2
View File
@@ -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++) {
+2 -2
View File
@@ -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)) {
+2 -1
View File
@@ -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); }
+5 -4
View File
@@ -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;
}
+2 -1
View File
@@ -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));
}
+1 -1
View File
@@ -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;
+43 -29
View File
@@ -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/
+2 -1
View File
@@ -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;
+7 -4
View File
@@ -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);
+3 -1
View File
@@ -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;
+3 -3
View File
@@ -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(
@@ -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: {