mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Call function tables without boxing or reference counting (#2173)
This commit is contained in:
@@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `handler-tables` | cpu | dispatch tables of functions: `Record<string, fn>` read as `t[k]!(…)`, `t[k]?.(…)` and through a checked local, a `Map<string, fn>`, an array of handlers, and an object literal of methods; some handlers declare fewer parameters than the table type |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/handler-tables.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Dispatch tables of functions: a `Record<string, fn>` of opcode handlers
|
||||
// called as `ops[name]!(…)`, `ops[name]?.(…)` and through a checked local,
|
||||
// a `Map<string, fn>` of the same handlers, an array of handlers indexed by
|
||||
// opcode number, and an object literal of methods. Some handlers declare
|
||||
// fewer parameters than the table's function type.
|
||||
type Op = (acc: number, arg: number) => number;
|
||||
|
||||
const ops: Record<string, Op> = {
|
||||
add: (acc, arg) => acc + arg,
|
||||
sub: (acc, arg) => acc - arg,
|
||||
mul: (acc, arg) => (acc * arg) % 1000003,
|
||||
neg: (acc) => -acc,
|
||||
half: (acc) => Math.floor(acc / 2),
|
||||
mix: (acc, arg) => (acc ^ arg) & 0xffff,
|
||||
};
|
||||
|
||||
const byName = new Map<string, Op>(Object.entries(ops));
|
||||
const byCode: Op[] = Object.values(ops);
|
||||
const names = Object.keys(ops);
|
||||
|
||||
const methods = {
|
||||
add(acc: number, arg: number): number {
|
||||
return acc + arg;
|
||||
},
|
||||
sub(acc: number, arg: number): number {
|
||||
return acc - arg;
|
||||
},
|
||||
mul(acc: number, arg: number): number {
|
||||
return (acc * arg) % 1000003;
|
||||
},
|
||||
};
|
||||
|
||||
interface Insn {
|
||||
name: string;
|
||||
code: number;
|
||||
arg: number;
|
||||
}
|
||||
|
||||
function program(length: number): Insn[] {
|
||||
const out: Insn[] = [];
|
||||
let seed = 12345;
|
||||
for (let i = 0; i < length; i++) {
|
||||
seed = (seed * 1103515245 + 12345) % 2147483648;
|
||||
const code = seed % (names.length + 1);
|
||||
// One in seven instructions names an opcode no table holds.
|
||||
out.push({ name: code < names.length ? names[code]! : "nop", code, arg: (seed >> 8) % 97 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runAsserted(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) if (insn.name !== "nop") acc = ops[insn.name]!(acc, insn.arg);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runOptional(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) acc = ops[insn.name]?.(acc, insn.arg) ?? acc + 1;
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runChecked(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
const handler = ops[insn.name];
|
||||
if (handler) acc = handler(acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runMap(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
const handler = byName.get(insn.name);
|
||||
if (handler !== undefined) acc = handler(acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runArray(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) if (insn.code < byCode.length) acc = byCode[insn.code]!(acc, insn.arg);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runMethods(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
if (insn.code === 0) acc = methods.add(acc, insn.arg);
|
||||
else if (insn.code === 1) acc = methods.sub(acc, insn.arg);
|
||||
else if (insn.code === 2) acc = methods["mul"](acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "4");
|
||||
const insns = program(200_000);
|
||||
const totals = [0, 0, 0, 0, 0, 0];
|
||||
for (let round = 0; round < rounds * 10; round++) {
|
||||
totals[0] = (totals[0]! + runAsserted(insns)) % 1e9;
|
||||
totals[1] = (totals[1]! + runOptional(insns)) % 1e9;
|
||||
totals[2] = (totals[2]! + runChecked(insns)) % 1e9;
|
||||
totals[3] = (totals[3]! + runMap(insns)) % 1e9;
|
||||
totals[4] = (totals[4]! + runArray(insns)) % 1e9;
|
||||
totals[5] = (totals[5]! + runMethods(insns)) % 1e9;
|
||||
}
|
||||
console.log(totals.join(" "));
|
||||
@@ -101,6 +101,13 @@ export class CheckedNarrows {
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to a checked extraction whose helper borrows its argument, so
|
||||
* emitCheckedNarrowReceiver can lend the payload instead of calling it. */
|
||||
export function borrowsCheckedNarrow(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "call" || host.checkedNarrows.get(e) === null) return false;
|
||||
return host.callLifetimes.borrowed.get(e.callee)?.has(0) === true && host.mayThrow.has(e.callee);
|
||||
}
|
||||
|
||||
/** A checked extraction consumed at once by a field or tag load. The
|
||||
* successful arm borrows the payload from the union, which itself borrows
|
||||
* from its source: a dense array read keeps the element in the array slot,
|
||||
@@ -113,7 +120,7 @@ export function emitCheckedNarrowReceiver(
|
||||
narrow: CheckedNarrowCall,
|
||||
): LlValue | null {
|
||||
const callee = narrow.call.callee;
|
||||
if (!host.callLifetimes.borrowed.get(callee)?.has(0) || !host.mayThrow.has(callee)) return null;
|
||||
if (!borrowsCheckedNarrow(host, narrow.call)) return null;
|
||||
const B = host.B;
|
||||
const read = host.optionalArrayReads.get(narrow.value);
|
||||
const union = read
|
||||
|
||||
@@ -277,6 +277,10 @@ export function emitFieldAbsentTest(B: BlockBuilder, value: string, undefinedTag
|
||||
return absent;
|
||||
}
|
||||
|
||||
/** ScrClosure.function_kind's SCR_FN_RECEIVER_FREE bit (scr_closure.h):
|
||||
* the body never observes the ambient receiver. */
|
||||
export const CLOSURE_RECEIVER_FREE = 16;
|
||||
|
||||
/** The right-hand side of a JS function-identity comparison: two closures
|
||||
* are one function when their identity roots match (a signature adapter
|
||||
* stands for the function it adapts). Both operands must be closures. */
|
||||
|
||||
@@ -299,6 +299,47 @@ test("receiver readers propagate backwards through direct calls and recursion",
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("signature adapters that only forward the receiver ignore it with their original", () => {
|
||||
const forward = (receiver: IrExpr): IrStmt => ({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "callValue",
|
||||
callee: ref("f.0"),
|
||||
receiver,
|
||||
args: [{ kind: "varRef", localId: "a.0", type: F64, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const ambient: IrExpr = { kind: "libCall", fn: "dyn.this", args: [], type: { kind: "dyn" }, loc };
|
||||
const adapter: IrFunction = {
|
||||
name: "%fn.adapt.x.impl",
|
||||
params: [{ localId: "a.0", name: "a", type: F64 }],
|
||||
captures: [{ localId: "f.0", name: "f", type: callback }],
|
||||
locals: [
|
||||
{ id: "f.0", name: "f", type: callback, mutable: false, boxed: true },
|
||||
{ id: "a.0", name: "a", type: F64, mutable: false },
|
||||
],
|
||||
returnType: BOOL,
|
||||
body: [forward(ambient)],
|
||||
loc,
|
||||
};
|
||||
const readers = (): AmbientReceiverReaders =>
|
||||
new AmbientReceiverReaders(new Map([[adapter.name, adapter]]));
|
||||
// The adapter itself reads the receiver, only to forward it.
|
||||
expect(readers().ignores(adapter)).toBe(false);
|
||||
expect(readers().forwardsOnly(adapter)).toBe(true);
|
||||
// Any other read of the receiver, or forwarding something else, does not.
|
||||
adapter.body = [thisRead, forward(ambient)];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
adapter.body = [forward(ref("a.0", F64))];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
adapter.body = [forward(ambient)];
|
||||
adapter.captures = [];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
});
|
||||
|
||||
test("virtual calls admit every same-named method", () => {
|
||||
const call: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
|
||||
@@ -183,14 +183,14 @@ import {
|
||||
unionBoxBits,
|
||||
} from "./inline-unions.js";
|
||||
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
|
||||
import { CheckedNarrows } from "./checked-narrows.js";
|
||||
import { borrowsCheckedNarrow, CheckedNarrows } from "./checked-narrows.js";
|
||||
import {
|
||||
emitDenseArraySet,
|
||||
emitDenseSlotUnlink,
|
||||
SCR_ELEM_I32,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { matchMapRead, pointerMapGet, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
@@ -213,7 +213,13 @@ import {
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
import { StackCaptures } from "./stack-captures.js";
|
||||
import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD, SCR_ARR_FROZEN } from "./common.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
emitFieldAbsentTest,
|
||||
f64Lit,
|
||||
SCR_ARR_FIELD,
|
||||
SCR_ARR_FROZEN,
|
||||
} from "./common.js";
|
||||
import { ffiExtendsNarrowIntegers } from "../targets.js";
|
||||
import {
|
||||
emitLiteralExpr,
|
||||
@@ -285,7 +291,12 @@ import {
|
||||
emitBytesSet,
|
||||
emitBytesIntrinsic,
|
||||
} from "./expr-bytes.js";
|
||||
import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
|
||||
import {
|
||||
emitRecordKeyGet,
|
||||
emitRegexIntrinsic,
|
||||
keyedRecordReadInto,
|
||||
pointerRecordKeyGet,
|
||||
} from "./expr-records.js";
|
||||
import {
|
||||
dynPromiseAdapter,
|
||||
streamTypedRefCommitAdapter,
|
||||
@@ -2560,7 +2571,7 @@ export class LlEmitter {
|
||||
ret === "void" ? ` ${call}` : ` %r = ${call}`,
|
||||
ret === "void" ? ` ret void` : ` ret ${ret} %r`,
|
||||
`}`,
|
||||
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
|
||||
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${this.closureFunctionKind(fn)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
|
||||
``,
|
||||
);
|
||||
if (lending)
|
||||
@@ -5737,7 +5748,10 @@ export class LlEmitter {
|
||||
unwrittenRead(e: IrExpr, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
|
||||
// A checked extraction reads nothing besides the union it narrows.
|
||||
const keys = rw.readKeys(e, (call) =>
|
||||
borrowsCheckedNarrow(this, call) ? [] : this.borrowedReturnKeys(call.callee),
|
||||
);
|
||||
return keys !== null && !rw.conflicts(keys, writes);
|
||||
}
|
||||
|
||||
@@ -6014,7 +6028,21 @@ export class LlEmitter {
|
||||
}
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
case "recordKeyGet":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
|
||||
case "mapIntrinsic": {
|
||||
// A Map binding's own pointer (maps are not plainReference types).
|
||||
if (!this.borrowsWithoutOwning(e) || e.receiver.kind !== "varRef") return false;
|
||||
const binding = this.binding(e.receiver.localId);
|
||||
return (
|
||||
binding.kind === "global" ||
|
||||
(binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz &&
|
||||
!this.localUnionStorage.has(e.receiver.localId))
|
||||
);
|
||||
}
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
@@ -7272,6 +7300,11 @@ export class LlEmitter {
|
||||
return this.nullableFieldGet(e) === null;
|
||||
case "recordGet":
|
||||
return true;
|
||||
// A keyed read whose hits are all the result's own pointer.
|
||||
case "recordKeyGet":
|
||||
return pointerRecordKeyGet(this, e);
|
||||
case "mapIntrinsic":
|
||||
return pointerMapGet(this, e);
|
||||
// emitReadReceiver lends a required element of a borrowed array when
|
||||
// computing the index removes no edge.
|
||||
case "arrayGet":
|
||||
@@ -7417,8 +7450,20 @@ export class LlEmitter {
|
||||
return wrapEmitterListener(this, target, adapterFn);
|
||||
}
|
||||
|
||||
unwrapNullableClosure(u: string, funcTag: number): string {
|
||||
return unwrapNullableClosure(this, u, funcTag);
|
||||
/** ScrClosure.function_kind for a closure over `fn`: generator 1, async
|
||||
* 2, own prototype 4, and SCR_FN_RECEIVER_FREE (16) when its body never
|
||||
* observes the ambient receiver, so indirect callers skip the bracket. */
|
||||
closureFunctionKind(fn: IrFunction): number {
|
||||
return (
|
||||
(fn.generator ? 1 : 0) +
|
||||
(fn.async ? 2 : 0) +
|
||||
(fn.ownsPrototype ? 4 : 0) +
|
||||
(this.receiverReaders.ignores(fn) ? CLOSURE_RECEIVER_FREE : 0)
|
||||
);
|
||||
}
|
||||
|
||||
unwrapNullableClosure(u: string, unionId: string, funcTag: number): string {
|
||||
return unwrapNullableClosure(this, u, unionId, funcTag);
|
||||
}
|
||||
|
||||
closeBindThunkFor(cbUnion: IrType, retServer: boolean): string {
|
||||
|
||||
@@ -974,10 +974,20 @@ export function wrapEmitterListener(
|
||||
export function unwrapNullableClosure(
|
||||
host: LlvmEmitterContext,
|
||||
u: string,
|
||||
unionId: string,
|
||||
funcTag: number,
|
||||
): string {
|
||||
const B = host.B;
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
const nullable = host.nullableUnions.get(unionId);
|
||||
if (nullable !== null) {
|
||||
// The union is the closure pointer itself (NULL for the unit arm).
|
||||
if (nullable.repr !== "single" || nullable.nullTag >= 0 || nullable.refTag !== funcTag)
|
||||
throw new InternalCompilerError("llvm emitter bug: nullable callback union shape");
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr ${retainSym(host.shapeHost, { kind: "union", unionId })}(ptr ${u})`);
|
||||
return r;
|
||||
}
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${slot}`);
|
||||
@@ -1026,9 +1036,9 @@ export function closeBindThunkFor(
|
||||
"llvm emitter bug: bound-close callback union lacks its func arm",
|
||||
);
|
||||
const oneParam = funcArm.params.length === 1;
|
||||
const cbNullable = host.nullableUnions.get(cbUnion.unionId);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_net_server_close_direct(ptr, ptr)`);
|
||||
let trampoline: string | null = null;
|
||||
@@ -1072,13 +1082,11 @@ export function closeBindThunkFor(
|
||||
` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`,
|
||||
` %bx = load ptr, ptr %capsp`,
|
||||
` %srv = call ptr @scr_box_get_ref(ptr %bx) ; +1`,
|
||||
` %tagp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 1`,
|
||||
` %tag = load i32, ptr %tagp`,
|
||||
...unionTagLines(cbNullable, "%cbu", "%tag"),
|
||||
` %isfn = icmp eq i32 %tag, ${funcTag}`,
|
||||
` br i1 %isfn, label %fn, label %go`,
|
||||
`fn:`,
|
||||
` %pp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 5`,
|
||||
` %pv = load ptr, ptr %pp`,
|
||||
...unionPeekLines(cbNullable, "%cbu", "%pv"),
|
||||
];
|
||||
if (oneParam) {
|
||||
d.push(
|
||||
@@ -1097,7 +1105,7 @@ export function closeBindThunkFor(
|
||||
` br label %go`,
|
||||
`go:`,
|
||||
` %regv = load ptr, ptr %regslot`,
|
||||
` call void @scr_union_release(ptr %cbu) ; the callee owns its +1 param`,
|
||||
` call void ${releaseSym(host.shapeHost, cbUnion)}(ptr %cbu) ; the callee owns its +1 param`,
|
||||
` call void @scr_net_server_close_direct(ptr %srv, ptr %regv) ; reg moves`,
|
||||
);
|
||||
if (retServer) {
|
||||
|
||||
@@ -42,7 +42,13 @@ import {
|
||||
} from "./classes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
f64Lit,
|
||||
ffiNativeTypeLl,
|
||||
ffiNativeParamLl,
|
||||
ffiNativeReturnLl,
|
||||
} from "./common.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import { emitCallArrayRead } from "./local-array-reads.js";
|
||||
@@ -761,8 +767,7 @@ export function emitCallExpr(
|
||||
`${c} = call ptr @scr_closure_new(ptr @${host.callTarget(e.fnName)}, ${host.sizeType} ${e.captures.length})`,
|
||||
);
|
||||
const out = host.own({ name: c, type: e.type });
|
||||
const functionKind =
|
||||
(target.generator ? 1 : 0) + (target.async ? 2 : 0) + (target.ownsPrototype ? 4 : 0);
|
||||
const functionKind = host.closureFunctionKind(target);
|
||||
if (functionKind) {
|
||||
const kindPtr = B.tmp();
|
||||
B.line(`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
@@ -800,6 +805,25 @@ export function emitCallExpr(
|
||||
: 0;
|
||||
host.declare(`declare void @scr_closure_adopt_identity(ptr, ptr, i32)`);
|
||||
B.line(`call void @scr_closure_adopt_identity(ptr ${c}, ptr ${original}, i32 ${length})`);
|
||||
if (host.receiverReaders.forwardsOnly(target)) {
|
||||
// The adapter forwards the ambient receiver to its original,
|
||||
// so it ignores the receiver exactly when the original does.
|
||||
const fromPtr = B.tmp(),
|
||||
from = B.tmp(),
|
||||
bit = B.tmp(),
|
||||
toPtr = B.tmp(),
|
||||
to = B.tmp(),
|
||||
kind = B.tmp();
|
||||
B.line(
|
||||
`${fromPtr} = getelementptr inbounds %ScrClosure, ptr ${original}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${from} = load i32, ptr ${fromPtr}`);
|
||||
B.line(`${bit} = and i32 ${from}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${toPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
B.line(`${to} = load i32, ptr ${toPtr}`);
|
||||
B.line(`${kind} = or i32 ${to}, ${bit}`);
|
||||
B.line(`store i32 ${kind}, ptr ${toPtr}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
@@ -813,7 +837,18 @@ export function emitCallExpr(
|
||||
const ft = e.callee.type;
|
||||
if (e.args.length !== ft.params.length)
|
||||
throw new LlvmUnsupportedError("callValue:arity", e.loc);
|
||||
const callee = host.stackCallbacks.emit(host, e.callee) ?? emitBorrowedInput(host, e.callee);
|
||||
// A receiver that reads the ambient `this` binds what the callee
|
||||
// would see without a binding: the call forwards it unchanged and
|
||||
// needs no receiver bracket (signature adapters forward this way).
|
||||
const forwardsReceiver =
|
||||
e.receiver?.kind === "libCall" &&
|
||||
e.receiver.fn === "dyn.this" &&
|
||||
e.receiver.args.length === 0;
|
||||
const callee =
|
||||
host.stackCallbacks.emit(host, e.callee) ??
|
||||
(lendsCallee(host, e, forwardsReceiver)
|
||||
? host.emitReadReceiver(e.callee)
|
||||
: emitBorrowedInput(host, e.callee));
|
||||
// A class instance bound as the callee's `this` (`h.fn(x)`) is boxed
|
||||
// only if the callee reads it: the receiver window records the
|
||||
// instance (owned by this frame for the whole call) with its typed-ref
|
||||
@@ -825,7 +860,10 @@ export function emitCallExpr(
|
||||
!host.shapeHost.isErrorClass(e.receiver.value.type.className)
|
||||
? host.emitExpr(e.receiver.value)
|
||||
: null;
|
||||
const receiver = e.receiver === undefined || lazyReceiver ? null : host.emitExpr(e.receiver);
|
||||
const receiver =
|
||||
e.receiver === undefined || lazyReceiver || forwardsReceiver
|
||||
? null
|
||||
: host.emitExpr(e.receiver);
|
||||
const pushReceiver = (): void => {
|
||||
if (lazyReceiver) {
|
||||
const t = lazyReceiver.type;
|
||||
@@ -865,7 +903,7 @@ export function emitCallExpr(
|
||||
? borrowableInputs(host, e.args, false, host.referenceWrites?.summary(known!.name))
|
||||
: [];
|
||||
let dynamic = false;
|
||||
if (!direct && e.receiver === undefined) {
|
||||
if (!direct && (e.receiver === undefined || forwardsReceiver)) {
|
||||
const references = new Set<number>();
|
||||
ft.params.forEach((param, index) => {
|
||||
if (isRefCounted(param)) references.add(index);
|
||||
@@ -898,16 +936,51 @@ export function emitCallExpr(
|
||||
lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a,
|
||||
)
|
||||
: args;
|
||||
const invoke = (target: string, needsReceiver: boolean, values = args): string => {
|
||||
// An entry through the closure's own pointer brackets the call only
|
||||
// when the closure may observe the receiver (SCR_FN_RECEIVER_FREE).
|
||||
const invoke = (
|
||||
target: string,
|
||||
bracket: boolean,
|
||||
values = args,
|
||||
indirect = false,
|
||||
): string => {
|
||||
const needsReceiver = bracket && !forwardsReceiver;
|
||||
let observes: string | null = null;
|
||||
if (needsReceiver) {
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
pushReceiver();
|
||||
if (indirect) {
|
||||
const kindPtr = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
free = B.tmp();
|
||||
observes = B.tmp();
|
||||
B.line(
|
||||
`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${callee.name}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${kind} = load i32, ptr ${kindPtr}`);
|
||||
B.line(`${free} = and i32 ${kind}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${observes} = icmp eq i32 ${free}, 0`);
|
||||
const push = B.newLabel("receiver.push"),
|
||||
pushed = B.newLabel("receiver.call");
|
||||
B.condBr(observes, push, pushed);
|
||||
B.startBlock(push);
|
||||
pushReceiver();
|
||||
B.br(pushed);
|
||||
B.startBlock(pushed);
|
||||
} else pushReceiver();
|
||||
}
|
||||
const result = e.type.kind === "void" ? "" : B.tmp();
|
||||
B.line(
|
||||
`${result ? `${result} = ` : ""}call ${host.llType(e.type)} ${target}(${argListOf(values)})`,
|
||||
);
|
||||
if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
if (observes !== null) {
|
||||
const pop = B.newLabel("receiver.pop"),
|
||||
popped = B.newLabel("receiver.done");
|
||||
B.condBr(observes, pop, popped);
|
||||
B.startBlock(pop);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
B.br(popped);
|
||||
B.startBlock(popped);
|
||||
} else if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
return result;
|
||||
};
|
||||
const lending =
|
||||
@@ -953,11 +1026,11 @@ export function emitCallExpr(
|
||||
B.line(`${has} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(has, borrowing, owning);
|
||||
B.startBlock(borrowing);
|
||||
const lentResult = invoke(entry, true);
|
||||
const lentResult = invoke(entry, true, args, true);
|
||||
const borrowingEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(owning);
|
||||
const ownedResult = invoke(fn, true, ownedArgs());
|
||||
const ownedResult = invoke(fn, true, ownedArgs(), true);
|
||||
const owningEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
@@ -966,7 +1039,7 @@ export function emitCallExpr(
|
||||
B.line(
|
||||
`${t} = phi ${host.llType(e.type)} [ ${lentResult}, %${borrowingEnd} ], [ ${ownedResult}, %${owningEnd} ]`,
|
||||
);
|
||||
} else if (!direct) t = invoke(fn, true, ownedArgs());
|
||||
} else if (!direct) t = invoke(fn, true, ownedArgs(), true);
|
||||
else {
|
||||
// A global initializer may not have run, or a callback may enter
|
||||
// through a boundary the census cannot prove. Validate the actual
|
||||
@@ -984,7 +1057,7 @@ export function emitCallExpr(
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(fallback);
|
||||
const indirect = invoke(fn, true, ownedArgs());
|
||||
const indirect = invoke(fn, true, ownedArgs(), true);
|
||||
const fallbackEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
@@ -1358,6 +1431,22 @@ function ctorArguments(
|
||||
* walk consumes without owning it (LlEmitter.canBorrowReceiver proved every
|
||||
* reference argument borrowable and every argument edge-preserving). The
|
||||
* result stays reachable from the borrowed arguments until the consumer. */
|
||||
/** A closure value read from a location the call cannot write (a keyed
|
||||
* table entry, a field, a checked extraction of either) is entered without
|
||||
* a reference of its own: the location keeps the closure alive until the
|
||||
* call returns. Plain stable bindings already lend (emitBorrowedInput). */
|
||||
function lendsCallee(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"callValue">,
|
||||
forwardsReceiver: boolean,
|
||||
): boolean {
|
||||
const writes = host.referenceWrites;
|
||||
if (writes === null || host.canBorrowCallArgument(e.callee) || !host.canBorrowReceiver(e.callee))
|
||||
return false;
|
||||
const later = e.receiver === undefined || forwardsReceiver ? e.args : [e.receiver, ...e.args];
|
||||
return borrowableInputs(host, [e.callee, ...later], false, writes.ownWrites(e))[0] === true;
|
||||
}
|
||||
|
||||
export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call">): LlValue {
|
||||
const callee = host.fnByName.get(e.callee);
|
||||
if (!callee || !host.borrowedReturns.has(e.callee))
|
||||
|
||||
@@ -499,10 +499,7 @@ export function emitOptionalChainReceiver(
|
||||
// needs no reference of its own while the body cannot remove an edge
|
||||
// (so nothing can overwrite the projection before the body finishes).
|
||||
const borrowReceiver =
|
||||
!multiple &&
|
||||
isRefCounted(narrowed) &&
|
||||
host.canBorrowReceiver(e.receiver) &&
|
||||
host.referenceEffects.preserves(e.body);
|
||||
!multiple && isRefCounted(narrowed) && borrowsChainReceiver(host, e.receiver, e.body);
|
||||
// An optional array read of a nullable-pointer union is the element
|
||||
// pointer itself, read inline and owned by the statement frame until
|
||||
// the body finishes; the bind slot borrows it like an emitExpr value.
|
||||
@@ -562,6 +559,16 @@ export function emitOptionalChainReceiver(
|
||||
};
|
||||
}
|
||||
|
||||
/** An optional chain's receiver can lend its payload to the bind slot when
|
||||
* the body leaves the receiver's locations alone: it removes no edge at
|
||||
* all, or it writes none of the locations the receiver reads. */
|
||||
function borrowsChainReceiver(host: LlvmEmitterContext, receiver: IrExpr, body: IrExpr): boolean {
|
||||
if (!host.canBorrowReceiver(receiver)) return false;
|
||||
if (host.referenceEffects.preserves(body)) return true;
|
||||
const writes = host.referenceWrites;
|
||||
return writes !== null && host.unwrittenRead(receiver, writes.expr(body));
|
||||
}
|
||||
|
||||
/** `a?.b ?? d` narrowed to the chain body's present arm. The body wraps a
|
||||
* value of the result type in a non-unit arm, so the default runs exactly
|
||||
* when the receiver is nullish and the intermediate result box is never
|
||||
@@ -588,16 +595,21 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
|
||||
if (unitTags.length === 0 || present.length !== 1) return null;
|
||||
const B = host.B;
|
||||
const narrowed = receiverDef.arms[present[0]!]!;
|
||||
const r = host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
// A receiver read from a location the body cannot write lends its
|
||||
// payload to the bind slot for the whole body (`table[k]?.(…) ?? d`).
|
||||
const borrowed =
|
||||
isRefCounted(narrowed) && borrowsChainReceiver(host, chain.receiver, chain.body.value);
|
||||
const r =
|
||||
borrowed || host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
const bindTy = host.llType(narrowed);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ${bindTy}`);
|
||||
B.line(
|
||||
`store ${bindTy} ${bindTy === "ptr" ? "null" : bindTy === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
|
||||
);
|
||||
host.ownSlot(bind, narrowed);
|
||||
if (!borrowed) host.ownSlot(bind, narrowed);
|
||||
const isUnit = host.tagInSet(r.name, receiverDef.id, unitTags);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
@@ -610,7 +622,10 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
B.line(`store ${bindTy} ${host.unionExtract(r.name, receiverDef.id, narrowed)}, ptr ${bind}`);
|
||||
const payload = borrowed
|
||||
? host.unionPeek(r.name, receiverDef.id)
|
||||
: host.unionExtract(r.name, receiverDef.id, narrowed);
|
||||
B.line(`store ${bindTy} ${payload}, ptr ${bind}`);
|
||||
host.chainSlots.set(chain.id, { name: bind, type: narrowed, slot: true });
|
||||
host.emitBranchInto(slot, chain.body.value);
|
||||
host.chainSlots.delete(chain.id);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitNullableIsTag, emitNullablePresent } from "./union-repr.js";
|
||||
import { isObjectArm } from "./nullable-unions.js";
|
||||
import { isPointerIdentityArm } from "./nullable-unions.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import {
|
||||
DYN,
|
||||
@@ -908,13 +908,13 @@ export function emitDynamicExpr(
|
||||
if (!def)
|
||||
throw new InternalCompilerError(`llvm emitter bug: equality of unknown union ${e.unionId}`);
|
||||
const nullable = host.nullableUnions.get(def.id);
|
||||
if (nullable && isObjectArm(nullable)) {
|
||||
if (nullable && isPointerIdentityArm(nullable)) {
|
||||
// Object identity, and NULL === NULL for the unit arm: one compare.
|
||||
const eq = B.tmp();
|
||||
B.line(`${eq} = icmp ${e.negated ? "ne" : "eq"} ptr ${l.name}, ${r.name}`);
|
||||
return { name: eq, type: e.type };
|
||||
}
|
||||
if (nullable) {
|
||||
if (nullable && nullable.repr === "single" && nullable.arm.kind === "string") {
|
||||
// String arm: identical pointers are equal (the same string or the
|
||||
// same unit encoding); otherwise equal only when both hold strings
|
||||
// with the same bytes.
|
||||
|
||||
@@ -10,6 +10,7 @@ import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js
|
||||
import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js";
|
||||
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
|
||||
import { preservesRegexInputs } from "./reference-effects.js";
|
||||
import { emitNumberMapSlot, emitStringMapEntry } from "./map-number-lookup.js";
|
||||
|
||||
/** Widen one tagged read. The source is borrowed unless the overflow map
|
||||
* supplied an owned value. Each selected reference payload is retained
|
||||
@@ -417,8 +418,8 @@ export function emitRecordKeyGet(
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const k = host.emitExpr(e.key);
|
||||
// The read runs no user code: both inputs lend for its duration.
|
||||
const [obj, k] = emitBorrowedInputs(host, [e.obj, e.key]) as [LlValue, LlValue];
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
@@ -440,6 +441,80 @@ export function emitRecordKeyGet(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
/** A keyed read whose every hit is the result's own pointer: the result
|
||||
* is a nullable-pointer union (nullable-unions.ts) with an undefined arm,
|
||||
* and each declared field and the overflow value hold exactly the result
|
||||
* type or its reference arm in a plain (non-inline) slot. A miss is the
|
||||
* immortal undefined encoding. Such a read can lend the stored pointer:
|
||||
* borrowedRecordKeyGet reads it without a reference. */
|
||||
export function pointerRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false;
|
||||
const resultType = e.type;
|
||||
const nullable = host.nullableUnions.single(resultType.unionId);
|
||||
if (nullable === null || undefinedArmTag(resultType, host.unionsById) < 0) return false;
|
||||
const exact = (t: IrType): boolean => typeEquals(t, resultType) || typeEquals(t, nullable.arm);
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
if (e.overflowOnly !== true) {
|
||||
for (const f of shape.fields)
|
||||
if (host.recordFieldInline(e.shapeId, f.name) || !exact(f.type)) return false;
|
||||
}
|
||||
return shape.indexValue === undefined || typeEquals(shape.indexValue, nullable.arm);
|
||||
}
|
||||
|
||||
/** A pointerRecordKeyGet read without a reference: the declared field or
|
||||
* overflow entry the key names, or the undefined encoding for a miss. The
|
||||
* record (and through it the stored value) must stay unchanged and alive
|
||||
* until the consumer is done with the pointer (canBorrowReceiver). */
|
||||
export function emitBorrowedRecordKeyGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: borrowed keyed read of a non-union");
|
||||
const resultType = e.type;
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
const key = host.canBorrowReceiver(e.key) ? host.emitReadReceiver(e.key) : host.emitExpr(e.key);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const join = B.newLabel("rkb.j");
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
for (const f of e.overflowOnly === true ? [] : shape.fields) {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
const lh = B.newLabel("rkb.h");
|
||||
const ln = B.newLabel("rkb.n");
|
||||
B.condBr(hit, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const { ptr, type } = host.recordFieldPtr(obj.name, e.shapeId, f.name);
|
||||
B.line(`store ptr ${host.loadRecordField(ptr, type, false)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
if (shape.indexValue) {
|
||||
const ovf = host.recordOvfPtr(obj.name, e.shapeId);
|
||||
const entry = emitStringMapEntry(host, ovf, key.name);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const lh = B.newLabel("rkb.h");
|
||||
const ln = B.newLabel("rkb.n");
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
B.line(`store ptr ${emitNumberMapSlot(host, entry, "ptr")}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const undefTag = undefinedArmTag(resultType, host.unionsById);
|
||||
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
/** A keyed read whose result is a scalar union (`r[k]` on a
|
||||
* `Record<string, number>`, at `number | undefined`) and whose every hit
|
||||
* is a number or boolean arm of it, or an inline field of the same union.
|
||||
@@ -735,27 +810,17 @@ export function keyedRecordReadInto(
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
|
||||
// The entry lookup filters absent keys inline; a hit retains the
|
||||
// stored value with its own type's retain (map-number-lookup.ts). A
|
||||
// nullable value's NULL is a stored unit arm: presence decides.
|
||||
const lh = B.newLabel("rkg.h");
|
||||
const ln = B.newLabel("rkg.n");
|
||||
let raw: string;
|
||||
if (iv.kind === "union" && host.nullableUnions.has(iv.unionId)) {
|
||||
// NULL is a stored unit arm of a nullable value: presence decides.
|
||||
host.declare(`declare zeroext i1 @scr_map_has_str(ptr, ptr)`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = call zeroext i1 @scr_map_has_str(ptr ${ovf}, ptr ${keyName})`);
|
||||
B.condBr(has, lh, ln);
|
||||
B.startBlock(lh);
|
||||
raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
|
||||
} else {
|
||||
raw = B.tmp();
|
||||
const isnull = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.condBr(isnull, ln, lh);
|
||||
B.startBlock(lh);
|
||||
}
|
||||
const entry = emitStringMapEntry(host, ovf, keyName);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const raw = host.retainHeap(emitNumberMapSlot(host, entry, "ptr"), iv);
|
||||
B.line(`store ${ty} ${surface(iv, raw, true)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
|
||||
@@ -95,11 +95,12 @@ export function sniCallbackArgs(host: LlvmEmitterContext, cb: LlValue): AbiArgum
|
||||
let type = cb.type,
|
||||
value = cb.name;
|
||||
if (type.kind === "union") {
|
||||
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
|
||||
const unionId = type.unionId;
|
||||
const arms = host.unionsById.get(unionId)?.arms ?? [];
|
||||
const tag = arms.findIndex((arm) => arm.kind === "func");
|
||||
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
|
||||
type = arms[tag]!;
|
||||
value = host.unwrapNullableClosure(value, tag);
|
||||
value = host.unwrapNullableClosure(value, unionId, tag);
|
||||
} else host.toHeap(cb);
|
||||
if (type.kind !== "func" || type.params[1] === undefined)
|
||||
throw new InternalCompilerError("SNI callback lacks its answer function");
|
||||
|
||||
@@ -165,7 +165,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
|
||||
if (funcTag < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its func arm`);
|
||||
cb = host.unwrapNullableClosure(args[cbIndex]!.name, funcTag);
|
||||
cb = host.unwrapNullableClosure(args[cbIndex]!.name, cbT.unionId, funcTag);
|
||||
}
|
||||
}
|
||||
const valueCount = reusePort ? 5 : 4;
|
||||
|
||||
@@ -12,13 +12,18 @@ import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import {
|
||||
emitCompactMapSlot,
|
||||
emitNumberMapGetRef,
|
||||
emitNumberMapGetScalar,
|
||||
emitNumberMapSlot,
|
||||
emitStringMapEntry,
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapGetRef,
|
||||
compactMapValues,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
import { rememberMapEntry } from "./map-entry-fusion.js";
|
||||
|
||||
@@ -490,3 +495,62 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
B.line(`store i32 ${selected}, ptr ${tag}`);
|
||||
return { value: { name: box, type: read.type }, owner };
|
||||
}
|
||||
|
||||
/** A string-keyed Map.get whose result is a nullable-pointer union over the
|
||||
* map's own value type (`Map<string, F>` read as `F | undefined`): a hit is
|
||||
* the stored pointer itself and a miss the immortal undefined encoding, so
|
||||
* the read can lend the stored value (emitBorrowedMapGet). */
|
||||
export function pointerMapGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (
|
||||
e.kind !== "mapIntrinsic" ||
|
||||
e.method !== "get" ||
|
||||
e.args.length !== 1 ||
|
||||
e.type.kind !== "union" ||
|
||||
e.receiver.type.kind !== "map" ||
|
||||
e.receiver.type.key.kind !== "string" ||
|
||||
!borrowsMapReadInputs(e)
|
||||
)
|
||||
return false;
|
||||
const nullable = host.nullableUnions.single(e.type.unionId);
|
||||
return (
|
||||
nullable !== null &&
|
||||
typeEquals(nullable.arm, e.receiver.type.value) &&
|
||||
undefinedArmTag(e.type, host.unionsById) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
/** A pointerMapGet read without a reference: the map (and through it the
|
||||
* stored value) must stay unchanged and alive until the consumer is done
|
||||
* with the pointer (canBorrowReceiver). */
|
||||
export function emitBorrowedMapGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "mapIntrinsic" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const mapType = e.receiver.type;
|
||||
if (e.type.kind !== "union" || mapType.kind !== "map")
|
||||
throw new InternalCompilerError("llvm emitter bug: borrowed Map.get shape");
|
||||
const receiver = host.emitReadReceiver(e.receiver);
|
||||
const keyExpr = e.args[0]!;
|
||||
const key = host.canBorrowReceiver(keyExpr)
|
||||
? host.emitReadReceiver(keyExpr)
|
||||
: host.emitExpr(keyExpr);
|
||||
const compact = compactMapValues(host, mapType.key, mapType.value);
|
||||
const entry = emitStringMapEntry(host, receiver.name, key.name, compact);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const hit = B.newLabel("mapb.hit"),
|
||||
miss = B.newLabel("mapb.miss"),
|
||||
join = B.newLabel("mapb.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const value = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr");
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const absent = host.unitInstanceRef(e.type.unionId, undefinedArmTag(e.type, host.unionsById));
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ptr [ ${value}, %${hit} ], [ ${absent}, %${miss} ]`);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type IrType,
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { NullableUnions } from "./nullable-unions.js";
|
||||
import { isObjectArm, isPointerIdentityArm, NullableUnions } from "./nullable-unions.js";
|
||||
|
||||
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
|
||||
const a: IrType = { kind: "record", shapeId: "a" };
|
||||
@@ -111,3 +111,22 @@ test("library, lifetime-region and debug builds keep record unions boxed", () =>
|
||||
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
|
||||
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("function arms are nullable pointers compared by identity root", () => {
|
||||
const fn: IrType = { kind: "func", params: [F64], ret: F64 };
|
||||
const unions = new NullableUnions(
|
||||
module([
|
||||
{ id: "fu", arms: [fn, UNDEFINED_T] },
|
||||
{ id: "fnu", arms: [NULL_T, fn, UNDEFINED_T] },
|
||||
{ id: "ff", arms: [fn, F64] },
|
||||
]),
|
||||
);
|
||||
expect(unions.single("fu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: fn });
|
||||
expect(unions.single("fnu")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0 });
|
||||
expect(unions.get("ff")).toBeNull();
|
||||
const single = unions.single("fu")!;
|
||||
// Always truthy, but `===` compares adapters by the function they adapt.
|
||||
expect(isObjectArm(single)).toBe(true);
|
||||
expect(isPointerIdentityArm(single)).toBe(false);
|
||||
expect(unions.armTypes().get("fu")).toEqual(fn);
|
||||
});
|
||||
|
||||
@@ -89,21 +89,30 @@ export const NULLABLE_ABSENT = "@sc_nullable_absent";
|
||||
|
||||
/** The reference arms whose instances are never NULL. Class and record
|
||||
* entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and
|
||||
* sets retain through a NULL-skipping wrapper (shapes.ts). Object arms are
|
||||
* always truthy and compare by identity; string arms are truthy when
|
||||
* non-empty and compare bytes. */
|
||||
* sets retain through a NULL-skipping wrapper (shapes.ts), as do closures
|
||||
* (function arms). Object arms are always truthy and compare by identity
|
||||
* (function arms by identity root); string arms are truthy when non-empty
|
||||
* and compare bytes. */
|
||||
export type NullableRefArm =
|
||||
| { kind: "object"; className: string }
|
||||
| { kind: "record"; shapeId: string }
|
||||
| { kind: "string" }
|
||||
| { kind: "stats" }
|
||||
| (IrType & { kind: "array" | "map" | "set" });
|
||||
| (IrType & { kind: "array" | "map" | "set" | "func" });
|
||||
|
||||
/** The arm is a JS object: always truthy, compared by identity. */
|
||||
/** The arm is a JS object: always truthy. */
|
||||
export function isObjectArm(nullable: PointerUnion): boolean {
|
||||
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
|
||||
}
|
||||
|
||||
/** The arm compares by pointer identity. Function arms compare identity
|
||||
* roots instead: a signature adapter is the function it adapts. */
|
||||
export function isPointerIdentityArm(nullable: PointerUnion): boolean {
|
||||
return (
|
||||
nullable.repr === "shapes" || (nullable.arm.kind !== "string" && nullable.arm.kind !== "func")
|
||||
);
|
||||
}
|
||||
|
||||
/** The representation decision for every union of one module. It is a
|
||||
* pure function of the module, so serialized-IR and in-memory emission
|
||||
* agree. */
|
||||
@@ -264,7 +273,8 @@ function nullableShape(
|
||||
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
|
||||
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
|
||||
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
|
||||
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set") return { ...base, arm };
|
||||
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func")
|
||||
return { ...base, arm };
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,24 @@ import type { LlValue } from "./expr-context.js";
|
||||
import { guardedValue } from "./borrowed-receivers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js";
|
||||
import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js";
|
||||
import {
|
||||
emitBorrowedMapGet,
|
||||
emitStackMapRead,
|
||||
matchMapRead,
|
||||
pointerMapGet,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
|
||||
import { emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { borrowsCheckedNarrow, emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
|
||||
import { borrowableInputs } from "./borrowed-inputs.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import {
|
||||
emitBorrowedRecordKeyGet,
|
||||
emitStackRecordKeyGet,
|
||||
pointerRecordKeyGet,
|
||||
scalarRecordKeyGet,
|
||||
} from "./expr-records.js";
|
||||
import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js";
|
||||
|
||||
/** Read a receiver whose value is consumed immediately by a field or tag
|
||||
@@ -92,6 +102,10 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
|
||||
return emitBorrowedResultCall(emitter, e);
|
||||
const arrayRead = emitter.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead);
|
||||
if (e.kind === "recordKeyGet" && emitter.canBorrowReceiver(e))
|
||||
return emitBorrowedRecordKeyGet(emitter, e);
|
||||
if (e.kind === "mapIntrinsic" && emitter.canBorrowReceiver(e))
|
||||
return emitBorrowedMapGet(emitter, e);
|
||||
const narrow = emitter.checkedNarrows.get(e);
|
||||
const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null;
|
||||
if (narrowed) return narrowed;
|
||||
@@ -294,7 +308,27 @@ export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
// snapshot, so neither the callee nor a later argument may replace
|
||||
// what it was read from: a callee that preserves every edge cannot,
|
||||
// and any other one must leave the argument's locations unwritten.
|
||||
// A keyed read lends the stored pointer while the record keeps it.
|
||||
case "recordKeyGet":
|
||||
return (
|
||||
pointerRecordKeyGet(emitter, e) &&
|
||||
emitter.canBorrowReceiver(e.obj) &&
|
||||
emitter.referenceEffects.preserves(e.key)
|
||||
);
|
||||
// So does a Map.get while the map keeps it.
|
||||
case "mapIntrinsic":
|
||||
return (
|
||||
pointerMapGet(emitter, e) &&
|
||||
emitter.canBorrowReceiver(e.receiver) &&
|
||||
emitter.referenceEffects.preserves(e.args[0]!)
|
||||
);
|
||||
case "call": {
|
||||
// A checked extraction lends the payload of the union it narrows
|
||||
// (emitCheckedNarrowReceiver).
|
||||
if (borrowsCheckedNarrow(emitter, e)) {
|
||||
const narrow = emitter.checkedNarrows.get(e)!;
|
||||
return emitter.canBorrowReceiver(narrow.value);
|
||||
}
|
||||
if (!emitter.borrowedReturns.has(e.callee)) return false;
|
||||
if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false;
|
||||
const preserving = emitter.referenceEffects.functions.has(e.callee);
|
||||
|
||||
@@ -97,3 +97,46 @@ test("an array that is read back or escapes keeps its element writes", () => {
|
||||
const input = fill("input", [store("input", element("input"))]);
|
||||
expect(writes(input).conflicts(["a"], writes(input).summary("input"))).toBe(true);
|
||||
});
|
||||
|
||||
test("a keyed record read depends on every field and keyed entry of records", () => {
|
||||
const handler: IrType = { kind: "func", params: [F64], ret: F64 };
|
||||
const table: IrType = { kind: "record", shapeId: "t" };
|
||||
const key: IrExpr = { kind: "strLit", value: "k", type: { kind: "string" }, loc };
|
||||
const read: IrExpr = {
|
||||
kind: "recordKeyGet",
|
||||
obj: ref("table", table),
|
||||
shapeId: "t",
|
||||
key,
|
||||
type: { kind: "union", unionId: "hu" },
|
||||
loc,
|
||||
};
|
||||
const keyedWrite: IrStmt = {
|
||||
kind: "recordKeySet",
|
||||
obj: ref("table", table),
|
||||
shapeId: "t",
|
||||
key,
|
||||
value: ref("h", handler),
|
||||
loc,
|
||||
};
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
name,
|
||||
loc,
|
||||
returnType: F64,
|
||||
params: [{ localId: "table", name: "table", type: table }],
|
||||
locals: [
|
||||
{ id: "table", name: "table", type: table, mutable: false },
|
||||
{ id: "h", name: "h", type: handler, mutable: false },
|
||||
],
|
||||
body,
|
||||
});
|
||||
const records = [{ id: "t", fields: [{ name: "add", type: handler }], indexValue: handler }];
|
||||
const quiet = fn("quiet", []);
|
||||
const writer = fn("writer", [keyedWrite]);
|
||||
const functions = new Map([quiet, writer].map((f) => [f.name, f]));
|
||||
const rw = new ReferenceWrites(functions, [], () => false, new Set(), { records });
|
||||
rw.enter(quiet);
|
||||
const keys = rw.readKeys(read);
|
||||
expect(keys).toEqual(expect.arrayContaining(["r:*", "r:add"]));
|
||||
expect(rw.conflicts(keys!, rw.summary("quiet"))).toBe(false);
|
||||
expect(rw.conflicts(keys!, rw.summary("writer"))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -92,6 +92,8 @@ const INERT_LIB_PREFIXES = [
|
||||
];
|
||||
const INERT_LIB_CALLS = new Set([
|
||||
"dyn.compare",
|
||||
// Reads the ambient receiver (a fresh reference to the bound value).
|
||||
"dyn.this",
|
||||
"error.new",
|
||||
"error.nodeThrow",
|
||||
"dyn.typedRefIs",
|
||||
@@ -923,6 +925,17 @@ export class ReferenceWrites {
|
||||
case "arrayGet":
|
||||
out.push(ARRAY_KEY);
|
||||
return this.expr(node.index) === null && walk(node.arr);
|
||||
case "recordKeyGet":
|
||||
// Any declared field or overflow entry the key may name.
|
||||
out.push(ANY_RECORD_KEY);
|
||||
for (const f of this.records.get(node.shapeId)?.fields ?? []) out.push(`r:${f.name}`);
|
||||
return this.expr(node.key) === null && walk(node.obj);
|
||||
case "mapIntrinsic":
|
||||
// A typed Map.get: any entry of any map.
|
||||
if (node.method !== "get" || node.args.length !== 1 || !borrowsMapReadInputs(node))
|
||||
return false;
|
||||
out.push(MAP_KEY);
|
||||
return this.expr(node.args[0]!) === null && walk(node.receiver);
|
||||
case "unionNarrow":
|
||||
case "upcast":
|
||||
case "downcast":
|
||||
|
||||
@@ -32,10 +32,12 @@ export class StackCallbacks {
|
||||
const count = B.tmp();
|
||||
B.line(`${count} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 2`);
|
||||
B.line(`store ${host.sizeType} ${value.captures.length}, ptr ${count}`);
|
||||
if (this.functions.get(value.fnName)?.ownsPrototype) {
|
||||
const target = this.functions.get(value.fnName);
|
||||
const functionKind = target ? host.closureFunctionKind(target) : 0;
|
||||
if (functionKind !== 0) {
|
||||
const kind = B.tmp();
|
||||
B.line(`${kind} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 4`);
|
||||
B.line(`store i32 4, ptr ${kind}`);
|
||||
B.line(`store i32 ${functionKind}, ptr ${kind}`);
|
||||
}
|
||||
value.captures.forEach((localId, index) => {
|
||||
const box = host.retainBox(host.captureBox(localId));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isBorrowSafeMath } from "./analysis.js";
|
||||
import type { IrExpr, IrFunction, IrLibFn, IrStmt } from "./ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
|
||||
import { everyExprChild, everyStmtChild } from "./traverse.js";
|
||||
|
||||
/** Expression kinds that run no user code of their own and never consult
|
||||
* the ambient receiver stack. Calls are admitted here and resolved through
|
||||
@@ -151,6 +152,7 @@ function opaqueOperand(e: IrExpr): boolean {
|
||||
* this set needs no receiver bracket. */
|
||||
export class AmbientReceiverReaders {
|
||||
private readers: Set<string> | null = null;
|
||||
private byMethod: Map<string, string[]> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
@@ -163,8 +165,63 @@ export class AmbientReceiverReaders {
|
||||
return !this.compute().has(fn.name);
|
||||
}
|
||||
|
||||
private compute(): Set<string> {
|
||||
if (this.readers) return this.readers;
|
||||
/** A signature adapter body that observes the ambient receiver only by
|
||||
* forwarding it, unchanged, to the original it captures (a callValue on
|
||||
* that capture whose receiver is the plain `this` read). Such an adapter
|
||||
* ignores the receiver exactly when its original does. */
|
||||
forwardsOnly(fn: IrFunction): boolean {
|
||||
if (fn.async || fn.generator) return false;
|
||||
const original = fn.captures?.[0]?.localId;
|
||||
if (original === undefined) return false;
|
||||
const readers = this.compute();
|
||||
const edge = (name: string): boolean => this.functions.has(name) && !readers.has(name);
|
||||
const stmt = (node: IrStmt): boolean =>
|
||||
RECEIVER_FREE_STMTS.has(node.kind) && everyStmtChild(node, expr, stmt);
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
if (node.kind === "callValue" && node.receiver !== undefined) {
|
||||
const receiver = node.receiver;
|
||||
return (
|
||||
receiver.kind === "libCall" &&
|
||||
receiver.fn === "dyn.this" &&
|
||||
receiver.args.length === 0 &&
|
||||
node.callee.kind === "varRef" &&
|
||||
node.callee.localId === original &&
|
||||
node.args.every(expr)
|
||||
);
|
||||
}
|
||||
return this.admits(node, edge) && everyExprChild(node, expr, stmt);
|
||||
};
|
||||
return fn.body.every(stmt);
|
||||
}
|
||||
|
||||
/** Whether one expression node, apart from its children, leaves the
|
||||
* ambient receiver alone; `edge` admits a direct callee. */
|
||||
private admits(expr: IrExpr, edge: (name: string) => boolean): boolean {
|
||||
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
|
||||
switch (expr.kind) {
|
||||
case "call":
|
||||
return edge(expr.callee);
|
||||
case "new":
|
||||
return edge(`%${expr.className}.constructor`);
|
||||
case "virtualCall": {
|
||||
// Any class's override may run: admit only when every
|
||||
// same-named method is itself receiver-free.
|
||||
const targets = this.methods().get(expr.method) ?? [];
|
||||
return targets.length > 0 && targets.every(edge);
|
||||
}
|
||||
case "libCall":
|
||||
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
|
||||
case "toString":
|
||||
return !opaqueOperand(expr.operand);
|
||||
default: {
|
||||
const operands = dataIntrinsicOperands(expr);
|
||||
return operands === null || !operands.some(opaqueOperand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private methods(): Map<string, string[]> {
|
||||
if (this.byMethod) return this.byMethod;
|
||||
const byMethod = new Map<string, string[]>();
|
||||
for (const name of this.functions.keys()) {
|
||||
const dot = name.lastIndexOf(".");
|
||||
@@ -174,6 +231,12 @@ export class AmbientReceiverReaders {
|
||||
list.push(name);
|
||||
byMethod.set(method, list);
|
||||
}
|
||||
this.byMethod = byMethod;
|
||||
return byMethod;
|
||||
}
|
||||
|
||||
private compute(): Set<string> {
|
||||
if (this.readers) return this.readers;
|
||||
const readers = new Set<string>();
|
||||
const callers = new Map<string, string[]>();
|
||||
const scan = this.nodes?.scanExcept(TRIVIALLY_RECEIVER_FREE);
|
||||
@@ -185,34 +248,11 @@ export class AmbientReceiverReaders {
|
||||
callees.add(name);
|
||||
return true;
|
||||
};
|
||||
const expr = (expr: IrExpr): boolean => {
|
||||
if (!RECEIVER_FREE_EXPRS.has(expr.kind)) return false;
|
||||
switch (expr.kind) {
|
||||
case "call":
|
||||
return edge(expr.callee);
|
||||
case "new":
|
||||
return edge(`%${expr.className}.constructor`);
|
||||
case "virtualCall": {
|
||||
// Any class's override may run: admit only when every
|
||||
// same-named method is itself receiver-free.
|
||||
const targets = byMethod.get(expr.method) ?? [];
|
||||
return targets.length > 0 && targets.every(edge);
|
||||
}
|
||||
case "libCall":
|
||||
return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn);
|
||||
case "toString":
|
||||
return !opaqueOperand(expr.operand);
|
||||
default: {
|
||||
const operands = dataIntrinsicOperands(expr);
|
||||
return operands === null || !operands.some(opaqueOperand);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!reads)
|
||||
eachNodeOfKinds(fn, scan, (node, statement) => {
|
||||
const free = statement
|
||||
? RECEIVER_FREE_STMTS.has((node as IrStmt).kind)
|
||||
: expr(node as IrExpr);
|
||||
: this.admits(node as IrExpr, edge);
|
||||
if (!free) reads = true;
|
||||
return free;
|
||||
});
|
||||
|
||||
@@ -40,9 +40,11 @@ try {
|
||||
const module = deserializeModule(readFileSync(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
const llvm = emitLlvmModule(module, {});
|
||||
// The fallback calls the closure's entry, after giving it an undefined
|
||||
// receiver when it reads one, so its edge comes from receiver.done then.
|
||||
const guards = [
|
||||
...llvm.matchAll(
|
||||
/(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %callback\.indirect\d+ \]/g,
|
||||
/(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %(?:callback\.indirect|receiver\.done)\d+ \]/g,
|
||||
),
|
||||
];
|
||||
// The quiet every and map callbacks; not the throwing one.
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8230-function-record-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8231-function-table-shapes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8232-optional-function-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -98,7 +98,7 @@ typedef struct ScrClosure {
|
||||
* (like the dyn→closure edge): a cycle through it is merely never
|
||||
* collected. */
|
||||
ScrBox *props;
|
||||
uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0] */
|
||||
uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0]; bit 4: SCR_FN_RECEIVER_FREE */
|
||||
/* The root's `length` as boxed values report it; meaningful only when
|
||||
* identity is set (the root's own boxes carry their arity). */
|
||||
uint32_t identity_length;
|
||||
@@ -117,6 +117,13 @@ typedef struct ScrClosure {
|
||||
ScrBox *caps[];
|
||||
} ScrClosure;
|
||||
|
||||
/* function_kind bit 4: the body never observes the ambient receiver (the
|
||||
* `this` an unbound call binds), so compiled callers skip the receiver
|
||||
* bracket around an indirect call. Set only by the compiler; the kind
|
||||
* proper is function_kind & SCR_FN_KIND_MASK. */
|
||||
#define SCR_FN_RECEIVER_FREE 16u
|
||||
#define SCR_FN_KIND_MASK 15u
|
||||
|
||||
ScrClosure *scr_closure_new(void *fn, size_t ncaps); /* +1; caller fills caps with +1 box refs */
|
||||
|
||||
static inline ScrClosure *scr_closure_retain(ScrClosure *c) {
|
||||
|
||||
@@ -6908,7 +6908,7 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) {
|
||||
default: break;
|
||||
}
|
||||
if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj ||
|
||||
scr_closure_identity(constructor->v.fn.clo)->function_kind != 4) {
|
||||
(scr_closure_identity(constructor->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) != 4) {
|
||||
static const char message[] = "Class extends value is not a native ordinary constructor";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
@@ -7056,7 +7056,8 @@ void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) {
|
||||
/* Ordinary native function constructors retain their live prototype table.
|
||||
* A constructor may replace the fresh receiver with another object. */
|
||||
ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) {
|
||||
if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj || scr_closure_identity(callee->v.fn.clo)->function_kind != 4)
|
||||
if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj ||
|
||||
(scr_closure_identity(callee->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) != 4)
|
||||
return scr_bytes_construct(callee, args, what);
|
||||
ScrDyn *prototype = scr_dyn_fn_get(callee, "prototype", 9);
|
||||
if (!prototype) return NULL;
|
||||
@@ -9191,7 +9192,9 @@ bool scr_dyn_is_callable(const ScrDyn *value) {
|
||||
|
||||
int scr_dyn_function_kind(const ScrDyn *value) {
|
||||
while (value->kind == SCR_DYN_PROXY) value = value->v.proxy.target;
|
||||
return value->kind == SCR_DYN_FUNC ? scr_closure_identity(value->v.fn.clo)->function_kind : 0;
|
||||
return value->kind == SCR_DYN_FUNC
|
||||
? (int)(scr_closure_identity(value->v.fn.clo)->function_kind & SCR_FN_KIND_MASK)
|
||||
: 0;
|
||||
}
|
||||
|
||||
void scr_dyn_adopt_identity(ScrClosure *adapter, const ScrDyn *value) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Records of functions used as dispatch tables: keyed reads consumed at once
|
||||
// (`t[k]!(…)`, `t[k]?.(…)`, a checked local), missing keys, handlers that
|
||||
// declare fewer parameters than the table's type, handlers that replace,
|
||||
// delete or re-add table entries (their own included) while they run, and
|
||||
// key order after such mutation.
|
||||
type Handler = (x: number, y: number) => number;
|
||||
|
||||
let log: string[] = [];
|
||||
|
||||
function make(tag: string): Handler {
|
||||
let calls = 0;
|
||||
return (x, y) => {
|
||||
calls++;
|
||||
log.push(tag + ":" + calls);
|
||||
return x * 10 + y;
|
||||
};
|
||||
}
|
||||
|
||||
const table: Record<string, Handler> = {
|
||||
add: (x, y) => x + y,
|
||||
neg: (x) => -x,
|
||||
pow: (x, y) => x ** y,
|
||||
made: make("made"),
|
||||
};
|
||||
|
||||
function keys(): string[] {
|
||||
return process.argv.length > 99 ? [] : ["add", "neg", "pow", "made", "missing", ""];
|
||||
}
|
||||
|
||||
for (const k of keys()) {
|
||||
const h = table[k] as Handler | undefined;
|
||||
console.log(JSON.stringify(k), typeof h, h === undefined, h !== undefined && h === table[k]);
|
||||
console.log(" optional", table[k]?.(3, 4) ?? "none");
|
||||
if (h) console.log(" checked", h(2, 5));
|
||||
try {
|
||||
console.log(" asserted", table[k]!(5, 2));
|
||||
} catch (e) {
|
||||
console.log(" asserted threw", (e as Error).name);
|
||||
}
|
||||
}
|
||||
|
||||
// A handler that replaces its own entry while it runs: the replaced closure
|
||||
// keeps running with its captured state.
|
||||
function selfReplacing(): Handler {
|
||||
const state = { hits: 0, label: "first" };
|
||||
return (x, y) => {
|
||||
table["swap"] = make("second");
|
||||
state.hits++;
|
||||
log.push(state.label + " still running after replace, hits " + state.hits);
|
||||
return x + y + state.hits;
|
||||
};
|
||||
}
|
||||
table["swap"] = selfReplacing();
|
||||
console.log("swap 1", table["swap"]!(1, 2));
|
||||
console.log("swap 2", table["swap"]?.(1, 2));
|
||||
console.log("swap 3", table["swap"]!(1, 2));
|
||||
|
||||
// A handler that deletes its own entry.
|
||||
function selfDeleting(name: string): Handler {
|
||||
const captured = [name, "captured"];
|
||||
return (x) => {
|
||||
delete table[name];
|
||||
log.push(captured.join(" ") + " after delete");
|
||||
return x;
|
||||
};
|
||||
}
|
||||
table["gone"] = selfDeleting("gone");
|
||||
const goneKey = keys()[0] === "add" ? "gone" : "add";
|
||||
console.log("gone", table[goneKey]?.(7, 0) ?? "none", table[goneKey]?.(7, 0) ?? "none");
|
||||
|
||||
// A checked local keeps its closure after the entry changes.
|
||||
table["kept"] = make("kept-old");
|
||||
const kept = table["kept"] as Handler | undefined;
|
||||
table["kept"] = make("kept-new");
|
||||
delete table["kept"];
|
||||
if (kept) console.log("kept", kept(4, 4));
|
||||
console.log("kept entry", table["kept"] === undefined);
|
||||
|
||||
// Iteration while handlers add and delete keys: key order follows inserts.
|
||||
const order: Record<string, Handler> = {
|
||||
b: (x) => x + 1,
|
||||
a: (x) => {
|
||||
delete order["c"];
|
||||
order["z"] = (v) => v * 2;
|
||||
return x + 2;
|
||||
},
|
||||
c: (x) => x + 3,
|
||||
"1": (x) => x + 4,
|
||||
};
|
||||
for (const k in order) console.log("in", k, order[k]?.(1, 0));
|
||||
console.log("keys", Object.keys(order).join(","));
|
||||
for (const k of Object.keys(order)) {
|
||||
order[k + k] = (x) => x;
|
||||
console.log("of", k, order[k]!(10, 0));
|
||||
}
|
||||
console.log("keys", Object.keys(order).join(","));
|
||||
|
||||
// Reassigning the table binding itself from inside a handler.
|
||||
let current: Record<string, Handler> = {
|
||||
go: (x) => {
|
||||
current = { go: (y) => y * 100 };
|
||||
const local = [x, x];
|
||||
return local[0]! + local[1]!;
|
||||
},
|
||||
};
|
||||
console.log("rebind", current["go"]!(3, 0), current["go"]!(3, 0));
|
||||
console.log(log.join("\n"));
|
||||
@@ -0,0 +1,108 @@
|
||||
// Function dispatch through other table shapes: Map<string, fn>, arrays of
|
||||
// handlers, object literals of methods (with and without `this`), handler
|
||||
// mutation of the Map during calls, and adapters for handlers that declare
|
||||
// fewer parameters than the table type.
|
||||
type Op = (acc: number, arg: number) => number;
|
||||
|
||||
const trace: string[] = [];
|
||||
const byName = new Map<string, Op>();
|
||||
byName.set("add", (acc, arg) => acc + arg);
|
||||
byName.set("half", (acc) => acc / 2);
|
||||
byName.set("reset", () => {
|
||||
byName.delete("reset");
|
||||
byName.set("reset", (acc) => acc - 1);
|
||||
trace.push("reset replaced itself");
|
||||
return 0;
|
||||
});
|
||||
byName.set("clear", (acc) => {
|
||||
const keep = [acc, "before clear"];
|
||||
byName.clear();
|
||||
trace.push(keep.join(" ") + ", size " + byName.size);
|
||||
return acc;
|
||||
});
|
||||
|
||||
function program(): string[] {
|
||||
return process.argv.length > 99 ? [] : ["add", "half", "nop", "reset", "reset", "add", "clear", "add"];
|
||||
}
|
||||
|
||||
let acc = 8;
|
||||
for (const name of program()) {
|
||||
const handler = byName.get(name);
|
||||
if (handler !== undefined) acc = handler(acc, 3);
|
||||
else trace.push("no " + name);
|
||||
console.log(name, acc, byName.get(name)?.(1, 1) ?? "-");
|
||||
}
|
||||
|
||||
const byCode: Op[] = [(a, b) => a - b, (a) => a * 3, (a, b) => Math.max(a, b)];
|
||||
let total = 1;
|
||||
for (let i = 0; i < 9; i++) {
|
||||
total = byCode[i % 4]?.(total, i) ?? total + 100;
|
||||
if (i % 4 < byCode.length) total = byCode[i % 4]!(total, 1);
|
||||
}
|
||||
console.log("array", total);
|
||||
|
||||
const counter = {
|
||||
base: 5,
|
||||
bump(by: number): number {
|
||||
this.base += by;
|
||||
return this.base;
|
||||
},
|
||||
twice(by: number): number {
|
||||
return this.bump(by) + this.bump(by);
|
||||
},
|
||||
plain(by: number): number {
|
||||
return by * 2;
|
||||
},
|
||||
};
|
||||
console.log("methods", counter.bump(1), counter.twice(2), counter.plain(3), counter["plain"](4));
|
||||
|
||||
// Methods that read `this`, called through computed keys and through a
|
||||
// signature adapter (a method declaring fewer parameters than its type).
|
||||
interface Account {
|
||||
balance: number;
|
||||
deposit: (amount: number, note: string) => number;
|
||||
withdraw: (amount: number, note: string) => number;
|
||||
peek: (amount: number, note: string) => number;
|
||||
}
|
||||
const account: Account = {
|
||||
balance: 10,
|
||||
deposit(amount) {
|
||||
this.balance += amount;
|
||||
return this.balance;
|
||||
},
|
||||
withdraw(amount, note) {
|
||||
this.balance -= amount;
|
||||
trace.push("withdraw " + note);
|
||||
return this.balance;
|
||||
},
|
||||
peek: (amount) => amount,
|
||||
};
|
||||
for (const op of ["deposit", "withdraw", "peek", "deposit"] as const) {
|
||||
console.log(op, account[op](5, "n-" + op), account.balance);
|
||||
}
|
||||
const detached = account.peek;
|
||||
console.log("detached", detached(3, "x"));
|
||||
|
||||
interface Machine {
|
||||
state: string;
|
||||
on: Record<string, (m: Machine, input: number) => string>;
|
||||
}
|
||||
const machine: Machine = {
|
||||
state: "idle",
|
||||
on: {
|
||||
idle: (m, input) => (input > 0 ? "running" : m.state),
|
||||
running: (m, input) => {
|
||||
if (input < 0) {
|
||||
m.on["stopped"] = () => "idle";
|
||||
return "stopped";
|
||||
}
|
||||
return m.state;
|
||||
},
|
||||
},
|
||||
};
|
||||
for (const input of [0, 1, 2, -1, 5, 3]) {
|
||||
const next = machine.on[machine.state]?.(machine, input) ?? machine.state;
|
||||
console.log("machine", machine.state, input, "->", next);
|
||||
machine.state = next;
|
||||
}
|
||||
console.log(trace.join("\n"));
|
||||
@@ -0,0 +1,75 @@
|
||||
// Values typed `F | undefined` for a function type F: identity and
|
||||
// equality (signature adapters compare as the function they adapt), typeof,
|
||||
// truthiness, storage in arrays, records, class fields and Maps, optional
|
||||
// parameters and returns.
|
||||
type Unary = (x: number) => number;
|
||||
type Binary = (x: number, y: number) => number;
|
||||
|
||||
const double: Unary = (x) => x * 2;
|
||||
const asBinary: Binary = double;
|
||||
const maybe = (flag: boolean): Unary | undefined => (flag ? double : undefined);
|
||||
|
||||
const a = maybe(true);
|
||||
const b = maybe(true);
|
||||
const none = maybe(false);
|
||||
console.log(a === b, a === none, none === undefined, typeof a, typeof none, !!a, !!none);
|
||||
console.log(a === double, (asBinary as unknown) === (double as unknown));
|
||||
|
||||
const slots: (Unary | undefined)[] = [double, undefined, (x) => x + 1];
|
||||
console.log(slots.map((f) => (f ? f(5) : "empty")).join(" "), slots.length);
|
||||
console.log(slots.indexOf(double), slots.includes(undefined), slots.filter((f) => f !== undefined).length);
|
||||
|
||||
interface Hooks {
|
||||
before?: Unary;
|
||||
after?: Unary;
|
||||
}
|
||||
const hooks: Hooks = { after: (x) => x - 1 };
|
||||
console.log(hooks.before?.(1) ?? "no before", hooks.after?.(1) ?? "no after", "before" in hooks);
|
||||
|
||||
class Button {
|
||||
onClick: Unary | undefined;
|
||||
clicks = 0;
|
||||
click(): number {
|
||||
this.clicks++;
|
||||
return this.onClick?.(this.clicks) ?? -1;
|
||||
}
|
||||
}
|
||||
const button = new Button();
|
||||
console.log(button.click());
|
||||
button.onClick = (n) => n * 100;
|
||||
console.log(button.click(), button.click());
|
||||
button.onClick = undefined;
|
||||
console.log(button.click(), button.onClick === undefined);
|
||||
|
||||
const registry = new Map<string, Unary | undefined>([
|
||||
["set", double],
|
||||
["unset", undefined],
|
||||
]);
|
||||
console.log(registry.get("set")?.(4), registry.get("unset")?.(4), registry.has("unset"), registry.get("other") === undefined);
|
||||
|
||||
function run(x: number, transform?: Unary, combine?: Binary): number {
|
||||
const t = transform ? transform(x) : x;
|
||||
return combine === undefined ? t : combine(t, x);
|
||||
}
|
||||
console.log(run(3), run(3, double), run(3, undefined, (p, q) => p * q), run(3, (x) => -x, asBinary));
|
||||
|
||||
function pick(name: string): Binary | undefined {
|
||||
switch (name) {
|
||||
case "mul":
|
||||
return (x, y) => x * y;
|
||||
case "first":
|
||||
return (x) => x;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
for (const name of ["mul", "first", "none"]) {
|
||||
const f = pick(name);
|
||||
console.log(name, f?.(6, 7), f === undefined ? "undefined" : typeof f);
|
||||
}
|
||||
|
||||
let late: Unary | undefined;
|
||||
const callLate = (): number => late?.(9) ?? 0;
|
||||
console.log(callLate());
|
||||
late = (x) => x + 0.5;
|
||||
console.log(callLate());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4700, "san": 5800 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4700, "san": 5800 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4700, "san": 5800 }
|
||||
Reference in New Issue
Block a user