mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Merge main into fix/narrowed-property-calls
This commit is contained in:
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user