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:
Malte Ubl
2026-10-09 10:10:21 -07:00
parent 3955982b41
commit 56bc1745cd
6 changed files with 251 additions and 37 deletions
@@ -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})`);