Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"effect": "3.22.2",
"eslint": "^9.20.0",
"js-yaml": "4.3.0",
"three": "0.186.1",
"tsx": "^4.19.0",
"typescript": "5.9.3",
"typescript-eslint": "^8.24.0",
Expand Down
20 changes: 19 additions & 1 deletion packages/compiler/src/backend/llvm/expr-stream-bridges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { typedRefConstructor } from "./shapes.js";
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { type IrType, DYN_CLASS_PROPERTIES, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { mangleRecordStruct } from "../mangle.js";
import { mangleFunction, mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { classFieldIndex, classStructSym } from "./classes.js";
import { llvmCommentText } from "./common.js";
Expand Down Expand Up @@ -201,6 +201,15 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
` br i1 %bag_pending, label %bag_fail, label %bag_keys`,
`bag_fail:`, ` call void @scr_dyn_release_v(ptr %bag)`, ` br label %done`, `bag_keys:`,
);
if (meta.def.prototypeDataHelper) {
host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`);
lines.push(
` %prototype = call ptr @${mangleFunction(meta.def.prototypeDataHelper)}()`,
` %with_prototype = call ptr @scr_dyn_set_prototype(ptr %bag, ptr %prototype)`,
` call void @scr_dyn_release_v(ptr %prototype)`,
` call void @scr_dyn_release_v(ptr %with_prototype)`,
);
}
fields.forEach((field, index) => lines.push(
` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`,
Expand Down Expand Up @@ -564,6 +573,15 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`);
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_obj()`);
if (meta.def.prototypeDataHelper) {
host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
const prototype = B.tmp(), attached = B.tmp();
B.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper)}()`);
B.line(`${attached} = call ptr @scr_dyn_set_prototype(ptr ${out}, ptr ${prototype})`);
B.line(`call void @scr_dyn_release_v(ptr ${prototype})`);
B.line(`call void @scr_dyn_release_v(ptr ${attached})`);
}
for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) {
const { index } = classFieldIndex(meta, field.name);
const fieldPtr = B.tmp();
Expand Down
21 changes: 19 additions & 2 deletions packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { dynUndefinedExpr, PoisonError, type Lowerer } from "./lowerer.js";
import { implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
import { accessorCall, findGenericMethodOn, findMethodOn, upcastTo, type ClassInfo } from "./lower-classes.js";
import { classPrototypeData, hasClassPrototypeData } from "./class-prototypes.js";

type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
interface Dispatch {
Expand All @@ -31,6 +32,7 @@ export class ClassDynamicDispatch {
private readonly computed = new Map<string, Omit<PropertyDispatch, "name"> & { keyLocal: string; branchIndex: number }>();
private propertyBag: IrFunction | null = null;
private readonly bagClasses = new Set<string>();
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>>();
private readonly generated = new Set<IrFunction>();

process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
Expand Down Expand Up @@ -82,21 +84,36 @@ export class ClassDynamicDispatch {
changed = true;
}
for (const className of this.boxed) {
if (this.bagClasses.has(className)) continue;
const info = lowerer.classes.get(className);
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
const existingInit = this.bagInitializers.get(className);
if (existingInit && existingInit.value.kind === "dynObjLit" && hasClassPrototypeData(info)) {
const prototype = classPrototypeData(lowerer, info, existingInit.loc);
if (prototype) {
existingInit.value = { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc: existingInit.loc };
changed = true;
}
}
if (this.bagClasses.has(className)) continue;
this.bagClasses.add(className);
this.ensurePropertyBag(info);
const loc = this.propertyBag.loc;
const type: IrType = { kind: "object", className };
const value = varRef("p.0", DYN, loc);
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
const bag: IrExpr = { kind: "fieldGet", obj: receiver, className, field: PROPERTY_BAG, type: DYN, loc };
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null;
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = {
kind: "fieldSet", obj: receiver, className, field: PROPERTY_BAG, value: prototype
? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc }
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc,
};
this.bagInitializers.set(className, initialize);
this.propertyBag.body.unshift({
kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc },
then: [
{ kind: "if", cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc }, then: [
{ kind: "fieldSet", obj: receiver, className, field: PROPERTY_BAG, value: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc },
initialize,
], else_: null, loc },
{ kind: "return", value: bag, loc },
], else_: null, loc,
Expand Down
67 changes: 67 additions & 0 deletions packages/compiler/src/frontend/lowering/class-prototypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, type IrExpr, type IrFunction, type SrcLoc } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { locOf } from "../program.js";
import { exactClassOfReceiver, findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
import type { Lowerer } from "./lowerer.js";

/** Data added to a top-level class prototype has shared identity and remains
* separate from instance fields. Allocate lazily so inheritance and module
* initialization order do not require hoisting source assignments. */
export function classPrototypeData(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrExpr | null {
if (info.localClass || info.mixinInstance || info.generic || info.genericInstance || info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) return null;
if (info.def.prototypeDataHelper === undefined) {
const base = info.base ? classPrototypeData(lowerer, info.base, loc) : null;
if (info.base && base === null) return null;
const name = `%prototype.data.${info.def.name}`;
const globalId = `%g.${name}`;
const readyId = `${globalId}.ready`;
info.def.prototypeDataHelper = name;
const className = info.def.jsName ?? info.def.name;
lowerer.globalsList.push({ id: globalId, name: `${className}.prototype`, type: DYN, mutable: true });
lowerer.globalsList.push({ id: readyId, name: `${className}.prototype.ready`, type: BOOL, mutable: true });
const value = varRef(globalId, DYN, loc);
const helper: IrFunction = {
name, params: [], returnType: DYN, locals: [], loc,
body: [
{ kind: "if", cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, then: [
{ kind: "assign", localId: globalId, value: base
? { kind: "libCall", fn: "dyn.objCreate", args: [base], type: DYN, loc }
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc },
{ kind: "assign", localId: readyId, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
], else_: null, loc },
{ kind: "return", value, loc },
],
};
lowerer.liftedFns.push(helper);
}
return { kind: "call", callee: info.def.prototypeDataHelper, args: [], type: DYN, loc };
}

export function hasClassPrototypeData(info: ClassInfo): boolean {
return info.def.prototypeDataHelper !== undefined || (info.base !== null && hasClassPrototypeData(info.base));
}

export function isCompiledPrototypeMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
return name === "constructor" || name === "__proto__" ||
!!findMethodOn(lowerer, info, name) || !!findGenericMethodOn(lowerer, info, name) ||
!!findMethodOn(lowerer, info, `get:${name}`) || !!findMethodOn(lowerer, info, `set:${name}`);
}

/** Compiled methods still live in vtables. Keep prototype reflection and
* method replacement fenced until those descriptors have a native view. */
export function lowerClassPrototypeData(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
if (expr.questionDotToken || expr.name.text !== "prototype") return null;
const info = exactClassOfReceiver(lowerer, expr.expression);
if (!info) return null;
const parent = expr.parent;
const name = ts.isPropertyAccessExpression(parent) && parent.expression === expr ? parent.name.text
: ts.isElementAccessExpression(parent) && parent.expression === expr && parent.argumentExpression && ts.isStringLiteral(parent.argumentExpression)
? parent.argumentExpression.text : null;
if (name === null || isCompiledPrototypeMember(lowerer, info, name)) {
lowerer.unsupported("SC1090", expr, "class prototype reflection and compiled method replacement (named prototype data properties compile)");
}
const value = classPrototypeData(lowerer, info, locOf(expr));
if (value === null) lowerer.unsupported("SC1090", expr, "prototype data on local, generic, mixin, or runtime-provided classes");
return value;
}
2 changes: 1 addition & 1 deletion packages/compiler/src/frontend/lowering/lower-builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ export function isNativeFfiRequire(lowerer: Lowerer, expr: ts.Expression | undef
const cr = createRequireSpecOf(lowerer, call);
if (cr === null || cr.spec === null || canonicalBuiltinModule(cr.spec) !== null) return null;
const dep = resolveImport(lowerer.program, cr.baseFile, cr.spec) ??
npmStaticDepSf7(lowerer.program, cr.baseFile, cr.spec);
npmStaticDepSf7(lowerer.program, cr.baseFile, cr.spec, "require");
if (dep === null || dep.fileName.endsWith(".json")) return null;
return { spec: cr.spec, baseFile: cr.baseFile, dep };
}
Expand Down
21 changes: 15 additions & 6 deletions packages/compiler/src/frontend/lowering/lower-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AstNode } from "../ts7/ast-node.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
import { lowerGenMethodCall } from "./lower-generators.js";
import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, NULL_T, STRING, SYMBOL_T, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, NULL_T, STRING, SYMBOL_T, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js";
import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js";
import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
Expand Down Expand Up @@ -604,7 +604,7 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration,
});
const fixed = positional.map((shape, i): IrExpr => {
if (i < passed.length) return passed[i]!;
if (shape.mode === "omittable") return shape.callDefault ?? lowerer.undefinedArgFor(shape.type, loc, blame);
if (shape.mode === "omittable" || lowerer.bareUndefinedArmedUnion(shape.type)) return shape.callDefault ?? lowerer.undefinedArgFor(shape.type, loc, blame);
if (shape.type.kind === "dyn") return { kind: "dynFrom", value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: DYN, loc };
lowerer.unsupported("SC1090", blame, "this call form");
});
Expand All @@ -630,8 +630,17 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration,
"spread arguments into fixed parameter positions (a spread can only fill a rest parameter)",
);
}
if (arg) return lowerer.lowerExprExpecting(arg, shape.type);
if (shape.mode !== "omittable") {
if (arg) {
let operand = arg;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (ts.isVoidExpression(operand)) {
const absent = omittedArgFor(lowerer, shape.type, loc);
const effect = lowerStaticallyUndefinedArgument(lowerer, operand);
if (absent && effect) return defaultAfterUndefined(effect, absent);
}
return lowerer.lowerExprExpecting(arg, shape.type);
}
if (shape.mode !== "omittable" && !lowerer.bareUndefinedArmedUnion(shape.type)) {
// A missing argument for a CHECKED-DYNAMIC param (an implicit-any
// JS signature called short — `mustCall(fn)` with `expected`
// omitted): JS fills undefined, and the dyn slot holds exactly
Expand Down Expand Up @@ -9389,7 +9398,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
// A CHECKED-DYNAMIC receiver (the JS file-scope object-literal
// identity story): the runtime dyn probe — OBJ member presence, ARR
// index bounds, Node's ToObject TypeError on nullish.
if (probed?.type.kind === "dyn" || (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) {
if (probed?.type.kind === "dyn" || (probed && isDynTypedRefType(probed.type)) || (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) {
const loc = locOf(call);
const receiver = lowerer.coerceToExpected(probed, DYN);
const key = ownPropertyKey(lowerer, lowerer.lowerExpr(keyNode));
Expand Down Expand Up @@ -9472,7 +9481,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const isPrimitive = probed !== null && probed !== undefined &&
(isUnitType(probed.type) || probed.type.kind === "string" ||
probed.type.kind === "f64" || probed.type.kind === "bool");
if (isDyn || isPrimitive || isFunction) {
if (isDyn || isPrimitive || isFunction || (probed && isDynTypedRefType(probed.type))) {
const fn = member === "keys" ? "dyn.objKeys" : member === "values" ? "dyn.objValues" : "dyn.objEntries";
let v = lowerer.lowerExpr(argNode);
if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) };
Expand Down
20 changes: 20 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { recordTextCodecClass } from "../../ir/ir.js";
import { classSymbolKeyOf } from "./symbol-fields.js";
import { lowerClassMethodValue } from "./class-method-values.js";
import { lowerGlobalValue } from "./lower-global-value.js";
import { lowerClassPrototypeData } from "./class-prototypes.js";

/** An assignable `obj.field` target — a class field, a record field, or a
* class ACCESSOR property (reads become getter calls, writes setter calls;
Expand Down Expand Up @@ -1511,6 +1512,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);

if (ts.isPropertyAccessExpression(expr)) {
const prototypeData = lowerClassPrototypeData(lowerer, expr);
if (prototypeData) return prototypeData;
// `super.x`: the base chain's GETTER, called directly (super
// dispatch is static in JS — never through the dynamic class).
// super.method() calls are routed at the call site; a bare super
Expand Down Expand Up @@ -2150,6 +2153,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
`reading the abstract property '${expr.name.text}' through a '${lowerer.checker.typeToString(lowerer.typeOf(expr.expression))}'-typed receiver (abstract property declarations are erased at runtime, so no shared slot exists — type the receiver as the concrete class, or declare an abstract getter instead)`,
);
}
if (isDynTypedRefType(recvLowered.type)) {
const info = lowerer.classes.get(recvLowered.type.className);
if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream) {
return lowerer.maybeNarrow({
kind: "dynKeyGet", value: lowerer.coerceToExpected(recvLowered, DYN),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc,
}, expr);
}
}
lowerer.unsupported(
"SC1090",
expr,
Expand Down Expand Up @@ -9656,6 +9668,14 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
const key: IrExpr = { kind: "strLit", value: field, type: STRING, loc: locOf(expr.name) };
const keyed = lowerUnionKeyedRead(lowerer, expr, value.type.unionId, value, key, field);
if (keyed) return keyed;
// JavaScript overloads commonly inspect a class brand on a scalar-or-
// instance argument (e.g. Matrix4.makeTranslation). Preserve primitive
// missing-property semantics and dispatch class data through its native
// capsule instead of demanding a field shared by every union arm.
if (isJsSourceFile(expr.getSourceFile()) && def.arms.some(isDynTypedRefType) && lowerer.dynConvertible(value.type)) {
return { kind: "dynKeyGet", value: lowerer.coerceToExpected(value, DYN), key,
...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}), type: DYN, loc: locOf(expr) };
}
lowerer.unsupported(
"SC1090",
expr,
Expand Down
Loading