mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Store local number arrays of proven int32s in 4-byte slots (#1920)
This commit is contained in:
@@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () =
|
||||
expect(found.has(idle)).toBe(false);
|
||||
expect(found.has(returned)).toBe(false);
|
||||
|
||||
// Only `fill` builds arrays.
|
||||
// Only `fill` builds arrays. They may hold int32 slots (element kind 7),
|
||||
// which does not change the choice of constructor.
|
||||
const llvm = emitLlvmModule(module(body, ["xs", "ys"]));
|
||||
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("a binding with the same id in another function does not count", () => {
|
||||
|
||||
@@ -242,9 +242,10 @@ function statePointer(
|
||||
return statePtr;
|
||||
}
|
||||
|
||||
/** How an array's dense slots hold its elements: the access kind, or
|
||||
* `cref` for references to compact class instances (SCR_ELEM_CREF: a 4-byte
|
||||
* slot holding the address over 8; LlEmitter.compactArrayElem). */
|
||||
/** How an array's dense slots hold its elements: the access kind (`i32`
|
||||
* for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact
|
||||
* class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8;
|
||||
* LlEmitter.compactArrayElem). */
|
||||
type SlotKind = ArrayAccess | "cref";
|
||||
|
||||
function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind {
|
||||
@@ -252,14 +253,14 @@ function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType):
|
||||
}
|
||||
|
||||
/** The dense slot type: boolean arrays store one byte per element, compact
|
||||
* references four, every other element kind an 8-byte slot. */
|
||||
* references and int32 arrays four, every other element kind an 8-byte slot. */
|
||||
function slotType(kind: SlotKind): "i8" | "i32" | "i64" {
|
||||
return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64";
|
||||
return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64";
|
||||
}
|
||||
|
||||
/** Bytes per dense slot (scr_arr_slot_width). */
|
||||
function slotWidth(kind: SlotKind): number {
|
||||
return kind === "bool" ? 1 : kind === "cref" ? 4 : 8;
|
||||
export function slotWidth(kind: SlotKind): number {
|
||||
return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8;
|
||||
}
|
||||
|
||||
/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */
|
||||
@@ -267,6 +268,17 @@ export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): numbe
|
||||
return slotWidth(slotKind(host, elemAccess(element), element));
|
||||
}
|
||||
|
||||
/** The runtime accessor family of an access: int32 arrays answer through
|
||||
* the double-valued entry points, which convert their slots. */
|
||||
export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" {
|
||||
return acc === "i32" ? "f64" : acc;
|
||||
}
|
||||
|
||||
/** The LLVM type of an access's values at the emitter's interface. */
|
||||
function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" {
|
||||
return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
}
|
||||
|
||||
function valuePointer(
|
||||
host: LlvmEmitterContext,
|
||||
guard: Pick<DenseGuard, "offset" | "data">,
|
||||
@@ -288,7 +300,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string {
|
||||
return state;
|
||||
}
|
||||
|
||||
export type ArrayAccess = "f64" | "bool" | "ref";
|
||||
/** The ScrElemKind of int32 arrays (scr_array.h). */
|
||||
export const SCR_ELEM_I32 = 7;
|
||||
|
||||
/** How a dense array's slots are read and written: doubles, booleans,
|
||||
* references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts),
|
||||
* whose values are doubles at the emitter's interface. */
|
||||
export type ArrayAccess = "f64" | "bool" | "ref" | "i32";
|
||||
|
||||
/** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference
|
||||
* element: when `index` lies in the hole-free prefix and the slot still
|
||||
@@ -344,15 +362,33 @@ export function emitDenseArrayGet(
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): string {
|
||||
return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven)
|
||||
.value;
|
||||
}
|
||||
|
||||
/** emitDenseArrayGet, with the int32 of an int32 array's element: the
|
||||
* slot itself on the dense path, the runtime getter's double converted
|
||||
* (exactly: the element is an int32) on the other. */
|
||||
export function emitDenseArrayRead(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
getter: string,
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): { value: string; int32: string | null } {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const ty = valueType(acc);
|
||||
const ret = acc === "bool" ? "zeroext i1" : ty;
|
||||
const kind = slotKind(host, acc, element);
|
||||
host.declare(`declare ${ret} @${getter}(ptr, double)`);
|
||||
if (acc === "ref" && retain && !isRefCounted(element)) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`);
|
||||
return t;
|
||||
return { value: t, int32: null };
|
||||
}
|
||||
const slow = B.newLabel("arr.get.slow"),
|
||||
join = B.newLabel("arr.get.join"),
|
||||
@@ -383,10 +419,16 @@ export function emitDenseArrayGet(
|
||||
B.startBlock(valueLabel);
|
||||
const ptr = valuePointer(host, guard, kind);
|
||||
let fast: string;
|
||||
let fastInt: string | null = null;
|
||||
let fastLabel = valueLabel;
|
||||
if (acc === "f64") {
|
||||
fast = B.tmp();
|
||||
B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
} else if (acc === "i32") {
|
||||
fastInt = B.tmp();
|
||||
fast = B.tmp();
|
||||
B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
B.line(`${fast} = sitofp i32 ${fastInt} to double`);
|
||||
} else if (acc === "bool") {
|
||||
const raw = B.tmp();
|
||||
fast = B.tmp();
|
||||
@@ -413,11 +455,21 @@ export function emitDenseArrayGet(
|
||||
const slowValue = B.tmp();
|
||||
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
|
||||
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
|
||||
let slowInt: string | null = null;
|
||||
if (fastInt !== null) {
|
||||
slowInt = B.tmp();
|
||||
B.line(`${slowInt} = fptosi double ${slowValue} to i32`);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`);
|
||||
return result;
|
||||
let int32: string | null = null;
|
||||
if (fastInt !== null && slowInt !== null) {
|
||||
int32 = B.tmp();
|
||||
B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`);
|
||||
}
|
||||
return { value: result, int32 };
|
||||
}
|
||||
|
||||
/** `scr_arr_get_number`: a numeric element, or NaN for every missing,
|
||||
@@ -428,13 +480,14 @@ export function emitDenseArrayNumber(
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
proven = false,
|
||||
acc: "f64" | "i32" = "f64",
|
||||
): string {
|
||||
return emitDenseArrayGet(
|
||||
host,
|
||||
array,
|
||||
index,
|
||||
indexExpr,
|
||||
"f64",
|
||||
acc,
|
||||
{ kind: "f64" },
|
||||
"scr_arr_get_number",
|
||||
false,
|
||||
@@ -501,9 +554,9 @@ export function emitDenseArraySet(
|
||||
proven = false,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const ty = valueType(acc);
|
||||
const kind = slotKind(host, acc, element);
|
||||
const setter = `scr_arr_set_${acc}`;
|
||||
const setter = `scr_arr_set_${runtimeAccess(acc)}`;
|
||||
host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
|
||||
const call = (attrs: string): void =>
|
||||
B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`);
|
||||
@@ -511,6 +564,13 @@ export function emitDenseArraySet(
|
||||
call("");
|
||||
return;
|
||||
}
|
||||
// An int32 array's stores are proven int32s (ir/int32-arrays.ts); both
|
||||
// dense paths below store the converted value.
|
||||
let int32Value: string | null = null;
|
||||
if (acc === "i32") {
|
||||
int32Value = B.tmp();
|
||||
B.line(`${int32Value} = fptosi double ${value} to i32`);
|
||||
}
|
||||
const slow = B.newLabel("arr.set.slow"),
|
||||
join = B.newLabel("arr.set.join"),
|
||||
filledLabel = B.newLabel("arr.set.filled");
|
||||
@@ -529,6 +589,8 @@ export function emitDenseArraySet(
|
||||
const storeValue = (valuePtr: string): void => {
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "i32") {
|
||||
B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "bool") {
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = zext i1 ${value} to i8`);
|
||||
@@ -716,12 +778,21 @@ export function emitDenseArrayPop(
|
||||
B.line(`${wide} = zext i8 ${raw} to i64`);
|
||||
raw = wide;
|
||||
} else if (slotTy === "i32") {
|
||||
// The 8-byte slot of a compact reference is its address.
|
||||
const wide = B.tmp(),
|
||||
address = B.tmp();
|
||||
B.line(`${wide} = zext i32 ${raw} to i64`);
|
||||
B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
|
||||
raw = address;
|
||||
if (kind === "cref") {
|
||||
// The 8-byte slot of a compact reference is its address.
|
||||
const wide = B.tmp(),
|
||||
address = B.tmp();
|
||||
B.line(`${wide} = zext i32 ${raw} to i64`);
|
||||
B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
|
||||
raw = address;
|
||||
} else {
|
||||
// The slot bits of an int32 element's double, as the f64 path returns them.
|
||||
const number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${number} = sitofp i32 ${raw} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
raw = bits;
|
||||
}
|
||||
}
|
||||
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
// Only an array with states has a state byte to clear.
|
||||
@@ -758,8 +829,8 @@ export function emitDenseArrayPush(
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const kind = element === null ? acc : slotKind(host, acc, element);
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const pusher = `scr_arr_push_${acc}`;
|
||||
const ty = valueType(acc);
|
||||
const pusher = `scr_arr_push_${runtimeAccess(acc)}`;
|
||||
host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`);
|
||||
const header = (field: number): string => {
|
||||
const ptr = B.tmp();
|
||||
@@ -797,6 +868,10 @@ export function emitDenseArrayPush(
|
||||
const valuePtr = valuePointer(host, { offset: len, data }, kind);
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "i32") {
|
||||
const int32 = B.tmp();
|
||||
B.line(`${int32} = fptosi double ${value} to i32`);
|
||||
B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "bool") {
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = zext i1 ${value} to i8`);
|
||||
|
||||
@@ -61,6 +61,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
// returns to a synchronous frame (emitPendingCheck).
|
||||
|
||||
@@ -411,7 +411,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const v = emitter.emitExpr(s.value);
|
||||
if (s.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
const acc = emitter.arrayAccess(s.arr, s.arr.type.elem);
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") emitter.toHeap(v);
|
||||
emitDenseArraySet(
|
||||
@@ -478,7 +478,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(dropsLast, pop, call);
|
||||
B.startBlock(pop);
|
||||
const elem = s.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const acc = emitter.arrayAccess(s.arr, elem);
|
||||
const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
|
||||
if (acc === "ref" && isRefCounted(elem)) {
|
||||
const old = B.tmp();
|
||||
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
} from "./emit-journal.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import { allocatedLiteral, selectInt32Arrays } from "../../ir/int32-arrays.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
@@ -147,7 +148,7 @@ import {
|
||||
} from "./inline-unions.js";
|
||||
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
|
||||
import { CheckedNarrows } from "./checked-narrows.js";
|
||||
import { emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
|
||||
import { emitDenseArraySet, emitDenseSlotUnlink, type ArrayAccess } from "./dense-array-access.js";
|
||||
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
@@ -483,6 +484,15 @@ function versionParameters(mod: IrModule): IrModule {
|
||||
return versionLoopBoundParameters(mod, mangleFunction);
|
||||
}
|
||||
|
||||
/** The int32 arrays the int32 slot analysis keeps in `mod`, under the
|
||||
* inputs LlEmitter gives it (selectInt32Arrays). */
|
||||
function provenInt32Arrays(mod: IrModule): ReadonlySet<string> {
|
||||
return analyzeInt32Slots(mod, {
|
||||
dynamicStores: dynamicFieldStores(mod),
|
||||
initializerBindings: findInitializerBindings(mod),
|
||||
}).int32Arrays;
|
||||
}
|
||||
|
||||
/** The emitter for `input` after the module passes every emission runs. */
|
||||
function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
@@ -491,28 +501,31 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
),
|
||||
provenInt32Arrays,
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
@@ -719,6 +732,53 @@ export class LlEmitter {
|
||||
return `, !tbaa !${tag}`;
|
||||
}
|
||||
|
||||
/** The current function's int32 array allocations: the array literals
|
||||
* its int32 arrays are written with. They construct SCR_ELEM_I32 arrays. */
|
||||
int32Allocations = new Set<IrExpr>();
|
||||
/** The fill temporaries (`new Array(n).fill(v)`) of those allocations:
|
||||
* the same arrays, so int32 too. */
|
||||
int32ArrayTemps = new Set<string>();
|
||||
|
||||
/** Collect the int32 allocations of `fn`, about to be emitted. */
|
||||
findInt32Allocations(fn: IrFunction): void {
|
||||
this.int32Allocations.clear();
|
||||
this.int32ArrayTemps.clear();
|
||||
if (this.int32Slots.int32Arrays.size === 0) return;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (
|
||||
(s.kind === "varDecl" || s.kind === "assign") &&
|
||||
this.int32Slots.int32Arrays.has(`${fn.name}\0${s.localId}`)
|
||||
) {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const allocation = value && allocatedLiteral(value);
|
||||
if (allocation) {
|
||||
this.int32Allocations.add(allocation.literal);
|
||||
if (allocation.temporary !== null) this.int32ArrayTemps.add(allocation.temporary);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
expr: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether `arr` is one of the current function's int32 arrays
|
||||
* (ir/int32-arrays.ts, kept by the int32 slot analysis). */
|
||||
int32Array(arr: IrExpr): boolean {
|
||||
if (arr.kind !== "varRef" || this.int32Slots.int32Arrays.size === 0) return false;
|
||||
return (
|
||||
this.int32ArrayTemps.has(arr.localId) ||
|
||||
this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** The access an element operation on `arr` (of element type `elem`)
|
||||
* takes: int32 arrays read and write their 4-byte slots. */
|
||||
arrayAccess(arr: IrExpr, elem: IrType): ArrayAccess {
|
||||
return this.int32Array(arr) ? "i32" : elemAccess(elem);
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
@@ -6507,7 +6567,7 @@ export class LlEmitter {
|
||||
return tagInSet(this, uName, unionId, tags);
|
||||
}
|
||||
|
||||
arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string {
|
||||
arrPush(arr: string, acc: ArrayAccess, value: string, element: IrType | null): string {
|
||||
return arrPush(this, arr, acc, value, element);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,10 @@ import {
|
||||
emitDenseArrayNumber,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArrayPush,
|
||||
runtimeAccess,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
@@ -115,7 +118,7 @@ export function tagInSet(
|
||||
export function arrPush(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
acc: ArrayAccess,
|
||||
value: string,
|
||||
element: IrType | null,
|
||||
): string {
|
||||
@@ -126,7 +129,7 @@ export function arrPush(
|
||||
export function emitArrayValues(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
values: LlValue[],
|
||||
prepend = false,
|
||||
@@ -142,12 +145,15 @@ export function emitArrayValues(
|
||||
}
|
||||
if (values.length === 1) {
|
||||
if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element);
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const flat = runtimeAccess(acc);
|
||||
const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr";
|
||||
host.declare(
|
||||
`declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`,
|
||||
`declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`,
|
||||
);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`);
|
||||
B.line(
|
||||
`${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
const capacity = Math.min(values.length, 64);
|
||||
@@ -165,8 +171,9 @@ export function emitArrayValues(
|
||||
const slot = B.tmp(),
|
||||
pointer = B.tmp();
|
||||
const value = values[start + i]!.name;
|
||||
// The runtime takes double bits for int32 arrays too, and converts.
|
||||
const pack =
|
||||
acc === "f64"
|
||||
acc === "f64" || acc === "i32"
|
||||
? `bitcast double ${value} to i64`
|
||||
: acc === "bool"
|
||||
? `zext i1 ${value} to i64`
|
||||
@@ -607,7 +614,12 @@ export function emitArrIntrinsic(
|
||||
if (e.receiver.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array");
|
||||
const elem = e.receiver.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
// Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others
|
||||
// take the runtime family's flat access.
|
||||
const access = host.arrayAccess(e.receiver, elem);
|
||||
if (access === "i32" && !INT32_ARRAY_METHODS.has(e.method))
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`);
|
||||
const acc = runtimeAccess(access);
|
||||
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const accArg = acc === "bool" ? "i1 zeroext" : accTy;
|
||||
const method = e.method;
|
||||
@@ -636,6 +648,7 @@ export function emitArrIntrinsic(
|
||||
index,
|
||||
e.args[0],
|
||||
host.provenDenseIndex(e.receiver, e.args[0]!),
|
||||
access === "i32" ? "i32" : "f64",
|
||||
),
|
||||
type: e.type,
|
||||
};
|
||||
@@ -672,7 +685,7 @@ export function emitArrIntrinsic(
|
||||
// no-op zero-argument call.
|
||||
const vs = e.args.map((a) => host.emitExpr(a));
|
||||
host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length));
|
||||
return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type };
|
||||
return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type };
|
||||
}
|
||||
case "pushSpread": {
|
||||
// `a.push(...src)`: append src's elements in order (borrowed src,
|
||||
@@ -746,7 +759,7 @@ export function emitArrIntrinsic(
|
||||
let slowLabel = "";
|
||||
if (method === "pop") {
|
||||
slowLabel = B.newLabel("remove.slow");
|
||||
denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel);
|
||||
denseRaw = emitDenseArrayPop(host, r.name, access, elem, la, slowLabel);
|
||||
denseLabel = B.currentLabel();
|
||||
B.br(lp);
|
||||
B.startBlock(slowLabel);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./integer-values.js";
|
||||
import {
|
||||
bitwiseIdentityOperand,
|
||||
INT32_RANGE,
|
||||
integerArithmeticRange,
|
||||
isBitwiseOp,
|
||||
withinInt32,
|
||||
@@ -33,7 +34,15 @@ import {
|
||||
emitPowerOfTwoRemainder,
|
||||
emitSignedIntegerRemainder,
|
||||
} from "./integer-remainder.js";
|
||||
import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
|
||||
import {
|
||||
denseSlotWidth,
|
||||
emitDenseArrayRead,
|
||||
emitDenseArrayState,
|
||||
runtimeAccess,
|
||||
SCR_ELEM_I32,
|
||||
slotWidth,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
|
||||
|
||||
export function emitLiteralExpr(
|
||||
@@ -894,9 +903,23 @@ export function emitContainerExpr(
|
||||
// the plain elements up to the next one (scr_arr_reserve_spread).
|
||||
const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i));
|
||||
const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread));
|
||||
B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
// An int32 array's allocation (ir/int32-arrays.ts) constructs
|
||||
// SCR_ELEM_I32 storage; its elements are proven int32s.
|
||||
const int32 = host.int32Allocations.has(e);
|
||||
if (int32) {
|
||||
host.declare(
|
||||
small
|
||||
? `declare ptr @scr_arr_new_small(i32)`
|
||||
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
|
||||
);
|
||||
B.line(
|
||||
small
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc = elemAccess(elem);
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
let buffer: string | undefined;
|
||||
const flush = (): void => {
|
||||
@@ -917,7 +940,8 @@ export function emitContainerExpr(
|
||||
B.line(
|
||||
`call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`,
|
||||
);
|
||||
host.emitArrayCopyLoop(arr, v.name, acc);
|
||||
// A literal with spreads is never an int32 allocation.
|
||||
host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc));
|
||||
return;
|
||||
}
|
||||
pending.push(host.emitExpr(el));
|
||||
@@ -962,19 +986,27 @@ export function emitContainerExpr(
|
||||
// Ref-element reads return +1 (the inline dense path retains like
|
||||
// the runtime getter); own registers the owned temp in the frame.
|
||||
const elem = e.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const t = emitDenseArrayGet(
|
||||
const acc = host.arrayAccess(e.arr, elem);
|
||||
const read = emitDenseArrayRead(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
acc,
|
||||
elem,
|
||||
`scr_arr_get_${acc}`,
|
||||
`scr_arr_get_${runtimeAccess(acc)}`,
|
||||
true,
|
||||
host.provenDenseIndex(e.arr, e.index),
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
if (read.int32 !== null)
|
||||
// An int32 array's element: its i32 is exact for index consumers.
|
||||
return {
|
||||
name: read.value,
|
||||
type: e.type,
|
||||
uint32: read.int32,
|
||||
integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE },
|
||||
};
|
||||
return host.own({ name: read.value, type: e.type });
|
||||
}
|
||||
case "arrayHas": {
|
||||
const arr = host.emitStableReceiver(e.arr, [e.index]);
|
||||
@@ -987,7 +1019,7 @@ export function emitContainerExpr(
|
||||
idx,
|
||||
e.index,
|
||||
"has",
|
||||
denseSlotWidth(host, e.arr.type.elem),
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -1002,7 +1034,7 @@ export function emitContainerExpr(
|
||||
idx,
|
||||
e.index,
|
||||
"state",
|
||||
denseSlotWidth(host, e.arr.type.elem),
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
@@ -476,7 +476,8 @@ function emitDenseReferenceArrayRead(
|
||||
no: string,
|
||||
slow: string,
|
||||
join: string,
|
||||
loadType: "ptr" | "cref" | "i64" | "i8" = "ptr",
|
||||
/** `i32`: an int32 array's slot, presented as the int32. */
|
||||
loadType: "ptr" | "cref" | "i64" | "i8" | "i32" = "ptr",
|
||||
): void {
|
||||
const B = host.B;
|
||||
const header = (field: number): string => {
|
||||
@@ -567,7 +568,7 @@ function emitDenseReferenceArrayRead(
|
||||
statePtr,
|
||||
data,
|
||||
cap,
|
||||
loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8,
|
||||
loadType === "i8" ? 1 : loadType === "cref" || loadType === "i32" ? 4 : 8,
|
||||
offset,
|
||||
);
|
||||
host.markMemoryPointer(statePtr, "array:present");
|
||||
@@ -578,9 +579,10 @@ function emitDenseReferenceArrayRead(
|
||||
B.startBlock(valueLabel);
|
||||
const valuePtr = B.tmp(),
|
||||
raw = B.tmp();
|
||||
// Boolean arrays store one byte per element, compact references four,
|
||||
// every other kind an 8-byte slot.
|
||||
const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64";
|
||||
// Boolean arrays store one byte per element, compact references and int32
|
||||
// arrays four, every other kind an 8-byte slot.
|
||||
const slotType =
|
||||
loadType === "i8" ? "i8" : loadType === "cref" || loadType === "i32" ? "i32" : "i64";
|
||||
B.line(
|
||||
`${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`,
|
||||
);
|
||||
@@ -740,8 +742,15 @@ export function emitLocalArrayRead(
|
||||
const no = B.newLabel("local.array.missing"),
|
||||
join = B.newLabel("local.array.join");
|
||||
const scalar = isScalarElement(read.element);
|
||||
// An int32 array's arms join on the int32 element, converted to the
|
||||
// payload's double once at the join, so a consumer converting the payload
|
||||
// back to an integer folds to the element.
|
||||
const int32 = scalar && read.element.kind === "f64" && host.int32Array(read.array);
|
||||
const element = int32 ? B.slot() : null;
|
||||
if (element) B.entryAllocas.push(`${element} = alloca i32`);
|
||||
const storeValue = (value: string): void => {
|
||||
if (scalar) {
|
||||
if (element) B.line(`store i32 ${value}, ptr ${element}`);
|
||||
else if (scalar) {
|
||||
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
|
||||
let bits = value;
|
||||
if (read.element.kind === "bool") {
|
||||
@@ -769,7 +778,13 @@ export function emitLocalArrayRead(
|
||||
no,
|
||||
slow,
|
||||
join,
|
||||
!scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64",
|
||||
!scalar
|
||||
? referenceLoad(host, read.array.type)
|
||||
: read.element.kind === "bool"
|
||||
? "i8"
|
||||
: int32
|
||||
? "i32"
|
||||
: "i64",
|
||||
);
|
||||
else B.br(slow);
|
||||
B.startBlock(slow);
|
||||
@@ -796,7 +811,8 @@ export function emitLocalArrayRead(
|
||||
B.line(
|
||||
`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
|
||||
);
|
||||
B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
if (element) B.line(`${bits} = fptosi double ${value} to i32`);
|
||||
else B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`);
|
||||
B.line(
|
||||
@@ -804,7 +820,8 @@ export function emitLocalArrayRead(
|
||||
);
|
||||
B.line(`${bits} = zext i1 ${value} to i64`);
|
||||
}
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
if (element) B.line(`store i32 ${bits}, ptr ${element}`);
|
||||
else B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
|
||||
} else {
|
||||
const value = B.tmp(),
|
||||
@@ -818,10 +835,20 @@ export function emitLocalArrayRead(
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(no);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
if (element) B.line(`store i32 0, ptr ${element}`);
|
||||
else B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store i32 ${read.missingTag}, ptr ${tag}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
if (element) {
|
||||
const value = B.tmp(),
|
||||
number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${value} = load i32, ptr ${element}`);
|
||||
B.line(`${number} = sitofp i32 ${value} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
}
|
||||
B.line(`store ptr ${box}, ptr ${localSlot}`);
|
||||
return read.borrow || scalar ? null : { slot: payload, type: read.element };
|
||||
}
|
||||
|
||||
@@ -208,6 +208,7 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"contracts.int32Returns[]": "fact",
|
||||
"contracts.int32Locals[]": "other",
|
||||
"contracts.int32Elements[]": "other",
|
||||
"int32ArrayCandidates[]": "other",
|
||||
"contracts.noThrow[]": "fact",
|
||||
"contracts.acyclic[]": "other",
|
||||
"contracts.immortalGlobals[]": "other",
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { reparseExpr } from "./serialize.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
transformExpr,
|
||||
transformStmt,
|
||||
transformStmtList,
|
||||
type IrTransform,
|
||||
} from "./traverse.js";
|
||||
|
||||
/** Local number arrays the backend may store as int32 slots.
|
||||
*
|
||||
* A candidate is a local `number[]` of a synchronous function that no
|
||||
* other binding can hold and only compiled element operations reach:
|
||||
* every write of the local stores a fresh array (an array literal, or
|
||||
* `new Array(n).fill(v)`), and every read of it is the receiver of an
|
||||
* element read, write, state test, `length`, `push` or `pop` (a discarded
|
||||
* `pop` included), or the array of an element-reader helper call. The
|
||||
* array never reaches a runtime function that reads its slots, a library
|
||||
* call, a closure, a return or untyped code.
|
||||
*
|
||||
* Candidates are only proposed here. The int32 slot analysis
|
||||
* (int32-slots.ts) keeps the ones whose every stored element is a proven
|
||||
* int32 (never -0), assuming the candidates' own elements are int32s, at
|
||||
* a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. Their
|
||||
* element-reader helper calls are inlined first (rewriteCandidates), so
|
||||
* no shared helper ever receives an int32 array. */
|
||||
|
||||
/** Array methods a candidate may be the receiver of: the runtime paths
|
||||
* behind them read and write int32 slots through the double-valued slot
|
||||
* accessors. */
|
||||
export const INT32_ARRAY_METHODS: ReadonlySet<string> = new Set([
|
||||
"length",
|
||||
"push",
|
||||
"pop",
|
||||
"getNumber",
|
||||
]);
|
||||
|
||||
/** An element-reader helper (`%arr.idxOr.*`): `(a, i) => a's state at i is
|
||||
* a value ? the element : undefined`, as one returned ternary. */
|
||||
export function isElementReader(fn: IrFunction): boolean {
|
||||
if (fn.params.length !== 2 || fn.body.length !== 1) return false;
|
||||
const [array, index] = fn.params;
|
||||
const ret = fn.body[0]!;
|
||||
if (array!.type.kind !== "array" || array!.type.elem.kind !== "f64") return false;
|
||||
if (index!.type.kind !== "f64" || ret.kind !== "return" || ret.value?.kind !== "ternary")
|
||||
return false;
|
||||
const isParam = (e: IrExpr, id: string): boolean => e.kind === "varRef" && e.localId === id;
|
||||
const { cond, then, else_ } = ret.value;
|
||||
return (
|
||||
cond.kind === "bin" &&
|
||||
cond.op === "===" &&
|
||||
cond.left.kind === "arrayState" &&
|
||||
isParam(cond.left.arr, array!.localId) &&
|
||||
isParam(cond.left.index, index!.localId) &&
|
||||
cond.right.kind === "numLit" &&
|
||||
cond.right.value === 1 &&
|
||||
then.kind === "unionWrap" &&
|
||||
then.value.kind === "arrayGet" &&
|
||||
isParam(then.value.arr, array!.localId) &&
|
||||
isParam(then.value.index, index!.localId) &&
|
||||
else_.kind === "unionWrap" &&
|
||||
else_.value.kind === "unitLit"
|
||||
);
|
||||
}
|
||||
|
||||
/** `new Array(n).fill(v)` as lowering spells it: `fill` over a sequence
|
||||
* that declares a temporary empty array, sets its length and yields it. */
|
||||
function fillAllocation(e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null {
|
||||
if (e.kind !== "arrIntrinsic" || e.method !== "fill" || e.receiver.kind !== "seqExpr")
|
||||
return null;
|
||||
const seq = e.receiver;
|
||||
const [decl, length] = seq.stmts;
|
||||
if (
|
||||
seq.stmts.length !== 2 ||
|
||||
decl?.kind !== "varDecl" ||
|
||||
decl.init?.kind !== "arrayLit" ||
|
||||
decl.init.elems.length !== 0 ||
|
||||
length?.kind !== "arraySetLength" ||
|
||||
length.arr.kind !== "varRef" ||
|
||||
length.arr.localId !== decl.localId ||
|
||||
seq.result.kind !== "varRef" ||
|
||||
seq.result.localId !== decl.localId
|
||||
)
|
||||
return null;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** A fresh number array: a literal without spreads, or the fill form. */
|
||||
export function int32ArrayAllocation(e: IrExpr): boolean {
|
||||
return (e.kind === "arrayLit" && (e.spreads?.length ?? 0) === 0) || fillAllocation(e) !== null;
|
||||
}
|
||||
|
||||
/** The array literal a candidate's allocation builds (the literal itself,
|
||||
* or the fill form's temporary's), and the fill form's temporary. */
|
||||
export function allocatedLiteral(
|
||||
e: IrExpr,
|
||||
): { literal: IrExpr & { kind: "arrayLit" }; temporary: string | null } | null {
|
||||
if (e.kind === "arrayLit") return { literal: e, temporary: null };
|
||||
const fill = fillAllocation(e);
|
||||
if (!fill || fill.receiver.kind !== "seqExpr") return null;
|
||||
const decl = fill.receiver.stmts[0]!;
|
||||
return decl.kind === "varDecl" && decl.init?.kind === "arrayLit"
|
||||
? { literal: decl.init, temporary: decl.localId }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `xs.pop()` whose value is unused, as lowering spells it: a sequence
|
||||
* that binds the array to a hidden local, then only reads that local's
|
||||
* length and truncates it with pop stores. Returns the hidden local and the
|
||||
* array read it binds. */
|
||||
function discardedPop(e: IrExpr): { local: string; array: IrExpr & { kind: "varRef" } } | null {
|
||||
if (e.kind !== "seqExpr") return null;
|
||||
const [bind, ...rest] = e.stmts;
|
||||
if (bind?.kind !== "varDecl" || bind.init?.kind !== "varRef") return null;
|
||||
const local = bind.localId;
|
||||
const isLocal = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === local;
|
||||
const expr = (x: IrExpr): boolean => {
|
||||
if (x.kind === "arrIntrinsic" && x.method === "length" && isLocal(x.receiver)) return true;
|
||||
return !isLocal(x) && everyExprChild(x, expr, stmt);
|
||||
};
|
||||
const stmt = (x: IrStmt): boolean => {
|
||||
if (x.kind === "arraySetLength" && x.pop === true && isLocal(x.arr)) return expr(x.length);
|
||||
return (
|
||||
!((x.kind === "varDecl" || x.kind === "assign") && x.localId === local) &&
|
||||
everyStmtChild(x, expr, stmt)
|
||||
);
|
||||
};
|
||||
return rest.every(stmt) && expr(e.result) ? { local, array: bind.init } : null;
|
||||
}
|
||||
|
||||
/** Whether evaluating `e` may assign the local `id`. */
|
||||
function writesLocal(e: IrExpr, id: string): boolean {
|
||||
const expr = (x: IrExpr): boolean =>
|
||||
!(x.kind === "assignExpr" && x.localId === id) && everyExprChild(x, expr, stmt);
|
||||
const stmt = (x: IrStmt): boolean =>
|
||||
!((x.kind === "varDecl" || x.kind === "assign") && x.localId === id) &&
|
||||
everyStmtChild(x, expr, stmt);
|
||||
return !expr(e);
|
||||
}
|
||||
|
||||
/** The candidate locals of `fn` (see the module comment). */
|
||||
export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string>): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
const captured = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
const params = new Set(fn.params.map((p) => p.localId));
|
||||
for (const local of fn.locals)
|
||||
if (
|
||||
local.type.kind === "array" &&
|
||||
local.type.elem.kind === "f64" &&
|
||||
!local.boxed &&
|
||||
!local.tdz &&
|
||||
!captured.has(local.id) &&
|
||||
!params.has(local.id)
|
||||
)
|
||||
result.add(local.id);
|
||||
if (result.size === 0) return result;
|
||||
const written = new Set<string>();
|
||||
// A receiver position: the candidate is read there without escaping.
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind !== "varRef") expr(e);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
result.delete(e.localId);
|
||||
return true;
|
||||
case "assignExpr":
|
||||
result.delete(e.localId);
|
||||
break;
|
||||
case "arrayGet":
|
||||
case "arrayState":
|
||||
case "arrayHas":
|
||||
receiver(e.arr);
|
||||
expr(e.index);
|
||||
return true;
|
||||
case "arrIntrinsic":
|
||||
if (!INT32_ARRAY_METHODS.has(e.method)) break;
|
||||
receiver(e.receiver);
|
||||
e.args.forEach(expr);
|
||||
return true;
|
||||
case "seqExpr": {
|
||||
// The pop's hidden local is the array itself; every use of it is a
|
||||
// length read or a pop store.
|
||||
const pop = discardedPop(e);
|
||||
if (!pop) break;
|
||||
e.stmts.slice(1).every(stmt);
|
||||
expr(e.result);
|
||||
return true;
|
||||
}
|
||||
case "call": {
|
||||
if (!readers.has(e.callee) || e.args.length !== 2) break;
|
||||
const [array, index] = e.args;
|
||||
// Inlining evaluates the index before the array read.
|
||||
if (array!.kind === "varRef" && writesLocal(index!, array!.localId))
|
||||
result.delete(array!.localId);
|
||||
receiver(array!);
|
||||
expr(index!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (value && int32ArrayAllocation(value)) written.add(s.localId);
|
||||
else if (value) result.delete(s.localId);
|
||||
if (value) expr(value);
|
||||
return true;
|
||||
}
|
||||
case "arraySet":
|
||||
receiver(s.arr);
|
||||
expr(s.index);
|
||||
expr(s.value);
|
||||
return true;
|
||||
case "arraySetUndefined":
|
||||
// Stores a present undefined: a state, and a cleared (+0) slot.
|
||||
receiver(s.arr);
|
||||
expr(s.index);
|
||||
return true;
|
||||
case "forOf":
|
||||
result.delete(s.localId);
|
||||
break;
|
||||
case "tryCatch":
|
||||
if (s.catchLocalId) result.delete(s.catchLocalId);
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
for (const id of result) if (!written.has(id)) result.delete(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The element values stored into each candidate: element writes, pushed
|
||||
* values, literal elements and fill values. */
|
||||
export function int32ArrayStores(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
): Map<string, IrExpr[]> {
|
||||
const stores = new Map<string, IrExpr[]>();
|
||||
const add = (id: string, value: IrExpr): void => {
|
||||
const list = stores.get(id);
|
||||
if (list) list.push(value);
|
||||
else stores.set(id, [value]);
|
||||
};
|
||||
const target = (e: IrExpr): string | null =>
|
||||
e.kind === "varRef" && candidates.has(e.localId) ? e.localId : null;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "arrIntrinsic" && e.method === "push") {
|
||||
const id = target(e.receiver);
|
||||
if (id !== null) for (const arg of e.args) add(id, arg);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if ((s.kind === "varDecl" || s.kind === "assign") && candidates.has(s.localId)) {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (value?.kind === "arrayLit") for (const el of value.elems) add(s.localId, el);
|
||||
else if (value) {
|
||||
const fill = fillAllocation(value);
|
||||
if (fill) add(s.localId, fill.args[0]!);
|
||||
}
|
||||
}
|
||||
if (s.kind === "arraySet") {
|
||||
const id = target(s.arr);
|
||||
if (id !== null) add(id, s.value);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
return stores;
|
||||
}
|
||||
|
||||
/** Rewrite a function's candidates for the backend. A discarded pop uses
|
||||
* the candidate in place of its hidden local. An element-reader helper call
|
||||
* whose array is a candidate is inlined: the helper's returned ternary, with
|
||||
* the candidate for its array and the index for its index, so the read means
|
||||
* the same. An index that is not a plain read or literal is bound to a fresh
|
||||
* local first: in its own declaration just before the statement when the
|
||||
* call is the whole value of a declaration or assignment (the form lowering
|
||||
* gives `a[i]!`, and the one the backend reads without a box), otherwise in a
|
||||
* sequence around the ternary. Selection keeps out indexes that write the
|
||||
* array, so evaluating the index before the array read is unobservable. */
|
||||
function rewriteCandidates(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
readers: ReadonlyMap<string, IrFunction>,
|
||||
): IrFunction {
|
||||
const locals = [...fn.locals];
|
||||
let fresh = 0;
|
||||
const bind = (index: IrExpr): { decl: IrStmt; ref: IrExpr } => {
|
||||
const id = `%int32.read.${fresh++}`;
|
||||
locals.push({ id, name: id, type: F64, mutable: false });
|
||||
return {
|
||||
decl: { kind: "varDecl", localId: id, init: index, loc: index.loc },
|
||||
ref: { kind: "varRef", localId: id, type: F64, loc: index.loc },
|
||||
};
|
||||
};
|
||||
const readerCall = (e: IrExpr | null): (IrExpr & { kind: "call" }) | null =>
|
||||
e?.kind === "call" &&
|
||||
e.args.length === 2 &&
|
||||
readers.has(e.callee) &&
|
||||
e.args[0]!.kind === "varRef" &&
|
||||
candidates.has(e.args[0]!.localId)
|
||||
? e
|
||||
: null;
|
||||
const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit";
|
||||
// The statements of a list, with complex read indexes declared first.
|
||||
const hoist = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return [s];
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const call = readerCall(value);
|
||||
if (!call || simple(call.args[1]!)) return [s];
|
||||
const { decl, ref } = bind(call.args[1]!);
|
||||
const read: IrExpr = { ...call, args: [call.args[0]!, ref] };
|
||||
return [decl, s.kind === "varDecl" ? { ...s, init: read } : { ...s, value: read }];
|
||||
});
|
||||
const lists = (s: IrStmt): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "if":
|
||||
return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) };
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "switch":
|
||||
return { ...s, cases: s.cases.map((c) => ({ ...c, body: hoist(c.body) })) };
|
||||
case "tryCatch":
|
||||
return {
|
||||
...s,
|
||||
tryBody: hoist(s.tryBody),
|
||||
catchBody: s.catchBody === null ? null : hoist(s.catchBody),
|
||||
finallyBody: s.finallyBody === null ? null : hoist(s.finallyBody),
|
||||
};
|
||||
}
|
||||
return s;
|
||||
};
|
||||
// Hidden pop locals replaced by their candidate array.
|
||||
const popLocals = new Set<string>();
|
||||
const rewrite = (e: IrExpr): IrExpr => {
|
||||
const pop = discardedPop(e);
|
||||
if (e.kind === "seqExpr" && pop && candidates.has(pop.array.localId)) {
|
||||
popLocals.add(pop.local);
|
||||
const array = pop.array;
|
||||
const substitute: IrTransform = {
|
||||
expr: (x) =>
|
||||
x.kind === "varRef" && x.localId === pop.local ? { ...array, loc: x.loc } : x,
|
||||
stmt: (x) => x,
|
||||
};
|
||||
return {
|
||||
...e,
|
||||
stmts: e.stmts.slice(1).map((x) => transformStmt(x, substitute)),
|
||||
result: transformExpr(e.result, substitute),
|
||||
};
|
||||
}
|
||||
const call = readerCall(e);
|
||||
if (!call) return e.kind === "seqExpr" ? { ...e, stmts: hoist(e.stmts) } : e;
|
||||
const helper = readers.get(call.callee)!;
|
||||
const array = call.args[0]!;
|
||||
const [arrayParam, indexParam] = helper.params;
|
||||
const ret = helper.body[0]!;
|
||||
if (ret.kind !== "return" || !ret.value) return e;
|
||||
let index = call.args[1]!;
|
||||
const stmts: IrStmt[] = [];
|
||||
if (!simple(index)) {
|
||||
const { decl, ref } = bind(index);
|
||||
stmts.push(decl);
|
||||
index = ref;
|
||||
}
|
||||
const operand = index;
|
||||
const body = transformExpr(reparseExpr(ret.value), {
|
||||
expr: (x) => {
|
||||
if (x.kind !== "varRef") return x;
|
||||
if (x.localId === arrayParam!.localId) return reparseExpr(array);
|
||||
if (x.localId === indexParam!.localId) return reparseExpr(operand);
|
||||
return x;
|
||||
},
|
||||
stmt: (x) => x,
|
||||
});
|
||||
const typed: IrExpr = { ...body, loc: e.loc };
|
||||
return stmts.length === 0
|
||||
? typed
|
||||
: { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc };
|
||||
};
|
||||
const body = transformStmtList(hoist(fn.body), { expr: rewrite, stmt: lists });
|
||||
if (fresh === 0 && popLocals.size === 0) return { ...fn, body };
|
||||
return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) };
|
||||
}
|
||||
|
||||
/** Propose the module's int32 array candidates (`function\0local` keys in
|
||||
* `int32ArrayCandidates`), only those in `keep` when given, and rewrite
|
||||
* their functions. */
|
||||
export function markInt32ArrayCandidates(
|
||||
mod: IrModule,
|
||||
keep: ReadonlySet<string> | null = null,
|
||||
): IrModule {
|
||||
if (mod.lib !== undefined) return mod;
|
||||
const readers = new Map<string, IrFunction>();
|
||||
for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn);
|
||||
const names = new Set(readers.keys());
|
||||
const keys: string[] = [];
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const candidates = int32ArrayCandidates(fn, names);
|
||||
for (const id of candidates)
|
||||
if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`);
|
||||
else candidates.delete(id);
|
||||
return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers);
|
||||
});
|
||||
return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys };
|
||||
}
|
||||
|
||||
/** The module the backend emits: rewritten for exactly the candidates the
|
||||
* int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a
|
||||
* module without int32 arrays is `mod` itself. A dropped candidate's
|
||||
* rewrite is undone by marking again without it, until every candidate
|
||||
* marked is kept. */
|
||||
export function selectInt32Arrays(
|
||||
mod: IrModule,
|
||||
prove: (marked: IrModule) => ReadonlySet<string>,
|
||||
): IrModule {
|
||||
let keep: ReadonlySet<string> | null = null;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
const marked = markInt32ArrayCandidates(mod, keep);
|
||||
if (marked === mod) return mod;
|
||||
const kept = prove(marked);
|
||||
const proposed = marked.int32ArrayCandidates ?? [];
|
||||
if (proposed.every((key) => kept.has(key))) return marked;
|
||||
keep = new Set(proposed.filter((key) => kept.has(key)));
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "./integer-ranges.js";
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
|
||||
import { int32ArrayStores } from "./int32-arrays.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js";
|
||||
|
||||
/** The storage slot an array-valued expression reads, as an
|
||||
@@ -169,7 +170,10 @@ export class Int32Slots {
|
||||
this.int32Locals.has(`${fnName}\0${localId}`) || this.int32Locals.has(`\0${localId}`)
|
||||
? INT32_RANGE
|
||||
: null;
|
||||
if (this.int32Elements.size > 0) result.elements = (arr) => this.hasInt32Elements(fnName, arr);
|
||||
if (this.int32Elements.size > 0) {
|
||||
result.elements = (arr) => this.hasInt32Elements(fnName, arr);
|
||||
if (this.narrowedReads) result.narrowedReads = true;
|
||||
}
|
||||
const params = this.paramFacts.get(fnName);
|
||||
if (params)
|
||||
result.params =
|
||||
@@ -234,6 +238,19 @@ export class Int32Slots {
|
||||
addInt32Elements(key: string): void {
|
||||
this.int32Elements.add(key);
|
||||
}
|
||||
dropInt32Elements(key: string): boolean {
|
||||
return this.int32Elements.delete(key);
|
||||
}
|
||||
hasInt32ElementSlot(key: string): boolean {
|
||||
return this.int32Elements.has(key);
|
||||
}
|
||||
/** Whether the module has int32 array candidates (the narrowedReads
|
||||
* range fact). */
|
||||
narrowedReads = false;
|
||||
/** Local number arrays (`function\0local`) stored as int32 slots: the
|
||||
* module's int32 array candidates whose every stored element the
|
||||
* fixpoint proved an int32. */
|
||||
readonly int32Arrays = new Set<string>();
|
||||
/** Whether every present element of the number array `arr` evaluates to
|
||||
* (read inside `fnName`) is an int32. */
|
||||
hasInt32Elements(fnName: string, arr: IrExpr): boolean {
|
||||
@@ -752,6 +769,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
}
|
||||
for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key);
|
||||
for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key);
|
||||
// Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32
|
||||
// elements until a store the fixpoint cannot prove drops one.
|
||||
const arrayCandidates = new Map<string, Set<string>>();
|
||||
slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0;
|
||||
for (const key of mod.int32ArrayCandidates ?? []) {
|
||||
const cut = key.indexOf("\0");
|
||||
const fnName = key.slice(0, cut);
|
||||
const set = arrayCandidates.get(fnName) ?? new Set<string>();
|
||||
set.add(key.slice(cut + 1));
|
||||
arrayCandidates.set(fnName, set);
|
||||
slots.addInt32Elements(`l:${key}`);
|
||||
}
|
||||
|
||||
/* ── candidate fields ── */
|
||||
const children = new Map<string, string[]>();
|
||||
@@ -1031,6 +1060,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
return false;
|
||||
};
|
||||
const hasCandidateWrite = (fn: IrFunction): boolean => {
|
||||
if (arrayCandidates.has(fn.name)) return true;
|
||||
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
|
||||
const returns = slots.hasReturn(fn.name);
|
||||
return !everyStmtList(fn.body, {
|
||||
@@ -1076,6 +1106,25 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
continue;
|
||||
}
|
||||
const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e));
|
||||
// Every element stored into an int32 array candidate must be an int32
|
||||
// under the facts that assume the candidates' own elements are; a
|
||||
// failing store drops its array, and the function is analyzed again
|
||||
// without it.
|
||||
const arrays = arrayCandidates.get(fn.name);
|
||||
if (arrays) {
|
||||
let dropped = false;
|
||||
for (const [id, values] of int32ArrayStores(fn, arrays)) {
|
||||
const key = `l:${fn.name}\0${id}`;
|
||||
if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue;
|
||||
slots.dropInt32Elements(key);
|
||||
reason(`array:${fn.name}\0${id}`, "an element store is not int32");
|
||||
dropped = true;
|
||||
}
|
||||
if (dropped && !queued.has(fn.name)) {
|
||||
queued.add(fn.name);
|
||||
queue.push(fn.name);
|
||||
}
|
||||
}
|
||||
// An int32 written into a slot still assumed non-negative: widen the
|
||||
// assumption when the value may be negative, and reanalyze its readers.
|
||||
const settle = (slot: string, e: IrExpr): void => {
|
||||
@@ -1144,6 +1193,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
}
|
||||
for (const proven of provenByFunction.values())
|
||||
for (const value of proven) slots.provenWrites.add(value);
|
||||
for (const [fnName, ids] of arrayCandidates)
|
||||
for (const id of ids)
|
||||
if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`))
|
||||
slots.int32Arrays.add(`${fnName}\0${id}`);
|
||||
const ranged = rangedParameters(slots, functions, retry, numeric, (callee) =>
|
||||
readers.get(`ret:${callee}`),
|
||||
);
|
||||
|
||||
@@ -220,6 +220,11 @@ export interface IntegerSlotFacts {
|
||||
/** Number arrays whose every present element is an int32 (Int32[]
|
||||
* contracts): an element read that found a value reads an int32. */
|
||||
elements?: (arr: IrExpr) => boolean;
|
||||
/** Element reads narrowed out of a union (`xs[i]!`, also when a union
|
||||
* local holds the read) keep the element's range, and a ternary arm that
|
||||
* always throws yields no value. Only modules with int32 array candidates
|
||||
* (ir/int32-arrays.ts) set it, so no other program's ranges change. */
|
||||
narrowedReads?: boolean;
|
||||
}
|
||||
|
||||
/** A local read, or a numeric local plus or minus an integer literal
|
||||
@@ -240,6 +245,50 @@ function offsetRef(e: IrExpr): { localId: string; offset: number } | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Library calls that always throw. */
|
||||
const THROWING_LIB = /^error\.\w*Throw$/;
|
||||
|
||||
function throws(e: IrExpr): boolean {
|
||||
return e.kind === "libCall" && THROWING_LIB.test(e.fn);
|
||||
}
|
||||
|
||||
/** The values written to each union-typed local of `fn` by declarations
|
||||
* and assignments; null for a local that another write reaches or that a
|
||||
* closure captures. */
|
||||
function unionLocalWrites(
|
||||
fn: IrFunction,
|
||||
captures: ReadonlySet<string>,
|
||||
): Map<string, IrExpr[] | null> {
|
||||
const unionLocals = new Set(
|
||||
fn.locals
|
||||
.filter((l) => l.type.kind === "union" && !l.boxed && !captures.has(l.id))
|
||||
.map((l) => l.id),
|
||||
);
|
||||
const result = new Map<string, IrExpr[] | null>();
|
||||
const add = (id: string, value: IrExpr | null): void => {
|
||||
if (!unionLocals.has(id)) return;
|
||||
const list = result.get(id);
|
||||
if (list === null) return;
|
||||
if (value === null) result.set(id, null);
|
||||
else if (list) list.push(value);
|
||||
else result.set(id, [value]);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" && s.init) add(s.localId, s.init);
|
||||
else if (s.kind === "assign") add(s.localId, s.value);
|
||||
else if (s.kind === "forOf") add(s.localId, null);
|
||||
else if (s.kind === "tryCatch" && s.catchLocalId) add(s.catchLocalId, null);
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr") add(e.localId, null);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Structured local dataflow. Calls cannot write uncaptured local slots,
|
||||
* but argument expressions can; unknown evaluation forms invalidate all
|
||||
* syntactically written slots before their children are inspected. Loops
|
||||
@@ -272,6 +321,10 @@ export function analyzeIntegerRangeFacts(
|
||||
.map((l) => l.id),
|
||||
);
|
||||
const loopLocals = new Map(fn.locals.filter((l) => !captures.has(l.id)).map((l) => [l.id, l]));
|
||||
// With narrowed reads, the values written to union locals (an element
|
||||
// read held before it is narrowed), or null for a local some other write
|
||||
// (assignExpr, for-of, catch, capture) reaches.
|
||||
const unionWrites = slots.narrowedReads ? unionLocalWrites(fn, captures) : null;
|
||||
// A captured binding has no local dataflow: only a proof that covers
|
||||
// every write of the shared box anywhere applies to its reads.
|
||||
const boxedRange = (localId: string): IntegerRange | null => {
|
||||
@@ -490,6 +543,45 @@ export function analyzeIntegerRangeFacts(
|
||||
return range;
|
||||
}
|
||||
|
||||
/** narrowedRange through a union local: every value written to it. */
|
||||
function narrowedValue(
|
||||
value: IrExpr,
|
||||
unionId: string,
|
||||
tag: number,
|
||||
): IntegerRange | null | "never" {
|
||||
if (value.kind !== "varRef") return narrowedRange(value, unionId, tag);
|
||||
const written = unionWrites?.get(value.localId);
|
||||
if (!written || written.length === 0) return null;
|
||||
let result: IntegerRange | null | "never" = "never";
|
||||
for (const w of written) {
|
||||
const arm = narrowedRange(w, unionId, tag);
|
||||
if (arm === "never") continue;
|
||||
if (arm === null) return null;
|
||||
result = result === "never" ? arm : union(result, arm);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** The range of the values of union tag `tag` that `value` (already
|
||||
* analyzed) can hold: "never" when no arm holds that tag. */
|
||||
function narrowedRange(
|
||||
value: IrExpr,
|
||||
unionId: string,
|
||||
tag: number,
|
||||
): IntegerRange | null | "never" {
|
||||
if (value.kind === "seqExpr") return narrowedRange(value.result, unionId, tag);
|
||||
if (value.kind === "ternary") {
|
||||
const yes = narrowedRange(value.then, unionId, tag),
|
||||
no = narrowedRange(value.else_, unionId, tag);
|
||||
if (yes === "never") return no;
|
||||
if (no === "never") return yes;
|
||||
return yes && no ? union(yes, no) : null;
|
||||
}
|
||||
if (value.kind === "unionWrap" && value.unionId === unionId) {
|
||||
if (value.tag !== tag) return "never";
|
||||
return value.value.kind === "arrayGet" ? (ranges.get(value.value) ?? null) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null {
|
||||
// Shared IR objects have to satisfy the proof at every occurrence.
|
||||
const previous = ranges.get(e);
|
||||
@@ -683,6 +775,10 @@ export function analyzeIntegerRangeFacts(
|
||||
right = expr(e.else_, no);
|
||||
merge(facts, yes, no);
|
||||
if (left && right) range = union(left, right);
|
||||
// With narrowed reads: an arm that always throws yields no value,
|
||||
// so the other arm's range is the result's.
|
||||
else if (slots.narrowedReads && throws(e.then)) range = right;
|
||||
else if (slots.narrowedReads && throws(e.else_)) range = left;
|
||||
break;
|
||||
}
|
||||
case "logical": {
|
||||
@@ -777,27 +873,45 @@ export function analyzeIntegerRangeFacts(
|
||||
expr(e.obj, facts);
|
||||
range = slotRange(e);
|
||||
break;
|
||||
case "unionNarrow":
|
||||
// Only modules with narrowed reads take this rule: the narrow
|
||||
// evaluates its operand first, and throws for every arm of another
|
||||
// tag, so an element read `xs[i]!` (a ternary of the wrapped
|
||||
// element and a wrapped undefined) has the element's range.
|
||||
if (!slots.narrowedReads || e.type.kind !== "f64") {
|
||||
range = conservative(e, facts);
|
||||
break;
|
||||
}
|
||||
expr(e.value, facts);
|
||||
{
|
||||
const arm = narrowedValue(e.value, e.unionId, e.tag);
|
||||
range = arm === "never" ? null : arm;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Do not assume the traversal order is an evaluation order for an
|
||||
// unknown node. Every child gets the same conservative entry state.
|
||||
havoc(facts, [e], facts);
|
||||
everyExprChild(
|
||||
e,
|
||||
(child) => {
|
||||
expr(child, new Map(facts));
|
||||
return true;
|
||||
},
|
||||
(child) => {
|
||||
body([child], new Map(facts));
|
||||
return true;
|
||||
},
|
||||
);
|
||||
// A direct call's result carries its whole-program return proof.
|
||||
range = slotRange(e);
|
||||
range = conservative(e, facts);
|
||||
break;
|
||||
}
|
||||
return remember(e, range);
|
||||
}
|
||||
/** An unknown node: do not assume the traversal order is an evaluation
|
||||
* order. Every child gets the same conservative entry state. */
|
||||
function conservative(e: IrExpr, facts: Facts): IntegerRange | null {
|
||||
havoc(facts, [e], facts);
|
||||
everyExprChild(
|
||||
e,
|
||||
(child) => {
|
||||
expr(child, new Map(facts));
|
||||
return true;
|
||||
},
|
||||
(child) => {
|
||||
body([child], new Map(facts));
|
||||
return true;
|
||||
},
|
||||
);
|
||||
// A direct call's result carries its whole-program return proof.
|
||||
return slotRange(e);
|
||||
}
|
||||
function induction(
|
||||
s: IrStmt & { kind: "for" },
|
||||
facts: Facts,
|
||||
|
||||
@@ -1073,6 +1073,11 @@ export interface IrModule {
|
||||
* write in the program (ir/contract-check.ts). Backends may rely on them
|
||||
* unconditionally. Absent when the program writes no contract. */
|
||||
contracts?: IrContracts;
|
||||
/** Local number arrays (`function\0local`) the backend may store as
|
||||
* int32 slots, proposed by ir/int32-arrays.ts after their element-reader
|
||||
* calls were inlined; the int32 slot analysis keeps the ones whose every
|
||||
* stored element it proves an int32. Absent when there is none. */
|
||||
int32ArrayCandidates?: string[];
|
||||
/** The WebAssembly modules translated into this program (wasm/
|
||||
* translate.ts), present exactly when the program uses the WebAssembly
|
||||
* API (possibly empty). The backend appends each module's translation
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { mkdtemp, readFile, rm, 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 type { IrModule } from "../src/ir/ir.js";
|
||||
import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function lowerIr(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-"));
|
||||
try {
|
||||
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" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const source = `
|
||||
function compose(n: number, rounds: number): number {
|
||||
const p: number[] = [];
|
||||
const q: number[] = [];
|
||||
for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1));
|
||||
for (let i = 0; i < n; i++) q.push(i);
|
||||
let check = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let i = 0; i < n; i++) q[i] = p[q[i]!]!;
|
||||
check = (check + q[r % n]!) | 0;
|
||||
}
|
||||
return check;
|
||||
}
|
||||
function counts(n: number): number {
|
||||
const c = new Array(n).fill(0) as number[];
|
||||
for (let i = 0; i < n; i++) c[(i * 5) % n] = i;
|
||||
while (c.length > 1) c.pop();
|
||||
return c[0]!;
|
||||
}
|
||||
function fraction(n: number): number {
|
||||
const a = [1, 2, 3];
|
||||
a[1] = n / 3;
|
||||
return a[1]!;
|
||||
}
|
||||
function negativeZero(): number {
|
||||
const a = [1, -0];
|
||||
return 1 / a[1]!;
|
||||
}
|
||||
function counter(n: number): number {
|
||||
const a = [0];
|
||||
for (let i = 0; i < n; i++) a[0] = a[0]! + 1;
|
||||
return a[0]!;
|
||||
}
|
||||
function joined(n: number): string {
|
||||
const a = [n | 0, 2];
|
||||
return a.join(",");
|
||||
}
|
||||
const { n } = JSON.parse('{"n": 8}') as { n: number };
|
||||
console.log(compose(n | 0, 3), counts(n | 0), fraction(n), negativeZero(), counter(n), joined(n));
|
||||
`;
|
||||
|
||||
/** Every function emitted for the source function \`name\` (its int32 ABI
|
||||
* body and its wrapper included). */
|
||||
function functions(llvm: string, name: string): string {
|
||||
const pattern = new RegExp(`^define [^@]*@sc_b?f_${name}[.\\w]*\\([^]*?^}`, "gm");
|
||||
return [...llvm.matchAll(pattern)].map((m) => m[0]).join("\n");
|
||||
}
|
||||
|
||||
test("arrays storing only int32 elements get 4-byte slots", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const compose = functions(llvm, "compose");
|
||||
expect(compose).not.toBe("");
|
||||
// Both arrays are allocated as SCR_ELEM_I32 (7).
|
||||
expect(compose.match(/@scr_arr_new_small\(i32 7\)/g)?.length).toBe(2);
|
||||
// Reads load an i32 slot; writes store one.
|
||||
expect(compose).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/);
|
||||
expect(compose).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/);
|
||||
// The element read through the helper's ternary stays in a stack box.
|
||||
expect(compose).not.toContain("@scr_union_new_f64");
|
||||
expect(compose).not.toMatch(/call [^\n]*@sc_b?f__x25_arr_idxOr/);
|
||||
// The fill form and a discarded pop.
|
||||
const counts = functions(llvm, "counts");
|
||||
expect(counts).toMatch(/@scr_arr_new\(i32 7, i64 0\)/);
|
||||
expect(counts).not.toContain("popArray");
|
||||
});
|
||||
|
||||
test("arrays with a store that may not be an int32, or that escape, keep doubles", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
for (const name of ["fraction", "negativeZero", "counter", "joined"]) {
|
||||
const fn = functions(llvm, name);
|
||||
expect(fn, name).not.toBe("");
|
||||
expect(fn, name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/);
|
||||
}
|
||||
});
|
||||
|
||||
test("only the candidates the proof keeps are rewritten", async () => {
|
||||
const mod = await lowerIr(source);
|
||||
const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? [];
|
||||
expect(proposed).toEqual(
|
||||
expect.arrayContaining(["compose\0p.0", "compose\0q.0", "counts\0c.0", "counter\0a.0"]),
|
||||
);
|
||||
// An array that reaches `join` is never proposed.
|
||||
expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false);
|
||||
// Without a kept candidate, the module is the input itself.
|
||||
expect(selectInt32Arrays(mod, () => new Set())).toBe(mod);
|
||||
const kept = new Set(["compose\0p.0", "compose\0q.0"]);
|
||||
const selected = selectInt32Arrays(
|
||||
mod,
|
||||
(marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))),
|
||||
);
|
||||
expect(selected.int32ArrayCandidates).toEqual([...kept]);
|
||||
for (const fn of selected.functions)
|
||||
if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name));
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7326-int32-element-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -277,6 +277,7 @@ typedef struct {
|
||||
static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) {
|
||||
if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index];
|
||||
if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]);
|
||||
if (s->elem == SCR_ELEM_I32) return scr_arr_f64_slot((double)((const int32_t *)s->data)[index]);
|
||||
return s->data[index];
|
||||
}
|
||||
|
||||
@@ -621,7 +622,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode);
|
||||
* paths pick the same capacity. */
|
||||
static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) {
|
||||
if (need <= a->cap) return;
|
||||
if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF ||
|
||||
if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || a->elem == SCR_ELEM_I32 ||
|
||||
(a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) {
|
||||
scr_arr_grow_dense_slow(a, need, mode);
|
||||
return;
|
||||
@@ -1904,6 +1905,10 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state,
|
||||
if (state != SCR_ARR_VALUE) scr_arr_cut(a, from);
|
||||
if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from);
|
||||
else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot);
|
||||
else if (a->elem == SCR_ELEM_I32) {
|
||||
int32_t v = scr_arr_i32_of(slot);
|
||||
for (size_t i = from; i < until; i++) ((int32_t *)a->data)[i] = v;
|
||||
}
|
||||
else for (size_t i = from; i < until; i++) a->data[i] = slot;
|
||||
if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from);
|
||||
} else {
|
||||
|
||||
@@ -48,6 +48,12 @@ typedef enum {
|
||||
* address over 8, 0 for NULL. Compact instances are never counted or
|
||||
* traced, so these arrays retain, release and trace nothing. */
|
||||
SCR_ELEM_CREF,
|
||||
/* Numbers that the compiler proves are always int32 (never -0), in
|
||||
* 4-byte slots: arrays that only compiled element accesses ever reach
|
||||
* (backend/llvm int32 arrays). Every runtime slot is the value's double
|
||||
* bits, as for SCR_ELEM_F64: scr_arr_dense_get and scr_arr_dense_put
|
||||
* convert, so the paths compiled code calls need no case of their own. */
|
||||
SCR_ELEM_I32,
|
||||
} ScrElemKind;
|
||||
|
||||
typedef enum {
|
||||
@@ -134,11 +140,12 @@ typedef struct ScrArr {
|
||||
* 0 is always valid. */
|
||||
uint32_t filled;
|
||||
/* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or
|
||||
* `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in
|
||||
* the same allocation, when the array has states (SCR_ARR_STATES), by one
|
||||
* state byte per slot in reverse slot order (scr_arr_states_end). Sparse
|
||||
* entries and properties keep 8-byte slots for every kind; a bool slot is
|
||||
* 0 or 1 wherever it lives, and a CREF slot's 8-byte form is the pointer
|
||||
* `cap` 4-byte slots for SCR_ELEM_CREF and SCR_ELEM_I32
|
||||
* (scr_arr_slot_width), followed in the same allocation, when the array
|
||||
* has states (SCR_ARR_STATES), by one state byte per slot in reverse slot
|
||||
* order (scr_arr_states_end). Sparse entries and properties keep 8-byte
|
||||
* slots for every kind; a bool slot is 0 or 1 wherever it lives, a CREF
|
||||
* slot's 8-byte form is the pointer and an I32 slot's the double's bits
|
||||
* (scr_arr_dense_get). Rarely
|
||||
* used storage (sparse indices, noncanonical numeric properties,
|
||||
* match-result metadata) lives out of line, and an array that has it
|
||||
@@ -260,7 +267,7 @@ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) {
|
||||
/* Bytes per dense value slot of element kind `elem`. */
|
||||
static inline size_t scr_elem_slot_width(uint8_t elem) {
|
||||
if (elem == SCR_ELEM_BOOL) return 1;
|
||||
if (__builtin_expect(elem == SCR_ELEM_CREF, 0)) return sizeof(uint32_t);
|
||||
if (__builtin_expect(elem == SCR_ELEM_CREF || elem == SCR_ELEM_I32, 0)) return sizeof(uint32_t);
|
||||
return sizeof(uint64_t);
|
||||
}
|
||||
|
||||
@@ -325,11 +332,38 @@ static inline void scr_arr_need_states(ScrArr *a) {
|
||||
static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; }
|
||||
static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); }
|
||||
|
||||
/* The bits of `d` as a slot, and back. */
|
||||
static inline uint64_t scr_arr_f64_slot(double d) {
|
||||
uint64_t slot;
|
||||
memcpy(&slot, &d, sizeof slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
static inline double scr_arr_slot_f64(uint64_t slot) {
|
||||
double d;
|
||||
memcpy(&d, &slot, sizeof d);
|
||||
return d;
|
||||
}
|
||||
|
||||
/* An int32 array's slot for a double: its stores are proven int32s that
|
||||
* are not -0 (and a cleared slot is +0), so anything else is a compiler
|
||||
* bug. */
|
||||
static inline int32_t scr_arr_i32_of(uint64_t slot) {
|
||||
double d = scr_arr_slot_f64(slot);
|
||||
if (__builtin_expect(!(d >= -2147483648.0 && d <= 2147483647.0 && d == (double)(int32_t)d) ||
|
||||
slot == UINT64_C(0x8000000000000000),
|
||||
0))
|
||||
__builtin_trap();
|
||||
return (int32_t)d;
|
||||
}
|
||||
|
||||
/* The dense value at `index` (< cap) as an 8-byte slot, and its store. */
|
||||
static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) {
|
||||
if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index];
|
||||
if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
|
||||
return scr_cref_decode(((const uint32_t *)a->data)[index]);
|
||||
if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
|
||||
return scr_arr_f64_slot((double)((const int32_t *)a->data)[index]);
|
||||
return a->data[index];
|
||||
}
|
||||
|
||||
@@ -337,6 +371,8 @@ static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) {
|
||||
if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot;
|
||||
else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
|
||||
((uint32_t *)a->data)[index] = scr_cref_encode(slot);
|
||||
else if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
|
||||
((int32_t *)a->data)[index] = scr_arr_i32_of(slot);
|
||||
else a->data[index] = slot;
|
||||
}
|
||||
|
||||
|
||||
@@ -1614,6 +1614,64 @@ static bool inline_slots(const ScrArr *a) {
|
||||
return a->data == (const uint64_t *)((const char *)a + ((sizeof(ScrArr) + 7) & ~(size_t)7));
|
||||
}
|
||||
|
||||
/* Int32 slots: 4-byte values, then the state bytes when the array has them. */
|
||||
static bool i32_layout_ok(const ScrArr *a) {
|
||||
return a->elem == SCR_ELEM_I32 && scr_arr_slot_width(a) == sizeof(int32_t);
|
||||
}
|
||||
|
||||
/* SCR_ELEM_I32: 4-byte int32 slots behind the double-valued entry points. */
|
||||
static void test_i32_slots(void) {
|
||||
ScrArr *a = scr_arr_new_small(SCR_ELEM_I32);
|
||||
for (int i = 0; i < 1000; i++) scr_arr_push_f64(a, (double)(i * 7 - 3000));
|
||||
check(i32_layout_ok(a) && !scr_arr_has_states(a) && a->filled == 1000,
|
||||
"int32 pushes grow four-byte storage without states");
|
||||
bool values_ok = true;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
values_ok &= ((const int32_t *)a->data)[i] == i * 7 - 3000;
|
||||
values_ok &= scr_arr_get_f64(a, i) == (double)(i * 7 - 3000);
|
||||
values_ok &= scr_arr_get_number(a, i) == (double)(i * 7 - 3000);
|
||||
}
|
||||
check(values_ok, "int32 values are stored as 4-byte integers and read as doubles");
|
||||
scr_arr_set_f64(a, 10, 2147483647);
|
||||
scr_arr_set_f64(a, 11, -2147483648.0);
|
||||
check(scr_arr_get_f64(a, 10) == 2147483647 && scr_arr_get_f64(a, 11) == -2147483648.0,
|
||||
"int32 extremes round-trip");
|
||||
check(scr_arr_pop_f64(a) == 999 * 7 - 3000 && scr_arr_len(a) == 999, "pop reads the last int32");
|
||||
scr_arr_set_len(a, 5);
|
||||
scr_arr_set_len(a, 8);
|
||||
check(i32_layout_ok(a) && scr_arr_has_states(a) && scr_arr_state(a, 6) == SCR_ARR_HOLE &&
|
||||
isnan(scr_arr_get_number(a, 6)) && scr_arr_get_f64(a, 4) == 4 * 7 - 3000,
|
||||
"int32 truncation clears slots and extension adds holes");
|
||||
scr_arr_set_f64(a, 20, 5);
|
||||
check(scr_arr_len(a) == 21 && scr_arr_state(a, 15) == SCR_ARR_HOLE && scr_arr_get_f64(a, 20) == 5,
|
||||
"an int32 store past the length leaves holes");
|
||||
scr_arr_release(scr_arr_fill_f64(a, -9, 0, 21));
|
||||
bool filled_ok = a->filled == 21;
|
||||
for (int i = 0; i < 21; i++) filled_ok &= scr_arr_get_f64(a, i) == -9;
|
||||
check(filled_ok, "int32 fill writes every slot");
|
||||
scr_arr_release(a);
|
||||
|
||||
ScrArr *s = scr_arr_new(SCR_ELEM_I32, 0);
|
||||
scr_arr_set_f64(s, 0, 1);
|
||||
scr_arr_set_f64(s, 3000000, 3);
|
||||
scr_arr_set_f64(s, 2000000, -2);
|
||||
check(arr_sparse_len(s) == 2 && scr_arr_get_f64(s, 2000000) == -2 && scr_arr_get_f64(s, 3000000) == 3,
|
||||
"int32 sparse entries keep double slots");
|
||||
scr_arr_set_len(s, 2000001);
|
||||
scr_arr_release(scr_arr_fill_f64(s, 4, 1, 2000000));
|
||||
check(arr_sparse_len(s) == 0 && i32_layout_ok(s) && scr_arr_get_f64(s, 2000000) == -2 &&
|
||||
scr_arr_get_f64(s, 1999999) == 4 && scr_arr_get_f64(s, 0) == 1,
|
||||
"densifying absorbs sparse entries into int32 slots");
|
||||
scr_arr_release(s);
|
||||
|
||||
ScrArr *k = scr_arr_new(SCR_ELEM_I32, 6);
|
||||
for (int i = 0; i < 6; i++) scr_arr_push_f64(k, i);
|
||||
scr_arr_push_f64(k, 6);
|
||||
check(i32_layout_ok(k) && scr_arr_get_f64(k, 6) == 6 && scr_arr_get_f64(k, 0) == 0,
|
||||
"a known-count int32 array grows out of its block");
|
||||
scr_arr_release(k);
|
||||
}
|
||||
|
||||
static void test_inline_slots(void) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
long strings0 = scr_str_live_count();
|
||||
@@ -2472,6 +2530,7 @@ int main(int argc, char **argv) {
|
||||
test_splice_drop();
|
||||
test_filled_prefix();
|
||||
test_bool_bytes();
|
||||
test_i32_slots();
|
||||
test_inline_slots();
|
||||
test_head_room();
|
||||
test_states_on_demand();
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// Local number arrays whose every stored element is an int32: reads, writes,
|
||||
// holes, present undefined, out-of-range and noncanonical indexes, growth,
|
||||
// pop, the fill form, reassignment, and arrays that do not qualify.
|
||||
|
||||
function compose(n: number, rounds: number): number {
|
||||
const p: number[] = [];
|
||||
const q: number[] = [];
|
||||
for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1));
|
||||
for (let i = 0; i < n; i++) q.push(i);
|
||||
let check = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let i = 0; i < n; i++) q[i] = p[q[i]!]!;
|
||||
check = (check + q[r % n]!) | 0;
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
function insertionSort(n: number): string {
|
||||
const a: number[] = [];
|
||||
let seed = 12345;
|
||||
for (let i = 0; i < n; i++) {
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
a.push((seed % 2001) - 1000);
|
||||
}
|
||||
for (let i = 1; i < n; i++) {
|
||||
for (let j = i; j > 0 && a[j - 1]! > a[j]!; j--) {
|
||||
const t = a[j]!;
|
||||
a[j] = a[j - 1]!;
|
||||
a[j - 1] = t;
|
||||
}
|
||||
}
|
||||
let sorted = true;
|
||||
for (let i = 1; i < n; i++) if (a[i - 1]! > a[i]!) sorted = false;
|
||||
return `${sorted} ${a[0]} ${a[n >> 1]} ${a[n - 1]} ${a.length}`;
|
||||
}
|
||||
|
||||
function reads(): string[] {
|
||||
const a = [7, -3, 2147483647, -2147483648, 0];
|
||||
const out: string[] = [];
|
||||
out.push(`${a[0]} ${a[1]} ${a[2]} ${a[3]} ${a[4]} ${a[5]}`);
|
||||
out.push(`${a[2]! + 1} ${a[3]! - 1} ${a[1]! / 2} ${a[2]! * 2}`);
|
||||
const idx = [0, 4, 5, -1, 1.5, NaN, -0, 2 ** 32, Infinity];
|
||||
for (const i of idx) out.push(`${i}: ${a[i]} ${a[i] === undefined} ${i in a}`);
|
||||
out.push(`len ${a.length}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
function holes(): string[] {
|
||||
const a = [1, 2, 3];
|
||||
const out: string[] = [];
|
||||
a[6] = 9;
|
||||
out.push(`len ${a.length} ${a[3]} ${a[6]} ${4 in a} ${6 in a}`);
|
||||
a[4] = -5;
|
||||
let sum = 0,
|
||||
present = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== undefined) {
|
||||
sum += a[i]!;
|
||||
present++;
|
||||
}
|
||||
}
|
||||
out.push(`sum ${sum} present ${present}`);
|
||||
// A hole read through a non-null assertion stores a present undefined.
|
||||
const q = [10, 20, 30];
|
||||
q[1] = a[3]!;
|
||||
out.push(`q ${q[0]} ${q[1]} ${q[2]} ${1 in q} ${q.length}`);
|
||||
q[1] = 21;
|
||||
out.push(`q ${q[1]} ${(q[0]! + q[1]!) | 0}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
function sparse(): string[] {
|
||||
const a = [1, 2];
|
||||
const out: string[] = [];
|
||||
a[100000] = 42;
|
||||
a[3] = 4;
|
||||
out.push(`len ${a.length} ${a[100000]} ${a[3]} ${a[2]} ${a[99999]}`);
|
||||
a[100000] = -42;
|
||||
out.push(`${a[100000]} ${a[100001]}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
function growth(n: number): string[] {
|
||||
const a: number[] = [];
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < n; i++) a.push((i * i) | 0);
|
||||
out.push(`len ${a.length} ${a[0]} ${a[n - 1]} ${a[n]}`);
|
||||
let sum = 0;
|
||||
while (a.length > n / 2) sum = (sum + a.pop()!) | 0;
|
||||
out.push(`popped ${sum} len ${a.length} last ${a[a.length - 1]}`);
|
||||
for (let i = 0; i < 3; i++) a.push(i - 3);
|
||||
out.push(`len ${a.length} ${a[a.length - 3]} ${a[a.length - 1]}`);
|
||||
while (a.length > 0) a.pop();
|
||||
out.push(`empty ${a.length} ${a.pop()} ${a[0]}`);
|
||||
a.push(5);
|
||||
out.push(`again ${a.length} ${a[0]}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
function filled(n: number): string[] {
|
||||
const counts = new Array(n).fill(0) as number[];
|
||||
const marks: number[] = new Array(n).fill(-1);
|
||||
for (let i = 0; i < 3 * n; i++) {
|
||||
const k = ((i * 37) % n) | 0;
|
||||
counts[k] = (i * 3 - 7) | 0;
|
||||
marks[(k + 1) % n] = k;
|
||||
}
|
||||
let a = 0,
|
||||
b = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
a = (a + counts[i]!) | 0;
|
||||
b = (b + marks[i]!) | 0;
|
||||
}
|
||||
return [`fill ${counts.length} ${a} ${b} ${counts[n]} ${marks[0]}`];
|
||||
}
|
||||
|
||||
function reassigned(rounds: number): string {
|
||||
let a = [1, 2, 3];
|
||||
let total = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
if (r % 3 === 0) a = [r, r + 1];
|
||||
else a.push(r);
|
||||
total = (total + a[a.length - 1]! + a.length) | 0;
|
||||
}
|
||||
return `reassigned ${total} ${a.length} ${a[0]}`;
|
||||
}
|
||||
|
||||
// These arrays do not qualify; they print the same values as before.
|
||||
function disqualified(): string[] {
|
||||
const out: string[] = [];
|
||||
const negZero = [0, -0];
|
||||
out.push(`${Object.is(negZero[1], -0)} ${1 / negZero[1]!}`);
|
||||
const fraction = [1, 2];
|
||||
fraction[1] = 2.5;
|
||||
out.push(`${fraction[1]}`);
|
||||
const big = [1];
|
||||
big[0] = big[0]! + 2147483647;
|
||||
out.push(`${big[0]}`);
|
||||
const joined = [3, 1, 2];
|
||||
joined.sort((x, y) => x - y);
|
||||
out.push(joined.join(","));
|
||||
const escaped = [4, 5];
|
||||
const alias = escaped;
|
||||
alias[0] = 6;
|
||||
out.push(`${escaped[0]} ${alias[1]}`);
|
||||
const counter = [0];
|
||||
for (let i = 0; i < 10; i++) counter[0] = counter[0]! + 1;
|
||||
out.push(`${counter[0]}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
const config = JSON.parse('{"n": 256, "rounds": 50, "sort": 300}') as {
|
||||
n: number;
|
||||
rounds: number;
|
||||
sort: number;
|
||||
};
|
||||
console.log(compose(config.n | 0, config.rounds | 0));
|
||||
console.log(insertionSort(config.sort | 0));
|
||||
for (const line of reads()) console.log(line);
|
||||
for (const line of holes()) console.log(line);
|
||||
for (const line of sparse()) console.log(line);
|
||||
for (const line of growth(config.n | 0)) console.log(line);
|
||||
for (const line of filled(17)) console.log(line);
|
||||
console.log(reassigned(config.rounds | 0));
|
||||
for (const line of disqualified()) console.log(line);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3700, "san": 4100 }
|
||||
Reference in New Issue
Block a user