mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Keep maybe-missing number reads unboxed (#949)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5070-optional-read-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5071-optional-chain-scalars.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5072-optional-nullish-ternary.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5073-optional-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5074-optional-pop-shift.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5075-optional-reads-unchecked/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user