mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Inline number-key Map/Set get and has
Number-key reads called scr_map_get_f64_ref and friends, which hashed the key and retained reference values through the map's val_retain function pointer. They now go through sc_map_entry_f64, a small internal IR helper LLVM inlines at each site: with the runtime's direct index present, an integral in-range key reads its slot and any other key is absent, with no call; maps without the index defer to scr_map_entry_f64. The generated code reads the entry's value slot itself and retains reference values with the value type's own (inlinable) retain, the adapter the collection was constructed with. Map.get with stack-boxed results, union and scalar values, and Map/Set.has all use it. The helper and its %ScrMapIx/%ScrMapEntry types follow the target's size type, so wasm32 builds verify as well.
This commit is contained in:
@@ -290,6 +290,7 @@ import { LlWalkers } from "./walkers.js";
|
||||
import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
|
||||
import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { literalCapWord } from "./string-key-hash.js";
|
||||
import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
|
||||
import {
|
||||
arrNewCall,
|
||||
boxAccess,
|
||||
@@ -1662,6 +1663,15 @@ export class LlEmitter {
|
||||
...(this.rcHelpers === null
|
||||
? []
|
||||
: [`%ScrMapRc = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr }`]),
|
||||
// The complete ScrMap and its { key, val, hash } entry, read by the
|
||||
// inline number-key lookup (sc_map_entry_f64): entries is field 12,
|
||||
// the direct index and its length fields 18 and 19.
|
||||
...(this.decls.has(NUMBER_MAP_ENTRY_DECL)
|
||||
? [
|
||||
`%ScrMapIx = type { ${this.sizeType}, i32, i32, ptr, ptr, ptr, ptr, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ${this.sizeType}, ptr, ptr, ptr, ${this.sizeType} }`,
|
||||
`%ScrMapEntry = type { i64, i64, i64 }`,
|
||||
]
|
||||
: []),
|
||||
// The runtime error prefix { rc, vt, name, message, code, cause } and the
|
||||
// class-object shape { rc, pre, post, ctor, name } — field reads on
|
||||
// builtin errors and classval loads GEP through these.
|
||||
@@ -2550,6 +2560,55 @@ export class LlEmitter {
|
||||
``,
|
||||
);
|
||||
}
|
||||
// Number-key Map/Set lookups probe the runtime's direct index inline:
|
||||
// while ndense is nonzero every live key is an integer below it, so an
|
||||
// in-range integral key reads its slot (entry index + 1, 0 = absent) and
|
||||
// any other key is absent. Maps without the index (ndense 0) and every
|
||||
// non-integral probe outside the table take the runtime lookup, which
|
||||
// keeps the complete SameValueZero semantics.
|
||||
if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
|
||||
const sz = this.sizeType;
|
||||
defs.push(
|
||||
`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 19`,
|
||||
` %nd = load ${sz}, ptr %ndp`,
|
||||
` %ndf = uitofp ${sz} %nd to double`,
|
||||
` %lo = fcmp oge double %k, 0.0`,
|
||||
` %hi = fcmp olt double %k, %ndf`,
|
||||
` %in = and i1 %lo, %hi`,
|
||||
` br i1 %in, label %direct, label %outside`,
|
||||
`direct:`,
|
||||
` %i = fptoui double %k to ${sz}`,
|
||||
` %back = uitofp ${sz} %i to double`,
|
||||
` %int = fcmp oeq double %back, %k`,
|
||||
` br i1 %int, label %slot, label %miss`,
|
||||
`slot:`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 18`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 12`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne ${sz} %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %r = call ptr @scr_map_entry_f64(ptr %m, double %k)`,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
// String comparisons enter through small internal wrappers that LLVM
|
||||
// inlines at each site: interned literals and shared values compare by
|
||||
// identity, and equality rejects different byte lengths, before any
|
||||
|
||||
@@ -21,7 +21,13 @@ import {
|
||||
emitMapLookupKey,
|
||||
borrowsMapReadInputs,
|
||||
borrowsMapMutationReceiver,
|
||||
inlineNumberKey,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import {
|
||||
emitNumberMapGetRef,
|
||||
emitNumberMapGetScalar,
|
||||
emitNumberMapHas,
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitStringSliceRead } from "./string-slices.js";
|
||||
import { emitDenseArrayNumber, emitDenseArrayPush } from "./dense-array-access.js";
|
||||
@@ -991,12 +997,18 @@ export function emitMapLikeIntrinsic(
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
const numberKey = inlineNumberKey(k);
|
||||
if (value.kind === "union") {
|
||||
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
|
||||
const raw = B.tmp();
|
||||
const isnull = B.tmp();
|
||||
const t = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_${kAcc}_ref(ptr ${r.name}, ${k.args})`);
|
||||
let raw: string;
|
||||
if (numberKey !== null) {
|
||||
raw = emitNumberMapGetRef(host, r.name, numberKey, value);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
|
||||
raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_${kAcc}_ref(ptr ${r.name}, ${k.args})`);
|
||||
}
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.line(`${t} = select i1 ${isnull}, ptr ${absent}, ptr ${raw}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
@@ -1006,16 +1018,40 @@ export function emitMapLikeIntrinsic(
|
||||
throw new InternalCompilerError("llvm emitter bug: map get union lacks its value arm");
|
||||
if (value.kind === "f64" || value.kind === "bool") {
|
||||
const outTy = value.kind === "f64" ? "double" : "i8";
|
||||
const outSlot = B.slot();
|
||||
B.entryAllocas.push(`${outSlot} = alloca ${outTy}`);
|
||||
B.line(`store ${outTy} ${value.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`);
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_map_get_${kAcc}_${value.kind === "f64" ? "f64" : "bool"}(ptr, ${kTy}, ptr)`,
|
||||
);
|
||||
const found = B.tmp();
|
||||
B.line(
|
||||
`${found} = call zeroext i1 @scr_map_get_${kAcc}_${value.kind === "f64" ? "f64" : "bool"}(ptr ${r.name}, ${k.args}, ptr ${outSlot})`,
|
||||
);
|
||||
let found: string;
|
||||
let readHit: () => string;
|
||||
if (numberKey !== null) {
|
||||
const scalar = emitNumberMapGetScalar(host, r.name, numberKey);
|
||||
found = scalar.found;
|
||||
readHit = () => {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
value.kind === "f64"
|
||||
? `${hit} = bitcast i64 ${scalar.bits} to double`
|
||||
: `${hit} = icmp ne i64 ${scalar.bits}, 0`,
|
||||
);
|
||||
return hit;
|
||||
};
|
||||
} else {
|
||||
const outSlot = B.slot();
|
||||
B.entryAllocas.push(`${outSlot} = alloca ${outTy}`);
|
||||
B.line(`store ${outTy} ${value.kind === "f64" ? f64Lit(0) : "0"}, ptr ${outSlot}`);
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_map_get_${kAcc}_${value.kind === "f64" ? "f64" : "bool"}(ptr, ${kTy}, ptr)`,
|
||||
);
|
||||
found = B.tmp();
|
||||
B.line(
|
||||
`${found} = call zeroext i1 @scr_map_get_${kAcc}_${value.kind === "f64" ? "f64" : "bool"}(ptr ${r.name}, ${k.args}, ptr ${outSlot})`,
|
||||
);
|
||||
readHit = () => {
|
||||
const rawOut = B.tmp();
|
||||
B.line(`${rawOut} = load ${outTy}, ptr ${outSlot}`);
|
||||
if (value.kind !== "bool") return rawOut;
|
||||
const hit = B.tmp();
|
||||
B.line(`${hit} = trunc i8 ${rawOut} to i1`);
|
||||
return hit;
|
||||
};
|
||||
}
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const lp = B.newLabel("mg.p");
|
||||
@@ -1023,13 +1059,7 @@ export function emitMapLikeIntrinsic(
|
||||
const lj = B.newLabel("mg.j");
|
||||
B.condBr(found, lp, la);
|
||||
B.startBlock(lp);
|
||||
const rawOut = B.tmp();
|
||||
B.line(`${rawOut} = load ${outTy}, ptr ${outSlot}`);
|
||||
let hit = rawOut;
|
||||
if (value.kind === "bool") {
|
||||
hit = B.tmp();
|
||||
B.line(`${hit} = trunc i8 ${rawOut} to i1`);
|
||||
}
|
||||
const hit = readHit();
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valueTag, { name: hit, type: value })}, ptr ${slot}`,
|
||||
);
|
||||
@@ -1042,6 +1072,10 @@ export function emitMapLikeIntrinsic(
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (numberKey !== null && isRefCounted(value)) {
|
||||
const raw = emitNumberMapGetRef(host, r.name, numberKey, value);
|
||||
return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag);
|
||||
}
|
||||
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_${kAcc}_ref(ptr ${r.name}, ${k.args})`);
|
||||
@@ -1068,6 +1102,7 @@ export function emitMapLikeIntrinsic(
|
||||
}
|
||||
case "has": {
|
||||
const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
||||
if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call zeroext i1 @scr_map_has_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`);
|
||||
|
||||
@@ -109,7 +109,7 @@ test("conditional and reassigned locals own payload snapshots without heap wrapp
|
||||
expect(facts(mod).get("item")?.ownerType).toEqual(STRING);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("@scr_map_get_f64_ref");
|
||||
expect(ir).toContain("call ptr @sc_map_entry_f64("); // inline number-key read
|
||||
expect(ir).toContain("@scr_str_release");
|
||||
expect(ir).not.toMatch(/@scr_union_(?:new|retain|release)/);
|
||||
expect(ir).toContain("store i64 0");
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* Inline number-key Map/Set reads.
|
||||
*
|
||||
* Number-keyed collections whose keys are small non-negative integers keep a
|
||||
* direct index in the runtime (scr_map.c). Reads go through the internal
|
||||
* sc_map_entry_f64 helper (emitter.ts helperDefs), which LLVM inlines: it
|
||||
* resolves directly indexed maps without a call and defers to
|
||||
* scr_map_entry_f64 otherwise. The generated code then reads the entry's
|
||||
* value slot itself and retains a reference value with the value type's own
|
||||
* retain, which is the adapter the collection was constructed with.
|
||||
*/
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)";
|
||||
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation. */
|
||||
export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
host.declare(NUMBER_MAP_ENTRY_DECL);
|
||||
const entry = host.B.tmp();
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Load a found entry's value slot as `type` (double, i64 or ptr). */
|
||||
export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type: string): string {
|
||||
const B = host.B;
|
||||
const slot = B.tmp(),
|
||||
value = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 1`);
|
||||
B.line(`${value} = load ${type}, ptr ${slot}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Map.get of a reference value: a +1 value, or null when absent — the
|
||||
* contract of scr_map_get_f64_ref. */
|
||||
export function emitNumberMapGetRef(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
value: IrType,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const entry = emitNumberMapEntry(host, map, key);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const hit = B.newLabel("mapn.hit"),
|
||||
miss = B.newLabel("mapn.miss"),
|
||||
join = B.newLabel("mapn.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const retained = host.retainValue(emitNumberMapSlot(host, entry, "ptr"), value);
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ptr [ ${retained}, %${hit} ], [ null, %${miss} ]`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Map.get of a scalar value: the found flag, and the raw 64-bit slot
|
||||
* (zero when absent). Booleans are stored as 0/1. */
|
||||
export function emitNumberMapGetScalar(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
): { found: string; bits: string } {
|
||||
const B = host.B;
|
||||
const entry = emitNumberMapEntry(host, map, key);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const hit = B.newLabel("mapn.hit"),
|
||||
miss = B.newLabel("mapn.miss"),
|
||||
join = B.newLabel("mapn.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const loaded = emitNumberMapSlot(host, entry, "i64");
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = phi i64 [ ${loaded}, %${hit} ], [ 0, %${miss} ]`);
|
||||
return { found, bits };
|
||||
}
|
||||
|
||||
/** Map.has / Set.has. */
|
||||
export function emitNumberMapHas(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
const entry = emitNumberMapEntry(host, map, key);
|
||||
const found = host.B.tmp();
|
||||
host.B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
return found;
|
||||
}
|
||||
@@ -94,19 +94,26 @@ function body(mod: IrModule, bits: 32 | 64 = 64): string {
|
||||
}
|
||||
|
||||
test("local map results keep scalar and owned reference payloads without heap wrappers on both ABIs", () => {
|
||||
for (const value of [F64, BOOL, STRING]) {
|
||||
const mod = fixture(value);
|
||||
expect(facts(mod).locals.has("value")).toBe(true);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("alloca %ScrUnion");
|
||||
expect(ir).toContain("select i1");
|
||||
expect(ir).not.toContain("@scr_union_new");
|
||||
expect(ir).not.toContain("@scr_union_release");
|
||||
if (value === BOOL) expect(ir).toMatch(/zext i8 %\w+ to i64/);
|
||||
if (value === STRING) expect(ir).toContain("@scr_str_release");
|
||||
const keys: IrType[] = [F64, STRING];
|
||||
const values: IrType[] = [F64, BOOL, STRING];
|
||||
for (const key of keys)
|
||||
for (const value of values) {
|
||||
const mod = fixture(value, key);
|
||||
expect(facts(mod).locals.has("value")).toBe(true);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("alloca %ScrUnion");
|
||||
expect(ir).toContain("select i1");
|
||||
expect(ir).not.toContain("@scr_union_new");
|
||||
expect(ir).not.toContain("@scr_union_release");
|
||||
if (key === F64) {
|
||||
// Number keys read the entry's slot inline; booleans are stored 0/1.
|
||||
expect(ir).toContain("call ptr @sc_map_entry_f64(");
|
||||
expect(ir).not.toContain("@scr_map_get_");
|
||||
} else if (value === BOOL) expect(ir).toMatch(/zext i8 %\w+ to i64/);
|
||||
if (value === STRING) expect(ir).toContain("@scr_str_release");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("nullable record results are the looked-up pointer itself on both ABIs", () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitNumberMapGetRef, emitNumberMapGetScalar } from "./map-number-lookup.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
|
||||
function hasTypedCollectionReceiver(
|
||||
@@ -144,7 +145,7 @@ export function emitMapLookupKey(
|
||||
host: LlvmEmitterContext,
|
||||
key: IrExpr,
|
||||
borrow: boolean,
|
||||
): { access: string; types: string; args: string } {
|
||||
): { access: string; types: string; args: string; value?: string } {
|
||||
const span =
|
||||
borrow && key.kind === "varRef" && key.type.kind === "string"
|
||||
? host.splitSpans.get(key.localId)
|
||||
@@ -158,7 +159,12 @@ export function emitMapLookupKey(
|
||||
const value = borrow ? host.emitReadReceiver(key) : host.emitExpr(key);
|
||||
const access = mapKeyAccess(key.type);
|
||||
const type = mapKeyParamType(access);
|
||||
return { access, types: type, args: `${type} ${value.name}` };
|
||||
return { access, types: type, args: `${type} ${value.name}`, value: value.name };
|
||||
}
|
||||
|
||||
/** The key's double operand when the lookup can take the inline number path. */
|
||||
export function inlineNumberKey(key: { access: string; value?: string }): string | null {
|
||||
return key.access === "f64" && key.value !== undefined ? key.value : null;
|
||||
}
|
||||
|
||||
export interface StackMapRead {
|
||||
@@ -182,9 +188,23 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
B.entryAllocas.push(`${box} = alloca %ScrUnion`);
|
||||
B.entryAllocas.push(`${payload} = getelementptr inbounds %ScrUnion, ptr ${box}, i32 0, i32 5`);
|
||||
B.entryAllocas.push(`${tag} = getelementptr inbounds %ScrUnion, ptr ${box}, i32 0, i32 1`);
|
||||
const found = B.tmp();
|
||||
let found = B.tmp();
|
||||
let owner: StackMapRead["owner"] = null;
|
||||
if (isRefCounted(read.value)) {
|
||||
const number = inlineNumberKey(key);
|
||||
if (number !== null) {
|
||||
if (isRefCounted(read.value)) {
|
||||
const raw = emitNumberMapGetRef(host, receiver.name, number, read.value);
|
||||
B.line(`${found} = icmp ne ptr ${raw}, null`);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store ptr ${raw}, ptr ${payload}`);
|
||||
owner = { slot: payload, type: read.value };
|
||||
} else {
|
||||
// Booleans are stored as 0/1, already the widened union payload.
|
||||
const scalar = emitNumberMapGetScalar(host, receiver.name, number);
|
||||
found = scalar.found;
|
||||
B.line(`store i64 ${scalar.bits}, ptr ${payload}`);
|
||||
}
|
||||
} else if (isRefCounted(read.value)) {
|
||||
const raw = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`);
|
||||
B.line(`${raw} = call ptr @scr_map_get_${access}_ref(ptr ${receiver.name}, ${key.args})`);
|
||||
|
||||
Reference in New Issue
Block a user