Files
scriptc/tests/corpus/4651-map-string-get-entry.ts
T
Malte Ubl 9b98571b4a Retain string-keyed map reads inline
Map.get of a reference value under a string key called scr_map_get_str_ref,
which retained the hit through the collection's value adapter: an indirect
tail call into an out-of-line retain function on every hit (in tsc-ts the
symbol tables' Symbol and Type retains, 0.4% of samples, were only reached
that way). Number keys already read the entry and retain with the value
type's own retain. String keys now do the same: scr_map_entry_str returns the
entry, and the generated code loads the value slot and retains it inline.
Both the ordinary and the stack-boxed (local) reads take the entry path.

Corpus 4651 keeps string-keyed results alive past delete, overwrite, clear
and the map itself for instances, records, arrays, nested maps, cycles and
union values; dropping the inline retain makes it (and 523) fail under ASan.
2026-10-11 06:42:21 -07:00

100 lines
3.7 KiB
TypeScript

// 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);