From 947f665896b917df015888c5efcd036dc4a1acc6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 13 Oct 2026 18:45:47 -0500 Subject: [PATCH] Store boolean arrays as one byte per element --- docs/content/docs/how-it-works.mdx | 2 +- examples/native-object/README.md | 2 +- packages/cli/test/native-link-info.test.ts | 4 +- .../backend/llvm/dense-array-access.test.ts | 24 ++- .../src/backend/llvm/dense-array-access.ts | 29 +-- .../src/backend/llvm/local-array-reads.ts | 12 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../corpus/5080-boolean-array-storage.ts.json | 6 + .../5081-boolean-array-dynamic-views.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_array.c | 158 +++++++++------ packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_lib.c | 2 +- packages/runtime/src/scr_map.c | 2 +- packages/runtime/src/scr_runtime.h | 26 ++- packages/runtime/test/test_array.c | 102 ++++++++++ tests/corpus/5080-boolean-array-storage.ts | 183 ++++++++++++++++++ .../5081-boolean-array-dynamic-views.ts | 23 +++ .../self-hosting-native-toolchain.test.ts | 2 +- 23 files changed, 503 insertions(+), 98 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5080-boolean-array-storage.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5081-boolean-array-dynamic-views.ts.json create mode 100644 tests/corpus/5080-boolean-array-storage.ts create mode 100644 tests/corpus/5081-boolean-array-dynamic-views.ts diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index 3d1bb011b..d93559b0e 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -70,7 +70,7 @@ Without `-o`, artifacts are written to `.scriptc/` next to the input. Different ### External linking -Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v17` checks runtime compatibility at link time. +Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v18` checks runtime compatibility at link time. `--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples. diff --git a/examples/native-object/README.md b/examples/native-object/README.md index 1e7124f85..d79619e6e 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_v17`, 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_v18`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 3d79e2806..6dde7fbd3 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: 17, marker: "scr_runtime_abi_v17" }, + runtime_abi: { version: 18, marker: "scr_runtime_abi_v18" }, 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_v17"); + expect(error?.stderr).toContain("scr_runtime_abi_v18"); }); }); diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index ce1e30640..dc23ceae2 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -77,6 +77,22 @@ test.each([32, 64] as const)("strict scalar reads check dense storage inline (%i if (bits === 32) expect(fn).toContain("trunc i64"); }); +test.each([32, 64] as const)("boolean reads load one byte per element (%i-bit)", (bits) => { + const read: IrExpr = { kind: "arrayGet", arr: ref("b", flags), index, type: BOOL, loc }; + const llvm = emitLlvmModule(module("flag", BOOL, [{ kind: "return", value: read, loc }]), { + pointerBits: bits, + }); + const fn = body(llvm, "flag"); + const size = bits === 32 ? "i32" : "i64"; + expect(fn).toMatch( + new RegExp( + `getelementptr inbounds i8, ptr %t\\d+, ${size} %t\\d+\\n %t(\\d+) = load i8, ptr .*\\n %t\\d+ = icmp ne i8 %t\\1, 0`, + ), + ); + expect(fn).not.toMatch(/getelementptr inbounds i64/); + expect(fn).toMatch(/call i1 @scr_arr_get_bool\(.*\) cold/); +}); + test("reference reads retain inline and keep a cold +1 runtime fallback", () => { const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc }; // An element removal in the body keeps the read from being lent as a @@ -114,7 +130,10 @@ test("element writes store inline, extend the length, and release replaced refer ); const fn = body(llvm, "store"); expect(fn).toMatch(/store double 0x4004000000000000, ptr /); - expect(fn).toMatch(/zext i1 .* to i64/); + // Boolean elements are one byte each. + expect(fn).toMatch( + /getelementptr inbounds i8, ptr .*\n %t\d+ = zext i1 .* to i8\n store i8 %t\d+, ptr /, + ); expect(fn).toMatch(/store i8 1, ptr /); expect(fn).toMatch(/icmp uge i64 .*/); expect(fn).toContain("call void @scr_str_release("); @@ -143,7 +162,8 @@ test.each([32, 64] as const)( const size = bits === 32 ? "i32" : "i64"; expect(fn.match(new RegExp(`icmp ult ${size} `, "g"))).toHaveLength(3); expect(fn).toMatch(/store double 0x4004000000000000, ptr /); - expect(fn).toMatch(/zext i1 .* to i64/); + expect(fn).toMatch(/zext i1 .* to i8/); + expect(fn.match(/getelementptr inbounds i8, ptr /g)).toHaveLength(4); expect(fn.match(/store i8 1, ptr /g)).toHaveLength(3); expect(fn.match(new RegExp(`add nuw ${size} .*, 1$`, "gm"))).toHaveLength(3); // Appends never release: the slot past the length holds no value. diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index f8613ec2d..83163d99f 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -189,14 +189,21 @@ function statePointer( return statePtr; } +/** The dense slot type: boolean arrays store one byte per element, every + * other element kind an 8-byte slot. */ +function slotType(acc: ArrayAccess): "i8" | "i64" { + return acc === "bool" ? "i8" : "i64"; +} + function valuePointer( host: LlvmEmitterContext, guard: Pick, + acc: ArrayAccess, ): string { const B = host.B; const valuePtr = B.tmp(); B.line( - `${valuePtr} = getelementptr inbounds i64, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, + `${valuePtr} = getelementptr inbounds ${slotType(acc)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); return valuePtr; @@ -244,7 +251,7 @@ export function emitDenseArrayGet( B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`); B.condBr(present, valueLabel, slow); B.startBlock(valueLabel); - const ptr = valuePointer(host, guard); + const ptr = valuePointer(host, guard, acc); let fast: string; let fastLabel = valueLabel; if (acc === "f64") { @@ -253,8 +260,8 @@ export function emitDenseArrayGet( } else if (acc === "bool") { const raw = B.tmp(); fast = B.tmp(); - B.line(`${raw} = load i64, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); - B.line(`${fast} = icmp ne i64 ${raw}, 0`); + B.line(`${raw} = load i8, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + B.line(`${fast} = icmp ne i8 ${raw}, 0`); } else { const raw = B.tmp(), nonnull = B.tmp(); @@ -373,14 +380,14 @@ export function emitDenseArraySet( B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); - B.line(`${bits} = zext i1 ${value} to i64`); - B.line(`store i64 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`${bits} = zext i1 ${value} to i8`); + B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } }; const statePtr = statePointer(host, guard); - const valuePtr = valuePointer(host, guard); + const valuePtr = valuePointer(host, guard, acc); let oldState: string | null = null; let old: string | null = null; if (acc === "ref") { @@ -444,7 +451,7 @@ export function emitDenseArraySet( // Below the bound the slot holds a present value: replace it, leaving // the state alone, and release a replaced reference after the store. B.startBlock(filledLabel); - const filledValuePtr = valuePointer(host, guard); + const filledValuePtr = valuePointer(host, guard, acc); let filledOld: string | null = null; if (acc === "ref") { filledOld = B.tmp(); @@ -504,13 +511,13 @@ export function emitDenseArrayPush( B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`); const statePtr = statePointer(host, { offset: len, states }); - const valuePtr = valuePointer(host, { offset: len, data }); + const valuePtr = valuePointer(host, { offset: len, data }, acc); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); - B.line(`${bits} = zext i1 ${value} to i64`); - B.line(`store i64 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`${bits} = zext i1 ${value} to i8`); + B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index ef3f67bf3..db56b273d 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -466,7 +466,7 @@ function emitDenseReferenceArrayRead( no: string, slow: string, join: string, - loadType: "ptr" | "i64" = "ptr", + loadType: "ptr" | "i64" | "i8" = "ptr", ): void { const B = host.B; const range = B.newLabel("local.array.range"), @@ -552,7 +552,11 @@ function emitDenseReferenceArrayRead( ); 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}`); + // Boolean arrays store one byte per element; every other kind an 8-byte slot. + const slotType = loadType === "i8" ? "i8" : "i64"; + B.line( + `${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`, + ); host.markMemoryPointer(valuePtr, "array:elements"); B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); present(raw); @@ -663,7 +667,7 @@ export function emitLocalArrayRead( if (read.element.kind === "bool") { const nonzero = B.tmp(); bits = B.tmp(); - B.line(`${nonzero} = icmp ne i64 ${value}, 0`); + B.line(`${nonzero} = icmp ne i8 ${value}, 0`); B.line(`${bits} = zext i1 ${nonzero} to i64`); } B.line(`store i64 ${bits}, ptr ${payload}`); @@ -684,7 +688,7 @@ export function emitLocalArrayRead( no, slow, join, - scalar ? "i64" : "ptr", + !scalar ? "ptr" : read.element.kind === "bool" ? "i8" : "i64", ); else B.br(slow); B.startBlock(slow); diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 131f1ae20..2d3550916 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 = 17 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v17" as const; +export const RUNTIME_ABI_VERSION = 18 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v18" 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 f35905e76..c8c76972a 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: 17, marker: "scr_runtime_abi_v17" }, + runtime_abi: { version: 18, marker: "scr_runtime_abi_v18" }, 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 f75d46c48..155f21d59 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -116,7 +116,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 17, marker: "scr_runtime_abi_v17" }, + runtime_abi: { version: 18, marker: "scr_runtime_abi_v18" }, 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 56fd2c8a4..ca1b86d36 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_v17"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v18"); 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_v17"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v18"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5080-boolean-array-storage.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5080-boolean-array-storage.ts.json new file mode 100644 index 000000000..d1c7860b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5080-boolean-array-storage.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5080-boolean-array-storage.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5081-boolean-array-dynamic-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5081-boolean-array-dynamic-views.ts.json new file mode 100644 index 000000000..ff120dad6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5081-boolean-array-dynamic-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5081-boolean-array-dynamic-views.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 43cc9438a..df08dcde0 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -306,7 +306,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 17, marker: "scr_runtime_abi_v17" }, + runtime_abi: { version: 18, marker: "scr_runtime_abi_v18" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 5f5364aff..518d7ced6 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -402,7 +402,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 17, marker: "scr_runtime_abi_v17" }, + runtime_abi: { version: 18, marker: "scr_runtime_abi_v18" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 6b26332e9..8cee62491 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -165,8 +165,19 @@ typedef struct { ScrArrSparseSlot *sparse; size_t sparse_len; size_t sparse_cap; + uint8_t elem; /* the owning array's element kind: the dense slot width */ } ScrArrStorage; +static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) { + if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index]; + return s->data[index]; +} + +/* The address of dense slot `index` (<= cap). */ +static uint8_t *scr_arr_dense_at(const ScrArr *a, size_t index) { + return (uint8_t *)a->data + index * scr_arr_slot_width(a); +} + static size_t scr_arr_sparse_lower_bound(const ScrArrSparseSlot *slots, size_t n, size_t index) { size_t lo = 0, hi = n; @@ -280,8 +291,10 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { x->sparse_len -= count; } -/* Bytes per dense slot: the 8-byte value and its state byte. */ +/* Bytes per dense slot: the value and its state byte. Boolean arrays + * store a one-byte value. */ #define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) +#define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) @@ -297,13 +310,15 @@ typedef enum { * literal keeps a 16-byte block rather than reserving 4 slots in a 48-byte * one. Growth keeps its own sequence (4, 8, 16, ...): rounding it as well * moves array storage into other size classes and measured slower. */ -static size_t scr_arr_block_cap(size_t cap) { +static size_t scr_arr_block_cap(size_t cap, bool bools) { #if SCR_SMALL_ALLOC - size_t bytes = cap * SCR_ARR_SLOT_BYTES; + size_t bytes = cap * (bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); if (bytes <= SCR_SA_MAX) { size_t block = (bytes + SCR_SA_STEP - 1) / SCR_SA_STEP * SCR_SA_STEP; - return block / SCR_ARR_SLOT_BYTES; + return bools ? block / SCR_ARR_BOOL_SLOT_BYTES : block / SCR_ARR_SLOT_BYTES; } +#else + (void)bools; #endif return cap; } @@ -317,9 +332,12 @@ static size_t scr_arr_block_cap(size_t cap) { static void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { if (need <= a->cap) return; if (need > SCR_ARR_MAX_LENGTH) scr_arr_oom(); + bool bools = a->elem == SCR_ELEM_BOOL; + size_t width = scr_arr_slot_width(a); + size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need); + cap = scr_arr_block_cap(need, bools); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) { @@ -332,13 +350,13 @@ static void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { * appends benefit from geometric headroom. */ if (mode == SCR_ARR_GROW_SPAN && need > SCR_ARR_DENSE_LIMIT) cap = need; } - if (cap > SIZE_MAX / SCR_ARR_SLOT_BYTES) scr_arr_oom(); + if (cap > SIZE_MAX / SCR_ARR_SLOT_BYTES) scr_arr_oom(); /* bounds both slot sizes */ if (!a->data && SCR_AX(a)->sparse_len == 0) { /* First storage: nothing to copy, move or absorb. */ - uint64_t *data = scr_mem_alloc(cap * SCR_ARR_SLOT_BYTES); + uint64_t *data = scr_mem_alloc(cap * unit); if (!data) scr_arr_oom(); a->data = data; - a->present = (uint8_t *)(data + cap); + a->present = (uint8_t *)data + cap * width; memset(a->present, SCR_ARR_HOLE, cap); a->cap = cap; return; @@ -346,17 +364,17 @@ static void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { /* Values and states share one allocation. Move the old states before * publishing the larger capacity: their previous location is now part * of the value area. Existing data and state pointers keep their ABI. */ - uint64_t *data = scr_mem_realloc(a->data, cap * SCR_ARR_SLOT_BYTES); + uint64_t *data = scr_mem_realloc(a->data, cap * unit); if (!data) scr_arr_oom(); - uint8_t *present = (uint8_t *)(data + cap); - if (a->cap) memmove(present, (uint8_t *)(data + a->cap), a->cap); + uint8_t *present = (uint8_t *)data + cap * width; + if (a->cap) memmove(present, (uint8_t *)data + a->cap * width, a->cap); memset(present + a->cap, SCR_ARR_HOLE, cap - a->cap); a->data = data; a->present = present; a->cap = cap; size_t moved = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, cap); for (size_t i = 0; i < moved; i++) { - a->data[SCR_AX(a)->sparse[i].index] = SCR_AX(a)->sparse[i].slot; + scr_arr_dense_put(a, SCR_AX(a)->sparse[i].index, SCR_AX(a)->sparse[i].slot); a->present[SCR_AX(a)->sparse[i].index] = SCR_AX(a)->sparse[i].state; } scr_arr_sparse_drop_prefix(a, moved); @@ -416,7 +434,7 @@ static uint8_t scr_arr_state_at_storage(const ScrArrStorage *s, size_t index, uint64_t *out) { if (index < s->cap) { if (s->present[index] == SCR_ARR_HOLE) return SCR_ARR_HOLE; - if (out) *out = s->data[index]; + if (out) *out = scr_arr_storage_get(s, index); return s->present[index]; } size_t pos = scr_arr_sparse_lower_bound(s->sparse, s->sparse_len, index); @@ -431,14 +449,14 @@ static bool scr_arr_slot_at_storage(const ScrArrStorage *s, size_t index, } static bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) { - ScrArrStorage s = { - a->data, a->present, a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap}; + ScrArrStorage s = {a->data, a->present, a->cap, SCR_AX(a)->sparse, + SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem}; return scr_arr_slot_at_storage(&s, index, out); } static uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t *out) { - ScrArrStorage s = { - a->data, a->present, a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap}; + ScrArrStorage s = {a->data, a->present, a->cap, SCR_AX(a)->sparse, + SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem}; return scr_arr_state_at_storage(&s, index, out); } @@ -449,7 +467,7 @@ static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot, if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); scr_arr_grow_for_index(a, index); if (index < a->cap) { - a->data[index] = slot; + scr_arr_dense_put(a, index, slot); a->present[index] = state; return; } @@ -472,9 +490,9 @@ static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out, uint8_t state = a->present[index]; if (state == SCR_ARR_HOLE) return false; scr_arr_cut(a, index); - *out = a->data[index]; + *out = scr_arr_dense_get(a, index); a->present[index] = SCR_ARR_HOLE; - a->data[index] = 0; + scr_arr_dense_put(a, index, 0); if (state_out) *state_out = state; return true; } @@ -498,8 +516,8 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); if (index < a->cap) { old_state = a->present[index]; - if (old_state == SCR_ARR_VALUE) old = a->data[index]; - a->data[index] = slot; + if (old_state == SCR_ARR_VALUE) old = scr_arr_dense_get(a, index); + scr_arr_dense_put(a, index, slot); a->present[index] = state; } else { size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); @@ -610,9 +628,8 @@ static bool scr_arr_prop_delete(ScrArr *a, double key) { } static ScrArrStorage scr_arr_take_storage(ScrArr *a) { - ScrArrStorage old = { - a->data, a->present, a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, - SCR_AX(a)->sparse_cap}; + ScrArrStorage old = {a->data, a->present, a->cap, SCR_AX(a)->sparse, + SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem}; a->data = NULL; a->present = NULL; a->cap = 0; @@ -641,7 +658,7 @@ static bool scr_arr_is_dense(const ScrArr *a) { static void scr_arr_move_dense(ScrArr *a, size_t to, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, to); - memmove(a->data + to, a->data + from, count * sizeof(*a->data)); + memmove(scr_arr_dense_at(a, to), scr_arr_dense_at(a, from), count * scr_arr_slot_width(a)); memmove(a->present + to, a->present + from, count); } @@ -649,24 +666,26 @@ static void scr_arr_clear_dense(ScrArr *a, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, from); memset(a->present + from, SCR_ARR_HOLE, count); - memset(a->data + from, 0, count * sizeof(*a->data)); + memset(scr_arr_dense_at(a, from), 0, count * scr_arr_slot_width(a)); } /* The destination owns no values in this range. Only retain adapters run * while copying references: no release can collect an intermediate copy. - * Self-appends use disjoint ranges after reserving capacity. */ + * Self-appends use disjoint ranges after reserving capacity. Both arrays + * hold the same element kind, so their slots have the same width. */ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, size_t from, size_t count, bool reverse, bool materialize_holes) { if (count == 0) return; scr_arr_cut(dst, to); if (!reverse) { - memcpy(dst->data + to, src->data + from, count * sizeof(*dst->data)); + memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), + count * scr_arr_slot_width(dst)); memcpy(dst->present + to, src->present + from, count); } else { for (size_t i = 0; i < count; i++) { size_t source = from + count - i - 1; - memcpy(dst->data + to + i, src->data + source, sizeof(*dst->data)); + scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, source)); dst->present[to + i] = src->present[source]; } } @@ -676,12 +695,21 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, if (dst->present[i] == SCR_ARR_VALUE) { if (refs) dst->data[i] = scr_elem_retain_slot(src, dst->data[i]); } else if (materialize_holes && dst->present[i] == SCR_ARR_HOLE) { - dst->data[i] = 0; + scr_arr_dense_put(dst, i, 0); dst->present[i] = SCR_ARR_UNDEFINED; } } } +/* Store `count` 8-byte value slots into dense storage starting at `to`. */ +static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t *slots) { + if (a->elem != SCR_ELEM_BOOL) { + memcpy(a->data + to, slots, count * sizeof(*slots)); + return; + } + for (size_t i = 0; i < count; i++) scr_arr_dense_put(a, to + i, slots[i]); +} + ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { ScrArr *a = scr_mem_alloc(sizeof(ScrArr)); if (!a) scr_arr_oom(); @@ -926,9 +954,10 @@ void scr_arr_set_len(ScrArr *a, double length) { } } } else { - for (size_t i = dense_stop; i < dense_end; i++) { - a->present[i] = 0; - a->data[i] = 0; + if (dense_stop < dense_end) { + memset(a->present + dense_stop, 0, dense_end - dense_stop); + memset(scr_arr_dense_at(a, dense_stop), 0, + (dense_end - dense_stop) * scr_arr_slot_width(a)); } if (a->ext) a->ext->sparse_len = scr_arr_sparse_lower_bound(a->ext->sparse, a->ext->sparse_len, next); } @@ -1053,7 +1082,8 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (scr_arr_dense_ok(a, until, until - from)) scr_arr_grow_dense(a, until); if (!refs && until <= a->cap) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); - for (size_t i = from; i < until; i++) a->data[i] = slot; + if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); + else for (size_t i = from; i < until; i++) a->data[i] = slot; memset(a->present + from, state, until - from); } else { for (size_t i = from; i < until; i++) { @@ -1241,8 +1271,8 @@ bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) { bool ap = ai < a->len && a->present[ai] == SCR_ARR_VALUE; bool bp = bi < b->len && b->present[bi] == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; - av = a->data[ai]; - bv = b->data[bi]; + av = scr_arr_dense_get(a, ai); + bv = scr_arr_dense_get(b, bi); } else { bool ap = scr_arr_read_value(a, i, &av); bool bp = scr_arr_read_value(b, j, &bv); @@ -1352,7 +1382,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { if (scr_arr_dense_ok(a, next, count)) { scr_arr_grow_dense(a, next); if (count) { - memcpy(a->data + from, slots, count * sizeof(*slots)); + scr_arr_put_slots(a, from, count, slots); memset(a->present + from, SCR_ARR_VALUE, count); } } else { @@ -1467,7 +1497,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { /* The prefix moves up behind the new first value. */ size_t f = a->filled; scr_arr_move_dense(a, 1, 0, old_len); - a->data[0] = slot; + scr_arr_dense_put(a, 0, slot); a->present[0] = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); @@ -1477,7 +1507,8 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { a->len = 0; scr_arr_store_owned(a, 0, slot); for (size_t i = 0; i < old.cap; i++) { - if (old.present[i]) scr_arr_store_state_owned(a, i + 1, old.data[i], old.present[i]); + if (old.present[i]) + scr_arr_store_state_owned(a, i + 1, scr_arr_storage_get(&old, i), old.present[i]); } for (size_t i = 0; i < old.sparse_len; i++) { scr_arr_store_state_owned(a, old.sparse[i].index + 1, old.sparse[i].slot, @@ -1509,7 +1540,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { scr_arr_grow_dense(a, old_len + count); size_t f = a->filled; scr_arr_move_dense(a, count, 0, old_len); - memcpy(a->data, slots, count * sizeof(*slots)); + scr_arr_put_slots(a, 0, count, slots); memset(a->present, SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); @@ -1519,7 +1550,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, i, slots[i]); for (size_t i = 0; i < old.cap; i++) { if (old.present[i] != SCR_ARR_HOLE) - scr_arr_store_state_owned(a, i + count, old.data[i], old.present[i]); + scr_arr_store_state_owned(a, i + count, scr_arr_storage_get(&old, i), old.present[i]); } for (size_t i = 0; i < old.sparse_len; i++) { scr_arr_store_state_owned(a, old.sparse[i].index + count, @@ -1562,7 +1593,7 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { } for (size_t i = 0; i < old.cap; i++) { if (old.present[i] != SCR_ARR_HOLE) { - scr_arr_store_state_owned(a, i + add, old.data[i], old.present[i]); + scr_arr_store_state_owned(a, i + add, scr_arr_storage_get(&old, i), old.present[i]); } } for (size_t i = 0; i < old.sparse_len; i++) { @@ -1582,10 +1613,9 @@ ScrArr *scr_arr_reverse(ScrArr *a) { if (a->filled != len) scr_arr_cut(a, 0); for (size_t i = 0; i < len / 2; i++) { size_t j = len - i - 1; - uint64_t slot; - memcpy(&slot, a->data + i, sizeof slot); - memcpy(a->data + i, a->data + j, sizeof slot); - memcpy(a->data + j, &slot, sizeof slot); + uint64_t slot = scr_arr_dense_get(a, i); + scr_arr_dense_put(a, i, scr_arr_dense_get(a, j)); + scr_arr_dense_put(a, j, slot); uint8_t state = a->present[i]; a->present[i] = a->present[j]; a->present[j] = state; @@ -1596,7 +1626,8 @@ ScrArr *scr_arr_reverse(ScrArr *a) { ScrArrStorage old = scr_arr_take_storage(a); a->len = len; for (size_t i = 0; i < old.cap; i++) { - if (old.present[i]) scr_arr_store_state_owned(a, len - 1 - i, old.data[i], old.present[i]); + if (old.present[i]) + scr_arr_store_state_owned(a, len - 1 - i, scr_arr_storage_get(&old, i), old.present[i]); } for (size_t i = 0; i < old.sparse_len; i++) { scr_arr_store_state_owned(a, len - 1 - old.sparse[i].index, @@ -1645,7 +1676,7 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { size_t old_len = a->len; if (scr_arr_is_dense(a)) { uint8_t state = a->present[0]; - if (slot_out && state == SCR_ARR_VALUE) *slot_out = a->data[0]; + if (slot_out && state == SCR_ARR_VALUE) *slot_out = scr_arr_dense_get(a, 0); /* The prefix slides down with the tail. */ size_t f = a->filled; scr_arr_move_dense(a, 0, 1, old_len - 1); @@ -1659,7 +1690,8 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { uint8_t state = scr_arr_state_at_storage(&old, 0, &s); a->len = 0; for (size_t i = 1; i < old.cap; i++) { - if (old.present[i]) scr_arr_store_state_owned(a, i - 1, old.data[i], old.present[i]); + if (old.present[i]) + scr_arr_store_state_owned(a, i - 1, scr_arr_storage_get(&old, i), old.present[i]); } for (size_t i = 0; i < old.sparse_len; i++) { if (old.sparse[i].index > 0) { @@ -1716,7 +1748,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { size_t old_len = a->len; if (scr_arr_is_dense(a)) { if (n) { - memcpy(out->data, a->data + from, n * sizeof(*a->data)); + memcpy(out->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); memcpy(out->present, a->present + from, n); } scr_arr_refill(out); @@ -1735,16 +1767,17 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { for (size_t i = 0; i < old.cap; i++) { if (old.present[i] == SCR_ARR_HOLE) continue; uint8_t state = old.present[i]; + uint64_t slot = scr_arr_storage_get(&old, i); size_t dst; if (i < from) { dst = i; - scr_arr_store_state_owned(a, dst, old.data[i], state); + scr_arr_store_state_owned(a, dst, slot, state); } else if (i >= from + n) { dst = i - n; - scr_arr_store_state_owned(a, dst, old.data[i], state); + scr_arr_store_state_owned(a, dst, slot, state); } else { dst = i - from; - scr_arr_store_state_owned(out, dst, old.data[i], state); + scr_arr_store_state_owned(out, dst, slot, state); } } for (size_t j = 0; j < old.sparse_len; j++) { @@ -1790,7 +1823,7 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, ? 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)); + memcpy(removed->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); memcpy(removed->present, a->present + from, n); } removed->len = n; @@ -1858,7 +1891,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou bool packed = a->filled == old_len; scr_arr_move_dense(a, from + count, from + n, old_len - from - n); if (count) { - memcpy(a->data + from, slots, count * sizeof(*slots)); + scr_arr_put_slots(a, from, count, slots); memset(a->present + from, SCR_ARR_VALUE, count); } if (next_len < old_len) scr_arr_clear_dense(a, next_len, old_len - next_len); @@ -1927,7 +1960,7 @@ double scr_arr_index_of_f64(ScrArr *a, double v) { double scr_arr_index_of_bool(ScrArr *a, bool v) { for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (a->present[i] == SCR_ARR_VALUE && ((a->data[i] != 0) == v)) return (double)i; + if (a->present[i] == SCR_ARR_VALUE && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && (SCR_AX(a)->sparse[i].slot != 0) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2042,7 +2075,7 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { if (i < from || a->present[i] == SCR_ARR_HOLE) continue; uint8_t state = a->present[i]; scr_arr_store_state_owned(out, i - from, - scr_arr_retain_state_slot(a, a->data[i], state), + scr_arr_retain_state_slot(a, scr_arr_dense_get(a, i), state), state); } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { @@ -2179,7 +2212,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (count && !values) scr_arr_oom(); size_t used = 0; for (size_t i = 0; i < dense; i++) - if (a->present[i] == SCR_ARR_VALUE) values[used++] = a->data[i]; + if (a->present[i] == SCR_ARR_VALUE) values[used++] = scr_arr_dense_get(a, i); for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) values[used++] = SCR_AX(a)->sparse[i].slot; ScrNumberSortKey *numbers = NULL; @@ -2203,12 +2236,13 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; if (copy) out->len = a->len; if (!copy && scr_arr_is_dense(a)) { - if (count) memcpy(a->data, values, count * sizeof(*values)); + if (count) scr_arr_put_slots(a, 0, count, values); scr_arr_cut(a, count); memset(a->present, SCR_ARR_VALUE, count); memset(a->present + count, SCR_ARR_UNDEFINED, undefined_count); scr_arr_clear_dense(a, count + undefined_count, a->len - count - undefined_count); - if (undefined_count) memset(a->data + count, 0, undefined_count * sizeof(*a->data)); + if (undefined_count) + memset(scr_arr_dense_at(a, count), 0, undefined_count * scr_arr_slot_width(a)); } else { ScrArrStorage old = {0}; if (!copy) old = scr_arr_take_storage(a); @@ -2457,7 +2491,7 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity) /* A permutation of the same owned slots: no reference count changes. */ for (size_t i = 0; sorted && i < n; i++) { if (i < a->cap) { - a->data[i] = src[i]; + scr_arr_dense_put(a, i, src[i]); } else { size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, i); SCR_AX(a)->sparse[pos].slot = src[i]; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 035c42318..ca834f754 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -126,7 +126,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v17(void) {} +void scr_runtime_abi_v18(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 6b2222b74..4c3916b3c 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -7625,7 +7625,7 @@ ScrArr *scr_set_to_arr_bool(const ScrMap *s) { 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; - out->data[out->len] = s->entries[i].key; + scr_arr_dense_put(out, out->len, s->entries[i].key != 0); out->present[out->len++] = SCR_ARR_VALUE; } return out; diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 2f8582c56..5b6e333e8 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1462,7 +1462,7 @@ void scr_set_add_all(ScrMap *set, ScrArr *values) { * Avoid taking a temporary owner only to release it after insertion. * Sparse/missing positions retain the existing accessor semantics. */ if (i < values->cap && values->present[i] == SCR_ARR_VALUE) { - uint64_t key = values->data[i]; + uint64_t key = scr_arr_dense_get(values, i); if (set->key_kind == SCR_MAP_KEY_F64) key = scr_map_f64_bits(scr_map_slot_to_f64(key)); scr_map_set(set, scr_map_hash_key(set, key), key, 0); } else if (values->elem == SCR_ELEM_STR) { diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 4456f8e8f..b01504305 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -74,7 +74,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v17(void); +void scr_runtime_abi_v18(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── @@ -1536,8 +1536,9 @@ ScrStr *scr_b64_missing_arg(void); /* ── arrays ───────────────────────────────────────────────────────── * Monomorphic, growable, refcounted. Elements live in 8-byte slots - * (doubles/bools/pointers via memcpy and casts); `elem` records what the - * slots hold so the array can release its own reference elements — ScrStr's + * (doubles/pointers via memcpy and casts), except SCR_ELEM_BOOL, whose + * dense storage holds one byte (0 or 1) per element; `elem` records what + * the slots hold so the array can release its own reference elements — ScrStr's * layout is untouched (interned-literal statics depend on it), so the * element kind lives here instead of in a shared object header. A separate * presence bitset distinguishes a hole from a present `undefined`-carrying @@ -1614,6 +1615,9 @@ typedef struct ScrArr { uint32_t filled; /* SCR_ELEM_REF only; NULL for every other element kind. */ const ScrArrOps *ops; + /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL + * (scr_arr_slot_width). Sparse entries and properties keep 8-byte slots + * for every kind; a bool slot is 0 or 1 wherever it lives. */ uint64_t *data; uint8_t *present; /* one state per dense slot; heap arrays share data allocation */ /* Rarely used storage (sparse indices, noncanonical numeric properties, @@ -1642,6 +1646,22 @@ _Static_assert(offsetof(ScrArr, filled) == 3 * sizeof(size_t) + 4, _Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 8 + sizeof(void *), "inline element reads use ScrArr.data as %ScrArr field 8"); +/* Bytes per dense value slot. */ +static inline size_t scr_arr_slot_width(const ScrArr *a) { + return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); +} + +/* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ +static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { + if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; + return a->data[index]; +} + +static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { + if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; + else a->data[index] = slot; +} + /* Lower the hole-free prefix to at most `index`: call before storing a * non-value state at `index`, or before moving states into [index, ...). */ static inline void scr_arr_cut(ScrArr *a, size_t index) { diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 14c50b5d8..0a05031b5 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -1365,6 +1365,107 @@ static void test_filled_prefix(void) { scr_arr_release(r); } +/* Boolean arrays store one byte per dense element, followed by the states in + * the same allocation. Growth moves the states past the larger value area, + * densifying absorbs sparse entries into bytes, and every moving and copying + * operation keeps values, holes and present undefined in place. */ +static bool bool_layout_ok(const ScrArr *a) { + return a->cap == 0 || a->present == (const uint8_t *)a->data + a->cap; +} + +static bool bool_states(const ScrArr *a, const char *want) { + if (scr_arr_len(a) != (double)strlen(want)) return false; + for (size_t i = 0; want[i]; i++) { + double state = scr_arr_state(a, (double)i); + char got = state == SCR_ARR_HOLE ? '_' + : state == SCR_ARR_UNDEFINED ? 'u' + : scr_arr_get_bool((ScrArr *)a, (double)i) ? 't' + : 'f'; + if (got != want[i]) return false; + } + return true; +} + +static void test_bool_bytes(void) { + ScrArr *a = scr_arr_new(SCR_ELEM_BOOL, 0); + for (int i = 0; i < 1000; i++) scr_arr_push_bool(a, i % 3 == 0); + check(bool_layout_ok(a) && a->filled == 1000, "bool pushes grow one-byte storage"); + bool values_ok = true; + for (int i = 0; i < 1000; i++) { + values_ok &= ((const uint8_t *)a->data)[i] == (i % 3 == 0); + values_ok &= scr_arr_get_bool(a, i) == (i % 3 == 0); + } + check(values_ok, "bool values are stored as 0/1 bytes"); + check(scr_arr_index_of_bool(a, false) == 1 && scr_arr_includes_bool(a, true), + "bool search reads bytes"); + scr_arr_set_len(a, 5); + scr_arr_set_len(a, 8); + scr_arr_set_undefined(a, 6); + check(bool_layout_ok(a) && bool_states(a, "tfftf_u_"), "bool truncation clears bytes"); + + ScrArr *s = scr_arr_new(SCR_ELEM_BOOL, 0); + scr_arr_set_bool(s, 0, true); + scr_arr_set_bool(s, 3000000, true); + scr_arr_set_bool(s, 2000000, false); + check(arr_sparse_len(s) == 2 && !scr_arr_get_bool(s, 2000000) && scr_arr_get_bool(s, 3000000), + "bool sparse entries keep their values"); + scr_arr_set_len(s, 2000001); + scr_arr_release(scr_arr_fill_bool(s, true, 1, 2000000)); + check(arr_sparse_len(s) == 0 && bool_layout_ok(s) && !scr_arr_get_bool(s, 2000000) && + scr_arr_get_bool(s, 1999999) && scr_arr_index_of_bool(s, false) == 2000000, + "densifying absorbs sparse bool entries into bytes"); + scr_arr_release(s); + + ScrArr *m = scr_arr_new(SCR_ELEM_BOOL, 0); + uint64_t slots[3] = {1, 0, 1}; + scr_arr_push_many(m, 3, slots); + scr_arr_unshift_many(m, 2, slots + 1); + check(bool_states(m, "fttft"), "slot buffers narrow to bytes"); + scr_arr_unshift_bool(m, false); + scr_arr_set_undefined(m, 2); + scr_arr_delete(m, 4); + check(bool_states(m, "ffut_t"), "unshift moves bytes and states together"); + ScrArr *removed = scr_arr_splice(m, 1, 2); + check(bool_states(removed, "fu") && bool_states(m, "ft_t"), "splice moves bytes out"); + scr_arr_release(removed); + ScrArr *items = scr_arr_new(SCR_ELEM_BOOL, 0); + scr_arr_push_bool(items, true); + scr_arr_push_bool(items, false); + scr_arr_release(scr_arr_splice_insert(m, 1, 1, items)); + check(bool_states(m, "ftf_t"), "splice insertion copies bytes in"); + scr_arr_splice_drop(m, 0, 1, 2, slots); + check(bool_states(m, "tftf_t"), "statement splice stores bytes"); + scr_arr_release(scr_arr_reverse(m)); + check(bool_states(m, "t_ftft"), "reverse swaps bytes"); + ScrArr *copy = scr_arr_slice(m, 1, INFINITY); + check(bool_states(copy, "_ftft") && bool_layout_ok(copy), "slice copies bytes"); + scr_arr_release(copy); + copy = scr_arr_to_reversed(m); + check(bool_states(copy, "tftfut"), "toReversed copies bytes and materializes holes"); + scr_arr_release(copy); + scr_arr_concat_copy(m, items); + scr_arr_push_spread(m, m); + check(bool_states(m, "t_ftfttftuftfttf"), "concat keeps holes; self spread reads them as undefined"); + scr_arr_release(scr_arr_copy_within(m, 0, 2, 5)); + check(scr_arr_index_eq(m, 0, items, 1) && scr_arr_index_eq(m, 1, items, 0), + "copyWithin moves bytes and index equality reads them"); + check(scr_arr_shift_bool(m) == false && scr_arr_pop_bool(m) == false, "shift and pop read bytes"); + scr_arr_release(scr_arr_sort_primitive(m, false)); + bool sorted = true; + for (double i = 1; i < scr_arr_len(m); i++) + if (scr_arr_state(m, i) == SCR_ARR_VALUE && scr_arr_state(m, i - 1) == SCR_ARR_VALUE) + sorted &= scr_arr_get_bool(m, i - 1) <= scr_arr_get_bool(m, i); + check(sorted, "default sort orders bytes"); + ScrStr *sep = scr_str_new(",", 1); + ScrStr *joined = scr_arr_join(items, sep); + check(strcmp(joined->data, "true,false") == 0, "join reads bytes"); + scr_str_release(joined); + scr_str_release(sep); + scr_arr_release(items); + scr_arr_release(m); + scr_arr_release(a); +} + int main(int argc, char **argv) { if (argc > 1) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); @@ -1410,6 +1511,7 @@ int main(int argc, char **argv) { test_bulk_reference_ownership(); test_splice_drop(); test_filled_prefix(); + test_bool_bytes(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed == 0 ? 0 : 1; diff --git a/tests/corpus/5080-boolean-array-storage.ts b/tests/corpus/5080-boolean-array-storage.ts new file mode 100644 index 000000000..99727ee78 --- /dev/null +++ b/tests/corpus/5080-boolean-array-storage.ts @@ -0,0 +1,183 @@ +// boolean[] keeps one byte per element in dense storage. Every array +// operation reads and writes those bytes: element access below and past the +// hole-free prefix, growth and truncation, the moving operations (push, pop, +// shift, unshift, splice, reverse, copyWithin), the copying ones (slice, +// concat, spread, toReversed, toSpliced, with), fill, sort, search, join, +// iteration and JSON. Holes and present undefined stay distinct throughout. +function show(label: string, xs: boolean[]): void { + const parts: string[] = []; + for (let i = 0; i < xs.length; i++) parts.push(i in xs ? String(xs[i]) : "_"); + let visits = 0; + let trues = 0; + xs.forEach((x) => { + visits++; + if (x) trues++; + }); + console.log(label, xs.length, parts.join(","), "visits", visits, "trues", trues); +} + +function showMaybe(label: string, xs: (boolean | undefined)[]): void { + const parts: string[] = []; + for (let i = 0; i < xs.length; i++) parts.push(i in xs ? String(xs[i]) : "_"); + let visits = 0; + xs.forEach(() => visits++); + console.log(label, xs.length, parts.join(","), "visits", visits); +} + +// A hole at `at` without `delete`: copying a hole deletes the target. +function punch(xs: boolean[], at: number): void { + const n = xs.length; + xs.length = n + 1; + xs.copyWithin(at, n, n + 1); + xs.length = n; +} + +// Element writes inside, at and past the length. +const a: boolean[] = new Array(6).fill(false); +show("fill", a); +a[1] = true; +a[4] = true; +show("write", a); +a[8] = true; +show("write-past", a); +console.log("in", 6 in a, 7 in a, 8 in a, a[7]); +a[6] = false; +a[7] = true; +show("close-gap", a); +a.length = 3; +show("truncate", a); +a.length = 5; +show("regrow", a); +a[3] = true; +a[4] = false; +show("rewrite", a); + +// Moving operations. +const b: boolean[] = [true, false, true, true, false, false, true]; +punch(b, 2); +show("punch", b); +console.log("push", b.push(true), b.push(false, true)); +show("pushed", b); +console.log("pop", b.pop(), b.pop(), b.length); +console.log("shift", b.shift(), b.shift(), b.length); +show("shifted", b); +console.log("unshift", b.unshift(false), b.unshift(true, true)); +show("unshifted", b); +console.log("splice", b.splice(1, 2).join(",")); +show("spliced", b); +console.log("splice-insert", b.splice(2, 1, true, false, false).join(",")); +show("inserted", b); +b.splice(0, 2, false); +show("splice-statement", b); +b.reverse(); +show("reverse", b); +b.copyWithin(0, 3, 6); +show("copyWithin", b); +const holey: boolean[] = [true, false, true, false]; +punch(holey, 1); +holey.reverse(); +show("reverse-holes", holey); +console.log("shift-hole", holey.shift(), holey.shift(), holey.length); +show("after-shift-hole", holey); + +// Copying operations preserve holes, except where iteration reads them. +const c: boolean[] = [false, true, false, true, true]; +punch(c, 3); +show("source", c); +show("slice", c.slice(1, 5)); +show("slice-neg", c.slice(-3)); +show("concat", c.concat([true], c)); +show("spread", [...c, false]); +show("toReversed", c.toReversed()); +show("toSpliced", c.toSpliced(1, 2, true)); +show("with", c.with(0, true)); +show("from", Array.from(c)); +const d: boolean[] = []; +d.push(...c); +show("push-spread", d); +d.unshift(...c); +show("unshift-spread", d); + +// fill, sort and search. +const e: boolean[] = new Array(10).fill(true, 0, 10); +e.fill(false, 2, 5); +e.fill(false, -2); +show("fill-range", e); +show("toSorted", e.toSorted()); +e.sort(); +show("sort", e); +const f: boolean[] = [true, false, false, true]; +f.sort((x, y) => Number(y) - Number(x)); +show("sort-cmp", f); +console.log("indexOf", e.indexOf(true), e.indexOf(false), e.lastIndexOf(false), [true].indexOf(false)); +console.log("includes", e.includes(true), [false, false].includes(true)); +console.log("find", e.find((x) => x), e.findIndex((x) => x), e.findLast((x) => !x), e.findLastIndex((x) => !x)); +console.log("some/every", e.some((x) => x), e.every((x) => x), [true, true].every((x) => x)); +console.log("count", e.filter((x) => x).length, e.map((x) => (x ? 1 : 0)).join("")); +console.log("reduce", e.reduce((n, x) => (x ? n + 1 : n), 0)); + +// Strings, iteration and JSON. +const g: boolean[] = [true, false, true]; +punch(g, 1); +console.log("join", g.join(), g.join(" | "), String(g), `${g}`); +const seen: string[] = []; +for (const x of g) seen.push(String(x)); +for (const [i, x] of g.entries()) seen.push(`${i}:${x}`); +for (const i of g.keys()) seen.push(`k${i}`); +console.log("iterate", seen.join(" ")); +console.log("json", JSON.stringify(g), JSON.stringify([true, false]), JSON.stringify({ flags: [false, true] })); +const parsed: boolean[] = JSON.parse("[true,false,false,true]") as boolean[]; +parsed.push(true); +console.log("parsed", parsed.length, parsed[3], parsed.join(",")); +console.log("inspect", [true, false], g); + +// Sets of booleans in both directions. +const fromArray = new Set([true, true, false, true]); +console.log("set", fromArray.size, fromArray.has(false)); +const back: boolean[] = [...fromArray]; +back.push(false); +show("set-spread", back); +show("set-from", Array.from(new Set([false, true]))); + +// Nested arrays and flat. +const grid: boolean[][] = [[true, false], [false], [true, true, false]]; +grid[1]![3] = true; +show("flat", grid.flat()); +console.log("grid", grid.map((row) => row.join("")).join("/")); + +// Present undefined stays distinct from a hole. +const m: (boolean | undefined)[] = [true, undefined, false]; +m[4] = true; +showMaybe("maybe", m); +m.splice(1, 0, undefined, false); +showMaybe("maybe-splice", m); +showMaybe("maybe-slice", m.slice(1)); +showMaybe("maybe-reverse", m.reverse()); +console.log("maybe-includes", m.includes(undefined), m.indexOf(undefined), m.indexOf(false)); +console.log("maybe-json", JSON.stringify(m)); +const n: (boolean | undefined)[] = new Array(4).fill(undefined); +n[2] = true; +showMaybe("maybe-fill", n); + +// Growth across allocation sizes, large sparse indices and densifying. +const big: boolean[] = []; +for (let i = 0; i < 70000; i++) big.push(i % 3 === 0); +let bigTrues = 0; +for (let i = 0; i < big.length; i++) if (big[i]) bigTrues++; +console.log("big", big.length, bigTrues, big[69999], big.lastIndexOf(true)); +const sparse: boolean[] = [true]; +sparse[3000000] = true; +sparse[2000000] = false; +console.log("sparse", sparse.length, 1 in sparse, sparse[2000000], sparse[3000000], sparse.indexOf(false)); +for (let i = 1; i < 200; i++) sparse[i] = i % 2 === 0; +console.log("sparse-dense", sparse.slice(0, 6).join(","), sparse.indexOf(false), sparse.includes(false)); +sparse.length = 4; +show("sparse-truncate", sparse); +const sieve: boolean[] = new Array(100001).fill(false); +let primes = 0; +for (let i = 2; i <= 100000; i++) { + if (sieve[i]) continue; + primes++; + for (let j = i * i; j <= 100000; j += i) sieve[j] = true; +} +console.log("sieve", primes, sieve.indexOf(false, 100), sieve.lastIndexOf(false)); diff --git a/tests/corpus/5081-boolean-array-dynamic-views.ts b/tests/corpus/5081-boolean-array-dynamic-views.ts new file mode 100644 index 000000000..8b73b805a --- /dev/null +++ b/tests/corpus/5081-boolean-array-dynamic-views.ts @@ -0,0 +1,23 @@ +// A boolean[] seen through unknown and any: reads, writes and growth through +// the dynamic view land in the same one-byte element storage, and JSON, +// structuredClone, deep equality and inspect read it back. +import { deepStrictEqual } from "node:assert"; +import { inspect, isDeepStrictEqual } from "node:util"; +const flags: boolean[] = [true, false, true]; +flags[5] = false; +const view: unknown = flags; +console.log(Array.isArray(view), JSON.stringify(view)); +const anyView = view as any; +console.log(anyView[0], anyView[1], anyView[3], anyView.length); +anyView[1] = true; +anyView[3] = true; +console.log(flags[1], flags[3], 4 in flags, flags.length); +const cloned = structuredClone(flags); +console.log(cloned.length, cloned.join(","), isDeepStrictEqual(cloned, flags)); +deepStrictEqual([true, false], [true, false]); +console.log(inspect(flags), inspect([false])); +const back = view as boolean[]; +back.push(true); +console.log(flags.length, flags[6]); +const rec: Record = { flags }; +console.log(JSON.stringify(rec)); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 2906ac1e2..17665e888 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -96,7 +96,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 17, marker: "scr_runtime_abi_v17" }, + runtime_abi: { version: 18, marker: "scr_runtime_abi_v18" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: {