mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Inline interface dispatch and lend array elements to it (#997)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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)
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5390-interface-loop-element-lifetimes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5391-interface-dispatch-hierarchies.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5392-interface-receiver-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5393-interface-array-holes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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(" "));
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user