Inline interface dispatch and lend array elements to it (#997)

This commit is contained in:
Chris Tate
2026-10-14 22:37:53 -05:00
committed by GitHub
19 changed files with 586 additions and 21 deletions
@@ -371,6 +371,7 @@ export function analyzeCallLifetimes(
const local = locals.get(param.localId);
if (
local &&
!param.owned &&
isRefCounted(param.type) &&
(stable(local) || unchangedCaptures.has(local.id)) &&
!uses.declarations.has(local.id)
+43 -1
View File
@@ -178,7 +178,7 @@ import { ReferenceEffects } from "./reference-effects.js";
import { ReferenceWrites, type Writes } from "./reference-writes.js";
import { LlvmDebugInfo } from "./debug-info.js";
import { StackCallbacks } from "./stack-callbacks.js";
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
import { dynHoleRead, findLoopArrayBorrows } from "./loop-array-borrows.js";
import { findAppendedArrayLiterals } from "./appended-array-literals.js";
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
import {
@@ -5662,6 +5662,7 @@ export class LlEmitter {
this.referenceEffects,
this.optionalArrayReads,
this.referenceWrites,
this.fnByName,
)
: new Set();
this.localArrayReads = findLocalArrayReads(
@@ -6000,6 +6001,11 @@ export class LlEmitter {
// hot instances out of line once they have many callers: every read pays
// a call, and an integer index converts to a double and back across it.
if (fn.name.startsWith("%arr.idxOr.")) attrs = `inlinehint ${attrs}`;
// Interface dispatch is a few key tests and direct calls ahead of the
// checked dynamic call. Keeping that fallback out of line leaves the
// tests small enough to inline at each call site.
if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`;
else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`;
const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
@@ -6223,6 +6229,9 @@ export class LlEmitter {
* Other writable bindings need an owned snapshot. */
canBorrowCallArgument(value: IrExpr): boolean {
if (value.kind === "strLit") return true;
// An exact capsule owns its instance while the capsule's binding holds it.
if (value.kind === "dynCheck" && value.exact)
return value.value.kind === "varRef" && this.canBorrowCallArgument(value.value);
// An inline union field has no box to lend.
if (this.inlineFieldGet(value)) return false;
// Class casts reinterpret the same pointer (prefix layouts).
@@ -6741,6 +6750,26 @@ export class LlEmitter {
emitBorrowedArrayRead(this, s.init, b.slot);
break;
}
const holeRead = this.loopArrayBorrows.has(s) ? dynHoleRead(s.init) : null;
if (holeRead && s.init.kind === "ternary") {
// A hole reads as the immortal undefined value, which needs no owner.
const present = this.emitExpr(s.init.cond);
const filled = B.newLabel("borrow.hole.filled"),
missing = B.newLabel("borrow.hole.missing"),
done = B.newLabel("borrow.hole.done");
B.condBr(present.name, filled, missing);
B.startBlock(filled);
emitBorrowedArrayRead(this, holeRead, b.slot);
B.br(done);
B.startBlock(missing);
this.declare("declare ptr @scr_dyn_undefined()");
const undef = B.tmp();
B.line(`${undef} = call ptr @scr_dyn_undefined()`);
B.line(`store ptr ${undef}, ptr ${b.slot}`);
B.br(done);
B.startBlock(done);
break;
}
// A stable lexical alias cannot outlive its unchanged local owner.
// Whole-value uses still retain their own copies, so returning or
// storing the alias preserves the ordinary heap representation.
@@ -8686,6 +8715,7 @@ export class LlEmitter {
this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
return { name: value, type: e.type };
}
if (e.kind === "dynCheck" && e.exact) return this.emitExactCapsuleObject(e.value, e.type);
if (e.kind === "chainRecv") {
// The chain's bind slot holds its receiver unchanged until the body
// finishes, owned by the slot itself or by the chain's receiver.
@@ -8983,6 +9013,18 @@ export class LlEmitter {
);
}
/** The instance of a capsule known to hold exactly this class (borrowed). */
emitExactCapsuleObject(value: IrExpr, type: IrType): LlValue {
const capsule = this.emitReadReceiver(value);
const slot = this.B.tmp();
const object = this.B.tmp();
this.B.line(
`${slot} = getelementptr inbounds i8, ptr ${capsule.name}, i64 16 ; ->v.typed_ref.ptr`,
);
this.B.line(`${object} = load ptr, ptr ${slot}`);
return { name: object, type };
}
canBorrowReceiver(e: IrExpr): boolean {
switch (e.kind) {
case "chainRecv":
@@ -373,6 +373,14 @@ export function emitSerializationExpr(
return fusedOut;
}
}
if (e.exact && isDynTypedRefType(e.type)) {
const object = host.emitExactCapsuleObject(e.value, e.type);
const retained = B.tmp();
B.line(
`${retained} = call ptr ${vAdapters(host.shapeHost, e.type).retain}(ptr ${object.name})`,
);
return host.own({ name: retained, type: e.type });
}
const dynV = emitBorrowedInput(host, e.value);
const helper = host.dyn.dynCheckHelper(e.type, e.preserveRefs);
if (isDynTypedRefType(e.type)) return emitClassCheck(host, e.type, dynV.name, helper);
@@ -1,10 +1,62 @@
import { isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import { F64, isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import type { CallLifetimes } from "./call-lifetimes.js";
import type { ReferenceEffects } from "./reference-effects.js";
import type { OptionalArrayReads } from "./local-array-reads.js";
import type { ReferenceWrites } from "./reference-writes.js";
/** Typed interface dispatch helpers (class-protocol-dispatch.ts), not
* their fallbacks or early method reads. */
function isProtocolDispatch(name: string): boolean {
return /^%dyn\.class\.protocol\.\d+$/.test(name);
}
/** A dispatch helper keeps a lent receiver alive: every statement ahead of
* its final fallback call preserves heap edges (key tests and calls to
* implementing methods that cannot release the receiver), and the fallback,
* which can run any code, takes its own reference to the receiver. */
function dispatchLends(
helper: IrFunction,
functions: ReadonlyMap<string, IrFunction>,
effects: ReferenceEffects,
): boolean {
const last = helper.body[helper.body.length - 1];
if (
last?.kind !== "return" ||
last.value?.kind !== "call" ||
!/^%dyn\.class\.protocol\.fallback\.\d+$/.test(last.value.callee) ||
last.value.args[0]?.kind !== "varRef" ||
last.value.args[0].localId !== helper.params[0]?.localId
)
return false;
if (functions.get(last.value.callee)?.params[0]?.owned !== true) return false;
return effects.preservesScope(helper.body.slice(0, -1));
}
/** A checked-dynamic element read that yields undefined for a hole:
* `arrayState(a, i) === 1 ? a[i] : undefined` (arrayValueRead's form for
* dyn elements). Returns the element read. */
export function dynHoleRead(value: IrExpr): (IrExpr & { kind: "arrayGet" }) | null {
if (value.kind !== "ternary" || value.type.kind !== "dyn") return null;
const { cond, then, else_ } = value;
if (
cond.kind !== "bin" ||
cond.op !== "===" ||
cond.left.kind !== "arrayState" ||
cond.right.kind !== "numLit" ||
cond.right.value !== 1 ||
then.kind !== "arrayGet" ||
then.type.kind !== "dyn" ||
else_.kind !== "dynFrom" ||
else_.value.kind !== "unitLit" ||
else_.value.unit !== "undefined"
)
return null;
const same = (left: IrExpr, right: IrExpr): boolean =>
left.kind === "varRef" && right.kind === "varRef" && left.localId === right.localId;
return same(cond.left.arr, then.arr) && same(cond.left.index, then.index) ? then : null;
}
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
* Initialization and unrelated work elsewhere in the function may mutate.
* The array remains owned by its unchanged local or global binding; the
@@ -19,11 +71,11 @@ export function findLoopArrayBorrows(
effects: ReferenceEffects,
optionalReads?: OptionalArrayReads,
writes?: ReferenceWrites | null,
functions?: ReadonlyMap<string, IrFunction>,
): ReadonlySet<IrStmt> {
const result = new Set<IrStmt>();
if (fn.async || fn.generator) return result;
const projected = lifetimes.locals.get(fn.name);
if (!projected?.size) return result;
const bindings = lifetimes.bindings.get(fn.name);
const parameters = new Set(fn.params.map((param) => param.localId));
const locals = new Map(fn.locals.map((local) => [local.id, local]));
@@ -58,6 +110,42 @@ export function findLoopArrayBorrows(
return true;
}
if (loop.kind !== "for" && loop.kind !== "while" && loop.kind !== "doWhile") return true;
// A binding whose one use is the receiver of an interface dispatch
// can hold a lent element when nothing else in the iteration can
// release it and the dispatch keeps it alive itself (dispatchLends).
const lentToDispatch = (id: string): boolean => {
let uses = 0;
let dispatch: (IrExpr & { kind: "call" }) | null = null;
everyStmtList(loop.body, {
stmt: () => true,
expr: (node) => {
if (node.kind === "varRef" && node.localId === id) uses++;
if (
node.kind === "call" &&
node.args[0]?.kind === "varRef" &&
node.args[0].localId === id &&
isProtocolDispatch(node.callee)
)
dispatch = node;
return true;
},
});
const call = dispatch as (IrExpr & { kind: "call" }) | null;
if (uses !== 1 || call === null || !functions) return false;
const helper = functions.get(call.callee);
if (!helper || !dispatchLends(helper, functions, effects)) return false;
if (!call.args.slice(1).every((arg) => effects.preserves(arg))) return false;
const placeholder: IrExpr = { kind: "numLit", value: 0, type: F64, loc: call.loc };
const rest = transformStmtList(loop.body, {
stmt: (node) => node,
expr: (node) => (node === call ? placeholder : node),
});
return (
effects.preservesScope(rest) &&
(!loop.cond || effects.preserves(loop.cond)) &&
(loop.kind !== "for" || !loop.update || effects.preservesScope([loop.update]))
);
};
const preserving =
effects.preservesScope(loop.body) &&
(!loop.cond || effects.preserves(loop.cond)) &&
@@ -70,22 +158,27 @@ export function findLoopArrayBorrows(
loop.cond ? writes.expr(loop.cond) : null,
loop.kind === "for" && loop.update ? writes.stmt(loop.update) : null,
]);
if (!preserving && !writes) return true;
if (!preserving && !writes && !functions) return true;
everyStmtList(loop.body, {
expr: () => true,
stmt: (stmt) => {
if (stmt.kind !== "varDecl" || !stmt.init || !bindings?.has(stmt.localId)) return true;
const hole = dynHoleRead(stmt.init);
const local = locals.get(stmt.localId);
if (
stmt.kind !== "varDecl" ||
!stmt.init ||
!isRefCounted(stmt.init.type) ||
!projected?.has(stmt.localId) ||
!bindings?.has(stmt.localId)
!(
projected?.has(stmt.localId) ||
(hole && local && !local.boxed && !local.tdz && lentToDispatch(stmt.localId))
)
)
return true;
const source =
stmt.init.kind === "arrayGet" ? stmt.init.arr : optionalReads?.get(stmt.init)?.array;
stmt.init.kind === "arrayGet"
? stmt.init.arr
: (hole?.arr ?? optionalReads?.get(stmt.init)?.array);
if (!source || !stableSource(source)) return true;
if (preserving) result.add(stmt);
if (preserving || (hole && !projected?.has(stmt.localId))) result.add(stmt);
else {
const keys = writes!.readKeys(source);
if (keys && !writes!.conflicts(["a", ...keys], loopWrites)) result.add(stmt);
@@ -144,8 +144,23 @@ function expressionPreservesEdges(
// Atomic read-modify-write on an integer typed array (a shared id
// counter): numbers only, no callback.
e.fn === "atomics.op" ||
// Class brand tests compare a capsule's key; they run no user code.
e.fn === "dyn.typedRefKeyIs" ||
e.fn === "dyn.typedRefIs" ||
isStableReceiverOperand(e, "")
);
// An exact capsule's instance is one load (and a retain when owned).
case "dynCheck":
return e.exact === true;
// Boxing a scalar or the undefined/null value allocates (or returns an
// immortal value) and runs no user code.
case "dynFrom":
return (
e.value.kind === "unitLit" ||
e.value.type.kind === "f64" ||
e.value.type.kind === "bool" ||
e.value.type.kind === "string"
);
case "call":
return call(e);
case "arrIntrinsic":
@@ -16,6 +16,7 @@ import {
typeKey,
type IrExpr,
type IrFunction,
type IrParam,
type IrStmt,
type IrType,
type SrcLoc,
@@ -1909,7 +1910,7 @@ export class ClassDynamicDispatch {
]);
let dispatch = this.dispatches.get(key);
if (!dispatch) {
const params = [invoke.recv, ...invoke.args].map((_, i) => ({
const params: IrParam[] = [invoke.recv, ...invoke.args].map((_, i) => ({
localId: `p.${i}`,
name: `p${i}`,
type: DYN,
@@ -2266,7 +2267,13 @@ export class ClassDynamicDispatch {
dispatch.classes.add(plan.key);
const loc = dispatch.fn.loc;
const type: IrType = { kind: "object", className: plan.info.def.name };
const receiver: IrExpr = { kind: "dynCheck", value: varRef("p.0", DYN, loc), type, loc };
const receiver: IrExpr = {
kind: "dynCheck",
value: varRef("p.0", DYN, loc),
exact: true,
type,
loc,
};
const arm = dispatch.field
? protocolFieldArm(lowerer, dispatch, plan.info, receiver)
: protocolArm(lowerer, dispatch, plan.info, receiver, this.asyncFree);
@@ -10,6 +10,7 @@ import {
typeKey,
type IrExpr,
type IrFunction,
type IrParam,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
@@ -46,6 +47,13 @@ export interface ProtocolDispatch {
classes: Set<string>;
}
/** The fallback takes its own reference to the receiver. A caller may lend
* the receiver from an array element, and the checked call can run code
* that removes that element. */
function ownedReceiver(params: IrParam[]): IrParam[] {
return params.map((param, index) => (index === 0 ? { ...param, owned: true } : param));
}
/** Lowered arguments whose evaluation can neither throw nor change any
* value. */
function effectFree(expr: IrExpr): boolean {
@@ -248,9 +256,16 @@ export function lowerProtocolMethodCall(
const { params, result, returnsVoid } = signature;
const loc = locOf(call);
const method = access.name.text;
const receiverLocal = lowerer.declareHiddenLocal("%protocolReceiver", DYN);
const receiver = varRef(receiverLocal.id, DYN, loc);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }];
// A const binding already holds the receiver for the whole call; other
// receivers are evaluated once into a hidden local.
const constant =
recv.kind === "varRef" &&
lowerer.ctx.locals.some((local) => local.id === recv.localId && !local.mutable);
const receiverLocal = constant ? null : lowerer.declareHiddenLocal("%protocolReceiver", DYN);
const receiver = receiverLocal ? varRef(receiverLocal.id, DYN, loc) : recv;
const stmts: IrStmt[] = receiverLocal
? [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }]
: [];
const args = call.arguments.map((arg, i) =>
lowerer.coerceToExpected(lowerer.lowerExprExpecting(arg, params[i]!), params[i]!),
);
@@ -260,7 +275,7 @@ export function lowerProtocolMethodCall(
let dispatch = lowerer.protocolDispatches.get(key);
if (!dispatch) {
const index = lowerer.protocolDispatches.size;
const signatureParams = [
const signatureParams: IrParam[] = [
{ localId: "p.0", name: "receiver", type: DYN },
...(probed ? [{ localId: "p.callee", name: "callee", type: DYN }] : []),
...params.map((type, i) => ({ localId: `p.${i + 1}`, name: `p${i + 1}`, type })),
@@ -292,7 +307,7 @@ export function lowerProtocolMethodCall(
};
const fallback: IrFunction = {
name: `%dyn.class.protocol.fallback.${index}`,
params: signatureParams,
params: ownedReceiver(signatureParams),
returnType: result,
locals: locals(),
body: [{ kind: "return", value: lowerer.coerceToExpected(dynamicCall, result), loc }],
@@ -495,7 +510,7 @@ export function lowerProtocolPropertyRead(
let dispatch = lowerer.protocolDispatches.get(key);
if (!dispatch) {
const index = lowerer.protocolDispatches.size;
const params = [{ localId: "p.0", name: "receiver", type: DYN }];
const params: IrParam[] = [{ localId: "p.0", name: "receiver", type: DYN }];
const locals = () => [{ id: "p.0", name: "receiver", type: DYN, mutable: false }];
const read: IrExpr = {
kind: "dynKeyGet",
@@ -506,7 +521,7 @@ export function lowerProtocolPropertyRead(
};
const fallback: IrFunction = {
name: `%dyn.class.protocol.fallback.${index}`,
params,
params: ownedReceiver(params),
returnType: result,
locals: locals(),
body: [{ kind: "return", value: lowerer.coerceToExpected(read, result), loc }],
+14 -1
View File
@@ -1577,6 +1577,9 @@ export interface IrParam {
localId: string;
name: string;
type: IrType;
/** Callers pass an owned reference, which the body holds for its whole
* run, even when the parameter is never rebound. */
owned?: true;
}
export interface IrGlobal {
@@ -3495,7 +3498,17 @@ export type IrExpr =
* corrupting memory). `preserveRefs` refuses structural copies of native
* references, including references nested in newly materialized arrays or
* tuples; collection seeds use it to preserve key identity. */
| { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; type: IrType; loc: SrcLoc };
/** `exact`: the value is known to be a capsule this program boxed from
* exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the
* expression), so the check is one load of the capsule's instance. */
| {
kind: "dynCheck";
value: IrExpr;
preserveRefs?: true;
exact?: true;
type: IrType;
loc: SrcLoc;
};
/** True when a type is a union with an undefined arm — the optional-flavored
* slot marker shared by the frontend (record literals may omit such fields)
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5390-interface-loop-element-lifetimes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5391-interface-dispatch-hierarchies.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5392-interface-receiver-bindings.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5393-interface-array-holes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5394-interface-implementers.ts"
],
"diags": []
}
@@ -0,0 +1,71 @@
// Loops over arrays of interface values whose method calls change the
// array: an implementation clears, splices, refills or replaces the array
// being iterated, or the variable that holds it. Each call still runs on
// the element it was made on, and the loop sees the array's new contents.
interface Shape {
area(): number;
}
class Square implements Shape {
side: number;
constructor(side: number) {
this.side = side;
}
area(): number {
return this.side * this.side;
}
}
let shapes: Shape[] = [];
const clearing: Shape = {
area(): number {
shapes.length = 0;
shapes = [];
return 1000;
},
};
const splicing: Shape = {
area(): number {
shapes.splice(0, shapes.length, new Square(9));
return 100;
},
};
const growing: Shape = {
area(): number {
if (shapes.length < 6) shapes.push(new Square(shapes.length));
return 10;
},
};
function total(list: Shape[]): number {
let sum = 0;
for (const shape of list) sum += shape.area();
return sum;
}
shapes = [new Square(1), clearing, new Square(2)];
console.log(total(shapes), shapes.length);
shapes = [new Square(1), splicing, new Square(2), new Square(3)];
const spliced = shapes;
console.log(total(shapes), spliced.length, spliced[0] === shapes[0]);
shapes = [growing, new Square(2)];
console.log(total(shapes), shapes.length);
// A getter-backed method that removes the element it belongs to.
class Removing implements Shape {
get area(): () => number {
shapes.splice(shapes.indexOf(this), 1);
return () => 5;
}
}
shapes = [new Square(1), new Removing(), new Square(2), new Square(3)];
console.log(total(shapes), shapes.length);
// Many rounds, so released elements would be reused if a call kept a
// stale element.
let checksum = 0;
for (let round = 0; round < 200; round++) {
shapes = [new Square(round % 7), clearing, new Square(3)];
checksum += total(shapes);
}
console.log(checksum);
@@ -0,0 +1,61 @@
// Interface calls on class hierarchies after dispatch is inlined at the
// call site: base and derived capsules, overrides several levels deep,
// abstract bases, super calls, and the same instance seen through a base
// class value and an interface value.
interface Meter {
read(scale: number): number;
label(): string;
}
abstract class Base implements Meter {
value: number;
constructor(value: number) {
this.value = value;
}
abstract read(scale: number): number;
label(): string {
return `base ${this.value}`;
}
}
class Linear extends Base {
read(scale: number): number {
return this.value * scale;
}
}
class Offset extends Linear {
offset: number;
constructor(value: number, offset: number) {
super(value);
this.offset = offset;
}
override read(scale: number): number {
return super.read(scale) + this.offset;
}
override label(): string {
return `offset ${super.label()}`;
}
}
class Clamped extends Offset {
override read(scale: number): number {
return Math.min(super.read(scale), 50);
}
}
const meters: Meter[] = [new Linear(2), new Offset(3, 1), new Clamped(10, 5)];
for (const meter of meters) console.log(meter.label(), meter.read(2), meter.read(10));
const asBase: Base = new Clamped(1, 1);
const asMeter: Meter = asBase;
console.log(asMeter.read(3), asBase.read(3), asMeter.label());
let sum = 0;
for (let i = 0; i < 300; i++) sum += meters[i % 3]!.read(i & 7);
console.log(sum);
const single: Meter[] = [new Offset(1, 1)];
let total = 0;
for (const meter of single) total += meter.read(4);
console.log(total);
@@ -0,0 +1,50 @@
// Interface calls whose receiver is a const, a let reassigned by an
// argument, a parameter, a property read, or an element read: the receiver
// is the value the expression had before the arguments ran.
interface Greeter {
greet(name: string): string;
}
class Polite implements Greeter {
title: string;
constructor(title: string) {
this.title = title;
}
greet(name: string): string {
return `${this.title} ${name}`;
}
}
const first: Greeter = new Polite("dear");
let current: Greeter = new Polite("hello");
function swap(next: Greeter): string {
current = next;
return "swapped";
}
console.log(first.greet("ann"));
console.log(current.greet(swap(first)), current.greet("bob"));
function viaParameter(greeter: Greeter, other: Greeter): string {
const a = greeter.greet("x");
greeter = other;
return `${a} ${greeter.greet("y")}`;
}
console.log(viaParameter(new Polite("one"), new Polite("two")));
class Holder {
greeter: Greeter = new Polite("held");
}
const holder = new Holder();
function replaceHeld(): string {
holder.greeter = new Polite("replaced");
return "z";
}
console.log(holder.greeter.greet(replaceHeld()), holder.greeter.greet("w"));
const list: Greeter[] = [new Polite("a"), new Polite("b")];
function shiftList(): string {
list.shift();
return "q";
}
console.log(list[0]!.greet(shiftList()), list.length, list[0]!.greet("r"));
@@ -0,0 +1,45 @@
// Arrays of interface values with holes and undefined entries written by
// untyped code, read by loops, index reads and array methods.
interface Shape {
area(): number;
}
class Square implements Shape {
side: number;
constructor(side: number) {
this.side = side;
}
area(): number {
return this.side * this.side;
}
}
const shapes: Shape[] = [new Square(1), new Square(2)];
function extend(view: unknown): void {
const array = view as unknown[];
array.length = 5;
array[3] = undefined;
array[4] = new Square(4);
}
extend(shapes);
let present = 0;
let missing = 0;
for (const shape of shapes) {
if (shape === undefined) missing++;
else present += shape.area();
}
console.log(shapes.length, present, missing);
console.log(2 in shapes, 3 in shapes, shapes[2] === undefined, shapes[4]!.area());
console.log(shapes.filter((s) => s !== undefined).length);
const areas: number[] = [];
shapes.forEach((s) => areas.push(s === undefined ? -1 : s.area()));
console.log(areas.join(","));
console.log(JSON.stringify(shapes));
function areaSum(list: Shape[]): number {
let sum = 0;
for (const shape of list) sum += shape === undefined ? -1 : shape.area();
return sum;
}
console.log(areaSum(shapes));
@@ -0,0 +1,70 @@
// Interfaces with one implementing class, with many, and with classes and
// object literals mixed; loops and calls through each, including methods
// with string, record and class results.
interface Single {
next(n: number): number;
}
class Only implements Single {
step: number;
constructor(step: number) {
this.step = step;
}
next(n: number): number {
return n + this.step;
}
}
const singles: Single[] = [new Only(1), new Only(2), new Only(3)];
let n = 0;
for (let round = 0; round < 100; round++) for (const s of singles) n = s.next(n) % 1000;
console.log(n);
type Point = { x: number; y: number };
interface Many {
name(): string;
where(): Point;
}
class A implements Many {
name(): string {
return "a";
}
where(): Point {
return { x: 1, y: 1 };
}
}
class B implements Many {
name(): string {
return "b";
}
where(): Point {
return { x: 2, y: 0 };
}
}
class C implements Many {
name(): string {
return "c";
}
where(): Point {
return { x: 0, y: 3 };
}
}
class D extends C {
override name(): string {
return "d";
}
}
const literal: Many = {
name: () => "literal",
where: () => ({ x: -1, y: -1 }),
};
const many: Many[] = [new A(), new B(), new C(), new D(), literal];
let names = "";
let x = 0;
for (let round = 0; round < 20; round++)
for (const m of many) {
names += m.name();
x += m.where().x;
}
console.log(names.length, names.slice(0, 12), x);
console.log(many.map((m) => `${m.name()}@${m.where().y}`).join(" "));
+44
View File
@@ -117,6 +117,50 @@ console.log(size(new Box()));
);
});
test("matched receivers unbox without reference counting, ahead of an out-of-line fallback", async () => {
const llvm = await emittedLlvm("inline", shapes(""));
const helper = llvm.indexOf("; %dyn.class.protocol.0");
const dispatch = llvm.slice(llvm.lastIndexOf("define", helper), llvm.indexOf("\n}\n", helper));
expect(dispatch).toContain("; ->v.typed_ref.ptr");
expect(dispatch).not.toMatch(/call ptr @sc_retain_(?:Circle|Rect)\(/);
expect(dispatch).toMatch(/\binlinehint\b/);
expect(llvm).toMatch(
/define internal [^\n]*@sc_bf__x25_dyn_class_protocol_fallback_0[^\n]*\bnoinline\b/,
);
});
test("loops lend interface array elements to dispatch that cannot release them", async () => {
const lent = await emittedLlvm(
"lent",
shapes(`function sum(list: Shape[]): number {
let t = 0;
for (const s of list) t += s.scale(2);
return t;
}
console.log(sum([circle, new Rect(1)]));`),
);
expect(bodies(lent, "sc_bf_sum")).not.toContain("@scr_dyn_retain_v");
// A method that can empty the array keeps the owned element read.
const owned = await emittedLlvm(
"owned",
shapes(`const kept: Shape[] = [];
class Emptying implements Shape {
scale(k: number): number {
kept.length = 0;
return k;
}
}
kept.push(new Emptying(), circle);
function sum(list: Shape[]): number {
let t = 0;
for (const s of list) t += s.scale(2);
return t;
}
console.log(sum(kept));`),
);
expect(bodies(owned, "sc_bf_sum")).toContain("@scr_dyn_retain_v");
});
test("a method an instance can replace keeps the checked dynamic call", async () => {
const llvm = await emittedLlvm(
"replaced",