mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Improve module cycles and typed returns
- Initialize import cycles in module evaluation order and expand typed iterator and optional-value support. - Reduce boxing for captured parameters and optional array reads. - Adapt cycle collection to long-lived graphs while keeping static executables small.
This commit is contained in:
@@ -266,11 +266,22 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
];
|
||||
if (cyc) {
|
||||
host.declare(`@scr_cyc_live = external global i64`);
|
||||
host.declare(`@scr_cyc_old_freed = external global i64`);
|
||||
lines.push(
|
||||
` %sf.live = load i64, ptr @scr_cyc_live`,
|
||||
` %sf.live1 = sub i64 %sf.live, 1`,
|
||||
` store i64 %sf.live1, ptr @scr_cyc_live`,
|
||||
` %sf.blk = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR}`,
|
||||
` %sf.genp = getelementptr inbounds i8, ptr %sf.blk, i64 22`,
|
||||
` %sf.gen = load i16, ptr %sf.genp`,
|
||||
` %sf.old = icmp eq i16 %sf.gen, 2`,
|
||||
` br i1 %sf.old, label %sf.oldcount, label %sf.accounted`,
|
||||
`sf.oldcount:`,
|
||||
` %sf.freed = load i64, ptr @scr_cyc_old_freed`,
|
||||
` %sf.freed1 = add i64 %sf.freed, 1`,
|
||||
` store i64 %sf.freed1, ptr @scr_cyc_old_freed`,
|
||||
` br label %sf.accounted`,
|
||||
`sf.accounted:`,
|
||||
);
|
||||
}
|
||||
const blk = cyc ? "%sf.blk" : "%o";
|
||||
|
||||
@@ -473,6 +473,50 @@ test("unchanged let bindings can own call inputs but subsequent writes and captu
|
||||
expect(analyze(fn).bindings.get(fn.name)?.size).toBe(0);
|
||||
});
|
||||
|
||||
test("strict union equality projects both operands without escaping either box", () => {
|
||||
const equal = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "unionEq",
|
||||
unionId: "optional",
|
||||
negated: false,
|
||||
sameValue: false,
|
||||
left,
|
||||
right,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const fn = helper("same", ["value", "other"]);
|
||||
fn.body = [ret(equal(ref("value"), ref("other")))];
|
||||
expect(analyze(fn).parameters.get("same")).toEqual(new Set([0, 1]));
|
||||
// A nested consumer inside an operand is still visited.
|
||||
fn.body = [ret(equal(ref("value"), call("escape", [ref("other")])))];
|
||||
expect(analyze(fn).parameters.get("same")).toEqual(new Set([0]));
|
||||
});
|
||||
|
||||
test("environment bodies prove their own locals but never their parameters", () => {
|
||||
const leaf = helper("leaf");
|
||||
const fn = helper("lifted");
|
||||
fn.captures = [{ localId: "box", name: "box", type: optional }];
|
||||
fn.locals.push({ ...local("box"), boxed: true }, local("item"));
|
||||
fn.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "item",
|
||||
init: { kind: "unionWrap", value: num(), unionId: "optional", tag: 0, type: optional, loc },
|
||||
loc,
|
||||
},
|
||||
ret(call("leaf", [ref("item")])),
|
||||
ret(call("leaf", [ref("box")])),
|
||||
ret(call("leaf", [ref("value")])),
|
||||
];
|
||||
const facts = analyze(fn, leaf);
|
||||
expect(facts.locals.get("lifted")).toEqual(new Set(["item"]));
|
||||
expect(facts.bindings.get("lifted")).toEqual(new Set(["item"]));
|
||||
expect(facts.parameters.has("lifted")).toBe(false);
|
||||
expect(facts.borrowed.has("lifted")).toBe(false);
|
||||
fn.async = true;
|
||||
expect(analyze(fn, leaf).locals.has("lifted")).toBe(false);
|
||||
});
|
||||
|
||||
test("strict union equality is a borrowing use of a local operand", () => {
|
||||
const fn = helper("same", ["left", "right"]);
|
||||
fn.returnType = BOOL;
|
||||
|
||||
@@ -261,7 +261,26 @@ function work(expr: IrExpr, parameter: IrType, boxed = false, tdz = false): stri
|
||||
...(tdz ? { tdz: true } : {}),
|
||||
},
|
||||
],
|
||||
body: [{ kind: "return", value: expr, loc: receiverLoc }],
|
||||
body: [
|
||||
{ kind: "return", value: expr, loc: receiverLoc },
|
||||
// An unreachable rebinding keeps a boxed parameter in its shared
|
||||
// box; a captured parameter that is never rebound uses a plain slot.
|
||||
...(boxed
|
||||
? [
|
||||
{
|
||||
kind: "assign" as const,
|
||||
localId: "value",
|
||||
value: {
|
||||
kind: "varRef" as const,
|
||||
localId: "value",
|
||||
type: parameter,
|
||||
loc: receiverLoc,
|
||||
},
|
||||
loc: receiverLoc,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
loc: receiverLoc,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -137,8 +137,8 @@ import {
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { NullableRefFields, type NullableRefField } from "./nullable-fields.js";
|
||||
import {
|
||||
emitCallArrayRead,
|
||||
emitLocalArrayRead,
|
||||
emitProjectedArrayRead,
|
||||
emitBorrowedArrayRead,
|
||||
findLocalArrayReads,
|
||||
findCallArrayReads,
|
||||
@@ -164,6 +164,7 @@ import {
|
||||
emitStringSliceSnapshot,
|
||||
type StringSliceSnapshot,
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
import { StackCaptures } from "./stack-captures.js";
|
||||
import {
|
||||
ABSENT_FIELD_PAYLOAD,
|
||||
@@ -519,6 +520,11 @@ export class LlEmitter {
|
||||
readonly stackCallbacks: StackCallbacks;
|
||||
readonly receiverReaders: AmbientReceiverReaders;
|
||||
private readonly stackCaptures: StackCaptures;
|
||||
private readonly lazyCaptures: LazyCaptures;
|
||||
/** Unchanged captured parameters of the current function: each maps to
|
||||
* the slot caching its box, NULL until the first closure needs it. */
|
||||
private readonly lazyCaptureBoxes = new Map<string, string>();
|
||||
private readonly lazyStackBoxes = new Map<string, string>();
|
||||
private borrowedParameters = new Set<string>();
|
||||
/** Parameters proven projection-only: callers may pass stack boxes. */
|
||||
private projectedParameters = new Set<string>();
|
||||
@@ -737,6 +743,7 @@ export class LlEmitter {
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName);
|
||||
for (const r of mod.records ?? []) this.recordsById.set(r.id, r);
|
||||
const traced = computeTraced(mod);
|
||||
this.tracedShapes = traced.shapes;
|
||||
@@ -4029,6 +4036,33 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** The box a new environment captures for `localId` (borrowed). An
|
||||
* unchanged parameter gets one box on first capture, holding its own
|
||||
* reference to the value; the function scope releases the cached box. */
|
||||
captureBox(localId: string): string {
|
||||
const cache = this.lazyCaptureBoxes.get(localId);
|
||||
if (cache === undefined) return this.loadBox(`%${mangleLocal(localId)}`);
|
||||
const B = this.B;
|
||||
const type = this.currentLocals.get(localId)!.type;
|
||||
const current = this.loadBox(cache);
|
||||
const missing = B.tmp();
|
||||
B.line(`${missing} = icmp eq ptr ${current}, null`);
|
||||
const create = B.newLabel("lazy.box"),
|
||||
join = B.newLabel("lazy.box.j");
|
||||
B.condBr(missing, create, join);
|
||||
B.startBlock(create);
|
||||
const stack = this.lazyStackBoxes.get(localId);
|
||||
const box = stack ?? B.tmp();
|
||||
if (!stack) B.line(`${box} = ${boxNewCall(this.shapeHost, type)}`);
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = load ${this.llType(type)}, ptr %${mangleLocal(localId)}`);
|
||||
this.boxSet(box, type, isRefCounted(type) ? this.retainValue(value, type) : value);
|
||||
B.line(`store ptr ${box}, ptr ${cache}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
return this.loadBox(cache);
|
||||
}
|
||||
|
||||
retainBox(box: string): string {
|
||||
this.needsRetainBox = true;
|
||||
const t = this.B.tmp();
|
||||
@@ -4076,6 +4110,13 @@ export class LlEmitter {
|
||||
this.throwIfUninitialized(empty, name);
|
||||
}
|
||||
|
||||
/** Throws the TDZ ReferenceError for `name` unless the i1 `flag` is set. */
|
||||
checkInitializedFlag(flag: string, name: string): void {
|
||||
const empty = this.B.tmp();
|
||||
this.B.line(`${empty} = xor i1 ${flag}, true`);
|
||||
this.throwIfUninitialized(empty, name);
|
||||
}
|
||||
|
||||
private throwIfUninitialized(empty: string, name: string): void {
|
||||
const B = this.B;
|
||||
const lt = B.newLabel("tdz.t");
|
||||
@@ -4269,13 +4310,23 @@ export class LlEmitter {
|
||||
this.unwindCleanups.clear();
|
||||
this.jumpTargets = [];
|
||||
this.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
this.lazyCaptureBoxes.clear();
|
||||
this.lazyStackBoxes.clear();
|
||||
// An unchanged captured parameter keeps its value in a plain slot; the
|
||||
// binding is boxed only for closures (captureBox).
|
||||
const lazyParameters = this.lazyCaptures.parameters(fn);
|
||||
for (const id of lazyParameters) {
|
||||
const { boxed: _boxed, ...plain } = this.currentLocals.get(id)!;
|
||||
this.currentLocals.set(id, plain);
|
||||
this.lazyCaptureBoxes.set(id, "");
|
||||
}
|
||||
this.currentConstantCallbacks =
|
||||
this.debug === null ? (this.constantCallbacks.get(fn.name) ?? new Map()) : new Map();
|
||||
// Preserve concrete source bindings for debugger inspection. Suspended
|
||||
// functions keep the established array lifetime across continuations.
|
||||
this.streamingSplitsEnabled = this.debug === null && !fn.async && !fn.generator;
|
||||
this.stackCallbacks.reset(this.streamingSplitsEnabled);
|
||||
this.stackCaptures.reset(fn, this.streamingSplitsEnabled);
|
||||
this.stackCaptures.reset(fn, this.streamingSplitsEnabled, lazyParameters);
|
||||
this.privateSplitLocals = this.streamingSplitsEnabled ? findPrivateSplitLocals(fn) : new Map();
|
||||
this.storedSplits.clear();
|
||||
this.splitSpans.clear();
|
||||
@@ -4403,7 +4454,7 @@ export class LlEmitter {
|
||||
B.line(`${value} = load double, ptr @${mangleGlobal(global.id)}`);
|
||||
this.storeIntegerView(global.id, { name: value, type: global.type });
|
||||
}
|
||||
for (const local of fn.locals) {
|
||||
for (const local of this.currentLocals.values()) {
|
||||
// Boxed locals' slots hold their capture BOX (a ptr); captured
|
||||
// (env-borrowed) locals bind the incoming box below. A caught-typed
|
||||
// local is a catch binding: its slot holds the ScrCaught snapshot
|
||||
@@ -4459,6 +4510,20 @@ export class LlEmitter {
|
||||
// params (callees own their params — callers passed +1). Boxed params
|
||||
// allocate the shared binding and move the raw value in.
|
||||
const fnScope: LlScopeEntry[] = [];
|
||||
for (const id of this.lazyCaptureBoxes.keys()) {
|
||||
const cache = B.slot();
|
||||
B.entryAllocas.push(`${cache} = alloca ptr ; lazy box ${this.currentLocals.get(id)!.name}`);
|
||||
B.line(`store ptr null, ptr ${cache}`);
|
||||
fnScope.push({ slot: cache, type: this.currentLocals.get(id)!.type, boxed: true });
|
||||
this.lazyCaptureBoxes.set(id, cache);
|
||||
// Environments that finish during the call can share an immortal
|
||||
// frame box. Its payload owner is released with the function scope.
|
||||
const stack = this.stackCaptures.emit(this, id, this.currentLocals.get(id)!.type);
|
||||
if (stack) {
|
||||
this.lazyStackBoxes.set(id, stack.box);
|
||||
if (stack.owner) fnScope.push(stack.owner);
|
||||
}
|
||||
}
|
||||
const borrowed = this.callLifetimes.borrowed.get(fn.name);
|
||||
for (const [index, p] of fn.params.entries()) {
|
||||
const local = this.currentLocals.get(p.localId)!;
|
||||
@@ -6149,11 +6214,9 @@ export class LlEmitter {
|
||||
if (result.owner) this.ownSlot(result.owner.slot, result.owner.type);
|
||||
return result.value;
|
||||
}
|
||||
const arrayRead = this.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(this, arrayRead);
|
||||
if (canStackUnion(e, this.unionsById)) return emitStackUnion(this, e).value;
|
||||
// An optional array read consumed by a projection needs no heap box:
|
||||
// a private stack box owns the payload until the statement frame ends.
|
||||
const arrayRead = e.type.kind === "union" ? this.optionalArrayReads.get(e) : null;
|
||||
if (arrayRead) return emitCallArrayRead(this, arrayRead, true);
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = this.emitUnionProjection(e.value);
|
||||
return { name: this.unionPeek(union.name), type: e.type };
|
||||
@@ -6200,6 +6263,11 @@ export class LlEmitter {
|
||||
return this.emitExpr(e);
|
||||
}
|
||||
|
||||
/** A private stack union source with an independently owned payload. */
|
||||
canStackReceiver(e: IrExpr): boolean {
|
||||
return this.isStackUnionSource(e);
|
||||
}
|
||||
|
||||
/** True for union sources that emitReadReceiver can produce as a private
|
||||
* stack box with an independently owned payload (no heap union). */
|
||||
isStackUnionSource(e: IrExpr): boolean {
|
||||
|
||||
@@ -13,6 +13,16 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
case "intrinsic": {
|
||||
if (e.name === "module.tdzCheck") {
|
||||
// An import-cycle binding's initialization flag: false is the
|
||||
// temporal dead zone (Node's catchable ReferenceError).
|
||||
const flag = host.emitExpr(e.args[0]!);
|
||||
const name = e.args[1]!;
|
||||
if (name.kind !== "strLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: module.tdzCheck name");
|
||||
host.checkInitializedFlag(flag.name, name.value);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.name === "module.await") {
|
||||
// Internal ESM dependency wait: pending promises park the module
|
||||
// fiber, while settled ones continue synchronously.
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
mangleClassStruct,
|
||||
mangleFnClosure,
|
||||
mangleFunction,
|
||||
mangleLocal,
|
||||
mangleWrapper,
|
||||
mangleVtStruct,
|
||||
} from "../mangle.js";
|
||||
@@ -587,7 +586,7 @@ export function emitCallExpr(
|
||||
B.line(`store i32 ${functionKind}, ptr ${kindPtr}`);
|
||||
}
|
||||
e.captures.forEach((localId, i) => {
|
||||
const box = host.loadBox(`%${mangleLocal(localId)}`);
|
||||
const box = host.captureBox(localId);
|
||||
const retained = host.retainBox(box);
|
||||
const caps = B.tmp();
|
||||
const capp = B.tmp();
|
||||
@@ -786,7 +785,7 @@ export function emitCallExpr(
|
||||
`${value} = call ptr @scr_classobj_new(ptr @${sym}, ${host.sizeType} ${e.captures.length})`,
|
||||
);
|
||||
e.captures.forEach((id, index) => {
|
||||
const box = host.loadBox(`%${mangleLocal(id)}`);
|
||||
const box = host.captureBox(id);
|
||||
const owned = host.retainBox(box);
|
||||
const caps = B.tmp();
|
||||
const slot = B.tmp();
|
||||
|
||||
@@ -279,7 +279,12 @@ export function emitControlExpr(
|
||||
throw new InternalCompilerError("llvm emitter bug: optChain union arms");
|
||||
const multiple = def.arms.length - unitTags.length > 1;
|
||||
const narrowed = multiple ? e.receiver.type : def.arms[narrowIdx]!;
|
||||
const r = host.emitExpr(e.receiver);
|
||||
// A single present arm is only tested and extracted (+1) before the
|
||||
// body runs, so a fresh receiver box can stay on the stack.
|
||||
const r =
|
||||
!multiple && host.canStackReceiver(e.receiver)
|
||||
? host.emitReadReceiver(e.receiver)
|
||||
: host.emitExpr(e.receiver);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ${host.llType(narrowed)}`);
|
||||
B.line(
|
||||
@@ -481,6 +486,8 @@ export function emitControlExpr(
|
||||
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
|
||||
if (unitTags.length === 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
|
||||
const fused = emitNullishOptionalChain(host, e);
|
||||
if (fused) return fused;
|
||||
if (
|
||||
!typeEquals(e.type, e.left.type) &&
|
||||
(e.left.kind === "varRef" ||
|
||||
@@ -547,3 +554,62 @@ export function emitControlExpr(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** `a?.b ?? d` narrowed to the chain body's present arm. The body wraps a
|
||||
* value of the result type in a non-unit arm, so the default runs exactly
|
||||
* when the receiver is nullish and the intermediate result box is never
|
||||
* built. Receiver, body and default keep their evaluation order. */
|
||||
function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">): LlValue | null {
|
||||
const chain = e.left;
|
||||
if (
|
||||
chain.kind !== "optChain" ||
|
||||
chain.receiver.type.kind !== "union" ||
|
||||
chain.type.kind !== "union" ||
|
||||
chain.body.kind !== "unionWrap" ||
|
||||
chain.body.unionId !== chain.type.unionId ||
|
||||
!typeEquals(chain.body.value.type, e.type) ||
|
||||
isUnitType(e.type) ||
|
||||
e.type.kind === "void"
|
||||
)
|
||||
return null;
|
||||
const resultArm = host.unionsById.get(chain.type.unionId)?.arms[chain.body.tag];
|
||||
const receiverDef = host.unionsById.get(chain.receiver.type.unionId);
|
||||
if (!resultArm || isUnitType(resultArm) || !typeEquals(resultArm, e.type) || !receiverDef)
|
||||
return null;
|
||||
const unitTags = receiverDef.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
|
||||
const present = receiverDef.arms.flatMap((a, i) => (isUnitType(a) ? [] : [i]));
|
||||
if (unitTags.length === 0 || present.length !== 1) return null;
|
||||
const B = host.B;
|
||||
const narrowed = receiverDef.arms[present[0]!]!;
|
||||
const r = host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
const bindTy = host.llType(narrowed);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ${bindTy}`);
|
||||
B.line(
|
||||
`store ${bindTy} ${bindTy === "ptr" ? "null" : bindTy === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
|
||||
);
|
||||
host.ownSlot(bind, narrowed);
|
||||
const isUnit = host.tagInSet(r.name, unitTags);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const lu = B.newLabel("nulc.u"),
|
||||
lb = B.newLabel("nulc.b"),
|
||||
lj = B.newLabel("nulc.j");
|
||||
B.condBr(isUnit, lu, lb);
|
||||
B.startBlock(lu);
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
B.line(`store ${bindTy} ${host.unionExtract(r.name, narrowed)}, ptr ${bind}`);
|
||||
host.chainSlots.set(chain.id, { name: bind, type: narrowed, slot: true });
|
||||
host.emitBranchInto(slot, chain.body.value);
|
||||
host.chainSlots.delete(chain.id);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
@@ -1089,12 +1089,14 @@ function emitUnionEqWrappedScalar(
|
||||
const arm = def.arms[wrap.tag]!;
|
||||
let union: LlValue;
|
||||
let plain: LlValue | null = null;
|
||||
const unionInput = (value: IrExpr): LlValue =>
|
||||
host.canStackReceiver(value) ? host.emitReadReceiver(value) : emitBorrowedInput(host, value);
|
||||
if (rightWrap) {
|
||||
union = emitBorrowedInput(host, e.left);
|
||||
union = unionInput(e.left);
|
||||
if (!isUnitType(arm)) plain = host.emitExpr(wrap.value);
|
||||
} else {
|
||||
if (!isUnitType(arm)) plain = host.emitExpr(wrap.value);
|
||||
union = emitBorrowedInput(host, e.right);
|
||||
union = unionInput(e.right);
|
||||
}
|
||||
const tag = host.unionTag(union.name);
|
||||
const tagMatch = B.tmp();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
funcOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
|
||||
const loc = { file: "captures.ts", start: 0, end: 0 };
|
||||
const record: IrType = { kind: "record", shapeId: "options" };
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: record, loc });
|
||||
const closure = (fnName: string, captures: string[]): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "closure", fnName, captures, type: funcOf([], F64), loc },
|
||||
loc,
|
||||
});
|
||||
const assign = (localId: string): IrStmt => ({ kind: "assign", localId, value: ref(localId), loc });
|
||||
|
||||
function outer(body: IrStmt[], extra: Partial<IrFunction> = {}): IrFunction {
|
||||
return {
|
||||
name: "outer",
|
||||
params: [
|
||||
{ localId: "options", name: "options", type: record },
|
||||
{ localId: "plain", name: "plain", type: record },
|
||||
],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
{ id: "options", name: "options", type: record, mutable: true, boxed: true },
|
||||
{ id: "plain", name: "plain", type: record, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
function lifted(name: string, body: IrStmt[], captured = "options"): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [],
|
||||
returnType: F64,
|
||||
captures: [{ localId: captured, name: captured, type: record }],
|
||||
locals: [{ id: captured, name: captured, type: record, mutable: true, boxed: true }],
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
function lazy(...functions: IrFunction[]): Set<string> {
|
||||
const map = new Map(functions.map((fn) => [fn.name, fn]));
|
||||
return new LazyCaptures(map).parameters(functions[0]!);
|
||||
}
|
||||
|
||||
test("captured parameters that nothing rebinds are boxed lazily", () => {
|
||||
const reader = lifted("reader", [{ kind: "return", value: ref("options"), loc }]);
|
||||
expect(lazy(outer([closure("reader", ["options"])]), reader)).toEqual(new Set(["options"]));
|
||||
});
|
||||
|
||||
test("rebinding in the body or in any nested function keeps the eager box", () => {
|
||||
const reader = lifted("reader", []);
|
||||
expect(lazy(outer([closure("reader", ["options"]), assign("options")]), reader).size).toBe(0);
|
||||
const writer = lifted("writer", [assign("options")]);
|
||||
expect(lazy(outer([closure("writer", ["options"])]), writer).size).toBe(0);
|
||||
// A write two closures deep reaches the same binding.
|
||||
const inner = lifted("inner", [{ kind: "return", value: ref("x"), loc }], "x");
|
||||
const middle = lifted("middle", [closure("inner", ["mid"])], "mid");
|
||||
expect(lazy(outer([closure("middle", ["options"])]), middle, inner).size).toBe(1);
|
||||
const deep = lifted("inner", [assign("x")], "x");
|
||||
expect(lazy(outer([closure("middle", ["options"])]), middle, deep).size).toBe(0);
|
||||
});
|
||||
|
||||
test("class environments, async bodies and unknown targets keep the eager box", () => {
|
||||
const classRef: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "classRef",
|
||||
className: "Local",
|
||||
captures: ["options"],
|
||||
type: { kind: "classval", className: "Local" },
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
expect(lazy(outer([classRef])).size).toBe(0);
|
||||
const reader = lifted("reader", []);
|
||||
expect(lazy(outer([closure("reader", ["options"])], { async: true }), reader).size).toBe(0);
|
||||
expect(lazy(outer([closure("missing", ["options"])])).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
|
||||
/** The local a node rebinds. Writes through the bound reference, such as a
|
||||
* field store or an array push, leave the binding unchanged. Catch bindings
|
||||
* and rethrow targets are conservatively treated as rebinding. */
|
||||
function rebound(node: IrStmt | IrExpr): string | null {
|
||||
switch (node.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
case "forOf":
|
||||
case "rethrow":
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
return node.localId;
|
||||
case "tryCatch":
|
||||
return node.catchLocalId;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** A captured parameter that no code rebinds holds its entry value for the
|
||||
* whole call, so every environment may observe a box created when the first
|
||||
* closure is built instead of on function entry. Paths that never create a
|
||||
* closure then allocate no box. The proof follows each capture into nested
|
||||
* function bodies; class environments keep the eager box. */
|
||||
export class LazyCaptures {
|
||||
private readonly unchanged = new Map<string, boolean>();
|
||||
|
||||
constructor(private readonly functions: ReadonlyMap<string, IrFunction>) {}
|
||||
|
||||
parameters(fn: IrFunction): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
const locals = new Map(fn.locals.map((local) => [local.id, local]));
|
||||
for (const param of fn.params) {
|
||||
const local = locals.get(param.localId);
|
||||
if (local?.boxed && !local.tdz) result.add(param.localId);
|
||||
}
|
||||
if (result.size === 0) return result;
|
||||
for (const id of this.rebindings(fn, result)) result.delete(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The subset of `ids` rebound in `fn` or in any function that captures
|
||||
* one of them, directly or through further nesting. */
|
||||
private rebindings(fn: IrFunction, ids: ReadonlySet<string>): Set<string> {
|
||||
const changed = new Set<string>();
|
||||
const nested: { target: string; local: string; source: string }[] = [];
|
||||
const visit = (node: IrStmt | IrExpr): boolean => {
|
||||
const target = rebound(node);
|
||||
if (target !== null && ids.has(target)) changed.add(target);
|
||||
if (node.kind === "classRef") {
|
||||
for (const id of node.captures ?? []) if (ids.has(id)) changed.add(id);
|
||||
} else if (node.kind === "closure") {
|
||||
const callee = this.functions.get(node.fnName);
|
||||
node.captures.forEach((id, index) => {
|
||||
if (!ids.has(id)) return;
|
||||
const capture = callee?.captures?.[index];
|
||||
if (!capture) changed.add(id);
|
||||
else nested.push({ target: node.fnName, local: capture.localId, source: id });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
everyStmtList(fn.body, { stmt: visit, expr: visit });
|
||||
for (const edge of nested) {
|
||||
if (!changed.has(edge.source) && !this.isUnchanged(edge.target, edge.local))
|
||||
changed.add(edge.source);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private isUnchanged(fnName: string, localId: string): boolean {
|
||||
const key = `${fnName}\0${localId}`;
|
||||
const known = this.unchanged.get(key);
|
||||
if (known !== undefined) return known;
|
||||
// Capture chains follow lexical nesting and cannot cycle; the
|
||||
// provisional entry only bounds work on malformed input.
|
||||
this.unchanged.set(key, false);
|
||||
const fn = this.functions.get(fnName);
|
||||
const result = fn !== undefined && this.rebindings(fn, new Set([localId])).size === 0;
|
||||
this.unchanged.set(key, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -272,6 +272,19 @@ export function emitCallArrayRead(
|
||||
return { name: value, type: read.type };
|
||||
}
|
||||
|
||||
/** A tag or payload projection consumes the box before its frame ends. The
|
||||
* payload owns one reference, so later operands may mutate the array without
|
||||
* invalidating the snapshot; the box itself never reaches runtime code. */
|
||||
export function emitProjectedArrayRead(host: LlvmEmitterContext, read: LocalArrayRead): LlValue {
|
||||
const slot = host.B.slot();
|
||||
host.B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const owner = emitLocalArrayRead(host, { ...read, borrow: false }, slot);
|
||||
if (owner) host.ownSlot(owner.slot, owner.type);
|
||||
const value = host.B.tmp();
|
||||
host.B.line(`${value} = load ptr, ptr ${slot}`);
|
||||
return { name: value, type: read.type };
|
||||
}
|
||||
|
||||
/** Share the checked dense lookup between optional stack boxes and strict
|
||||
* borrowed reads. Capacity, length, and presence guards precede every load;
|
||||
* sparse and noncanonical indices keep their runtime lookup semantics. */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IrExpr, IrFunction } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { mangleLocal } from "../mangle.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** A synchronous, invocation-only consumer cannot retain the environment.
|
||||
@@ -39,7 +38,7 @@ export class StackCallbacks {
|
||||
B.line(`store i32 4, ptr ${kind}`);
|
||||
}
|
||||
value.captures.forEach((localId, index) => {
|
||||
const box = host.retainBox(host.loadBox(`%${mangleLocal(localId)}`));
|
||||
const box = host.retainBox(host.captureBox(localId));
|
||||
const caps = B.tmp();
|
||||
const slot = B.tmp();
|
||||
B.line(`${caps} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 1`);
|
||||
|
||||
@@ -25,7 +25,9 @@ export class StackCaptures {
|
||||
private readonly callbacks: StackCallbacks,
|
||||
) {}
|
||||
|
||||
reset(fn: IrFunction, enabled: boolean): void {
|
||||
/** `parameters` are unchanged captured parameters (LazyCaptures). Their
|
||||
* scope is the whole call, so they need no single declaration. */
|
||||
reset(fn: IrFunction, enabled: boolean, parameters: ReadonlySet<string> = new Set()): void {
|
||||
this.locals = new Set();
|
||||
this.remaining = enabled ? 2048 : 0;
|
||||
if (!enabled) return;
|
||||
@@ -34,7 +36,7 @@ export class StackCaptures {
|
||||
);
|
||||
if (possible.size === 0) return;
|
||||
const invalid = new Set([
|
||||
...fn.params.map((param) => param.localId),
|
||||
...fn.params.map((param) => param.localId).filter((id) => !parameters.has(id)),
|
||||
...(fn.captures ?? []).map((capture) => capture.localId),
|
||||
...(fn.classCaptures ?? []).map((capture) => capture.localId),
|
||||
]);
|
||||
@@ -89,7 +91,8 @@ export class StackCaptures {
|
||||
};
|
||||
if (!fn.body.every(stmt)) return;
|
||||
for (const id of possible) {
|
||||
if (!invalid.has(id) && declarations.get(id) === 1) this.locals.add(id);
|
||||
if (!invalid.has(id) && (declarations.get(id) === 1 || parameters.has(id)))
|
||||
this.locals.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ export function computeMayThrow(mod: IrModule): { fns: Set<string>; indirect: bo
|
||||
// Module dependency evaluation has await's rejection behavior,
|
||||
// while deliberately avoiding await's extra settled-promise turn.
|
||||
if (rec.name === "module.await") f.throws = true;
|
||||
// A cycle binding read before its declaration ran (TDZ).
|
||||
if (rec.name === "module.tdzCheck") f.throws = true;
|
||||
break;
|
||||
case "yieldExpr":
|
||||
// A consumer .throw() surfaces at the yield (and .return()'s
|
||||
|
||||
@@ -197,17 +197,19 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
|
||||
hint: "export declarations directly: export function f() {}",
|
||||
},
|
||||
SC1015: { feature: "dynamic import()", milestone: "M4" },
|
||||
// Benign cycles are ADMITTED (Node runs them as cache hits): cycles of
|
||||
// ES modules whose top levels are declaration-only and whose
|
||||
// cycle-crossing bindings are used only inside function bodies (mutual
|
||||
// recursion, cross-module class references), plus closing edges that
|
||||
// bind nothing readable. SC1016 remains for exactly the cycles where
|
||||
// Node's partial initialization is observable — the message names the
|
||||
// offending binding or top-level statement.
|
||||
// ES-module cycles are ADMITTED and evaluate in Node's order: module
|
||||
// bodies run depth-first, function declarations are callable from the
|
||||
// start, and let/const bindings that can be read before their
|
||||
// declaration carry an initialization check (Node's TDZ ReferenceError).
|
||||
// SC1016 remains for the shapes without an exact representation —
|
||||
// CommonJS members, top-level await, default-export expressions,
|
||||
// namespace imports or re-exports of cycle members, and var or class
|
||||
// bindings reachable before their declaration ran. The message names
|
||||
// the offending construct.
|
||||
SC1016: {
|
||||
feature: "circular imports",
|
||||
milestone: "later",
|
||||
hint: "cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window",
|
||||
hint: "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge",
|
||||
},
|
||||
// Class DECLARATIONS shipped; SC1020's remaining use is class expressions.
|
||||
SC1020: { feature: "class expressions", milestone: "later" },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { identityPreservingWidening } from "../coercions/identity.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import {
|
||||
@@ -987,6 +988,12 @@ export function lowerArrayFromCall(
|
||||
if (src.type.kind === "dyn" && args.length === 1) {
|
||||
return lowerCheckedArrayFrom(lowerer, src, loc);
|
||||
}
|
||||
// A generator (or an iterator adapted into one) drains in order.
|
||||
if (src.type.kind === "generator" && args.length === 1) {
|
||||
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const drained = arrT?.kind === "array" ? generatorDrain(lowerer, src, arrT.elem, loc) : null;
|
||||
if (drained) return drained;
|
||||
}
|
||||
lowerer.noLowering(
|
||||
"Array.from with this argument shape",
|
||||
call,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerArrayIteratorValue } from "../iterator-adapters.js";
|
||||
import { identityPreservingWidening } from "../coercions/identity.js";
|
||||
import { arrayConcatHelper } from "./array-concat.js";
|
||||
import { boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
@@ -158,7 +159,15 @@ export function lowerArrayMethodCall(
|
||||
): IrExpr | null {
|
||||
if (lowerer.chainBlocked(access, call)) return null;
|
||||
const name = access.name.text;
|
||||
if (!ARRAY_METHODS.has(name) && name !== "sort" && name !== "shift" && name !== "splice")
|
||||
const iteratorMethod =
|
||||
(name === "values" || name === "keys" || name === "entries") && call.arguments.length === 0;
|
||||
if (
|
||||
!ARRAY_METHODS.has(name) &&
|
||||
name !== "sort" &&
|
||||
name !== "shift" &&
|
||||
name !== "splice" &&
|
||||
!iteratorMethod
|
||||
)
|
||||
return null;
|
||||
let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const checkerReceiver = lowerer.checker.getTypeAtLocation(access.expression);
|
||||
@@ -201,6 +210,18 @@ export function lowerArrayMethodCall(
|
||||
if (!probedUntyped && !lowerer.isStdlibMember(access)) return null;
|
||||
let elem = receiverIr.elem;
|
||||
const loc = locOf(call);
|
||||
if (iteratorMethod) {
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
// Only a static array resolves to a generator here; other lowered
|
||||
// receivers (island arrays, narrowed unions, checked dynamic values)
|
||||
// continue to their dedicated handling below.
|
||||
if (receiver.type.kind === "array") {
|
||||
const value = type ? lowerArrayIteratorValue(lowerer, receiver, name, type, loc) : null;
|
||||
if (value) return value;
|
||||
lowerer.unsupported("SC1090", call, `'${name}()' iterators over this array element type`);
|
||||
}
|
||||
}
|
||||
// An island handle behind an array-typed .d.ts surface
|
||||
// (`parts().join("-")` — arrays never exit eagerly, so the value
|
||||
// stays jsval): the ENGINE's own Array.prototype method runs on the
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/* Import-cycle initialization state: module bindings of a cycle that some
|
||||
* read or write can reach before their declarations ran
|
||||
* (cycleEarlyBindings) get a BOOL initialization flag. The declaring
|
||||
* module's %init sets the flag right after the declaration's store, and
|
||||
* every access from code that can run earlier checks it first — Node's
|
||||
* temporal-dead-zone ReferenceError, with the same message. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { cycleEarlyBindings, locOf } from "../program.js";
|
||||
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import {
|
||||
BOOL,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrGlobal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmt, transformStmtList } from "../../ir/traverse.js";
|
||||
|
||||
/** One flagged binding: its storage, its flag, and the module bodies whose
|
||||
* top-level code only runs after the declaration (no check needed). */
|
||||
export interface CycleInitFlag {
|
||||
global: IrGlobal;
|
||||
flagId: string;
|
||||
name: string;
|
||||
/** The declaring module. */
|
||||
module: ts.SourceFile;
|
||||
settled: ReadonlySet<ts.SourceFile>;
|
||||
}
|
||||
|
||||
/** Registers the initialization flags after global collection. Bindings
|
||||
* whose early access has no exact representation are refused: a `var`
|
||||
* (Node reads `undefined` there, but module storage only starts when the
|
||||
* declaring module's body starts), a class (its references are static), a
|
||||
* binding without module storage, or one with a use the lowering resolves
|
||||
* from the binding's type (a property key, a `typeof` operand). */
|
||||
export function markCycleEarlyBindings(lowerer: Lowerer): void {
|
||||
const early = cycleEarlyBindings(
|
||||
lowerer.program,
|
||||
lowerer.entry,
|
||||
lowerer.moduleOrder,
|
||||
lowerer.forkTargets.length === 0 && lowerer.workerTargets.length === 0,
|
||||
);
|
||||
const refuse = (node: ts.Node, reason: string): void => {
|
||||
if (lowerer.remainder) return;
|
||||
lowerer.pushDiag(unsupportedDiag("SC1016", locOf(node), `circular imports (${reason})`));
|
||||
};
|
||||
for (const binding of early) {
|
||||
const where = `'${binding.name.text}' declared in ${binding.module.fileName} can be accessed before its declaration runs`;
|
||||
if (binding.kind === "var") {
|
||||
refuse(binding.reference, `the var binding ${where}, where Node reads undefined`);
|
||||
continue;
|
||||
}
|
||||
if (binding.kind === "class") {
|
||||
refuse(binding.reference, `the class ${where}`);
|
||||
continue;
|
||||
}
|
||||
const global = lowerer.globalsBySymbol.get(binding.symbol);
|
||||
if (global === undefined) {
|
||||
refuse(binding.reference, `the binding ${where}, and it has no module storage to check`);
|
||||
continue;
|
||||
}
|
||||
if (binding.folded !== undefined) {
|
||||
refuse(
|
||||
binding.folded,
|
||||
`the binding ${where}, and this use is resolved from its type rather than its storage`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (lowerer.cycleInitFlags.has(global.id)) continue;
|
||||
const flagId = `${global.id}%initialized`;
|
||||
lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
|
||||
global.initFlag = flagId;
|
||||
const flag: CycleInitFlag = {
|
||||
global,
|
||||
flagId,
|
||||
name: binding.name.text,
|
||||
module: binding.module,
|
||||
settled: binding.settled,
|
||||
};
|
||||
lowerer.cycleInitFlags.set(global.id, flag);
|
||||
const stmt = declaringStatement(binding.name);
|
||||
if (stmt !== null) {
|
||||
const list = lowerer.cycleInitDeclarations.get(stmt) ?? [];
|
||||
list.push(flag);
|
||||
lowerer.cycleInitDeclarations.set(stmt, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function declaringStatement(name: ts.Node): ts.Statement | null {
|
||||
for (let p: ts.Node | undefined = name.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isVariableStatement(p)) return p;
|
||||
if (ts.isSourceFile(p)) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFlag(flag: CycleInitFlag, loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "assign",
|
||||
localId: flag.flagId,
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The declaring statement's IR with each flagged binding's flag set right
|
||||
* after the binding's first store (or after the whole statement when the
|
||||
* declaration stores nothing, as `let x;` can). */
|
||||
export function withCycleInitFlags(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.Statement,
|
||||
lowered: IrStmt[],
|
||||
): IrStmt[] {
|
||||
const flags = lowerer.cycleInitDeclarations.get(stmt);
|
||||
if (flags === undefined) return lowered;
|
||||
const pending = new Map(flags.map((f) => [f.global.id, f] as const));
|
||||
const place = (list: IrStmt[]): IrStmt[] => {
|
||||
const out: IrStmt[] = [];
|
||||
for (const s of list) {
|
||||
if (pending.size === 0) {
|
||||
out.push(s);
|
||||
continue;
|
||||
}
|
||||
if (s.kind === "assign" && pending.has(s.localId)) {
|
||||
out.push({ ...s, initializes: true }, setFlag(pending.get(s.localId)!, s.loc));
|
||||
pending.delete(s.localId);
|
||||
continue;
|
||||
}
|
||||
if (s.kind === "block") {
|
||||
out.push({ ...s, body: place(s.body) });
|
||||
continue;
|
||||
}
|
||||
out.push(s);
|
||||
// A store nested in another construct: the flag follows the construct.
|
||||
for (const [id, flag] of [...pending]) {
|
||||
let stores = false;
|
||||
let nested = false;
|
||||
everyStmt(s, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr" && e.localId === id) nested = true;
|
||||
return true;
|
||||
},
|
||||
stmt: (inner) => {
|
||||
if (inner.kind === "assign" && inner.localId === id) {
|
||||
(inner as { initializes?: true }).initializes = true;
|
||||
stores = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (nested && !lowerer.remainder) {
|
||||
lowerer.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1016",
|
||||
locOf(stmt),
|
||||
`circular imports (the declaration of '${flag.name}' stores it inside an expression, where its initialization cannot be marked)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (stores || nested) {
|
||||
out.push(setFlag(flag, s.loc));
|
||||
pending.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const out = place(lowered);
|
||||
const loc = locOf(stmt);
|
||||
for (const flag of pending.values()) out.push(setFlag(flag, loc));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Inserts the initialization checks into every function that can access a
|
||||
* flagged binding before its declaration ran: every function except the
|
||||
* %init bodies of settled modules, and in the declaring module's own %init
|
||||
* only the statements up to the one that sets the flag (earlier reads
|
||||
* there come from inlined callback bodies; tsc rejects direct ones). Reads
|
||||
* check before reading; writes check after evaluating the assigned value,
|
||||
* as PutValue does. The declaration's own store is never checked. */
|
||||
export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void {
|
||||
if (lowerer.cycleInitFlags.size === 0) return;
|
||||
const fileOfInit = new Map<string, ts.SourceFile>();
|
||||
for (const [sf, name] of lowerer.initNameOf) fileOfInit.set(name, sf);
|
||||
for (const fn of mod.functions) rewriteFunction(lowerer, fn, fileOfInit.get(fn.name));
|
||||
}
|
||||
|
||||
function rewriteFunction(
|
||||
lowerer: Lowerer,
|
||||
fn: IrFunction,
|
||||
initOf: ts.SourceFile | undefined,
|
||||
): void {
|
||||
const active = new Map<string, CycleInitFlag>();
|
||||
for (const [id, flag] of lowerer.cycleInitFlags) {
|
||||
if (initOf === undefined || initOf === flag.module || !flag.settled.has(initOf)) {
|
||||
active.set(id, flag);
|
||||
}
|
||||
}
|
||||
if (active.size === 0) return;
|
||||
const produced = new WeakSet<object>();
|
||||
let temps = 0;
|
||||
const check = (flag: CycleInitFlag, loc: SrcLoc): IrStmt => {
|
||||
const expr: IrExpr = {
|
||||
kind: "intrinsic",
|
||||
name: "module.tdzCheck",
|
||||
args: [
|
||||
{ kind: "varRef", localId: flag.flagId, type: BOOL, loc },
|
||||
{ kind: "strLit", value: flag.name, type: STRING, loc },
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
return { kind: "exprStmt", expr, loc };
|
||||
};
|
||||
const temp = (type: IrType): string => {
|
||||
const id = `%cycleInit.${temps++}`;
|
||||
fn.locals.push({ id, name: "%cycleInit", type, mutable: false });
|
||||
return id;
|
||||
};
|
||||
const transform = {
|
||||
expr: (e: IrExpr): IrExpr => {
|
||||
if (produced.has(e)) return e;
|
||||
if (e.kind !== "varRef" && e.kind !== "incDec" && e.kind !== "assignExpr") return e;
|
||||
const flag = active.get(e.localId);
|
||||
if (flag === undefined) return e;
|
||||
if (e.kind === "assignExpr") {
|
||||
const t = temp(e.value.type);
|
||||
const value: IrExpr = { kind: "varRef", localId: t, type: e.value.type, loc: e.loc };
|
||||
const write: IrExpr = { ...e, value };
|
||||
produced.add(write);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: t, init: e.value, loc: e.loc }, check(flag, e.loc)],
|
||||
result: write,
|
||||
type: e.type,
|
||||
loc: e.loc,
|
||||
};
|
||||
}
|
||||
produced.add(e);
|
||||
return { kind: "seqExpr", stmts: [check(flag, e.loc)], result: e, type: e.type, loc: e.loc };
|
||||
},
|
||||
stmt: (s: IrStmt): IrStmt => {
|
||||
if (produced.has(s) || s.kind !== "assign" || s.initializes) return s;
|
||||
const flag = active.get(s.localId);
|
||||
if (flag === undefined) return s;
|
||||
const t = temp(s.value.type);
|
||||
const write: IrStmt = {
|
||||
kind: "assign",
|
||||
localId: s.localId,
|
||||
value: { kind: "varRef", localId: t, type: s.value.type, loc: s.loc },
|
||||
loc: s.loc,
|
||||
};
|
||||
produced.add(write);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: t, init: s.value, loc: s.loc },
|
||||
check(flag, s.loc),
|
||||
write,
|
||||
],
|
||||
loc: s.loc,
|
||||
};
|
||||
},
|
||||
};
|
||||
if (initOf === undefined) {
|
||||
fn.body = transformStmtList(fn.body, transform);
|
||||
return;
|
||||
}
|
||||
// A module %init: its own bindings stop needing checks once the
|
||||
// top-level statement setting their flag completed.
|
||||
fn.body = fn.body.map((s) => {
|
||||
if (active.size === 0) return s;
|
||||
const out = transformStmtList([s], transform)[0]!;
|
||||
for (const [id, flag] of [...active]) {
|
||||
if (flag.module !== initOf) continue;
|
||||
const sets = !everyStmt(out, {
|
||||
expr: () => true,
|
||||
stmt: (inner) => !(inner.kind === "assign" && inner.localId === flag.flagId),
|
||||
});
|
||||
if (sets) active.delete(id);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/** Class layouts that read module storage directly (an evaluated heritage
|
||||
* value, a symbol-keyed field) bypass the checks: refuse when that storage
|
||||
* is a flagged binding. */
|
||||
export function refuseUncheckedCycleReads(
|
||||
lowerer: Lowerer,
|
||||
classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[],
|
||||
): void {
|
||||
if (lowerer.cycleInitFlags.size === 0 || lowerer.remainder) return;
|
||||
for (const cls of classes) {
|
||||
const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)];
|
||||
for (const id of ids) {
|
||||
const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id);
|
||||
if (flag === undefined) continue;
|
||||
const loc = flag.global.source?.loc ?? { file: lowerer.entry.fileName, start: 0, end: 0 };
|
||||
lowerer.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1016",
|
||||
loc,
|
||||
`circular imports (the class layout reads '${flag.name}' directly, before an initialization check can run)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
type IrExpr,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
typeEquals,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { typeKey, genResultRecord } from "../type-mapper.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { extractIteratorValue } from "./lower-generators.js";
|
||||
import { lowerSafeIndexRead } from "./array-values.js";
|
||||
|
||||
/** The conversion into an iterator-typed slot (a generator type): a
|
||||
* checked-dynamic iterator or a generator with other channel types steps
|
||||
* through an interned adapter generator. Null when neither applies. */
|
||||
export function iteratorSlotAdapter(
|
||||
lowerer: Lowerer,
|
||||
expr: IrExpr,
|
||||
expected: IrType & { kind: "generator" },
|
||||
): IrExpr | null {
|
||||
if (expr.type.kind === "dyn") return dynIteratorAdapter(lowerer, expr, expected);
|
||||
if (expr.type.kind === "generator") return generatorRetypeAdapter(lowerer, expr, expected);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `resumed.0 = value` — the adapters' early-exit marker around a yield. */
|
||||
function setResumed(value: boolean, loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "assign",
|
||||
localId: "resumed.0",
|
||||
value: { kind: "boolLit", value, type: BOOL, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A yield expression's type: a valueless next channel resumes with void. */
|
||||
function resumeType(nextT: IrType): IrType {
|
||||
return nextT.kind === "undefinedT" ? VOID : nextT;
|
||||
}
|
||||
|
||||
/** Convert a stepped value into a channel type. Records and arrays box into
|
||||
* checked values by reference, so consumers observe the same objects. */
|
||||
function intoChannel(lowerer: Lowerer, value: IrExpr, target: IrType): IrExpr | null {
|
||||
const coerced = lowerer.coerceToExpected(value, target);
|
||||
if (!typeEquals(coerced.type, target)) return null;
|
||||
if (
|
||||
coerced.kind === "dynFrom" &&
|
||||
streamTypedRefEligible(coerced.value.type) &&
|
||||
coerced.value.type.kind !== "bytes"
|
||||
)
|
||||
coerced.liveRef = true;
|
||||
return coerced;
|
||||
}
|
||||
|
||||
/** A checked-dynamic iterator (a Map, Set or array iterator, or any other
|
||||
* object following the iterator protocol) flowing into a generator-typed
|
||||
* slot (`IterableIterator<T>`, `Iterator<T>`, `Generator<T>`). The interned
|
||||
* adapter is a generator over the source: each `next()` steps the source
|
||||
* once and forwards its value, so iteration stays lazy and observes the
|
||||
* collection live, exactly like the source iterator. Values convert into
|
||||
* the slot's channel types with the usual checked extraction. Null when
|
||||
* the slot is not a synchronous generator with a yielding channel. */
|
||||
function dynIteratorAdapter(
|
||||
lowerer: Lowerer,
|
||||
expr: IrExpr,
|
||||
expected: IrType & { kind: "generator" },
|
||||
): IrExpr | null {
|
||||
if (expected.async || expected.yieldT.kind === "void") return null;
|
||||
const resultType = genResultRecord(
|
||||
expected.yieldT,
|
||||
expected.retT,
|
||||
lowerer.shapes,
|
||||
lowerer.unions,
|
||||
);
|
||||
if (!resultType) return null;
|
||||
const loc = expr.loc;
|
||||
const key = `iterAdapt:${typeKey(expected)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%iter.adapt.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const locals: IrLocal[] = [];
|
||||
const local = (id: string, type: IrType, mutable = false): IrExpr => {
|
||||
locals.push({ id, name: id.slice(0, id.indexOf(".")), type, mutable });
|
||||
return varRef(id, type, loc);
|
||||
};
|
||||
const source = local("source.0", DYN);
|
||||
const iterator = local("iterator.0", DYN);
|
||||
const next = local("next.0", DYN);
|
||||
const step = local("step.0", DYN, true);
|
||||
const forwards = expected.nextT.kind === "dyn";
|
||||
const sent = forwards ? local("sent.0", DYN, true) : null;
|
||||
const get = (value: IrExpr, field: string): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value,
|
||||
key: { kind: "strLit", value: field, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const advance = (args: IrExpr[]): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorResult",
|
||||
args: [
|
||||
{
|
||||
kind: "dynCall",
|
||||
callee: next,
|
||||
receiver: iterator,
|
||||
calleeName: "iterator.next",
|
||||
args,
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const returned =
|
||||
expected.retT.kind === "void"
|
||||
? null
|
||||
: intoChannel(lowerer, get(step, "value"), expected.retT);
|
||||
const yielded = (): IrExpr | null => intoChannel(lowerer, get(step, "value"), expected.yieldT);
|
||||
if ((expected.retT.kind !== "void" && !returned) || !yielded()) {
|
||||
lowerer.valueHelpers.delete(key);
|
||||
return null;
|
||||
}
|
||||
const finish: IrStmt = { kind: "return", value: returned, loc };
|
||||
// One suspension: yield the current value and take the resume value.
|
||||
const suspend = (): IrStmt => {
|
||||
const resumed: IrExpr = {
|
||||
kind: "yieldExpr",
|
||||
value: yielded()!,
|
||||
type: resumeType(expected.nextT),
|
||||
loc,
|
||||
};
|
||||
return sent
|
||||
? { kind: "assign", localId: "sent.0", value: resumed, loc }
|
||||
: { kind: "exprStmt", expr: resumed, loc };
|
||||
};
|
||||
const resumedFlag = local("resumed.0", BOOL, true);
|
||||
const closer = local("closer.0", DYN);
|
||||
const stepOnce = (): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: "step.0",
|
||||
value: advance(sent ? [sent] : []),
|
||||
loc,
|
||||
});
|
||||
// An early exit from a consumer's loop closes the adapter. JS calls the
|
||||
// source's own return() then; a source without one (the native Map, Set
|
||||
// and array iterators) stays open, so the adapter suspends again on the
|
||||
// same value instead of completing, and later steps resume the source.
|
||||
const closeOrPark: IrStmt = {
|
||||
kind: "if",
|
||||
cond: { kind: "unary", op: "!", operand: resumedFlag, type: BOOL, loc },
|
||||
then: [
|
||||
{ kind: "varDecl", localId: "closer.0", init: get(iterator, "return"), loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "nullish", value: closer, type: BOOL, loc },
|
||||
then: [suspend(), stepOnce(), { kind: "continue", loc }],
|
||||
else_: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorResult",
|
||||
args: [
|
||||
{
|
||||
kind: "dynCall",
|
||||
callee: closer,
|
||||
receiver: iterator,
|
||||
calleeName: "iterator.return",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "iterator.0",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorResult",
|
||||
args: [
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "dyn.iterator",
|
||||
args: [source, { kind: "strLit", value: "value is not iterable", type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: "next.0", init: get(iterator, "next"), loc },
|
||||
{ kind: "varDecl", localId: "step.0", init: advance([]), loc },
|
||||
...(sent
|
||||
? [{ kind: "varDecl" as const, localId: "sent.0", init: dynUndefinedExpr(loc), loc }]
|
||||
: []),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "resumed.0",
|
||||
init: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "truthy", value: get(step, "done"), type: BOOL, loc },
|
||||
then: [finish],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
setResumed(false, loc),
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [suspend(), setResumed(true, loc)],
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
finallyBody: [closeOrPark],
|
||||
loc,
|
||||
},
|
||||
stepOnce(),
|
||||
],
|
||||
loc,
|
||||
},
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "source.0", name: "source", type: DYN }],
|
||||
returnType: expected.retT,
|
||||
locals,
|
||||
body,
|
||||
generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [expr], type: expected, loc };
|
||||
}
|
||||
|
||||
/** A generator flowing into a slot with other channel types (a
|
||||
* `Generator<number, void, undefined>` returned through an
|
||||
* `IterableIterator<number>` annotation). The interned adapter generator
|
||||
* resumes the source once per step and converts each value between the
|
||||
* channel types, so iteration stays lazy. Null when a channel has no
|
||||
* conversion. */
|
||||
function generatorRetypeAdapter(
|
||||
lowerer: Lowerer,
|
||||
expr: IrExpr,
|
||||
expected: IrType & { kind: "generator" },
|
||||
): IrExpr | null {
|
||||
const from = expr.type;
|
||||
if (from.kind !== "generator" || from.async || expected.async) return null;
|
||||
if (from.yieldT.kind === "void" || expected.yieldT.kind === "void") return null;
|
||||
const fromResult = genResultRecord(from.yieldT, from.retT, lowerer.shapes, lowerer.unions);
|
||||
const toResult = genResultRecord(expected.yieldT, expected.retT, lowerer.shapes, lowerer.unions);
|
||||
const fromShape = fromResult ? lowerer.shapes.get(fromResult.shapeId) : undefined;
|
||||
const valueT = fromShape?.fields.find((field) => field.name === "value")?.type;
|
||||
if (!fromResult || !toResult || !valueT) return null;
|
||||
const loc = expr.loc;
|
||||
const key = `genRetype:${typeKey(from)}:${typeKey(expected)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
const source = varRef("source.0", from, loc);
|
||||
const step = varRef("step.0", fromResult, loc);
|
||||
const field = (f: "value" | "done", type: IrType): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: step,
|
||||
shapeId: fromResult.shapeId,
|
||||
field: f,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
const extracted = extractIteratorValue(
|
||||
lowerer,
|
||||
from.yieldT,
|
||||
valueT,
|
||||
field("value", valueT),
|
||||
loc,
|
||||
);
|
||||
const yielded = extracted ? intoChannel(lowerer, extracted, expected.yieldT) : null;
|
||||
if (!yielded) return null;
|
||||
// The completion value: the source's return value, or undefined.
|
||||
let returned: IrExpr | null = null;
|
||||
if (expected.retT.kind !== "void") {
|
||||
returned =
|
||||
from.retT.kind === "void"
|
||||
? intoChannel(
|
||||
lowerer,
|
||||
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
expected.retT,
|
||||
)
|
||||
: intoChannel(lowerer, field("value", valueT), expected.retT);
|
||||
if (!returned) return null;
|
||||
}
|
||||
// The resume value: forwarded when both sides carry one.
|
||||
const forwards = expected.nextT.kind !== "undefinedT" && from.nextT.kind !== "undefinedT";
|
||||
const sentRef = varRef("sent.0", expected.nextT, loc);
|
||||
const sent = forwards ? intoChannel(lowerer, sentRef, from.nextT) : null;
|
||||
if (forwards && !sent) return null;
|
||||
if (!forwards && from.nextT.kind !== "undefinedT" && from.nextT.kind !== "dyn") return null;
|
||||
const resume = (arg: IrExpr | null): IrExpr => ({
|
||||
kind: "genResume",
|
||||
mode: "next",
|
||||
gen: source,
|
||||
arg,
|
||||
type: fromResult,
|
||||
loc,
|
||||
});
|
||||
const resumed: IrExpr = {
|
||||
kind: "yieldExpr",
|
||||
value: yielded,
|
||||
type: resumeType(expected.nextT),
|
||||
loc,
|
||||
};
|
||||
name = `%gen.retype.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "source.0", name: "source", type: from }],
|
||||
returnType: expected.retT,
|
||||
locals: [
|
||||
{ id: "source.0", name: "source", type: from, mutable: false },
|
||||
{ id: "step.0", name: "step", type: fromResult, mutable: true },
|
||||
...(forwards ? [{ id: "sent.0", name: "sent", type: expected.nextT, mutable: true }] : []),
|
||||
{ id: "resumed.0", name: "resumed", type: BOOL, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{ kind: "varDecl", localId: "step.0", init: resume(null), loc },
|
||||
...(forwards ? [{ kind: "varDecl" as const, localId: "sent.0", init: null, loc }] : []),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "resumed.0",
|
||||
init: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: field("done", BOOL),
|
||||
then: [{ kind: "return", value: returned, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
setResumed(false, loc),
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [
|
||||
forwards
|
||||
? { kind: "assign", localId: "sent.0", value: resumed, loc }
|
||||
: { kind: "exprStmt", expr: resumed, loc },
|
||||
setResumed(true, loc),
|
||||
],
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// Closing the adapter early closes the source generator too.
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: varRef("resumed.0", BOOL, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "genResume",
|
||||
mode: "return",
|
||||
gen: source,
|
||||
arg: null,
|
||||
type: fromResult,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "assign", localId: "step.0", value: resume(sent), loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType: toResult },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [expr], type: expected, loc };
|
||||
}
|
||||
|
||||
/** Drain a synchronous generator into a fresh `elem[]` — the eager
|
||||
* consumers of an iterator (`[...it]`, `Array.from(it)`). Each step resumes
|
||||
* the generator once and appends its value; completion stops the drain
|
||||
* without appending the return value, as in JS. Null when the yielded
|
||||
* values cannot be extracted into the element type. */
|
||||
export function generatorDrain(
|
||||
lowerer: Lowerer,
|
||||
source: IrExpr,
|
||||
elem: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const gen = source.type;
|
||||
if (gen.kind !== "generator" || gen.async || gen.yieldT.kind === "void") return null;
|
||||
const resultType = genResultRecord(gen.yieldT, gen.retT, lowerer.shapes, lowerer.unions);
|
||||
const shape = resultType ? lowerer.shapes.get(resultType.shapeId) : undefined;
|
||||
const valueT = shape?.fields.find((field) => field.name === "value")?.type;
|
||||
if (!resultType || !valueT) return null;
|
||||
const arrT: IrType = { kind: "array", elem };
|
||||
const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
const genRef = varRef("gen.0", gen, loc);
|
||||
const outRef = varRef("out.0", arrT, loc);
|
||||
const stepRef = varRef("step.0", resultType, loc);
|
||||
const raw = extractIteratorValue(
|
||||
lowerer,
|
||||
gen.yieldT,
|
||||
valueT,
|
||||
{
|
||||
kind: "recordGet",
|
||||
obj: stepRef,
|
||||
shapeId: resultType.shapeId,
|
||||
field: "value",
|
||||
type: valueT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
);
|
||||
if (!raw) return null;
|
||||
const value = lowerer.coerceToExpected(raw, elem);
|
||||
if (!typeEquals(value.type, elem)) return null;
|
||||
name = `%gen.drain.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "gen.0", name: "gen", type: gen }],
|
||||
returnType: arrT,
|
||||
locals: [
|
||||
{ id: "gen.0", name: "gen", type: gen, mutable: false },
|
||||
{ id: "out.0", name: "out", type: arrT, mutable: false },
|
||||
{ id: "step.0", name: "step", type: resultType, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: arrT, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "step.0",
|
||||
init: {
|
||||
kind: "genResume",
|
||||
mode: "next",
|
||||
gen: genRef,
|
||||
arg: null,
|
||||
type: resultType,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "recordGet",
|
||||
obj: stepRef,
|
||||
shapeId: resultType.shapeId,
|
||||
field: "done",
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [{ kind: "break", loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: outRef,
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: outRef, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [source], type: arrT, loc };
|
||||
}
|
||||
|
||||
/** `list.values()`, `list.keys()` and `list.entries()` as values: a
|
||||
* generator over the live array. Each step compares the index with the
|
||||
* current length and reads the current element, so elements appended
|
||||
* during iteration are visited and completion is final, as with the
|
||||
* native array iterator. `type` is the call's generator type. */
|
||||
export function lowerArrayIteratorValue(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
method: "values" | "keys" | "entries",
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const arrT = receiver.type;
|
||||
if (arrT.kind !== "array" || type.kind !== "generator") return null;
|
||||
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
|
||||
if (!resultType || type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null;
|
||||
const tuple = type.yieldT.kind === "record" ? lowerer.shapes.get(type.yieldT.shapeId) : undefined;
|
||||
if (method === "entries" && (!tuple?.tuple || tuple.fields.length !== 2)) return null;
|
||||
const key = `arrIter:${method}:${typeKey(arrT)}:${typeKey(type)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
const list = varRef("list.0", arrT, loc);
|
||||
const index = varRef("index.0", F64, loc);
|
||||
const element = (): IrExpr | null => {
|
||||
const read = lowerSafeIndexRead(lowerer, list, index, loc);
|
||||
if (!read) return null;
|
||||
const target =
|
||||
method === "values" ? type.yieldT : tuple!.fields.find((f) => f.name === "1")!.type;
|
||||
const value = lowerer.coerceToExpected(read, target);
|
||||
return typeEquals(value.type, target) ? value : null;
|
||||
};
|
||||
let yielded: IrExpr | null;
|
||||
if (method === "keys") yielded = index;
|
||||
else if (method === "values") yielded = element();
|
||||
else {
|
||||
const value = element();
|
||||
yielded =
|
||||
value === null
|
||||
? null
|
||||
: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "0", value: index },
|
||||
{ name: "1", value },
|
||||
],
|
||||
type: type.yieldT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null;
|
||||
name = `%arr.iter.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "list.0", name: "list", type: arrT }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "list.0", name: "list", type: arrT, mutable: false },
|
||||
{ id: "index.0", name: "index", type: F64, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "index.0",
|
||||
init: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index,
|
||||
right: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: list,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc },
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "index.0",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: index,
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type, loc };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { generatorDrain } from "./iterator-adapters.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
|
||||
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
|
||||
import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
@@ -359,7 +360,10 @@ export function lowerExpr(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
}
|
||||
lowerExprDepth++;
|
||||
try {
|
||||
return lowerExprInner(lowerer, expr);
|
||||
const lowered = lowerExprInner(lowerer, expr);
|
||||
if (lowered.type.kind === "void" && (lowerer.typeOf(expr).flags & ts.TypeFlags.Never) !== 0)
|
||||
lowerer.neverValued.add(lowered);
|
||||
return lowered;
|
||||
} finally {
|
||||
lowerExprDepth--;
|
||||
}
|
||||
@@ -4344,6 +4348,37 @@ export function lowerNullishCoalesce(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A plain value read that may be runtime-optional: an unchecked element
|
||||
* read, or a binding, member or call that carries one. Lowering these
|
||||
* without a destination type is the same as lowering them into one. */
|
||||
function isPlainValueRead(node: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return (
|
||||
ts.isElementAccessExpression(node) ||
|
||||
ts.isIdentifier(node) ||
|
||||
ts.isPropertyAccessExpression(node) ||
|
||||
ts.isCallExpression(node)
|
||||
);
|
||||
}
|
||||
|
||||
/** A member access or call receiver needs the present value itself (JS
|
||||
* throws a TypeError reading a member of undefined). */
|
||||
function isMemberReceiver(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return (
|
||||
((ts.isPropertyAccessExpression(parent) ||
|
||||
ts.isElementAccessExpression(parent) ||
|
||||
ts.isCallExpression(parent)) &&
|
||||
parent.expression === child) ||
|
||||
(ts.isTaggedTemplateExpression(parent) && parent.tag === child)
|
||||
);
|
||||
}
|
||||
|
||||
function lowerNullishPair(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
@@ -4414,6 +4449,29 @@ function lowerNullishPair(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A runtime-optional default (`name ?? names[0]`): the checker types an
|
||||
// unchecked element read as the bare element type, but an out-of-range
|
||||
// read is undefined at runtime. When the default shares the left's
|
||||
// optional union, that union is the result, so the undefined state
|
||||
// reaches the consumer (an optional return or parameter takes it as its
|
||||
// undefined arm; a plain slot still extracts the element with its
|
||||
// checked narrow).
|
||||
if (
|
||||
rest.length === 1 &&
|
||||
typeEquals(type, rest[0]!) &&
|
||||
isPlainValueRead(expr.right) &&
|
||||
!isMemberReceiver(expr)
|
||||
) {
|
||||
const raw = lowerer.lowerExpr(expr.right);
|
||||
if (typeEquals(raw.type, left.type))
|
||||
return { kind: "nullish", left, right: raw, type: left.type, loc };
|
||||
// `names[0] ?? undefined`: an explicit unit default stays the left's arm.
|
||||
if (isUnitType(raw.type) && lowerer.armTag(left.type.unionId, raw.type) >= 0) {
|
||||
const right = lowerer.coerceInto(expr.right, raw, left.type);
|
||||
return { kind: "nullish", left, right, type: left.type, loc };
|
||||
}
|
||||
return { kind: "nullish", left, right: lowerer.coerceInto(expr.right, raw, type), type, loc };
|
||||
}
|
||||
if (typeEquals(type, left.type) || (rest.length === 1 && typeEquals(type, rest[0]!))) {
|
||||
const right = lowerer.lowerExprExpecting(expr.right, type);
|
||||
return { kind: "nullish", left, right, type, loc };
|
||||
@@ -5927,6 +5985,12 @@ export function lowerArrayLiteral(
|
||||
loc: locOf(el),
|
||||
};
|
||||
}
|
||||
// `[...it]` over a generator (or an iterator adapted into one):
|
||||
// drain it once, in order, into a fresh element array.
|
||||
if (src.type.kind === "generator") {
|
||||
const drained = generatorDrain(lowerer, src, type.elem, locOf(el));
|
||||
if (drained) src = drained;
|
||||
}
|
||||
// A native checked iterable can supply scalar elements even when
|
||||
// the checker inferred a typed array from its producer. Drain once
|
||||
// and validate the elements before the ordinary spread copy.
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
resolveNpmImport,
|
||||
} from "../program.js";
|
||||
import type { CycleEdge } from "../program.js";
|
||||
import { markCycleEarlyBindings, withCycleInitFlags } from "./cycle-initialization.js";
|
||||
import {
|
||||
invalidJsonModuleDiag,
|
||||
npmEmbedFailedDiag,
|
||||
@@ -507,6 +508,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
}
|
||||
lowerer.collecting = wasCollecting;
|
||||
}
|
||||
markCycleEarlyBindings(lowerer);
|
||||
}
|
||||
|
||||
/** The npm-import chokepoint, run once per pass during collection (so
|
||||
@@ -3088,8 +3090,16 @@ export function lowerFileInit(
|
||||
body.push(...lowerer.lowerStaticFieldInits(statics[at]!.info));
|
||||
at++;
|
||||
}
|
||||
const deferred = lowerer.deferredModuleInitializers?.defer(lowerer, stmt);
|
||||
const stmtIr = deferred ? [deferred] : lowerer.lowerStmts([stmt]);
|
||||
// A cycle binding's declaration stays at its position: its
|
||||
// initialization flag must be set where the declaration runs.
|
||||
const deferred = lowerer.cycleInitDeclarations.has(stmt)
|
||||
? null
|
||||
: lowerer.deferredModuleInitializers?.defer(lowerer, stmt);
|
||||
const stmtIr = withCycleInitFlags(
|
||||
lowerer,
|
||||
stmt,
|
||||
deferred ? [deferred] : lowerer.lowerStmts([stmt]),
|
||||
);
|
||||
// Class EXPRESSIONS inside this statement queued their static
|
||||
// inits while it lowered: they land immediately before it — JS's
|
||||
// order for the supported whole-initializer positions.
|
||||
|
||||
@@ -4242,11 +4242,15 @@ export function bindPatternTarget(
|
||||
// through later locals, templates, operators, and narrowing instead
|
||||
// of being extracted with a checked arm read.
|
||||
let runtimeOptional = false;
|
||||
// The optional-read pre-pass may already have widened the binding to
|
||||
// that union; its reads still need the runtime-optional treatment.
|
||||
if (
|
||||
!dynStays &&
|
||||
value.type.kind === "union" &&
|
||||
lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(value.type), type)
|
||||
(typeEquals(lowerer.stripUndefinedArm(value.type), type) ||
|
||||
(typeEquals(value.type, type) &&
|
||||
typeEquals(lowerer.stripUndefinedArm(value.type), lowerer.irTypeOf(name))))
|
||||
) {
|
||||
type = value.type;
|
||||
runtimeOptional = true;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
checkCycleBindingAccesses,
|
||||
refuseUncheckedCycleReads,
|
||||
type CycleInitFlag,
|
||||
} from "./cycle-initialization.js";
|
||||
import { identityPreservingWidening } from "./coercions/identity.js";
|
||||
import { checkedClassAssertion } from "./class-assertions.js";
|
||||
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
|
||||
@@ -5,6 +10,7 @@ import { isOptionalProcessStreamProperty } from "./builtins/process.js";
|
||||
import { concreteCollectionNarrow } from "./collection-narrowing.js";
|
||||
import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { iteratorSlotAdapter } from "./iterator-adapters.js";
|
||||
import type { FieldLift } from "./coercions/structural-plans.js";
|
||||
import { lowerBuiltinCall } from "./builtin-calls.js";
|
||||
import { CoercionState } from "./coercions/state.js";
|
||||
@@ -60,6 +66,7 @@ import {
|
||||
utilTypesModuleValue,
|
||||
} from "./lower-builtin-values.js";
|
||||
import { everyExprChild, everyStmtChild, transformStmtList } from "../../ir/traverse.js";
|
||||
import { RuntimeOptionalProvenance } from "./runtime-optional-provenance.js";
|
||||
import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
|
||||
import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js";
|
||||
import { UnregisteredClassTypes } from "./unregistered-class-types.js";
|
||||
@@ -2220,6 +2227,10 @@ export class Lowerer {
|
||||
* node carrying the ?. token is marked handled so the receiver-typed
|
||||
* lowerings stop declining it (chainBlocked). */
|
||||
readonly chainRecvByNode = new Map<ts.Node, IrExpr>();
|
||||
/** Lowered void calls whose checked type is `never` (they throw or exit).
|
||||
* TypeScript lets such a call stand where any value is expected;
|
||||
* coerceToExpected turns them into diverging sequences. */
|
||||
readonly neverValued = new Set<IrExpr>();
|
||||
readonly chainNarrowedType = new Map<ts.Node, ts.Type>();
|
||||
readonly chainHandled = new Set<ts.Node>();
|
||||
/** for-of-over-matchAll bindings whose `.index` reads the companion-index
|
||||
@@ -2463,6 +2474,10 @@ export class Lowerer {
|
||||
* function called above the declaration statement reads that, never a
|
||||
* NULL slot. Filled by collectGlobals. */
|
||||
readonly varGlobalEntryInits = new Map<ts.SourceFile, IrGlobal[]>();
|
||||
/** Import-cycle bindings with an initialization flag, by global id, and
|
||||
* the top-level statements declaring them (cycle-initialization.ts). */
|
||||
readonly cycleInitFlags = new Map<string, CycleInitFlag>();
|
||||
readonly cycleInitDeclarations = new Map<ts.Statement, CycleInitFlag[]>();
|
||||
|
||||
get ctx(): FnCtx {
|
||||
const top = this.fnStack[this.fnStack.length - 1];
|
||||
@@ -3641,6 +3656,9 @@ export class Lowerer {
|
||||
* but it is not a runtime proof; promoting only the affected slots keeps
|
||||
* the rest of the dense ABI unchanged. */
|
||||
analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void {
|
||||
const provenance = RuntimeOptionalProvenance.fromEnvironment();
|
||||
// The site whose value the current promotion follows (debug only).
|
||||
let provenanceSite: ts.Node | undefined;
|
||||
const optionalSymbols = new Set<ts.Symbol>();
|
||||
const optionalReturns = new Set<ts.Symbol>();
|
||||
const arithmeticReturns = new Map<ts.Symbol, IrType>();
|
||||
@@ -3943,6 +3961,7 @@ export class Lowerer {
|
||||
const symbol = symbolOf(el.name);
|
||||
if (symbol && !el.initializer && !optionalSymbols.has(symbol)) {
|
||||
optionalSymbols.add(symbol);
|
||||
provenance?.note("pattern-binding", el.name, init);
|
||||
// Indexed flow facts can survive a mutating method call in the
|
||||
// checker. The array's element ABI describes every value a
|
||||
// position may hold after that call, so bind from that type.
|
||||
@@ -4089,6 +4108,7 @@ export class Lowerer {
|
||||
const widened = this.runtimeOptionalType(current);
|
||||
const previous = this.runtimeOptionalBindingTypes.get(symbol);
|
||||
this.runtimeOptionalBindingTypes.set(symbol, widened);
|
||||
provenance?.note("callback-parameter", parameter.name, provenanceSite);
|
||||
if (!previous || !typeEquals(previous, widened)) changed = true;
|
||||
if (!optionalSymbols.has(symbol)) {
|
||||
optionalSymbols.add(symbol);
|
||||
@@ -4099,6 +4119,7 @@ export class Lowerer {
|
||||
const fnType = this.mapTypeOf(this.typeOf(fn));
|
||||
if (fnType?.kind === "func" && bodyReturnsOptional(fn)) {
|
||||
const widenedReturn = this.runtimeOptionalType(fnType.ret);
|
||||
provenance?.note("callback-return", fn, provenanceSite);
|
||||
const previousReturn = this.runtimeOptionalFunctionReturnType(fn, fnType.ret);
|
||||
this.runtimeOptionalFunctionReturns.set(fn, widenedReturn);
|
||||
if (!typeEquals(previousReturn, widenedReturn)) changed = true;
|
||||
@@ -4248,6 +4269,7 @@ export class Lowerer {
|
||||
const symbol = symbolOf(node.name);
|
||||
if (symbol && !optionalSymbols.has(symbol)) {
|
||||
optionalSymbols.add(symbol);
|
||||
provenance?.note("binding", node.name, node.initializer);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -4270,6 +4292,7 @@ export class Lowerer {
|
||||
if (symbol && valueSymbol && sourceOptional) {
|
||||
const aliasNew = !optionalReturns.has(symbol);
|
||||
if (aliasNew) optionalReturns.add(symbol);
|
||||
if (aliasNew) provenance?.note("function-alias", node.name, node.initializer);
|
||||
if (aliasNew) changed = true;
|
||||
const valueType = this.mapTypeOf(this.typeOf(node.name));
|
||||
if (valueType?.kind === "func" && sourceSig) {
|
||||
@@ -4306,7 +4329,10 @@ export class Lowerer {
|
||||
const value = ts.isPropertyAssignment(prop)
|
||||
? prop.initializer
|
||||
: (prop.name as ts.Expression);
|
||||
if (mayBeOptional(value) && noteField(node, name.text)) changed = true;
|
||||
if (mayBeOptional(value) && noteField(node, name.text)) {
|
||||
provenance?.note("literal-field", name, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4323,16 +4349,21 @@ export class Lowerer {
|
||||
const symbol = symbolOf(node.left);
|
||||
if (symbol && !optionalSymbols.has(symbol)) {
|
||||
optionalSymbols.add(symbol);
|
||||
provenance?.note("assigned-binding", node.left, node.right);
|
||||
changed = true;
|
||||
}
|
||||
} else if (ts.isPropertyAccessExpression(node.left)) {
|
||||
const field = symbolOf(node.left.name);
|
||||
if (field && staticFieldsBySymbol.has(field) && !optionalSymbols.has(field)) {
|
||||
optionalSymbols.add(field);
|
||||
provenance?.note("static-field", node.left, node.right);
|
||||
changed = true;
|
||||
} else if (ts.isIdentifier(node.left.expression)) {
|
||||
const symbol = symbolOf(node.left.expression);
|
||||
if (symbol && noteFieldSymbol(symbol, node.left.name.text)) changed = true;
|
||||
if (symbol && noteFieldSymbol(symbol, node.left.name.text)) {
|
||||
provenance?.note("assigned-field", node.left, node.right);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4384,6 +4415,7 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
if (nativeArrayReceiver) {
|
||||
provenanceSite = node;
|
||||
if (promoteHofCallback(callback, callbackIndices)) changed = true;
|
||||
const method = node.expression.name.text;
|
||||
if (
|
||||
@@ -4435,6 +4467,7 @@ export class Lowerer {
|
||||
if (typeEquals(shape.type, widened)) continue;
|
||||
shape.type = widened;
|
||||
const parameter = functionDeclBySymbol.get(target)?.parameters[i];
|
||||
provenance?.note("parameter", parameter, node, "(omitted or undefined argument)");
|
||||
if (parameter) {
|
||||
for (const bound of boundIdentifiersOf(parameter.name)) {
|
||||
const boundSymbol = symbolOf(bound);
|
||||
@@ -4464,6 +4497,7 @@ export class Lowerer {
|
||||
const optionalCallbackParams = callbackSlot.params.flatMap((type, index) =>
|
||||
type.kind === "union" && this.armTag(type.unionId, UNDEFINED_T) >= 0 ? [index] : [],
|
||||
);
|
||||
provenanceSite = node;
|
||||
if (
|
||||
optionalCallbackParams.length > 0 &&
|
||||
promoteHofCallback(arg, optionalCallbackParams)
|
||||
@@ -4483,6 +4517,7 @@ export class Lowerer {
|
||||
if (typeEquals(rest.type, widened)) continue;
|
||||
rest.type = widened;
|
||||
const parameter = functionDeclBySymbol.get(target)?.parameters[i];
|
||||
provenance?.note("rest-parameter", parameter, arg);
|
||||
if (parameter && ts.isIdentifier(parameter.name)) {
|
||||
const bound = symbolOf(parameter.name);
|
||||
if (bound) this.runtimeOptionalBindingTypes.set(bound, widened);
|
||||
@@ -4508,6 +4543,12 @@ export class Lowerer {
|
||||
if (set.size !== before) {
|
||||
changed = true;
|
||||
const parameter = functionDeclBySymbol.get(target)?.parameters[i];
|
||||
provenance?.note(
|
||||
"parameter",
|
||||
parameter,
|
||||
arg,
|
||||
target === symbol ? "" : "(override)",
|
||||
);
|
||||
if (parameter) {
|
||||
for (const bound of boundIdentifiersOf(parameter.name)) {
|
||||
const boundSymbol = symbolOf(bound);
|
||||
@@ -4530,6 +4571,7 @@ export class Lowerer {
|
||||
for (const expression of returnsOf(decl)) {
|
||||
if (mayBeOptional(expression) && !optionalReturns.has(symbol)) {
|
||||
optionalReturns.add(symbol);
|
||||
provenance?.note("return", decl.name ?? decl, expression);
|
||||
changed = true;
|
||||
}
|
||||
const arithmetic = optionalPrimitiveResultType(expression);
|
||||
@@ -4546,6 +4588,7 @@ export class Lowerer {
|
||||
for (const [symbol, field] of staticFieldsBySymbol) {
|
||||
if (!optionalSymbols.has(symbol) && mayBeOptional(field.initializer)) {
|
||||
optionalSymbols.add(symbol);
|
||||
provenance?.note("static-field", field.initializer.parent, field.initializer);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -4574,6 +4617,12 @@ export class Lowerer {
|
||||
for (const symbol of family) {
|
||||
if (!optionalReturns.has(symbol)) {
|
||||
optionalReturns.add(symbol);
|
||||
provenance?.note(
|
||||
"return",
|
||||
functionDeclBySymbol.get(symbol),
|
||||
undefined,
|
||||
"(override family)",
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -4717,6 +4766,8 @@ export class Lowerer {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
provenance?.flush();
|
||||
}
|
||||
|
||||
run(): LowerResult {
|
||||
@@ -4952,6 +5003,7 @@ export class Lowerer {
|
||||
// the emitted program references, flushing deferred class diagnostics
|
||||
// that a reached type makes relevant.
|
||||
const artifacts = this.moduleArtifacts(functions);
|
||||
refuseUncheckedCycleReads(this, artifacts.classes ?? []);
|
||||
// Types still naming a class that never REGISTERED after retention's
|
||||
// flush (JS graphs whose class fences deferred to runtime — the
|
||||
// sentence-walker's path params, printer tables whose func-typed
|
||||
@@ -4980,6 +5032,7 @@ export class Lowerer {
|
||||
...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}),
|
||||
};
|
||||
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
|
||||
if (module) checkCycleBindingAccesses(this, module);
|
||||
return {
|
||||
module,
|
||||
diagnostics: this.diags,
|
||||
@@ -6432,6 +6485,15 @@ export class Lowerer {
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
// Iterator-typed slots hold generators and built-in collection
|
||||
// iterators; any other iterator object (a user class) stays fenced.
|
||||
if (expected.kind === "generator" && actual.kind !== "generator") {
|
||||
this.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`'${this.fmt(actual)}' values in an iterator-typed slot (Iterator and IterableIterator slots hold generators and built-in collection iterators)`,
|
||||
);
|
||||
}
|
||||
// Everything else — a plain-kind mismatch like a string flowing into a
|
||||
// class-instance slot — is tsc-rejected in the lowering world and only
|
||||
// reaches here through preflight's project-world second chance (e.g. a
|
||||
@@ -6523,6 +6585,14 @@ export class Lowerer {
|
||||
* else (including a DIFFERENT union) is left for requireExactShape, which
|
||||
* rejects union mismatches with SC2003. */
|
||||
coerceToExpected(expr: IrExpr, expected: IrType): IrExpr {
|
||||
if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr))
|
||||
return this.divergentValue(expr, expected);
|
||||
// Iterator-typed slots: native iterators and differently typed
|
||||
// generators step through an adapter generator.
|
||||
if (expected.kind === "generator" && !typeEquals(expr.type, expected)) {
|
||||
const adapted = iteratorSlotAdapter(this, expr, expected);
|
||||
if (adapted) return adapted;
|
||||
}
|
||||
// Island boundary, both directions. IN: any static value flowing into
|
||||
// an any-typed slot marshals implicitly (tsc allows the assignment;
|
||||
// the marshal is where its semantics live). OUT: an 'any' value
|
||||
@@ -8598,6 +8668,33 @@ export class Lowerer {
|
||||
);
|
||||
}
|
||||
|
||||
/** A never-returning call in a value position of type `expected`: the
|
||||
* call runs for its effects (it throws or exits), and the expression ends
|
||||
* in an unreachable trap instead of producing a value. The result reads an
|
||||
* uninitialized hidden local that control never reaches. */
|
||||
divergentValue(call: IrExpr, expected: IrType): IrExpr {
|
||||
const loc = call.loc;
|
||||
const slot = this.declareHiddenLocal("%never", expected);
|
||||
slot.mutable = true;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "exprStmt", expr: call, loc },
|
||||
{ kind: "varDecl", localId: slot.id, init: null, loc },
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC9002",
|
||||
message: "unreachable: a 'never' call returned — please report this",
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: varRef(slot.id, expected, loc),
|
||||
diverges: true,
|
||||
type: expected,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A function-scope local bound to NO ts.Symbol — the hidden ABI slot of a
|
||||
* defaulted parameter (the parameter's symbol binds to the separately-
|
||||
* declared body local; nothing in the source can name this one). */
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** Debug-only record of why the indexed-read analysis widened a binding,
|
||||
* parameter, return, or field to a runtime-optional representation. It is
|
||||
* enabled by SCRIPTC_DEBUG_RUNTIME_OPTIONAL=1 and writes one line per
|
||||
* widening to stderr; the disabled path records nothing. Not a public API:
|
||||
* the output format may change at any time. */
|
||||
export class RuntimeOptionalProvenance {
|
||||
private readonly lines: string[] = [];
|
||||
private readonly seen = new Set<string>();
|
||||
|
||||
static fromEnvironment(): RuntimeOptionalProvenance | null {
|
||||
return process.env["SCRIPTC_DEBUG_RUNTIME_OPTIONAL"] === "1"
|
||||
? new RuntimeOptionalProvenance()
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `target` names the widened slot; `cause` is the expression or site
|
||||
* whose value made it optional. */
|
||||
note(kind: string, target: ts.Node | undefined, cause: ts.Node | undefined, detail = ""): void {
|
||||
const line = `${kind} ${describe(target)}${detail ? ` ${detail}` : ""} <- ${describe(cause)}`;
|
||||
if (this.seen.has(line)) return;
|
||||
this.seen.add(line);
|
||||
this.lines.push(line);
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
for (const line of this.lines) process.stderr.write(`scriptc runtime-optional ${line}\n`);
|
||||
this.lines.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function describe(node: ts.Node | undefined): string {
|
||||
if (!node) return "?";
|
||||
const sf = node.getSourceFile();
|
||||
const start = node.getStart();
|
||||
const pos = ts.getLineAndCharacterOfPosition(sf, start);
|
||||
const text = sf.text.slice(start, node.end).replace(/\s+/g, " ");
|
||||
const short = text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
||||
return `${sf.fileName}:${pos.line + 1}:${pos.character + 1} \`${short}\``;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import {
|
||||
checkPreflight,
|
||||
cycleEarlyBindings,
|
||||
isNodeEsmFile,
|
||||
loadProgram,
|
||||
makeCycleAdmission,
|
||||
@@ -198,9 +199,17 @@ test.for([
|
||||
["export { first, alias }; export function read() { return 1; }", null],
|
||||
["type Value = typeof alias; export function read(first: number = 1) { return first; }", null],
|
||||
["class Box { value = alias; } export function read(value = first) { return value; }", null],
|
||||
["const value = alias; export function read() { return value; }", "alias"],
|
||||
["class Box { static value = first; } export function read() { return 1; }", "first"],
|
||||
["class Box { [alias] = 1; } export function read() { return 1; }", "alias"],
|
||||
// Top-level reads during the cycle are admitted by the exact
|
||||
// initialization rule (the lowering checks them); shapes without an
|
||||
// initialization state keep the fence.
|
||||
["const value = alias; export function read() { return value; }", null],
|
||||
["class Box { static value = first; } export function read() { return 1; }", null],
|
||||
["class Box { [alias] = 1; } export function read() { return 1; }", null],
|
||||
[
|
||||
'import * as whole from "./main.ts"; const value = whole.first; export function read() { return value; }',
|
||||
"the namespace import 'whole'",
|
||||
],
|
||||
["export default first + 1; export function read() { return 1; }", "default export expression"],
|
||||
])("cycle binding indexes preserve initialization safety: %s", ([body, refused]) => {
|
||||
const directory = mkdtempSync(
|
||||
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-cycle-bindings-"),
|
||||
@@ -219,8 +228,8 @@ test.for([
|
||||
const cycles = checkPreflight(load).filter((diagnostic) => diagnostic.code === "SC1016");
|
||||
if (refused === null) expect(cycles).toEqual([]);
|
||||
else {
|
||||
expect(cycles).toHaveLength(1);
|
||||
expect(cycles[0]!.message).toContain(`binding '${refused}' is read`);
|
||||
expect(cycles.length).toBeGreaterThan(0);
|
||||
for (const cycle of cycles) expect(cycle.message).toContain(refused);
|
||||
}
|
||||
} finally {
|
||||
load.dispose();
|
||||
@@ -269,3 +278,51 @@ test("repeated cycle edges reuse a file's binding index", () => {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.for([
|
||||
[
|
||||
// A shared instance built by a user constructor: the reads of it in
|
||||
// the partner's function can run early; the class cannot.
|
||||
{
|
||||
"main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);',
|
||||
"a.ts":
|
||||
'import { f } from "./b.ts"; export class C { n = 1; } export const shared = new C(); console.log(f());',
|
||||
"b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }',
|
||||
},
|
||||
["shared"],
|
||||
],
|
||||
[
|
||||
// The partner's top level reads the importer's const before it ran.
|
||||
{
|
||||
"main.ts": 'import { a } from "./a.ts"; console.log(a);',
|
||||
"a.ts": 'import { b } from "./b.ts"; export const a: number = b + 1;',
|
||||
"b.ts": 'import { a } from "./a.ts"; export const b: number = a + 1;',
|
||||
},
|
||||
["a"],
|
||||
],
|
||||
[
|
||||
// Declarations complete before any user code runs: nothing to check.
|
||||
{
|
||||
"main.ts": 'import { start } from "./a.ts"; console.log(start());',
|
||||
"a.ts":
|
||||
'import { hop } from "./b.ts"; export const limit = 3; export function start(): number { return hop(limit); } console.log(start());',
|
||||
"b.ts":
|
||||
'import { limit } from "./a.ts"; export function hop(n: number): number { return n + limit; }',
|
||||
},
|
||||
[],
|
||||
],
|
||||
] as const)("cycle bindings read before initialization: %j", ([files, early]) => {
|
||||
const directory = mkdtempSync(
|
||||
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-cycle-early-"),
|
||||
);
|
||||
for (const [name, text] of Object.entries(files)) writeFileSync(join(directory, name), text);
|
||||
const load = loadProgram(join(directory, "main.ts"));
|
||||
try {
|
||||
expect(checkPreflight(load)).toEqual([]);
|
||||
const found = cycleEarlyBindings(load.program, load.entry, load.moduleOrder, true);
|
||||
expect(found.map((binding) => binding.name.text).sort()).toEqual([...early]);
|
||||
} finally {
|
||||
load.dispose();
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1812,10 +1812,11 @@ function nsBindingUsesAreBareStatements7(
|
||||
* Node runs legal ESM cycles all over the ecosystem: the revisited module
|
||||
* is a cache hit, and the only hazard is a READ of a binding before its
|
||||
* declaration executed (TDZ ReferenceError for let/const/class, a stale
|
||||
* `undefined` for var) — runtime evaluation-order semantics the static
|
||||
* story does not model, so cycles where such a read is REACHABLE keep the
|
||||
* SC1016 fence. A cycle is admitted when partial initialization is
|
||||
* provably unobservable — the DECLARATION-ONLY INIT WINDOW rule:
|
||||
* `undefined` for var). Cycles where such a read is REACHABLE are admitted
|
||||
* by the exact initialization rule (exactCycleRefusal7 below), which
|
||||
* models the TDZ with initialization checks. A cycle needs no checks when
|
||||
* partial initialization is provably unobservable — the DECLARATION-ONLY
|
||||
* INIT WINDOW rule:
|
||||
*
|
||||
* 1. Every module of the cycle cluster (the strongly-connected component
|
||||
* — any member may be mid-initialization while another's top level
|
||||
@@ -1906,6 +1907,18 @@ function inTypePosition7(node: ts.Node): boolean {
|
||||
* callback-invoking builtin, a getter, an iterator, or an implicit
|
||||
* coercion of a user object) disqualifies the module's cycles. */
|
||||
function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | null {
|
||||
const nonInert = topLevelInertness7(program);
|
||||
for (const stmt of sf.statements) {
|
||||
const offence = nonInert(stmt);
|
||||
if (offence !== null) return offence;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The per-statement form of nonInertTopLevel7: answers, for one
|
||||
* top-level statement, the first construct that could execute user code
|
||||
* while the statement runs, or null when the statement is inert. */
|
||||
function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Node | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const PRIM =
|
||||
ts.TypeFlags.StringLike |
|
||||
@@ -2070,7 +2083,7 @@ function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | nu
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const stmt of sf.statements) {
|
||||
return (stmt: ts.Statement): ts.Node | null => {
|
||||
if (
|
||||
ts.isImportDeclaration(stmt) ||
|
||||
ts.isExportDeclaration(stmt) ||
|
||||
@@ -2083,36 +2096,25 @@ function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | nu
|
||||
ts.ModifierFlags.Ambient) !==
|
||||
0
|
||||
) {
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
if (ts.isEnumDeclaration(stmt)) {
|
||||
const bad = stmt.members.find((m) => m.initializer !== undefined && !inert(m.initializer));
|
||||
if (bad !== undefined) return bad;
|
||||
continue;
|
||||
}
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
const bad = inertClass(stmt);
|
||||
if (bad !== null) return bad;
|
||||
continue;
|
||||
return stmt.members.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null;
|
||||
}
|
||||
if (ts.isClassDeclaration(stmt)) return inertClass(stmt);
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(d.name)) return d.name; // patterns can invoke getters/iterators
|
||||
if (d.initializer !== undefined && !inert(d.initializer)) return d.initializer;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt)) {
|
||||
if (!inert(stmt.expression)) return stmt;
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt)) return inert(stmt.expression) ? null : stmt;
|
||||
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
||||
if (!inert(stmt.expression)) return stmt;
|
||||
continue;
|
||||
return inert(stmt.expression) ? null : stmt;
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/** The head of a property/element-access chain. */
|
||||
@@ -2182,13 +2184,16 @@ function backEdgeUseOffence7(
|
||||
* e.dep already mid-initialization in a depth-first walk), answers null
|
||||
* to ADMIT the cycle — the guarded %init calls reproduce Node's cache-hit
|
||||
* order and nothing can observe the partial initialization — or the
|
||||
* human-readable reason it keeps the SC1016 fence. Two admission chances:
|
||||
* human-readable reason it keeps the SC1016 fence. Three admission chances:
|
||||
* - the CHEAP PER-EDGE rule: the closing statement binds nothing (a
|
||||
* side-effect import) or binds only namespace objects whose every use
|
||||
* is a bare expression statement — no read crosses the edge at all;
|
||||
* - the DECLARATION-ONLY INIT WINDOW rule (the admission block above):
|
||||
* every cluster member is an ES module with an inert top level, and
|
||||
* the closing edge's bindings are used only in deferred positions.
|
||||
* the closing edge's bindings are used only in deferred positions;
|
||||
* - the EXACT INITIALIZATION rule (exactCycleRefusal7): partial
|
||||
* initialization is observable, but every observation is modeled —
|
||||
* the lowering checks each binding cycleEarlyBindings reports.
|
||||
* Tarjan SCCs (lazy, rooted at each queried importer) and cluster
|
||||
* verdicts are memoized across calls, so `edgesOf` must answer the same
|
||||
* edges for the same file every time. */
|
||||
@@ -2238,10 +2243,11 @@ export function makeCycleAdmission(
|
||||
const nsf = node.getSourceFile();
|
||||
return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, node.getStart(nsf)).line + 1}`;
|
||||
};
|
||||
// Cluster verdict memo (keyed by the component array identity): the
|
||||
// reason the cluster's cycles stay fenced, or null when its every
|
||||
// member passes the inert-top-level bar.
|
||||
const sccVerdict = new Map<ts.SourceFile[], string | null>();
|
||||
// Cluster verdict memo (keyed by the component array identity): true
|
||||
// when every member is an ES module passing the inert-top-level bar.
|
||||
const sccVerdict = new Map<ts.SourceFile[], boolean>();
|
||||
// The exact-initialization verdict, same keying (exactCycleRefusal7).
|
||||
const exactVerdict = new Map<ts.SourceFile[], string | null>();
|
||||
// A module can close many cycle edges, each importing many bindings.
|
||||
// Index its identifiers once for this admission pass. ASTs are immutable;
|
||||
// the pass owns the index so it cannot retain a disposed program.
|
||||
@@ -2283,29 +2289,455 @@ export function makeCycleAdmission(
|
||||
return "the cycle's module cluster could not be analyzed";
|
||||
}
|
||||
if (!sccVerdict.has(comp)) {
|
||||
let reason: string | null = null;
|
||||
for (const m of comp) {
|
||||
if (isCjsJsFile7(m, program)) {
|
||||
reason = `${m.fileName} is a CommonJS module — admission covers ES-module cycles only`;
|
||||
break;
|
||||
sccVerdict.set(
|
||||
comp,
|
||||
comp.every((m) => !isCjsJsFile7(m, program) && nonInertTopLevel7(program, m) === null),
|
||||
);
|
||||
}
|
||||
if (
|
||||
sccVerdict.get(comp) === true &&
|
||||
backEdgeUseOffence7(program, e.stmt, usesOf(importer)) === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// The EXACT INITIALIZATION rule: the cycle runs in Node's evaluation
|
||||
// order and every binding that can be observed before its declaration
|
||||
// runs carries an initialization check (cycleEarlyBindings).
|
||||
if (!exactVerdict.has(comp)) {
|
||||
const reason = exactCycleRefusal7(program, comp, lineOf);
|
||||
exactVerdict.set(comp, reason);
|
||||
if (reason === null) {
|
||||
let members = exactCycleMembers7.get(program);
|
||||
if (members === undefined) {
|
||||
members = new Set();
|
||||
exactCycleMembers7.set(program, members);
|
||||
}
|
||||
const off = nonInertTopLevel7(program, m);
|
||||
if (off !== null) {
|
||||
reason = `top-level code at ${lineOf(off)} can run user code during the cycle's init window — only declaration-only module bodies are admitted`;
|
||||
break;
|
||||
for (const m of comp) members.add(m);
|
||||
}
|
||||
}
|
||||
return exactVerdict.get(comp) ?? null;
|
||||
};
|
||||
}
|
||||
|
||||
/* ── exact cyclic initialization ─────────────────────────────────────────
|
||||
* Cycles that fail the declaration-only rule still evaluate exactly like
|
||||
* Node when their shape is representable: the guarded %init calls already
|
||||
* run every module body in ES evaluation order (depth-first postorder,
|
||||
* a module mid-evaluation answered from the cache), function declarations
|
||||
* are callable from the start, and live bindings are shared globals. What
|
||||
* remains is the temporal dead zone: a let/const binding read before its
|
||||
* declaration ran throws ReferenceError. cycleEarlyBindings finds every
|
||||
* such binding whose read CAN run early; the lowering gives each one an
|
||||
* initialization flag and checks it at every read or write that can run
|
||||
* before the declaration. The shapes below have no such representation
|
||||
* and keep the SC1016 fence. */
|
||||
|
||||
/** Program → the modules of every cycle the exact rule admitted. */
|
||||
const exactCycleMembers7 = new WeakMap<ts.Program, Set<ts.SourceFile>>();
|
||||
|
||||
/** The modules of `program` whose import cycles were admitted by the
|
||||
* exact initialization rule (not the declaration-only rule). */
|
||||
export function exactCycleMembers(program: ts.Program): ReadonlySet<ts.SourceFile> {
|
||||
return exactCycleMembers7.get(program) ?? new Set();
|
||||
}
|
||||
|
||||
/** True when the module's top level (outside every function-like body)
|
||||
* contains `await` or `for await`. */
|
||||
function hasTopLevelAwait7(sf: ts.SourceFile): boolean {
|
||||
let found = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (found) return "stop";
|
||||
if (node !== sf && ts.isFunctionLike(node)) return "skip";
|
||||
if (
|
||||
ts.isAwaitExpression(node) ||
|
||||
(ts.isForOfStatement(node) && node.awaitModifier !== undefined)
|
||||
) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The reason a strongly-connected module cluster cannot be initialized
|
||||
* exactly, or null when it can. */
|
||||
function exactCycleRefusal7(
|
||||
program: ts.Program,
|
||||
comp: readonly ts.SourceFile[],
|
||||
lineOf: (node: ts.Node) => string,
|
||||
): string | null {
|
||||
const members = new Set(comp);
|
||||
const targetOf = (stmt: ts.ImportDeclaration | ts.ExportDeclaration): ts.SourceFile | null => {
|
||||
const spec = stmt.moduleSpecifier;
|
||||
if (spec === undefined || !ts.isStringLiteral(spec)) return null;
|
||||
const sf = stmt.getSourceFile();
|
||||
return resolveImport7(program, sf, spec.text) ?? npmStaticDepSf7(program, sf, spec.text);
|
||||
};
|
||||
for (const m of comp) {
|
||||
if (isCjsJsFile7(m, program) || /\.c[jt]s$/.test(m.fileName)) {
|
||||
return `${m.fileName} is a CommonJS module — cyclic initialization covers ES modules only`;
|
||||
}
|
||||
if (hasTopLevelAwait7(m)) {
|
||||
return `${m.fileName} uses top-level await inside the cycle`;
|
||||
}
|
||||
for (const stmt of m.statements) {
|
||||
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
||||
return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
||||
}
|
||||
if (
|
||||
ts.isExportDeclaration(stmt) &&
|
||||
!stmt.isTypeOnly &&
|
||||
(stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause))
|
||||
) {
|
||||
const dep = targetOf(stmt);
|
||||
if (dep !== null && members.has(dep)) {
|
||||
return `the namespace re-export at ${lineOf(stmt)} names a module of the cycle`;
|
||||
}
|
||||
}
|
||||
sccVerdict.set(comp, reason);
|
||||
if (
|
||||
ts.isImportDeclaration(stmt) &&
|
||||
stmt.importClause !== undefined &&
|
||||
!stmt.importClause.isTypeOnly &&
|
||||
stmt.importClause.namedBindings !== undefined &&
|
||||
ts.isNamespaceImport(stmt.importClause.namedBindings)
|
||||
) {
|
||||
const dep = targetOf(stmt);
|
||||
const nsName = stmt.importClause.namedBindings.name;
|
||||
if (
|
||||
dep !== null &&
|
||||
members.has(dep) &&
|
||||
!nsBindingUsesAreBareStatements7(program, m, nsName)
|
||||
) {
|
||||
return `the namespace import '${nsName.text}' at ${lineOf(stmt)} names a module of the cycle`;
|
||||
}
|
||||
}
|
||||
if (ts.isClassDeclaration(stmt) && stmt.name === undefined) {
|
||||
return `the anonymous default class at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
||||
}
|
||||
const ambient =
|
||||
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
||||
ts.ModifierFlags.Ambient) !==
|
||||
0;
|
||||
if (ambient) continue;
|
||||
if (
|
||||
ts.isEnumDeclaration(stmt) &&
|
||||
(ts.getCombinedModifierFlags(stmt) & ts.ModifierFlags.Const) === 0
|
||||
) {
|
||||
return `the enum '${stmt.name.text}' at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
||||
}
|
||||
if (ts.isModuleDeclaration(stmt) && !typeOnlyNamespace7(stmt)) {
|
||||
return `the namespace at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
||||
}
|
||||
}
|
||||
const clusterReason = sccVerdict.get(comp);
|
||||
if (clusterReason === undefined) throw new Error("missing module-cycle verdict");
|
||||
if (clusterReason !== null) return clusterReason;
|
||||
const use = backEdgeUseOffence7(program, e.stmt, usesOf(importer));
|
||||
if (use !== null) {
|
||||
return `the cycle-crossing binding '${use.name}' is read at ${lineOf(use.node)}, outside any function body — a read during the init window observes the partially-initialized module (Node's TDZ ReferenceError / stale var), which is not modeled`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A module-scope binding of an exactly-initialized cycle that some read
|
||||
* or write can reach before its declaration ran. */
|
||||
export interface CycleEarlyBinding {
|
||||
symbol: ts.Symbol;
|
||||
/** The declaration's name. */
|
||||
name: ts.Identifier;
|
||||
kind: "lexical" | "var" | "class";
|
||||
module: ts.SourceFile;
|
||||
/** One reference that can run before the declaration. */
|
||||
reference: ts.Identifier;
|
||||
/** A reference in a position the lowering may resolve at compile time
|
||||
* from the binding's type (a property key, a `typeof` operand). */
|
||||
folded?: ts.Identifier;
|
||||
/** Modules whose TOP-LEVEL code runs only after the declaration ran:
|
||||
* modules outside the cycle and cycle members evaluated later. */
|
||||
settled: ReadonlySet<ts.SourceFile>;
|
||||
}
|
||||
|
||||
/** Every binding of the program's exactly-initialized cycles that can be
|
||||
* observed before its declaration executes. `order` is the compiled module
|
||||
* order; `singleRoot` is false when more than one module graph root runs
|
||||
* (fork or worker entries), so a cycle can be entered at different
|
||||
* members. With one root and a purely ES-module graph the evaluation order
|
||||
* is static and the analysis is precise up to "user code may run": a
|
||||
* binding needs its check when its declaration completes after some
|
||||
* top-level code that can call user functions and a deferred position
|
||||
* (function body, parameter default, instance field) references it, or
|
||||
* when the top level of a cycle member evaluated before it reads it.
|
||||
* Otherwise every deferred reference and every top-level reference from
|
||||
* another member counts. */
|
||||
export function cycleEarlyBindings(
|
||||
program: ts.Program,
|
||||
entry: ts.SourceFile,
|
||||
order: readonly ts.SourceFile[],
|
||||
singleRoot: boolean,
|
||||
): CycleEarlyBinding[] {
|
||||
const exact = exactCycleMembers7.get(program);
|
||||
if (exact === undefined || exact.size === 0) return [];
|
||||
// Every lowering pass of one compile asks with the same inputs.
|
||||
const key = `${entry.fileName}\0${order.map((sf) => sf.fileName).join("\0")}\0${singleRoot}`;
|
||||
const memo = earlyBindingsMemo7.get(program);
|
||||
if (memo !== undefined && memo.key === key) return memo.bindings;
|
||||
const bindings = computeCycleEarlyBindings7(program, entry, order, singleRoot, exact);
|
||||
earlyBindingsMemo7.set(program, { key, bindings });
|
||||
return bindings;
|
||||
}
|
||||
|
||||
const earlyBindingsMemo7 = new WeakMap<
|
||||
ts.Program,
|
||||
{ key: string; bindings: CycleEarlyBinding[] }
|
||||
>();
|
||||
|
||||
function computeCycleEarlyBindings7(
|
||||
program: ts.Program,
|
||||
entry: ts.SourceFile,
|
||||
order: readonly ts.SourceFile[],
|
||||
singleRoot: boolean,
|
||||
exact: ReadonlySet<ts.SourceFile>,
|
||||
): CycleEarlyBinding[] {
|
||||
const inOrder = new Set(order);
|
||||
const members = [...exact].filter((m) => inOrder.has(m));
|
||||
if (members.length === 0) return [];
|
||||
const memberSet = new Set(members);
|
||||
const depsOf = (sf: ts.SourceFile): ts.SourceFile[] =>
|
||||
orderedImportsOf(program, sf).flatMap(({ dep }) =>
|
||||
dep !== null && dep !== sf && inOrder.has(dep) ? [dep] : [],
|
||||
);
|
||||
// Strongly-connected components among the exactly-initialized members.
|
||||
const comps: ts.SourceFile[][] = [];
|
||||
{
|
||||
const index = new Map<ts.SourceFile, number>();
|
||||
const low = new Map<ts.SourceFile, number>();
|
||||
const onStack = new Set<ts.SourceFile>();
|
||||
const stack: ts.SourceFile[] = [];
|
||||
let next = 0;
|
||||
const connect = (v: ts.SourceFile): void => {
|
||||
index.set(v, next);
|
||||
low.set(v, next);
|
||||
next++;
|
||||
stack.push(v);
|
||||
onStack.add(v);
|
||||
for (const w of depsOf(v)) {
|
||||
if (!memberSet.has(w)) continue;
|
||||
if (!index.has(w)) {
|
||||
connect(w);
|
||||
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
||||
} else if (onStack.has(w)) {
|
||||
low.set(v, Math.min(low.get(v)!, index.get(w)!));
|
||||
}
|
||||
}
|
||||
if (low.get(v) === index.get(v)) {
|
||||
const comp: ts.SourceFile[] = [];
|
||||
for (;;) {
|
||||
const w = stack.pop()!;
|
||||
onStack.delete(w);
|
||||
comp.push(w);
|
||||
if (w === v) break;
|
||||
}
|
||||
if (comp.length > 1) comps.push(comp);
|
||||
}
|
||||
};
|
||||
for (const m of members) if (!index.has(m)) connect(m);
|
||||
}
|
||||
if (comps.length === 0) return [];
|
||||
// The static evaluation order: Node's depth-first postorder from the
|
||||
// entry over ES import edges. Known only for one root and a graph with
|
||||
// no CommonJS module (a require() evaluates its target mid-body).
|
||||
const position = new Map<ts.SourceFile, number>();
|
||||
if (
|
||||
singleRoot &&
|
||||
!order.some((sf) => isCjsJsFile7(sf, program) || /\.c[jt]s$/.test(sf.fileName))
|
||||
) {
|
||||
const seen = new Set<ts.SourceFile>();
|
||||
const visit = (sf: ts.SourceFile): void => {
|
||||
seen.add(sf);
|
||||
for (const dep of depsOf(sf)) if (!seen.has(dep)) visit(dep);
|
||||
position.set(sf, position.size);
|
||||
};
|
||||
visit(entry);
|
||||
}
|
||||
const checker = program.getTypeChecker();
|
||||
const nonInert = topLevelInertness7(program);
|
||||
const out: CycleEarlyBinding[] = [];
|
||||
for (const comp of comps) {
|
||||
const known = comp.every((m) => position.has(m));
|
||||
const evaluation = known
|
||||
? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!)
|
||||
: comp.slice();
|
||||
interface Candidate {
|
||||
symbol: ts.Symbol;
|
||||
name: ts.Identifier;
|
||||
kind: CycleEarlyBinding["kind"];
|
||||
module: ts.SourceFile;
|
||||
/** User code can run between the cycle's start and the end of the
|
||||
* declaration (always true when the order is unknown). */
|
||||
exposed: boolean;
|
||||
rank: number;
|
||||
start: number;
|
||||
end: number;
|
||||
reference: ts.Identifier | null;
|
||||
folded: ts.Identifier | null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const candidates = new Map<ts.Symbol, Candidate>();
|
||||
const names = new Set<string>();
|
||||
let dirty = !known;
|
||||
evaluation.forEach((m, rank) => {
|
||||
for (const stmt of m.statements) {
|
||||
const offence = nonInert(stmt);
|
||||
const exposed = dirty || offence !== null;
|
||||
const declared: { name: ts.Identifier; kind: CycleEarlyBinding["kind"] }[] = [];
|
||||
if (
|
||||
ts.isVariableStatement(stmt) &&
|
||||
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
||||
ts.ModifierFlags.Ambient) ===
|
||||
0
|
||||
) {
|
||||
const lexical =
|
||||
(stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
|
||||
for (const d of stmt.declarationList.declarations) {
|
||||
for (const name of boundNames7(d.name)) {
|
||||
declared.push({ name, kind: lexical ? "lexical" : "var" });
|
||||
}
|
||||
}
|
||||
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
|
||||
declared.push({ name: stmt.name, kind: "class" });
|
||||
}
|
||||
for (const { name, kind } of declared) {
|
||||
const symbol = checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) continue;
|
||||
candidates.set(symbol, {
|
||||
symbol,
|
||||
name,
|
||||
kind,
|
||||
module: m,
|
||||
exposed,
|
||||
rank,
|
||||
start: stmt.getStart(m),
|
||||
end: stmt.getEnd(),
|
||||
reference: null,
|
||||
folded: null,
|
||||
});
|
||||
names.add(name.text);
|
||||
}
|
||||
if (offence !== null) dirty = true;
|
||||
}
|
||||
});
|
||||
if (candidates.size === 0) continue;
|
||||
const rankOf = new Map(evaluation.map((m, i) => [m, i] as const));
|
||||
// References: every identifier spelled like a candidate, resolved
|
||||
// through import aliases to the declaration's symbol.
|
||||
for (const sf of order) {
|
||||
const refs: ts.Identifier[] = [];
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (ts.isImportDeclaration(node)) return "skip";
|
||||
if (ts.isIdentifier(node) && names.has(node.text)) refs.push(node);
|
||||
return undefined;
|
||||
});
|
||||
if (refs.length === 0) continue;
|
||||
checker.prefetchSymbolNodesExact(refs);
|
||||
for (const ref of refs) {
|
||||
const parent = ref.parent;
|
||||
if (parent !== undefined && ts.isExportSpecifier(parent)) continue;
|
||||
if (inTypePosition7(ref)) continue;
|
||||
let symbol = checker.getSymbolAtLocation(ref);
|
||||
if (
|
||||
parent !== undefined &&
|
||||
ts.isShorthandPropertyAssignment(parent) &&
|
||||
parent.name === ref
|
||||
) {
|
||||
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
|
||||
}
|
||||
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = checker.getAliasedSymbol(symbol);
|
||||
}
|
||||
const c = symbol === undefined ? undefined : candidates.get(symbol);
|
||||
if (c === undefined || ref === c.name) continue;
|
||||
if (c.folded === null && foldablePosition7(ref)) c.folded = ref;
|
||||
if (c.reference !== null) continue;
|
||||
// A class body names its own inner binding, initialized throughout.
|
||||
if (
|
||||
c.kind === "class" &&
|
||||
sf === c.module &&
|
||||
ref.getStart(sf) >= c.start &&
|
||||
ref.getEnd() <= c.end
|
||||
)
|
||||
continue;
|
||||
let early: boolean;
|
||||
if (inDeferredPosition7(ref)) early = c.exposed;
|
||||
else if (sf === c.module) early = ref.getStart(sf) < c.start;
|
||||
else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false;
|
||||
else early = !known || rankOf.get(sf)! < c.rank;
|
||||
if (early) c.reference = ref;
|
||||
}
|
||||
}
|
||||
for (const c of candidates.values()) {
|
||||
if (c.reference === null) continue;
|
||||
const settled = new Set<ts.SourceFile>(order.filter((sf) => !rankOf.has(sf)));
|
||||
if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m);
|
||||
out.push({
|
||||
symbol: c.symbol,
|
||||
name: c.name,
|
||||
kind: c.kind,
|
||||
module: c.module,
|
||||
reference: c.reference,
|
||||
...(c.folded !== null ? { folded: c.folded } : {}),
|
||||
settled,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True for a reference whose value the lowering can take from the
|
||||
* binding's checker type instead of its storage: a property key (element
|
||||
* access argument, computed name, `in` operand, through parentheses,
|
||||
* casts and template spans) or a `typeof` operand. */
|
||||
function foldablePosition7(ref: ts.Identifier): boolean {
|
||||
let child: ts.Node = ref;
|
||||
for (let p: ts.Node | undefined = ref.parent; p !== undefined; p = p.parent) {
|
||||
if (
|
||||
ts.isParenthesizedExpression(p) ||
|
||||
ts.isAsExpression(p) ||
|
||||
ts.isSatisfiesExpression(p) ||
|
||||
ts.isNonNullExpression(p) ||
|
||||
ts.isTemplateSpan(p) ||
|
||||
ts.isTemplateExpression(p)
|
||||
) {
|
||||
child = p;
|
||||
continue;
|
||||
}
|
||||
if (ts.isTypeOfExpression(p)) return true;
|
||||
if (ts.isElementAccessExpression(p)) return p.argumentExpression === child;
|
||||
if (ts.isComputedPropertyName(p)) return true;
|
||||
if (ts.isBinaryExpression(p))
|
||||
return p.operatorToken.kind === ts.SyntaxKind.InKeyword && p.left === child;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The identifiers a binding name (identifier or destructuring pattern)
|
||||
* declares. */
|
||||
function boundNames7(name: ts.BindingName): ts.Identifier[] {
|
||||
if (ts.isIdentifier(name)) return [name];
|
||||
const out: ts.Identifier[] = [];
|
||||
for (const el of name.elements) {
|
||||
if (ts.isOmittedExpression(el) || el.name === undefined) continue;
|
||||
out.push(...boundNames7(el.name));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A namespace declaration whose body holds only types (interfaces, type
|
||||
* aliases, nested type-only namespaces) — no runtime object exists. */
|
||||
function typeOnlyNamespace7(decl: ts.ModuleDeclaration): boolean {
|
||||
let body = decl.body;
|
||||
while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body;
|
||||
if (body === undefined || !ts.isModuleBlock(body)) return true;
|
||||
return body.statements.every(
|
||||
(s) =>
|
||||
ts.isInterfaceDeclaration(s) ||
|
||||
ts.isTypeAliasDeclaration(s) ||
|
||||
(ts.isModuleDeclaration(s) && typeOnlyNamespace7(s)),
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves an import specifier from `from` to a source file of the
|
||||
@@ -2774,10 +3206,9 @@ function preflight7(load: LoadResult): {
|
||||
// benign-cycle admission block above). Node evaluates such cycles
|
||||
// benignly (the revisited module is a cache hit, and no binding read
|
||||
// can hit TDZ or a stale slot), so the guarded %init calls reproduce
|
||||
// its order exactly. Cycles that fail both keep the SC1016 fence,
|
||||
// because a read through them mid-cycle observes TDZ (let/const) or
|
||||
// undefined (var) in Node — runtime evaluation-order semantics the
|
||||
// static story does not model.
|
||||
// its order exactly. Cycles that fail both are judged by the exact
|
||||
// initialization rule: admitted when every early read is modeled by an
|
||||
// initialization check, fenced with SC1016 otherwise.
|
||||
const edges = new Map<ts.SourceFile, CycleEdge[]>();
|
||||
// Import edges Node's RESOLUTION refuses before any module evaluates —
|
||||
// recorded in source order per file (interleaved with the resolved deps
|
||||
@@ -3334,9 +3765,10 @@ function preflight7(load: LoadResult): {
|
||||
// Node's evaluation order: depth-first postorder from the entry, each
|
||||
// module once, back-edges answered from the cache (a module already
|
||||
// evaluating is skipped) — which the guarded %init calls reproduce
|
||||
// exactly. Cycles are admitted only where Node's partially-initialized
|
||||
// semantics are UNOBSERVABLE (JS's partial-initialization is otherwise a
|
||||
// silent-misbehavior trap, so SC1016 stays for the rest):
|
||||
// exactly. Cycles are admitted where Node's partially-initialized
|
||||
// semantics are UNOBSERVABLE or exactly modeled (JS's partial
|
||||
// initialization is otherwise a silent-misbehavior trap, so SC1016
|
||||
// stays for the rest):
|
||||
// - a DIRECT SELF-IMPORT (the package self-name reference resolving to
|
||||
// the importing module itself — Node's self-reference rule): the
|
||||
// imported bindings alias the module's OWN top-level bindings with
|
||||
@@ -3348,7 +3780,11 @@ function preflight7(load: LoadResult): {
|
||||
// benign-cycle admission block: every module of the cycle's
|
||||
// strongly-connected component has an inert top level and the
|
||||
// closing edge's bindings are used only in deferred positions, so no
|
||||
// read can execute before every member initialized).
|
||||
// read can execute before every member initialized);
|
||||
// - the EXACT INITIALIZATION rule: an ES-module cluster whose early
|
||||
// reads are all let/const bindings with module storage — the
|
||||
// lowering checks each one (cycleEarlyBindings) and throws Node's
|
||||
// ReferenceError on a read before the declaration ran.
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, (sf) => edges.get(sf) ?? []);
|
||||
const order: ts.SourceFile[] = [];
|
||||
const state = new Map<ts.SourceFile, "visiting" | "done">();
|
||||
@@ -3362,9 +3798,9 @@ function preflight7(load: LoadResult): {
|
||||
if (s === "done") continue;
|
||||
if (s === "visiting") {
|
||||
// A back-edge: Node answers it from the cache (the module is
|
||||
// already evaluating) — benign exactly when nothing can observe
|
||||
// the partial initialization through this edge's bindings, by the
|
||||
// cheap per-edge rule or the declaration-only init window rule.
|
||||
// already evaluating) — admitted by the cheap per-edge rule, the
|
||||
// declaration-only init window rule, or the exact initialization
|
||||
// rule.
|
||||
const reason = cycleAdmissionReason(sf, e);
|
||||
if (reason !== null) {
|
||||
const cycleStart = stack.indexOf(e.dep.fileName);
|
||||
|
||||
@@ -2735,6 +2735,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
}
|
||||
// Native collection iterators use checked runtime iterator objects.
|
||||
if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN;
|
||||
// Array iterators (`list.values()` and friends) are generators over the
|
||||
// live array: each step reads the current length and element.
|
||||
if (isStdlibInterface("ArrayIterator")) {
|
||||
const arg = checker.getTypeArguments(widened as ts.TypeReference)[0];
|
||||
const yieldT = arg ? mapType(arg, ctx) : null;
|
||||
if (!yieldT || yieldT.kind === "void" || isUnitType(yieldT)) return null;
|
||||
if (!genResultRecord(yieldT, VOID, ctx.shapes, unions)) return null;
|
||||
return { kind: "generator", yieldT, retT: VOID, nextT: UNDEFINED_T };
|
||||
}
|
||||
// Generator<T, TReturn, TNext>, AsyncGenerator<T, TReturn, TNext> (and
|
||||
// the lib's IterableIterator<T, ...>, the older sync annotation spelling):
|
||||
// the generator kind. Channel normalization keeps the runtime honest:
|
||||
@@ -2756,7 +2765,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (
|
||||
isStdlibInterface("Generator") ||
|
||||
isStdlibInterface("AsyncGenerator") ||
|
||||
isStdlibInterface("IterableIterator")
|
||||
isStdlibInterface("IterableIterator") ||
|
||||
// An Iterator<T> annotation holds a single-pass iterator object: a
|
||||
// generator, or a native iterator adapted into one.
|
||||
isStdlibInterface("Iterator")
|
||||
) {
|
||||
const args = checker.getTypeArguments(widened as ts.TypeReference);
|
||||
const channels = genChannels(args[0], args[1], args[2], ctx);
|
||||
|
||||
@@ -1551,6 +1551,11 @@ export interface IrGlobal {
|
||||
/** Lexical record/function/checked-value bindings use their initially-null
|
||||
* pointer as a TDZ sentinel. Reads and later writes throw until initializing assign. */
|
||||
tdz?: true;
|
||||
/** An import-cycle binding whose initialization state lives in this BOOL
|
||||
* global instead: the declaration's store is the `assign` marked
|
||||
* `initializes`, followed by the flag's set; accesses that can run
|
||||
* earlier test it with the module.tdzCheck intrinsic. */
|
||||
initFlag?: string;
|
||||
/** Original declaration and lexical scope, when this is a source binding. */
|
||||
source?: IrBindingSource;
|
||||
}
|
||||
@@ -3028,6 +3033,11 @@ export type IrExpr =
|
||||
stmts: IrStmt[];
|
||||
result: IrExpr;
|
||||
generatorDelegate?: true;
|
||||
/** The statements end in a `runtimeFence` that control never reaches:
|
||||
* the value of a call whose checked type is `never` (it throws or exits)
|
||||
* flowing into a typed slot. `result` is a placeholder read of an
|
||||
* uninitialized hidden local and is never evaluated. */
|
||||
diverges?: true;
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
@@ -3261,7 +3271,11 @@ export type IrExpr =
|
||||
* result's inner type (promise arguments never reach here — the
|
||||
* frontend returns them as-is, the spec's native-promise identity;
|
||||
* thenables and promise-armed unions fence); the backend mints a fresh
|
||||
* promise and fulfills it immediately per the inner kind. */
|
||||
* promise and fulfills it immediately per the inner kind.
|
||||
* module.tdzCheck: two args, a BOOL initialization flag and the binding's
|
||||
* source name as a strLit; void. Throws the catchable ReferenceError
|
||||
* "Cannot access 'name' before initialization" while the flag is false
|
||||
* (a module binding of an import cycle read before its declaration). */
|
||||
| {
|
||||
kind: "intrinsic";
|
||||
name:
|
||||
@@ -3271,7 +3285,8 @@ export type IrExpr =
|
||||
| "promise.all"
|
||||
| "promise.reject"
|
||||
| "promise.resolve"
|
||||
| "module.await";
|
||||
| "module.await"
|
||||
| "module.tdzCheck";
|
||||
args: IrExpr[];
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
|
||||
@@ -1639,6 +1639,12 @@ function validateFunction(
|
||||
allowed.add("throw");
|
||||
allowed.add("runtimeFence");
|
||||
}
|
||||
if (e.diverges) {
|
||||
const last = e.stmts[e.stmts.length - 1];
|
||||
if (last?.kind !== "runtimeFence")
|
||||
err("diverging seqExpr must end in a runtimeFence", e.loc);
|
||||
allowed.add("runtimeFence");
|
||||
}
|
||||
const flat = (ss: IrStmt[]): void => {
|
||||
for (const s of ss) {
|
||||
if (!allowed.has(s.kind)) {
|
||||
@@ -3717,6 +3723,17 @@ function validateFunction(
|
||||
}
|
||||
|
||||
function checkIntrinsicExpr(e: IrExpr & { kind: "intrinsic" }): void {
|
||||
if (e.name === "module.tdzCheck") {
|
||||
const [flag, name] = e.args;
|
||||
if (e.args.length !== 2 || flag === undefined || name?.kind !== "strLit") {
|
||||
err("module.tdzCheck takes a flag and a binding-name literal", e.loc);
|
||||
return;
|
||||
}
|
||||
checkExpr(flag);
|
||||
if (flag.type.kind !== "bool") err("module.tdzCheck flag must be bool", e.loc);
|
||||
if (e.type.kind !== "void") err("module.tdzCheck must be void", e.loc);
|
||||
return;
|
||||
}
|
||||
if (e.name === "module.await") {
|
||||
if (e.args.length !== 1) err("module.await takes exactly one argument", e.loc);
|
||||
for (const a of e.args) {
|
||||
@@ -5860,7 +5877,7 @@ function validateFunction(
|
||||
}
|
||||
case "assign": {
|
||||
const binding = locals.get(s.localId) ?? globals.get(s.localId);
|
||||
if (s.initializes && !binding?.tdz) {
|
||||
if (s.initializes && !binding?.tdz && globals.get(s.localId)?.initFlag === undefined) {
|
||||
err(`initializing assign requires a TDZ binding "${s.localId}"`, s.loc);
|
||||
}
|
||||
if (!binding) err(`assign to undeclared local/global "${s.localId}"`, s.loc);
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"name": "circular imports",
|
||||
"status": "unsupported",
|
||||
"code": "SC1016",
|
||||
"note": "cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window"
|
||||
"note": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge"
|
||||
},
|
||||
{
|
||||
"id": "diagnostic.sc1020",
|
||||
|
||||
@@ -318,3 +318,37 @@ console.log(alias(new Item()).value, snapshot(new Item()).value);
|
||||
expect(snapshot).toContain("@sc_bf__x25_Item_constructor");
|
||||
expect(snapshot).toContain("@sc_release_Item");
|
||||
});
|
||||
|
||||
test("unchanged captured parameters are boxed when a closure first needs them", async () => {
|
||||
const mod = await lower(`
|
||||
function invoke(fn: (value: number) => number, value: number): number { return fn(value); }
|
||||
function framed(seed: number, label: string): number {
|
||||
return invoke((value) => value + seed + label.length, 3);
|
||||
}
|
||||
function deferred(options: { limit: number }, skip: boolean): () => number {
|
||||
if (skip) return () => -1;
|
||||
return () => options.limit;
|
||||
}
|
||||
function rebound(count: number): () => number {
|
||||
const read = () => count;
|
||||
count++;
|
||||
return read;
|
||||
}
|
||||
console.log(framed(1, "ab"), deferred({ limit: 4 }, false)(), deferred({ limit: 4 }, true)(), rebound(5)());
|
||||
`);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
// The entry block runs on every call; the lazy box is created later.
|
||||
const entry = (text: string): string => {
|
||||
const labels = [...text.matchAll(/^[\w.]+:$/gm)];
|
||||
return labels.length > 1 ? text.slice(0, labels[1]!.index) : text;
|
||||
};
|
||||
const framed = body(llvm, "sc_f_framed");
|
||||
expect(framed).toContain("alloca %ScrBox");
|
||||
expect(framed).not.toContain("@scr_box_new");
|
||||
expect(framed).not.toContain("@scr_closure_new");
|
||||
const deferred = body(llvm, "sc_f_deferred");
|
||||
expect(deferred).toContain("lazy.box");
|
||||
expect(entry(deferred)).not.toContain("@scr_box_new");
|
||||
expect(deferred).toContain("@scr_box_new");
|
||||
expect(entry(body(llvm, "sc_f_rebound"))).toContain("@scr_box_new");
|
||||
});
|
||||
|
||||
@@ -247,17 +247,57 @@ console.log(work(["first"]));
|
||||
expect(work).not.toContain("@scr_union_release");
|
||||
});
|
||||
|
||||
test("whole-union string comparisons borrow a caller's stack box", async () => {
|
||||
test("whole-union string comparisons only project their operands", async () => {
|
||||
const module = await lower(`
|
||||
function size(text: string): number { return text.length + (text === "" ? 1 : 0); }
|
||||
function work(words: string[], index: number): number { return size(words[index]); }
|
||||
console.log(work([""], 0));
|
||||
`);
|
||||
const facts = analyzeCallLifetimes(new Map(module.functions.map((fn) => [fn.name, fn])));
|
||||
// Strict union equality only reads the tag and payload of both boxes.
|
||||
expect(facts.parameters.has("size")).toBe(true);
|
||||
expect(facts.parameters.get("size")?.has(0)).toBe(true);
|
||||
const work = body(emitLlvmModule(module), "sc_bf_work");
|
||||
expect(work).toContain("alloca %ScrUnion");
|
||||
expect(work).not.toMatch(/@scr_union_new/);
|
||||
expect(work).not.toContain("@scr_union_new");
|
||||
expect(work).toContain("@sc_bf_size");
|
||||
});
|
||||
|
||||
test("class element comparisons, tests and optional chains read through stack tags", async () => {
|
||||
const module = await lower(`
|
||||
class Part { weight = 1; }
|
||||
function same(parts: Part[], index: number, probe: Part): boolean { return parts[index] === probe; }
|
||||
function missing(parts: Part[], index: number): boolean { return parts[index] === undefined; }
|
||||
function weight(parts: Part[], index: number): number { return parts[index]?.weight ?? 0; }
|
||||
function count(parts: Part[], probe: Part): number {
|
||||
let hits = 0;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part === probe) hits++;
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
const parts = [new Part()];
|
||||
console.log(same(parts, 0, parts[0]), missing(parts, 3), weight(parts, 2), count(parts, parts[0]));
|
||||
`);
|
||||
const llvm = emitLlvmModule(module);
|
||||
for (const name of ["same", "missing", "weight", "count"]) {
|
||||
const work = body(llvm, `sc_bf_${name}`);
|
||||
expect(work, name).toContain("alloca %ScrUnion");
|
||||
expect(work, name).not.toContain("@scr_union_new");
|
||||
expect(work, name).not.toMatch(/call ptr @sc_bf__x25_arr_idxOr/);
|
||||
}
|
||||
});
|
||||
|
||||
test("an element read stored beyond its comparison keeps a heap union", async () => {
|
||||
const module = await lower(`
|
||||
class Part { weight = 1; }
|
||||
const kept: (Part | undefined)[] = [];
|
||||
function keep(parts: Part[], index: number, probe: Part): boolean {
|
||||
const part = parts[index];
|
||||
kept.push(part);
|
||||
return part === probe;
|
||||
}
|
||||
console.log(keep([new Part()], 4, new Part()), kept.length);
|
||||
`);
|
||||
const work = body(emitLlvmModule(module), "sc_bf_keep");
|
||||
expect(work).toMatch(/@scr_union_new|@sc_bf__x25_arr_idxOr/);
|
||||
});
|
||||
|
||||
@@ -11677,7 +11677,7 @@
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window"
|
||||
"hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11771,66 +11771,6 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle-tdz-read/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle-tdz-read/b.ts",
|
||||
"<repo>/tests/diagnostics/cycle-tdz-read/a.ts",
|
||||
"<repo>/tests/diagnostics/cycle-tdz-read/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cycle-tdz-read/a.ts → <repo>/tests/diagnostics/cycle-tdz-read/b.ts → <repo>/tests/diagnostics/cycle-tdz-read/a.ts; the cycle-crossing binding 'a' is read at <repo>/tests/diagnostics/cycle-tdz-read/b.ts:2, outside any function body — a read during the init window observes the partially-initialized module (Node's TDZ ReferenceError / stale var), which is not modeled) are not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/cycle-tdz-read/a.ts",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle-window-call/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle-window-call/b.ts",
|
||||
"<repo>/tests/diagnostics/cycle-window-call/a.ts",
|
||||
"<repo>/tests/diagnostics/cycle-window-call/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cycle-window-call/a.ts → <repo>/tests/diagnostics/cycle-window-call/b.ts → <repo>/tests/diagnostics/cycle-window-call/a.ts; top-level code at <repo>/tests/diagnostics/cycle-window-call/a.ts:5 can run user code during the cycle's init window — only declaration-only module bodies are admitted) are not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/cycle-window-call/a.ts",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle/b.ts",
|
||||
"<repo>/tests/diagnostics/cycle/a.ts",
|
||||
"<repo>/tests/diagnostics/cycle/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cycle/a.ts → <repo>/tests/diagnostics/cycle/b.ts → <repo>/tests/diagnostics/cycle/a.ts; the cycle-crossing binding 'a' is read at <repo>/tests/diagnostics/cycle/b.ts:2, outside any function body — a read during the init window observes the partially-initialized module (Node's TDZ ReferenceError / stale var), which is not modeled) are not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/cycle/a.ts",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/dataview.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/dataview.ts"
|
||||
@@ -11900,12 +11840,6 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/dyn-import-cycle-tdz/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/dyn-import-cycle-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/dynamic-import-static.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/dynamic-import-static.ts"
|
||||
@@ -14825,6 +14759,173 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/iterator-typed-returns.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/iterator-typed-returns.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/never-returning-calls.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/never-returning-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/optional-element-defaults.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/optional-element-defaults.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/iterator-slot-fences.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/iterator-slot-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4060-cycle-shared-instance/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4060-cycle-shared-instance/report.ts",
|
||||
"<repo>/tests/corpus/4060-cycle-shared-instance/inventory.ts",
|
||||
"<repo>/tests/corpus/4060-cycle-shared-instance/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4061-cycle-evaluation-order/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4061-cycle-evaluation-order/south.ts",
|
||||
"<repo>/tests/corpus/4061-cycle-evaluation-order/east.ts",
|
||||
"<repo>/tests/corpus/4061-cycle-evaluation-order/north.ts",
|
||||
"<repo>/tests/corpus/4061-cycle-evaluation-order/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4062-cycle-tdz-read/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4062-cycle-tdz-read/probe.ts",
|
||||
"<repo>/tests/corpus/4062-cycle-tdz-read/limits.ts",
|
||||
"<repo>/tests/corpus/4062-cycle-tdz-read/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4063-cycle-hoisted-calls/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4063-cycle-hoisted-calls/odd.ts",
|
||||
"<repo>/tests/corpus/4063-cycle-hoisted-calls/even.ts",
|
||||
"<repo>/tests/corpus/4063-cycle-hoisted-calls/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4064-cycle-classes-after-init/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4064-cycle-classes-after-init/item.ts",
|
||||
"<repo>/tests/corpus/4064-cycle-classes-after-init/shelf.ts",
|
||||
"<repo>/tests/corpus/4064-cycle-classes-after-init/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4065-cycle-init-throws/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4065-cycle-init-throws/rules.ts",
|
||||
"<repo>/tests/corpus/4065-cycle-init-throws/ledger.ts",
|
||||
"<repo>/tests/corpus/4065-cycle-init-throws/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4066-cycle-top-level-tdz/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4066-cycle-top-level-tdz/margin.ts",
|
||||
"<repo>/tests/corpus/4066-cycle-top-level-tdz/frame.ts",
|
||||
"<repo>/tests/corpus/4066-cycle-top-level-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4067-dynamic-import-cycle-tdz/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4067-dynamic-import-cycle-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle-class-early/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle-class-early/dogs.ts",
|
||||
"<repo>/tests/diagnostics/cycle-class-early/pets.ts",
|
||||
"<repo>/tests/diagnostics/cycle-class-early/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle-default-export/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle-default-export/b.ts",
|
||||
"<repo>/tests/diagnostics/cycle-default-export/a.ts",
|
||||
"<repo>/tests/diagnostics/cycle-default-export/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cycle-default-export/a.ts → <repo>/tests/diagnostics/cycle-default-export/b.ts → <repo>/tests/diagnostics/cycle-default-export/a.ts; the default export expression at <repo>/tests/diagnostics/cycle-default-export/a.ts:2 has no initialization state inside the cycle) are not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/cycle-default-export/a.ts",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle-namespace-import/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle-namespace-import/b.ts",
|
||||
"<repo>/tests/diagnostics/cycle-namespace-import/a.ts",
|
||||
"<repo>/tests/diagnostics/cycle-namespace-import/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cycle-namespace-import/a.ts → <repo>/tests/diagnostics/cycle-namespace-import/b.ts → <repo>/tests/diagnostics/cycle-namespace-import/a.ts; the namespace import 'helpers' at <repo>/tests/diagnostics/cycle-namespace-import/a.ts:1 names a module of the cycle) are not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/cycle-namespace-import/a.ts",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/cycle-var-early/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cycle-var-early/b.ts",
|
||||
"<repo>/tests/diagnostics/cycle-var-early/a.ts",
|
||||
"<repo>/tests/diagnostics/cycle-var-early/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/cycle-generations-long-lived.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/cycle-generations-long-lived.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4045-optional-element-projections.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4045-optional-element-projections.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4046-captured-parameter-values.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4046-captured-parameter-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4047-captured-parameter-rebinding.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4047-captured-parameter-rebinding.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3138-array-large-storage.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3138-array-large-storage.ts"
|
||||
|
||||
+250
-146
@@ -39,12 +39,17 @@
|
||||
* from it drags in every node below, and none of it is ever garbage.
|
||||
*
|
||||
* So the collector is generational, in the shape CPython uses for this
|
||||
* same algorithm. Each header carries a `gen`; a pass names the oldest
|
||||
* generation it will walk and SKIPS every object above it, exactly as it
|
||||
* skips NULL and immortals. Objects that survive a pass are promoted, so
|
||||
* the next nursery pass never walks them again: the cost of a nursery pass
|
||||
* is proportional to what has been allocated since the last one, not to
|
||||
* the live heap.
|
||||
* same algorithm: nursery, mature and old. Each header carries a `gen`; a
|
||||
* pass names the oldest generation it will walk and SKIPS every object above
|
||||
* it, exactly as it skips NULL and immortals. Objects that survive a pass are
|
||||
* promoted one generation, so the next pass at a lower level never walks
|
||||
* them again: the cost of a nursery pass is proportional to what has been
|
||||
* allocated since the last one, and the cost of a mature pass to what has
|
||||
* survived a nursery pass since the last mature one — not to the live heap.
|
||||
* The third generation is what keeps a long-lived structure out of the walk
|
||||
* that reclaims medium-lived garbage: objects that die after surviving one
|
||||
* pass (scratch structures built for a phase of work, say) are reclaimed by
|
||||
* mature passes that never touch the data that has been live for a while.
|
||||
*
|
||||
* Restricting the walk stays sound because trial deletion is already
|
||||
* conservative in the right direction. An edge from a skipped (older)
|
||||
@@ -53,41 +58,57 @@
|
||||
* answer the collector gives for a reference held by the stack. Edges the
|
||||
* other way are simply not followed; the older object was not a candidate
|
||||
* for freeing in this pass. What this gives up is cycles that SPAN
|
||||
* generations: those are invisible to a nursery pass and are found by the
|
||||
* full pass, which walks every generation and so is exactly the old
|
||||
* algorithm.
|
||||
* generations: those are invisible to a restricted pass and are found by
|
||||
* the full pass, which walks every generation and so is exactly the
|
||||
* single-generation algorithm.
|
||||
*
|
||||
* Scheduling. One counter drives the release path: candidates buffered
|
||||
* since the last pass. When it trips, the pass runs at the nursery level
|
||||
* unless either full-pass condition holds:
|
||||
* heap growth the live cycle-headered heap is a fraction past its size
|
||||
* after the last full pass — the standard mature-generation
|
||||
* rule, which catches garbage made from fresh allocation;
|
||||
* mature backlog the mature candidate buffer has reached a fraction of the
|
||||
* since the last pass. When it trips, the pass runs at the highest level
|
||||
* whose condition holds:
|
||||
* full, growth the live cycle-headered heap is a fraction past its size
|
||||
* after the last full pass. The fraction starts at 1/4 and
|
||||
* ADAPTS to what full passes find: a growth-triggered full
|
||||
* pass that reclaims few OLD objects relative to the growth
|
||||
* that triggered it, twice in a row, doubles the fraction
|
||||
* (up to 1/2), and one that does reclaim them restores 1/4.
|
||||
* Garbage younger than the old generation does not count:
|
||||
* the mature level reclaims it without walking the old heap.
|
||||
* This is what stops a large retained structure from being
|
||||
* re-walked at every quarter of growth while it is building
|
||||
* or while the garbage is all young.
|
||||
* full, backlog the old candidate buffer has reached a fraction of the
|
||||
* live heap. This one is not redundant: live data that is
|
||||
* unlinked INTO a dead cycle grows no counter at all (it
|
||||
* was tallied when it was allocated) and is invisible to a
|
||||
* nursery pass, so without it a program that churns its
|
||||
* restricted pass, so without it a program that churns its
|
||||
* long-lived structures would never collect anything. But
|
||||
* a mature object is also buffered whenever a temporary
|
||||
* an old object is also buffered whenever a temporary
|
||||
* reference to it is dropped, so merely READING a live
|
||||
* structure (walking a tree, say) refills the buffer with
|
||||
* objects that are not garbage. A full pass that frees
|
||||
* almost nothing therefore doubles this fraction, and a
|
||||
* productive one restores it.
|
||||
* scheduled age a mature candidate has waited through a nursery's worth
|
||||
* of event-loop checkpoints. Candidate COUNT cannot bound
|
||||
* the garbage behind one root, and an idle heap does not
|
||||
* trip growth, so this keeps a sparse mature backlog from
|
||||
* objects that are not garbage. A pass that frees almost
|
||||
* nothing therefore doubles this fraction, and a productive
|
||||
* one restores it.
|
||||
* mature the same two rules one level down, with a fixed 1/4
|
||||
* growth fraction measured from the last mature-or-full
|
||||
* pass and a backlog rule over the mature buffer.
|
||||
* scheduled age a mature or old candidate has waited through a nursery's
|
||||
* worth of event-loop checkpoints. Candidate COUNT cannot
|
||||
* bound the garbage behind one root, and an idle heap does
|
||||
* not trip growth, so this keeps a sparse backlog from
|
||||
* floating forever without putting a full walk on every
|
||||
* turn.
|
||||
* The first two bound collection work to a constant factor of mutator work —
|
||||
* one heap-sized walk per live/N candidates — while the scheduled age is the
|
||||
* liveness backstop, limiting its forced full walks to one per nursery's worth
|
||||
* of checkpoints while mature roots wait. That is the trade: a fixed root
|
||||
* threshold bounds floating garbage tightly and pays unbounded time for it;
|
||||
* this bounds ordinary mutator-triggered work and lets garbage float in
|
||||
* proportion, but not forever.
|
||||
* The growth and backlog rules bound collection work to a constant factor of
|
||||
* mutator work — one heap-sized walk per live/N candidates or per growth of
|
||||
* live/N — while the scheduled age is the liveness backstop, limiting its
|
||||
* forced full walks to one per nursery's worth of checkpoints while older
|
||||
* roots wait. That is the trade: a fixed root threshold bounds floating
|
||||
* garbage tightly and pays unbounded time for it; this bounds ordinary
|
||||
* mutator-triggered work and lets garbage float in proportion, but not
|
||||
* forever. Peak memory stays bounded the same way: young garbage floats by
|
||||
* at most a quarter of the heap before a mature pass, and old garbage by at
|
||||
* most half the heap's size after the last full pass, and by more than a
|
||||
* quarter only once two full passes in a row have shown that little of the
|
||||
* growth was old garbage.
|
||||
*/
|
||||
#include "scr_runtime.h"
|
||||
|
||||
@@ -154,10 +175,11 @@ void scr_rc_destroy(void *obj, void (*destroy)(void *)) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Live cycle-headered objects — what the full-pass trigger watches.
|
||||
* Exported: the inline allocation paths (scr_runtime.h, llvm/alloc.ts)
|
||||
* maintain it too. */
|
||||
/* Live cycle-headered objects and the running count of OLD objects freed.
|
||||
* The inline allocation/free paths (scr_runtime.h, llvm/alloc.ts) maintain
|
||||
* both counters so full passes can measure old-generation reclamation. */
|
||||
SCR_TL size_t scr_cyc_live = 0;
|
||||
SCR_TL size_t scr_cyc_old_freed = 0;
|
||||
|
||||
void *scr_cyc_alloc(size_t size, ScrTraceFn trace, ScrCycFreeFn free_fn) {
|
||||
return scr_cyc_alloc_inline(size, trace, free_fn);
|
||||
@@ -213,23 +235,46 @@ static SCR_TL ScrVec scr_white;
|
||||
static SCR_TL ScrVec scr_xgen;
|
||||
|
||||
/* Objects above this generation are invisible to the pass in flight. */
|
||||
static SCR_TL unsigned scr_gen_limit = SCR_CYC_MATURE;
|
||||
static SCR_TL unsigned scr_gen_limit = SCR_CYC_OLD;
|
||||
|
||||
static size_t scr_cyc_pass(unsigned gen_limit);
|
||||
|
||||
/* ── when to collect ──────────────────────────────────────────────────── */
|
||||
|
||||
#define SCR_CYC_NURSERY_CANDIDATES 256 /* nursery trigger */
|
||||
#define SCR_CYC_FULL_GROWTH_DIV 4 /* full pass per +1/4 of live heap */
|
||||
#define SCR_CYC_FULL_FLOOR 4096 /* ...but not below this many objects */
|
||||
|
||||
/* Live count as of the end of the last full pass. */
|
||||
/* Scheduling decisions run once per pass, never per object, so they are
|
||||
* kept out of line and built for size: every executable that can release a
|
||||
* cycle-capable value links them, including ones that never form a cycle. */
|
||||
#define SCR_CYC_SCHEDULING __attribute__((cold, noinline, minsize))
|
||||
|
||||
/* Per-pass and cross-generation bookkeeping: work that is proportional to
|
||||
* candidates, dead objects or skipped edges and dominated by the calls it
|
||||
* makes, never the per-edge graph walk. Built for size for the same reason. */
|
||||
#define SCR_CYC_COMPACT __attribute__((noinline, minsize))
|
||||
#define SCR_CYC_GROWTH_DIV 4 /* growth pass per +1/4 of live heap */
|
||||
#define SCR_CYC_GROWTH_FLOOR 4096 /* ...but not below this many objects */
|
||||
|
||||
/* Live count as of the end of the last full pass, and of the last pass that
|
||||
* walked the mature generation (mature or full). */
|
||||
static SCR_TL size_t scr_cyc_live_after_full = 0;
|
||||
static SCR_TL size_t scr_cyc_live_after_mature = 0;
|
||||
|
||||
/* Doublings of the mature-backlog threshold earned by consecutive full passes
|
||||
* that found almost nothing to free (see scr_cyc_mature_threshold). */
|
||||
/* Doublings of the full-pass growth fraction earned by consecutive
|
||||
* growth-triggered full passes that reclaimed little of the old generation
|
||||
* (see scr_cyc_scheduled_pass). Capped at a fraction of 1/2: the heap may grow
|
||||
* at most by half past its size after the last full pass before the next.
|
||||
* The streak counts those unproductive passes since the last productive one;
|
||||
* the fraction only grows once two have run in a row. */
|
||||
#define SCR_CYC_GROWTH_MAX_SHIFT 1
|
||||
static SCR_TL unsigned scr_cyc_growth_shift = 0;
|
||||
static SCR_TL unsigned scr_cyc_growth_streak = 0;
|
||||
|
||||
/* Doublings of each generation's backlog threshold earned by consecutive
|
||||
* passes that found almost nothing to free (see scr_cyc_backlog_threshold).
|
||||
* Only the mature and old entries are used. */
|
||||
#define SCR_CYC_BACKLOG_MAX_SHIFT 8
|
||||
static SCR_TL unsigned scr_cyc_backlog_shift = 0;
|
||||
static SCR_TL unsigned scr_cyc_backlog_shift[SCR_CYC_NGENS];
|
||||
|
||||
static size_t scr_cyc_nursery_threshold(void) {
|
||||
static SCR_TL size_t cached = 0;
|
||||
@@ -241,81 +286,128 @@ static size_t scr_cyc_nursery_threshold(void) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
/* Buffered candidates across both generations, and the count at which the
|
||||
/* Buffered candidates across every generation, and the count at which the
|
||||
* release path next collects. Keeping a running total (rather than summing
|
||||
* the buffers) is what holds the hot path to one load and one compare, as
|
||||
* it was when there was a single buffer. The trigger is re-armed at the end
|
||||
* of every pass to "whatever survived, plus a nursery's worth": without that
|
||||
* hysteresis a large mature buffer — which a nursery pass cannot drain —
|
||||
* hysteresis a large older buffer — which a nursery pass cannot drain —
|
||||
* would re-arm on every single release. Both start at zero, so the first
|
||||
* release runs one trivial pass that arms them properly. */
|
||||
static SCR_TL size_t scr_cyc_nbuffered = 0;
|
||||
static SCR_TL size_t scr_cyc_trigger = 0;
|
||||
|
||||
/* Scheduled nursery passes for which at least one mature root was waiting.
|
||||
* A full pass resets the age; with no mature backlog there is nothing to age. */
|
||||
/* Scheduled nursery passes for which at least one mature or old root was
|
||||
* waiting. A full pass resets the age; with no such backlog there is nothing
|
||||
* to age. */
|
||||
static SCR_TL size_t scr_cyc_scheduled_mature_age = 0;
|
||||
|
||||
static size_t scr_cyc_buffered(void) {
|
||||
return scr_roots[SCR_CYC_NURSERY].n + scr_roots[SCR_CYC_MATURE].n;
|
||||
return scr_roots[SCR_CYC_NURSERY].n + scr_roots[SCR_CYC_MATURE].n
|
||||
+ scr_roots[SCR_CYC_OLD].n;
|
||||
}
|
||||
|
||||
/* A full pass is due once the live heap has grown a fraction past what it
|
||||
* was when the last one finished. Since a pass resets the baseline to the
|
||||
* live count it leaves behind, an unproductive full pass cannot re-trigger
|
||||
* itself — the next one waits for another fraction of growth. */
|
||||
static bool scr_cyc_full_due(void) {
|
||||
size_t base = scr_cyc_live_after_full;
|
||||
if (base < SCR_CYC_FULL_FLOOR) base = SCR_CYC_FULL_FLOOR;
|
||||
return scr_cyc_live > base + base / SCR_CYC_FULL_GROWTH_DIV;
|
||||
static bool scr_cyc_older_backlog(void) {
|
||||
return scr_roots[SCR_CYC_MATURE].n != 0 || scr_roots[SCR_CYC_OLD].n != 0;
|
||||
}
|
||||
|
||||
/* Cold half of the release path: pick the generation and collect.
|
||||
static size_t scr_cyc_growth_base(size_t live_after) {
|
||||
return live_after < SCR_CYC_GROWTH_FLOOR ? SCR_CYC_GROWTH_FLOOR : live_after;
|
||||
}
|
||||
|
||||
/* A pass is due once the live heap has grown a fraction past what it was
|
||||
* when the last pass at that level finished. Since a pass resets its
|
||||
* baseline to the live count it leaves behind, an unproductive pass cannot
|
||||
* re-trigger itself — the next one waits for another fraction of growth. */
|
||||
static bool scr_cyc_grown(size_t live_after, unsigned shift) {
|
||||
size_t base = scr_cyc_growth_base(live_after);
|
||||
return scr_cyc_live > base + ((base / SCR_CYC_GROWTH_DIV) << shift);
|
||||
}
|
||||
|
||||
/* The backlog rule for one generation's candidate buffer.
|
||||
*
|
||||
* Heap growth is not the only thing that owes a full pass. Live data that
|
||||
* TURNS INTO garbage — a mature structure unlinked into a dead cycle —
|
||||
* grows no counter at all: those objects were already tallied in
|
||||
* scr_cyc_live when they were allocated, so scr_cyc_full_due stays false
|
||||
* forever, and a nursery pass cannot see them because they are mature. Left
|
||||
* at that, a program that churns its long-lived structures without
|
||||
* allocating would never collect anything. So the mature buffer's own size
|
||||
* is the second full-pass trigger, at a fraction of the live heap: that
|
||||
* bounds uncollected mature candidates proportionally and keeps the
|
||||
* amortized cost linear (one heap-sized walk per live/N candidates).
|
||||
* Heap growth is not the only thing that owes a pass. Live data that TURNS
|
||||
* INTO garbage — an older structure unlinked into a dead cycle — grows no
|
||||
* counter at all: those objects were already tallied in scr_cyc_live when
|
||||
* they were allocated, so the growth rules stay false forever, and a
|
||||
* restricted pass cannot see them because they sit above its level. Left at
|
||||
* that, a program that churns its long-lived structures without allocating
|
||||
* would never collect anything. So a buffer's own size is the second trigger
|
||||
* for its level, at a fraction of the live heap: that bounds uncollected
|
||||
* candidates proportionally and keeps the amortized cost linear (one walk
|
||||
* per live/N candidates).
|
||||
*
|
||||
* Re-buffered live objects cannot be told apart from new garbage until a
|
||||
* pass walks them, and a program that keeps traversing one live structure
|
||||
* buffers it again after every pass. Each full pass that frees almost none
|
||||
* of its candidates doubles the threshold, so repeated unproductive walks
|
||||
* thin out geometrically; once it exceeds the mature population this
|
||||
* trigger rests, leaving heap growth, the scheduled age and exit to collect.
|
||||
* That only delays garbage made from existing data: replacing it requires
|
||||
* allocation, which grows the heap and trips the growth trigger. The first
|
||||
* productive full pass restores the original fraction. */
|
||||
static size_t scr_cyc_mature_threshold(void) {
|
||||
size_t t = scr_cyc_live / SCR_CYC_FULL_GROWTH_DIV;
|
||||
* buffers it again after every pass. Each backlog-level pass that frees
|
||||
* almost none of its candidates doubles the threshold, so repeated
|
||||
* unproductive walks thin out geometrically; once it exceeds the population
|
||||
* this trigger rests, leaving heap growth, the scheduled age and exit to
|
||||
* collect. That only delays garbage made from existing data: replacing it
|
||||
* requires allocation, which grows the heap and trips a growth trigger. The
|
||||
* first productive pass at that level restores the original fraction. */
|
||||
static size_t scr_cyc_backlog_threshold(unsigned gen) {
|
||||
size_t t = scr_cyc_live / SCR_CYC_GROWTH_DIV;
|
||||
if (t < SCR_CYC_NURSERY_CANDIDATES) t = SCR_CYC_NURSERY_CANDIDATES;
|
||||
return t > (SIZE_MAX >> scr_cyc_backlog_shift) ? SIZE_MAX : t << scr_cyc_backlog_shift;
|
||||
unsigned shift = scr_cyc_backlog_shift[gen];
|
||||
return t > (SIZE_MAX >> shift) ? SIZE_MAX : t << shift;
|
||||
}
|
||||
|
||||
/* A scheduled full pass, adapting the backlog threshold to its yield. The
|
||||
* explicit sweep (scr_collect_cycles) is not a scheduling decision and
|
||||
* leaves the threshold alone. Productive means at least one object freed
|
||||
* per FULL_GROWTH_DIV candidates; a pass with no candidates says nothing. */
|
||||
static void scr_cyc_full_pass(void) {
|
||||
size_t freed = scr_cyc_pass(SCR_CYC_MATURE);
|
||||
if (scr_cands.n == 0) return;
|
||||
if (freed * SCR_CYC_FULL_GROWTH_DIV >= scr_cands.n)
|
||||
scr_cyc_backlog_shift = 0;
|
||||
else if (scr_cyc_backlog_shift < SCR_CYC_BACKLOG_MAX_SHIFT)
|
||||
scr_cyc_backlog_shift++;
|
||||
/* One scheduled pass at `gen`. A mature or old pass adapts that level's
|
||||
* backlog threshold to its yield; the explicit sweep (scr_collect_cycles) is
|
||||
* not a scheduling decision and leaves the thresholds alone. Productive means
|
||||
* at least one object freed per GROWTH_DIV candidates; a pass with no
|
||||
* candidates says nothing.
|
||||
*
|
||||
* A full pass that heap growth triggered also adapts the growth fraction to
|
||||
* what it reclaimed from the OLD generation — the one part of its work no
|
||||
* restricted pass could have done. Weighing that against the growth since the
|
||||
* last full pass keeps full passes frequent while old structures are being
|
||||
* discarded (their garbage is what floats until the next full pass) and
|
||||
* spaces them out while the heap is growing with live or young data, where
|
||||
* each walk of the retained heap buys almost nothing. Backing off only after
|
||||
* two unproductive passes in a row keeps a program that discards old
|
||||
* structures at a steady rate on the base schedule: its passes alternate
|
||||
* between finding the garbage just aged into the old generation and finding
|
||||
* it still younger, and a single miss says little.
|
||||
*
|
||||
* Every level shares this one body, so a program that links the collector
|
||||
* carries a single copy of the scheduling logic. */
|
||||
static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_growth) {
|
||||
size_t base = scr_cyc_growth_base(scr_cyc_live_after_full);
|
||||
size_t growth = scr_cyc_live > base ? scr_cyc_live - base : 0;
|
||||
size_t old_freed_before = scr_cyc_old_freed;
|
||||
size_t freed = scr_cyc_pass(gen);
|
||||
if (gen == SCR_CYC_NURSERY) return;
|
||||
if (scr_cands.n != 0) {
|
||||
if (freed * SCR_CYC_GROWTH_DIV >= scr_cands.n)
|
||||
scr_cyc_backlog_shift[gen] = 0;
|
||||
else if (scr_cyc_backlog_shift[gen] < SCR_CYC_BACKLOG_MAX_SHIFT)
|
||||
scr_cyc_backlog_shift[gen]++;
|
||||
}
|
||||
if (!by_growth) return;
|
||||
size_t reclaimed = scr_cyc_old_freed - old_freed_before;
|
||||
if (reclaimed * SCR_CYC_GROWTH_DIV >= growth) {
|
||||
scr_cyc_growth_shift = 0;
|
||||
scr_cyc_growth_streak = 0;
|
||||
} else if (++scr_cyc_growth_streak >= 2
|
||||
&& scr_cyc_growth_shift < SCR_CYC_GROWTH_MAX_SHIFT) {
|
||||
scr_cyc_growth_shift++;
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_cyc_collect_due(void) {
|
||||
if (scr_cyc_full_due() || scr_roots[SCR_CYC_MATURE].n >= scr_cyc_mature_threshold())
|
||||
scr_cyc_full_pass();
|
||||
else
|
||||
scr_cyc_pass(SCR_CYC_NURSERY);
|
||||
/* Cold half of the release path: pick the level and collect. It runs once
|
||||
* per pass, so it is kept out of line and built for size, which leaves the
|
||||
* hot buffering path in scr_cyc_on_release small. */
|
||||
static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) {
|
||||
unsigned gen = SCR_CYC_NURSERY;
|
||||
bool by_growth = scr_cyc_grown(scr_cyc_live_after_full, scr_cyc_growth_shift);
|
||||
if (by_growth || scr_roots[SCR_CYC_OLD].n >= scr_cyc_backlog_threshold(SCR_CYC_OLD))
|
||||
gen = SCR_CYC_OLD;
|
||||
else if (scr_cyc_grown(scr_cyc_live_after_mature, 0)
|
||||
|| scr_roots[SCR_CYC_MATURE].n >= scr_cyc_backlog_threshold(SCR_CYC_MATURE))
|
||||
gen = SCR_CYC_MATURE;
|
||||
scr_cyc_scheduled_pass(gen, by_growth);
|
||||
}
|
||||
|
||||
/* One scheduled pass, for callers that reach a natural collection point
|
||||
@@ -328,11 +420,11 @@ void scr_cyc_collect_scheduled(void) {
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
return;
|
||||
}
|
||||
if (scr_roots[SCR_CYC_MATURE].n == 0) {
|
||||
if (!scr_cyc_older_backlog()) {
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
} else if (++scr_cyc_scheduled_mature_age
|
||||
>= scr_cyc_nursery_threshold()) {
|
||||
scr_cyc_full_pass();
|
||||
scr_cyc_scheduled_pass(SCR_CYC_OLD, false);
|
||||
return;
|
||||
}
|
||||
scr_cyc_collect_due();
|
||||
@@ -353,7 +445,7 @@ void scr_cyc_on_dead(void *obj) {
|
||||
scr_cyc_nbuffered--;
|
||||
/* A death must not consume room reserved for a future candidate. */
|
||||
scr_cyc_trigger--;
|
||||
if (h->gen == SCR_CYC_MATURE && b->n == 0)
|
||||
if (h->gen != SCR_CYC_NURSERY && !scr_cyc_older_backlog())
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
}
|
||||
|
||||
@@ -417,7 +509,7 @@ static void scr_sb_visit(void *child, void *ctx) {
|
||||
static void scr_scan_black(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
h->color = SCR_CYC_BLACK;
|
||||
if (h->gen < SCR_CYC_MATURE) scr_cyc_push(&scr_promote, obj);
|
||||
if (h->gen < SCR_CYC_OLD) scr_cyc_push(&scr_promote, obj);
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push(&scr_restore_pending, obj);
|
||||
return;
|
||||
@@ -483,7 +575,7 @@ static void scr_scan(void *obj, uintptr_t depth) {
|
||||
* edges were never trial-deleted, so anything it references kept that count
|
||||
* and was scan-blacked rather than whitened — the same conservatism that
|
||||
* makes restricting the walk sound in the first place. */
|
||||
static void scr_xg_pin_visit(void *child, void *ctx) {
|
||||
static SCR_CYC_COMPACT void scr_xg_pin_visit(void *child, void *ctx) {
|
||||
(void)ctx;
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (scr_cyc_hdr(child)->gen <= scr_gen_limit) return; /* markGray had it */
|
||||
@@ -509,7 +601,7 @@ static void scr_xg_destroy(void *obj) {
|
||||
scr_xg_freed++;
|
||||
}
|
||||
|
||||
static void scr_xg_release(void *obj) {
|
||||
static SCR_CYC_COMPACT void scr_xg_release(void *obj) {
|
||||
if (SCR_RC(obj) > 1) {
|
||||
SCR_RC(obj) -= 1;
|
||||
scr_cyc_on_release(obj); /* lost a reference: a possible cycle root */
|
||||
@@ -522,43 +614,12 @@ static void scr_xg_release(void *obj) {
|
||||
|
||||
/* One pass over every candidate at or below `gen_limit`; returns how many
|
||||
* objects it freed. */
|
||||
static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
if (scr_collecting) return 0;
|
||||
scr_collecting = true;
|
||||
scr_gen_limit = gen_limit;
|
||||
|
||||
/* markRoots: keep live candidates (still purple), drop the rest — an
|
||||
* object re-retained since buffering is black. Drain the buffers before
|
||||
* walking: tracing only adjusts counts and worklists, so nothing can
|
||||
* re-buffer underneath us. Candidates ABOVE the limit keep their slots
|
||||
* and wait for a full pass. */
|
||||
scr_cands.n = 0;
|
||||
for (unsigned g = 0; g <= gen_limit; g++) {
|
||||
for (size_t i = 0; i < scr_roots[g].n; i++) {
|
||||
void *obj = scr_roots[g].v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
h->buffered = 0;
|
||||
if (h->color == SCR_CYC_PURPLE)
|
||||
scr_cyc_push(&scr_cands, obj);
|
||||
}
|
||||
scr_roots[g].n = 0;
|
||||
}
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i], 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
scr_cyc_hdr(obj)->trace(obj, scr_mg_visit, NULL);
|
||||
}
|
||||
|
||||
scr_white.n = 0;
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_scan(scr_cands.v[i], 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
/* A later outside root may have restored this deferred white node and
|
||||
* its descendants already. Never scan a black node's edges again. */
|
||||
if (scr_cyc_hdr(obj)->color == SCR_CYC_WHITE)
|
||||
scr_cyc_hdr(obj)->trace(obj, scr_scan_visit, NULL);
|
||||
}
|
||||
|
||||
/* The second half of a pass: settle the white set, promote, re-buffer,
|
||||
* tear down and re-arm. Its loops run once per candidate or dead object and
|
||||
* spend their time in the trace and teardown calls they make, so this half is
|
||||
* built for size; the graph walk above it stays fully optimized. Returns how
|
||||
* many objects the pass freed. */
|
||||
static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
size_t dead = 0;
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
@@ -573,7 +634,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
* `gen` still holds the values the walk filtered on (promotion below is
|
||||
* what changes them). A full pass skips no edge, so it pins nothing. */
|
||||
scr_xgen.n = 0;
|
||||
if (gen_limit < SCR_CYC_MATURE) {
|
||||
if (gen_limit < SCR_CYC_OLD) {
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
scr_cyc_hdr(obj)->trace(obj, scr_xg_pin_visit, NULL);
|
||||
@@ -581,10 +642,12 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
}
|
||||
|
||||
/* Every phase that consults SCR_CYC_SKIP has run, so the recorded
|
||||
* survivors can graduate now. They are externally referenced, so none of
|
||||
* them is in the white set about to be freed. */
|
||||
for (size_t i = 0; i < scr_promote.n; i++)
|
||||
scr_cyc_hdr(scr_promote.v[i])->gen = SCR_CYC_MATURE;
|
||||
* survivors can graduate now, one generation each. They are externally
|
||||
* referenced, so none of them is in the white set about to be freed. */
|
||||
for (size_t i = 0; i < scr_promote.n; i++) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(scr_promote.v[i]);
|
||||
if (h->gen < SCR_CYC_OLD) h->gen++;
|
||||
}
|
||||
scr_promote.n = 0;
|
||||
|
||||
/* A restricted pass may have spared a candidate only because the edge
|
||||
@@ -595,13 +658,14 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
* root, and a dead cycle whose members all got spared that way would
|
||||
* never be walked again by any pass, full ones included. So a restricted
|
||||
* pass hands its survivors back as candidates in the generation they were
|
||||
* just promoted into, where a full pass — which skips nothing, and so
|
||||
* judges them on real reference counts — will settle them. A full pass
|
||||
* just promoted into, where a later pass at that level — and in the end a
|
||||
* full pass, which skips nothing and so judges them on real reference
|
||||
* counts — will settle them. A full pass
|
||||
* needs none of this: it walked every edge, so rc > 0 there means a
|
||||
* genuine outside reference and Bacon-Rajan's own reasoning retires the
|
||||
* candidate. Re-buffering costs a walk, never correctness: an object that
|
||||
* is truly live gets re-blackened by the next retain and dropped then. */
|
||||
if (gen_limit < SCR_CYC_MATURE) {
|
||||
if (gen_limit < SCR_CYC_OLD) {
|
||||
for (size_t i = 0; i < scr_cands.n; i++) {
|
||||
void *obj = scr_cands.v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
@@ -640,7 +704,8 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
scr_xgen.n = 0;
|
||||
freed += scr_xg_freed;
|
||||
|
||||
if (gen_limit >= SCR_CYC_MATURE) {
|
||||
if (gen_limit >= SCR_CYC_MATURE) scr_cyc_live_after_mature = scr_cyc_live;
|
||||
if (gen_limit >= SCR_CYC_OLD) {
|
||||
scr_cyc_live_after_full = scr_cyc_live;
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
}
|
||||
@@ -652,6 +717,46 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
return freed;
|
||||
}
|
||||
|
||||
static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
if (scr_collecting) return 0;
|
||||
scr_collecting = true;
|
||||
scr_gen_limit = gen_limit;
|
||||
|
||||
/* markRoots: keep live candidates (still purple), drop the rest — an
|
||||
* object re-retained since buffering is black. Drain the buffers before
|
||||
* walking: tracing only adjusts counts and worklists, so nothing can
|
||||
* re-buffer underneath us. Candidates ABOVE the limit keep their slots
|
||||
* and wait for a pass at their own level. */
|
||||
scr_cands.n = 0;
|
||||
for (unsigned g = 0; g <= gen_limit; g++) {
|
||||
for (size_t i = 0; i < scr_roots[g].n; i++) {
|
||||
void *obj = scr_roots[g].v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
h->buffered = 0;
|
||||
if (h->color == SCR_CYC_PURPLE)
|
||||
scr_cyc_push(&scr_cands, obj);
|
||||
}
|
||||
scr_roots[g].n = 0;
|
||||
}
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i], 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
scr_cyc_hdr(obj)->trace(obj, scr_mg_visit, NULL);
|
||||
}
|
||||
|
||||
scr_white.n = 0;
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_scan(scr_cands.v[i], 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
/* A later outside root may have restored this deferred white node and
|
||||
* its descendants already. Never scan a black node's edges again. */
|
||||
if (scr_cyc_hdr(obj)->color == SCR_CYC_WHITE)
|
||||
scr_cyc_hdr(obj)->trace(obj, scr_scan_visit, NULL);
|
||||
}
|
||||
|
||||
return scr_cyc_settle(gen_limit);
|
||||
}
|
||||
|
||||
void scr_collect_cycles(void) {
|
||||
/* The full pass, run to a fixpoint. A teardown's releases can drop the
|
||||
* last reference to a further cycle, so one pass does not always drain
|
||||
@@ -661,8 +766,7 @@ void scr_collect_cycles(void) {
|
||||
* can have buffered anything new, so a pass that comes up empty ends
|
||||
* this; and since each repeat needs a strictly smaller live heap to
|
||||
* continue, it terminates. */
|
||||
while (scr_cyc_pass(SCR_CYC_MATURE)
|
||||
&& (scr_roots[SCR_CYC_NURSERY].n || scr_roots[SCR_CYC_MATURE].n)) {
|
||||
while (scr_cyc_pass(SCR_CYC_OLD) && scr_cyc_buffered()) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +775,7 @@ void scr_collect_cycles(void) {
|
||||
void scr_cyc_context_cleanup(void) {
|
||||
scr_collect_cycles();
|
||||
ScrVec *vectors[] = {&scr_roots[SCR_CYC_NURSERY], &scr_roots[SCR_CYC_MATURE],
|
||||
&scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen};
|
||||
&scr_roots[SCR_CYC_OLD], &scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen};
|
||||
for (size_t i = 0; i < sizeof vectors / sizeof vectors[0]; i++) {
|
||||
free(vectors[i]->v);
|
||||
*vectors[i] = (ScrVec){0};
|
||||
|
||||
@@ -250,8 +250,8 @@ static void shared_property_keys(void) {
|
||||
static void checked_leaf_cycles(void) {
|
||||
/* Object properties omit primitives from trial deletion, while arrays and
|
||||
* captured unknown values still trace them. Exercise shared leaves through
|
||||
* both edge kinds, with and without an external owner, in either generation. */
|
||||
for (int old = 0; old < 2; old++) {
|
||||
* both edge kinds, with and without an external owner, in every generation. */
|
||||
for (uint16_t gen = SCR_CYC_NURSERY; gen < SCR_CYC_NGENS; gen++) {
|
||||
for (int external = 0; external < 2; external++) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
long before_dyns = scr_dyn_live_count();
|
||||
@@ -260,7 +260,7 @@ static void checked_leaf_cycles(void) {
|
||||
ScrStr *text = scr_str_new("survives either edge", 20);
|
||||
ScrDyn *leaf = scr_dyn_new_str(text);
|
||||
scr_str_release(text);
|
||||
if (old) scr_cyc_hdr(leaf)->gen = SCR_CYC_MATURE;
|
||||
scr_cyc_hdr(leaf)->gen = gen;
|
||||
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
|
||||
ScrDyn *array = scr_dyn_new_arr();
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
|
||||
@@ -336,12 +336,13 @@ char **scr_worker_argv(void);
|
||||
* scan: restore externally-referenced subgraphs; gather: free the
|
||||
* dead cycle members, releasing only edges that LEAVE the white set).
|
||||
* It is GENERATIONAL: each header carries a generation, a pass names the
|
||||
* oldest one it will walk, and objects that survive a pass are promoted out
|
||||
* of the nursery so later nursery passes never re-walk them. That is what
|
||||
* keeps a pass proportional to recent allocation rather than to the whole
|
||||
* live heap — see the generation note in scr_cycle.c for the soundness
|
||||
* argument and the schedule. Collection points: program exit (before the RC
|
||||
* audit), event-loop quiescence, and the per-generation triggers.
|
||||
* oldest one it will walk, and objects that survive a pass are promoted one
|
||||
* generation (nursery, mature, old) so later restricted passes never re-walk
|
||||
* them. That is what keeps a pass proportional to recent allocation rather
|
||||
* than to the whole live heap — see the generation note in scr_cycle.c for
|
||||
* the soundness argument and the schedule. Collection points: program
|
||||
* exit (before the RC audit), event-loop quiescence, and the per-generation
|
||||
* triggers.
|
||||
* SCR_CYCLE_THRESHOLD pins the nursery trigger to a fixed candidate count.
|
||||
* There is no concurrent or incremental collection.
|
||||
*
|
||||
@@ -367,8 +368,9 @@ enum {
|
||||
};
|
||||
|
||||
/* Generations. A candidate sits in the buffer named by its own `gen`, and a
|
||||
* pass walks only objects at or below the generation it collects. */
|
||||
enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_NGENS = 2 };
|
||||
* pass walks only objects at or below the generation it collects; a pass at
|
||||
* SCR_CYC_OLD is a full pass. */
|
||||
enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_OLD = 2, SCR_CYC_NGENS = 3 };
|
||||
|
||||
typedef struct ScrCycHdr {
|
||||
ScrTraceFn trace;
|
||||
@@ -380,7 +382,7 @@ typedef struct ScrCycHdr {
|
||||
#endif
|
||||
uint32_t color; /* SCR_CYC_* */
|
||||
uint16_t buffered; /* 1 = sitting in its generation's candidate buffer */
|
||||
uint16_t gen; /* SCR_CYC_NURSERY..SCR_CYC_MATURE (the walk filter) */
|
||||
uint16_t gen; /* SCR_CYC_NURSERY..SCR_CYC_OLD (the walk filter) */
|
||||
size_t buf_index; /* position there (O(1) removal when rc hits 0) */
|
||||
} ScrCycHdr;
|
||||
|
||||
@@ -400,6 +402,8 @@ typedef struct ScrCycHdr {
|
||||
_Static_assert(sizeof(ScrCycHdr) == 32, "LLVM backend expects a 32-byte cycle header");
|
||||
_Static_assert(offsetof(ScrCycHdr, color) == 16,
|
||||
"LLVM backend's inlined mark-live stores i32 0 at obj-16");
|
||||
_Static_assert(offsetof(ScrCycHdr, gen) == 22,
|
||||
"LLVM backend's inline free reads the generation at header+22");
|
||||
#elif UINTPTR_MAX == UINT32_MAX
|
||||
_Static_assert(sizeof(ScrCycHdr) == 24, "wasm32 cycle payloads require an aligned 24-byte header");
|
||||
_Static_assert(offsetof(ScrCycHdr, color) == 12,
|
||||
@@ -561,6 +565,7 @@ static inline void scr_mem_free(void *p) { free(p); }
|
||||
* count, block. Every other header word starts zero (SCR_CYC_BLACK,
|
||||
* SCR_CYC_NURSERY, unbuffered). */
|
||||
extern SCR_TL size_t scr_cyc_live;
|
||||
extern SCR_TL size_t scr_cyc_old_freed;
|
||||
extern SCR_TL void (*scr_weak_dispose_hook)(void *);
|
||||
static inline void *scr_cyc_alloc_inline(size_t size, ScrTraceFn trace, ScrCycFreeFn free_fn) {
|
||||
if (size > SIZE_MAX - sizeof(ScrCycHdr)) scr_trap("scriptc: out of memory\n");
|
||||
@@ -574,9 +579,12 @@ static inline void *scr_cyc_alloc_inline(size_t size, ScrTraceFn trace, ScrCycFr
|
||||
static inline void scr_cyc_free_inline(void *obj) {
|
||||
if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj);
|
||||
scr_cyc_live--;
|
||||
if (scr_cyc_hdr(obj)->gen == SCR_CYC_OLD) scr_cyc_old_freed++;
|
||||
scr_mem_free(scr_cyc_hdr(obj));
|
||||
}
|
||||
_Static_assert(SCR_CYC_NURSERY == 0, "zeroed cycle headers start in the nursery");
|
||||
_Static_assert(sizeof(((ScrCycHdr *)0)->gen) == 2 && SCR_CYC_OLD == 2,
|
||||
"LLVM backend's inline free compares an i16 generation against 2");
|
||||
|
||||
/* Dispose an object whose reference count already reached zero. The caller
|
||||
* removes its cycle candidate first. Nested disposals keep bounded stack
|
||||
|
||||
@@ -244,6 +244,107 @@ static void check_unproductive_backlog_backs_off(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Survive two passes: nursery to mature, then mature to old. */
|
||||
static void age_to_old(Node *node) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
read_live(node);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
check(scr_cyc_hdr(node)->gen == SCR_CYC_OLD, "survivor did not reach the old generation");
|
||||
}
|
||||
|
||||
/* Fresh live leaves, each released once so the release path keeps running
|
||||
* its passes, until `target` objects have been freed or `limit` leaves have
|
||||
* been made. Returns how many it made. */
|
||||
static size_t grow_until(Node **keep, size_t limit, size_t target) {
|
||||
size_t n = 0;
|
||||
while (freed < target && n < limit) {
|
||||
keep[n] = make_leaf();
|
||||
read_live(keep[n]);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void drop_leaves(Node **keep, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
keep[i]->rc--;
|
||||
scr_cyc_on_dead(keep[i]);
|
||||
node_free(keep[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* A dead cycle that has aged into the old generation is reachable only by a
|
||||
* full pass. While the heap grows with live data, full passes find nothing
|
||||
* and back off, but the dead cycle must still be reclaimed before the heap
|
||||
* has grown by half past its size after the last full pass. Runs first, so
|
||||
* the live heap is below the growth floor and that bound is known. */
|
||||
static void check_old_garbage_reclaimed_while_growing(void) {
|
||||
enum { RING = 64, FLOOR = 4096, LIMIT = 4 * FLOOR };
|
||||
static Node *keep[LIMIT];
|
||||
size_t before = freed;
|
||||
Node *ring = make_ring(RING);
|
||||
age_to_old(ring);
|
||||
release_live(ring); /* last outside owner gone: a dead old cycle */
|
||||
size_t grown = grow_until(keep, LIMIT, before + RING);
|
||||
check(freed == before + RING, "dead old cycle was never reclaimed while the heap grew");
|
||||
check(grown <= FLOOR + FLOOR / 2, "dead old cycle outlived the growth bound");
|
||||
drop_leaves(keep, grown);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
/* Dead rings that survived at least one pass, each holding a traced edge
|
||||
* into an old ring: whichever level reclaims them must pay those edges off,
|
||||
* and must do so without an explicit sweep. */
|
||||
static void check_older_rings_into_old(void) {
|
||||
enum { OLD_RING = 2048, RING = 8, RINGS = 1024, LIMIT = 4 * (OLD_RING + RING * RINGS) };
|
||||
static Node *keep[LIMIT];
|
||||
Node *old = make_ring(OLD_RING);
|
||||
age_to_old(old);
|
||||
size_t before = freed;
|
||||
Node *rings[RINGS];
|
||||
for (size_t i = 0; i < RINGS; i++) {
|
||||
rings[i] = make_ring(RING);
|
||||
rings[i]->other = old;
|
||||
old->rc++;
|
||||
read_live(rings[i]);
|
||||
}
|
||||
scr_cyc_collect_scheduled(); /* every ring survives at least one pass */
|
||||
for (size_t i = 0; i < RINGS; i++) {
|
||||
check(scr_cyc_hdr(rings[i])->gen != SCR_CYC_NURSERY, "ring was not promoted");
|
||||
release_live(rings[i]);
|
||||
}
|
||||
size_t grown = grow_until(keep, LIMIT, before + RINGS * RING);
|
||||
check(freed == before + RINGS * RING, "dead older rings were never reclaimed");
|
||||
check(old->rc == 2, "edges into the old generation were not paid off");
|
||||
drop_leaves(keep, grown);
|
||||
release_live(old);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
/* A dead cycle spanning the mature and old generations is spared by mature
|
||||
* passes; promotion and re-buffering must still hand it to a full pass. */
|
||||
static void check_cycle_spanning_mature_and_old(void) {
|
||||
enum { LIMIT = 4 * 4096 };
|
||||
static Node *keep[LIMIT];
|
||||
Node *old = make_ring(4);
|
||||
age_to_old(old);
|
||||
Node *young = make_ring(4);
|
||||
young->other = old;
|
||||
old->rc++;
|
||||
old->other = young;
|
||||
young->rc++;
|
||||
read_live(young);
|
||||
scr_cyc_collect_scheduled(); /* young survives a pass and is promoted */
|
||||
size_t before = freed;
|
||||
release_live(old);
|
||||
release_live(young); /* only the cycle's own edges remain */
|
||||
size_t grown = grow_until(keep, LIMIT, before + 8);
|
||||
check(freed == before + 8, "cycle spanning generations was never reclaimed");
|
||||
drop_leaves(keep, grown);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
static void check_deep_ring(void) {
|
||||
enum { DEPTH = 100000 };
|
||||
size_t before = freed;
|
||||
@@ -415,6 +516,9 @@ static void check_deferred_white_restoration(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
check_old_garbage_reclaimed_while_growing();
|
||||
check_older_rings_into_old();
|
||||
check_cycle_spanning_mature_and_old();
|
||||
check_unproductive_backlog_backs_off();
|
||||
check_sparse_mature_backlog(1);
|
||||
check_sparse_mature_backlog(2);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Element reads of class arrays compared, tested, and chained directly.
|
||||
// Holes, explicit undefined, out-of-range indices, operand order, and
|
||||
// mutation during later operands keep JavaScript's results.
|
||||
class Part {
|
||||
readonly label: string;
|
||||
weight: number;
|
||||
constructor(label: string, weight: number) {
|
||||
this.label = label;
|
||||
this.weight = weight;
|
||||
}
|
||||
heavier(limit: number): boolean {
|
||||
return this.weight > limit;
|
||||
}
|
||||
}
|
||||
|
||||
const bolt = new Part("bolt", 2);
|
||||
const gear = new Part("gear", 7);
|
||||
const nut = new Part("nut", 1);
|
||||
const parts: Part[] = [bolt, gear, nut];
|
||||
const order = [2, 0, 1, 5, -1, 1.5];
|
||||
|
||||
function positions(list: Part[], probe: Part): string {
|
||||
const found: string[] = [];
|
||||
for (let j = 0; j < order.length; j++) {
|
||||
if (list[order[j]] === probe) found.push(`same@${j}`);
|
||||
if (list[order[j]] !== probe) found.push(`diff@${j}`);
|
||||
if (list[order[j]] === undefined) found.push(`missing@${j}`);
|
||||
}
|
||||
return found.join(" ");
|
||||
}
|
||||
console.log(positions(parts, gear));
|
||||
|
||||
function weights(list: Part[]): string {
|
||||
const out: string[] = [];
|
||||
for (let j = 0; j < order.length; j++) {
|
||||
out.push(String(list[order[j]]?.weight ?? "none"));
|
||||
out.push(list[order[j]]?.label ?? "-");
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
console.log(weights(parts));
|
||||
|
||||
function looped(list: Part[], probe: Part): number {
|
||||
let hits = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (item === probe) hits += 10;
|
||||
if (item !== undefined && item.heavier(1)) hits++;
|
||||
if (!item) hits += 100;
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
console.log(looped(parts, bolt));
|
||||
|
||||
// Holes and explicit undefined values stored behind a bare element type.
|
||||
const sparse: Part[] = [bolt];
|
||||
sparse[3] = nut;
|
||||
const unknownSlot: Part[] = [];
|
||||
sparse[2] = unknownSlot[9];
|
||||
console.log(sparse.length, looped(sparse, nut), 1 in sparse, 2 in sparse);
|
||||
console.log(sparse[1] === sparse[2], sparse[1] === undefined, sparse[2] === undefined);
|
||||
console.log(sparse[1]?.label ?? "hole", sparse[2]?.label ?? "explicit");
|
||||
|
||||
// The left operand is read before the right operand mutates the array.
|
||||
const queue: Part[] = [bolt, gear];
|
||||
console.log(queue[0] === queue.shift(), queue.length);
|
||||
console.log(queue[0] === (queue[0] = nut), queue[0].label);
|
||||
let reads = 0;
|
||||
function at(list: Part[], index: number): number {
|
||||
reads++;
|
||||
return index;
|
||||
}
|
||||
console.log(parts[at(parts, 1)] === parts[at(parts, 1)], reads);
|
||||
console.log(parts[at(parts, 9)] === parts[at(parts, 8)], reads);
|
||||
|
||||
// Reads inside callbacks and nested functions use the same rules.
|
||||
const probes = [bolt, nut, new Part("bolt", 2)];
|
||||
const matches = probes.map((probe) => {
|
||||
let count = 0;
|
||||
for (let i = 0; i <= parts.length; i++) {
|
||||
const item = parts[i];
|
||||
if (item === probe) count++;
|
||||
count += item?.weight ?? 0;
|
||||
}
|
||||
return count;
|
||||
});
|
||||
console.log(matches.join(" "));
|
||||
|
||||
// Failed member reads still throw Node's TypeError.
|
||||
try {
|
||||
const shelf: Part[] = [bolt];
|
||||
console.log(shelf[order[3]].heavier(0));
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
|
||||
// Identity survives repeated reads, and a mutated element is observed.
|
||||
const first = parts[0];
|
||||
parts[0].weight = 11;
|
||||
console.log(first === parts[0], first.weight, parts[0] === bolt, parts.indexOf(bolt));
|
||||
@@ -0,0 +1,97 @@
|
||||
// Parameters captured by nested functions but never rebound. Closures see
|
||||
// the entry value and later changes to the referenced object, on early
|
||||
// returns, escaping callbacks, loops, nesting, and exceptions.
|
||||
interface Filter {
|
||||
key: number;
|
||||
skip: boolean;
|
||||
label: string;
|
||||
}
|
||||
const values = [3, 1, 4, 1, 5, 9, 2, 6];
|
||||
|
||||
function matching(filter: Filter): number {
|
||||
if (filter.skip) return -1;
|
||||
return values.filter((value) => value % 3 === filter.key).length;
|
||||
}
|
||||
console.log(matching({ key: 1, skip: false, label: "a" }), matching({ key: 0, skip: true, label: "b" }));
|
||||
|
||||
function makeReaders(filter: Filter, scale: number, tag: string, enabled: boolean): (() => string)[] {
|
||||
const readers: (() => string)[] = [];
|
||||
if (!enabled) return readers;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
readers.push(() => `${tag}:${filter.label}:${filter.key * scale + i}`);
|
||||
}
|
||||
readers.push(() => (enabled ? tag.toUpperCase() : "off"));
|
||||
return readers;
|
||||
}
|
||||
const shared: Filter = { key: 2, skip: false, label: "first" };
|
||||
const readers = makeReaders(shared, 10, "r", true);
|
||||
shared.label = "renamed";
|
||||
shared.key = 5;
|
||||
console.log(readers.map((read) => read()).join(" "), makeReaders(shared, 1, "x", false).length);
|
||||
|
||||
function nested(base: number, word: string): () => () => string {
|
||||
return () => {
|
||||
const inner = () => `${word}${base}`;
|
||||
return inner;
|
||||
};
|
||||
}
|
||||
const outer = nested(7, "n");
|
||||
console.log(outer()(), outer()(), nested(1, "m")()());
|
||||
|
||||
function guarded(limit: number, items: number[]): number {
|
||||
if (items.length === 0) throw new Error(`empty for ${limit}`);
|
||||
const picked = items.filter((item) => item < limit);
|
||||
if (picked.length === 0) throw new Error(`none below ${limit}`);
|
||||
return picked.reduce((sum, item) => sum + item + limit, 0);
|
||||
}
|
||||
for (const input of [[], [8, 9], [1, 2, 9]]) {
|
||||
try {
|
||||
console.log(guarded(5, input));
|
||||
} catch (error) {
|
||||
console.log("caught", (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function throwsInside(code: string): string {
|
||||
try {
|
||||
[1, 2].forEach((n) => {
|
||||
if (n === 2) throw new Error(`${code}-${n}`);
|
||||
});
|
||||
return "unreachable";
|
||||
} catch (error) {
|
||||
return `${(error as Error).message} ${code}`;
|
||||
}
|
||||
}
|
||||
console.log(throwsInside("q"));
|
||||
|
||||
// Recursive and repeated creation keeps each call's own value.
|
||||
function countdown(n: number): string[] {
|
||||
if (n === 0) return [];
|
||||
const here = () => `step${n}`;
|
||||
return [here(), ...countdown(n - 1), here()];
|
||||
}
|
||||
console.log(countdown(3).join(","));
|
||||
|
||||
class Counter {
|
||||
total = 0;
|
||||
addAll(amount: number, times: number): number {
|
||||
const add = () => {
|
||||
this.total += amount;
|
||||
};
|
||||
for (let i = 0; i < times; i++) add();
|
||||
return this.total;
|
||||
}
|
||||
}
|
||||
const counter = new Counter();
|
||||
console.log(counter.addAll(2, 3), counter.addAll(5, 1));
|
||||
|
||||
function delayed(message: string, order: string[]): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
order.push(message);
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
const order: string[] = [];
|
||||
Promise.all([delayed("later", order), delayed("last", order)]).then(() => console.log(order.join(" ")));
|
||||
@@ -0,0 +1,81 @@
|
||||
// Captured parameters that are rebound anywhere keep one shared binding:
|
||||
// the body, nested closures, compound and destructuring assignments, and
|
||||
// loop headers all update the value every closure observes.
|
||||
function reassignedAfter(count: number): () => number {
|
||||
const read = () => count;
|
||||
count = count * 2;
|
||||
return read;
|
||||
}
|
||||
console.log(reassignedAfter(4)());
|
||||
|
||||
function reassignedInside(start: number): [() => number, () => void] {
|
||||
return [() => start, () => {
|
||||
start += 10;
|
||||
}];
|
||||
}
|
||||
const [get, bump] = reassignedInside(1);
|
||||
bump();
|
||||
bump();
|
||||
console.log(get());
|
||||
|
||||
function deeplyReassigned(name: string): () => string {
|
||||
const outer = () => {
|
||||
const inner = () => {
|
||||
name = name + "!";
|
||||
};
|
||||
inner();
|
||||
return name;
|
||||
};
|
||||
outer();
|
||||
return () => name;
|
||||
}
|
||||
console.log(deeplyReassigned("hey")());
|
||||
|
||||
function compound(total: number, flag: boolean, text: string): () => string {
|
||||
const show = () => `${total}/${flag}/${text}`;
|
||||
total++;
|
||||
total -= 3;
|
||||
flag = !flag;
|
||||
text ??= "unused";
|
||||
text += "+";
|
||||
return show;
|
||||
}
|
||||
console.log(compound(10, false, "t")());
|
||||
|
||||
function destructured(left: number, right: number): () => string {
|
||||
const show = () => `${left},${right}`;
|
||||
[left, right] = [right, left];
|
||||
({ value: left } = { value: left * 100 });
|
||||
return show;
|
||||
}
|
||||
console.log(destructured(1, 2)());
|
||||
|
||||
function loopTarget(item: string, list: string[]): () => string {
|
||||
const show = () => item;
|
||||
for (item of list) {
|
||||
if (item === "stop") break;
|
||||
}
|
||||
return show;
|
||||
}
|
||||
console.log(loopTarget("none", ["a", "stop", "b"])());
|
||||
|
||||
function conditional(level: number, raise: boolean): (() => number)[] {
|
||||
const before = () => level;
|
||||
if (raise) level = level + 1;
|
||||
const after = () => level * 10;
|
||||
return [before, after];
|
||||
}
|
||||
console.log(conditional(1, true).map((f) => f()), conditional(1, false).map((f) => f()));
|
||||
|
||||
function defaulted(size = 3, unit = "cm"): () => string {
|
||||
return () => `${size}${unit}`;
|
||||
}
|
||||
console.log(defaulted()(), defaulted(5)(), defaulted(undefined, "mm")());
|
||||
|
||||
function earlyAndLate(value: number, early: boolean): () => number {
|
||||
if (early) return () => value;
|
||||
const read = () => value;
|
||||
value = -value;
|
||||
return read;
|
||||
}
|
||||
console.log(earlyAndLate(3, true)(), earlyAndLate(3, false)());
|
||||
@@ -0,0 +1,17 @@
|
||||
import { summary } from "./report.ts";
|
||||
|
||||
export class Inventory {
|
||||
items: Map<string, number> = new Map();
|
||||
add(name: string, qty: number): void {
|
||||
this.items.set(name, (this.items.get(name) ?? 0) + qty);
|
||||
}
|
||||
count(): number {
|
||||
let total = 0;
|
||||
for (const qty of this.items.values()) total += qty;
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
export const stock = new Inventory();
|
||||
stock.add("nut", 10);
|
||||
console.log("inventory ready:", summary());
|
||||
@@ -0,0 +1,9 @@
|
||||
// A shared instance exported from one member of an import cycle and read
|
||||
// through a function of the other member. inventory.ts evaluates after
|
||||
// report.ts, so report's function only runs once `stock` initialized.
|
||||
import { stock } from "./inventory.ts";
|
||||
import { summary } from "./report.ts";
|
||||
|
||||
stock.add("bolt", 4);
|
||||
console.log(summary());
|
||||
console.log(stock.count());
|
||||
@@ -0,0 +1,7 @@
|
||||
import { stock } from "./inventory.ts";
|
||||
|
||||
export function summary(): string {
|
||||
const parts: string[] = [];
|
||||
for (const [name, qty] of stock.items) parts.push(`${name}=${qty}`);
|
||||
return parts.join(", ") + ` (total ${stock.count()})`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { south } from "./south.ts";
|
||||
|
||||
export function east(n: number): string {
|
||||
return n <= 0 ? "E" : "e" + south(n - 1);
|
||||
}
|
||||
console.log("east body", east(1));
|
||||
@@ -0,0 +1,9 @@
|
||||
// Side effects across a three-module cycle run in ES evaluation order:
|
||||
// the depth-first walk from main reaches north → east → south, and south's
|
||||
// import of north is answered from the module cache, so the bodies run
|
||||
// south, east, north, then main. Each body calls into the others.
|
||||
import { north, log } from "./north.ts";
|
||||
|
||||
log("main body");
|
||||
console.log(north(3));
|
||||
console.log(log("done"));
|
||||
@@ -0,0 +1,12 @@
|
||||
import { east } from "./east.ts";
|
||||
|
||||
const lines: string[] = [];
|
||||
export function log(line: string): number {
|
||||
lines.push(line);
|
||||
console.log(`[${lines.length}] ${line}`);
|
||||
return lines.length;
|
||||
}
|
||||
export function north(n: number): string {
|
||||
return n <= 0 ? "N" : "n" + east(n - 1);
|
||||
}
|
||||
log("north body " + east(2));
|
||||
@@ -0,0 +1,6 @@
|
||||
import { north } from "./north.ts";
|
||||
|
||||
export function south(n: number): string {
|
||||
return n <= 0 ? "S" : "s" + north(n - 1);
|
||||
}
|
||||
console.log("south body", south(0), typeof north);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { probe, spend, clear } from "./probe.ts";
|
||||
|
||||
console.log("before:", probe());
|
||||
console.log("spend before:", spend(1));
|
||||
console.log("clear before:", clear());
|
||||
export const limit: number = 8;
|
||||
export let used = 0;
|
||||
export function record(n: number): void {
|
||||
used += n;
|
||||
}
|
||||
export function reset(): void {
|
||||
used = 0;
|
||||
}
|
||||
console.log("after:", probe());
|
||||
console.log("spend after:", spend(3));
|
||||
export { probe };
|
||||
@@ -0,0 +1,6 @@
|
||||
// Reading or writing a let/const binding of an import cycle before its
|
||||
// declaration ran throws ReferenceError, exactly as Node reports it; the
|
||||
// same reads succeed once the declaration ran.
|
||||
import { limit, used, probe } from "./limits.ts";
|
||||
|
||||
console.log("main", limit, used, probe());
|
||||
@@ -0,0 +1,28 @@
|
||||
import { limit, used, record, reset } from "./limits.ts";
|
||||
|
||||
export function probe(): string {
|
||||
try {
|
||||
return `${used}/${limit}`;
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
return `${err.name}: ${err.message} (${e instanceof ReferenceError})`;
|
||||
}
|
||||
}
|
||||
|
||||
export function spend(n: number): string {
|
||||
try {
|
||||
record(n);
|
||||
return "ok " + used;
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
export function clear(): string {
|
||||
try {
|
||||
reset();
|
||||
return "cleared";
|
||||
} catch (e) {
|
||||
return (e as Error).name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isOdd } from "./odd.ts";
|
||||
|
||||
export function isEven(n: number): boolean {
|
||||
return n === 0 ? true : isOdd(n - 1);
|
||||
}
|
||||
export function parity(n: number): string {
|
||||
return `${n} is ${labels[isEven(n) ? 0 : 1]}`;
|
||||
}
|
||||
console.log("even body", isEven(4), isOdd(4));
|
||||
const labels = ["even", "odd"];
|
||||
console.log("even ready", parity(2));
|
||||
@@ -0,0 +1,8 @@
|
||||
// Function declarations of an import cycle are initialized before any
|
||||
// module body runs: each member's top level calls functions of the other
|
||||
// member while the cycle is still evaluating, including mutual recursion.
|
||||
import { isEven, parity } from "./even.ts";
|
||||
import { isOdd } from "./odd.ts";
|
||||
|
||||
console.log(isEven(10), isOdd(7), isEven(3));
|
||||
console.log(parity(5), parity(8));
|
||||
@@ -0,0 +1,6 @@
|
||||
import { isEven } from "./even.ts";
|
||||
|
||||
export function isOdd(n: number): boolean {
|
||||
return n === 0 ? false : isEven(n - 1);
|
||||
}
|
||||
console.log("odd body", typeof isEven, isOdd(1));
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Shelf } from "./shelf.ts";
|
||||
|
||||
export class Item {
|
||||
static created = 0;
|
||||
readonly name: string;
|
||||
readonly shelf: Shelf | null;
|
||||
constructor(name: string, shelf: Shelf | null) {
|
||||
this.name = name;
|
||||
this.shelf = shelf;
|
||||
Item.created++;
|
||||
}
|
||||
placed(): string {
|
||||
return this.shelf instanceof Shelf ? `${this.name}@${this.shelf.label}` : `${this.name}@none`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Classes of two cycle members reference each other (construction,
|
||||
// instanceof, statics) from method bodies that run after both class
|
||||
// declarations evaluated; a shared default instance is built at load.
|
||||
import { Shelf, defaultShelf } from "./shelf.ts";
|
||||
import { Item } from "./item.ts";
|
||||
|
||||
const shelf = new Shelf("garage");
|
||||
shelf.add("rake");
|
||||
shelf.add("hose");
|
||||
console.log(shelf.describe());
|
||||
console.log(defaultShelf.describe());
|
||||
console.log(new Item("loose", null).placed(), Item.created, Shelf.count);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Item } from "./item.ts";
|
||||
|
||||
export class Shelf {
|
||||
static count = 0;
|
||||
items: Item[] = [];
|
||||
readonly label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
Shelf.count++;
|
||||
}
|
||||
add(name: string): Item {
|
||||
const item = new Item(name, this);
|
||||
this.items.push(item);
|
||||
return item;
|
||||
}
|
||||
describe(): string {
|
||||
return `${this.label}: ${this.items.map((i) => i.placed()).join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultShelf = new Shelf("default");
|
||||
defaultShelf.add("box");
|
||||
console.log("shelf ready", Item.created);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { validate } from "./rules.ts";
|
||||
|
||||
console.log("ledger body");
|
||||
export const total: number = validate([3, 4, -1]);
|
||||
console.log("ledger never finishes", total);
|
||||
@@ -0,0 +1,7 @@
|
||||
// @exit: 1
|
||||
// A module body of an import cycle throws during initialization: the
|
||||
// error propagates out of the whole graph, so the rest of the throwing
|
||||
// body, its importers and main never run (stdout and exit code match).
|
||||
import { total } from "./ledger.ts";
|
||||
|
||||
console.log("main never runs", total);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { total } from "./ledger.ts";
|
||||
|
||||
console.log("rules body");
|
||||
export function validate(entries: number[]): number {
|
||||
let sum = 0;
|
||||
for (const e of entries) {
|
||||
if (e < 0) throw new RangeError(`negative entry ${e}`);
|
||||
sum += e;
|
||||
}
|
||||
return sum + total;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { margin } from "./margin.ts";
|
||||
|
||||
console.log("frame body");
|
||||
export const width: number = margin * 2;
|
||||
@@ -0,0 +1,7 @@
|
||||
// @exit: 1
|
||||
// A cycle member's top level reads a const of the module that imported
|
||||
// it before that module's body ran: Node throws an uncaught
|
||||
// ReferenceError there, after the earlier output.
|
||||
import { width } from "./frame.ts";
|
||||
|
||||
console.log("main never runs", width);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { width } from "./frame.ts";
|
||||
|
||||
console.log("margin body");
|
||||
export const margin: number = width / 10;
|
||||
+2
-2
@@ -2,6 +2,6 @@ import { b } from "./b.ts";
|
||||
|
||||
export const a: string = "A";
|
||||
|
||||
export function useB(): string {
|
||||
return b;
|
||||
export function label(): string {
|
||||
return a + b;
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
import { a } from "./a.ts";
|
||||
|
||||
console.log("b body");
|
||||
export const b: string = a + "B";
|
||||
@@ -0,0 +1,14 @@
|
||||
// @dynamic
|
||||
// import() of a module whose cycle partner reads a const of it at top
|
||||
// level: the partner evaluates first, its read throws ReferenceError, and
|
||||
// the import() promise rejects with that error.
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const ns = await import("./a.ts");
|
||||
console.log("loaded", ns.label());
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.log("rejected:", err.name, err.message);
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,115 @@
|
||||
// Long-lived cyclic structures next to garbage of every age.
|
||||
//
|
||||
// Warehouses keep bins with parent back-pointers and a name index for the
|
||||
// whole run. Each restock builds scratch pick lists (doubly linked, pointing
|
||||
// into the bins) that live through many collector passes before they are
|
||||
// dropped, and every few rounds a whole warehouse is retired after it has
|
||||
// been live for a long time. Every dead structure must be reclaimed without
|
||||
// touching the live ones: the sanitized lane's RC audit checks that nothing
|
||||
// is left over at exit, and the checksums check that nothing live was freed.
|
||||
class Bin {
|
||||
label: string;
|
||||
parent: Bin | null = null;
|
||||
children: Bin[] = [];
|
||||
stock: number;
|
||||
constructor(label: string, stock: number) {
|
||||
this.label = label;
|
||||
this.stock = stock;
|
||||
}
|
||||
}
|
||||
|
||||
class Warehouse {
|
||||
name: string;
|
||||
root: Bin;
|
||||
index: Map<string, Bin> = new Map();
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.root = new Bin(`${name}/root`, 0);
|
||||
this.index.set(this.root.label, this.root);
|
||||
}
|
||||
add(parent: Bin, label: string, stock: number): Bin {
|
||||
const bin = new Bin(`${this.name}/${label}`, stock);
|
||||
bin.parent = parent;
|
||||
parent.children.push(bin);
|
||||
this.index.set(bin.label, bin);
|
||||
return bin;
|
||||
}
|
||||
}
|
||||
|
||||
class Pick {
|
||||
bin: Bin;
|
||||
qty: number;
|
||||
prev: Pick | null = null;
|
||||
next: Pick | null = null;
|
||||
constructor(bin: Bin, qty: number) {
|
||||
this.bin = bin;
|
||||
this.qty = qty;
|
||||
}
|
||||
}
|
||||
|
||||
function stockWarehouse(name: string, bins: number): Warehouse {
|
||||
const w = new Warehouse(name);
|
||||
const all: Bin[] = [w.root];
|
||||
for (let i = 1; i < bins; i++) all.push(w.add(all[(i - 1) >> 2], `b${i}`, (i * 7) % 13));
|
||||
return w;
|
||||
}
|
||||
|
||||
function pickList(w: Warehouse, round: number, length: number): Pick | null {
|
||||
let head: Pick | null = null;
|
||||
let tail: Pick | null = null;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const bin = w.index.get(`${w.name}/b${1 + ((i * 31 + round) % (w.index.size - 1))}`);
|
||||
if (bin === undefined) continue;
|
||||
const p = new Pick(bin, 1 + (i % 3));
|
||||
if (tail === null) head = p;
|
||||
else {
|
||||
tail.next = p;
|
||||
p.prev = tail;
|
||||
}
|
||||
tail = p;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
function depth(bin: Bin): number {
|
||||
let d = 0;
|
||||
for (let b: Bin | null = bin.parent; b !== null; b = b.parent) d++;
|
||||
return d;
|
||||
}
|
||||
|
||||
const warehouses: Warehouse[] = [];
|
||||
for (let i = 0; i < 4; i++) warehouses.push(stockWarehouse(`w${i}`, 1500));
|
||||
|
||||
let pending: (Pick | null)[] = [];
|
||||
let picked = 0;
|
||||
let retired = 0;
|
||||
for (let round = 0; round < 24; round++) {
|
||||
const w = warehouses[round % warehouses.length];
|
||||
pending.push(pickList(w, round, 400));
|
||||
// Lists wait a few rounds before they are fulfilled and dropped.
|
||||
if (pending.length > 3) {
|
||||
for (const head of pending) {
|
||||
for (let p = head; p !== null; p = p.next) picked += p.qty * (1 + depth(p.bin));
|
||||
}
|
||||
pending = [];
|
||||
}
|
||||
// Retire the oldest warehouse every eight rounds and open a fresh one.
|
||||
if (round % 8 === 7) {
|
||||
const gone = warehouses.shift();
|
||||
if (gone !== undefined) retired += gone.index.size;
|
||||
warehouses.push(stockWarehouse(`w${4 + round}`, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
let bins = 0;
|
||||
let stock = 0;
|
||||
for (const w of warehouses) {
|
||||
bins += w.index.size;
|
||||
for (const bin of w.index.values()) {
|
||||
stock += bin.stock;
|
||||
if (bin.parent !== null && !bin.parent.children.includes(bin)) console.log("broken link", bin.label);
|
||||
}
|
||||
}
|
||||
console.log(`warehouses ${warehouses.map((w) => w.name).join(",")}`);
|
||||
console.log(`bins ${bins} stock ${stock} retired ${retired}`);
|
||||
console.log(`picked ${picked} pending ${pending.length}`);
|
||||
@@ -0,0 +1,148 @@
|
||||
// Map, Set and array iterators returned through methods annotated
|
||||
// IterableIterator<T> or Iterator<T>, forwarded through further methods
|
||||
// and functions, and stored in variables. Iteration stays lazy and live:
|
||||
// entries added while a loop runs are visited, and an exhausted iterator
|
||||
// stays exhausted.
|
||||
|
||||
interface Slot {
|
||||
n: number;
|
||||
}
|
||||
|
||||
class Inventory {
|
||||
private readonly counts = new Map<string, number>();
|
||||
private readonly tags = new Set<string>();
|
||||
readonly slots = new Map<string, Slot>();
|
||||
readonly history: string[] = [];
|
||||
add(name: string, n: number): void {
|
||||
this.counts.set(name, n);
|
||||
this.tags.add(name.toUpperCase());
|
||||
this.history.push(name);
|
||||
}
|
||||
entries(): IterableIterator<[string, number]> {
|
||||
return this.counts.entries();
|
||||
}
|
||||
names(): IterableIterator<string> {
|
||||
return this.counts.keys();
|
||||
}
|
||||
amounts(): Iterator<number> {
|
||||
return this.counts.values();
|
||||
}
|
||||
tagList(): IterableIterator<string> {
|
||||
return this.tags.values();
|
||||
}
|
||||
recent(): IterableIterator<string> {
|
||||
return this.history.values();
|
||||
}
|
||||
positions(): IterableIterator<[number, string]> {
|
||||
return this.history.entries();
|
||||
}
|
||||
slotEntries(): IterableIterator<[string, Slot]> {
|
||||
return this.slots.entries();
|
||||
}
|
||||
}
|
||||
|
||||
class Store {
|
||||
readonly stock = new Inventory();
|
||||
entries(): IterableIterator<[string, number]> {
|
||||
return this.stock.entries();
|
||||
}
|
||||
}
|
||||
|
||||
function forward(store: Store): IterableIterator<[string, number]> {
|
||||
return store.entries();
|
||||
}
|
||||
|
||||
function* steps(): Generator<number, void, undefined> {
|
||||
yield 1;
|
||||
yield 2;
|
||||
}
|
||||
function stepsAsIterator(): IterableIterator<number> {
|
||||
return steps();
|
||||
}
|
||||
|
||||
const store = new Store();
|
||||
store.stock.add("bolt", 3);
|
||||
store.stock.add("nut", 5);
|
||||
|
||||
for (const [name, n] of forward(store)) console.log("entry", name, n);
|
||||
|
||||
let grew = false;
|
||||
for (const name of store.stock.names()) {
|
||||
console.log("name", name);
|
||||
if (!grew) {
|
||||
store.stock.add("washer", 9);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
|
||||
const amounts = store.stock.amounts();
|
||||
let step = amounts.next();
|
||||
while (!step.done) {
|
||||
console.log("amount", step.value);
|
||||
step = amounts.next();
|
||||
}
|
||||
console.log("after", amounts.next().done);
|
||||
|
||||
const stored: IterableIterator<[string, number]> = store.entries();
|
||||
for (const [name] of stored) {
|
||||
console.log("first only", name);
|
||||
break;
|
||||
}
|
||||
for (const [name] of stored) console.log("rest", name);
|
||||
|
||||
console.log("spread", [...store.stock.tagList()]);
|
||||
console.log("from", Array.from(store.stock.names()));
|
||||
console.log("positions", [...store.stock.positions()]);
|
||||
|
||||
let total = 0;
|
||||
for (const v of store.stock.recent()) {
|
||||
total += v.length;
|
||||
if (v === "bolt") store.stock.history.push("pin");
|
||||
}
|
||||
console.log("total", total, store.stock.history.length);
|
||||
|
||||
store.stock.slots.set("a", { n: 1 });
|
||||
for (const [, slot] of store.stock.slotEntries()) slot.n += 10;
|
||||
console.log("slot", store.stock.slots.get("a")!.n);
|
||||
|
||||
const counted = stepsAsIterator();
|
||||
console.log("generator", counted.next().value, [...counted]);
|
||||
|
||||
const empty = new Inventory();
|
||||
console.log("empty", [...empty.entries()], empty.amounts().next().done);
|
||||
|
||||
// Leaving a loop early closes a generator (its finally block runs) but
|
||||
// leaves a native iterator open, so a later loop resumes where it stopped.
|
||||
function* guarded(): Generator<number, void, undefined> {
|
||||
try {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
} finally {
|
||||
console.log("guarded closed");
|
||||
}
|
||||
}
|
||||
function guardedSteps(): IterableIterator<number> {
|
||||
return guarded();
|
||||
}
|
||||
const closing = guardedSteps();
|
||||
for (const n of closing) {
|
||||
console.log("guarded", n);
|
||||
if (n === 2) break;
|
||||
}
|
||||
console.log("guarded after", closing.next().done);
|
||||
|
||||
const resumable = store.entries();
|
||||
for (const [name] of resumable) {
|
||||
console.log("before throw", name);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
for (const [name] of resumable) {
|
||||
console.log("throwing at", name);
|
||||
throw new Error("stop");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log("caught", e.message);
|
||||
}
|
||||
for (const [name] of resumable) console.log("resumed", name);
|
||||
@@ -0,0 +1,102 @@
|
||||
// A call to a function returning `never` can stand wherever a value is
|
||||
// expected: it still runs (and throws), and the surrounding expression
|
||||
// never needs its value. Each case runs both its normal path and its
|
||||
// throwing path.
|
||||
|
||||
class Point {
|
||||
x: number;
|
||||
constructor(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
throw new Error(message);
|
||||
}
|
||||
function rangeFail(): never {
|
||||
throw new RangeError("out of range");
|
||||
}
|
||||
|
||||
function lengthOf(text: string): number {
|
||||
if (text.length > 0) return text.length;
|
||||
return fail("empty input");
|
||||
}
|
||||
function checked(text: string): number {
|
||||
const n: number = text.length > 0 ? text.length : fail("no length");
|
||||
return n;
|
||||
}
|
||||
function labelOf(value: string | undefined): string {
|
||||
return value ?? fail("missing label");
|
||||
}
|
||||
function pointOf(value: Point | undefined): Point {
|
||||
return value ?? fail("missing point");
|
||||
}
|
||||
const makePoint = (ok: boolean): Point => (ok ? new Point(3) : rangeFail());
|
||||
const count: () => number = () => fail("no count");
|
||||
function listOf(ok: boolean): string[] {
|
||||
if (ok) return ["a"];
|
||||
const items: string[] = fail("no list");
|
||||
return items;
|
||||
}
|
||||
function maybe(flag: boolean): number | undefined {
|
||||
if (flag) return 1;
|
||||
return fail("no maybe");
|
||||
}
|
||||
function twice(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
function same<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
class Reader {
|
||||
pos = 0;
|
||||
error(message: string): never {
|
||||
throw new SyntaxError(message + " at " + this.pos);
|
||||
}
|
||||
digit(c: string): number {
|
||||
return c >= "0" && c <= "9" ? c.charCodeAt(0) - 48 : this.error("bad digit " + c);
|
||||
}
|
||||
}
|
||||
|
||||
function attempt(label: string, run: () => unknown): void {
|
||||
try {
|
||||
console.log(label, run());
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log(label, "caught", e.name, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
attempt("length", () => lengthOf("abc"));
|
||||
attempt("length empty", () => lengthOf(""));
|
||||
attempt("checked", () => checked("ab"));
|
||||
attempt("checked empty", () => checked(""));
|
||||
attempt("label", () => labelOf("tag"));
|
||||
attempt("label missing", () => labelOf(undefined));
|
||||
attempt("point", () => pointOf(new Point(5)).x);
|
||||
attempt("point missing", () => pointOf(undefined).x);
|
||||
attempt("arrow", () => makePoint(true).x);
|
||||
attempt("arrow fails", () => makePoint(false).x);
|
||||
attempt("count", () => count());
|
||||
attempt("list", () => listOf(true).length);
|
||||
attempt("list fails", () => listOf(false).length);
|
||||
attempt("maybe", () => maybe(true));
|
||||
attempt("maybe fails", () => maybe(false));
|
||||
attempt("argument", () => twice(fail("as argument")));
|
||||
let total = 0;
|
||||
attempt("generic", () => same<number>(total > 0 ? total : fail("generic")));
|
||||
attempt("assigned", () => {
|
||||
total = fail("assigned");
|
||||
return total;
|
||||
});
|
||||
attempt("digit", () => new Reader().digit("7"));
|
||||
attempt("digit fails", () => new Reader().digit("q"));
|
||||
|
||||
async function load(ok: boolean): Promise<number> {
|
||||
if (ok) return 7;
|
||||
return fail("async failure");
|
||||
}
|
||||
load(true).then((v) => console.log("async", v));
|
||||
load(false).catch((e) => {
|
||||
if (e instanceof Error) console.log("async caught", e.message);
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
// Unchecked element reads are undefined past the end of an array even
|
||||
// though the element type omits undefined. Slots whose types admit
|
||||
// undefined receive that value as their undefined state; present
|
||||
// elements keep their ordinary values.
|
||||
|
||||
function attempt(label: string, run: () => unknown): void {
|
||||
try {
|
||||
console.log(label, run());
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.log(label, "threw", e.name);
|
||||
}
|
||||
}
|
||||
|
||||
const noNames: string[] = [];
|
||||
const names = ["ada", "bo"];
|
||||
const scores = [7];
|
||||
|
||||
function firstName(preferred: string | undefined, list: string[]): string | undefined {
|
||||
return preferred ?? list[0];
|
||||
}
|
||||
function thirdScore(list: number[]): number | undefined {
|
||||
return list[2];
|
||||
}
|
||||
function describe(value: string | undefined): string {
|
||||
return value === undefined ? "none" : value;
|
||||
}
|
||||
function pickOr(flag: boolean, list: string[]): string | undefined {
|
||||
return flag ? list[0] : "fixed";
|
||||
}
|
||||
function eitherName(list: string[]): string | undefined {
|
||||
return list[1] || list[0];
|
||||
}
|
||||
function stored(preferred: string | undefined, list: string[]): string | undefined {
|
||||
const chosen = preferred ?? list[0];
|
||||
return chosen;
|
||||
}
|
||||
function fromPattern(preferred: string | undefined, list: string[]): string | undefined {
|
||||
const [head] = list;
|
||||
return preferred ?? head;
|
||||
}
|
||||
function laterRead(preferred: string | undefined, list: string[]): () => string | undefined {
|
||||
return () => preferred ?? list[0];
|
||||
}
|
||||
function orFallback<T>(value: T | undefined, fallback: T): T {
|
||||
return value === undefined ? fallback : value;
|
||||
}
|
||||
|
||||
class Shelf {
|
||||
items: string[] = [];
|
||||
label: string | undefined;
|
||||
top(): string | undefined {
|
||||
return this.label ?? this.items[this.items.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
attempt("first empty", () => firstName(undefined, noNames));
|
||||
attempt("first named", () => firstName(undefined, names));
|
||||
attempt("first preferred", () => firstName("cy", noNames));
|
||||
attempt("third", () => thirdScore(scores));
|
||||
attempt("describe", () => describe(noNames[0]));
|
||||
attempt("describe present", () => describe(names[1]));
|
||||
attempt("ternary", () => pickOr(true, noNames));
|
||||
attempt("or", () => eitherName(noNames));
|
||||
attempt("or present", () => eitherName(names));
|
||||
attempt("stored", () => stored(undefined, noNames));
|
||||
attempt("pattern", () => fromPattern(undefined, noNames));
|
||||
attempt("pattern present", () => fromPattern(undefined, names));
|
||||
attempt("capture", () => laterRead(undefined, noNames)());
|
||||
attempt("generic", () => orFallback(noNames[0] ?? undefined, "fallback"));
|
||||
attempt("explicit generic", () => orFallback<string | undefined>(noNames[3], "unused"));
|
||||
attempt("length", () => (firstName(undefined, names) ?? "").length);
|
||||
let missing: string | undefined;
|
||||
attempt("concat", () => (missing ?? noNames[0]) + "!");
|
||||
attempt("template", () => `${firstName(undefined, noNames)}`);
|
||||
|
||||
const shelf = new Shelf();
|
||||
attempt("shelf empty", () => shelf.top());
|
||||
shelf.items.push("box", "jar");
|
||||
attempt("shelf top", () => shelf.top());
|
||||
shelf.label = "label";
|
||||
attempt("shelf label", () => shelf.top());
|
||||
|
||||
// A member read needs the present value: undefined still throws there.
|
||||
attempt("member", () => (missing ?? noNames[0]).length);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Pet } from "./pets.ts";
|
||||
export class Dog extends Pet {}
|
||||
export function makeDog(name: string): Pet {
|
||||
return new Dog(name);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { first } from "./pets.ts";
|
||||
console.log(first.speak());
|
||||
@@ -0,0 +1,11 @@
|
||||
import { makeDog } from "./dogs.ts";
|
||||
export class Pet {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
speak(): string {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
export const first = makeDog("rex");
|
||||
@@ -0,0 +1,2 @@
|
||||
import { scale } from "./b.ts";
|
||||
export default scale(4);
|
||||
@@ -0,0 +1,7 @@
|
||||
import size from "./a.ts";
|
||||
export function scale(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
export function current(): number {
|
||||
return size;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import size from "./a.ts";
|
||||
console.log(size);
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as helpers from "./b.ts";
|
||||
export const base = 2;
|
||||
export const total: number = helpers.double(base);
|
||||
@@ -0,0 +1,4 @@
|
||||
import * as a from "./a.ts";
|
||||
export function double(n: number): number {
|
||||
return n * a.base;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { total } from "./a.ts";
|
||||
console.log(total);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { b } from "./b.ts";
|
||||
export const a: string = "A";
|
||||
export function useB(): string {
|
||||
return b;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { a } from "./a.ts";
|
||||
export const b: string = a + "B";
|
||||
@@ -1,2 +0,0 @@
|
||||
import { useB } from "./a.ts";
|
||||
console.log(useB());
|
||||
@@ -0,0 +1,4 @@
|
||||
import { read } from "./b.ts";
|
||||
console.log(read());
|
||||
export var level = 3;
|
||||
export { read };
|
||||
@@ -0,0 +1,4 @@
|
||||
import { level } from "./a.ts";
|
||||
export function read(): number {
|
||||
return level;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { read } from "./a.ts";
|
||||
console.log(read());
|
||||
@@ -1,5 +0,0 @@
|
||||
import { bee } from "./b.ts";
|
||||
export function aye(n: number): number {
|
||||
return n <= 0 ? 0 : bee(n - 1) + 1;
|
||||
}
|
||||
console.log("a-init", aye(2));
|
||||
@@ -1,4 +0,0 @@
|
||||
import { aye } from "./a.ts";
|
||||
export function bee(n: number): number {
|
||||
return n <= 0 ? 0 : aye(n - 1) + 1;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { aye } from "./a.ts";
|
||||
console.log(aye(5));
|
||||
@@ -1,2 +0,0 @@
|
||||
import { b } from "./b.ts";
|
||||
export const a: number = b + 1;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { a } from "./a.ts";
|
||||
export const b: number = a + 1;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { a } from "./a.ts";
|
||||
console.log(a);
|
||||
@@ -1,12 +0,0 @@
|
||||
// @dynamic
|
||||
// The REFUSED shape through the --dynamic subgraph: the cycle behind this
|
||||
// import() reads a cycle-crossing binding at a module's top level, which
|
||||
// in Node observes the partially-initialized exporter (TDZ
|
||||
// ReferenceError). The dynamic-subgraph walk fences it with the same
|
||||
// narrowed SC1016 preflight's static walk mints, naming the binding and
|
||||
// the offending line.
|
||||
async function main(): Promise<void> {
|
||||
const ns = await import("./a.ts");
|
||||
console.log(ns.useB());
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,21 @@
|
||||
// Iterator-typed slots hold generators and built-in collection iterators.
|
||||
// A class implementing the iterator protocol stays a pointed fence there,
|
||||
// and Iterable<T> annotations have no lowering yet.
|
||||
|
||||
class Countdown implements Iterator<number> {
|
||||
n = 2;
|
||||
next(): IteratorResult<number> {
|
||||
return this.n > 0 ? { value: this.n--, done: false } : { value: undefined, done: true };
|
||||
}
|
||||
}
|
||||
function firstOf(it: Iterator<number>): void {
|
||||
console.log(it.next().value);
|
||||
}
|
||||
firstOf(new Countdown());
|
||||
|
||||
function total(values: Iterable<number>): number {
|
||||
let sum = 0;
|
||||
for (const v of values) sum += v;
|
||||
return sum;
|
||||
}
|
||||
console.log(total([1, 2]));
|
||||
@@ -38,7 +38,7 @@ const sinhMethod = Math.sinh; // fixed-arity Math function values now lower
|
||||
// (n-ary Math.min/max LOWER now — the variadic battery lives in the
|
||||
// corpus; the mixed spread/positional list is the form that stays fenced.)
|
||||
const clamped = Math.min(1, ...[2, 3]);
|
||||
const entries = [1, 2].entries();
|
||||
const entries = [1, 2].entries(); // array iterators lower now
|
||||
const arrAt = [1, 2].at(0);
|
||||
const norm = "abc".normalize();
|
||||
const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower
|
||||
|
||||
@@ -4,4 +4,4 @@ cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a
|
||||
| ^
|
||||
2 |
|
||||
|
||||
hint: cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window
|
||||
hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
|
||||
@@ -0,0 +1,8 @@
|
||||
cycle-class-early/dogs.ts:2:26 - error SC1016: circular imports (the class 'Pet' declared in cycle-class-early/pets.ts can be accessed before its declaration runs) are not supported yet
|
||||
|
||||
1 | import { Pet } from "./pets.ts";
|
||||
2 | export class Dog extends Pet {}
|
||||
| ^~~
|
||||
3 | export function makeDog(name: string): Pet {
|
||||
|
||||
hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
|
||||
@@ -0,0 +1,7 @@
|
||||
cycle-default-export/a.ts:1:1 - error SC1016: circular imports (cycle-default-export/a.ts → cycle-default-export/b.ts → cycle-default-export/a.ts; the default export expression at cycle-default-export/a.ts:2 has no initialization state inside the cycle) are not supported yet
|
||||
|
||||
1 | import { scale } from "./b.ts";
|
||||
| ^
|
||||
2 | export default scale(4);
|
||||
|
||||
hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
|
||||
@@ -0,0 +1,7 @@
|
||||
cycle-namespace-import/a.ts:1:1 - error SC1016: circular imports (cycle-namespace-import/a.ts → cycle-namespace-import/b.ts → cycle-namespace-import/a.ts; the namespace import 'helpers' at cycle-namespace-import/a.ts:1 names a module of the cycle) are not supported yet
|
||||
|
||||
1 | import * as helpers from "./b.ts";
|
||||
| ^
|
||||
2 | export const base = 2;
|
||||
|
||||
hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
|
||||
@@ -1,7 +0,0 @@
|
||||
cycle-tdz-read/a.ts:1:1 - error SC1016: circular imports (cycle-tdz-read/a.ts → cycle-tdz-read/b.ts → cycle-tdz-read/a.ts; the cycle-crossing binding 'a' is read at cycle-tdz-read/b.ts:2, outside any function body — a read during the init window observes the partially-initialized module (Node's TDZ ReferenceError / stale var), which is not modeled) are not supported yet
|
||||
|
||||
1 | import { b } from "./b.ts";
|
||||
| ^
|
||||
2 | export const a: string = "A";
|
||||
|
||||
hint: cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window
|
||||
@@ -0,0 +1,8 @@
|
||||
cycle-var-early/b.ts:3:10 - error SC1016: circular imports (the var binding 'level' declared in cycle-var-early/a.ts can be accessed before its declaration runs, where Node reads undefined) are not supported yet
|
||||
|
||||
2 | export function read(): number {
|
||||
3 | return level;
|
||||
| ^~~~~
|
||||
4 | }
|
||||
|
||||
hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
|
||||
@@ -1,7 +0,0 @@
|
||||
cycle-window-call/a.ts:1:1 - error SC1016: circular imports (cycle-window-call/a.ts → cycle-window-call/b.ts → cycle-window-call/a.ts; top-level code at cycle-window-call/a.ts:5 can run user code during the cycle's init window — only declaration-only module bodies are admitted) are not supported yet
|
||||
|
||||
1 | import { bee } from "./b.ts";
|
||||
| ^
|
||||
2 | export function aye(n: number): number {
|
||||
|
||||
hint: cycles of ES modules with declaration-only top levels whose cycle-crossing bindings are only used inside function bodies compile as-is; move the named top-level read or call into a function body (or break the named edge) so nothing runs during the cycle's init window
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user