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
49 changes: 29 additions & 20 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3772,19 +3772,41 @@ export class LlEmitter {
B.line(`store ${this.sizeType} 0, ptr ${integerSlot}`);
this.integerLoopBindings.set(integerLoop.localId, integerSlot);
} else if (s.init) {
this.emitStmt(s.init);
// A multi-declarator head shares the loop's scope. Emitting its
// IR block as an ordinary block would release captured/ref locals
// before the first condition.
const initializers = s.init.kind === "block" ? s.init.body : [s.init];
for (const initializer of initializers) this.emitStmt(initializer);
}
const lc = B.newLabel("loop.c");
const lb = B.newLabel("loop.b");
const le = B.newLabel("loop.e");
// JS `for (let i ...)`: each iteration gets a FRESH binding holding
// a copy of the previous one (closures made in iteration k keep
// seeing iteration k's value) — only observable, and only emitted,
// when the init variable is captured (boxed). The freshening (and
// the update) live in the continue-target block.
const initLocal = s.init?.kind === "varDecl" ? this.currentLocals.get(s.init.localId) : undefined;
const freshens = initLocal?.boxed === true;
const lu = s.update || freshens ? B.newLabel("loop.u") : lc;
// when a let variable is captured (boxed). Freshen before the first
// condition and again in the continue-target block before updating.
const initializers = s.init?.kind === "block" ? s.init.body : s.init ? [s.init] : [];
const capturedLets = initializers.flatMap((init) => {
const local = init.kind === "varDecl" ? this.currentLocals.get(init.localId) : undefined;
return local?.boxed && local.mutable ? [local] : [];
});
const freshenBindings = (): void => {
for (const local of capturedLets) {
const slot = `%${mangleLocal(local.id)}`;
const fresh = B.tmp();
const old = B.tmp();
B.line(`${fresh} = ${boxNewCall(this.shapeHost, local.type)} ; per-iteration ${local.name}`);
B.line(`${old} = load ptr, ptr ${slot}`);
const val = this.boxGet(old, local.type);
this.boxSet(fresh, local.type, val);
this.declare(`declare void @scr_box_release(ptr)`);
B.line(`call void @scr_box_release(ptr ${old})`);
B.line(`store ptr ${fresh}, ptr ${slot}`);
}
};
const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
freshenBindings();
B.br(lc);
B.startBlock(lc);
if (integerLoop && integerSlot) {
Expand Down Expand Up @@ -3815,20 +3837,7 @@ export class LlEmitter {
B.br(lu);
if (lu !== lc) {
B.startBlock(lu);
if (freshens && initLocal) {
const slot = `%${mangleLocal(initLocal.id)}`;
const fresh = B.tmp();
const old = B.tmp();
B.line(`${fresh} = ${boxNewCall(this.shapeHost, initLocal.type)} ; per-iteration ${initLocal.name}`);
B.line(`${old} = load ptr, ptr ${slot}`);
const val = this.boxGet(old, initLocal.type); // ref: +1 out
this.boxSet(fresh, initLocal.type, val); // takes ownership
this.declare(`declare void @scr_box_release(ptr)`);
B.line(`call void @scr_box_release(ptr ${old})`);
B.line(`store ptr ${fresh}, ptr ${slot}`);
// The wrapper scope's entry releases whatever the slot points
// to at loop exit — now the freshest binding. Nothing to fix.
}
freshenBindings();
if (integerLoop && integerSlot) {
const old = B.tmp();
const next = B.tmp();
Expand Down
8 changes: 4 additions & 4 deletions packages/compiler/src/backend/llvm/expr-stream-bridges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,14 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
host.declare(`declare ptr @scr_dyn_new_obj()`);
host.declare(`declare ptr @scr_dyn_copy_data_properties(ptr, ptr)`);
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
host.declare(`declare void @scr_str_release(ptr)`);
host.declare(`declare void @scr_dyn_key_delete(ptr, ptr, i1 zeroext)`);
lines.push(
` %bag = call ptr @scr_dyn_new_obj()`,
` %bag_copy = call ptr @scr_dyn_copy_data_properties(ptr %bag, ptr %d)`,
` %bag_copy = call ptr @scr_dyn_copy_property_descriptors(ptr %bag, ptr %d)`,
` call void @scr_dyn_release_v(ptr %bag_copy)`,
` %bag_pending = call zeroext i1 @scr_exc_pending()`,
` br i1 %bag_pending, label %bag_fail, label %bag_keys`,
Expand Down Expand Up @@ -597,13 +597,13 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
host.declare(`declare ptr @scr_dyn_copy_data_properties(ptr, ptr)`);
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
const slot = B.tmp(), bag = B.tmp(), copied = B.tmp();
B.line(`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`);
B.line(`${bag} = load ptr, ptr ${slot}`);
B.line(`${copied} = call ptr @scr_dyn_copy_data_properties(ptr ${out}, ptr ${bag})`);
B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`);
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
}
B.terminate(`ret ptr ${out}`);
Expand Down
53 changes: 53 additions & 0 deletions packages/compiler/src/frontend/lowering/class-descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as ts from "../ts7/adapter.js";
import { DYN, type IrExpr, type IrStmt, isDynTypedRefType } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import type { ClassInfo } from "./lower-classes.js";
import { isCompiledPrototypeMember } from "./class-prototypes.js";
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";

function literalName(name: ts.PropertyName): string | null {
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
}

/** Native layout fields cannot change descriptors. New named data properties
* live in the instance's shared bag, preserving attributes and identity. */
export function lowerClassDataDescriptor(lowerer: Lowerer, call: ts.CallExpression, member: string, target: IrExpr): IrExpr | null {
if (!isDynTypedRefType(target.type)) return null;
const info = lowerer.classes.get(target.type.className);
if (!info || info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream) return null;
const safeName = (owner: ClassInfo, name: string): boolean =>
!owner.fields.has(name) && !isCompiledPrototypeMember(lowerer, owner, name) &&
owner.subclasses.every((child) => safeName(child, name));
const descriptor = (node: ts.Expression): boolean => ts.isObjectLiteralExpression(node) &&
node.properties.every((p) => ts.isPropertyAssignment(p) &&
["value", "writable", "enumerable", "configurable"].includes(literalName(p.name) ?? ""));
const descriptors = call.arguments[member === "defineProperty" ? 2 : 1]!;
if (member === "defineProperty") {
const key = call.arguments[1]!;
if (!ts.isStringLiteral(key) || !safeName(info, key.text) || !descriptor(descriptors)) return null;
} else {
if (!ts.isObjectLiteralExpression(descriptors) || !descriptors.properties.every((p) => {
if (!ts.isPropertyAssignment(p)) return false;
const name = literalName(p.name);
return name !== null && name !== "__proto__" && safeName(info, name) && descriptor(p.initializer);
})) return null;
}
const loc = locOf(call);
const receiver = lowerer.declareHiddenLocal("%descriptorReceiver", target.type);
const desc = lowerer.declareHiddenLocal("%descriptors", DYN);
const value = varRef(receiver.id, target.type, loc);
const boxed = lowerer.coerceToExpected(value, DYN);
const helper = classPropertiesHelper(lowerer, loc);
const bag: IrExpr = { kind: "call", callee: helper.name, args: [boxed], type: DYN, loc };
const args = member === "defineProperty"
? [bag, lowerer.lowerExprExpecting(call.arguments[1]!, DYN), varRef(desc.id, DYN, loc)]
: [bag, varRef(desc.id, DYN, loc)];
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: receiver.id, init: target, loc },
{ kind: "varDecl", localId: desc.id, init: lowerer.lowerExprExpecting(descriptors, DYN), loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: member === "defineProperty" ? "dyn.defineProperty" : "dyn.defineProps", args, type: DYN, loc }, loc },
];
return { kind: "seqExpr", stmts, result: value, type: value.type, loc };
}
23 changes: 15 additions & 8 deletions packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { varRef } from "../../ir/build.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
Expand All @@ -21,6 +21,19 @@ interface PropertyDispatch {
classes: Set<string>;
}

export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc): IrFunction {
const name = "%dyn.class.properties";
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
if (existing) return existing;
const helper: IrFunction = {
name, params: [{ localId: "p.0", name: "value", type: DYN }], returnType: DYN,
locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
body: [{ kind: "return", value: varRef("p.0", DYN, loc), loc }], loc,
};
lowerer.liftedFns.push(helper);
return helper;
}

/** Calls on native class capsules keep the instance's compiled methods. The
* reachable-body fixed point discovers both the boxed classes and method
* names before generating checked native dispatch; ordinary dyn receivers
Expand Down Expand Up @@ -73,14 +86,8 @@ export class ClassDynamicDispatch {
if (this.boxed.size === 0) return false;
let changed = false;
if (!this.propertyBag) {
const loc = functions[0]!.loc;
this.propertyBag = {
name: "%dyn.class.properties", params: [{ localId: "p.0", name: "value", type: DYN }], returnType: DYN,
locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
body: [{ kind: "return", value: varRef("p.0", DYN, loc), loc }], loc,
};
this.propertyBag = classPropertiesHelper(lowerer, functions[0]!.loc);
this.generated.add(this.propertyBag);
lowerer.liftedFns.push(this.propertyBag);
changed = true;
}
for (const className of this.boxed) {
Expand Down
54 changes: 54 additions & 0 deletions packages/compiler/src/frontend/lowering/class-static-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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 { isJsSourceFile } from "../program.js";
import { findStaticOn, findGenericStaticOn, type ClassInfo } from "./lower-classes.js";
import type { Lowerer } from "./lowerer.js";

const constructorMembers = new Set([
"name", "length", "prototype", "__proto__", "constructor", "call", "apply", "bind",
"arguments", "caller", "toString", "toLocaleString", "valueOf", "hasOwnProperty",
"isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__",
"__lookupGetter__", "__lookupSetter__",
]);

/** Expando data on an exact JS constructor. Declared statics and Function
* members retain their existing lowering; this bag cannot replace them. */
export function classStaticDataFor(lowerer: Lowerer, info: ClassInfo, name: string, loc: SrcLoc): IrExpr | null {
if (!info.decl || !isJsSourceFile(info.decl.getSourceFile()) || name.startsWith("#") || constructorMembers.has(name)) return null;
if (findStaticOn(lowerer, info, name) || findGenericStaticOn(lowerer, info, name)) return null;
for (let c: ClassInfo | null = info; c; c = c.base) {
if (c.decl?.members.some((m) => m.name &&
ts.canHaveModifiers(m) && ts.getModifiers(m)?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword) &&
(ts.isComputedPropertyName(m.name) || ((ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) && m.name.text === name)))) return null;
}
return classStaticData(lowerer, info, loc);
}

function classStaticData(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrExpr | null {
if (info.localClass || info.mixinInstance || info.generic || info.genericInstance || info.classDecorators || info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) return null;
if (info.staticDataHelper === undefined) {
const base = info.base ? classStaticData(lowerer, info.base, loc) : null;
if (info.base && base === null) return null;
const name = `%static.data.${info.def.name}`;
const globalId = `%g.${name}`;
const readyId = `${globalId}.ready`;
info.staticDataHelper = name;
lowerer.globalsList.push({ id: globalId, name: `${info.def.name}.staticData`, type: DYN, mutable: true });
lowerer.globalsList.push({ id: readyId, name: `${info.def.name}.staticDataReady`, type: BOOL, mutable: true });
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: varRef(globalId, DYN, loc), loc },
],
};
lowerer.liftedFns.push(helper);
}
return { kind: "call", callee: info.staticDataHelper, args: [], type: DYN, loc };
}
25 changes: 8 additions & 17 deletions packages/compiler/src/frontend/lowering/containers/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,23 +193,14 @@ export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol:
return { kind: "bytesNew", source: count, type, loc };
}
const src = lowerer.lowerExpr(argNode);
if (src.type.kind === "union" && lowerer.armTag(src.type.unionId, UNDEFINED_T) >= 0) {
const present = lowerer.stripUndefinedArm(src.type);
if (present.kind === "array" && present.elem.kind === "f64") {
const undefTag = lowerer.armTag(src.type.unionId, UNDEFINED_T);
const presentTag = lowerer.armTag(src.type.unionId, present);
if (undefTag >= 0 && presentTag >= 0) {
const empty: IrExpr = { kind: "arrayLit", elems: [], type: present, loc };
const source: IrExpr = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId: src.type.unionId, tag: undefTag, negated: false, value: src, type: BOOL, loc },
then: empty,
else_: { kind: "unionNarrow", unionId: src.type.unionId, tag: presentTag, value: src, type: present, loc },
type: present,
loc,
};
return { kind: "bytesNew", source, type, loc };
}
if (src.type.kind === "union") {
const arms = lowerer.unions.get(src.type.unionId)?.arms;
if (arms?.every((arm) => arm.kind === "f64" || arm.kind === "bytes" ||
typeEquals(arm, UNDEFINED_T) || (arm.kind === "array" && arm.elem.kind === "f64"))) {
// The checked constructor dispatch preserves each arm's copy or
// length semantics, including undefined -> empty, and evaluates
// an effectful source expression exactly once.
return { kind: "bytesNew", source: lowerer.coerceInto(argNode, src, DYN), type, loc };
}
}
if (
Expand Down
Loading
Loading