Check cycle bindings only where early code can reach the reading function (#1882)

This commit is contained in:
Chris Tate
2026-10-23 10:02:29 -05:00
committed by GitHub
22 changed files with 590 additions and 66 deletions
+48 -3
View File
@@ -371,15 +371,60 @@ test("repeated cycle edges reuse a file's binding index", () => {
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.
// A shared instance built by a user constructor that calls the
// partner's function: its read of the instance can run early.
{
"main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);',
"a.ts":
'import { f } from "./b.ts"; export class C { n = 1; constructor() { console.log(f()); } } export const shared = new C();',
"b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }',
},
["shared"],
],
[
// User code runs while the instance is built, but nothing it reaches
// calls the partner's function before the declaration completed.
{
"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 runs first and calls a hoisted function of
// the importer that reads a const declared later; a second function
// reads another const but nothing calls it early.
{
"main.ts": 'import { late } from "./a.ts"; console.log(late());',
"a.ts":
'import { probe } from "./b.ts"; export function read(): string { return name; } export function late(): string { return other + probe; } export const name = "n"; export const other = "o";',
"b.ts":
'import { read } from "./a.ts"; function attempt(): string { try { return read(); } catch { return "tdz"; } } export const probe = attempt();',
},
["name"],
],
[
// The partner's class calls a method through `this` in a static
// initializer; the method reads a const of the importer declared later.
{
"main.ts": 'import { name } from "./a.ts"; console.log(name);',
"a.ts": 'import { Probe } from "./b.ts"; export const name = "n"; console.log(Probe.value);',
"b.ts":
'import { name } from "./a.ts"; export class Probe { static read(): string { return name; } static value = this.read(); }',
},
["name"],
],
[
// The same through a static block of a class stored by a const.
{
"main.ts": 'import { name } from "./a.ts"; console.log(name);',
"a.ts": 'import { Box } from "./b.ts"; export const name = "n"; console.log(Box.value);',
"b.ts":
'import { name } from "./a.ts"; export const Box = class { static value = ""; static read(): string { return name; } static { this.value = this.read(); } };',
},
["name"],
],
[
// The partner's top level reads the importer's const before it ran.
+345 -63
View File
@@ -3903,22 +3903,7 @@ function computeCycleEarlyBindings7(
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;
localNames: Map<string, string>;
}
const candidates = new Map<ts.Symbol, Candidate>();
const candidates = new Map<ts.Symbol, CycleCandidate7>();
const names = new Set<string>();
let dirty = !known;
evaluation.forEach((m, rank) => {
@@ -3955,6 +3940,7 @@ function computeCycleEarlyBindings7(
start: ts.tokenStartOfNode(stmt),
end: stmt.end,
reference: null,
deferredReference: null,
folded: null,
localNames: new Map(),
});
@@ -4023,15 +4009,27 @@ function computeCycleEarlyBindings7(
ref.end <= c.end
)
continue;
if (inDeferredPosition7(ref)) {
if (c.exposed && c.deferredReference === null) c.deferredReference = ref;
continue;
}
let early: boolean;
if (inDeferredPosition7(ref)) early = c.exposed;
else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
if (sf === c.module) early = ts.tokenStartOfNode(ref) < 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;
}
}
// With a static evaluation order, a deferred reference only runs early
// when the code that runs before the declaration completes can reach
// the function holding it.
const reached =
known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference)
? cycleReachedDeferredReferences7(program, evaluation, candidates)
: null;
for (const c of candidates.values()) {
if (c.reference === null && c.deferredReference !== null)
c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null);
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);
@@ -4050,6 +4048,299 @@ function computeCycleEarlyBindings7(
return out;
}
/** Collects into `units` the callables an initializer expression stores
* (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a
* stored node that run where it is evaluated. */
function storedCallables7(
checker: ts.TypeChecker,
e: ts.Expression,
units: ts.Node[],
immediate: Map<ts.Node, ts.Node[]>,
): void {
while (
ts.isParenthesizedExpression(e) ||
ts.isAsExpression(e) ||
ts.isSatisfiesExpression(e) ||
ts.isNonNullExpression(e)
)
e = e.expression;
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
units.push(e);
} else if (ts.isClassExpression(e)) {
units.push(e);
immediate.set(e, classSyncParts7(e));
} else if (ts.isObjectLiteralExpression(e)) {
for (const p of e.properties.nodes) {
if (
ts.isMethodDeclaration(p) ||
ts.isGetAccessorDeclaration(p) ||
ts.isSetAccessorDeclaration(p)
) {
// A computed key evaluates where the literal does; a plain
// member-chain key (`[Equal.symbol]`) runs no user code.
if (!ts.isComputedPropertyName(p.name)) units.push(p);
else if (memberChainKey7(p.name.expression)) {
units.push(p);
immediate.set(p, [p.name]);
}
} else if (ts.isPropertyAssignment(p))
storedCallables7(checker, p.initializer, units, immediate);
}
} else if (ts.isArrayLiteralExpression(e)) {
for (const el of e.elements.nodes)
if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate);
} else if (ts.isCallExpression(e)) {
// `const f = wrap(callback)` where wrap only returns closures over
// the callback: it runs when the stored result is called.
e.arguments.nodes.forEach((arg, index) => {
let fn: ts.Expression = arg;
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
if (
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
returnsOnlyClosuresOver7(checker, e.expression, index)
)
units.push(fn);
});
}
}
/** A binding of an import cycle and what the reference scan found. */
interface CycleCandidate7 {
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;
/** A reference read at top level before the declaration ran. */
reference: ts.Identifier | null;
/** A deferred reference (function body, parameter default, instance
* field) when user code may run before the declaration completes. */
deferredReference: ts.Identifier | null;
folded: ts.Identifier | null;
localNames: Map<string, string>;
}
/** For one import cycle with a static evaluation order: the first deferred
* reference to each candidate that code running before the candidate's
* declaration completes can reach, or null when the walk cannot be precise
* (top-level await, a default-exported function or class declaration).
*
* The cycle's top-level code runs in evaluation order. A stored callable
* (a function or class declaration, or a function, class or object-literal
* method stored by a variable initializer) becomes reachable once
* reachable code names its binding, through an import alias or a
* namespace member too; every other function-like in reachable code counts
* as reachable, since it may be handed to a callee. A namespace import of
* a cycle member used other than for a member read makes every stored
* callable reachable. moduleEarlyBindingsOf7 is the single-module form. */
function cycleReachedDeferredReferences7(
program: ts.Program,
evaluation: readonly ts.SourceFile[],
candidates: ReadonlyMap<ts.Symbol, CycleCandidate7>,
): Map<ts.Symbol, ts.Identifier> | null {
const checker = program.getTypeChecker();
const members = new Set(evaluation);
for (const sf of evaluation) {
if (hasTopLevelAwait7(sf)) return null;
for (const stmt of sf.statements.nodes) {
if (
(ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
ts.ModifierFlags.Default) !==
0
)
return null;
}
}
const stored = new Set<ts.Node>();
const immediate = new Map<ts.Node, ts.Node[]>();
const storedNames: ts.Identifier[] = [];
const unitsOfName = new Map<ts.Identifier, ts.Node[]>();
const store = (name: ts.Identifier, units: ts.Node[]): void => {
if (units.length === 0) return;
for (const u of units) stored.add(u);
storedNames.push(name);
unitsOfName.set(name, units);
};
for (const sf of evaluation) {
for (const stmt of sf.statements.nodes) {
if (
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
ts.ModifierFlags.Ambient) !==
0
)
continue;
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
store(stmt.name, [stmt]);
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
store(stmt.name, [stmt]);
} else if (ts.isVariableStatement(stmt)) {
for (const d of stmt.declarationList.declarations.nodes) {
if (ts.isIdentifier(d.name) && d.initializer !== undefined) {
const units: ts.Node[] = [];
storedCallables7(checker, d.initializer, units, immediate);
store(d.name, units);
}
}
}
}
}
const unitsBySymbol = new Map<ts.Symbol, ts.Node[]>();
const allUnits: ts.Node[] = [];
for (const name of storedNames) {
const symbol = checker.getSymbolAtLocation(name);
if (symbol === undefined) continue;
const units = unitsOfName.get(name)!;
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]);
allUnits.push(...units);
}
// A named class expression's inner name (`class Impl { static x =
// Impl.m(); }`) names the class inside its own body, as the binding it
// is stored in names it outside.
const innerNames: ts.Identifier[] = [];
for (const unit of allUnits) {
if (!ts.isClassExpression(unit) || unit.name === undefined) continue;
const symbol = checker.getSymbolAtLocation(unit.name);
if (symbol === undefined) continue;
innerNames.push(unit.name);
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]);
}
// The identifier texts worth resolving: declared names, and the local
// names of imports (named, default and namespace) in cycle members.
const names = new Set<string>();
for (const c of candidates.values()) names.add(c.name.text);
for (const name of storedNames) names.add(name.text);
for (const name of innerNames) names.add(name.text);
for (const sf of evaluation) {
for (const stmt of sf.statements.nodes) {
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue;
const clause = stmt.importClause;
if (clause.name !== undefined) names.add(clause.name.text);
const bindings = clause.namedBindings;
if (bindings === undefined) continue;
if (ts.isNamedImports(bindings))
for (const element of bindings.elements.nodes) names.add(element.name.text);
else names.add(bindings.name.text);
}
}
const idents: ts.Identifier[] = [];
for (const sf of evaluation) {
ts.walkPreorder(sf, (node) => {
if (ts.isImportDeclaration(node)) return "skip";
if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node);
return undefined;
});
}
checker.prefetchSymbolNodesExact(idents);
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
const parent = id.parent;
let symbol = checker.getSymbolAtLocation(id);
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
}
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol);
}
return symbol;
};
const isMemberNamespace = (symbol: ts.Symbol): boolean =>
(symbol.flags & ts.SymbolFlags.ValueModule) !== 0 &&
symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d));
const declared = new Set<ts.Symbol>();
const reachedUnits = new Set<ts.Node>();
const out = new Map<ts.Symbol, ts.Identifier>();
const work: ts.Node[] = [];
const reach = (unit: ts.Node): void => {
if (reachedUnits.has(unit)) return;
reachedUnits.add(unit);
work.push(unit);
};
const visit = (root: ts.Node): void => {
ts.walkPreorder(root, (node) => {
if (node !== root && stored.has(node)) {
for (const part of immediate.get(node) ?? []) visit(part);
return "skip";
}
if (ts.isImportDeclaration(node)) return "skip";
if (node.kind === ts.SyntaxKind.ThisKeyword) {
// `this` in a static initializer or block names the class, so it
// reaches the class's methods as naming the class does.
const owner = staticThisClass7(node);
if (owner !== undefined && stored.has(owner)) reach(owner);
return undefined;
}
if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
if (inTypePosition7(node)) return undefined;
const parent = node.parent;
if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined;
if (parent !== undefined && declaredName7(parent) === node) return undefined;
const symbol = symbolOf(node);
if (symbol === undefined) return undefined;
if (isMemberNamespace(symbol)) {
// `ns.member` resolves through the member's own identifier; any
// other use can reach every export.
if (
!(
parent !== undefined &&
ts.isPropertyAccessExpression(parent) &&
parent.expression === node
)
)
for (const unit of allUnits) reach(unit);
return undefined;
}
const c = candidates.get(symbol);
if (
c !== undefined &&
node !== c.name &&
!declared.has(symbol) &&
!out.has(symbol) &&
inDeferredPosition7(node) &&
!(
c.kind === "class" &&
ts.getSourceFileOfNode(node) === c.module &&
ts.tokenStartOfNode(node) >= c.start &&
node.end <= c.end
)
)
out.set(symbol, node);
for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit);
return undefined;
});
};
const drain = (): void => {
while (work.length > 0) visit(work.pop()!);
};
const declare = (name: ts.BindingName): void => {
for (const id of boundNames7(name)) {
const symbol = checker.getSymbolAtLocation(id);
if (symbol !== undefined) declared.add(symbol);
}
};
for (const sf of evaluation) {
for (const stmt of sf.statements.nodes) {
if (ts.isClassDeclaration(stmt)) {
for (const part of classSyncParts7(stmt)) visit(part);
drain();
if (stmt.name !== undefined) declare(stmt.name);
} else if (ts.isVariableStatement(stmt)) {
for (const d of stmt.declarationList.declarations.nodes) {
visit(d);
drain();
declare(d.name);
}
} else if (!ts.isFunctionDeclaration(stmt)) visit(stmt);
drain();
}
}
return out;
}
/** Top-level let/const and class bindings, in modules outside the
* exactly-initialized cycles, that code running before their declaration
* completes can read.
@@ -4114,51 +4405,8 @@ function moduleEarlyBindingsOf7(
storedBy.set(name.text, list);
};
const checker = program.getTypeChecker();
const storedIn = (e: ts.Expression, units: ts.Node[]): void => {
while (
ts.isParenthesizedExpression(e) ||
ts.isAsExpression(e) ||
ts.isSatisfiesExpression(e) ||
ts.isNonNullExpression(e)
)
e = e.expression;
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
units.push(e);
} else if (ts.isClassExpression(e)) {
units.push(e);
immediate.set(e, classSyncParts7(e));
} else if (ts.isObjectLiteralExpression(e)) {
for (const p of e.properties.nodes) {
if (
ts.isMethodDeclaration(p) ||
ts.isGetAccessorDeclaration(p) ||
ts.isSetAccessorDeclaration(p)
) {
// A computed key evaluates where the literal does; a plain
// member-chain key (`[Equal.symbol]`) runs no user code.
if (!ts.isComputedPropertyName(p.name)) units.push(p);
else if (memberChainKey7(p.name.expression)) {
units.push(p);
immediate.set(p, [p.name]);
}
} else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units);
}
} else if (ts.isArrayLiteralExpression(e)) {
for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units);
} else if (ts.isCallExpression(e)) {
// `const f = wrap(callback)` where wrap only returns closures over
// the callback: it runs when the stored result is called.
e.arguments.nodes.forEach((arg, index) => {
let fn: ts.Expression = arg;
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
if (
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
returnsOnlyClosuresOver7(checker, e.expression, index)
)
units.push(fn);
});
}
};
const storedIn = (e: ts.Expression, units: ts.Node[]): void =>
storedCallables7(checker, e, units, immediate);
sf.statements.nodes.forEach((stmt, index) => {
if (ambient(stmt)) return;
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
@@ -4411,6 +4659,40 @@ function returnsOnlyClosuresOver7(
* expressions, decorators, computed member keys, static field
* initializers and static blocks. Methods and instance fields wait for a
* call or a construction. */
/** The class whose static field initializer or static block evaluates `this`
* (through arrow functions, which keep the enclosing `this`), or undefined
* when another function or a class's instance side binds it. */
function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined {
let child = node;
for (let n = node.parent; n !== undefined; child = n, n = n.parent) {
if (ts.isArrowFunction(n)) continue;
if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration;
if (ts.isPropertyDeclaration(n)) {
if (n.initializer !== child) return undefined;
return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0
? (n.parent as ts.ClassLikeDeclaration)
: undefined;
}
if (ts.isComputedPropertyName(n)) {
// A computed key evaluates in the scope around the class.
const member = n.parent;
const cls = member?.parent;
if (cls === undefined) return undefined;
child = cls;
n = cls;
continue;
}
if (
ts.isFunctionLike(n) ||
ts.isClassDeclaration(n) ||
ts.isClassExpression(n) ||
ts.isSourceFile(n)
)
return undefined;
}
return undefined;
}
function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] {
const decorators = (n: ts.Node): ts.Node[] =>
((n as { modifiers?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []).filter(
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/util.ts",
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/core.ts",
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts",
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts",
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/7382-cycle-static-this-tdz/beta.ts",
"<repo>/tests/corpus/7382-cycle-static-this-tdz/alpha.ts",
"<repo>/tests/corpus/7382-cycle-static-this-tdz/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts",
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts",
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts"
],
"diags": []
}
@@ -0,0 +1,5 @@
import { describe, kind, mark } from "./util.ts";
export const version = 3;
export const marked = mark({});
console.log("core body", kind(), describe());
@@ -0,0 +1,10 @@
// A cycle member runs user code before it declares a constant that only
// functions read, one of them as a computed key and one under `typeof`
// (both resolved from the constant's literal type). Nothing calls those
// functions before the declaration ran, so they read the constant
// normally once the other member's top level calls them.
import { marked } from "./core.ts";
import { describe, kind, mark } from "./util.ts";
console.log(JSON.stringify(marked), kind());
console.log(JSON.stringify(mark({ seen: false })), describe());
@@ -0,0 +1,23 @@
import { version } from "./core.ts";
export function makeCounter(): () => number {
let n = 0;
return () => ++n;
}
const next = makeCounter();
console.log("util body", next(), next());
export const TAG = "~tagged";
export function mark(box: Record<string, boolean>): Record<string, boolean> {
box[TAG] = true;
return box;
}
export function kind(): string {
return typeof TAG;
}
export function describe(): string {
return `core v${version}`;
}
@@ -0,0 +1,13 @@
import { probe } from "./beta.ts";
export function report(): string {
return `alpha ${NAME}`;
}
export function late(): string {
return `${OTHER} after ${probe}`;
}
export const NAME = "ready";
export const OTHER = "other";
console.log("alpha body", report());
@@ -0,0 +1,12 @@
import { report } from "./alpha.ts";
function attempt(): string {
try {
return report();
} catch (error) {
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
}
}
export const probe = attempt();
console.log("beta body", probe);
@@ -0,0 +1,7 @@
// The first cycle member to run calls a function of the other member
// before that member declared the constant the function reads: Node's
// temporal-dead-zone ReferenceError. A second function of the same member
// reads another constant but runs only after the cycle finished.
import { late, report } from "./alpha.ts";
console.log(report(), late());
@@ -0,0 +1,11 @@
import { Probe, Lazy } from "./beta.ts";
export { Probe };
export const label = "ready";
export const other = "other";
export function later(): string {
return `${Lazy.describe()} ${other}`;
}
console.log("alpha body", Probe.result, Lazy.value);
@@ -0,0 +1,26 @@
import { label, other } from "./alpha.ts";
function guard(read: () => string): string {
try {
return read();
} catch (error) {
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
}
}
export class Probe {
static helper(): string {
return guard(() => `label ${label}`);
}
static result = this.helper();
}
export class Lazy {
static read(): string {
return guard(() => `other ${other}`);
}
static describe(): string {
return `lazy ${other}`;
}
static value = [0].map(() => this.read()).join("");
}
@@ -0,0 +1,8 @@
// The first cycle member to run builds classes whose static initializers
// call a method through `this`, directly and from an arrow function; both
// methods read constants of the other member before it declared them:
// Node's temporal-dead-zone ReferenceError. A third method reads a constant
// only after the cycle finished.
import { label, Probe, later } from "./alpha.ts";
console.log(label, Probe.result, later());
@@ -0,0 +1,11 @@
import { Probe, Lazy } from "./beta.ts";
export { Probe };
export const label = "ready";
export const other = "other";
export function later(): string {
return `${Lazy.describe()} ${other}`;
}
console.log("alpha body", Probe.result, Lazy.value);
@@ -0,0 +1,26 @@
import { label, other } from "./alpha.ts";
function guard(read: () => string): string {
try {
return read();
} catch (error) {
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
}
}
export const Probe = class ProbeImpl {
static helper(): string {
return guard(() => `label ${label}`);
}
static result = ProbeImpl.helper();
};
export const Lazy = class LazyImpl {
static read(): string {
return guard(() => `other ${other}`);
}
static describe(): string {
return `lazy ${other}`;
}
static value = [0].map(() => LazyImpl.read()).join("");
};
@@ -0,0 +1,9 @@
// The first cycle member to run builds classes from named class
// expressions whose static initializers call a method through the inner
// name, directly and from an arrow function; both methods read constants
// of the other member before it declared them: Node's temporal-dead-zone
// ReferenceError. A third method reads a constant only after the cycle
// finished.
import { label, Probe, later } from "./alpha.ts";
console.log(label, Probe.result, later());
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3500 }
@@ -0,0 +1 @@
{ "plain": 1800, "san": 2600 }
@@ -0,0 +1 @@
{ "plain": 1800, "san": 2600 }
@@ -0,0 +1 @@
{ "plain": 1800, "san": 2600 }