Keep maybe-missing number reads unboxed (#949)

This commit is contained in:
Chris Tate
2026-10-13 19:55:51 -05:00
committed by GitHub
24 changed files with 1152 additions and 121 deletions
@@ -73,7 +73,11 @@ function eligible(local: IrLocal): boolean {
* extracts and retains the payload, so the stored union is only projected. */
export type NullableFieldTest = (className: string, field: string) => boolean;
function collectUses(fn: IrFunction, nullableField: NullableFieldTest): Uses {
function collectUses(
fn: IrFunction,
nullableField: NullableFieldTest,
registerReturn: boolean,
): Uses {
const uses: Uses = {
invalid: new Set(),
written: new Set(),
@@ -161,6 +165,10 @@ function collectUses(fn: IrFunction, nullableField: NullableFieldTest): Uses {
}
function stmt(node: IrStmt, inExpression: boolean): boolean {
switch (node.kind) {
case "return":
// A register result reads the returned binding's tag and bits.
if (registerReturn && node.value?.kind === "varRef") return true;
break;
case "varDecl":
uses.declarations.set(node.localId, (uses.declarations.get(node.localId) ?? 0) + 1);
break;
@@ -220,6 +228,9 @@ export function analyzeCallLifetimes(
/** Rebound parameters that only ever hold borrowed walk pointers
* (walk-borrows.ts): they keep the borrowed convention although written. */
walkParameters: ReadonlyMap<string, ReadonlySet<number>> = new Map(),
/** Functions returning a scalar union as tag and payload bits
* (Int32Abi.union): `return x` only projects `x`. */
registerReturns: ReadonlySet<string> = new Set(),
): CallLifetimes {
const usesByFunction = new Map<string, Uses>();
const lazyCaptures = new LazyCaptures(functions);
@@ -234,7 +245,7 @@ export function analyzeCallLifetimes(
};
for (const fn of functions.values()) {
if (fn.async || fn.generator) continue;
const uses = collectUses(fn, nullableField);
const uses = collectUses(fn, nullableField, registerReturns.has(fn.name));
usesByFunction.set(fn.name, uses);
// Class-capturing bodies get only local facts (computed below from
// their uses; class captures are already invalid). Binding and
+155 -18
View File
@@ -82,7 +82,13 @@ import { analyzeInt32Slots, type Int32Slots } from "../../ir/int32-slots.js";
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
import { foldConstantGlobals } from "../../ir/constant-globals.js";
import { findIntegerViews } from "./integer-views.js";
import { findInt32Abis, INT32_ABI_SUFFIX, type Int32Abi } from "./int32-abi.js";
import {
findInt32Abis,
INT32_ABI_SUFFIX,
SCALAR_UNION_DUMMY,
SCALAR_UNION_RESULT,
type Int32Abi,
} from "./int32-abi.js";
import { findInt32Locals, type Int32Local } from "./int32-locals.js";
import { exactInteger, narrowInteger } from "./integer-values.js";
import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
@@ -119,10 +125,13 @@ import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
import { canStackUnion, emitStackUnion, findLocalStackUnions } from "./stack-unions.js";
import {
allocateLocalUnion,
emitScalarOptional,
findLocalUnionStorage,
isScalarOptionalSource,
storeLocalUnion,
type LocalUnionStorageProof,
type LocalUnionStorage,
type UnionSources,
} from "./local-union-storage.js";
import { BlockBuilder } from "./blocks.js";
import { NullableRefFields, type NullableRefField } from "./nullable-fields.js";
@@ -610,6 +619,8 @@ export class LlEmitter {
* whose bindings end with the list (emitBlock and function bodies). */
private stmtCursor: { stmts: IrStmt[]; index: number } | null = null;
readonly optionalArrayReads: OptionalArrayReads;
/** Union sources a binding or projection can hold in a stack box. */
readonly unionSources: UnionSources;
readonly checkedNarrows: CheckedNarrows;
callArrayReads = new Map<IrExpr, LocalArrayRead>();
mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() };
@@ -843,6 +854,9 @@ export class LlEmitter {
currentReturnType: IrType = VOID;
/** The current body returns its number result as i32. */
private currentInt32Result = false;
/** The function's i32 entry returns its scalar union result as tag and
* payload bits (Int32Abi.union). */
private currentUnionResult = false;
/** Active only while emitting a body lowered with LLVM's
* switched-coroutine intrinsics (wasm32 async bodies and generators,
* native synchronous generators; see coroLowered). */
@@ -967,6 +981,13 @@ export class LlEmitter {
this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) =>
this.nullableUnions.has(id),
);
this.unionSources = {
unions: this.boxedUnionsById,
receiverUnions: this.unionsById,
nullableField: (e) => this.nullableFieldGet(e) !== null,
arrayRead: (e) => this.optionalArrayReads.get(e) !== null,
scalarCall: (e) => this.scalarResultCall(e),
};
this.checkedNarrows = new CheckedNarrows(this.fnByName);
this.referenceEffects = new ReferenceEffects(
this.fnByName,
@@ -1026,6 +1047,7 @@ export class LlEmitter {
this.fnByName,
(className, field) => this.nullableFields.get(className, field) !== null,
walkParameters,
new Set([...this.int32Abis].filter(([, abi]) => abi.union).map(([name]) => name)),
);
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
this.stackCallbacks = new StackCallbacks(this.fnByName);
@@ -4138,11 +4160,13 @@ export class LlEmitter {
? "ret void"
: this.currentInt32Result
? "ret i32 0"
: t === "double"
? `ret double ${f64Lit(0)}`
: t === "i1"
? "ret i1 false"
: "ret ptr null";
: this.currentUnionResult
? `ret ${SCALAR_UNION_DUMMY}`
: t === "double"
? `ret double ${f64Lit(0)}`
: t === "i1"
? "ret i1 false"
: "ret ptr null";
}
// Copy the entries now: later declarations extend these lexical scopes.
// Release order and the handler are part of the shared block's identity.
@@ -5434,12 +5458,7 @@ export class LlEmitter {
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
this.localStackUnions = findLocalStackUnions(fn, this.callLifetimes, this.boxedUnionsById);
this.localUnionStorageProofs = findLocalUnionStorage(
fn,
this.callLifetimes,
this.boxedUnionsById,
(e) => this.nullableFieldGet(e) !== null,
);
this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources);
this.localUnionStorage = new Map();
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
this.integerRanges = analyzeIntegerRanges(numericFn, this.int32Slots.facts(fn.name));
@@ -5465,6 +5484,7 @@ export class LlEmitter {
this.currentReturnType = fn.returnType;
const int32Abi = this.int32Abis.get(fn.name);
this.currentInt32Result = int32Abi?.result === true;
this.currentUnionResult = int32Abi?.union === true;
this.currentGenerator = fn.generator ?? null;
this.currentAsyncGenerator = fn.async === true && fn.generator !== undefined;
this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
@@ -5734,7 +5754,11 @@ export class LlEmitter {
});
// Lifted functions receive their closure first (the callValue ABI).
if (fn.captures !== undefined) params.unshift("ptr %sc_env");
const ret = int32Abi?.result ? "i32" : this.llType(fn.returnType);
const ret = int32Abi?.result
? "i32"
: int32Abi?.union
? SCALAR_UNION_RESULT
: this.llType(fn.returnType);
let attrs = coro !== null ? "#1" : FN_ATTRS;
// The shared indexed-read helper (lowerSafeIndexRead) is a dense bounds
// test, a state test and a load behind cold runtime fallbacks, called
@@ -5771,7 +5795,18 @@ export class LlEmitter {
} else args.push(`${type} %p${index}`);
});
const ret = this.llType(fn.returnType);
const call = `call ${abi.result ? "i32" : ret} @${symbol}${INT32_ABI_SUFFIX}(${args.join(", ")})`;
const entry = `@${symbol}${INT32_ABI_SUFFIX}(${args.join(", ")})`;
if (abi.union) {
// Box the register pair for every caller of the ordinary ABI.
const B = new BlockBuilder();
this.B = B;
for (const line of lines) B.line(line.trim());
const pair = B.tmp();
B.line(`${pair} = call ${SCALAR_UNION_RESULT} ${entry}`);
B.terminate(`ret ptr ${this.boxScalarUnion(pair, fn.returnType)}`);
return `define internal ptr @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
}
const call = `call ${abi.result ? "i32" : ret} ${entry}`;
if (ret === "void") lines.push(` ${call}`, " ret void");
else if (abi.result)
lines.push(` %r = ${call}`, ` %d = sitofp i32 %r to double`, ` ret double %d`);
@@ -5779,6 +5814,86 @@ export class LlEmitter {
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\nentry:\n${lines.join("\n")}\n}`;
}
/** The SCALAR_UNION_RESULT pair of a `%ScrUnion` box of `type`. */
scalarUnionPair(box: string, type: IrType): string {
const B = this.B;
const unionId = type.kind === "union" ? type.unionId : "";
const tag = this.unionTag(box, unionId);
const payload = B.tmp(),
bits = B.tmp(),
partial = B.tmp(),
pair = B.tmp();
B.line(`${payload} = getelementptr inbounds %ScrUnion, ptr ${box}, i32 0, i32 5`);
B.line(`${bits} = load i64, ptr ${payload}`);
B.line(`${partial} = insertvalue ${SCALAR_UNION_RESULT} poison, i32 ${tag}, 0`);
B.line(`${pair} = insertvalue ${SCALAR_UNION_RESULT} ${partial}, i64 ${bits}, 1`);
return pair;
}
/** A private stack box holding a SCALAR_UNION_RESULT pair of `type`. */
stackScalarUnion(pair: string, type: IrType): LlValue {
const B = this.B;
const box = B.slot(),
tagSlot = B.slot(),
payloadSlot = B.slot();
B.entryAllocas.push(`${box} = alloca %ScrUnion`);
B.entryAllocas.push(`${tagSlot} = getelementptr inbounds %ScrUnion, ptr ${box}, i32 0, i32 1`);
B.entryAllocas.push(
`${payloadSlot} = getelementptr inbounds %ScrUnion, ptr ${box}, i32 0, i32 5`,
);
const tag = B.tmp(),
bits = B.tmp();
B.line(`${tag} = extractvalue ${SCALAR_UNION_RESULT} ${pair}, 0`);
B.line(`${bits} = extractvalue ${SCALAR_UNION_RESULT} ${pair}, 1`);
B.line(`store i32 ${tag}, ptr ${tagSlot}`);
B.line(`store i64 ${bits}, ptr ${payloadSlot}`);
return { name: box, type };
}
/** A direct call whose i32 entry returns its scalar union in registers. */
scalarResultCall(e: IrExpr): boolean {
return e.kind === "call" && this.int32Abis.get(e.callee)?.union === true;
}
/** A fresh heap box (+1) for a SCALAR_UNION_RESULT pair of `type`; the
* exception dummy (tag -1) answers NULL. */
boxScalarUnion(pair: string, type: IrType): string {
const B = this.B;
const unionId = type.kind === "union" ? type.unionId : "";
const arms = this.unionsById.get(unionId)!.arms;
const tag = B.tmp(),
bits = B.tmp();
B.line(`${tag} = extractvalue ${SCALAR_UNION_RESULT} ${pair}, 0`);
B.line(`${bits} = extractvalue ${SCALAR_UNION_RESULT} ${pair}, 1`);
const join = B.newLabel("union.box.join"),
none = B.newLabel("union.box.none");
const labels = arms.map(() => B.newLabel("union.box.arm"));
B.terminate(
`switch i32 ${tag}, label %${none} [ ${labels.map((label, armTag) => `i32 ${armTag}, label %${label}`).join(" ")} ]`,
);
const incoming: string[] = [];
arms.forEach((arm, armTag) => {
B.startBlock(labels[armTag]!);
let value: string;
if (isUnitType(arm)) value = this.unitInstanceRef(unionId, armTag);
else {
const payload = B.tmp();
if (arm.kind === "bool") B.line(`${payload} = trunc i64 ${bits} to i1`);
else B.line(`${payload} = bitcast i64 ${bits} to double`);
value = this.unionNewOwned(unionId, armTag, { name: payload, type: arm });
}
incoming.push(`[ ${value}, %${B.currentLabel()} ]`);
B.br(join);
});
B.startBlock(none);
incoming.push(`[ null, %${none} ]`);
B.br(join);
B.startBlock(join);
const box = B.tmp();
B.line(`${box} = phi ptr ${incoming.join(", ")}`);
return box;
}
/** The i32 bits of a number proven an int32 (a direct call's i32
* argument or an i32 result): its exact integer when the value carries
* one, else the conversion the proof makes exact. */
@@ -7317,8 +7432,16 @@ export class LlEmitter {
break;
}
let v: LlValue | null = null;
const moved = this.finallyStack.length === 0 ? this.movableReturn(s.value) : null;
if (moved !== null) {
const moved =
this.finallyStack.length === 0 && !this.currentUnionResult
? this.movableReturn(s.value)
: null;
if (s.value !== null && this.currentUnionResult) {
// The tag and payload bits leave in registers; a heap box the
// value needed is released with its frame below.
const box = this.emitUnionProjection(s.value);
v = { name: this.scalarUnionPair(box.name, s.value.type), type: s.value.type };
} else if (moved !== null) {
// Returning an owned local hands its reference to the caller: no
// retain now and no release of the local below.
const t = B.tmp();
@@ -7359,7 +7482,7 @@ export class LlEmitter {
}
if (this.finallyStack.length > 0) {
let pretSlot: string | null = null;
if (v !== null && isRefCounted(v.type)) {
if (v !== null && isRefCounted(v.type) && !this.currentUnionResult) {
pretSlot = B.slot();
B.entryAllocas.push(`${pretSlot} = alloca ptr ; pending return (through finally)`);
B.line(`store ptr ${v.name}, ptr ${pretSlot}`);
@@ -7395,6 +7518,7 @@ export class LlEmitter {
} else if (v === null) B.terminate("ret void");
else if (this.currentInt32Result)
B.terminate(`ret i32 ${this.int32Argument(v, s.value!)}`);
else if (this.currentUnionResult) B.terminate(`ret ${SCALAR_UNION_RESULT} ${v.name}`);
else B.terminate(`ret ${this.llType(s.value!.type)} ${v.name}`);
}
break;
@@ -8328,6 +8452,17 @@ export class LlEmitter {
B.line(`${value} = load ptr, ptr ${slot}`);
return { name: value, type: e.type };
}
// Scalar optional values and register results answer a private stack
// box. A ternary of borrowable arms above keeps lending its arm.
if (
(e.kind === "optChain" ||
e.kind === "nullish" ||
e.kind === "ternary" ||
e.kind === "arrIntrinsic") &&
isScalarOptionalSource(e, this.unionSources)
)
return emitScalarOptional(this, e);
if (e.kind === "call" && this.scalarResultCall(e)) return emitCallExpr(this, e, false, true);
if (e.kind === "call" && this.canBorrowReceiver(e)) return emitBorrowedResultCall(this, e);
return this.emitExpr(e);
}
@@ -8344,7 +8479,9 @@ export class LlEmitter {
return (
canStackUnion(e, this.boxedUnionsById) ||
matchMapRead(e, this.boxedUnionsById) !== null ||
this.optionalArrayReads.get(e) !== null
this.optionalArrayReads.get(e) !== null ||
isScalarOptionalSource(e, this.unionSources) ||
this.scalarResultCall(e)
);
}
@@ -9,7 +9,7 @@ import {
type IrExpr,
type IrFunction,
} from "../../ir/ir.js";
import { INT32_ABI_SUFFIX } from "./int32-abi.js";
import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
import {
mangleAsyncAdoptCall,
@@ -30,6 +30,7 @@ import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js";
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
import { emitCallArrayRead } from "./local-array-reads.js";
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
import { emitStackMapRead } from "./map-read-lifetimes.js";
import {
borrowableInputs,
@@ -58,6 +59,9 @@ export function emitCallExpr(
| "virtualCall"
>,
awaitEntry: false | "await" | "adopt" = false,
/** A projection consumes a scalar union result (Int32Abi.union): answer
* a private stack box instead of a heap box. */
stackResult = false,
): LlValue {
const B = host.B;
switch (e.kind) {
@@ -111,6 +115,16 @@ export function emitCallExpr(
return result.value;
}
if (canStackUnion(a, host.boxedUnionsById)) return emitStackUnion(host, a).value;
if (
(a.kind === "optChain" ||
a.kind === "nullish" ||
a.kind === "ternary" ||
a.kind === "arrIntrinsic") &&
isScalarOptionalSource(a, host.unionSources)
)
return emitScalarOptional(host, a);
if (a.kind === "call" && host.scalarResultCall(a))
return emitCallExpr(host, a, false, true);
if (host.nullableFieldGet(a))
return { name: host.emitOwnedNullableStack(a).box, type: a.type };
}
@@ -164,6 +178,13 @@ export function emitCallExpr(
return { name: "", type: e.type };
}
const t = B.tmp();
if (abi?.union) {
B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`);
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
if (borrowed) host.releaseFrame(host.frames.pop()!);
if (stackResult) return host.stackScalarUnion(t, e.type);
return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type });
}
if (abi?.result) {
B.line(`${t} = call i32 ${target}(${argList})`);
const out = host.int32Result(t, e.type);
@@ -513,6 +513,9 @@ export function emitStableReceiver(
export function emitArrIntrinsic(
host: LlvmEmitterContext,
e: IrExpr & { kind: "arrIntrinsic" },
/** A `pop`/`shift` of number or boolean elements stores its scalar
* union result's tag and payload bits here instead of boxing it. */
into?: { box: string; tag: string; payload: string },
): LlValue {
const B = host.B;
// Every array intrinsic borrows its receiver, including mutation and
@@ -656,6 +659,25 @@ export function emitArrIntrinsic(
const raw = B.tmp();
const value = B.tmp();
B.line(`${raw} = load i64, ptr ${rawSlot}`);
if (into) {
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
let bits = raw;
if (elem.kind === "bool") {
const nonzero = B.tmp();
bits = B.tmp();
B.line(`${nonzero} = icmp ne i64 ${raw}, 0`);
B.line(`${bits} = zext i1 ${nonzero} to i64`);
}
B.line(`store i32 ${tag}, ptr ${into.tag}`);
B.line(`store i64 ${bits}, ptr ${into.payload}`);
B.br(lj);
B.startBlock(la);
B.line(`store i32 ${undefTag}, ptr ${into.tag}`);
B.line(`store i64 0, ptr ${into.payload}`);
B.br(lj);
B.startBlock(lj);
return { name: into.box, type: e.type };
}
if (elem.kind === "f64") B.line(`${value} = bitcast i64 ${raw} to double`);
else if (elem.kind === "bool") B.line(`${value} = icmp ne i64 ${raw}, 0`);
else B.line(`${value} = inttoptr i64 ${raw} to ptr`);
@@ -193,81 +193,14 @@ export function emitControlExpr(
return host.own({ name: t, type: e.type });
}
if (e.receiver.type.kind !== "union")
throw new LlvmUnsupportedError(`optChain:${e.receiver.type.kind}`, e.loc);
const def = host.unionsById.get(e.receiver.type.unionId);
if (!def)
throw new InternalCompilerError(
`llvm emitter bug: optChain of unknown union ${e.receiver.type.unionId}`,
);
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
const narrowIdx = def.arms.findIndex((a) => !isUnitType(a));
if (unitTags.length === 0 || narrowIdx < 0)
throw new InternalCompilerError("llvm emitter bug: optChain union arms");
const multiple = def.arms.length - unitTags.length > 1;
const narrowed = multiple ? e.receiver.type : def.arms[narrowIdx]!;
// `owner.field?.member`: a receiver projected from a borrowable owner
// 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);
// 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.
const nullableRead =
!borrowReceiver &&
!multiple &&
host.nullableUnions.has(def.id) &&
host.optionalArrayReads.get(e.receiver) !== null;
// A single present arm is only tested and extracted (+1) before the
// body runs, so a fresh receiver box can stay on the stack.
const stacked =
!borrowReceiver && !nullableRead && !multiple && host.canStackReceiver(e.receiver);
let r: LlValue;
const nullableField = borrowReceiver ? host.nullableFieldGet(e.receiver) : null;
if (nullableField !== null && e.receiver.kind === "fieldGet") {
// A nullable-pointer field stores no union: test and peek the
// pointer itself, through a private stack box when the union's own
// representation is boxed (emitUnionProjection's form).
const owner = host.emitReadReceiver(e.receiver.obj);
const { ptr } = host.classFieldPtr(owner.name, e.receiver.className, e.receiver.field);
const p = B.tmp();
B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
r = {
name: host.nullableUnions.get(def.id) ? p : host.stackNullableBox(p, nullableField),
type: e.receiver.type,
};
} else if (borrowReceiver || stacked || nullableRead) r = host.emitReadReceiver(e.receiver);
else r = host.emitExpr(e.receiver);
// The bind slot borrows a ref payload whenever something else keeps it
// alive until the body finishes: the borrowed projection above, or the
// receiver value the statement frame owns (emitExpr). Stack receivers
// may borrow their payload from a container, so they keep their +1.
const borrowBind = !multiple && !stacked && isRefCounted(narrowed);
const bind = B.slot();
B.entryAllocas.push(`${bind} = alloca ${host.llType(narrowed)}`);
B.line(
`store ${host.llType(narrowed)} ${host.llType(narrowed) === "ptr" ? "null" : host.llType(narrowed) === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
);
if (!borrowBind) host.ownSlot(bind, narrowed);
const isUnit = host.tagInSet(r.name, def.id, unitTags);
const extract = (): string =>
multiple
? host.retainValue(r.name, narrowed)
: borrowBind
? host.unionPeek(r.name, def.id)
: host.unionExtract(r.name, def.id, narrowed);
const { isUnit, bindPresent } = emitOptionalChainReceiver(host, e);
if (e.type.kind === "void") {
// Statement form (cb?.()): no result value at all.
const lb = B.newLabel("oc.b");
const lj = B.newLabel("oc.j");
B.condBr(isUnit, lj, lb);
B.startBlock(lb);
B.line(`store ${host.llType(narrowed)} ${extract()}, ptr ${bind}`);
host.chainSlots.set(e.id, { name: bind, type: narrowed, slot: true });
bindPresent();
host.frames.push([]);
host.emitExpr(e.body);
host.releaseFrame(host.frames.pop()!);
@@ -296,8 +229,7 @@ export function emitControlExpr(
B.line(`store ptr ${ur}, ptr ${slotD}`);
B.br(lj);
B.startBlock(lb);
B.line(`store ${host.llType(narrowed)} ${extract()}, ptr ${bind}`);
host.chainSlots.set(e.id, { name: bind, type: narrowed, slot: true });
bindPresent();
host.emitBranchInto(slotD, e.body);
host.chainSlots.delete(e.id);
B.br(lj);
@@ -324,8 +256,7 @@ export function emitControlExpr(
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`);
B.br(lj);
B.startBlock(lb);
B.line(`store ${host.llType(narrowed)} ${extract()}, ptr ${bind}`);
host.chainSlots.set(e.id, { name: bind, type: narrowed, slot: true });
bindPresent();
host.emitBranchInto(slot, e.body);
host.chainSlots.delete(e.id);
B.br(lj);
@@ -349,6 +280,34 @@ export function emitControlExpr(
// arm (+1 for ref kinds), falsy releases and runs the default.
if (e.left.type.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: orDefault left is not a union");
if (
e.retag === undefined &&
!host.nullableUnions.has(e.left.type.unionId) &&
host.isStackUnionSource(e.left)
) {
// A stack box (`arr[i] || d`): test truthiness and extract the
// payload (+1). The box is never retained or released.
const l = host.emitUnionProjection(e.left);
const ty = host.llType(e.type);
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${ty}`);
const lt = B.newLabel("ord.t"),
lf = B.newLabel("ord.f"),
lj = B.newLabel("ord.j");
B.condBr(host.truthy(l), lt, lf);
B.startBlock(lt);
B.line(
`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`,
);
B.br(lj);
B.startBlock(lf);
host.emitBranchInto(slot, e.right);
B.br(lj);
B.startBlock(lj);
const t = B.tmp();
B.line(`${t} = load ${ty}, ptr ${slot}`);
return host.own({ name: t, type: e.type });
}
const l = host.emitExpr(e.left);
host.moveTemp(l);
const ty = host.llType(e.type);
@@ -502,6 +461,91 @@ export function emitControlExpr(
}
}
/** The receiver half of a union-receiver `a?.b`. The receiver evaluates
* once; `isUnit` tests its unit arms, and `bindPresent` fills the chain's
* bind slot with the narrowed payload for the body to read through
* chainRecv. The caller deletes the chain slot after the body. */
export function emitOptionalChainReceiver(
host: LlvmEmitterContext,
e: ExprOf<"optChain">,
): { isUnit: string; bindPresent: () => void } {
const B = host.B;
if (e.receiver.type.kind !== "union")
throw new LlvmUnsupportedError(`optChain:${e.receiver.type.kind}`, e.loc);
const def = host.unionsById.get(e.receiver.type.unionId);
if (!def)
throw new InternalCompilerError(
`llvm emitter bug: optChain of unknown union ${e.receiver.type.unionId}`,
);
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
const narrowIdx = def.arms.findIndex((a) => !isUnitType(a));
if (unitTags.length === 0 || narrowIdx < 0)
throw new InternalCompilerError("llvm emitter bug: optChain union arms");
const multiple = def.arms.length - unitTags.length > 1;
const narrowed = multiple ? e.receiver.type : def.arms[narrowIdx]!;
// `owner.field?.member`: a receiver projected from a borrowable owner
// 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);
// 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.
const nullableRead =
!borrowReceiver &&
!multiple &&
host.nullableUnions.has(def.id) &&
host.optionalArrayReads.get(e.receiver) !== null;
// A single present arm is only tested and extracted (+1) before the
// body runs, so a fresh receiver box can stay on the stack.
const stacked =
!borrowReceiver && !nullableRead && !multiple && host.canStackReceiver(e.receiver);
let r: LlValue;
const nullableField = borrowReceiver ? host.nullableFieldGet(e.receiver) : null;
if (nullableField !== null && e.receiver.kind === "fieldGet") {
// A nullable-pointer field stores no union: test and peek the
// pointer itself, through a private stack box when the union's own
// representation is boxed (emitUnionProjection's form).
const owner = host.emitReadReceiver(e.receiver.obj);
const { ptr } = host.classFieldPtr(owner.name, e.receiver.className, e.receiver.field);
const p = B.tmp();
B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
r = {
name: host.nullableUnions.get(def.id) ? p : host.stackNullableBox(p, nullableField),
type: e.receiver.type,
};
} else if (borrowReceiver || stacked || nullableRead) r = host.emitReadReceiver(e.receiver);
else r = host.emitExpr(e.receiver);
// The bind slot borrows a ref payload whenever something else keeps it
// alive until the body finishes: the borrowed projection above, or the
// receiver value the statement frame owns (emitExpr). Stack receivers
// may borrow their payload from a container, so they keep their +1.
const borrowBind = !multiple && !stacked && isRefCounted(narrowed);
const bind = B.slot();
B.entryAllocas.push(`${bind} = alloca ${host.llType(narrowed)}`);
B.line(
`store ${host.llType(narrowed)} ${host.llType(narrowed) === "ptr" ? "null" : host.llType(narrowed) === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
);
if (!borrowBind) host.ownSlot(bind, narrowed);
const isUnit = host.tagInSet(r.name, def.id, unitTags);
const extract = (): string =>
multiple
? host.retainValue(r.name, narrowed)
: borrowBind
? host.unionPeek(r.name, def.id)
: host.unionExtract(r.name, def.id, narrowed);
return {
isUnit,
bindPresent: () => {
B.line(`store ${host.llType(narrowed)} ${extract()}, ptr ${bind}`);
host.chainSlots.set(e.id, { name: bind, type: narrowed, slot: true });
},
};
}
/** `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
@@ -3,12 +3,14 @@ import {
BOOL,
F64,
STRING,
UNDEFINED_T,
VOID,
funcOf,
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";
@@ -133,3 +135,96 @@ test("debug builds keep the double ABI", () => {
expect(llvm).not.toContain(".i32(");
expect(llvm).toMatch(/define internal double @sc_f_mix\(double %p_\w+, double %p_\w+\)/);
});
const optional: IrType = { kind: "union", unionId: "optional" };
const wrap = (value: IrExpr, tag: number): IrExpr => ({
kind: "unionWrap",
value,
tag,
unionId: "optional",
type: optional,
loc,
});
const missing = (): IrExpr =>
wrap({ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, 1);
function unionModule(): IrModule {
const positive: IrExpr = {
kind: "bin",
op: ">",
left: ref("n"),
right: num(0),
type: BOOL,
loc,
};
return {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
unions: [{ id: "optional", arms: [F64, UNDEFINED_T] }],
functions: [
fn(
"main",
[],
[
stmt({
kind: "nullish",
left: call("maybe", [num(3)], optional),
right: num(0),
type: F64,
loc,
}),
stmt({
kind: "closure",
fnName: "maybe",
captures: [],
type: funcOf([F64], optional),
loc,
}),
],
VOID,
),
fn(
"maybe",
["n"],
[
stmt(call("throws", [], VOID)),
ret({
kind: "ternary",
cond: positive,
then: wrap(ref("n"), 0),
else_: missing(),
type: optional,
loc,
}),
],
optional,
),
fn(
"throws",
[],
[{ kind: "throw", value: { kind: "strLit", value: "no", type: STRING, loc }, loc }],
VOID,
),
],
};
}
test("scalar union results return tag and payload bits on direct calls", () => {
const mod = unionModule();
expect(validateModule(mod)).toEqual([]);
const llvm = emitLlvmModule(mod);
const entry = define(llvm, "sc_f_maybe.i32");
expect(entry).toMatch(/^define internal \{ i32, i64 \} @sc_f_maybe\.i32\(double %p_\w+\)/);
expect(entry).toContain("ret { i32, i64 } { i32 -1, i64 0 }");
expect(entry).not.toContain("@scr_union_new");
// A projecting caller reads the pair without a heap box.
const main = define(llvm, "sc_f_main");
expect(main).toContain("call { i32, i64 } @sc_f_maybe.i32(double ");
expect(main).not.toContain("@scr_union_new");
// The closure keeps the boxed result of the ordinary symbol.
const wrapper = define(llvm, "sc_f_maybe");
expect(wrapper).toContain("define internal ptr @sc_f_maybe(double %p0)");
expect(wrapper).toContain("call { i32, i64 } @sc_f_maybe.i32(double %p0)");
expect(wrapper).toContain("call ptr @scr_union_new_f64(i32 0, double ");
});
@@ -1,14 +1,33 @@
import type { IrExpr, IrFunction, IrModule } from "../../ir/ir.js";
import {
isUnitType,
type IrExpr,
type IrFunction,
type IrModule,
type IrType,
} from "../../ir/ir.js";
import type { Int32Slots } from "../../ir/int32-slots.js";
import { everyStmtList } from "../../ir/traverse.js";
/** The i32 entry of a function: which number parameters it receives as
* i32, and whether its number result is returned as i32. */
* i32, and whether its number result is returned as i32 or its scalar
* union result as tag and payload bits. */
export interface Int32Abi {
params: readonly boolean[];
result: boolean;
/** The result is a union of numbers, booleans and unit values (such as
* `number | undefined`), returned as SCALAR_UNION_RESULT instead of a
* heap box. */
union: boolean;
}
/** The register pair of a scalar union result: the union tag and the
* payload bits of the `%ScrUnion` box it replaces. */
export const SCALAR_UNION_RESULT = "{ i32, i64 }";
/** The result of an entry that left a pending exception: no arm has tag
* -1, so the double-ABI wrapper answers the usual NULL dummy. */
export const SCALAR_UNION_DUMMY = `${SCALAR_UNION_RESULT} { i32 -1, i64 0 }`;
/** Suffix of a function's i32 entry symbol. The ordinary symbol stays a
* double-ABI wrapper around it. */
export const INT32_ABI_SUFFIX = ".i32";
@@ -26,6 +45,11 @@ export const INT32_ABI_SUFFIX = ".i32";
* fib(n - 2)`, keeps its double: the incoming double is that result, where
* an i32 would add a conversion to the path that returns it.
*
* A scalar union result (every arm a number, a boolean or a unit value)
* owns no reference, so the entry returns its tag and payload bits in
* registers. Callers that only project it never build the heap box; the
* others box it at the call, as the double-ABI wrapper does.
*
* Only direct `call` and `new` sites use the i32 entry. Every other way to
* reach the function (closure and virtual-dispatch entries, constructor
* thunks, adapters, the runtime) keeps the double-ABI symbol, which becomes
@@ -34,10 +58,12 @@ export const INT32_ABI_SUFFIX = ".i32";
* debug builds (numbers stay doubles for the debugger) select none. */
export function findInt32Abis(mod: IrModule, slots: Int32Slots): Map<string, Int32Abi> {
const result = new Map<string, Int32Abi>();
const unions = new Map((mod.unions ?? []).map((def) => [def.id, def.arms]));
for (const fn of mod.functions) {
if (fn.async || fn.generator || fn.captures !== undefined || !slots.called.has(fn.name))
continue;
const ret = fn.returnType.kind === "f64" && slots.hasReturn(fn.name);
const union = scalarUnionResult(fn, unions);
let params = fn.params.map(
(param) => param.type.kind === "f64" && slots.hasParam(fn.name, param.localId),
);
@@ -45,11 +71,23 @@ export function findInt32Abis(mod: IrModule, slots: Int32Slots): Map<string, Int
const returned = returnedLocals(fn);
params = params.map((i32, index) => i32 && !returned.has(fn.params[index]!.localId));
}
if (ret || params.includes(true)) result.set(fn.name, { params, result: ret });
if (ret || union || params.includes(true)) result.set(fn.name, { params, result: ret, union });
}
return result;
}
function scalarUnionResult(
fn: IrFunction,
unions: ReadonlyMap<string, readonly IrType[]>,
): boolean {
if (fn.returnType.kind !== "union") return false;
const arms = unions.get(fn.returnType.unionId);
return (
arms !== undefined &&
arms.every((arm) => arm.kind === "f64" || arm.kind === "bool" || isUnitType(arm))
);
}
/** Locals some `return` hands back as its value (directly or as an arm of
* a conditional). */
function returnedLocals(fn: IrFunction): Set<string> {
@@ -91,11 +91,14 @@ function fixture(): IrModule {
}
function facts(mod: IrModule) {
const lifetimes = analyzeCallLifetimes(new Map(mod.functions.map((fn) => [fn.name, fn])));
return findLocalUnionStorage(
mod.functions[1]!,
lifetimes,
new Map(mod.unions!.map((u) => [u.id, u])),
);
const unions = new Map(mod.unions!.map((u) => [u.id, u]));
return findLocalUnionStorage(mod.functions[1]!, lifetimes, {
unions,
receiverUnions: unions,
nullableField: () => false,
arrayRead: () => false,
scalarCall: () => false,
});
}
function body(mod: IrModule, pointerBits: 32 | 64): string {
expect(validateModule(mod)).toEqual([]);
@@ -320,3 +323,102 @@ test("equality whose other operand captures the whole local keeps the heap path"
};
expect(facts(mod).has("item")).toBe(false);
});
test("number bindings of optional reads and `??` chains stay in stack storage", () => {
const maybe: IrType = { kind: "union", unionId: "maybe" };
const numbers: IrType = { kind: "array", elem: F64 };
const read = (index: IrExpr): IrExpr => ({
kind: "ternary",
cond: {
kind: "bin",
op: "===",
left: { kind: "arrayState", arr: ref("values", numbers), index, type: F64, loc },
right: { kind: "numLit", value: 1, type: F64, loc },
type: BOOL,
loc,
},
then: {
kind: "unionWrap",
value: { kind: "arrayGet", arr: ref("values", numbers), index, type: F64, loc },
tag: 0,
unionId: "maybe",
type: maybe,
loc,
},
else_: {
kind: "unionWrap",
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
tag: 1,
unionId: "maybe",
type: maybe,
loc,
},
type: maybe,
loc,
});
const zero: IrExpr = { kind: "numLit", value: 0, type: F64, loc };
const mod: IrModule = {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
unions: [{ id: "maybe", arms: [F64, UNDEFINED_T] }],
functions: [
{ name: "main", params: [], locals: [], body: [], returnType: VOID, loc },
{
name: "work",
loc,
returnType: F64,
params: [
{ localId: "values", name: "values", type: numbers },
{ localId: "i", name: "i", type: F64 },
{ localId: "choose", name: "choose", type: BOOL },
],
locals: [
{ id: "values", name: "values", type: numbers, mutable: true },
{ id: "i", name: "i", type: F64, mutable: true },
{ id: "choose", name: "choose", type: BOOL, mutable: true },
{ id: "value", name: "value", type: maybe, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "value",
loc,
init: {
kind: "nullish",
left: read(ref("i", F64)),
right: read(zero),
type: maybe,
loc,
},
},
{
kind: "assign",
localId: "value",
loc,
value: {
kind: "ternary",
cond: ref("choose", BOOL),
then: read(zero),
else_: read(ref("i", F64)),
type: maybe,
loc,
},
},
{
kind: "return",
loc,
value: {
kind: "nullish",
left: ref("value", maybe),
right: { kind: "numLit", value: -1, type: F64, loc },
type: F64,
loc,
},
},
],
},
],
};
expect(body(mod, 64)).not.toMatch(/@scr_union_(?:new|retain|release)/);
});
@@ -1,5 +1,7 @@
import { undefinedArmTag } from "../../ir/analysis.js";
import {
isRefCounted,
isUnitType,
type IrExpr,
type IrFunction,
type IrType,
@@ -7,7 +9,10 @@ import {
} from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import type { CallLifetimes } from "./call-lifetimes.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { emitArrIntrinsic } from "./expr-containers.js";
import { emitOptionalChainReceiver } from "./expr-control.js";
import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js";
import { emitLocalArrayRead } from "./local-array-reads.js";
import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js";
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
@@ -31,22 +36,81 @@ function isUnionCopy(expr: IrExpr, type: IrType): boolean {
);
}
function supportedSource(
expr: IrExpr,
type: IrType,
unions: ReadonlyMap<string, IrUnionDef>,
nullableField: CopiedUnionSource,
): boolean {
/** The union sources a binding or projection can snapshot. */
export interface UnionSources {
/** The tagged-box unions a stack box can hold. */
unions: ReadonlyMap<string, IrUnionDef>;
/** Every union, including nullable-pointer receivers of `a?.b`. */
receiverUnions: ReadonlyMap<string, IrUnionDef>;
/** Nullable-pointer field reads (copied with their own owner). */
nullableField: CopiedUnionSource;
/** Optional array element reads (OptionalArrayReads). */
arrayRead: CopiedUnionSource;
/** Direct calls returning a scalar union in registers (Int32Abi.union). */
scalarCall: CopiedUnionSource;
}
/** Every arm is a number, a boolean or a unit value: the box owns nothing,
* so its tag and payload bits are the whole value. */
function isScalarUnion(type: IrType, unions: ReadonlyMap<string, IrUnionDef>): boolean {
if (type.kind !== "union") return false;
const arms = unions.get(type.unionId)?.arms;
return (
arms !== undefined &&
arms.every((arm) => arm.kind === "f64" || arm.kind === "bool" || isUnitType(arm))
);
}
function sameUnion(expr: IrExpr, type: IrType): boolean {
return expr.type.kind === "union" && type.kind === "union" && expr.type.unionId === type.unionId;
}
/** `a?.b`, pass-through `a ?? b`, `c ? a : b` and `pop()`/`shift()` of
* number or boolean elements, as a scalar union built from other sources
* in a private stack box. Ownerless payloads make every snapshot a copy of
* bits, so no heap box is needed while consumers only project. */
export function isScalarOptionalSource(expr: IrExpr, sources: UnionSources): boolean {
if (!isScalarUnion(expr.type, sources.unions)) return false;
if (expr.kind === "arrIntrinsic")
return (
(expr.method === "pop" || expr.method === "shift") &&
expr.receiver.type.kind === "array" &&
(expr.receiver.type.elem.kind === "f64" || expr.receiver.type.elem.kind === "bool")
);
if (expr.kind === "optChain") {
if (expr.receiver.type.kind !== "union") return false;
const arms = sources.receiverUnions.get(expr.receiver.type.unionId)?.arms;
return (
arms !== undefined &&
arms.filter((arm) => !isUnitType(arm)).length === 1 &&
undefinedArmTag(expr.type, sources.unions) >= 0 &&
supportedSource(expr.body, expr.type, sources)
);
}
if (expr.kind === "ternary")
return (
supportedSource(expr.then, type, unions, nullableField) &&
supportedSource(expr.else_, type, unions, nullableField)
supportedSource(expr.then, expr.type, sources) &&
supportedSource(expr.else_, expr.type, sources)
);
return (
canStackUnion(expr, unions) ||
matchMapRead(expr, unions) !== null ||
nullableField(expr) ||
isUnionCopy(expr, type)
expr.kind === "nullish" &&
supportedSource(expr.left, expr.type, sources) &&
supportedSource(expr.right, expr.type, sources)
);
}
function supportedSource(expr: IrExpr, type: IrType, sources: UnionSources): boolean {
if (!sameUnion(expr, type)) return false;
if (sources.arrayRead(expr)) return true;
if (expr.kind === "ternary")
return supportedSource(expr.then, type, sources) && supportedSource(expr.else_, type, sources);
return (
canStackUnion(expr, sources.unions) ||
matchMapRead(expr, sources.unions) !== null ||
sources.nullableField(expr) ||
isUnionCopy(expr, type) ||
isScalarOptionalSource(expr, sources) ||
sources.scalarCall(expr)
);
}
@@ -57,9 +121,9 @@ function supportedSource(
export function findLocalUnionStorage(
fn: IrFunction,
lifetimes: CallLifetimes,
unions: ReadonlyMap<string, IrUnionDef>,
nullableField: CopiedUnionSource = () => false,
sources: UnionSources,
): Map<string, LocalUnionStorageProof> {
const unions = sources.unions;
const types = new Map(fn.locals.map((local) => [local.id, local.type]));
const result = new Map<string, LocalUnionStorageProof>();
const safe = lifetimes.projectedLocals.get(fn.name);
@@ -77,8 +141,7 @@ export function findLocalUnionStorage(
if (stmt.kind === "varDecl" || stmt.kind === "assign") {
const value = stmt.kind === "varDecl" ? stmt.init : stmt.value;
const type = types.get(stmt.localId);
if (!value || !type || !supportedSource(value, type, unions, nullableField))
result.delete(stmt.localId);
if (!value || !type || !supportedSource(value, type, sources)) result.delete(stmt.localId);
}
return true;
},
@@ -122,7 +185,8 @@ export function storeLocalUnion(
expr: IrExpr,
): void {
const B = host.B;
if (expr.kind === "ternary") {
const read = host.optionalArrayReads.get(expr);
if (expr.kind === "ternary" && read === null) {
const cond = host.emitExpr(expr.cond);
const yes = B.newLabel("union.then"),
no = B.newLabel("union.else"),
@@ -147,7 +211,27 @@ export function storeLocalUnion(
// boxed union table), and every source has the binding's own union.
const unionId = expr.type.kind === "union" ? expr.type.unionId : "";
host.requireBoxedUnion(unionId);
if (host.nullableFieldGet(expr) !== null) {
if (read) {
// The snapshot owns its reference element, if any; a missing element
// leaves a NULL owner.
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ptr`);
const owned = emitLocalArrayRead(host, { ...read, borrow: false }, slot);
source = B.tmp();
B.line(`${source} = load ptr, ptr ${slot}`);
if (owned) {
owner = B.tmp();
B.line(`${owner} = load ptr, ptr ${owned.slot}`);
}
} else if (
expr.kind === "optChain" ||
expr.kind === "nullish" ||
expr.kind === "call" ||
expr.kind === "arrIntrinsic"
) {
// Scalar optional sources and register results: a private stack box.
source = host.emitReadReceiver(expr).name;
} else if (host.nullableFieldGet(expr) !== null) {
const copied = host.emitOwnedNullableStack(expr);
source = copied.box;
owner = copied.owner.name;
@@ -207,3 +291,51 @@ export function storeLocalUnion(
host.releaseValue(old, storage.ownerType);
}
}
/** Build a scalar optional source (isScalarOptionalSource) in a fresh
* stack box. Operands evaluate in source order; a binding being assigned
* is only overwritten after the whole value is known. */
export function emitScalarOptional(
host: LlvmEmitterContext,
expr: ExprOf<"optChain" | "nullish" | "ternary" | "arrIntrinsic">,
): LlValue {
const B = host.B;
const storage = allocateLocalUnion(host, { ownerType: null });
if (expr.kind === "arrIntrinsic") return emitArrIntrinsic(host, expr, storage);
if (expr.kind === "ternary") {
storeLocalUnion(host, storage, expr);
return { name: storage.box, type: expr.type };
}
const join = B.newLabel("optional.join");
if (expr.kind === "optChain") {
const { isUnit, bindPresent } = emitOptionalChainReceiver(host, expr);
const missing = B.newLabel("optional.missing"),
present = B.newLabel("optional.present");
B.condBr(isUnit, missing, present);
B.startBlock(missing);
B.line(`store i32 ${undefinedArmTag(expr.type, host.unionsById)}, ptr ${storage.tag}`);
B.line(`store i64 0, ptr ${storage.payload}`);
B.br(join);
B.startBlock(present);
bindPresent();
host.frames.push([]);
storeLocalUnion(host, storage, expr.body);
host.releaseFrame(host.frames.pop()!);
host.chainSlots.delete(expr.id);
B.br(join);
} else {
storeLocalUnion(host, storage, expr.left);
const unionId = expr.type.kind === "union" ? expr.type.unionId : "";
const arms = host.unionsById.get(unionId)!.arms;
const unitTags = arms.flatMap((arm, tag) => (isUnitType(arm) ? [tag] : []));
const fallback = B.newLabel("optional.default");
B.condBr(host.tagInSet(storage.box, unionId, unitTags), fallback, join);
B.startBlock(fallback);
host.frames.push([]);
storeLocalUnion(host, storage, expr.right);
host.releaseFrame(host.frames.pop()!);
B.br(join);
}
B.startBlock(join);
return { name: storage.box, type: expr.type };
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5070-optional-read-bindings.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5071-optional-chain-scalars.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5072-optional-nullish-ternary.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5073-optional-results.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5074-optional-pop-shift.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5075-optional-reads-unchecked/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5076-optional-escapes.ts"
],
"diags": []
}
@@ -0,0 +1,48 @@
// Reassigned bindings of optional element reads keep each snapshot's
// value: holes, present undefined, missing and noncanonical indices read
// as undefined, and later array writes do not change an earlier read.
const numbers = [5, 0, NaN, -1];
for (let i = -1; i <= numbers.length; i++) {
let value = numbers[i];
if (value === undefined) value = 100;
let next = numbers[i + 1];
next = next ?? numbers[i];
console.log(i, value, next, next === undefined);
}
const holey: number[] = [1, , 3] as number[];
let hole = holey[1];
console.log(hole === undefined, hole);
hole = holey[2];
console.log(hole, holey[1.5], holey[-1]);
const words = ["x", "yy"];
let word = words[0];
words[0] = "changed";
console.log(word, word?.length, words[0]);
word = words[5];
console.log(word === undefined, word ?? "fallback");
const flags = [true, false];
let flag = flags[1];
console.log(flag, flag === false, flag === undefined);
flag = flags[9];
console.log(flag, flag ?? "none");
let total = 0;
let cursor = numbers[0];
for (let i = 1; cursor !== undefined; i++) {
total += cursor;
cursor = numbers[i];
}
console.log(total);
function firstAbove(values: number[], limit: number): number {
let found = values[0];
for (let i = 1; i < values.length + 2; i++) {
const candidate = values[i];
if (candidate !== undefined && candidate > limit) found = candidate;
}
return found ?? -1;
}
console.log(firstAbove([1, 9, 4, 12], 5), firstAbove([], 0));
@@ -0,0 +1,67 @@
// Optional chains that end in number or boolean values: missing elements,
// null receivers and nested reads give undefined, the chain body runs only
// for a present receiver, and `??` evaluates its default lazily.
interface Rec {
id: number;
score: number;
flag: boolean;
}
const recs: Rec[] = [
{ id: 1, score: 3, flag: true },
{ id: 2, score: 0, flag: false },
{ id: 3, score: NaN, flag: true },
];
let lo = Infinity;
let hi = -Infinity;
let count = 0;
for (let i = 0; i <= recs.length; i++) {
const score = recs[i]?.score;
if (score === undefined) {
console.log("missing", i);
continue;
}
lo = Math.min(lo, score);
hi = Math.max(hi, score);
count++;
}
console.log(lo, hi, count);
for (let i = 0; i <= recs.length; i++) {
console.log(
recs[i]?.score ?? -1,
recs[i]?.flag ?? "none",
recs[i]?.score || "falsy",
recs[i]?.flag === true,
recs[i]?.score === 3,
!recs[i]?.flag,
);
let flag = recs[i]?.flag;
if (flag === undefined) flag = false;
console.log(flag);
}
const grid: number[][] = [[1, 2], [3]];
for (let r = 0; r < 3; r++)
for (let c = 0; c < 3; c++) {
const cell = grid[r]?.[c];
console.log(r, c, cell, grid[r]?.[c] ?? 9, cell === undefined ? "u" : cell > 1);
}
const nothing: Rec | null | undefined = recs.length > 5 ? recs[0] : null;
console.log(nothing?.score, nothing?.score ?? 5, nothing?.flag === undefined);
const log: string[] = [];
function fallback(label: string): number {
log.push(label);
return 7;
}
let k = 0;
const first = recs[k++]?.score ?? fallback("first");
const second = recs[k + 10]?.score ?? fallback("second");
console.log(first, second, k, log.join(","));
function describe(value: number | undefined): string {
return value === undefined ? "none" : `value ${value * 2}`;
}
for (let i = 0; i < 5; i++) console.log(describe(recs[i]?.score));
@@ -0,0 +1,29 @@
// `??`, `||` and conditional chains over optional element reads choose
// the first present (or truthy) value without changing evaluation order.
const numbers = [5, 0, NaN, -1];
for (let i = 0; i < 6; i++) {
const a = numbers[i] ?? numbers[i - 2] ?? 0;
let b = numbers[i + 3] ?? numbers[i];
b = numbers[i + 7] ?? b;
console.log(a, b, numbers[i] || 11, numbers[i + 1] || numbers[i] || 12);
}
let calls = 0;
function index(i: number): number {
calls++;
return i;
}
console.log(numbers[index(0)] ?? numbers[index(1)], calls);
console.log(numbers[index(9)] ?? numbers[index(1)], calls);
function pick(values: number[], i: number): number {
return values[i] ?? values[values.length - 1] ?? NaN;
}
console.log(pick([], 0), pick([4], 3), pick([4, 8], 0));
let best: number | undefined;
for (const i of [3, 0, 2, 9, 1]) {
const candidate = numbers[i];
best = candidate !== undefined && (best === undefined || candidate > best) ? candidate : best;
}
console.log(best);
+99
View File
@@ -0,0 +1,99 @@
// Functions whose results are `number | undefined` and similar unions:
// direct calls, function values, overridden methods, exceptions and
// finally blocks all observe the same values.
function at(values: number[], i: number): number | undefined {
return values[i];
}
function flagAt(values: boolean[], i: number): boolean | undefined {
return values[i];
}
function positive(n: number): number | null {
return n > 0 ? n : null;
}
function three(n: number): number | null | undefined {
return n > 1 ? n : n === 1 ? null : undefined;
}
function same(value: number | undefined): number | undefined {
return value;
}
function thrower(n: number): number | undefined {
if (n === 3) throw new Error(`boom ${n}`);
return n > 4 ? undefined : n * 2;
}
function guarded(n: number): number | undefined {
try {
if (n > 2) return n;
return undefined;
} finally {
console.log("finally", n);
}
}
function find(values: number[], limit: number): number | undefined {
for (let i = 0; i < values.length; i++) {
const value = values[i];
if (value !== undefined && value > limit) return value;
}
return undefined;
}
function last(values: number[], i: number): number | undefined {
return i < 0 ? undefined : (at(values, i) ?? last(values, i - 1));
}
class Store {
data: number[] = [4, 5];
get(i: number): number | undefined {
return this.data[i];
}
}
class Override extends Store {
get(i: number): number | undefined {
return i === 0 ? 99 : super.get(i);
}
}
const values = [1, 2, 3];
for (let i = -1; i < 6; i++) {
console.log(i, at(values, i), at(values, i) ?? "none", at(values, i) === undefined);
console.log(flagAt([true, false], i), positive(i), three(i), same(at(values, i)));
try {
console.log(thrower(i));
} catch (error) {
console.log("caught", (error as Error).message);
}
console.log(guarded(i), find(values, i), last(values, i + 3));
}
const value: (values: number[], i: number) => number | undefined = at;
console.log(value(values, 1), value(values, 7), [0, 1, 5].map((i) => at(values, i)));
const stores: Store[] = [new Store(), new Override()];
for (const store of stores) console.log(store.get(0), store.get(1), store.get(2));
const kept: (number | undefined)[] = [];
for (let i = 0; i < 4; i++) kept.push(at(values, i));
console.log(kept);
let carried: number | undefined = at(values, 0);
for (let i = 0; i < 5; i++) carried = at(values, i) ?? carried;
console.log(carried);
const record = { missing: at(values, 9), present: at(values, 0) };
console.log(record, JSON.stringify(record));
function sum(a: number[], lo: number, hi: number): number | undefined {
if (hi - lo <= 1) return a[lo];
const mid = (lo + hi) >> 1;
const left = sum(a, lo, mid);
const right = sum(a, mid, hi);
if (left === undefined) return right;
if (right === undefined) return left;
return left + right;
}
console.log(sum(values, 0, 10), sum([], 0, 4));
function base(a: number[], lo: number, hi: number): number {
if (hi - lo <= 1) {
let v = a[lo];
if (v === undefined) v = 0;
return v;
}
const mid = (lo + hi) >> 1;
return base(a, lo, mid) + base(a, mid, hi);
}
console.log(base(values, 0, 8));
+36
View File
@@ -0,0 +1,36 @@
// pop() and shift() of number and boolean arrays answer undefined once
// the array is empty; consumers that test or default the result see the
// removed element otherwise.
const stack = [1, 0, NaN, 4];
let total = 0;
for (;;) {
const top = stack.pop();
if (top === undefined) break;
total += top;
console.log(top, stack.length);
}
console.log(total, stack.pop(), stack.pop() ?? "empty");
const queue = [true, false, true];
while (queue.length > 0) console.log(queue.shift() ?? "none", queue.length);
console.log(queue.shift(), queue.shift() === undefined);
const work = [3, 1, 2];
let visited = 0;
while (work.length) {
const next = work.shift()!;
visited += next;
if (next > 2) work.push(next - 2);
}
console.log(visited);
function drain(values: number[]): number {
let last = values.pop() ?? -1;
let count = 0;
for (let item = values.pop(); item !== undefined; item = values.pop()) {
last = item;
count++;
}
return last * 10 + count;
}
console.log(drain([7, 8, 9]), drain([]));
@@ -0,0 +1,72 @@
// Kernels over maybe-missing elements with noUncheckedIndexedAccess: game
// tree minimax over ragged rows, divide-and-conquer sums that read past
// the end, and min/max of record fields.
interface Rec {
id: number;
score: number;
}
function leaf(grid: number[][], node: number): number {
return grid[node >> 3]?.[node & 7] ?? 0;
}
function minimax(grid: number[][], node: number, depth: number, maxing: boolean): number {
if (depth === 0) return leaf(grid, node % 90);
let best = maxing ? -Infinity : Infinity;
for (let c = 0; c < 3; c++) {
const score = minimax(grid, node * 3 + c, depth - 1, !maxing);
best = maxing ? Math.max(best, score) : Math.min(best, score);
}
return best;
}
const grid: number[][] = [];
for (let r = 0; r < 10; r++) {
const row: number[] = [];
for (let c = 0; c < (r % 3 === 0 ? 5 : 8); c++) row.push((r * 37 + c * 11) % 23);
grid.push(row);
}
console.log(minimax(grid, 0, 5, true), minimax(grid, 1, 4, false));
function sum(a: number[], lo: number, hi: number): number {
if (hi - lo <= 1) {
let v = a[lo];
if (v === undefined) v = 0;
return v;
}
const mid = (lo + hi) >> 1;
return sum(a, lo, mid) + sum(a, mid, hi);
}
function maybeSum(a: number[], lo: number, hi: number): number | undefined {
if (hi - lo <= 1) return a[lo];
const mid = (lo + hi) >> 1;
const left = maybeSum(a, lo, mid);
const right = maybeSum(a, mid, hi);
return left === undefined ? right : right === undefined ? left : left + right;
}
const values: number[] = [];
for (let i = 0; i < 100; i++) values.push((i * 7) % 13);
console.log(sum(values, 0, values.length + 9), maybeSum(values, 0, 140), maybeSum(values, 100, 120));
const recs: Rec[] = [];
for (let i = 0; i < 50; i++) recs.push({ id: i, score: (i * 7919) % 101 });
let lo = Infinity;
let hi = -Infinity;
for (let i = 0; i <= recs.length + 1; i++) {
const score = recs[i]?.score;
if (score === undefined) continue;
lo = Math.min(lo, score);
hi = Math.max(hi, score);
}
let best = -Infinity;
for (let i = 0; i < recs.length; i++) best = Math.max(best, recs[i]!.score);
function choose(values: number[], useFirst: boolean): number | string {
const value = useFirst ? values[0] : values[1];
if (value === undefined) return "missing";
return value;
}
for (let i = 0; i < 4; i++) {
const c = i % 2 === 0 ? values[i * 40] : values[i * 40 + 1];
console.log(c, c ?? "none", choose(values.slice(i * 50), i > 1));
}
const first = values[0];
const missing = values[500];
console.log(lo, hi, best, first ?? -1, missing ?? -1, missing === undefined);
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
+30
View File
@@ -0,0 +1,30 @@
// Scalar optional values that escape into arrays, fields, maps, closures
// and stored parameters keep their own heap values.
const kept: (number | undefined)[] = [];
function keep(x: number | undefined): void { kept.push(x); }
function keepTwice(x: number | undefined): number | undefined { kept.push(x); return x; }
class Holder { v: number | undefined = undefined; set(x: number | undefined) { this.v = x; } }
const h = new Holder();
const m = new Map<string, number | undefined>();
const fns: (() => number | undefined)[] = [];
function at(xs: number[], i: number): number | undefined { return xs[i]; }
const xs = [10, 20];
for (let i = 0; i < 4; i++) {
let a: number | undefined = xs[i];
let b: number | undefined = xs[i + 1];
if (i === 2) { a = 5; b = undefined; }
const pick = i % 2 === 0 ? a : b;
keep(i % 2 === 0 ? a : b);
keep(xs[i] ?? xs[0]);
keep(at(xs, i));
keepTwice(pick);
h.set(i > 1 ? at(xs, i) : xs[i]);
m.set("k" + i, at(xs, i) ?? at(xs, 0));
const captured = at(xs, i);
fns.push(() => captured);
console.log(pick, h.v, m.get("k" + i));
}
console.log(kept, fns.map((f) => f()));
function deep(n: number): number | undefined { return n === 0 ? undefined : (deep(n - 1) ?? n); }
try { console.log(deep(1e7)); } catch (e) { console.log("caught", (e as Error).name); }
console.log(deep(100));