mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Merge main into the docs rewrite
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4840-array-hole-free-prefix.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(""));
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user