From 9843f8efd2e0124fe81e6844b355ded71a9ee001 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 21:06:15 -0500 Subject: [PATCH] Inline interface dispatch and lend array elements to it Interface dispatch now unboxes a receiver it has matched by key with one load and no reference count change, and keeps its checked fallback out of line so the key tests inline at each call site. A loop over an array of interface values lends each element to a dispatch whose implementing methods cannot release it; the fallback, which can run any code, takes its own reference to the receiver. --- .../src/backend/llvm/call-lifetimes.ts | 1 + packages/compiler/src/backend/llvm/emitter.ts | 44 ++++++- .../compiler/src/backend/llvm/expr-async.ts | 8 ++ .../src/backend/llvm/loop-array-borrows.ts | 113 ++++++++++++++++-- .../src/backend/llvm/reference-effects.ts | 15 +++ .../lowering/class-dynamic-dispatch.ts | 11 +- .../lowering/class-protocol-dispatch.ts | 29 +++-- packages/compiler/src/ir/ir.ts | 15 ++- ...0-interface-loop-element-lifetimes.ts.json | 6 + ...391-interface-dispatch-hierarchies.ts.json | 6 + .../5392-interface-receiver-bindings.ts.json | 6 + .../corpus/5393-interface-array-holes.ts.json | 6 + .../5394-interface-implementers.ts.json | 6 + .../5390-interface-loop-element-lifetimes.ts | 71 +++++++++++ .../5391-interface-dispatch-hierarchies.ts | 61 ++++++++++ .../5392-interface-receiver-bindings.ts | 50 ++++++++ tests/corpus/5393-interface-array-holes.ts | 45 +++++++ tests/corpus/5394-interface-implementers.ts | 70 +++++++++++ tests/harness/interface-dispatch.test.ts | 44 +++++++ 19 files changed, 586 insertions(+), 21 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5390-interface-loop-element-lifetimes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5391-interface-dispatch-hierarchies.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5392-interface-receiver-bindings.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5393-interface-array-holes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5394-interface-implementers.ts.json create mode 100644 tests/corpus/5390-interface-loop-element-lifetimes.ts create mode 100644 tests/corpus/5391-interface-dispatch-hierarchies.ts create mode 100644 tests/corpus/5392-interface-receiver-bindings.ts create mode 100644 tests/corpus/5393-interface-array-holes.ts create mode 100644 tests/corpus/5394-interface-implementers.ts 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",