diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index bdd6260ed..6a068353a 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -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. diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 932ac7054..6d8ffe4c0 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -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; - } - const candidates = new Map(); + const candidates = new Map(); const names = new Set(); 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(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, +): 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; +} + +/** 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, +): Map | 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(); + const immediate = new Map(); + const storedNames: ts.Identifier[] = []; + const unitsOfName = new Map(); + 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(); + 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(); + 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(); + const reachedUnits = new Set(); + const out = new Map(); + 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 }).modifiers?.nodes ?? []).filter( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..18ef785c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7380-cycle-deferred-folded-key/util.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/core.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..65d9b19e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..986683684 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7382-cycle-static-this-tdz/beta.ts", + "/tests/corpus/7382-cycle-static-this-tdz/alpha.ts", + "/tests/corpus/7382-cycle-static-this-tdz/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json new file mode 100644 index 000000000..69b6d63bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts", + "/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts", + "/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7380-cycle-deferred-folded-key/core.ts b/tests/corpus/7380-cycle-deferred-folded-key/core.ts new file mode 100644 index 000000000..7f4f4ab14 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/core.ts @@ -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()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/main.ts b/tests/corpus/7380-cycle-deferred-folded-key/main.ts new file mode 100644 index 000000000..92617eba3 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/main.ts @@ -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()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/util.ts b/tests/corpus/7380-cycle-deferred-folded-key/util.ts new file mode 100644 index 000000000..a038d9e63 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/util.ts @@ -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): Record { + box[TAG] = true; + return box; +} + +export function kind(): string { + return typeof TAG; +} + +export function describe(): string { + return `core v${version}`; +} diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts new file mode 100644 index 000000000..8dc002c27 --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts @@ -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()); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts new file mode 100644 index 000000000..c3d1551ff --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts @@ -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); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts new file mode 100644 index 000000000..6a0b7e3ca --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts @@ -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()); diff --git a/tests/corpus/7382-cycle-static-this-tdz/alpha.ts b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts @@ -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); diff --git a/tests/corpus/7382-cycle-static-this-tdz/beta.ts b/tests/corpus/7382-cycle-static-this-tdz/beta.ts new file mode 100644 index 000000000..428907967 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/beta.ts @@ -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(""); +} diff --git a/tests/corpus/7382-cycle-static-this-tdz/main.ts b/tests/corpus/7382-cycle-static-this-tdz/main.ts new file mode 100644 index 000000000..e3bbf1581 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/main.ts @@ -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()); diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts @@ -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); diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts new file mode 100644 index 000000000..d1d89ad1a --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts @@ -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(""); +}; diff --git a/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts b/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts new file mode 100644 index 000000000..93b7a47ee --- /dev/null +++ b/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts @@ -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()); diff --git a/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json b/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json b/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } diff --git a/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json b/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } diff --git a/tests/harness/corpus-costs/7383-cycle-class-expression-name-tdz/main.ts.json b/tests/harness/corpus-costs/7383-cycle-class-expression-name-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7383-cycle-class-expression-name-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 }