mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Reuse a string-key Map read's entry for the following write
`const cur = m.get(k); m.set(k, f(cur))` and `m.set(k, (m.get(k) ?? 0) + 1)` probed a string-keyed map's hash index twice. When only inert code (reads, scalar arithmetic, compiler-resolved throws) runs between the read and the write, the read parks the map, the key and the resolved entry in local slots and the write overwrites that entry's value in place if the map and key are identical at run time and the entry was found. Misses and every other case take the ordinary set, so insertion order and SameValueZero semantics are unchanged. Only string-keyed typed maps with scalar values fuse; number-key writes after a read already store into the found entry through findReadNumberMapWrites.
This commit is contained in:
@@ -191,6 +191,7 @@ import {
|
||||
matchMapRead,
|
||||
type MapReadLifetimes,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { findMapEntryFusions, type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { analyzeWalks, findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
|
||||
@@ -728,6 +729,8 @@ export class LlEmitter {
|
||||
callArrayReads = new Map<IrExpr, LocalArrayRead>();
|
||||
mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() };
|
||||
readNumberMapWrites: ReadonlySet<IrExpr> = new Set();
|
||||
/** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */
|
||||
mapEntryFusion: MapEntryFusion | null = null;
|
||||
readonly callLifetimes: CallLifetimes;
|
||||
readonly constantCallbacks: Map<string, Map<string, string>>;
|
||||
currentConstantCallbacks: ReadonlyMap<string, string> = new Map();
|
||||
@@ -5474,7 +5477,11 @@ export class LlEmitter {
|
||||
}
|
||||
const g = this.globalTypes.get(id);
|
||||
if (!g) throw new InternalCompilerError(`llvm emitter bug: unknown binding ${id}`);
|
||||
return { kind: "global", slot: `@${mangleGlobal(id)}`, type: g };
|
||||
return {
|
||||
kind: "global",
|
||||
slot: `@${mangleGlobal(id)}`,
|
||||
type: g,
|
||||
};
|
||||
}
|
||||
|
||||
private emitDebugLocal(local: IrLocal, slot: string, arg = 0, captured = false): void {
|
||||
@@ -5945,6 +5952,7 @@ export class LlEmitter {
|
||||
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
|
||||
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
|
||||
this.readNumberMapWrites = findReadNumberMapWrites(fn);
|
||||
this.mapEntryFusion = this.debug === null ? findMapEntryFusions(fn) : null;
|
||||
this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
|
||||
this.localStackObjects = this.stackObjects
|
||||
? findLocalStackObjects(this, fn, this.callLifetimes)
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
emitStringMapHas,
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
import { emitStringSliceRead } from "./string-slices.js";
|
||||
import {
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
@@ -1265,6 +1266,7 @@ export function emitMapLikeIntrinsic(
|
||||
? emitNumberMapGetScalar(host, r.name, numberKey)
|
||||
: emitStringMapGetScalar(host, r.name, stringKey!);
|
||||
found = scalar.found;
|
||||
if (stringKey !== null) rememberMapEntry(host, e, r.name, stringKey, scalar.entry);
|
||||
readHit = () => {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
@@ -1335,6 +1337,7 @@ export function emitMapLikeIntrinsic(
|
||||
const v = host.emitExpr(e.args[1]!);
|
||||
const vAcc = elemAccess(receiverType.value);
|
||||
if (vAcc === "ref") host.moveTemp(v);
|
||||
const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name);
|
||||
if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
const bits = B.tmp();
|
||||
B.line(
|
||||
@@ -1342,10 +1345,13 @@ export function emitMapLikeIntrinsic(
|
||||
? `${bits} = bitcast double ${v.name} to i64`
|
||||
: `${bits} = zext i1 ${v.name} to i64`,
|
||||
);
|
||||
emitNumberMapSetScalar(host, r.name, k.name, bits, () =>
|
||||
host.mapSet(r.name, kAcc, vAcc, k.name, v.name),
|
||||
);
|
||||
} else host.mapSet(r.name, kAcc, vAcc, k.name, v.name);
|
||||
emitNumberMapSetScalar(host, r.name, k.name, bits, ordinary);
|
||||
} else if (
|
||||
kAcc !== "str" ||
|
||||
vAcc === "ref" ||
|
||||
!emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
)
|
||||
ordinary();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "add": {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
STRING,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { findMapEntryFusions } from "./map-entry-fusion.js";
|
||||
|
||||
const loc = { file: "map-fusion.ts", start: 0, end: 1 };
|
||||
const optional: IrType = { kind: "union", unionId: "optional" };
|
||||
const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc });
|
||||
|
||||
function mapType(key: IrType, value: IrType): IrType {
|
||||
return { kind: "map", key, value };
|
||||
}
|
||||
|
||||
function get(map: IrExpr, key: IrExpr): IrExpr {
|
||||
return { kind: "mapIntrinsic", method: "get", receiver: map, args: [key], type: optional, loc };
|
||||
}
|
||||
|
||||
function set(map: IrExpr, key: IrExpr, value: IrExpr): IrStmt {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
loc,
|
||||
expr: {
|
||||
kind: "mapIntrinsic",
|
||||
method: "set",
|
||||
receiver: map,
|
||||
args: [key, value],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** `(cur is undefined ? 0 : narrow(cur)) + 1` over the optional local. */
|
||||
function increment(value: IrType): IrExpr {
|
||||
const cur = ref("cur", optional);
|
||||
if (value.kind === "bool") return { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
return {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
value: cur,
|
||||
unionId: "optional",
|
||||
tag: 1,
|
||||
negated: false,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "numLit", value: 0, type: F64, loc },
|
||||
else_: { kind: "unionNarrow", unionId: "optional", tag: 0, value: cur, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(
|
||||
body: (map: IrExpr, key: IrExpr, other: IrExpr) => IrStmt[],
|
||||
key: IrType = STRING,
|
||||
value: IrType = F64,
|
||||
): IrModule {
|
||||
const map = mapType(key, value);
|
||||
const params = [
|
||||
{ localId: "map", name: "map", type: map },
|
||||
{ localId: "other", name: "other", type: map },
|
||||
{ localId: "key", name: "key", type: key },
|
||||
];
|
||||
const work: IrFunction = {
|
||||
name: "work",
|
||||
params,
|
||||
returnType: VOID,
|
||||
loc,
|
||||
locals: [
|
||||
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })),
|
||||
{ id: "cur", name: "cur", type: optional, mutable: false },
|
||||
],
|
||||
body: body(ref("map", map), ref("key", key), ref("other", map)),
|
||||
};
|
||||
return {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
unions: [{ id: "optional", arms: [value, UNDEFINED_T] }],
|
||||
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }, work],
|
||||
};
|
||||
}
|
||||
|
||||
const declare = (map: IrExpr, key: IrExpr): IrStmt => ({
|
||||
kind: "varDecl",
|
||||
localId: "cur",
|
||||
init: get(map, key),
|
||||
loc,
|
||||
});
|
||||
|
||||
function pairs(mod: IrModule): number {
|
||||
return findMapEntryFusions(mod.functions[1]!).writes.size;
|
||||
}
|
||||
|
||||
function body(mod: IrModule, bits: 32 | 64 = 64): string {
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: bits });
|
||||
return /^define internal [^\n]*@sc_(?:b)?f_work\([^]*?^}/m.exec(llvm)![0];
|
||||
}
|
||||
|
||||
test("a read followed by a write of the same map pairs, and the write reuses the entry", () => {
|
||||
for (const value of [F64, BOOL]) {
|
||||
const mod = fixture(
|
||||
(map, k) => [declare(map, k), set(map, k, increment(value))],
|
||||
STRING,
|
||||
value,
|
||||
);
|
||||
expect(pairs(mod)).toBe(1);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("mapfuse.hit");
|
||||
// The miss still inserts through the runtime.
|
||||
expect(ir).toMatch(/call void @scr_map_set_str_(?:f64|bool)\(/);
|
||||
expect(ir).toContain("@sc_map_entry_str(");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("number keys keep the write-after-read lookup instead of pairing", () => {
|
||||
// findReadNumberMapWrites stores such writes into the found entry through
|
||||
// a read-only probe that LLVM merges with the read's.
|
||||
const mod = fixture((map, k) => [declare(map, k), set(map, k, increment(F64))], F64, F64);
|
||||
expect(pairs(mod)).toBe(0);
|
||||
expect(body(mod)).not.toContain("mapfuse.hit");
|
||||
});
|
||||
|
||||
test("the single-statement form pairs an unconditional nested read", () => {
|
||||
const mod = fixture((map, k) => [
|
||||
set(map, k, {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: {
|
||||
kind: "nullish",
|
||||
left: get(map, k),
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
]);
|
||||
expect(pairs(mod)).toBe(1);
|
||||
});
|
||||
|
||||
test("reads behind a condition, intervening statements and other effects do not pair", () => {
|
||||
// The read only runs on one arm.
|
||||
const conditional = fixture((map, k) => [
|
||||
set(map, k, {
|
||||
kind: "ternary",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
then: {
|
||||
kind: "nullish",
|
||||
left: get(map, k),
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
else_: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
]);
|
||||
expect(pairs(conditional)).toBe(0);
|
||||
// A mutation of any map between the read and the write.
|
||||
const between = fixture((map, k, other) => [
|
||||
declare(map, k),
|
||||
set(other, k, { kind: "numLit", value: 1, type: F64, loc }),
|
||||
set(map, k, increment(F64)),
|
||||
]);
|
||||
expect(findMapEntryFusions(between.functions[1]!).reads.size).toBe(1);
|
||||
expect(
|
||||
[...findMapEntryFusions(between.functions[1]!).writes.keys()].map(
|
||||
(w) => w.kind === "mapIntrinsic" && w.receiver.kind === "varRef" && w.receiver.localId,
|
||||
),
|
||||
).toEqual(["other"]);
|
||||
// A call in the written value may run arbitrary code.
|
||||
const call = fixture((map, k) => [
|
||||
declare(map, k),
|
||||
set(map, k, { kind: "call", callee: "main", args: [], type: F64, loc }),
|
||||
]);
|
||||
expect(pairs(call)).toBe(0);
|
||||
});
|
||||
|
||||
test("writes to another map or key stay correct at run time", () => {
|
||||
// The pairing is syntactic; identity checks decide reuse when it runs.
|
||||
const mod = fixture((map, k, other) => [declare(map, k), set(other, k, increment(F64))]);
|
||||
expect(pairs(mod)).toBe(1);
|
||||
const ir = body(mod);
|
||||
expect(ir).toMatch(/icmp eq ptr %\w+, %\w+/);
|
||||
expect(ir).toContain("mapfuse.miss");
|
||||
});
|
||||
|
||||
test("reference values never fuse", () => {
|
||||
const mod = fixture(
|
||||
(map, k) => [declare(map, k), set(map, k, { kind: "strLit", value: "x", type: STRING, loc })],
|
||||
STRING,
|
||||
STRING,
|
||||
);
|
||||
expect(pairs(mod)).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
/* Map get-then-set fusion.
|
||||
*
|
||||
* `const cur = m.get(k); m.set(k, f(cur))` and `m.set(k, (m.get(k) ?? 0) + 1)`
|
||||
* probe the hash index twice for one key. The read already resolves the
|
||||
* entry (sc_map_entry_str, map-number-lookup.ts), and an entry stays valid
|
||||
* until the next mutation of its map. When nothing that
|
||||
* runs between the read and the write can mutate any collection or run user
|
||||
* code, the write reuses that entry: the read parks the map, the key and the
|
||||
* entry in function-local slots, and the write overwrites the entry's value
|
||||
* slot in place when the map and key are the very same (identical
|
||||
* pointers) and the entry was found. Every other case, a miss
|
||||
* included, takes the ordinary set, which inserts at the end. An existing
|
||||
* key's overwrite keeps its position, so insertion order is unchanged, and a
|
||||
* key match by identity implies the SameValueZero match the set would find.
|
||||
*
|
||||
* Only scalar values (number, boolean) fuse: overwriting them releases
|
||||
* nothing, exactly like the runtime's own overwrite. Only string-keyed
|
||||
* typed maps fuse here: a number-key write after a read of the same key
|
||||
* already probes through the read-only entry lookup that LLVM merges with
|
||||
* the read's (findReadNumberMapWrites, map-read-lifetimes.ts).
|
||||
*/
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { borrowsMapReadInputs, borrowsMapMutationReceiver } from "./map-read-lifetimes.js";
|
||||
import { mapKeyAccess } from "./shapes.js";
|
||||
|
||||
type MapOp = Extract<IrExpr, { kind: "mapIntrinsic" }>;
|
||||
|
||||
/** One fused read/write pair. The slots exist once the read emits through
|
||||
* an entry path; a write whose read took another path stays ordinary. */
|
||||
export interface MapEntryPair {
|
||||
slots: { map: string; key: string; entry: string } | null;
|
||||
}
|
||||
|
||||
export interface MapEntryFusion {
|
||||
reads: Map<IrExpr, MapEntryPair>;
|
||||
writes: Map<IrExpr, MapEntryPair>;
|
||||
}
|
||||
|
||||
/** A typed string-keyed Map read/write with a scalar value. */
|
||||
function fusableOp(e: IrExpr, method: "get" | "set"): e is MapOp {
|
||||
if (e.kind !== "mapIntrinsic" || e.method !== method) return false;
|
||||
const receiver = e.receiver.type;
|
||||
if (receiver.kind !== "map") return false;
|
||||
if (receiver.value.kind !== "f64" && receiver.value.kind !== "bool") return false;
|
||||
if (mapKeyAccess(receiver.key) !== "str") return false;
|
||||
return method === "get" ? borrowsMapReadInputs(e) : borrowsMapMutationReceiver(e);
|
||||
}
|
||||
|
||||
/** Expression kinds that run no user code and mutate no collection or
|
||||
* reference (reads, scalar arithmetic, control within the expression, and
|
||||
* compiler-resolved throws, which leave the statement). */
|
||||
const INERT_KINDS: ReadonlySet<string> = new Set([
|
||||
"numLit",
|
||||
"strLit",
|
||||
"boolLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"bin",
|
||||
"unary",
|
||||
"toBool",
|
||||
"logical",
|
||||
"ternary",
|
||||
"nullish",
|
||||
"orDefault",
|
||||
"strEq",
|
||||
"unionIsTag",
|
||||
"unionNarrow",
|
||||
"recordGet",
|
||||
"fieldGet",
|
||||
]);
|
||||
|
||||
function inert(e: IrExpr, allowed: IrExpr | null): boolean {
|
||||
if (e === allowed) return true;
|
||||
if (e.kind === "mapIntrinsic") {
|
||||
if (!borrowsMapReadInputs(e) || (e.method !== "get" && e.method !== "has")) return false;
|
||||
} else if (e.kind === "libCall") {
|
||||
if (e.fn !== "error.nodeThrow") return false;
|
||||
} else if (!INERT_KINDS.has(e.kind)) return false;
|
||||
return everyExprChild(
|
||||
e,
|
||||
(child) => inert(child, allowed),
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether evaluating `e` always evaluates `target` (it is not behind a
|
||||
* short-circuit or a conditional arm). */
|
||||
function alwaysEvaluates(e: IrExpr, target: IrExpr): boolean {
|
||||
if (e === target) return true;
|
||||
switch (e.kind) {
|
||||
case "bin":
|
||||
return alwaysEvaluates(e.left, target) || alwaysEvaluates(e.right, target);
|
||||
case "unary":
|
||||
case "toBool":
|
||||
return alwaysEvaluates(e.operand, target);
|
||||
case "logical":
|
||||
case "nullish":
|
||||
case "orDefault":
|
||||
return alwaysEvaluates(e.left, target);
|
||||
case "ternary":
|
||||
return alwaysEvaluates(e.cond, target);
|
||||
case "unionIsTag":
|
||||
case "unionNarrow":
|
||||
return alwaysEvaluates(e.value, target);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The single fusable read of `key`'s map inside `e`, if exactly one. */
|
||||
function soleRead(e: IrExpr): MapOp | null {
|
||||
let found: MapOp | null = null;
|
||||
let count = 0;
|
||||
const visit = (node: IrExpr): boolean => {
|
||||
if (node.kind === "mapIntrinsic" && node.method === "get") {
|
||||
count++;
|
||||
if (fusableOp(node, "get")) found = node;
|
||||
}
|
||||
return everyExprChild(node, visit, () => true);
|
||||
};
|
||||
visit(e);
|
||||
return count === 1 ? found : null;
|
||||
}
|
||||
|
||||
/** Same scalar value kind and key access on both sides. */
|
||||
function compatible(read: MapOp, write: MapOp): boolean {
|
||||
const r = read.receiver.type,
|
||||
w = write.receiver.type;
|
||||
return (
|
||||
r.kind === "map" &&
|
||||
w.kind === "map" &&
|
||||
r.value.kind === w.value.kind &&
|
||||
mapKeyAccess(r.key) === mapKeyAccess(w.key)
|
||||
);
|
||||
}
|
||||
|
||||
/** Find the fusable read/write pairs of one function. Coroutines keep their
|
||||
* frames in their own storage and are left alone. */
|
||||
export function findMapEntryFusions(fn: IrFunction): MapEntryFusion {
|
||||
const out: MapEntryFusion = { reads: new Map(), writes: new Map() };
|
||||
if (fn.async || fn.generator) return out;
|
||||
const pair = (read: MapOp, write: MapOp): void => {
|
||||
if (!compatible(read, write) || out.reads.has(read) || out.writes.has(write)) return;
|
||||
const entry: MapEntryPair = { slots: null };
|
||||
out.reads.set(read, entry);
|
||||
out.writes.set(write, entry);
|
||||
};
|
||||
const writeOf = (s: IrStmt | undefined): MapOp | null =>
|
||||
s?.kind === "exprStmt" && fusableOp(s.expr, "set") ? s.expr : null;
|
||||
const visitList = (list: readonly IrStmt[]): void => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const s = list[i]!;
|
||||
// m.set(k, ...m.get(k)...): the read is the only one and runs
|
||||
// unconditionally after the receiver and key.
|
||||
const single = writeOf(s);
|
||||
if (single) {
|
||||
const read = soleRead(single.args[1]!);
|
||||
if (
|
||||
read &&
|
||||
alwaysEvaluates(single.args[1]!, read) &&
|
||||
inert(single.receiver, null) &&
|
||||
inert(single.args[0]!, null) &&
|
||||
inert(single.args[1]!, read)
|
||||
)
|
||||
pair(read, single);
|
||||
}
|
||||
// const cur = m.get(k); m.set(k, ...): the read is the declaration's
|
||||
// whole initializer, and the write follows directly.
|
||||
const value =
|
||||
s.kind === "varDecl" ? s.init : s.kind === "assign" && !s.initializes ? s.value : null;
|
||||
const next = writeOf(list[i + 1]);
|
||||
if (
|
||||
value &&
|
||||
next &&
|
||||
fusableOp(value, "get") &&
|
||||
inert(value.receiver, null) &&
|
||||
inert(value.args[0]!, null) &&
|
||||
inert(next.receiver, null) &&
|
||||
next.args.every((arg) => inert(arg, null))
|
||||
)
|
||||
pair(value, next);
|
||||
}
|
||||
for (const s of list) visitStmt(s);
|
||||
};
|
||||
const visitStmt = (s: IrStmt): void => {
|
||||
switch (s.kind) {
|
||||
case "if":
|
||||
visitList(s.then);
|
||||
if (s.else_) visitList(s.else_);
|
||||
return;
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "forOf":
|
||||
case "block":
|
||||
visitList(s.body);
|
||||
return;
|
||||
case "for":
|
||||
visitList(s.body);
|
||||
return;
|
||||
case "switch":
|
||||
for (const c of s.cases) visitList(c.body);
|
||||
return;
|
||||
case "tryCatch":
|
||||
visitList(s.tryBody);
|
||||
if (s.catchBody) visitList(s.catchBody);
|
||||
if (s.finallyBody) visitList(s.finallyBody);
|
||||
return;
|
||||
default:
|
||||
everyStmtChild(
|
||||
s,
|
||||
() => true,
|
||||
(child) => (visitStmt(child), true),
|
||||
);
|
||||
}
|
||||
};
|
||||
visitList(fn.body);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** After a fused read resolved `entry` for the string `key` in `map`: park
|
||||
* all three for the paired write. */
|
||||
export function rememberMapEntry(
|
||||
host: LlvmEmitterContext,
|
||||
read: IrExpr,
|
||||
map: string,
|
||||
key: string,
|
||||
entry: string,
|
||||
): void {
|
||||
const pair = host.mapEntryFusion?.reads.get(read);
|
||||
if (!pair) return;
|
||||
const B = host.B;
|
||||
if (pair.slots === null) {
|
||||
const slots = { map: B.slot(), key: B.slot(), entry: B.slot() };
|
||||
B.entryAllocas.push(
|
||||
`${slots.map} = alloca ptr ; fused map read: map`,
|
||||
`${slots.key} = alloca ptr ; fused map read: key`,
|
||||
`${slots.entry} = alloca ptr ; fused map read: entry`,
|
||||
`store ptr null, ptr ${slots.entry}`,
|
||||
);
|
||||
pair.slots = slots;
|
||||
}
|
||||
const slots = pair.slots;
|
||||
B.line(`store ptr ${map}, ptr ${slots.map}`);
|
||||
B.line(`store ptr ${key}, ptr ${slots.key}`);
|
||||
B.line(`store ptr ${entry}, ptr ${slots.entry}`);
|
||||
}
|
||||
|
||||
/** Emit a fused write: overwrite the parked entry's value in place when it
|
||||
* belongs to this map and key, else run `ordinary`. False when `write` is
|
||||
* not fused or its read never parked an entry (emit the ordinary set). */
|
||||
export function emitFusedMapSet(
|
||||
host: LlvmEmitterContext,
|
||||
write: IrExpr,
|
||||
map: string,
|
||||
key: string,
|
||||
value: { name: string; access: "f64" | "bool" },
|
||||
ordinary: () => void,
|
||||
): boolean {
|
||||
const slots = host.mapEntryFusion?.writes.get(write)?.slots;
|
||||
if (!slots) return false;
|
||||
const B = host.B;
|
||||
const parkedMap = B.tmp(),
|
||||
parkedKey = B.tmp(),
|
||||
entry = B.tmp(),
|
||||
sameMap = B.tmp(),
|
||||
sameKey = B.tmp(),
|
||||
found = B.tmp(),
|
||||
both = B.tmp(),
|
||||
reuse = B.tmp();
|
||||
B.line(`${parkedMap} = load ptr, ptr ${slots.map}`);
|
||||
B.line(`${parkedKey} = load ptr, ptr ${slots.key}`);
|
||||
B.line(`${entry} = load ptr, ptr ${slots.entry}`);
|
||||
// The entry is valid only between its read and this write.
|
||||
B.line(`store ptr null, ptr ${slots.entry}`);
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
B.line(`${sameMap} = icmp eq ptr ${parkedMap}, ${map}`);
|
||||
B.line(`${sameKey} = icmp eq ptr ${parkedKey}, ${key}`);
|
||||
B.line(`${both} = and i1 ${sameMap}, ${sameKey}`);
|
||||
B.line(`${reuse} = select i1 ${found}, i1 ${both}, i1 false`);
|
||||
const hit = B.newLabel("mapfuse.hit"),
|
||||
miss = B.newLabel("mapfuse.miss"),
|
||||
done = B.newLabel("mapfuse.done");
|
||||
B.condBr(reuse, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const slot = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 1`);
|
||||
if (value.access === "f64") B.line(`${bits} = bitcast double ${value.name} to i64`);
|
||||
else B.line(`${bits} = zext i1 ${value.name} to i64`);
|
||||
B.line(`store i64 ${bits}, ptr ${slot}`);
|
||||
B.br(done);
|
||||
B.startBlock(miss);
|
||||
ordinary();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
return true;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export function emitStringMapGetScalar(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
): { found: string; bits: string } {
|
||||
): { found: string; bits: string; entry: string } {
|
||||
return emitMapEntryGetScalar(host, emitStringMapEntry(host, map, key));
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function emitNumberMapGetScalar(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
): { found: string; bits: string } {
|
||||
): { found: string; bits: string; entry: string } {
|
||||
return emitMapEntryGetScalar(host, emitNumberMapEntry(host, map, key));
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export function emitNumberMapGetScalar(
|
||||
function emitMapEntryGetScalar(
|
||||
host: LlvmEmitterContext,
|
||||
entry: string,
|
||||
): { found: string; bits: string } {
|
||||
): { found: string; bits: string; entry: string } {
|
||||
const B = host.B;
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
@@ -145,7 +145,7 @@ function emitMapEntryGetScalar(
|
||||
B.startBlock(join);
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = phi i64 [ ${loaded}, %${hit} ], [ 0, %${miss} ]`);
|
||||
return { found, bits };
|
||||
return { found, bits, entry };
|
||||
}
|
||||
|
||||
/** Map.has / Set.has. */
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
emitStringMapGetRef,
|
||||
} from "./map-number-lookup.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
import { rememberMapEntry } from "./map-entry-fusion.js";
|
||||
|
||||
function hasTypedCollectionReceiver(
|
||||
expr: IrExpr,
|
||||
@@ -64,6 +65,8 @@ export function borrowsMapMutationReceiver(expr: IrExpr): boolean {
|
||||
}
|
||||
|
||||
export interface LocalMapRead {
|
||||
/** The Map.get expression itself. */
|
||||
expr: IrExpr;
|
||||
type: IrType;
|
||||
receiver: IrExpr;
|
||||
key: IrExpr;
|
||||
@@ -99,6 +102,7 @@ export function matchMapRead(
|
||||
const missingTag = arms.findIndex((arm) => arm.kind === "undefinedT");
|
||||
if (presentTag < 0 || missingTag < 0) return null;
|
||||
return {
|
||||
expr,
|
||||
type: expr.type,
|
||||
receiver: expr.receiver,
|
||||
key: expr.args[0]!,
|
||||
@@ -314,6 +318,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
} else if (string !== null && !isRefCounted(read.value)) {
|
||||
const scalar = emitStringMapGetScalar(host, receiver.name, string);
|
||||
found = scalar.found;
|
||||
rememberMapEntry(host, read.expr, receiver.name, string, scalar.entry);
|
||||
B.line(`store i64 ${scalar.bits}, ptr ${payload}`);
|
||||
} else if (isRefCounted(read.value)) {
|
||||
const stringKey = entryStringKey(key);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/map-get-set-fusion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Map get followed by set on the same key reuses the read's entry when
|
||||
// nothing in between can touch a collection. Present and absent keys, NaN
|
||||
// and signed zeros, aliases, other maps and keys, and intervening mutations
|
||||
// must keep Node's values and insertion order.
|
||||
|
||||
function show(label: string, m: Map<string, number>): void {
|
||||
const parts: string[] = [];
|
||||
for (const [k, v] of m) parts.push(k + "=" + v);
|
||||
console.log(label, m.size, parts.join(" "));
|
||||
}
|
||||
|
||||
function showNumbers(label: string, m: Map<number, number>): void {
|
||||
const parts: string[] = [];
|
||||
for (const [k, v] of m) parts.push((Object.is(k, -0) ? "-0" : String(k)) + "=" + v);
|
||||
console.log(label, m.size, parts.join(" "));
|
||||
}
|
||||
|
||||
// Counting: the first occurrence inserts, later ones overwrite in place.
|
||||
const words = "the cat and the hat and the bat".split(" ");
|
||||
const counts: Map<string, number> = new Map();
|
||||
for (const w of words) {
|
||||
const cur = counts.get(w);
|
||||
counts.set(w, (cur === undefined ? 0 : cur) + 1);
|
||||
}
|
||||
show("counts", counts);
|
||||
|
||||
// The single-statement form.
|
||||
const lengths: Map<string, number> = new Map();
|
||||
for (const w of words) lengths.set(w, (lengths.get(w) ?? 0) + w.length);
|
||||
show("lengths", lengths);
|
||||
|
||||
// Number keys: NaN finds NaN, -0 and +0 are one key (SameValueZero).
|
||||
const nums: Map<number, number> = new Map();
|
||||
const keys = [1, NaN, -0, 0, 2, NaN, 0, -0, 1, Number.NaN];
|
||||
for (const k of keys) {
|
||||
const prev = nums.get(k);
|
||||
nums.set(k, prev === undefined ? 100 : prev + 1);
|
||||
}
|
||||
showNumbers("nums", nums);
|
||||
console.log("has -0", nums.has(-0), "get 0", nums.get(0), "get NaN", nums.get(NaN));
|
||||
|
||||
// A NaN value is stored and read back.
|
||||
const nanValues: Map<string, number> = new Map();
|
||||
nanValues.set("x", 1);
|
||||
const x0 = nanValues.get("x");
|
||||
nanValues.set("x", (x0 ?? 0) * NaN);
|
||||
console.log("nan value", nanValues.get("x"));
|
||||
|
||||
// An alias of the same map, and a write to a different map or key.
|
||||
const a: Map<string, number> = new Map([
|
||||
["p", 1],
|
||||
["q", 2],
|
||||
]);
|
||||
const alias = a;
|
||||
const other: Map<string, number> = new Map([["p", 50]]);
|
||||
const p1 = a.get("p");
|
||||
alias.set("p", (p1 ?? 0) + 10);
|
||||
const p2 = a.get("p");
|
||||
other.set("p", (p2 ?? 0) + 1);
|
||||
const q1 = a.get("q");
|
||||
a.set("r", (q1 ?? 0) + 3);
|
||||
show("a", a);
|
||||
show("other", other);
|
||||
|
||||
// The map changes between the read and the write: delete re-appends,
|
||||
// clear empties, a fresh insert grows the table.
|
||||
const m: Map<string, number> = new Map([
|
||||
["k1", 1],
|
||||
["k2", 2],
|
||||
["k3", 3],
|
||||
]);
|
||||
const v1 = m.get("k1");
|
||||
m.delete("k1");
|
||||
m.set("k1", (v1 ?? 0) + 1);
|
||||
show("deleted", m);
|
||||
const v2 = m.get("k2");
|
||||
m.set("k2", (v2 ?? 0) + (m.delete("k3") ? 100 : 0));
|
||||
show("delete inside", m);
|
||||
const v3 = m.get("k2");
|
||||
m.clear();
|
||||
m.set("k2", (v3 ?? 0) + 1);
|
||||
show("cleared", m);
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const key = "g" + (i % 17);
|
||||
const before = m.get(key);
|
||||
m.set(key, (before ?? 0) + i);
|
||||
}
|
||||
show("grown", m);
|
||||
|
||||
// Keys built fresh for the write still update the same entry.
|
||||
const fresh: Map<string, number> = new Map();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const got = fresh.get("id" + (i % 2));
|
||||
fresh.set("id" + (i % 2), (got ?? 0) + i);
|
||||
}
|
||||
show("fresh", fresh);
|
||||
|
||||
// Boolean values toggle in place.
|
||||
const flags: Map<number, boolean> = new Map();
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const k = i % 3;
|
||||
const f = flags.get(k);
|
||||
flags.set(k, f === undefined ? true : !f);
|
||||
}
|
||||
console.log("flags", [...flags.entries()].map(([k, v]) => k + ":" + v).join(" "));
|
||||
|
||||
// Inside a function, behind a condition, and with a throwing key read.
|
||||
type Row = { cat: string; v: number };
|
||||
function aggregate(rows: Row[]): Map<string, number> {
|
||||
const agg: Map<string, number> = new Map();
|
||||
for (const r of rows) {
|
||||
if (r.v < 0) continue;
|
||||
const cur = agg.get(r.cat);
|
||||
agg.set(r.cat, (cur === undefined ? 0 : cur) + r.v);
|
||||
}
|
||||
return agg;
|
||||
}
|
||||
const rows: Row[] = [
|
||||
{ cat: "b", v: 2 },
|
||||
{ cat: "a", v: 1 },
|
||||
{ cat: "b", v: -5 },
|
||||
{ cat: "c", v: 4 },
|
||||
{ cat: "a", v: 8 },
|
||||
];
|
||||
show("aggregate", aggregate(rows));
|
||||
try {
|
||||
const maybe: (Row | undefined)[] = [rows[0], undefined];
|
||||
const agg: Map<string, number> = new Map();
|
||||
for (const r of maybe) {
|
||||
const cur = agg.get((r as Row).cat);
|
||||
agg.set((r as Row).cat, (cur ?? 0) + 1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("threw", e instanceof TypeError);
|
||||
}
|
||||
Reference in New Issue
Block a user