Merge main into fix/narrowed-property-calls

This commit is contained in:
Chris Tate
2026-10-11 22:29:21 -05:00
10 changed files with 213 additions and 7 deletions
@@ -301,7 +301,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, literalCapWord32 } from "./string-key-hash.js";
import { NUMBER_MAP_ENTRY_DECL } from "./map-number-lookup.js";
import { NUMBER_MAP_ENTRY_DECL, STRING_MAP_ENTRY_DECL } from "./map-number-lookup.js";
import { STRING_READ_HELPERS } from "./string-reads.js";
import {
emitConcatInputs,
@@ -1899,9 +1899,13 @@ export class LlEmitter {
...(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 }`,
]
: []),
// String-keyed reference reads load the value slot of the entry
// scr_map_entry_str returns (map-number-lookup.ts).
...(this.decls.has(NUMBER_MAP_ENTRY_DECL) || this.decls.has(STRING_MAP_ENTRY_DECL)
? [`%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.
@@ -23,11 +23,13 @@ import {
borrowsMapReadInputs,
borrowsMapMutationReceiver,
inlineNumberKey,
entryStringKey,
} from "./map-read-lifetimes.js";
import {
emitNumberMapGetRef,
emitNumberMapGetScalar,
emitNumberMapHas,
emitStringMapGetRef,
} from "./map-number-lookup.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
import { emitStringSliceRead } from "./string-slices.js";
@@ -1119,12 +1121,15 @@ export function emitMapLikeIntrinsic(
return host.own({ name: t, type: e.type });
}
const numberKey = inlineNumberKey(k);
const stringKey = entryStringKey(k);
if (value.kind === "union") {
const isnull = B.tmp();
const t = B.tmp();
let raw: string;
if (numberKey !== null) {
raw = emitNumberMapGetRef(host, r.name, numberKey, value);
} else if (stringKey !== null) {
raw = emitStringMapGetRef(host, r.name, stringKey, value);
} else {
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
raw = B.tmp();
@@ -1197,6 +1202,10 @@ export function emitMapLikeIntrinsic(
const raw = emitNumberMapGetRef(host, r.name, numberKey, value);
return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag);
}
if (stringKey !== null && isRefCounted(value)) {
const raw = emitStringMapGetRef(host, r.name, stringKey, 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})`);
@@ -7,6 +7,9 @@
* 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.
* String-keyed reference reads use the same entry contract through
* scr_map_entry_str, so their retain is inline rather than the runtime's
* indirect adapter call.
*/
import type { IrType } from "../../ir/ir.js";
import type { LlvmEmitterContext } from "./expr-context.js";
@@ -31,6 +34,16 @@ export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type:
return value;
}
export const STRING_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_str(ptr, ptr)";
/** The entry for a string `key`, or null when absent. Valid until the next mutation. */
export function emitStringMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
host.declare(STRING_MAP_ENTRY_DECL);
const entry = host.B.tmp();
host.B.line(`${entry} = call ptr @scr_map_entry_str(ptr ${map}, ptr ${key})`);
return entry;
}
/** Map.get of a reference value: a +1 value, or null when absent — the
* contract of scr_map_get_f64_ref. */
export function emitNumberMapGetRef(
@@ -39,8 +52,24 @@ export function emitNumberMapGetRef(
key: string,
value: IrType,
): string {
return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value);
}
/** Map.get of a reference value under a string key, with scr_map_get_str_ref's
* contract. The value type's own retain replaces the collection's adapter, so
* the retain is inline instead of an indirect call in the runtime. */
export function emitStringMapGetRef(
host: LlvmEmitterContext,
map: string,
key: string,
value: IrType,
): string {
return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key), value);
}
/** The +1 value of a found entry, or null for a missing (null) entry. */
function emitMapEntryGetRef(host: LlvmEmitterContext, entry: 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"),
@@ -111,6 +111,11 @@ test("local map results keep scalar and owned reference payloads without heap wr
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/);
else if (value === STRING) {
// String keys read reference values through the entry and retain inline.
expect(ir).toContain("call ptr @scr_map_entry_str(");
expect(ir).not.toContain("@scr_map_get_str_ref");
}
if (value === STRING) expect(ir).toContain("@scr_str_release");
}
}
@@ -9,7 +9,11 @@ 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 {
emitNumberMapGetRef,
emitNumberMapGetScalar,
emitStringMapGetRef,
} from "./map-number-lookup.js";
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
function hasTypedCollectionReceiver(
@@ -167,6 +171,11 @@ export function inlineNumberKey(key: { access: string; value?: string }): string
return key.access === "f64" && key.value !== undefined ? key.value : null;
}
/** The key's string operand when a reference read can take the entry path. */
export function entryStringKey(key: { access: string; value?: string }): string | null {
return key.access === "str" && key.value !== undefined ? key.value : null;
}
export interface StackMapRead {
value: LlValue;
owner: { slot: string; type: IrType } | null;
@@ -205,9 +214,14 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
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})`);
const stringKey = entryStringKey(key);
let raw: string;
if (stringKey !== null) raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value);
else {
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})`);
}
B.line(`${found} = icmp ne ptr ${raw}, null`);
B.line(`store i64 0, ptr ${payload}`);
B.line(`store ptr ${raw}, ptr ${payload}`);
@@ -15901,6 +15901,12 @@
"<repo>/tests/corpus/switch-integer-dispatch.ts"
],
"diags": []
},
"<repo>/tests/corpus/4651-map-string-get-entry.ts": {
"order": [
"<repo>/tests/corpus/4651-map-string-get-entry.ts"
],
"diags": []
}
}
}
+5
View File
@@ -1086,6 +1086,11 @@ bool scr_map_get_str_bool(const ScrMap *m, const ScrStr *key, bool *out) {
return true;
}
const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key) {
size_t e = scr_map_find_str(m, key);
return e == SCR_MAP_EMPTY ? NULL : &m->entries[e];
}
void *scr_map_get_str_ref(const ScrMap *m, const ScrStr *key) {
size_t e = scr_map_find_str(m, key);
if (e == SCR_MAP_EMPTY) return NULL;
+4
View File
@@ -2032,6 +2032,10 @@ _Static_assert(sizeof(ScrMapEntry) == 24 && offsetof(ScrMapEntry, val) == 8,
/* Entry for a number key, or NULL when absent. The pointer is valid until
* the next mutation; generated code reads the value slot through it. */
const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key);
/* Entry for a string key, or NULL when absent, on the same terms. Typed
* reference reads retain the value with its static type's retain instead
* of the collection's adapter. */
const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key);
/* Called once on a fresh UNION_VALUE collection, before its first insert. */
void scr_map_union_keys(ScrMap *map, const uint8_t *kinds);
+31
View File
@@ -126,6 +126,36 @@ static void test_string_keys(void) {
scr_map_release(m);
}
/* scr_map_entry_str: the entry of a string key, for generated code that
* reads the value slot and retains it with the value type's own retain. The
* lookup itself takes no reference, in linear and bucketed maps alike. */
static void test_string_entries(void) {
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_REF, scr_str_retain_v, scr_str_release_v, NULL);
ScrStr *missing = S("missing");
check(scr_map_entry_str(m, missing) == NULL, "empty map has no entry");
char name[16];
for (int i = 0; i < 40; i++) {
snprintf(name, sizeof name, "key%d", i);
ScrStr *k = S(name);
scr_map_set_str_ref(m, k, S(name)); /* value +1 moves in */
scr_str_release(k);
ScrStr *probe = S("key0"); /* a different object with equal bytes */
const ScrMapEntry *e = scr_map_entry_str(m, probe);
check(e != NULL, "entry found by content");
ScrStr *v = (ScrStr *)(uintptr_t)e->val;
check(v->len == 4 && memcmp(v->data, "key0", 4) == 0, "entry value slot holds the value");
check(v->rc == 1, "entry lookup takes no reference");
scr_str_release(probe);
}
check(scr_map_entry_str(m, missing) == NULL, "absent key has no entry");
ScrStr *k7 = S("key7");
check(scr_map_delete_str(m, k7), "delete");
check(scr_map_entry_str(m, k7) == NULL, "deleted key has no entry");
scr_str_release(k7);
scr_str_release(missing);
scr_map_release(m);
}
/* String keys hash to 32 bits; heap strings cache the value in their header
* and static strings (compiler literals, runtime one-character strings)
* carry it precomputed. The vectors are shared with the compiler's
@@ -601,6 +631,7 @@ int main(void) {
test_key_bytes();
test_string_bucket_collisions();
test_string_keys();
test_string_entries();
test_same_value_zero();
test_span_keys();
test_string_hash_cache();
+99
View File
@@ -0,0 +1,99 @@
// String-keyed Map.get of reference values reads the entry and retains the
// value with its static type's retain. Results must outlive the entry: they
// survive delete, overwrite, clear and the map itself, for class instances,
// records, arrays, nested maps, cyclic objects and union-typed values, with
// literal, computed and sliced keys.
class Node {
name: string;
next: Node | undefined = undefined;
constructor(name: string) {
this.name = name;
}
}
interface Box {
label: string;
items: number[];
}
const nodes = new Map<string, Node>();
for (let i = 0; i < 20; i++) nodes.set("n" + i, new Node("node-" + i));
const a = nodes.get("n3");
const b = nodes.get("n" + (1 + 2) * 4);
const missing = nodes.get("nope");
console.log(a?.name, b?.name, missing === undefined);
nodes.delete("n3");
nodes.set("n12", new Node("replacement"));
console.log("after delete/overwrite:", a?.name, b?.name, nodes.get("n12")?.name);
// A cycle through the map value and the result.
const ring = new Map<string, Node>();
const first = new Node("first");
const second = new Node("second");
first.next = second;
second.next = first;
ring.set("first", first);
ring.set("second", second);
const viaMap = ring.get("second");
ring.clear();
console.log("cycle:", viaMap?.next?.next?.name, ring.size);
// Records and arrays as values; keys sliced from a larger string.
const boxes = new Map<string, Box>();
const source = "alpha,beta,gamma";
for (const part of source.split(",")) boxes.set(part, { label: part.toUpperCase(), items: [part.length] });
const keyed = boxes.get(source.slice(6, 10));
boxes.set("beta", { label: "B2", items: [] });
console.log("box:", keyed?.label, keyed?.items.join("|"), boxes.get("beta")?.label);
const lists = new Map<string, number[]>([["evens", [0, 2, 4]], ["odds", [1, 3]]]);
const evens = lists.get("evens");
lists.delete("evens");
evens?.push(6);
console.log("array:", evens?.join(","), lists.has("evens"));
// Maps as values: the inner map outlives its entry in the outer one.
const outer = new Map<string, Map<string, Node>>();
outer.set("inner", new Map<string, Node>([["x", new Node("inner-x")]]));
const inner = outer.get("inner");
outer.delete("inner");
inner?.set("y", new Node("inner-y"));
console.log("nested:", inner?.get("x")?.name, inner?.get("y")?.name, outer.size);
// Union-typed values: nullable instances and mixed arms.
const maybe = new Map<string, Node | null>();
maybe.set("set", new Node("present"));
maybe.set("null", null);
const present = maybe.get("set");
const isNull = maybe.get("null");
const absent = maybe.get("absent");
maybe.delete("set");
console.log("nullable:", present?.name, isNull === null, absent === undefined);
const mixed = new Map<string, Node | string>();
mixed.set("node", new Node("mixed-node"));
mixed.set("text", "plain");
const m1 = mixed.get("node");
const m2 = mixed.get("text");
mixed.set("node", "overwritten");
console.log("mixed:", typeof m1 === "string" ? m1 : m1?.name, m2, mixed.get("node"));
// Many lookups through a growing map (linear scan, then buckets), keeping
// every result alive past the map.
function collect(): Node[] {
const table = new Map<string, Node>();
const out: Node[] = [];
for (let i = 0; i < 64; i++) {
table.set(`k${i}`, new Node(`v${i}`));
const back = table.get(`k${Math.floor(i / 2)}`);
if (back !== undefined) out.push(back);
}
return out;
}
const kept = collect();
console.log("kept:", kept.length, kept[0]!.name, kept[63]!.name);
// A result held in a local across a loop that rewrites the entry.
const counters = new Map<string, Box>();
counters.set("c", { label: "start", items: [] });
const held = counters.get("c")!;
for (let i = 0; i < 5; i++) counters.set("c", { label: "round" + i, items: [i] });
console.log("held:", held.label, counters.get("c")?.label);