mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-11 02:03:59 +08:00
478 lines
15 KiB
TypeScript
478 lines
15 KiB
TypeScript
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
|
import { constructedValue } from "./constructor-returns.js";
|
|
import {
|
|
BOOL,
|
|
DYN,
|
|
F64,
|
|
STRING,
|
|
typeEquals,
|
|
typeKey,
|
|
type IrExpr,
|
|
type IrFunction,
|
|
type IrStmt,
|
|
type IrType,
|
|
type SrcLoc,
|
|
} from "../../ir/ir.js";
|
|
import { transformExpr, transformStmtList } from "../../ir/traverse.js";
|
|
import type { DispatchSites } from "./dispatch-sites.js";
|
|
import type { Lowerer } from "./lowerer.js";
|
|
import type { ParamShape } from "./call-signatures.js";
|
|
import { classPrototypeData } from "./class-prototypes.js";
|
|
import { classInstanceOf } from "./class-dynamic-dispatch.js";
|
|
import { inheritedBuiltinCtorFence } from "./lower-classes.js";
|
|
|
|
const PREFIX = "%class.construct:";
|
|
const PACKED = `${PREFIX}packed`;
|
|
const INSTANCEOF = "%class.instanceof:dynamic";
|
|
|
|
/** `value instanceof callee` for an untyped constructor value, through one
|
|
* shared helper. `refusal` is the site's message for a runtime
|
|
* constructor with no instance test. */
|
|
export function checkedClassInstanceOf(
|
|
lowerer: Lowerer,
|
|
value: IrExpr,
|
|
callee: IrExpr,
|
|
refusal: string,
|
|
loc: SrcLoc,
|
|
): IrExpr {
|
|
if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) {
|
|
const params = [
|
|
{ localId: "value", name: "value", type: DYN },
|
|
{ localId: "callee", name: "callee", type: DYN },
|
|
{ localId: "refusal", name: "refusal", type: STRING },
|
|
];
|
|
lowerer.liftedFns.push({
|
|
name: INSTANCEOF,
|
|
params,
|
|
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
|
returnType: BOOL,
|
|
body: [
|
|
{
|
|
kind: "return",
|
|
value: {
|
|
kind: "libCall",
|
|
fn: "bytes.instanceOf",
|
|
args: [
|
|
varRef("value", DYN, loc),
|
|
varRef("callee", DYN, loc),
|
|
varRef("refusal", STRING, loc),
|
|
],
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
],
|
|
loc,
|
|
});
|
|
}
|
|
return {
|
|
kind: "call",
|
|
callee: INSTANCEOF,
|
|
args: [value, callee, { kind: "strLit", value: refusal, type: STRING, loc }],
|
|
type: BOOL,
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** Checked construction dispatches to emitted native constructor thunks. */
|
|
export function checkedClassConstruction(
|
|
lowerer: Lowerer,
|
|
callee: IrExpr,
|
|
args: IrExpr[],
|
|
loc: SrcLoc,
|
|
): IrExpr {
|
|
const name = `${PREFIX}${args.length}`;
|
|
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
|
const params = [callee, ...args].map((_, index) => ({
|
|
localId: `p.${index}`,
|
|
name: `p${index}`,
|
|
type: DYN,
|
|
}));
|
|
lowerer.liftedFns.push({
|
|
name,
|
|
params,
|
|
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
|
returnType: DYN,
|
|
speculativeDispatch: true,
|
|
body: [
|
|
{
|
|
kind: "return",
|
|
value: {
|
|
kind: "libCall",
|
|
fn: "dyn.construct",
|
|
args: [
|
|
varRef("p.0", DYN, loc),
|
|
{
|
|
kind: "dynArrLit",
|
|
elems: params.slice(1).map((param) => varRef(param.localId, DYN, loc)),
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
{ kind: "strLit", value: "constructor", type: STRING, loc },
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
],
|
|
loc,
|
|
});
|
|
}
|
|
return { kind: "call", callee: name, args: [callee, ...args], type: DYN, loc };
|
|
}
|
|
|
|
/** Runtime-length constructor spreads keep argument evaluation before defaults. */
|
|
export function checkedClassConstructionPacked(
|
|
lowerer: Lowerer,
|
|
callee: IrExpr,
|
|
packed: IrExpr,
|
|
loc: SrcLoc,
|
|
): IrExpr {
|
|
if (!lowerer.liftedFns.some((fn) => fn.name === PACKED)) {
|
|
const params = [
|
|
{ localId: "p.0", name: "callee", type: DYN },
|
|
{ localId: "p.1", name: "arguments", type: DYN },
|
|
];
|
|
lowerer.liftedFns.push({
|
|
name: PACKED,
|
|
params,
|
|
locals: params.map((param) => ({
|
|
id: param.localId,
|
|
name: param.name,
|
|
type: param.type,
|
|
mutable: false,
|
|
})),
|
|
returnType: DYN,
|
|
speculativeDispatch: true,
|
|
body: [
|
|
{
|
|
kind: "return",
|
|
value: {
|
|
kind: "libCall",
|
|
fn: "dyn.construct",
|
|
args: [
|
|
varRef("p.0", DYN, loc),
|
|
varRef("p.1", DYN, loc),
|
|
{ kind: "strLit", value: "constructor", type: STRING, loc },
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
],
|
|
loc,
|
|
});
|
|
}
|
|
return { kind: "call", callee: PACKED, args: [callee, packed], type: DYN, loc };
|
|
}
|
|
|
|
function constructorArguments(
|
|
lowerer: Lowerer,
|
|
params: ParamShape[],
|
|
incoming: IrExpr[],
|
|
loc: SrcLoc,
|
|
packed?: IrExpr,
|
|
): IrExpr[] | null {
|
|
const args: IrExpr[] = [];
|
|
let index = 0;
|
|
for (const param of params) {
|
|
if (
|
|
packed &&
|
|
(param.mode === "rest" || param.mode === "dynRest" || param.mode === "arguments")
|
|
) {
|
|
const rest: IrExpr =
|
|
param.mode === "arguments"
|
|
? packed
|
|
: {
|
|
kind: "libCall",
|
|
fn: "dyn.arrayProtoCall",
|
|
args: [
|
|
packed,
|
|
{ kind: "strLit", value: "slice", type: STRING, loc },
|
|
{
|
|
kind: "dynArrLit",
|
|
elems: [
|
|
{
|
|
kind: "dynFrom",
|
|
value: { kind: "numLit", value: index, type: F64, loc },
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
const converted = lowerer.coerceToExpected(rest, param.type);
|
|
if (!typeEquals(converted.type, param.type)) return null;
|
|
args.push(converted);
|
|
continue;
|
|
}
|
|
if ((param.mode === "dynRest" || param.mode === "arguments") && param.type.kind === "dyn") {
|
|
args.push({
|
|
kind: "dynArrLit",
|
|
elems: param.mode === "arguments" ? incoming : incoming.slice(index),
|
|
type: DYN,
|
|
loc,
|
|
});
|
|
index = incoming.length;
|
|
} else if (param.mode === "rest" && param.type.kind === "array") {
|
|
const elems = incoming
|
|
.slice(index)
|
|
.map((value) =>
|
|
lowerer.coerceToExpected(value, param.type.kind === "array" ? param.type.elem : DYN),
|
|
);
|
|
if (
|
|
elems.some(
|
|
(value) => !typeEquals(value.type, param.type.kind === "array" ? param.type.elem : DYN),
|
|
)
|
|
)
|
|
return null;
|
|
args.push({ kind: "arrayLit", elems, type: param.type, loc });
|
|
index = incoming.length;
|
|
} else {
|
|
if (param.mode !== "required" && param.mode !== "omittable") return null;
|
|
let value: IrExpr = packed
|
|
? {
|
|
kind: "libCall",
|
|
fn: "dyn.arrAt",
|
|
args: [packed, { kind: "numLit", value: index++, type: F64, loc }],
|
|
type: DYN,
|
|
loc,
|
|
}
|
|
: (incoming[index++] ?? param.callDefault ?? dynUndefinedExpr(loc));
|
|
if (param.callDefault && value.type.kind === "dyn")
|
|
value = {
|
|
kind: "ternary",
|
|
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
|
|
then: lowerer.coerceToExpected(param.callDefault, DYN),
|
|
else_: value,
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
const converted = lowerer.coerceToExpected(value, param.type);
|
|
if (!typeEquals(converted.type, param.type)) return null;
|
|
args.push(converted);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export class ClassConstructionDispatch {
|
|
private readonly constructors = new Set<string>();
|
|
private readonly completed = new Map<string, Set<string>>();
|
|
private prototypeHelper: IrFunction | null = null;
|
|
private readonly prototypes = new Set<string>();
|
|
|
|
process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean {
|
|
const discover = (type: IrType): void => {
|
|
if (type.kind === "classval") this.constructors.add(type.className);
|
|
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
|
|
};
|
|
const prototypeReads = new Set<IrFunction>();
|
|
for (const fn of functions) {
|
|
const found = sites.of(fn);
|
|
found.values.forEach(discover);
|
|
if (found.basePrototypes) prototypeReads.add(fn);
|
|
}
|
|
let changed = false;
|
|
for (const fn of functions) {
|
|
if (fn === this.prototypeHelper || !prototypeReads.has(fn)) continue;
|
|
fn.body = transformStmtList(fn.body, {
|
|
stmt: (stmt) => stmt,
|
|
expr: (expr) => {
|
|
if (expr.kind !== "libCall" || expr.fn !== "dyn.classBasePrototype") return expr;
|
|
if (!this.prototypeHelper) {
|
|
const loc = expr.loc;
|
|
this.prototypeHelper = {
|
|
name: "%class.basePrototype",
|
|
params: [{ localId: "value", name: "value", type: DYN }],
|
|
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
|
returnType: DYN,
|
|
loc,
|
|
body: [
|
|
{ kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc },
|
|
],
|
|
};
|
|
lowerer.liftedFns.push(this.prototypeHelper);
|
|
changed = true;
|
|
}
|
|
return {
|
|
kind: "call",
|
|
callee: this.prototypeHelper.name,
|
|
args: expr.args,
|
|
type: DYN,
|
|
loc: expr.loc,
|
|
};
|
|
},
|
|
});
|
|
}
|
|
if (this.prototypeHelper)
|
|
for (const name of this.constructors) {
|
|
if (this.prototypes.has(name)) continue;
|
|
this.prototypes.add(name);
|
|
const info = lowerer.classes.get(name);
|
|
if (!info) continue;
|
|
const loc = this.prototypeHelper.loc;
|
|
const type: IrType = { kind: "classval", className: name };
|
|
const value = varRef("value", DYN, loc);
|
|
const prototype = classPrototypeData(lowerer, info, loc, {
|
|
kind: "dynCheck",
|
|
value,
|
|
type,
|
|
loc,
|
|
});
|
|
if (!prototype) continue;
|
|
const resolved = transformExpr(prototype, {
|
|
stmt: (stmt) => stmt,
|
|
expr: (expr) =>
|
|
expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype"
|
|
? {
|
|
kind: "call",
|
|
callee: this.prototypeHelper!.name,
|
|
args: expr.args,
|
|
type: DYN,
|
|
loc: expr.loc,
|
|
}
|
|
: expr,
|
|
});
|
|
this.prototypeHelper.body.unshift({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "libCall",
|
|
fn: "dyn.classIs",
|
|
args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: [{ kind: "return", value: resolved, loc }],
|
|
else_: null,
|
|
loc,
|
|
});
|
|
changed = true;
|
|
}
|
|
for (const fn of functions) {
|
|
if (!fn.name.startsWith(PREFIX) && fn.name !== INSTANCEOF) continue;
|
|
let completed = this.completed.get(fn.name);
|
|
if (!completed) this.completed.set(fn.name, (completed = new Set()));
|
|
const loc = fn.loc;
|
|
for (const name of this.constructors) {
|
|
if (completed.has(name)) continue;
|
|
completed.add(name);
|
|
const info = lowerer.classes.get(name);
|
|
if (!info || info.generic || info.def.runtime) continue;
|
|
if (fn.name === INSTANCEOF) {
|
|
const type: IrType = { kind: "classval", className: name };
|
|
const callee = varRef("callee", DYN, loc);
|
|
const result = classInstanceOf(lowerer, varRef("value", DYN, loc), info, loc, {
|
|
kind: "dynCheck",
|
|
value: callee,
|
|
type,
|
|
loc,
|
|
});
|
|
fn.body.unshift({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "libCall",
|
|
fn: "dyn.classIs",
|
|
args: [callee, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: [{ kind: "return", value: result, loc }],
|
|
else_: null,
|
|
loc,
|
|
});
|
|
changed = true;
|
|
continue;
|
|
}
|
|
const type: IrType = { kind: "classval", className: name };
|
|
const callee = varRef("p.0", DYN, loc);
|
|
// A constructor inherited from a runtime stream or EventEmitter
|
|
// takes no arguments here: arguments report at run time, as they do
|
|
// at compile time for typed construction.
|
|
const fence = inheritedBuiltinCtorFence(lowerer, info, loc);
|
|
if (fence !== null && fn.name !== PACKED && fn.params.length > 1) {
|
|
fn.body.unshift({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "libCall",
|
|
fn: "dyn.classIs",
|
|
args: [callee, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: [fence],
|
|
else_: null,
|
|
loc,
|
|
});
|
|
changed = true;
|
|
continue;
|
|
}
|
|
const args = constructorArguments(
|
|
lowerer,
|
|
info.ctorParams,
|
|
fn.params.slice(1).map((param) => varRef(param.localId, DYN, loc)),
|
|
loc,
|
|
fn.name === PACKED ? varRef("p.1", DYN, loc) : undefined,
|
|
);
|
|
if (!args) continue;
|
|
const value = constructedValue(lowerer, info, {
|
|
kind: "newValue",
|
|
callee: { kind: "dynCheck", value: callee, type, loc },
|
|
args,
|
|
type: { kind: "object", className: name },
|
|
loc,
|
|
});
|
|
const body: IrStmt[] = [
|
|
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
|
|
];
|
|
// A spread call's argument count is known only at run time.
|
|
if (fence !== null && fn.name === PACKED)
|
|
body.unshift({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "bin",
|
|
op: ">",
|
|
left: {
|
|
kind: "libCall",
|
|
fn: "dyn.arrLen",
|
|
args: [varRef("p.1", DYN, loc)],
|
|
type: F64,
|
|
loc,
|
|
},
|
|
right: { kind: "numLit", value: 0, type: F64, loc },
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: [fence],
|
|
else_: null,
|
|
loc,
|
|
});
|
|
fn.body.unshift({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "libCall",
|
|
fn: "dyn.classIs",
|
|
args: [callee, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: body,
|
|
else_: null,
|
|
loc,
|
|
});
|
|
lowerer.noteEdge(`%${name}.constructor`);
|
|
changed = true;
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
}
|