Store boolean arrays as one byte per element (#947)

This commit is contained in:
Chris Tate
2026-10-13 19:13:20 -05:00
committed by GitHub
23 changed files with 503 additions and 98 deletions
+1 -1
View File
@@ -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.
+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_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.
+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: 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");
});
});
@@ -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.
@@ -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<DenseGuard, "offset" | "data">,
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)}`);
}
@@ -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);
+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 = 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;
@@ -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: {
@@ -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",
@@ -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 () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5080-boolean-array-storage.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5081-boolean-array-dynamic-views.ts"
],
"diags": []
}
@@ -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,
@@ -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,
+96 -62
View File
@@ -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];
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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) {
+23 -3
View File
@@ -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) {
+102
View File
@@ -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;
+183
View File
@@ -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<boolean>(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<boolean>(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<boolean>([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<boolean>([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<boolean | undefined>(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<boolean>(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));
@@ -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<string, unknown> = { flags };
console.log(JSON.stringify(rec));
@@ -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: {