mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-09 10:50:02 +08:00
A JavaScript prototype method value runs over the call-site receiver when its private members are fields (brand-checked reads and writes) or private methods it calls. A private accessor, or a private method read as a value, would read a public property of the same name, so such a method keeps its native receiver, as before. A super method call with spread arguments over the call-site receiver now spreads them into the dynamic call instead of rejecting them.
642 lines
23 KiB
TypeScript
642 lines
23 KiB
TypeScript
import * as ts from "../ts7/adapter.js";
|
|
import {
|
|
BOOL,
|
|
DYN,
|
|
STRING,
|
|
RUNTIME_ERROR_CLASSES,
|
|
typeEquals,
|
|
typeKey,
|
|
type IrExpr,
|
|
type IrFunction,
|
|
type IrStmt,
|
|
type IrType,
|
|
type SrcLoc,
|
|
} from "../../ir/ir.js";
|
|
import { nodeThrowExpr, varRef } from "../../ir/build.js";
|
|
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
|
import { isJsSourceFile, locOf } from "../program.js";
|
|
import { newFnCtx, type Lowerer } from "./lowerer.js";
|
|
import {
|
|
classMemberNameOf,
|
|
findGenericMethodOn,
|
|
findMethodOn,
|
|
genericOverrideBelow,
|
|
type ClassInfo,
|
|
} from "./lower-classes.js";
|
|
import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js";
|
|
import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js";
|
|
import { errorToStringMethod } from "./error-methods.js";
|
|
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
|
|
import { perEvaluationPrivateNames } from "./private-brands.js";
|
|
|
|
export interface ClassMethodValueSelection {
|
|
expression: ts.PropertyAccessExpression;
|
|
info: ClassInfo;
|
|
method: string;
|
|
fallback: IrExpr;
|
|
fn: IrFunction;
|
|
checked: boolean;
|
|
values: Map<string, IrExpr>;
|
|
}
|
|
|
|
/** A method value retains its declaration's identity, not the receiver from
|
|
* extraction. Its native thunk validates the receiver supplied at call time. */
|
|
export function lowerClassMethodValue(
|
|
lowerer: Lowerer,
|
|
expr: ts.PropertyAccessExpression,
|
|
info: ClassInfo,
|
|
): IrExpr | null {
|
|
const method = expr.name.text;
|
|
if (method === "toString" && findMethodOn(lowerer, info, method)?.declarer.builtinError) {
|
|
return errorToStringMethod(lowerer, lowerer.lowerExpr(expr.expression));
|
|
}
|
|
const loc = locOf(expr);
|
|
const receiver = lowerer.lowerExpr(expr.expression);
|
|
const declared = findMethodOn(lowerer, info, method);
|
|
// A JS checker may name a class while the value is held in checked
|
|
// storage. Read its actual property so own replacements and getters
|
|
// participate, and never pass a dynamic value to a native selector. A
|
|
// method whose private names belong to one evaluation is that
|
|
// evaluation's prototype method as well.
|
|
if (receiver.type.kind === "dyn" || (!!declared && perEvaluationPrivateNames(declared.declarer)))
|
|
return {
|
|
kind: "dynKeyGet",
|
|
value: lowerer.coerceToExpected(receiver, DYN),
|
|
key: { kind: "strLit", value: method, type: STRING, loc },
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
// Abstract declarations have no prototype function. Concrete descendants
|
|
// provide the extracted value through the same reachability-driven selector.
|
|
let value = declared?.sig.abstract
|
|
? nodeThrowExpr(
|
|
1,
|
|
"",
|
|
`Missing implementation of method '${method}'`,
|
|
funcTypeFromParamShapes(declared.sig.params, declared.sig.ret),
|
|
loc,
|
|
)
|
|
: methodValue(lowerer, expr, info);
|
|
if (!value) return null;
|
|
const callback = isClassCallback(lowerer, info, method);
|
|
const local = callback ? lowerer.declareHiddenLocal("%callbackReceiver", receiver.type) : null;
|
|
const reference = local ? varRef(local.id, receiver.type, loc) : receiver;
|
|
const finish = (result: IrExpr): IrExpr => {
|
|
if (!local) return result;
|
|
const selected = classCallbackValue(lowerer, reference, method, result, loc);
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }],
|
|
result: selected,
|
|
type: selected.type,
|
|
loc,
|
|
};
|
|
};
|
|
const checked =
|
|
isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type);
|
|
if (checked) value = lowerer.coerceToExpected(value, DYN);
|
|
// Select the declaration when extracting the value. Calling the value
|
|
// later must not redispatch the method name on a different receiver.
|
|
const name = `%method.select:${info.def.name}.${method}:${checked ? "checked" : "typed"}`;
|
|
const receiverType: IrType = { kind: "object", className: info.def.name };
|
|
if (!lowerer.classMethodValueHelpers.has(name)) {
|
|
const body: IrStmt[] = [];
|
|
body.push({ kind: "return", value, loc });
|
|
const fn: IrFunction = {
|
|
name,
|
|
params: [{ localId: "this.0", name: "this", type: receiverType }],
|
|
returnType: value.type,
|
|
locals: [{ id: "this.0", name: "this", type: receiverType, mutable: false }],
|
|
body,
|
|
loc,
|
|
};
|
|
lowerer.liftedFns.push(fn);
|
|
lowerer.classMethodValueSelections.set(name, {
|
|
expression: expr,
|
|
info,
|
|
method,
|
|
fallback: value,
|
|
fn,
|
|
checked,
|
|
values: new Map(),
|
|
});
|
|
lowerer.classMethodValueHelpers.add(name);
|
|
}
|
|
return finish({ kind: "call", callee: name, args: [reference], type: value.type, loc });
|
|
}
|
|
|
|
/** Method extraction selects only constructible descendants, then expands
|
|
* at the same reachability fixed point as constructors. A late class must
|
|
* join an existing selector without reaching every imported subclass. */
|
|
export function refreshClassMethodValueSelections(lowerer: Lowerer): boolean {
|
|
let changed = false;
|
|
for (const selection of lowerer.classMethodValueSelections.values()) {
|
|
let added = false;
|
|
for (const candidate of lowerer.classes.values()) {
|
|
if (
|
|
candidate === selection.info ||
|
|
selection.values.has(candidate.def.name) ||
|
|
!lowerer.classCanBeConstructed(candidate) ||
|
|
!lowerer.isSubclassOf(candidate.def.name, selection.info.def.name) ||
|
|
!candidate.methods.has(selection.method) ||
|
|
candidate.methods.get(selection.method)?.abstract
|
|
)
|
|
continue;
|
|
let value = methodValue(lowerer, selection.expression, candidate);
|
|
if (value && selection.checked) value = lowerer.coerceToExpected(value, DYN);
|
|
if (value && !typeEquals(value.type, selection.fallback.type)) {
|
|
const widened = lowerer.coerceCovariantFunction(value, selection.fallback.type);
|
|
// Only a representation-preserving view can keep extraction's
|
|
// identity contract across differently typed base/derived views.
|
|
if (widened) value = widened;
|
|
}
|
|
if (!value || !typeEquals(value.type, selection.fallback.type))
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
selection.expression,
|
|
"method values with incompatible override signatures",
|
|
);
|
|
selection.values.set(candidate.def.name, value);
|
|
added = true;
|
|
}
|
|
if (!added) continue;
|
|
const loc = selection.fn.loc;
|
|
const receiver = varRef("this.0", selection.fn.params[0]!.type, loc);
|
|
const names = [...selection.values.keys()];
|
|
const values = [...selection.values]
|
|
.map(([className, value]) => ({
|
|
className,
|
|
value,
|
|
ancestors: names.filter((name) => lowerer.isSubclassOf(className, name)).length,
|
|
}))
|
|
.sort((a, b) => b.ancestors - a.ancestors);
|
|
selection.fn.body = values.map(({ className, value }): IrStmt => ({
|
|
kind: "if",
|
|
cond: { kind: "instanceOf", value: receiver, className, type: BOOL, loc },
|
|
then: [{ kind: "return", value, loc }],
|
|
else_: null,
|
|
loc,
|
|
}));
|
|
selection.fn.body.push({ kind: "return", value: selection.fallback, loc });
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function methodValue(
|
|
lowerer: Lowerer,
|
|
expr: ts.PropertyAccessExpression,
|
|
info: ClassInfo,
|
|
): IrExpr | null {
|
|
return classMethodValue(lowerer, expr, info, expr.name.text, locOf(expr));
|
|
}
|
|
|
|
export function classMethodValue(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
info: ClassInfo,
|
|
method: string,
|
|
loc: SrcLoc,
|
|
): IrExpr | null {
|
|
const found = findMethodOn(lowerer, info, method);
|
|
const generic = found ? null : findGenericMethodOn(lowerer, info, method);
|
|
if (!found && !generic) return null;
|
|
const declaration = found?.declarer ?? generic!.declarer;
|
|
const reflected = declaration.prototypeMethodValues?.get(method);
|
|
if (reflected) return reflected;
|
|
const checkedMethod = reflectedClassMethodValue(lowerer, declaration, method);
|
|
if (checkedMethod) return checkedMethod;
|
|
let owner: ClassInfo;
|
|
let params: ParamShape[];
|
|
let ret: IrType;
|
|
let callee: string;
|
|
if (found) {
|
|
owner = found.declarer;
|
|
if (found.sig.abstract)
|
|
lowerer.unsupported("SC1090", blame, "values of abstract method declarations");
|
|
params = found.sig.params;
|
|
ret = found.sig.ret;
|
|
callee = `%${owner.def.name}.${method}`;
|
|
if (!owner.builtinError) lowerer.noteEdge(callee);
|
|
} else if (
|
|
generic?.info.implicitParams &&
|
|
generic.declarer.decl &&
|
|
!lowerer.overrideBelow(info, method) &&
|
|
!genericOverrideBelow(info, method)
|
|
) {
|
|
owner = generic.declarer;
|
|
const instance = implicitDefaultInstance(lowerer, owner.decl!, generic.info);
|
|
params = instance.params;
|
|
ret = instance.returnType;
|
|
callee = instance.name;
|
|
} else if (
|
|
generic &&
|
|
!generic.info.implicitParams &&
|
|
generic.info.typeParams.length > 0 &&
|
|
generic.declarer.decl &&
|
|
!lowerer.overrideBelow(info, method) &&
|
|
!genericOverrideBelow(info, method)
|
|
) {
|
|
// A generic method as a value: its erased instance.
|
|
owner = generic.declarer;
|
|
const instance = erasedGenericInstance(lowerer, blame, generic.info);
|
|
params = instance.params;
|
|
ret = instance.returnType;
|
|
callee = instance.name;
|
|
} else {
|
|
lowerer.unsupported("SC1090", blame, "values of builtin or unspecialized generic methods");
|
|
}
|
|
const type = funcTypeFromParamShapes(params, ret);
|
|
const name = `%method.value:${callee}`;
|
|
if (!lowerer.classMethodValueHelpers.has(name)) {
|
|
const thunkParams = params.map((param, i) => ({
|
|
localId: `p.${i}`,
|
|
name: `p${i}`,
|
|
type: param.type,
|
|
}));
|
|
const receiverType: IrType = { kind: "object", className: owner.def.name };
|
|
const receiverName = `%method.receiver:${owner.def.name}`;
|
|
if (!lowerer.classMethodValueHelpers.has(receiverName)) {
|
|
lowerer.liftedFns.push({
|
|
name: receiverName,
|
|
params: [{ localId: "this.0", name: "this", type: DYN }],
|
|
returnType: receiverType,
|
|
locals: [{ id: "this.0", name: "this", type: DYN, mutable: false }],
|
|
body: [
|
|
{
|
|
kind: "return",
|
|
value: { kind: "dynCheck", value: varRef("this.0", DYN, loc), type: receiverType, loc },
|
|
loc,
|
|
},
|
|
],
|
|
loc,
|
|
});
|
|
lowerer.classMethodValueHelpers.add(receiverName);
|
|
}
|
|
const receiver: IrExpr = {
|
|
kind: "call",
|
|
callee: receiverName,
|
|
args: [{ kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }],
|
|
type: receiverType,
|
|
loc,
|
|
};
|
|
const call: IrExpr = owner.builtinError
|
|
? {
|
|
kind: "libCall",
|
|
fn: "error.toString",
|
|
args: [receiver],
|
|
type: ret,
|
|
loc,
|
|
}
|
|
: {
|
|
kind: "call",
|
|
callee,
|
|
args: [receiver, ...thunkParams.map((p) => varRef(p.localId, p.type, loc))],
|
|
type: ret,
|
|
loc,
|
|
};
|
|
const fn: IrFunction = {
|
|
name,
|
|
params: thunkParams,
|
|
returnType: ret,
|
|
locals: thunkParams.map((p) => ({
|
|
id: p.localId,
|
|
name: p.name,
|
|
type: p.type,
|
|
mutable: false,
|
|
})),
|
|
body:
|
|
ret.kind === "void"
|
|
? [
|
|
{ kind: "exprStmt", expr: call, loc },
|
|
{ kind: "return", value: null, loc },
|
|
]
|
|
: [{ kind: "return", value: call, loc }],
|
|
loc,
|
|
};
|
|
lowerer.liftedFns.push(fn);
|
|
lowerer.classMethodValueHelpers.add(name);
|
|
}
|
|
return { kind: "closure", fnName: name, captures: [], type, loc };
|
|
}
|
|
|
|
/** Public JS prototype methods are ordinary callables over their actual
|
|
* receiver, including descriptor-created copies of class instances. */
|
|
export function reflectedClassMethodValue(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
method: string,
|
|
): IrExpr | null {
|
|
return reflectedMethodValue(lowerer, info, method, false);
|
|
}
|
|
|
|
/** Static JavaScript methods retain their identity and receive the class
|
|
* value supplied by the call, including inherited and detached methods. */
|
|
export function reflectedClassStaticMethodValue(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
method: string,
|
|
): IrExpr | null {
|
|
return reflectedMethodValue(lowerer, info, method, true);
|
|
}
|
|
|
|
function reflectedMethodValue(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
method: string,
|
|
staticMethod: boolean,
|
|
): IrExpr | null {
|
|
const existing = (staticMethod ? info.staticMethodValues : info.prototypeMethodValues)?.get(
|
|
method,
|
|
);
|
|
if (existing) return existing;
|
|
const declaration = info.decl;
|
|
if (!declaration) return null;
|
|
const member = declaration.members.nodes.find((member) => {
|
|
if (
|
|
(!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) ||
|
|
!member.name ||
|
|
!!ts
|
|
.getModifiers(member)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) !== staticMethod
|
|
)
|
|
return false;
|
|
const name = classMemberNameOf(lowerer, member.name);
|
|
return (
|
|
(ts.isMethodDeclaration(member)
|
|
? name
|
|
: ts.isGetAccessor(member)
|
|
? `get:${name}`
|
|
: ts.isSetAccessor(member)
|
|
? `set:${name}`
|
|
: null) === method
|
|
);
|
|
});
|
|
if (!member || (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body)
|
|
return null;
|
|
let needsNativeReceiver =
|
|
!isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
|
|
info.localClass !== undefined ||
|
|
lowerer.errorHierarchyClassOf(info.def.name);
|
|
// `super.m(...)` in a prototype method reads the base class's prototype,
|
|
// which lowerSuperMethodCall answers over the call-site receiver when the
|
|
// base has a prototype object. Other uses of `super` keep the native
|
|
// receiver.
|
|
const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base);
|
|
// Over a dynamic receiver, private fields are brand-checked and read, and private methods
|
|
// brand-checked and called; a private accessor, or a private method read as a value, would become a
|
|
// lookup of its name.
|
|
const privateAccessors = new Set<string>();
|
|
const privateMethods = new Set<string>();
|
|
for (const other of declaration.members.nodes)
|
|
if (other.name !== undefined && ts.isPrivateIdentifier(other.name)) {
|
|
if (ts.isAccessor(other)) privateAccessors.add(other.name.text);
|
|
else if (ts.isMethodDeclaration(other)) privateMethods.add(other.name.text);
|
|
}
|
|
ts.walkPreorder(member, (node) => {
|
|
if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node)))
|
|
needsNativeReceiver = true;
|
|
if (!ts.isPrivateIdentifier(node)) return;
|
|
// Private members on a static method's receiver (the class value) keep
|
|
// the native receiver; prototype methods brand-check theirs, except
|
|
// private accessors and private methods used other than by a call.
|
|
if (
|
|
staticMethod ||
|
|
privateAccessors.has(node.text) ||
|
|
(privateMethods.has(node.text) && !isPrivateMethodCallee(node))
|
|
)
|
|
needsNativeReceiver = true;
|
|
});
|
|
if (needsNativeReceiver) return null;
|
|
// The method reads itself as a value inside its own body: it is the
|
|
// closure its lowering in progress produces.
|
|
if (lowerer.methodLambdasInProgress.has(member)) {
|
|
const pending = lowerer.methodLambdasInProgress.get(member);
|
|
if (!pending) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
member,
|
|
"class methods that read their own value while their signature is computed",
|
|
);
|
|
}
|
|
pending.selfReads++;
|
|
return {
|
|
kind: "closure",
|
|
fnName: pending.fnName,
|
|
captures: [],
|
|
type: pending.type,
|
|
loc: locOf(member),
|
|
};
|
|
}
|
|
// The method may be lowered on top of an unrelated caller's frames (a
|
|
// `super` call in another class's method reflects this class): its `this`
|
|
// is its own receiver, never an enclosing function's.
|
|
const context = newFnCtx(false, null, null, DYN);
|
|
context.thisBoundary = true;
|
|
const previousClass = lowerer.currentClass;
|
|
lowerer.currentClass = info;
|
|
lowerer.fnStack.push(context);
|
|
lowerer.methodLambdasInProgress.set(member, null);
|
|
try {
|
|
const value = lowerer.lowerLambda(member);
|
|
const pending = lowerer.methodLambdasInProgress.get(member);
|
|
if (
|
|
pending &&
|
|
pending.selfReads > 0 &&
|
|
(value.kind !== "closure" ||
|
|
value.fnName !== pending.fnName ||
|
|
value.captures.length > 0 ||
|
|
!typeEquals(value.type, pending.type))
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
member,
|
|
"class methods that read their own value inside their body when the method's value type is only settled by its body",
|
|
);
|
|
}
|
|
if (value.kind === "closure" && value.captures.length === 0) {
|
|
const body = lowerer.liftedFnNamed(value.fnName);
|
|
if (body?.captures?.length === 0) delete body.captures;
|
|
}
|
|
const cache = staticMethod
|
|
? (info.staticMethodValues ??= new Map())
|
|
: (info.prototypeMethodValues ??= new Map());
|
|
cache.set(method, value);
|
|
return value;
|
|
} finally {
|
|
lowerer.methodLambdasInProgress.delete(member);
|
|
lowerer.fnStack.pop();
|
|
lowerer.currentClass = previousClass;
|
|
}
|
|
}
|
|
|
|
/** Whether `base` and every class above it have a complete prototype object
|
|
* (reflectClassPrototype): module-level program classes that neither extend
|
|
* Date nor a runtime-provided class. */
|
|
function prototypeChainViewable(base: ClassInfo): boolean {
|
|
for (let c: ClassInfo | null = base; c; c = c.base)
|
|
if (
|
|
c.decl === null ||
|
|
c.localClass !== undefined ||
|
|
c.generic !== undefined ||
|
|
c.genericInstance !== undefined ||
|
|
c.mixinInstance !== undefined ||
|
|
c.callableBase !== undefined ||
|
|
c.extendsDate !== undefined ||
|
|
c.def.runtime ||
|
|
c.builtinEmitter ||
|
|
c.builtinStream ||
|
|
c.builtinError ||
|
|
c.builtinEventTarget
|
|
)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/** `super` as the receiver of a called member: `super.m(...)`. */
|
|
/** Whether `node`, a private name, is the method a call calls (`x.#m(...)`). */
|
|
function isPrivateMethodCallee(node: ts.Node): boolean {
|
|
const access = node.parent;
|
|
return (
|
|
access !== undefined &&
|
|
ts.isPropertyAccessExpression(access) &&
|
|
access.name === node &&
|
|
access.parent !== undefined &&
|
|
ts.isCallExpression(access.parent) &&
|
|
access.parent.expression === access
|
|
);
|
|
}
|
|
|
|
function isSuperMethodCallee(node: ts.Node): boolean {
|
|
const access = node.parent;
|
|
return (
|
|
access !== undefined &&
|
|
ts.isPropertyAccessExpression(access) &&
|
|
access.expression === node &&
|
|
access.parent !== undefined &&
|
|
ts.isCallExpression(access.parent) &&
|
|
access.parent.expression === access
|
|
);
|
|
}
|
|
|
|
/** Complete adapters after all native class bodies and instantiations exist. */
|
|
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
|
|
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
|
for (const fn of [...functions]) {
|
|
if (fn.name.startsWith("%method.receiver:") && fn.returnType.kind === "object") {
|
|
const owner = fn.returnType.className;
|
|
const loc = fn.loc;
|
|
const receiver = varRef("this.0", DYN, loc);
|
|
const branches: IrStmt[] = [];
|
|
if (lowerer.isSubclassOf(owner, "%Error")) {
|
|
const base: IrExpr = {
|
|
kind: "dynCheck",
|
|
value: receiver,
|
|
type: { kind: "object", className: "%Error" },
|
|
loc,
|
|
};
|
|
branches.push({
|
|
kind: "if",
|
|
cond: { kind: "dynTest", test: "error", value: receiver, type: BOOL, loc },
|
|
then: [
|
|
{
|
|
kind: "if",
|
|
cond: { kind: "instanceOf", value: base, className: owner, type: BOOL, loc },
|
|
then: [
|
|
{
|
|
kind: "return",
|
|
value:
|
|
owner === "%Error"
|
|
? base
|
|
: { kind: "downcast", value: base, type: fn.returnType, loc },
|
|
loc,
|
|
},
|
|
],
|
|
else_: null,
|
|
loc,
|
|
},
|
|
],
|
|
else_: null,
|
|
loc,
|
|
});
|
|
}
|
|
for (const info of lowerer.classes.values()) {
|
|
const name = info.def.name;
|
|
if (RUNTIME_ERROR_CLASSES.has(name)) continue;
|
|
if (
|
|
!lowerer.isSubclassOf(name, owner) &&
|
|
!lowerer.isSubclassOf(owner, name) &&
|
|
name !== owner
|
|
)
|
|
continue;
|
|
const type: IrType = { kind: "object", className: name };
|
|
const checked: IrExpr = { kind: "dynCheck", value: receiver, type, loc };
|
|
const value: IrExpr =
|
|
name === owner
|
|
? checked
|
|
: lowerer.isSubclassOf(name, owner)
|
|
? { kind: "upcast", value: checked, type: fn.returnType, loc }
|
|
: { kind: "downcast", value: checked, type: fn.returnType, loc };
|
|
const returned: IrStmt = { kind: "return", value, loc };
|
|
const then: IrStmt[] =
|
|
name === owner || lowerer.isSubclassOf(name, owner)
|
|
? [returned]
|
|
: [
|
|
{
|
|
kind: "if",
|
|
cond: { kind: "instanceOf", value: checked, className: owner, type: BOOL, loc },
|
|
then: [returned],
|
|
else_: null,
|
|
loc,
|
|
},
|
|
];
|
|
branches.push({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "libCall",
|
|
fn: "dyn.typedRefIs",
|
|
args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then,
|
|
else_: null,
|
|
loc,
|
|
});
|
|
}
|
|
fn.body.unshift(...branches);
|
|
}
|
|
if (!fn.name.startsWith("%method.value:")) continue;
|
|
const target = byName.get(fn.name.slice("%method.value:".length));
|
|
const thisParam = target?.params[0];
|
|
if (!target || !thisParam || target.async || target.generator) continue;
|
|
const usesThis =
|
|
!!target.classCaptures?.length ||
|
|
!everyStmtList(target.body, {
|
|
stmt: () => true,
|
|
expr: (expr) =>
|
|
!(expr.kind === "varRef" && expr.localId === thisParam.localId) &&
|
|
!(expr.kind === "closure" && expr.captures.includes(thisParam.localId)),
|
|
});
|
|
if (usesThis) continue;
|
|
// A method that never uses this remains callable when detached. Without
|
|
// class captures it needs no class environment, so the detached copy
|
|
// drops the (empty) capture list, which requires a receiver.
|
|
const name = `%method.receiverless:${target.name}`;
|
|
const { classCaptures: _classCaptures, ...detached } = target;
|
|
functions.push({
|
|
...detached,
|
|
name,
|
|
params: target.params.slice(1),
|
|
locals: target.locals.filter((local) => local.id !== thisParam.localId),
|
|
});
|
|
fn.body = transformStmtList(fn.body, {
|
|
stmt: (stmt) => stmt,
|
|
expr: (expr) =>
|
|
expr.kind === "call" && expr.callee === target.name
|
|
? { ...expr, callee: name, args: expr.args.slice(1) }
|
|
: expr,
|
|
});
|
|
}
|
|
}
|