Merge main into the docs rewrite

This commit is contained in:
Chris Tate
2026-10-13 07:46:52 -05:00
33 changed files with 1832 additions and 154 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_v14` checks runtime compatibility at link time.
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v15` 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_v14`, 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_v15`, 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: 14, marker: "scr_runtime_abi_v14" },
runtime_abi: { version: 15, marker: "scr_runtime_abi_v15" },
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_v14");
expect(error?.stderr).toContain("scr_runtime_abi_v15");
});
});
+7 -5
View File
@@ -10,17 +10,19 @@ import type {
import type { LlvmEmitterContext } from "./expr-context.js";
/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for
* field (its static assertions pin `cyc` and `data`). */
* field (its static assertions pin `cyc`, `filled` and `data`). */
export const SCR_ARR_FIELD = {
rc: 0,
len: 1,
cap: 2,
elem: 3,
cyc: 4,
ops: 5,
data: 6,
present: 7,
ext: 8,
spare: 5,
filled: 6,
ops: 7,
data: 8,
present: 9,
ext: 10,
} as const;
/** Lines that decide whether the double `index` is an integer in
@@ -1,5 +1,6 @@
import { isBorrowSafeMath } from "../../ir/analysis.js";
import type { IrExpr, IrFunction, IrLibFn, IrModule, IrStmt } from "../../ir/ir.js";
import { exprFunctionReference, moduleFunctionReferences } from "../../ir/function-references.js";
import { everyStmtList } from "../../ir/traverse.js";
import type { CallLifetimes } from "./call-lifetimes.js";
@@ -38,31 +39,14 @@ export function findConstantCallbacks(
);
const globals = new Map<string, IrExpr>();
const writes = new Set<string>();
const escaped = new Set<string>([
mod.entry,
...(mod.lib?.exports.map((entry) => entry.fnName) ?? []),
]);
// A function named anywhere but as a direct call's callee can be entered
// by a generated adapter or the runtime, which a direct-call census cannot
// specialize: the entry, exports, class helpers and methods here, and
// closures and retag helpers in the bodies (collected by the walk below).
const escaped = new Set<string>(moduleFunctionReferences(mod));
for (const cls of mod.classes ?? [])
for (const method of ["constructor", ...(cls.methods ?? [])])
escaped.add(`%${cls.name}.${method}`);
// Metadata and non-call expression references can expose an entry to a
// generated adapter or runtime callback. A direct-call census cannot
// specialize those entries. Conservatively reject every such reference,
// including future metadata that names a function.
function externalReferences(value: unknown): void {
if (typeof value === "string") {
if (functions.has(value)) escaped.add(value);
} else if (Array.isArray(value)) value.forEach(externalReferences);
else if (value && typeof value === "object") {
const object = value as Record<string, unknown>;
for (const key in object) {
if (object["kind"] === "call" && key === "callee") continue;
externalReferences(object[key]);
}
}
}
const { functions: _functions, ...metadata } = mod;
externalReferences(metadata);
const incoming = new Map<string, { owner: string; args: IrExpr[] }[]>();
// Where each lifted function's environment is created, for resolving its
// immutable captures in the creating scope.
@@ -100,8 +84,9 @@ export function findConstantCallbacks(
}
if (expr.kind === "callValue" && expr.callee.kind === "varRef")
callees.add(expr.callee.localId);
const referenced = exprFunctionReference(expr);
if (referenced !== undefined) escaped.add(referenced);
if (expr.kind === "closure") {
escaped.add(expr.fnName);
const sites = closureSites.get(expr.fnName) ?? [];
sites.push({ owner: fn.name, captures: expr.captures });
closureSites.set(expr.fnName, sites);
@@ -111,7 +96,6 @@ export function findConstantCallbacks(
return true;
},
});
externalReferences(fn.body);
definitions.set(fn.name, defs);
constDefinitions.set(fn.name, consts);
indirectCallees.set(fn.name, callees);
@@ -152,6 +152,56 @@ test.each([32, 64] as const)(
},
);
test.each([32, 64] as const)(
"indices below the hole-free prefix bound skip the state bytes (%i-bit)",
(bits) => {
const read: IrExpr = { kind: "arrayGet", arr: ref("a", nums), index, type: F64, loc };
const llvm = emitLlvmModule(
module("bound", VOID, [
{
kind: "arraySet",
arr: ref("a", nums),
index,
value: { kind: "bin", op: "+", left: read, right: read, type: F64, loc },
loc,
},
{
kind: "arraySet",
arr: ref("s", words),
index,
value: { kind: "strLit", value: "x", type: STRING, loc },
loc,
},
]),
{ pointerBits: bits },
);
const fn = body(llvm, "bound");
const size = bits === 32 ? "i32" : "i64";
// The bound is %ScrArr field 6, an i32 compared against the index.
const bound = fn.match(/getelementptr inbounds %ScrArr, ptr .*, i32 0, i32 6$/gm) ?? [];
expect(bound.length).toBeGreaterThanOrEqual(4);
expect(fn).toMatch(/load i32, ptr /);
// The prefix blocks load or store the slot without a state access.
const blocks = (label: string): string[] =>
[...fn.matchAll(new RegExp(`^${label}\\d+:\\n((?: .*\\n)*)`, "gm"))].map((m) => m[1]!);
const reads = blocks("arr\\.get\\.value");
expect(reads).toHaveLength(2);
for (const block of reads) {
expect(block).toMatch(/load double, ptr /);
expect(block).not.toMatch(/i8/);
}
const writes = blocks("arr\\.set\\.filled");
expect(writes).toHaveLength(2);
for (const block of writes) expect(block).not.toMatch(/i8/);
expect(writes[0]).toMatch(/store double .*, ptr /);
// The replaced string below the bound is released after the store.
expect(writes[1]).toMatch(/store ptr .*\n[^]*call void @scr_str_release\(/);
// A state-checked store at the bound extends it.
expect(fn).toMatch(new RegExp(`icmp eq ${size} .*`));
expect(fn).toMatch(/store i32 .*, ptr /);
},
);
test("state and presence queries answer dense slots inline", () => {
const state: IrExpr = { kind: "arrayState", arr: ref("b", flags), index, type: F64, loc };
const has: IrExpr = { kind: "arrayHas", arr: ref("s", words), index, type: BOOL, loc };
@@ -8,10 +8,16 @@
* `undefined`, sparse indices, noncanonical numeric properties (negative,
* fractional, NaN, infinite, 2^32-1), and out-of-range reads branch to the
* unchanged runtime call, which is marked cold. The ScrArr field indices
* (SCR_ARR_FIELD) match the C layout in scr_runtime.h. */
* (SCR_ARR_FIELD) match the C layout in scr_runtime.h.
*
* Indices below the array's hole-free prefix bound (`filled`) skip the
* state bytes altogether: the runtime keeps every state below it a present
* value, so reads load the slot directly and writes replace it without
* touching its state. Other dense indices take the state-checked path,
* whose writes extend the bound when they continue it. */
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
import { SCR_ARR_FIELD } from "./common.js";
import { f64Lit, SCR_ARR_FIELD } from "./common.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { exactInteger, widenInteger } from "./integer-values.js";
@@ -28,6 +34,8 @@ interface DenseGuard {
offset: string;
/** The array length loaded by the guard, in the target's size type. */
len: string;
/** The hole-free prefix bound loaded by the guard (i32). */
filled: string;
/** Dense value and state storage base pointers. */
data: string;
states: string;
@@ -36,7 +44,10 @@ interface DenseGuard {
/** Branch to `slow` unless `index` names a canonical array index inside
* dense storage. Reads also require it below the length; writes may land
* at or past the length while still inside the dense capacity (the runtime
* then extends the length over hole states exactly the same way). */
* then extends the length over hole states exactly the same way). An index
* below the hole-free prefix bound branches to `filledLabel` first; both
* that block and the returned (state-checked) dense block may use the
* guard's values. */
function emitDenseGuard(
host: LlvmEmitterContext,
array: string,
@@ -44,6 +55,7 @@ function emitDenseGuard(
indexExpr: IrExpr | undefined,
slow: string,
requireBelowLen: boolean,
filledLabel: string,
): DenseGuard {
const B = host.B;
const size = host.sizeType;
@@ -72,17 +84,8 @@ function emitDenseGuard(
const lenPtr = B.tmp(),
len = B.tmp(),
capPtr = B.tmp(),
cap = B.tmp();
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`);
host.markMemoryPointer(lenPtr, "array:header");
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 2`);
host.markMemoryPointer(capPtr, "array:header");
B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
// Load the storage bases before branching: the header of a live array is
// always readable, and unconditional loads let LLVM hoist them out of
// loops whose only calls are read-only fallbacks.
const dataPtr = B.tmp(),
cap = B.tmp(),
dataPtr = B.tmp(),
data = B.tmp(),
statesPtr = B.tmp(),
states = B.tmp();
@@ -91,17 +94,58 @@ function emitDenseGuard(
);
host.markMemoryPointer(dataPtr, "array:header");
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
const loadChecked = (): void => {
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`);
host.markMemoryPointer(lenPtr, "array:header");
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 2`);
host.markMemoryPointer(capPtr, "array:header");
B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
B.line(
`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.present}`,
);
host.markMemoryPointer(statesPtr, "array:header");
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
};
// Reads load the storage bases before branching: the header of a live
// array is always readable, and unconditional loads let LLVM hoist them
// out of loops whose only calls are read-only fallbacks. Writes load the
// fields that only the state-checked path needs inside it, which keeps
// them off a hot loop of prefix stores (that path stores to the header).
const eagerHeader = requireBelowLen;
if (eagerHeader) loadChecked();
const filledPtr = B.tmp(),
filled = B.tmp(),
filledWide = B.tmp();
B.line(
`${statesPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.present}`,
`${filledPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.filled}`,
);
host.markMemoryPointer(statesPtr, "array:header");
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
host.markMemoryPointer(filledPtr, "array:header");
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${filledWide} = zext i32 ${filled} to i64`);
const widen = (value: string): string => {
if (size === "i64") return value;
const t = B.tmp();
B.line(`${t} = zext i32 ${value} to i64`);
return t;
};
let offset = wide;
if (size !== "i64") {
offset = B.tmp();
B.line(`${offset} = trunc i64 ${wide} to i32`);
}
// The bound never exceeds the length or the dense capacity.
let inFilled = B.tmp();
B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
if (integral) {
const t = B.tmp();
B.line(`${t} = and i1 ${integral}, ${inFilled}`);
inFilled = t;
}
const check = B.newLabel("arr.check");
B.condBr(inFilled, filledLabel, check);
B.startBlock(check);
if (!eagerHeader) loadChecked();
const conditions: string[] = [];
if (integral) conditions.push(integral);
const belowCap = B.tmp();
@@ -121,15 +165,21 @@ function emitDenseGuard(
const dense = B.newLabel("arr.dense");
B.condBr(ok, dense, slow);
B.startBlock(dense);
let offset = wide;
if (size !== "i64") {
offset = B.tmp();
B.line(`${offset} = trunc i64 ${wide} to i32`);
}
return { offset, len, data, states };
return { offset, len, filled, data, states };
}
function statePointer(host: LlvmEmitterContext, guard: DenseGuard): string {
/** The i32 prefix bound in the target's size type. */
function sizedFilled(host: LlvmEmitterContext, filled: string): string {
if (host.sizeType !== "i64") return filled;
const t = host.B.tmp();
host.B.line(`${t} = zext i32 ${filled} to i64`);
return t;
}
function statePointer(
host: LlvmEmitterContext,
guard: Pick<DenseGuard, "offset" | "states">,
): string {
const B = host.B;
const statePtr = B.tmp();
B.line(
@@ -139,7 +189,10 @@ function statePointer(host: LlvmEmitterContext, guard: DenseGuard): string {
return statePtr;
}
function valuePointer(host: LlvmEmitterContext, guard: DenseGuard): string {
function valuePointer(
host: LlvmEmitterContext,
guard: Pick<DenseGuard, "offset" | "data">,
): string {
const B = host.B;
const valuePtr = B.tmp();
B.line(
@@ -183,12 +236,12 @@ export function emitDenseArrayGet(
return t;
}
const slow = B.newLabel("arr.get.slow"),
join = B.newLabel("arr.get.join");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true);
join = B.newLabel("arr.get.join"),
valueLabel = B.newLabel("arr.get.value");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel);
const state = loadState(host, guard);
const present = B.tmp();
B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
const valueLabel = B.newLabel("arr.get.value");
B.condBr(present, valueLabel, slow);
B.startBlock(valueLabel);
const ptr = valuePointer(host, guard);
@@ -259,8 +312,9 @@ export function emitDenseArrayState(
const ty = kind === "state" ? "double" : "i1";
host.declare(`declare ${kind === "state" ? "double" : "zeroext i1"} @${fn}(ptr, double)`);
const slow = B.newLabel("arr.state.slow"),
join = B.newLabel("arr.state.join");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true);
join = B.newLabel("arr.state.join"),
filledLabel = B.newLabel("arr.state.filled");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, filledLabel);
const state = loadState(host, guard);
const fast = B.tmp();
if (kind === "state") B.line(`${fast} = uitofp i8 ${state} to double`);
@@ -269,13 +323,19 @@ export function emitDenseArrayState(
B.br(fastLabel);
B.startBlock(fastLabel);
B.br(join);
// Below the bound every state is a present value.
B.startBlock(filledLabel);
B.br(join);
B.startBlock(slow);
const slowValue = B.tmp();
B.line(`${slowValue} = call ${ty} @${fn}(ptr ${array}, double ${index.name})${READ_FALLBACK}`);
B.br(join);
B.startBlock(join);
const result = B.tmp();
B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`);
const filledValue = kind === "state" ? f64Lit(SCR_ARR_VALUE) : "true";
B.line(
`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${filledValue}, %${filledLabel} ], [ ${slowValue}, %${slow} ]`,
);
return result;
}
@@ -304,9 +364,21 @@ export function emitDenseArraySet(
return;
}
const slow = B.newLabel("arr.set.slow"),
join = B.newLabel("arr.set.join");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false);
join = B.newLabel("arr.set.join"),
filledLabel = B.newLabel("arr.set.filled");
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel);
const { offset, len } = guard;
const storeValue = (valuePtr: string): void => {
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)}`);
} else {
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
};
const statePtr = statePointer(host, guard);
const valuePtr = valuePointer(host, guard);
let oldState: string | null = null;
@@ -317,16 +389,26 @@ export function emitDenseArraySet(
B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
B.line(`${old} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
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)}`);
} else {
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
storeValue(valuePtr);
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
// A store at the bound continues the hole-free prefix.
const filledWide = sizedFilled(host, guard.filled),
continues = B.tmp();
B.line(`${continues} = icmp eq ${host.sizeType} ${offset}, ${filledWide}`);
const bumpLabel = B.newLabel("arr.set.bump"),
afterBump = B.newLabel("arr.set.state");
B.condBr(continues, bumpLabel, afterBump);
B.startBlock(bumpLabel);
const nextFilled = B.tmp(),
filledPtr = B.tmp();
B.line(`${nextFilled} = add i32 ${guard.filled}, 1`);
B.line(
`${filledPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.filled}`,
);
host.markMemoryPointer(filledPtr, "array:header");
B.line(`store i32 ${nextFilled}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.br(afterBump);
B.startBlock(afterBump);
// Writes at or past the length extend it over the (hole) gap, exactly
// like the runtime's dense store.
const grows = B.tmp();
@@ -352,6 +434,20 @@ export function emitDenseArraySet(
host.releaseValue(old, element);
}
B.br(join);
// 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);
let filledOld: string | null = null;
if (acc === "ref") {
filledOld = B.tmp();
B.line(
`${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`,
);
}
storeValue(filledValuePtr);
if (filledOld) host.releaseValue(filledOld, element);
B.br(join);
B.startBlock(slow);
call(" cold");
B.br(join);
@@ -400,8 +496,8 @@ export function emitDenseArrayPush(
states = B.tmp();
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, len, data, states });
const valuePtr = valuePointer(host, { offset: len, len, data, states });
const statePtr = statePointer(host, { offset: len, states });
const valuePtr = valuePointer(host, { offset: len, data });
if (acc === "f64") {
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "bool") {
@@ -416,6 +512,18 @@ export function emitDenseArrayPush(
fastValue = B.tmp();
B.line(`${next} = add nuw ${size} ${len}, 1`);
B.line(`store ${size} ${next}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
// An append to a hole-free array keeps it hole-free.
const filledPtr = header(SCR_ARR_FIELD.filled),
filled = B.tmp(),
packed = B.tmp(),
nextFilled = B.tmp(),
bumped = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
const filledWide = sizedFilled(host, filled);
B.line(`${packed} = icmp eq ${size} ${filledWide}, ${len}`);
B.line(`${nextFilled} = add i32 ${filled}, 1`);
B.line(`${bumped} = select i1 ${packed}, i32 ${nextFilled}, i32 ${filled}`);
B.line(`store i32 ${bumped}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${fastValue} = sitofp ${size} ${next} to double`);
B.br(join);
B.startBlock(slow);
@@ -1204,6 +1204,7 @@ export class LlDyn {
{ index: 2, type: host.sizeType, name: "capacity" },
{ index: SCR_ARR_FIELD.data, type: "ptr" as const, name: "data" },
{ index: SCR_ARR_FIELD.present, type: "ptr" as const, name: "presence" },
{ index: SCR_ARR_FIELD.filled, type: "i32" as const, name: "filled" },
];
members.forEach((member) => {
const cachedPtr = B.tmp();
+80 -8
View File
@@ -81,6 +81,8 @@ import { analyzeInt32Slots, type Int32Slots } from "../../ir/int32-slots.js";
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
import { foldConstantGlobals } from "../../ir/constant-globals.js";
import { findIntegerViews } from "./integer-views.js";
import { findInt32Locals, type Int32Local } from "./int32-locals.js";
import { exactInteger, narrowInteger } from "./integer-values.js";
import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
import {
@@ -768,6 +770,11 @@ export class LlEmitter {
private byteWindowEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
bytesBounds: ReadonlySet<IrExpr | IrStmt> = new Set();
integerViews = new Map<string, string>();
/** Number locals of the current function stored only as an i32
* (int32-locals.ts); their slots are `alloca i32`. */
int32Locals = new Map<string, Int32Local>();
/** The written values (and `++`/`--` nodes) the int32 proof covers. */
int32LocalWrites: ReadonlySet<IrExpr> = new Set();
/** Enclosing try-with-FINALLY regions, innermost last: a `return`
* inside one runs every crossed finally (innermost first) before the
* actual ret — the runtime ABI’s pending-return path, with the finally
@@ -1837,13 +1844,13 @@ export class LlEmitter {
`%ScrFfiTable = type { ptr, ${this.sizeType}, ${this.sizeType}, ptr, i8, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
`%ScrRegex = type { ${this.sizeType}, ptr, ptr, ptr, double, ptr, ptr }`,
// ScrArr mirrors scr_runtime.h field-for-field: { rc, len, cap, elem,
// cyc, ops, data, present, ext } (field numbers in SCR_ARR_FIELD),
// where ops points at the element type's constant RC descriptor
// cyc, spare, filled, ops, data, present, ext } (field numbers in
// SCR_ARR_FIELD), where filled bounds the hole-free prefix, ops points at the element type's constant RC descriptor
// (%ScrArrOps, SCR_ELEM_REF only) and ext at the out-of-line sparse,
// property and metadata storage (NULL until first used). Live dynamic
// stream commits swap its dense, presence and out-of-line storage while
// preserving the target object's identity.
`%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr }`,
`%ScrArr = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, i8, i8, i16, i32, ptr, ptr, ptr, ptr }`,
`%ScrArrOps = type { ptr, ptr, ptr }`,
// The ScrMap prefix { rc, key_kind, val_kind, val_retain, val_release,
// val_trace, key_retain, key_release, key_trace } — the inline RC
@@ -2027,16 +2034,16 @@ export class LlEmitter {
for (const [, inst] of this.templateStringsInstances) {
// One immortal ScrArr per tagged-template site: a [N x ptr] data
// global of interned cooked-string literals, and the ScrArr header
// over it (rc == SIZE_MAX, len == cap, SCR_ELEM_STR = 2, no REF
// entry points). Every dense slot is present; reads retain immortal
// strings — a no-op.
// over it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2,
// no REF entry points). Every dense slot is present; reads retain
// immortal strings — a no-op.
const n = inst.slots.length;
const present =
n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
out.push(
`@${inst.sym}_data = internal constant [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ]`,
`@${inst.sym}_present = internal constant [${n} x i8] ${present}`,
`@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i32 2, i32 0, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`,
`@${inst.sym} = internal global %ScrArr { ${this.sizeType} -1, ${this.sizeType} ${n}, ${this.sizeType} ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr @${inst.sym}_present, ptr null }`,
);
}
if (this.templateStringsInstances.size > 0) out.push(``);
@@ -5096,6 +5103,20 @@ export class LlEmitter {
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
this.integerRanges = analyzeIntegerRanges(numericFn, this.int32Slots.facts(fn.name));
this.bytesBounds = findBytesBounds(numericFn, this.integerRanges);
// Debug builds keep every number local a double for the debugger's
// variable view. Lazily boxed captured parameters keep their double,
// which the box is created from.
const int32Locals =
this.debug === null
? findInt32Locals(
fn,
this.integerRanges,
this.int32Slots.facts(fn.name),
new Set(this.lazyCaptureBoxes.keys()),
)
: null;
this.int32Locals = int32Locals?.locals ?? new Map();
this.int32LocalWrites = int32Locals?.values ?? new Set();
this.chainSlots.clear();
this.finallyStack = [];
this.tryStack = [];
@@ -5144,6 +5165,8 @@ export class LlEmitter {
const paramIds = new Set(fn.params.map((p) => p.localId));
if (this.debug === null) {
for (const id of findIntegerViews(numericFn)) {
// An i32 slot is its own view.
if (this.int32Locals.has(id)) continue;
const slot = B.slot();
B.entryAllocas.push(
`${slot} = alloca i32 ; integer view ${this.numericLocals.get(id)!.name}`,
@@ -5165,7 +5188,9 @@ export class LlEmitter {
const slotTy =
local.boxed || this.captureIds.has(local.id) || local.type.kind === "caught"
? "ptr"
: this.llType(local.type);
: this.int32Locals.has(local.id)
? "i32"
: this.llType(local.type);
B.entryAllocas.push(`%${mangleLocal(local.id)} = alloca ${slotTy} ; ${local.name}`);
this.emitDebugLocal(
local,
@@ -5244,6 +5269,8 @@ export class LlEmitter {
fnScope.push({ slot, type: p.type, boxed: true });
continue;
}
const incoming = { name: `%p_${mangleLocal(p.localId)}`, type: p.type };
if (this.storeInt32Local(p.localId, incoming)) continue;
B.line(`store ${this.llType(p.type)} %p_${mangleLocal(p.localId)}, ptr ${slot}`);
this.storeIntegerView(p.localId, { name: `%p_${mangleLocal(p.localId)}`, type: p.type });
if (isRefCounted(p.type) && !borrowed?.has(index)) fnScope.push({ slot, type: p.type });
@@ -5712,6 +5739,10 @@ export class LlEmitter {
this.frames.push([]);
switch (s.kind) {
case "varDecl": {
if (s.init && this.int32Locals.has(s.localId)) {
this.storeInt32Local(s.localId, this.emitExpr(s.init), s.init);
break;
}
const b = this.binding(s.localId);
if (this.walkBorrows.has(s.localId) && b.kind === "local") {
// A borrowed walk pointer: no retain now, no release at scope exit.
@@ -5862,6 +5893,10 @@ export class LlEmitter {
break;
}
case "assign": {
if (this.int32Locals.has(s.localId)) {
this.storeInt32Local(s.localId, this.emitExpr(s.value), s.value);
break;
}
const owner = this.reboundOwnerSlots.get(s.localId);
if (owner !== undefined && !this.walkBorrows.has(s.localId)) {
// A rebound borrowed parameter: the new value moves into the owner
@@ -8317,6 +8352,43 @@ export class LlEmitter {
return emitToUint32(this, value, expr, uint32);
}
/** Stores a number into its int32-stored local, when it is one. The
* value is proven to be in the local's range, so its low 32 bits are the
* exact value. */
storeInt32Local(localId: string, value: LlValue, expr?: IrExpr): boolean {
const local = this.int32Locals.get(localId);
if (local === undefined) return false;
if (expr !== undefined && !this.int32LocalWrites.has(expr))
throw new InternalCompilerError(`llvm emitter bug: unproven write of int32 local ${localId}`);
const B = this.B;
const integer = exactInteger(this, value, expr);
let bits: string;
if (integer && /^-?\d+$/.test(integer.name)) bits = String(Number(integer.name) | 0); // a literal's i32 bits
else if (integer) bits = narrowInteger(this, integer);
else if (value.uint32 !== undefined) bits = value.uint32;
else {
// An Int32 contract proved this write without a range at the value.
bits = B.tmp();
B.line(`${bits} = ${local.signed ? "fptosi" : "fptoui"} double ${value.name} to i32`);
}
B.line(`store i32 ${bits}, ptr %${mangleLocal(localId)}`);
return true;
}
/** The number of an int32-stored local's i32, with the i32 as its
* integer and ToUint32 forms. */
int32LocalValue(localId: string, bits: string, type: IrType): LlValue {
const local = this.int32Locals.get(localId)!;
const number = this.B.tmp();
this.B.line(`${number} = ${local.signed ? "sitofp" : "uitofp"} i32 ${bits} to double`);
return {
name: number,
type,
uint32: bits,
integer: { name: bits, type: "i32", signed: local.signed, range: local.range },
};
}
storeIntegerView(localId: string, value: LlValue, expr?: IrExpr): void {
const slot = this.integerViews.get(localId);
if (slot === undefined) return;
@@ -81,6 +81,11 @@ export function emitLiteralExpr(
},
};
}
if (host.int32Locals.has(e.localId)) {
const bits = B.tmp();
B.line(`${bits} = load i32, ptr ${host.binding(e.localId).slot}`);
return host.int32LocalValue(e.localId, bits, e.type);
}
const b = host.binding(e.localId);
if (b.kind === "global") host.checkGlobalTdz(e.localId);
if (b.kind === "boxed") {
@@ -368,6 +373,17 @@ export function emitOperatorExpr(
// globals, capture boxes): read, write ±1, yield old (postfix) or
// new (prefix).
const b = host.binding(e.localId);
if (host.int32Locals.has(e.localId)) {
if (!host.int32LocalWrites.has(e))
throw new InternalCompilerError(`llvm emitter bug: unproven ++/-- of ${e.localId}`);
// The written value is proven in the local's range: no wrap.
const old = B.tmp();
const next = B.tmp();
B.line(`${old} = load i32, ptr ${b.slot}`);
B.line(`${next} = ${e.op === "+" ? "add" : "sub"} i32 ${old}, 1`);
B.line(`store i32 ${next}, ptr ${b.slot}`);
return host.int32LocalValue(e.localId, e.prefix ? next : old, e.type);
}
if (b.kind === "boxed") {
const box = host.loadBox(b.slot);
const old = b.local!.tdz
@@ -434,6 +450,7 @@ export function emitOperatorExpr(
}
const b = host.binding(e.localId);
const v = host.emitExpr(e.value);
if (host.storeInt32Local(e.localId, v, e.value)) return v;
if (b.kind === "global") host.checkGlobalTdz(e.localId);
if (b.kind === "boxed") {
// box_set takes ownership of the passed reference, so hand it a
@@ -178,6 +178,7 @@ export function streamTypedRefCommitAdapter(
{ name: "cap", index: 2, type: host.sizeType },
{ name: "data", index: SCR_ARR_FIELD.data, type: "ptr" },
{ name: "present", index: SCR_ARR_FIELD.present, type: "ptr" },
{ name: "filled", index: SCR_ARR_FIELD.filled, type: "i32" },
{ name: "ext", index: SCR_ARR_FIELD.ext, type: "ptr" },
];
for (const member of storageMembers) {
@@ -0,0 +1,102 @@
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import {
integerArithmeticRange,
withinInt32,
withinUint32,
type IntegerRange,
type IntegerRanges,
type IntegerSlotFacts,
} from "../../ir/integer-ranges.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
/** How an int32-stored local holds its value: the i32 bits of an int32
* (`signed`) or of a uint32, and the range every write lands in. */
export interface Int32Local {
signed: boolean;
range: IntegerRange;
}
/** Number locals whose every write is an exact int32 (or every write an
* exact uint32), so an i32 slot can be their only storage. A read widens
* the i32 back to the same double, which is exact because the range
* analysis never proves -0, NaN or a fraction; integer consumers take the
* i32 directly, and no double is carried through loops.
*
* A write qualifies through its proven range (analyzeIntegerRanges), or,
* for an Int32 contract local, through the contract, which checked every
* write with the same analysis. `++`/`--` qualify through the range of the
* value they write. A parameter also needs a proof for its incoming value
* (every call site, or its contract). For-of and catch bindings, captured,
* boxed and TDZ locals keep their ordinary storage, as do async functions
* and generators, whose locals live in frames the range analysis does not
* follow (it proves nothing there). `excluded` names further locals the
* caller reads through other storage. */
export function findInt32Locals(
fn: IrFunction,
ranges: IntegerRanges,
facts: IntegerSlotFacts,
excluded: ReadonlySet<string>,
): { locals: Map<string, Int32Local>; values: ReadonlySet<IrExpr> } {
const result = new Map<string, Int32Local>();
// The written values the proof covers; the emitter stores no other.
const values = new Set<IrExpr>();
if (fn.async || fn.generator || ranges.size === 0) return { locals: result, values };
const captured = new Set(
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
);
const contract = (id: string): IntegerRange | null => facts.locals?.(id) ?? null;
const writes = new Map<string, (IntegerRange | null)[]>();
for (const local of fn.locals) {
if (local.type.kind !== "f64" || local.boxed || local.tdz) continue;
if (captured.has(local.id) || excluded.has(local.id)) continue;
writes.set(local.id, []);
}
for (const param of fn.params) {
const list = writes.get(param.localId);
if (list) list.push(facts.params?.get(param.localId) ?? contract(param.localId));
}
const write = (id: string, range: IntegerRange | null | undefined, value?: IrExpr): void => {
const list = writes.get(id);
if (!list) return;
list.push(range ?? contract(id));
if (value) values.add(value);
};
const expr = (node: IrExpr): boolean => {
if (node.kind === "assignExpr") write(node.localId, ranges.get(node.value), node.value);
else if (node.kind === "incDec") {
// The analysis records the value the expression yields: the new
// value for prefix forms, the old one for postfix forms.
const yielded = ranges.get(node) ?? null;
write(
node.localId,
yielded && !node.prefix
? integerArithmeticRange(node.op, yielded, { min: 1, max: 1 })
: yielded,
node,
);
}
return everyExprChild(node, expr, stmt);
};
const stmt = (node: IrStmt): boolean => {
if (node.kind === "varDecl") {
if (node.init) write(node.localId, ranges.get(node.init), node.init);
} else if (node.kind === "assign") write(node.localId, ranges.get(node.value), node.value);
else if (node.kind === "forOf") writes.get(node.localId)?.push(null);
else if (node.kind === "tryCatch" && node.catchLocalId)
writes.get(node.catchLocalId)?.push(null);
return everyStmtChild(node, expr, stmt);
};
fn.body.every(stmt);
for (const [id, list] of writes) {
// An unwritten local is never read as a number.
if (list.length === 0 || list.some((range) => range === null)) continue;
const range = { min: Infinity, max: -Infinity };
for (const write of list as IntegerRange[]) {
range.min = Math.min(range.min, write.min);
range.max = Math.max(range.max, write.max);
}
if (withinInt32(range)) result.set(id, { signed: true, range });
else if (withinUint32(range)) result.set(id, { signed: false, range });
}
return { locals: result, values };
}
@@ -497,6 +497,25 @@ function emitDenseReferenceArrayRead(
B.startBlock(range);
B.br(dense);
B.startBlock(dense);
// Below the hole-free prefix bound every state is a present value.
const filledPtr = B.tmp(),
filled = B.tmp(),
belowFilled = B.tmp(),
lenLabel = B.newLabel("local.array.len");
B.line(
`${filledPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.filled}`,
);
host.markMemoryPointer(filledPtr, "array:header");
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
let filledSized = filled;
if (host.sizeType !== "i32") {
filledSized = B.tmp();
B.line(`${filledSized} = zext i32 ${filled} to ${host.sizeType}`);
}
B.line(`${belowFilled} = icmp ult ${host.sizeType} ${offset}, ${filledSized}`);
const valueLabel = B.newLabel("local.array.value");
B.condBr(belowFilled, valueLabel, lenLabel);
B.startBlock(lenLabel);
const lenPtr = B.tmp(),
len = B.tmp(),
belowLen = B.tmp();
@@ -504,8 +523,7 @@ function emitDenseReferenceArrayRead(
host.markMemoryPointer(lenPtr, "array:header");
B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`);
const stateLabel = B.newLabel("local.array.state"),
valueLabel = B.newLabel("local.array.value");
const stateLabel = B.newLabel("local.array.state");
B.condBr(belowLen, stateLabel, no);
B.startBlock(stateLabel);
const statesPtr = B.tmp(),
+2 -2
View File
@@ -319,8 +319,8 @@ function rcHeaderTest(cycle: "arr" | "map", yes: string, no: string): string[] {
if (cycle === "arr") {
return [
` %cycp = getelementptr inbounds %ScrArr, ptr %o, i32 0, i32 ${SCR_ARR_FIELD.cyc} ; cyc`,
` %cyc = load i32, ptr %cycp`,
` %headered = icmp ne i32 %cyc, 0`,
` %cyc = load i8, ptr %cycp`,
` %headered = icmp ne i8 %cyc, 0`,
` br i1 %headered, label %${yes}, label %${no}`,
];
}
+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 = 14 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v14" as const;
export const RUNTIME_ABI_VERSION = 15 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v15" 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: 14, marker: "scr_runtime_abi_v14" },
runtime_abi: { version: 15, marker: "scr_runtime_abi_v15" },
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: 14, marker: "scr_runtime_abi_v14" },
runtime_abi: { version: 15, marker: "scr_runtime_abi_v15" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -0,0 +1,340 @@
import { expect, test } from "vitest";
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
import { functionCallFacts } from "./int32-slots.js";
import {
F64,
VOID,
type IrExpr,
type IrFunction,
type IrModule,
type IrStmt,
type IrType,
type SrcLoc,
} from "./ir.js";
import { everyStmtList } from "./traverse.js";
/* Every plain `string` field of the IR, classified. The analyses that ask
* which functions escape (constant-callbacks.ts, int32-slots.ts) read only
* the fields classified "reference" here, through function-references.ts.
* A new string field anywhere in the IR fails to compile until it is
* classified, and a new "reference" fails the tests below until the shared
* helpers report it. */
type Role =
/** Names a module function as a value or a runtime entry. */
| "reference"
/** A direct call's callee. */
| "callee"
/** A class method name: the function is `%<class>.<method>`. */
| "method"
/** A verified contract fact about a function, not a way to reach it. */
| "fact"
/** Never a module function's name. */
| "other";
type Leaf = IrExpr | IrStmt | IrType | SrcLoc;
type PathsOf<V, P extends string> = V extends string
? string extends V
? P
: never
: V extends readonly (infer E)[]
? PathsOf<E, `${P}[]`>
: V extends Leaf
? never
: V extends object
? StringFields<V, `${P}.`>
: never;
type StringFields<T, P extends string = ""> = {
[K in keyof T & string]-?: PathsOf<NonNullable<T[K]>, `${P}${K}`>;
}[keyof T & string];
type Classified<U extends { kind: string }> = {
[K in U["kind"] as [StringFields<Extract<U, { kind: K }>>] extends [never] ? never : K]: Record<
StringFields<Extract<U, { kind: K }>>,
Role
>;
};
const EXPR_FIELDS: Classified<IrExpr> = {
numLit: { spelling: "other" },
strLit: { value: "other" },
moduleNsRef: { moduleId: "other" },
varRef: { localId: "other" },
incDec: { localId: "other" },
fieldIncDec: { className: "other", field: "other" },
assignExpr: { localId: "other" },
orDefault: { retag: "reference" },
optChain: { id: "other" },
chainRecv: { id: "other" },
regexLit: { pattern: "other", flags: "other" },
templateStrings: { key: "other", "cooked[]": "other" },
call: { callee: "callee" },
ffiCall: { import: "other" },
closure: { fnName: "reference", "captures[]": "other" },
new: { className: "other" },
classRef: { className: "other", "captures[]": "other" },
instanceOf: { className: "other" },
virtualCall: { className: "other", method: "method" },
fieldGet: { className: "other", field: "other" },
recordLit: { "fields[].name": "other" },
recordClone: { "overrides[].name": "other" },
recordGet: { shapeId: "other", field: "other" },
recordKeyGet: { shapeId: "other" },
dynFrom: { fnName: "other" },
dynCall: { calleeName: "other", "spreads[].what": "other" },
dynInvoke: { method: "other", calleeName: "other" },
dynHasKey: { key: "other" },
dynDestrCheck: { spelling: "other", firstProp: "other" },
dynIterN: { notIterableMessage: "other" },
recordOvfKeys: { shapeId: "other" },
recordOvfHas: { shapeId: "other" },
recordHas: { shapeId: "other", field: "other" },
fieldAbsent: { unionId: "other" },
unionWrap: { unionId: "other" },
unionRetype: { unionId: "other", fromId: "other" },
unionFuncEq: { unionId: "other" },
caughtTest: { className: "other" },
caughtCheck: { className: "other" },
unionNarrow: { unionId: "other" },
unionDisc: { unionId: "other", field: "other" },
unionKeyGet: { unionId: "other" },
unionIsTag: { unionId: "other" },
unionEq: { unionId: "other" },
jsonStringify: { indent: "other" },
};
const STMT_FIELDS: Classified<IrStmt> = {
varDecl: { localId: "other" },
assign: { localId: "other" },
while: { "labels[]": "other" },
doWhile: { "labels[]": "other" },
switch: { "labels[]": "other" },
for: { "labels[]": "other" },
forOf: { localId: "other", "labels[]": "other" },
fieldSet: { className: "other", field: "other" },
recordSet: { shapeId: "other", field: "other" },
recordKeySet: { shapeId: "other" },
recordKeyDelete: { shapeId: "other" },
break: { label: "other" },
continue: { label: "other" },
block: { "labels[]": "other" },
runtimeFence: { code: "other", message: "other" },
rethrow: { localId: "other" },
tryCatch: { catchLocalId: "other" },
};
const TYPE_FIELDS: Classified<IrType> = {
object: { className: "other" },
record: { shapeId: "other" },
classval: { className: "other" },
moduleNs: { moduleId: "other" },
union: { unionId: "other" },
};
const FUNCTION_FIELDS: Record<StringFields<Omit<IrFunction, "body">>, Role> = {
name: "other",
sourceName: "other",
asyncCacheGlobal: "other",
asyncCycleCacheGlobal: "other",
"params[].name": "other",
"params[].localId": "other",
"locals[].name": "other",
"locals[].id": "other",
"captures[].name": "other",
"captures[].localId": "other",
"classCaptures[].name": "other",
"classCaptures[].localId": "other",
};
const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
sourceFile: "other",
entry: "reference",
"classes[].name": "other",
"classes[].base": "other",
"classes[].jsName": "other",
"classes[].genericOf": "other",
"classes[].baseValueGlobal": "other",
"classes[].prototypeDataHelper": "reference",
"classes[].instancePrototypeHelper": "reference",
"classes[].localCaptures[].name": "other",
"classes[].localCaptures[].localId": "other",
"classes[].symbolFields[].field": "other",
"classes[].symbolFields[].globalId": "other",
"classes[].fields[].name": "other",
"classes[].methods[]": "method",
"classes[].abstractMethods[]": "method",
"globals[].id": "other",
"globals[].name": "other",
"globals[].initFlag": "other",
"records[].id": "other",
"records[].fields[].name": "other",
"records[].declaredOrder[]": "other",
"unions[].id": "other",
"unions[].discriminant.field": "other",
"unions[].discriminant.cases[].values[]": "other",
"ffiImports[].name": "other",
"ffiImports[].symbol": "other",
"ffiImports[].library": "other",
"ffiImports[].callbackTarget": "other",
"ffiImports[].params[].context": "other",
"ffiImports[].params[].callback.id": "other",
"ffiImports[].params[].callback.release": "other",
"ffiImports[].params[].callback.params[].context": "other",
"lib.profileName": "other",
"lib.prefix": "other",
"lib.initSymbol": "other",
"lib.sinkRegisterSymbol": "other",
"lib.collectSymbol": "other",
"lib.resultResetSymbol": "other",
"lib.callbackRegisterSymbol": "other",
"lib.callbacks[].name": "other",
"lib.callbacks[].unregisteredTrap": "other",
"lib.exports[].symbol": "other",
"lib.exports[].fnName": "reference",
"lib.exports[].inboundBytesTrap": "other",
"lib.exports[].inboundIntTrap": "other",
"lib.trapOverlays[].code": "other",
"lib.trapOverlays[].teaching": "other",
"lib.trapOverlays[].remediation": "other",
"lib.identity.buildIdSymbol": "other",
"lib.identity.abiVersionSymbol": "other",
"lib.identity.buildId": "other",
"contracts.int32Fields[]": "other",
"contracts.int32Params[]": "other",
"contracts.int32Returns[]": "fact",
"contracts.int32Locals[]": "other",
"contracts.noThrow[]": "fact",
"contracts.acyclic[]": "other",
"contracts.immortalGlobals[]": "other",
"contracts.programLifetime[]": "other",
};
const loc = { file: "references.ts", start: 0, end: 0 };
const TARGET = "%m0.target";
const target: IrFunction = {
name: TARGET,
params: [{ localId: "n", name: "n", type: F64 }],
locals: [{ id: "n", name: "n", type: F64, mutable: true }],
returnType: VOID,
body: [],
loc,
};
const entry = (body: IrStmt[] = []): IrFunction => ({
name: "%main",
params: [],
locals: [],
returnType: VOID,
body,
loc,
});
const module = (body: IrStmt[] = []): IrModule => ({
irVersion: 16,
sourceFile: loc.file,
entry: "%main",
functions: [target, entry(body)],
});
const exprStmt = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
/** A module whose only mention of TARGET, other than its definition, is
* through the given reference field. */
const REFERENCE_FIXTURES: Record<string, () => IrModule> = {
"closure.fnName": () =>
module([
exprStmt({
kind: "closure",
fnName: TARGET,
captures: [],
type: { kind: "func", params: [F64], ret: VOID },
loc,
}),
]),
"orDefault.retag": () =>
module([
exprStmt({
kind: "orDefault",
left: { kind: "numLit", value: 0, type: F64, loc },
right: { kind: "numLit", value: 1, type: F64, loc },
retag: TARGET,
type: F64,
loc,
}),
]),
entry: () => ({ ...module(), entry: TARGET }),
"classes[].prototypeDataHelper": () => ({
...module(),
classes: [{ name: "%m0.Cell", fields: [], prototypeDataHelper: TARGET, loc }],
}),
"classes[].instancePrototypeHelper": () => ({
...module(),
classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }],
}),
"lib.exports[].fnName": () => ({
...module(),
lib: {
profileName: "references",
prefix: "ref_",
initSymbol: "ref_init",
sinkRegisterSymbol: "ref_sink",
collectSymbol: null,
resultResetSymbol: null,
threadInstances: false,
exports: [{ symbol: "ref_target", fnName: TARGET, params: ["f64"], returns: "void" }],
trapOverlays: [],
},
}),
};
function referencesOf(mod: IrModule): Set<string> {
const names = new Set(moduleFunctionReferences(mod));
for (const fn of mod.functions)
everyStmtList(fn.body, {
expr: (expr) => {
const name = exprFunctionReference(expr);
if (name !== undefined) names.add(name);
return true;
},
stmt: () => true,
});
return names;
}
const referencePaths = (table: object, prefix: string): string[] =>
Object.entries(table).flatMap(([key, value]) =>
typeof value === "string"
? value === "reference"
? [prefix + key]
: []
: referencePaths(value as object, `${key}.`),
);
test("every IR field that can name a function has a reference fixture", () => {
const references = [
...referencePaths(EXPR_FIELDS, ""),
...referencePaths(STMT_FIELDS, ""),
...referencePaths(TYPE_FIELDS, ""),
...referencePaths(FUNCTION_FIELDS, "functions[]."),
...referencePaths(MODULE_FIELDS, ""),
];
expect(references.sort()).toEqual(Object.keys(REFERENCE_FIXTURES).sort());
});
test.each(Object.keys(REFERENCE_FIXTURES))("%s escapes its function", (path) => {
const mod = REFERENCE_FIXTURES[path]!();
expect(referencesOf(mod).has(TARGET)).toBe(true);
const facts = functionCallFacts(mod, new Map(), (name) => [name]);
expect(facts.valueEscaped.has(TARGET)).toBe(true);
});
test("direct calls and look-alike strings do not escape a function", () => {
const mod = module([
exprStmt({ kind: "call", callee: TARGET, args: [], type: VOID, loc }),
exprStmt({ kind: "strLit", value: TARGET, type: { kind: "string" }, loc }),
exprStmt({
kind: "dynFrom",
value: { kind: "numLit", value: 0, type: F64, loc },
fnName: TARGET,
type: { kind: "dyn" },
loc,
}),
]);
expect(referencesOf(mod).has(TARGET)).toBe(false);
expect(functionCallFacts(mod, new Map(), (name) => [name]).valueEscaped.has(TARGET)).toBe(false);
});
@@ -0,0 +1,26 @@
import type { IrExpr, IrModule } from "./ir.js";
/** The function an expression names as a value rather than as a direct
* call's callee: a closure's code, or the union→union retag helper an
* `orDefault` hands its left box to. These are the only expression fields
* that hold a module function's name besides `call.callee`
* (function-references.test.ts classifies every string field of the IR). */
export function exprFunctionReference(expr: IrExpr): string | undefined {
if (expr.kind === "closure") return expr.fnName;
if (expr.kind === "orDefault") return expr.retag;
return undefined;
}
/** Functions the module's metadata names: the entry, library exports and
* the class prototype helpers the runtime calls. Class methods and
* constructors follow the `%<class>.<method>` convention instead of being
* named here; callers that need them derive them from `mod.classes`. */
export function moduleFunctionReferences(mod: IrModule): string[] {
const names = [mod.entry];
for (const entry of mod.lib?.exports ?? []) names.push(entry.fnName);
for (const cls of mod.classes ?? []) {
if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper);
if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper);
}
return names;
}
+40 -43
View File
@@ -7,8 +7,9 @@ import {
type IntegerRanges,
type IntegerSlotFacts,
} from "./integer-ranges.js";
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js";
import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js";
/** Whole-program int32 specialization of numeric storage.
*
@@ -265,18 +266,6 @@ const DYN_READ_ONLY = new Set<string>([
"text.decodeStream",
"worker.new",
]);
/** IR metadata keys whose strings never name a function. */
const NON_REFERENCE_KEYS = new Set([
"loc",
"source",
"name",
"sourceName",
"jsName",
"localId",
"type",
"returnType",
]);
function walkFunction(
fn: IrFunction,
visit: { expr?: (e: IrExpr) => void; stmt?: (s: IrStmt) => void },
@@ -480,36 +469,8 @@ export function functionCallFacts(
virtualTargets: Set<string>;
runtimeVirtual: (name: string) => boolean;
} {
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
const escaped = new Set<string>();
const escaped = new Set<string>(moduleFunctionReferences(mod));
const virtualTargets = new Set<string>();
const seenObjects = new Set<object>();
const scan = (
value: unknown,
key: string | null,
parent: Record<string, unknown> | null,
): void => {
if (typeof value === "string") {
if (functions.has(value)) {
const direct =
key === "callee" && parent?.kind === "call"
? true
: key === "value" && parent?.kind === "strLit";
if (!direct) escaped.add(value);
}
return;
}
if (value === null || typeof value !== "object" || seenObjects.has(value)) return;
seenObjects.add(value);
if (Array.isArray(value)) {
for (const item of value) scan(item, null, null);
return;
}
const record = value as Record<string, unknown>;
for (const [k, v] of Object.entries(record)) if (!NON_REFERENCE_KEYS.has(k)) scan(v, k, record);
};
scan(mod, null, null);
if (typeof mod.entry === "string") escaped.add(mod.entry);
// Virtual dispatch: a `virtualCall` can reach the method's implementation
// in any class of the receiver's subtree or the one it inherits. Classes
// under a runtime-provided root keep every method escaped: the runtime
@@ -525,6 +486,8 @@ export function functionCallFacts(
for (const fn of mod.functions)
walkFunction(fn, {
expr: (e) => {
const referenced = exprFunctionReference(e);
if (referenced !== undefined) escaped.add(referenced);
if (e.kind !== "virtualCall") return;
for (const name of subtree(e.className)) virtualTargets.add(`%${name}.${e.method}`);
for (let c = classes.get(e.className); c; c = c.base ? classes.get(c.base) : undefined)
@@ -825,13 +788,47 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
queue.push(fnName);
}
};
const candidateArgs = (callee: IrFunction | undefined, args: number, offset: number): boolean => {
if (!callee) return false;
for (let i = 0; i < args; i++) {
const param = callee.params[i + offset];
if (param && slots.hasParam(callee.name, param.localId)) return true;
}
return false;
};
const hasCandidateWrite = (fn: IrFunction): boolean => {
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
const returns = slots.hasReturn(fn.name);
return !everyStmtList(fn.body, {
stmt: (s) => {
if (s.kind === "assign") return !box(s.localId);
if (s.kind === "varDecl") return s.init === null || !box(s.localId);
if (s.kind === "fieldSet") {
const family = slots.family(s.className, s.field);
return !family || !slots.hasFamily(family);
}
return !(s.kind === "return" && returns);
},
expr: (e) => {
if (e.kind === "call") return !candidateArgs(functions.get(e.callee), e.args.length, 0);
if (e.kind === "new")
return !candidateArgs(functions.get(`%${e.className}.constructor`), e.args.length, 1);
if (e.kind === "assignExpr") return !box(e.localId);
return true;
},
});
};
const provenByFunction = new Map<string, IrExpr[]>();
while (queue.length) {
const fn = functions.get(queue.pop()!)!;
queued.delete(fn.name);
const params = slots.facts(fn.name).params;
if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name);
const ranges: IntegerRanges = analyzeIntegerRanges(numeric(fn), slots.facts(fn.name));
// Range facts are consulted only at writes into candidate slots; slots
// are only ever dropped, so a body without such a write needs none.
const ranges: IntegerRanges = hasCandidateWrite(fn)
? analyzeIntegerRanges(numeric(fn), slots.facts(fn.name))
: new Map();
const proven: IrExpr[] = [];
const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e));
const args = (callee: IrFunction | undefined, list: IrExpr[], offset: number): void => {
@@ -0,0 +1,221 @@
import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
const repoRoot = join(import.meta.dirname, "../../..");
const source = `
import type { Int32 } from "@scriptc/types";
function rsync(a: Uint8Array): number {
let s1 = 0,
s2 = 0;
for (let i = 0; i < a.length; i++) {
s1 = (s1 + a[i]) & 0xffff;
s2 = (s2 + s1) & 0xffff;
}
return ((s2 << 16) | s1) >>> 0;
}
function mix(s: string): number {
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + i) | 0;
return h;
}
function counters(): number {
let up = 0;
const before = up++;
const after = ++up;
let down = 3;
const was = down--;
return before * 1000 + after * 100 + was * 10 + down;
}
function unsigned(a: Uint32Array): number {
let acc = 0;
for (let i = 0; i < a.length; i++) acc = (acc + (a[i] ^ i)) >>> 0;
return acc;
}
function fraction(n: number): number {
let h = 0;
for (let i = 0; i < n; i++) h = (h + i) | 0;
h = h / 2;
return h;
}
function mixed(n: number): number {
let m = -1 | 0;
if (n > 1) m = m >>> 0;
return m;
}
function captured(words: string[]): number {
let h = 17;
const add = (w: string): void => {
for (let i = 0; i < w.length; i++) h = (Math.imul(h, 31) + w.charCodeAt(i)) | 0;
};
for (const w of words) add(w);
return h;
}
function capturedParam(seed: number): () => number {
return () => seed;
}
function* frames(words: string[]): Generator<number> {
let h = 0;
for (const w of words) {
for (let i = 0; i < w.length; i++) h = (Math.imul(h, 33) ^ w.charCodeAt(i)) | 0;
yield h;
}
}
async function suspended(words: string[]): Promise<number> {
let total = 0;
for (const w of words) {
await Promise.resolve();
total = (total + w.length) | 0;
}
return total;
}
function contract(seed: Int32, steps: Int32): Int32 {
let acc: Int32 = seed;
for (let i: Int32 = 0; i < steps; i = (i + 1) | 0) acc = (Math.imul(acc, 3) + i) | 0;
return acc;
}
const bytes = new Uint8Array([1, 2, 3, 250]);
console.log(rsync(bytes), mix("abcd"), counters(), unsigned(new Uint32Array([4294967295, 7])));
console.log(fraction(5), mixed(3), captured(["ab", "c"]), capturedParam(3)());
console.log([...frames(["ab", "cd"])].join(" "), contract(7, 20));
suspended(["ab"]).then((t) => console.log(t));
`;
const body = (ll: string, name: string): string =>
ll.match(
new RegExp(`define internal [^\\n]+ @sc_(?:b)?f_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`),
)![1]!;
/** Every function definition whose header or body matches. */
const functionsWith = (ll: string, pattern: RegExp): string[] =>
ll.split(/^(?=define )/m).filter((fn) => fn.startsWith("define ") && pattern.test(fn));
/** The blocks of a function's first loop, from its header to its exit. */
const loopOf = (fn: string): string => {
const start = fn.search(/\nloop\.c\d+:/);
expect(start).toBeGreaterThan(0);
const end = fn.indexOf("\nloop.e", start);
return fn.slice(start, end < 0 ? undefined : end);
};
/** The function without pure instructions whose results nothing uses,
* repeatedly, as LLVM's dead code elimination leaves it. */
const live = (fn: string): string => {
const pure =
/^\s*(%[\w.]+) = (?:[su]itofp|fpto[su]i|f(?:add|sub|mul|div|neg|cmp)|select|[sz]ext|trunc|add|sub|mul|and|or|xor|shl|lshr|ashr|icmp|phi|freeze|bitcast|getelementptr|load) /;
let lines = fn.split("\n");
for (let changed = true; changed;) {
changed = false;
const text = lines.join("\n");
lines = lines.filter((line) => {
const name = line.match(pure)?.[1];
if (!name) return true;
const uses = text.split(new RegExp(`${name.replace(/\./g, "\\.")}(?![\\w.])`)).length - 2;
if (uses > 0) return true;
changed = true;
return false;
});
}
return lines.join("\n");
};
/** What keeps a double carrier of a local in a function's loop once dead
* values are gone: double loads and stores of local slots, and widened
* reads of a local's i32. */
const carriers = (fn: string): string[] => {
const loop = loopOf(live(fn));
const reads = new Set(
[...loop.matchAll(/^\s*(%[\w.]+) = load i32, ptr %sc_l_\w+$/gm)].map((m) => m[1]),
);
return [
...loop.matchAll(/^\s*(?:%[\w.]+ = load double, ptr|store double [^,]+, ptr) %sc_l_\w+$/gm),
...[...loop.matchAll(/^\s*%[\w.]+ = [su]itofp i32 (%[\w.]+) to double$/gm)].filter((m) =>
reads.has(m[1]),
),
].map((m) => m[0].trim());
};
test("int32-closed locals are stored as i32 and keep doubles only for floating consumers", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-locals-"));
try {
await symlink(join(repoRoot, "node_modules"), join(dir, "node_modules"));
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
for (const pointerBits of [32, 64] as const) {
const ll = emitLlvmModule(mod, { pointerBits, wasi: pointerBits === 32 });
// Loop-carried accumulators and counters: i32 slots, no per-iteration
// conversion to or from a double, no integer view beside them.
const rsync = body(ll, "rsync");
expect(rsync).toMatch(/alloca i32 ; s1\n/);
expect(rsync).toMatch(/alloca i32 ; s2\n/);
expect(rsync).not.toMatch(/alloca double ; s[12]\n/);
expect(rsync).not.toContain("integer view");
expect(carriers(rsync)).toEqual([]);
const mix = body(ll, "mix");
expect(mix).toMatch(/alloca i32 ; h\n/);
expect(mix).toMatch(/alloca i32 ; i\n/);
expect(carriers(mix)).toEqual([]);
// The result is the one double the function needs.
expect(mix).toMatch(/ = sitofp i32 %\w+ to double\n\s+ret double/);
// ++ and -- write proven int32s in place.
const counters = body(ll, "counters");
expect(counters).toMatch(/alloca i32 ; up\n/);
expect(counters).toMatch(/alloca i32 ; down\n/);
expect(counters.match(/ = add i32 %\w+, 1\n/g)).toHaveLength(2);
expect(counters).toMatch(/ = sub i32 %\w+, 1\n/);
// A uint32 accumulator holds its bits and widens unsigned.
const unsigned = body(ll, "unsigned");
expect(unsigned).toMatch(/alloca i32 ; acc\n/);
expect(carriers(unsigned)).toEqual([]);
expect(unsigned).toMatch(/ = uitofp i32 %\w+ to double\n\s+ret double/);
// Locals that leave int32, or hold both int32 and uint32 values.
expect(body(ll, "fraction")).toMatch(/alloca double ; h\n/);
expect(body(ll, "mixed")).toMatch(/alloca double ; m\n/);
// Captured bindings keep their boxes, inside and outside the closure.
const captured = body(ll, "captured");
expect(captured).toMatch(/alloca ptr ; h\n/);
expect(captured).not.toMatch(/alloca i32 ; h\n/);
const closures = functionsWith(ll, /; captured h\n/);
expect(closures.length).toBeGreaterThan(0);
for (const fn of closures) expect(fn).not.toMatch(/alloca i32 ; h\n/);
expect(body(ll, "capturedParam")).not.toMatch(/alloca i32 ; seed\n/);
// Generator and async frames keep doubles.
for (const name of ["frames", "suspended"]) {
const fns = functionsWith(ll, new RegExp(`^define [^\\n]*${name}`));
expect(fns.length).toBeGreaterThan(0);
for (const fn of fns) expect(fn).not.toMatch(/alloca i32 ; (?:h|total)\n/);
}
// Int32 contract locals and parameters use the same storage; the
// parameter converts once on entry.
const contract = body(ll, "contract");
expect(contract).toMatch(/alloca i32 ; acc\n/);
expect(contract).toMatch(/alloca i32 ; seed\n/);
expect(contract).toMatch(/ = fptosi double %p_\w+ to i32\n/);
expect(carriers(contract)).toEqual([]);
}
// Debug builds keep every number local a double for the variable view.
const debug = emitLlvmModule(mod, { debugSources: new Map([[entry, source]]) });
expect(debug).not.toMatch(/alloca i32 ; (?:s1|h|acc)\n/);
expect(debug).toMatch(/alloca double ; s1\n/);
expect(debug).toContain("@llvm.dbg.declare");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
@@ -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_v14");
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v15");
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_v14");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v15");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4840-array-hole-free-prefix.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4860-int32-local-storage.ts"
],
"diags": []
}
@@ -306,7 +306,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target,
runtime_abi: { version: 14, marker: "scr_runtime_abi_v14" },
runtime_abi: { version: 15, marker: "scr_runtime_abi_v15" },
compiler: {
command: compiler,
identity: compilerVersion,
@@ -402,7 +402,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: matrix.target,
runtime_abi: { version: 14, marker: "scr_runtime_abi_v14" },
runtime_abi: { version: 15, marker: "scr_runtime_abi_v15" },
compiler: {
command: compiler,
identity: compilerVersion,
+97 -1
View File
@@ -446,6 +446,7 @@ static uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t *out) {
* the destination length and does not need a retain. */
static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot,
uint8_t state) {
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;
@@ -470,6 +471,7 @@ static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out,
if (index < a->cap) {
uint8_t state = a->present[index];
if (state == SCR_ARR_HOLE) return false;
scr_arr_cut(a, index);
*out = a->data[index];
a->present[index] = SCR_ARR_HOLE;
a->data[index] = 0;
@@ -493,6 +495,7 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot,
uint8_t state) {
uint8_t old_state;
uint64_t old = 0;
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];
@@ -516,6 +519,22 @@ static void scr_arr_replace_owned(ScrArr *a, size_t index, uint64_t slot) {
scr_arr_replace_state_owned(a, index, slot, SCR_ARR_VALUE);
}
/* A value was just stored at `index` (with the length already covering it):
* extend the hole-free prefix when the store continues it. */
static void scr_arr_note_value(ScrArr *a, size_t index) {
if (index == a->filled && index < a->cap && index < a->len) a->filled = (uint32_t)index + 1;
}
/* scr_arr_refill bounded to [0, limit): for range operations whose cost is
* proportional to the range, not to the whole array. */
static void scr_arr_refill_to(ScrArr *a, size_t limit) {
size_t end = a->len < a->cap ? a->len : a->cap;
if (limit < end) end = limit;
size_t f = a->filled;
while (f < end && a->present[f] == SCR_ARR_VALUE) f++;
a->filled = (uint32_t)f;
}
static bool scr_arr_prop_get_state(const ScrArr *a, double key, uint64_t *out,
uint8_t *state_out) {
char text[64];
@@ -597,6 +616,7 @@ static ScrArrStorage scr_arr_take_storage(ScrArr *a) {
a->data = NULL;
a->present = NULL;
a->cap = 0;
a->filled = 0;
if (a->ext) {
a->ext->sparse = NULL;
a->ext->sparse_len = 0;
@@ -620,12 +640,14 @@ 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(a->present + to, a->present + from, count);
}
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));
}
@@ -637,6 +659,7 @@ 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(dst->present + to, src->present + from, count);
@@ -665,8 +688,10 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) {
a->rc = 1;
a->len = 0;
a->cap = 0;
a->elem = elem;
a->elem = (uint8_t)elem;
a->cyc = 0;
a->spare = 0;
a->filled = 0;
a->ops = NULL;
a->data = NULL;
a->present = NULL;
@@ -756,6 +781,8 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) {
a->cap = 0;
a->elem = SCR_ELEM_REF;
a->cyc = cyc;
a->spare = 0;
a->filled = 0;
a->ops = ops;
a->data = NULL;
a->present = NULL;
@@ -873,6 +900,7 @@ void scr_arr_set_len(ScrArr *a, double length) {
}
size_t next = (size_t)length;
if (next < a->len) {
scr_arr_cut(a, next);
/* Spare capacity is already empty. Repeatedly clearing a reused work
* array must cost only the elements removed, not its historical peak. */
size_t dense_end = a->len < a->cap ? a->len : a->cap;
@@ -922,10 +950,21 @@ void scr_arr_copy_range(ScrArr *dst, size_t dst_start, const ScrArr *src,
scr_arr_copy_range_ex(dst, dst_start, src, src_start, count, reverse, false);
}
static void scr_arr_copy_range_states(ScrArr *dst, size_t dst_start, const ScrArr *src,
size_t src_start, size_t count, bool reverse,
bool materialize_holes);
void scr_arr_copy_range_ex(ScrArr *dst, size_t dst_start, const ScrArr *src,
size_t src_start, size_t count, bool reverse,
bool materialize_holes) {
if (count == 0) return;
scr_arr_copy_range_states(dst, dst_start, src, src_start, count, reverse, materialize_holes);
scr_arr_refill_to(dst, dst_start + count);
}
static void scr_arr_copy_range_states(ScrArr *dst, size_t dst_start, const ScrArr *src,
size_t src_start, size_t count, bool reverse,
bool materialize_holes) {
if (src != dst && src_start <= src->cap && count <= src->cap - src_start &&
dst_start <= dst->cap && count <= dst->cap - dst_start) {
size_t empty = 0;
@@ -980,7 +1019,11 @@ ScrArr *scr_arr_copy_within(ScrArr *a, double target, double start, double end)
if (count == 0 || from == to) return scr_arr_retain(a);
if (!scr_elem_is_ref(a->elem) && from < a->cap && count <= a->cap - from &&
to < a->cap && count <= a->cap - to) {
/* Values moved within the hole-free prefix keep it. */
size_t f = a->filled;
scr_arr_move_dense(a, to, from, count);
if (from + count <= f && to + count <= f) a->filled = (uint32_t)f;
else scr_arr_refill_to(a, to + count);
return scr_arr_retain(a);
}
bool backwards = from < to && to - from < count;
@@ -997,6 +1040,7 @@ ScrArr *scr_arr_copy_within(ScrArr *a, double target, double start, double end)
scr_arr_replace_state_owned(a, to + offset, slot, state);
}
}
scr_arr_refill_to(a, to + count);
return scr_arr_retain(a);
}
@@ -1008,6 +1052,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state,
bool refs = scr_elem_is_ref(a->elem);
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;
memset(a->present + from, state, until - from);
} else {
@@ -1016,6 +1061,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state,
scr_arr_replace_state_owned(a, i, value, state);
}
}
if (state == SCR_ARR_VALUE) scr_arr_refill(a);
return scr_arr_retain(a);
}
@@ -1161,6 +1207,7 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) {
if (i >= 0 && i < (double)a->cap) {
size_t idx = (size_t)i;
if ((double)idx == i) {
if (idx < a->filled) return scr_slot_to_ptr(a->data[idx]);
if (idx >= a->len || a->present[idx] != SCR_ARR_VALUE) return NULL;
return scr_slot_to_ptr(a->data[idx]);
}
@@ -1236,6 +1283,7 @@ static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) {
/* Unlink-then-release: a release can trigger a cycle collection, which
* must never see a heap edge whose count was already given up. */
scr_arr_replace_owned(a, idx, slot);
scr_arr_note_value(a, idx);
}
void scr_arr_set_f64(ScrArr *a, double i, double v) {
@@ -1282,6 +1330,7 @@ static double scr_arr_push_slot(ScrArr *a, uint64_t slot) {
size_t idx = a->len;
scr_arr_store_owned(a, idx, slot);
a->len++;
scr_arr_note_value(a, idx);
return SCR_ARR_LEN_F64(a->len);
}
@@ -1310,6 +1359,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) {
for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, from + i, slots[i]);
}
a->len = next;
scr_arr_refill(a);
return (double)next;
}
@@ -1322,6 +1372,7 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) {
scr_arr_grow_dense(a, old_len + add);
scr_arr_copy_dense(a, old_len, src, 0, add, false, true);
a->len += add;
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
if (src == a) {
@@ -1339,6 +1390,7 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) {
}
a->len = old_len + add;
scr_arr_free_storage(&old);
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
a->len = old_len + add;
@@ -1351,6 +1403,7 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) {
scr_arr_store_state_owned(a, old_len + i, 0, SCR_ARR_UNDEFINED);
}
}
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
@@ -1363,6 +1416,7 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) {
scr_arr_grow_dense(a, old_len + add);
scr_arr_copy_dense(a, old_len, src, 0, add, false, false);
a->len += add;
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
if (src == a) {
@@ -1380,6 +1434,7 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) {
}
a->len = old_len + add;
scr_arr_free_storage(&old);
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
a->len = old_len + add;
@@ -1392,6 +1447,7 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) {
scr_arr_copy_index(a, old_len + SCR_AX(src)->sparse[i].index, src,
SCR_AX(src)->sparse[i].index);
}
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
@@ -1408,10 +1464,13 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) {
size_t old_len = a->len;
if (scr_arr_is_dense(a) && scr_arr_dense_ok(a, old_len + 1, 1)) {
scr_arr_grow_dense(a, old_len + 1);
/* 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;
a->present[0] = SCR_ARR_VALUE;
a->len++;
a->filled = (uint32_t)(f + 1);
return SCR_ARR_LEN_F64(a->len);
}
ScrArrStorage old = scr_arr_take_storage(a);
@@ -1426,6 +1485,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) {
}
a->len = old_len + 1;
scr_arr_free_storage(&old);
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
@@ -1447,9 +1507,13 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) {
if (count > SCR_ARR_MAX_LENGTH - old_len) scr_arr_oom();
if (scr_arr_is_dense(a) && scr_arr_dense_ok(a, old_len + count, count)) {
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));
memset(a->present, SCR_ARR_VALUE, count);
a->len = old_len + count;
a->filled = (uint32_t)(f + count);
return SCR_ARR_LEN_F64(a->len);
} else {
ScrArrStorage old = scr_arr_take_storage(a);
for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, i, slots[i]);
@@ -1464,6 +1528,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) {
scr_arr_free_storage(&old);
}
a->len = old_len + count;
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
@@ -1479,6 +1544,7 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) {
/* A self-spread reads the original values at their moved location. */
scr_arr_copy_dense(a, 0, src, src == a ? add : 0, add, false, true);
a->len += add;
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
ScrArrStorage old = scr_arr_take_storage(a);
@@ -1505,12 +1571,15 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) {
}
a->len = old_len + add;
scr_arr_free_storage(&old);
scr_arr_refill(a);
return SCR_ARR_LEN_F64(a->len);
}
ScrArr *scr_arr_reverse(ScrArr *a) {
size_t len = a->len;
if (scr_arr_is_dense(a)) {
/* A hole-free array stays hole-free; any other prefix is recomputed. */
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;
@@ -1521,6 +1590,7 @@ ScrArr *scr_arr_reverse(ScrArr *a) {
a->present[i] = a->present[j];
a->present[j] = state;
}
scr_arr_refill(a);
return scr_arr_retain(a);
}
ScrArrStorage old = scr_arr_take_storage(a);
@@ -1533,6 +1603,7 @@ ScrArr *scr_arr_reverse(ScrArr *a) {
old.sparse[i].slot, old.sparse[i].state);
}
scr_arr_free_storage(&old);
scr_arr_refill(a);
return scr_arr_retain(a);
}
@@ -1575,9 +1646,12 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) {
if (scr_arr_is_dense(a)) {
uint8_t state = a->present[0];
if (slot_out && state == SCR_ARR_VALUE) *slot_out = a->data[0];
/* The prefix slides down with the tail. */
size_t f = a->filled;
scr_arr_move_dense(a, 0, 1, old_len - 1);
scr_arr_clear_dense(a, old_len - 1, 1);
a->len--;
a->filled = (uint32_t)(f ? f - 1 : 0);
return state;
}
ScrArrStorage old = scr_arr_take_storage(a);
@@ -1595,6 +1669,7 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) {
}
a->len = old_len - 1;
scr_arr_free_storage(&old);
scr_arr_refill(a);
if (slot_out) *slot_out = s;
return state;
}
@@ -1644,9 +1719,15 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) {
memcpy(out->data, a->data + from, n * sizeof(*a->data));
memcpy(out->present, a->present + from, n);
}
scr_arr_refill(out);
/* The tail moves down over the removed range; values below the prefix
* bound that stay values keep it. */
size_t f = a->filled;
scr_arr_move_dense(a, from, from + n, old_len - from - n);
scr_arr_clear_dense(a, old_len - n, n);
a->len -= n;
if (f == old_len) a->filled = (uint32_t)a->len;
else scr_arr_refill(a);
return out;
}
ScrArrStorage old = scr_arr_take_storage(a);
@@ -1678,6 +1759,8 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) {
}
a->len = old_len - n;
scr_arr_free_storage(&old);
scr_arr_refill(a);
scr_arr_refill(out);
return out;
}
@@ -1711,11 +1794,13 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount,
memcpy(removed->present, a->present + from, n);
}
removed->len = n;
scr_arr_refill(removed);
scr_arr_grow_dense(a, next_len);
scr_arr_move_dense(a, from + add, from + n, a->len - from - n);
scr_arr_copy_dense(a, from, items, 0, add, false, true);
if (next_len < a->len) scr_arr_clear_dense(a, next_len, a->len - next_len);
a->len = next_len;
scr_arr_refill(a);
scr_arr_release(snapshot);
return removed;
}
@@ -1770,6 +1855,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou
}
size_t old_len = a->len;
scr_arr_grow_dense(a, next_len);
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));
@@ -1777,6 +1863,9 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou
}
if (next_len < old_len) scr_arr_clear_dense(a, next_len, old_len - next_len);
a->len = next_len;
/* Values inserted into a hole-free array keep it hole-free. */
if (packed) a->filled = (uint32_t)next_len;
else scr_arr_refill(a);
for (size_t i = 0; i < kept; i++) scr_elem_release(a, removed[i]);
if (removed != inline_removed) scr_mem_free(removed);
}
@@ -1804,6 +1893,7 @@ ScrArr *scr_arr_flat_copy(const ScrArr *a, ScrArr *out, bool flatten) {
scr_arr_copy_index(out, at, a, i);
}
}
scr_arr_refill(out);
return scr_arr_retain(out);
}
@@ -1942,6 +2032,7 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) {
out->len = n;
if (to <= a->cap) {
scr_arr_copy_dense(out, 0, a, from, n, false, false);
scr_arr_refill(out);
return out;
}
for (size_t i = 0; i < a->cap && i < to; i++) {
@@ -1960,6 +2051,7 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) {
SCR_AX(a)->sparse[i].state);
}
}
scr_arr_refill(out);
return out;
}
@@ -2109,6 +2201,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) {
if (copy) out->len = a->len;
if (!copy && scr_arr_is_dense(a)) {
if (count) memcpy(a->data, values, count * sizeof(*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);
@@ -2123,6 +2216,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) {
if (!copy) scr_arr_free_storage(&old);
}
free(values);
scr_arr_refill(out);
return copy ? out : scr_arr_retain(a);
}
@@ -2368,6 +2462,7 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity)
* holes, no storage-level undefined and no sparse tail. */
static bool scr_arr_dense_values(const ScrArr *a) {
if (!scr_arr_is_dense(a)) return false;
if (a->filled == a->len) return true;
const uint8_t *present = a->present;
bool all = true;
for (size_t i = 0; i < a->len; i++) all &= present[i] == SCR_ARR_VALUE;
@@ -2443,6 +2538,7 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned) {
} else {
for (size_t i = 0; i < n; i++) scr_arr_set_slot(a, (double)i, src[i]);
}
scr_arr_refill(a);
free(src);
return true;
}
+1 -1
View File
@@ -126,7 +126,7 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
void scr_runtime_abi_v14(void) {}
void scr_runtime_abi_v15(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
+36 -11
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_v14(void);
void scr_runtime_abi_v15(void);
void scr_runtime_workers_v8(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
@@ -1583,7 +1583,8 @@ typedef struct ScrArr {
size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */
size_t len;
size_t cap; /* dense capacity; sparse entries cover indices >= cap */
ScrElemKind elem;
/* One ScrElemKind. */
uint8_t elem;
/* 1 exactly when ops->trace is non-NULL: the element type carries a cycle
* header, so the array is CYCLE-CAPABLE (an element can point back at the
* array that owns it) and allocates with the hidden collector header —
@@ -1591,7 +1592,15 @@ typedef struct ScrArr {
* the slot storage (all elements are traced children). Arrays of acyclic
* ref elements and the historic element kinds keep the lean 1-word
* header. Kept beside `elem` so the RC fast paths test it with one load. */
uint32_t cyc;
uint8_t cyc;
uint16_t spare;
/* A lower bound on the hole-free prefix: filled <= len, filled <= cap,
* and every dense state below it is SCR_ARR_VALUE. Element reads below
* it need no state check and element writes below it leave the states
* alone. Any operation that may store another state (a hole, present
* undefined) or move states below it lowers it first (scr_arr_cut);
* 0 is always valid. */
uint32_t filled;
/* SCR_ELEM_REF only; NULL for every other element kind. */
const ScrArrOps *ops;
uint64_t *data;
@@ -1611,15 +1620,31 @@ typedef struct ScrArrExt {
size_t prop_cap;
struct ScrDyn *metadata; /* heterogeneous named properties of match results */
} ScrArrExt;
/* The LLVM backend's inline retain/release read `cyc` through its mirrored
* %ScrArr type (field 4, i32) to decide mark-live / candidate buffering
* exactly like scr_arr_retain and scr_arr_release; its inline element reads
* use data (field 6) and present (field 7). */
_Static_assert(sizeof(ScrElemKind) == sizeof(uint32_t), "ScrArr.elem is an i32 (%ScrArr field 3)");
_Static_assert(offsetof(ScrArr, cyc) == 3 * sizeof(size_t) + sizeof(uint32_t),
/* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read
* `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like
* scr_arr_retain and scr_arr_release; its inline element access uses filled
* (field 6, i32), data (field 8) and present (field 9). */
_Static_assert(offsetof(ScrArr, cyc) == 3 * sizeof(size_t) + 1,
"inline RC fast paths read ScrArr.cyc as %ScrArr field 4");
_Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 2 * sizeof(uint32_t) + sizeof(void *),
"inline element reads use ScrArr.data as %ScrArr field 6");
_Static_assert(offsetof(ScrArr, filled) == 3 * sizeof(size_t) + 4,
"inline element access reads ScrArr.filled as %ScrArr field 6");
_Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 8 + sizeof(void *),
"inline element reads use ScrArr.data as %ScrArr field 8");
/* 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) {
if (a->filled > index) a->filled = (uint32_t)index;
}
/* Extend the hole-free prefix over populated dense states. Starts at the
* current bound, so after an append it costs only the appended range. */
static inline void scr_arr_refill(ScrArr *a) {
size_t end = a->len < a->cap ? a->len : a->cap;
size_t f = a->filled;
while (f < end && a->present[f] == SCR_ARR_VALUE) f++;
a->filled = (uint32_t)f;
}
typedef struct ScrArrSparseSlot {
size_t index;
+81
View File
@@ -1266,6 +1266,86 @@ static void test_exact_construction(void) {
scr_arr_release(refs);
}
/* The hole-free prefix bound never covers a non-value state, and the
* operations that keep an array hole-free keep the bound at its length. */
static bool filled_ok(const ScrArr *a) {
if (a->filled > a->len || a->filled > a->cap) return false;
for (size_t i = 0; i < a->filled; i++)
if (a->present[i] != SCR_ARR_VALUE) return false;
return true;
}
static void test_filled_prefix(void) {
ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0);
for (int i = 0; i < 8; i++) scr_arr_push_f64(a, i);
check(a->filled == 8 && filled_ok(a), "pushes keep the bound at the length");
scr_arr_set_len(a, 12);
check(a->filled == 8 && filled_ok(a), "length growth leaves the bound below the holes");
scr_arr_set_f64(a, 8, 8);
check(a->filled == 9 && filled_ok(a), "a write at the bound extends it");
scr_arr_set_f64(a, 11, 11);
check(a->filled == 9 && filled_ok(a), "a write past a hole does not extend it");
scr_arr_release(scr_arr_fill_f64(a, 0, 0, INFINITY));
check(a->filled == 12 && filled_ok(a), "fill over the whole array restores it");
scr_arr_delete(a, 3);
check(a->filled == 3 && filled_ok(a), "delete cuts the bound at the hole");
scr_arr_set_f64(a, 3, 3);
check(a->filled == 4 && filled_ok(a), "refilling the hole continues the prefix");
scr_arr_set_undefined(a, 1);
check(a->filled == 1 && filled_ok(a), "present undefined cuts the bound");
scr_arr_set_len(a, 0);
check(a->filled == 0 && filled_ok(a), "truncation cuts the bound");
scr_arr_release(a);
ScrArr *b = scr_arr_new(SCR_ELEM_F64, 0);
for (int i = 0; i < 10; i++) scr_arr_push_f64(b, i);
check(scr_arr_shift_f64(b) == 0 && b->filled == 9 && filled_ok(b), "shift keeps a full bound");
scr_arr_unshift_f64(b, -1);
check(b->filled == 10 && filled_ok(b), "unshift keeps a full bound");
scr_arr_release(scr_arr_splice(b, 2, 3));
check(b->filled == 7 && filled_ok(b), "splice removal keeps a full bound");
double values[2] = {5, 6};
uint64_t two[2];
memcpy(two, values, sizeof two);
scr_arr_splice_drop(b, 1, 0, 2, two);
check(b->filled == 9 && filled_ok(b), "splice insertion keeps a full bound");
scr_arr_release(scr_arr_reverse(b));
check(b->filled == 9 && filled_ok(b), "reverse keeps a full bound");
scr_arr_release(scr_arr_copy_within(b, 0, 4, 6));
check(b->filled == 9 && filled_ok(b), "copyWithin inside the prefix keeps it");
scr_arr_set_len(b, 12);
scr_arr_release(scr_arr_copy_within(b, 2, 10, 12));
check(b->filled == 2 && filled_ok(b), "copyWithin of holes cuts the bound");
ScrArr *s = scr_arr_slice(b, 0, 2);
check(s->filled == 2 && filled_ok(s), "slice of a hole-free range is hole-free");
scr_arr_release(s);
s = scr_arr_slice(b, 0, INFINITY);
check(s->filled == 2 && filled_ok(s), "slice keeps holes out of the bound");
scr_arr_release(s);
scr_arr_release(scr_arr_reverse(b));
check(filled_ok(b), "reverse of a holey array recomputes the bound");
scr_arr_release(scr_arr_sort_primitive(b, false));
check(filled_ok(b), "sorting moves holes past the bound");
for (size_t i = 0; i < b->len; i++) {
if (i < b->cap && b->present[i] != SCR_ARR_VALUE) break;
check(i < b->filled, "sorted values form the bound");
}
while (scr_arr_len(b) > 0) {
scr_arr_pop_state(b, NULL);
check(filled_ok(b), "pop keeps the bound below the length");
}
scr_arr_release(b);
ScrArr *r = scr_arr_new(SCR_ELEM_STR, 0);
for (int i = 0; i < 4; i++) scr_arr_push_ref(r, scr_str_new("ab", 2));
check(r->filled == 4 && filled_ok(r), "reference pushes keep the bound");
scr_arr_delete(r, 0);
check(r->filled == 0 && filled_ok(r), "deleting the first reference empties the bound");
scr_arr_set_ref(r, 0, scr_str_new("c", 1));
check(r->filled == 1 && filled_ok(r), "a write at an empty bound extends it by one");
scr_arr_release(r);
}
int main(int argc, char **argv) {
if (argc > 1) {
ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0);
@@ -1310,6 +1390,7 @@ int main(int argc, char **argv) {
test_large_storage();
test_bulk_reference_ownership();
test_splice_drop();
test_filled_prefix();
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
return failed == 0 ? 0 : 1;
+199
View File
@@ -0,0 +1,199 @@
// Arrays that start hole-free and then gain holes, present undefined, or
// moved elements through every mutating operation. After each step the
// whole array is read back by index, by `in`, and by forEach (which skips
// holes), and then written again by index, so element reads and writes
// below and past the hole-free prefix all agree with JS semantics.
function show(label: string, xs: (number | 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);
}
function showNumbers(label: string, xs: number[]): void {
const parts: string[] = [];
for (let i = 0; i < xs.length; i++) parts.push(i in xs ? String(xs[i]) : "_");
let visits = 0;
let sum = 0;
xs.forEach((x) => {
visits++;
sum += x;
});
console.log(label, xs.length, parts.join(","), "visits", visits, "sum", sum);
}
function rewrite(xs: number[], from: number, to: number, base: number): void {
for (let i = from; i < to; i++) xs[i] = base + i;
}
// A hole at `at` without `delete`: copying a hole deletes the target.
function punch<T>(xs: T[], at: number): void {
const n = xs.length;
xs.length = n + 1;
xs.copyWithin(at, n, n + 1);
xs.length = n;
}
// fill, then length growth, then writes into and past the old length.
const a: number[] = new Array<number>(5).fill(1);
showNumbers("fill", a);
a[2] = 9;
showNumbers("write-in-prefix", a);
a.length = 8;
showNumbers("grow", a);
console.log("grow-in", 4 in a, 5 in a, a[6]);
a[5] = 50;
showNumbers("write-hole", a);
a[7] = 70;
showNumbers("write-last", a);
a[6] = 60;
showNumbers("close-gap", a);
a.length = 3;
showNumbers("truncate", a);
a.length = 6;
showNumbers("regrow", a);
rewrite(a, 0, 6, 100);
showNumbers("rewrite", a);
// a hole in the middle, then the hole is refilled.
const b: number[] = [1, 2, 3, 4, 5, 6];
punch(b, 2);
showNumbers("punch", b);
b[1] = 20;
b[4] = 40;
showNumbers("after-punch-writes", b);
b[2] = 30;
showNumbers("refill", b);
punch(b, 0);
punch(b, 5);
showNumbers("punch-ends", b);
b.push(7);
showNumbers("push-after-holes", b);
console.log("pop", b.pop(), b.pop(), b.length);
showNumbers("popped", b);
// sparse writes past the length and appends at the length.
const c: number[] = [];
for (let i = 0; i < 4; i++) c[i] = i * 2;
showNumbers("append-by-index", c);
c[10] = 1;
showNumbers("sparse-write", c);
for (let i = 4; i < 10; i++) c[i] = -i;
showNumbers("fill-gap", c);
// new Array(n) filled by index from the front and from the back.
const d: number[] = new Array<number>(6);
showNumbers("new-array", d);
for (let i = 0; i < 3; i++) d[i] = i;
showNumbers("front-half", d);
for (let i = 5; i >= 3; i--) d[i] = i;
showNumbers("back-half", d);
// present undefined in a nullable array.
const e: (number | undefined)[] = [1, 2, 3, 4];
e[1] = undefined;
show("undefined", e);
e[1] = 5;
e[3] = undefined;
show("undefined-last", e);
e.length = 6;
show("undefined-grow", e);
e.fill(undefined, 4);
show("fill-undefined", e);
e.fill(8);
show("fill-all", e);
// structural operations on hole-free and holey arrays.
for (const holey of [false, true]) {
const tag = holey ? "holey" : "packed";
const make = (): number[] => {
const xs: number[] = [10, 11, 12, 13, 14, 15, 16];
if (holey) punch(xs, 3);
return xs;
};
const s = make();
console.log(tag, "shift", s.shift());
showNumbers(tag + "-shift", s);
s[0] = 1;
s[s.length - 1] = 2;
showNumbers(tag + "-shift-write", s);
const u = make();
u.unshift(1, 2);
showNumbers(tag + "-unshift", u);
u[3] = 3;
showNumbers(tag + "-unshift-write", u);
const sp = make();
const removed = sp.splice(1, 2);
showNumbers(tag + "-splice", sp);
showNumbers(tag + "-removed", removed);
sp.splice(2, 0, 7, 8, 9);
showNumbers(tag + "-splice-insert", sp);
sp[4] = 4;
showNumbers(tag + "-splice-write", sp);
const r = make();
r.reverse();
showNumbers(tag + "-reverse", r);
r[3] = 33;
showNumbers(tag + "-reverse-write", r);
const so = make();
so.sort((x, y) => y - x);
showNumbers(tag + "-sort", so);
so[so.length - 1] = 99;
showNumbers(tag + "-sort-write", so);
const cw = make();
cw.copyWithin(0, 3, 6);
showNumbers(tag + "-copy-within", cw);
cw[1] = 1;
showNumbers(tag + "-copy-within-write", cw);
const sl = make().slice(2, 6);
showNumbers(tag + "-slice", sl);
sl[1] = 1;
showNumbers(tag + "-slice-write", sl);
const cc = make().concat(make());
showNumbers(tag + "-concat", cc);
cc[3] = 3;
showNumbers(tag + "-concat-write", cc);
const m = make().map((x) => x * 2);
showNumbers(tag + "-map", m);
const f = make().filter((x) => x % 2 === 0);
showNumbers(tag + "-filter", f);
}
// references: overwrite, punch, truncate and regrow string and object arrays.
class Box {
v: number;
constructor(v: number) {
this.v = v;
}
}
const names: string[] = ["a", "b", "c", "d"].map((s) => s + s);
names[1] = "x" + names[0];
punch(names, 2);
names.length = 6;
names[5] = "tail";
const nameParts: string[] = [];
for (let i = 0; i < names.length; i++) nameParts.push(i in names ? names[i]! : "_");
console.log("names", nameParts.join(","));
const boxes: Box[] = [];
for (let i = 0; i < 5; i++) boxes.push(new Box(i));
boxes[2] = new Box(20);
punch(boxes, 3);
boxes.length = 4;
boxes[1] = new Box(10);
let boxSum = 0;
boxes.forEach((box) => (boxSum += box.v));
console.log("boxes", boxes.length, 3 in boxes, boxSum, boxes[0]!.v, boxes[2]!.v);
// boolean flags: holes read as undefined (falsy) until written.
const flags: boolean[] = new Array<boolean>(8).fill(false);
for (let i = 2; i < 8; i += 2) flags[i] = true;
flags.length = 10;
let truthy = 0;
for (let i = 0; i < flags.length; i++) if (flags[i]) truthy++;
console.log("flags", truthy, 9 in flags, flags.indexOf(true), flags.lastIndexOf(false));
flags[9] = true;
flags[8] = true;
truthy = 0;
for (let i = 0; i < flags.length; i++) if (flags[i]) truthy++;
console.log("flags-written", truthy, flags.join(""));
+326
View File
@@ -0,0 +1,326 @@
// Number locals whose every write is an int32 (or every write a uint32):
// string and byte hashes, checksums and PRNGs, counters that cross 2^31
// through `| 0`, uint32 accumulators above 2^31, ++/-- counters, Int32
// contract locals and parameters. Locals that also hold fractions, -0, NaN
// or values beyond 32 bits, and locals that closures capture or that live
// in generator and async frames, read back the same numbers.
import type { Int32 } from "@scriptc/types";
function show(label: string, value: number): void {
console.log(label, value, Object.is(value, -0) ? "negative zero" : "");
}
function words(n: number): string[] {
const out: string[] = [];
let s = 12345;
for (let i = 0; i < n; i++) {
s = (Math.imul(s, 1103515245) + 12345) | 0;
const len = 1 + ((s >>> 16) & 15);
let w = "";
for (let j = 0; j < len; j++) {
s = (Math.imul(s, 1103515245) + 12345) | 0;
w += String.fromCharCode(97 + ((s >>> 16) % 26));
}
out.push(w);
}
out.push("", "é漢字🙂", "x".repeat(300));
return out;
}
function bytes(n: number, seed: number): Uint8Array {
const out = new Uint8Array(n);
let s = seed | 0;
for (let i = 0; i < n; i++) {
s = (Math.imul(s, 1103515245) + 12345) | 0;
out[i] = s >>> 24;
}
return out;
}
// String hashes, as a hash table or an interner computes them.
function djb2(s: string): number {
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
return h;
}
function javaHash(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
return h;
}
function sdbm(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) h = (s.charCodeAt(i) + (h << 6) + (h << 16) - h) | 0;
return h;
}
function fnv1a(s: string): number {
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
return h >>> 0;
}
// Checksums over bytes.
function rsyncWeak(a: Uint8Array): number {
let s1 = 0,
s2 = 0;
for (let i = 0; i < a.length; i++) {
s1 = (s1 + a[i]) & 0xffff;
s2 = (s2 + s1) & 0xffff;
}
return ((s2 << 16) | s1) >>> 0;
}
function rolling(a: Uint8Array, window: number): number {
// rsync's rolling update: drop the leaving byte, add the entering one.
let s1 = 0,
s2 = 0,
best = 0;
for (let i = 0; i < window; i++) {
s1 = (s1 + a[i]) & 0xffff;
s2 = (s2 + s1) & 0xffff;
}
for (let i = window; i < a.length; i++) {
s1 = (s1 - a[i - window] + a[i]) & 0xffff;
s2 = (s2 - window * a[i - window] + s1) & 0xffff;
const digest = ((s2 << 16) | s1) >>> 0;
if (digest > best) best = digest;
}
return best;
}
function fletcherWrap(a: Uint8Array): number {
let s1 = 0,
s2 = 0;
for (let i = 0; i < a.length; i++) {
s1 = (s1 + a[i]) | 0;
s2 = (s2 + s1) | 0;
}
return (s1 ^ s2) >>> 0;
}
function adler32(a: Uint8Array): number {
// `%` results stay ordinary numbers.
let x = 1,
y = 0;
for (let i = 0; i < a.length; i++) {
x = (x + a[i]) % 65521;
y = (y + x) % 65521;
}
return ((y << 16) | x) >>> 0;
}
function murmur3(a: Uint8Array, seed: number): number {
let h = seed | 0;
const n = a.length & ~3;
for (let i = 0; i < n; i += 4) {
let k = a[i] | (a[i + 1] << 8) | (a[i + 2] << 16) | (a[i + 3] << 24);
k = Math.imul(k, 0xcc9e2d51);
k = (k << 15) | (k >>> 17);
k = Math.imul(k, 0x1b873593);
h ^= k;
h = (h << 13) | (h >>> 19);
h = (Math.imul(h, 5) + 0xe6546b64) | 0;
}
h ^= a.length;
h ^= h >>> 16;
h = Math.imul(h, 0x85ebca6b);
h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35);
h ^= h >>> 16;
return h >>> 0;
}
function inet(a: Uint8Array): number {
let sum = 0;
for (let i = 0; i + 1 < a.length; i += 2) sum = (sum + ((a[i] << 8) | a[i + 1])) >>> 0;
while (sum >>> 16) sum = ((sum & 0xffff) + (sum >>> 16)) >>> 0;
return ~sum & 0xffff;
}
// PRNGs.
function xorshift32(seed: number, n: number): number {
let x = seed | 0,
acc = 0;
for (let i = 0; i < n; i++) {
x ^= x << 13;
x ^= x >>> 17;
x ^= x << 5;
acc = (acc + (x >>> 0)) >>> 0;
}
return acc;
}
function sfc32(a: number, b: number, c: number, d: number, n: number): number {
let total = 0;
for (let i = 0; i < n; i++) {
a |= 0;
b |= 0;
c |= 0;
d |= 0;
const t = (((a + b) | 0) + d) | 0;
d = (d + 1) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
c = (c + t) | 0;
total = (total + (t >>> 0)) >>> 0;
}
return total;
}
function mulberry32(seed: number): () => number {
// The state is captured by the generator closure.
let a = seed | 0;
return () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Counters crossing 2^31 through `| 0`, and uint32 counters above 2^31.
function wrapping(): void {
let c = 2147483645;
const seen: number[] = [];
for (let i = 0; i < 5; i++) {
c = (c + 1) | 0;
seen.push(c);
}
console.log("wrap up", seen.join(" "));
let d = -2147483646;
seen.length = 0;
for (let i = 0; i < 4; i++) {
d = (d - 1) | 0;
seen.push(d);
}
console.log("wrap down", seen.join(" "));
let u = 4294967290;
seen.length = 0;
for (let i = 0; i < 8; i++) {
u = (u + 1) >>> 0;
seen.push(u);
}
console.log("uint32 wrap", seen.join(" "));
let big = 0;
for (let i = 0; i < 40; i++) big = (big + 0x7654321f) >>> 0;
show("uint32 big", big);
show("uint32 half", big / 2);
show("uint32 string", Number(String(big)));
}
// ++ and -- on int32 counters, read as values.
function counters(limit: number): void {
let up = 0,
down = 10;
const log: string[] = [];
for (let i = 0; i < 3; i++) {
const before = up++;
const after = ++up;
log.push(`${before}/${after}`);
}
while (down > 6) log.push(String(down--));
let k = 0;
while (k < (limit | 0)) k++;
log.push(String(--k), String(k));
console.log("counters", log.join(" "));
}
// Locals that leave int32 keep their exact values.
function leaves(n: number): void {
let h = 0;
for (let i = 0; i < n; i++) h = (h + i) | 0;
h = h * 1.5;
show("fraction", h);
let z = 7 | 0;
if (n > 0) z = -0;
show("negative zero", z);
let m = 5 | 0;
for (let i = 0; i < n; i++) m = m % 2 === 0 ? m >>> 0 : m | 0;
show("mixed", m);
let both = -1 | 0;
if (n > 2) both = both >>> 0;
show("signed then unsigned", both);
let wide = 1 | 0;
for (let i = 0; i < 40; i++) wide = wide * 2;
show("past 2^32", wide);
let nan = 3 | 0;
if (n > 1) nan = (nan * 0) / 0;
show("nan", nan);
let divided = 1 | 0;
divided = (divided / 3) | 0;
show("divided", 1 / (divided - 0));
}
// Captured accumulators: the closure and the function share one binding.
function captured(list: string[]): void {
let h = 17;
const mixIn = (s: string): void => {
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
};
for (const w of list) mixIn(w);
let count = 0;
const bump = (): number => ++count;
for (let i = 0; i < 5; i++) bump();
const reads: (() => number)[] = [];
for (let i = 0; i < 3; i++) {
let local = (i * 7) | 0;
reads.push(() => local);
local = (local + 1) | 0;
}
console.log("captured", h, count, reads.map((r) => r()).join(" "));
}
// Generators and async functions keep their locals in frames.
function* hashes(list: string[]): Generator<number> {
let h = 0;
for (const w of list) {
for (let i = 0; i < w.length; i++) h = (Math.imul(h, 33) ^ w.charCodeAt(i)) | 0;
yield h;
}
}
async function asyncSum(list: string[]): Promise<number> {
let total = 0;
for (const w of list) {
await Promise.resolve();
total = (total + djb2(w)) | 0;
}
return total;
}
// Int32 contract locals and parameters.
function contract(seed: Int32, steps: Int32): Int32 {
let acc: Int32 = seed;
for (let i: Int32 = 0; i < steps; i = (i + 1) | 0) acc = (Math.imul(acc, 3) + i) | 0;
return acc;
}
const list = words(400);
let a = 0,
b = 0,
c = 0,
d = 0;
for (const w of list) {
a = (a + djb2(w)) | 0;
b = (b ^ javaHash(w)) | 0;
c = (c + sdbm(w)) | 0;
d = (d ^ fnv1a(w)) >>> 0;
}
console.log("string hashes", a, b, c, d);
console.log("samples", djb2("hello"), javaHash("hello"), sdbm("hello"), fnv1a("hello"));
console.log("empty", djb2(""), javaHash(""), sdbm(""), fnv1a(""));
const data = bytes(1 << 14, 7);
console.log("rsync", rsyncWeak(data), rolling(data, 64));
console.log("fletcher", fletcherWrap(data), "adler32", adler32(data));
console.log("murmur3", murmur3(data, 0), murmur3(data, -1), murmur3(data.subarray(0, 7), 42));
console.log("inet", inet(data), inet(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0x01])));
console.log("xorshift32", xorshift32(2463534242, 1000), xorshift32(1, 1));
console.log("sfc32", sfc32(1, 2, 3, 4, 10000), sfc32(-1, 0x7fffffff, -2147483648, 0, 3));
const next = mulberry32(42);
const draws: string[] = [];
for (let i = 0; i < 4; i++) draws.push(next().toFixed(6));
console.log("mulberry32", draws.join(" "));
wrapping();
counters(5);
leaves(4);
leaves(0);
captured(list.slice(0, 50));
console.log("generator", [...hashes(["ab", "cd", "efg"])].join(" "));
console.log("contract", contract(7, 20), contract(-2147483648, 3));
asyncSum(list).then((total) => console.log("async", total));
@@ -96,7 +96,7 @@ for (const backend of ["llvm"] as const) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 14, marker: "scr_runtime_abi_v14" },
runtime_abi: { version: 15, marker: "scr_runtime_abi_v15" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
flavors: {