diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.ts b/packages/compiler/src/backend/llvm/call-lifetimes.ts index 4a6b6ca9b..7c5931751 100644 --- a/packages/compiler/src/backend/llvm/call-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/call-lifetimes.ts @@ -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) diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 77ac02413..5198593f0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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": diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index ec2fb6853..6d2e16568 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/loop-array-borrows.ts b/packages/compiler/src/backend/llvm/loop-array-borrows.ts index 07ae3ed9f..9335894d9 100644 --- a/packages/compiler/src/backend/llvm/loop-array-borrows.ts +++ b/packages/compiler/src/backend/llvm/loop-array-borrows.ts @@ -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, + 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, ): ReadonlySet { const result = new Set(); 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); diff --git a/packages/compiler/src/backend/llvm/reference-effects.ts b/packages/compiler/src/backend/llvm/reference-effects.ts index ecee62e8e..88875fce4 100644 --- a/packages/compiler/src/backend/llvm/reference-effects.ts +++ b/packages/compiler/src/backend/llvm/reference-effects.ts @@ -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": diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fd2725868..507c3d243 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 3c067828f..2f66a5203 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -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; } +/** 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 }], diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 179acc019..3f592e9db 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5390-interface-loop-element-lifetimes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5390-interface-loop-element-lifetimes.ts.json new file mode 100644 index 000000000..d85f68b44 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5390-interface-loop-element-lifetimes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5390-interface-loop-element-lifetimes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5391-interface-dispatch-hierarchies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5391-interface-dispatch-hierarchies.ts.json new file mode 100644 index 000000000..ea551aa18 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5391-interface-dispatch-hierarchies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5391-interface-dispatch-hierarchies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5392-interface-receiver-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5392-interface-receiver-bindings.ts.json new file mode 100644 index 000000000..c4356553a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5392-interface-receiver-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5392-interface-receiver-bindings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5393-interface-array-holes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5393-interface-array-holes.ts.json new file mode 100644 index 000000000..2ad68471e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5393-interface-array-holes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5393-interface-array-holes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5394-interface-implementers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5394-interface-implementers.ts.json new file mode 100644 index 000000000..a9941a015 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5394-interface-implementers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5394-interface-implementers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5390-interface-loop-element-lifetimes.ts b/tests/corpus/5390-interface-loop-element-lifetimes.ts new file mode 100644 index 000000000..64a645d80 --- /dev/null +++ b/tests/corpus/5390-interface-loop-element-lifetimes.ts @@ -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); diff --git a/tests/corpus/5391-interface-dispatch-hierarchies.ts b/tests/corpus/5391-interface-dispatch-hierarchies.ts new file mode 100644 index 000000000..e22866bbb --- /dev/null +++ b/tests/corpus/5391-interface-dispatch-hierarchies.ts @@ -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); diff --git a/tests/corpus/5392-interface-receiver-bindings.ts b/tests/corpus/5392-interface-receiver-bindings.ts new file mode 100644 index 000000000..ef1a9671e --- /dev/null +++ b/tests/corpus/5392-interface-receiver-bindings.ts @@ -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")); diff --git a/tests/corpus/5393-interface-array-holes.ts b/tests/corpus/5393-interface-array-holes.ts new file mode 100644 index 000000000..1aab1e2e1 --- /dev/null +++ b/tests/corpus/5393-interface-array-holes.ts @@ -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)); diff --git a/tests/corpus/5394-interface-implementers.ts b/tests/corpus/5394-interface-implementers.ts new file mode 100644 index 000000000..39bacb955 --- /dev/null +++ b/tests/corpus/5394-interface-implementers.ts @@ -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(" ")); diff --git a/tests/harness/interface-dispatch.test.ts b/tests/harness/interface-dispatch.test.ts index d0e0bf723..21e455e70 100644 --- a/tests/harness/interface-dispatch.test.ts +++ b/tests/harness/interface-dispatch.test.ts @@ -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",