From fd768cd914b37428bb1053880463cde151c2e582 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 00:24:12 -0500 Subject: [PATCH] Initialize import cycles that re-export a member with export * MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An `export * from` declaration binds each re-exported name to the binding of the module that declares it, like a named re-export, so it has no initialization state of its own. Import cycles whose modules re-export another member of the cycle this way now compile with the exact cycle initialization: re-exported names read the same values, and throw the same temporal dead zone errors, as in Node. `export * as ns` of a cycle member keeps the SC1016 refusal. pi-coding-agent's sdk.js (sdk.js → agent-session-runtime.js → agent-session-services.js → sdk.js) has this shape. Refs #1777. --- packages/compiler/src/frontend/program.ts | 6 ++++- .../8213-cycle-star-reexport/main.ts.json | 9 +++++++ tests/corpus/8213-cycle-star-reexport/main.ts | 11 ++++++++ .../8213-cycle-star-reexport/runtime.ts | 17 ++++++++++++ tests/corpus/8213-cycle-star-reexport/sdk.ts | 9 +++++++ .../8213-cycle-star-reexport/services.ts | 27 +++++++++++++++++++ .../8213-cycle-star-reexport/main.ts.json | 1 + 7 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json create mode 100644 tests/corpus/8213-cycle-star-reexport/main.ts create mode 100644 tests/corpus/8213-cycle-star-reexport/runtime.ts create mode 100644 tests/corpus/8213-cycle-star-reexport/sdk.ts create mode 100644 tests/corpus/8213-cycle-star-reexport/services.ts create mode 100644 tests/harness/corpus-costs/8213-cycle-star-reexport/main.ts.json diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 8075379b8..bf75127c4 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -3927,10 +3927,14 @@ function exactCycleRefusal7( if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`; } + // `export * from` binds each name to the declaring member's own + // binding, like a named re-export, so only `export * as ns` (a + // namespace object of a member) has state of its own. if ( ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && - (stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause)) + stmt.exportClause !== undefined && + ts.isNamespaceExport(stmt.exportClause) ) { const dep = targetOf(stmt); if (dep !== null && members.has(dep)) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json new file mode 100644 index 000000000..c22b59997 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8213-cycle-star-reexport/main.ts.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8213-cycle-star-reexport/services.ts", + "/tests/corpus/8213-cycle-star-reexport/runtime.ts", + "/tests/corpus/8213-cycle-star-reexport/sdk.ts", + "/tests/corpus/8213-cycle-star-reexport/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8213-cycle-star-reexport/main.ts b/tests/corpus/8213-cycle-star-reexport/main.ts new file mode 100644 index 000000000..04cd7cea1 --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/main.ts @@ -0,0 +1,11 @@ +// A module of an import cycle re-exports another member with `export *` +// (pi-coding-agent's sdk.js → agent-session-runtime.js → +// agent-session-services.js → sdk.js). The re-exported names are live +// bindings of the member that declares them, so they read the same values, +// and the same temporal dead zone errors, as in Node. +import { createSession, describeRuntime, Runtime, SDK_VERSION } from "./sdk.ts"; +import { servicesReport } from "./services.ts"; + +console.log(servicesReport()); +console.log(createSession("main").describe()); +console.log(describeRuntime(), new Runtime("direct").name, SDK_VERSION); diff --git a/tests/corpus/8213-cycle-star-reexport/runtime.ts b/tests/corpus/8213-cycle-star-reexport/runtime.ts new file mode 100644 index 000000000..f3653d255 --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/runtime.ts @@ -0,0 +1,17 @@ +import { createServices, servicesLabel } from "./services.ts"; + +export class Runtime { + readonly name: string; + constructor(name: string) { + this.name = name; + } + describe(): string { + return `${this.name} runtime with ${servicesLabel}`; + } +} + +export const runtimeLabel = "runtime"; + +export function describeRuntime(): string { + return `${runtimeLabel} (${typeof createServices})`; +} diff --git a/tests/corpus/8213-cycle-star-reexport/sdk.ts b/tests/corpus/8213-cycle-star-reexport/sdk.ts new file mode 100644 index 000000000..7d67384c4 --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/sdk.ts @@ -0,0 +1,9 @@ +import { createServices } from "./services.ts"; + +export * from "./runtime.ts"; + +export const SDK_VERSION = "1.4.0"; + +export function createSession(name: string) { + return createServices(name); +} diff --git a/tests/corpus/8213-cycle-star-reexport/services.ts b/tests/corpus/8213-cycle-star-reexport/services.ts new file mode 100644 index 000000000..62e6b684e --- /dev/null +++ b/tests/corpus/8213-cycle-star-reexport/services.ts @@ -0,0 +1,27 @@ +import { Runtime, SDK_VERSION, describeRuntime, runtimeLabel } from "./sdk.ts"; + +export const servicesLabel = "services"; + +function probe(read: () => unknown): string { + try { + return String(read()); + } catch (error) { + return `${(error as Error).name}: ${(error as Error).message}`; + } +} + +// This module runs first: sdk.ts and runtime.ts have not run yet. +const early = [ + probe(() => SDK_VERSION), + probe(() => runtimeLabel), + probe(() => typeof describeRuntime), + probe(() => new Runtime("early").name), +]; + +export function servicesReport(): string { + return `early ${early.join(" | ")}; late ${SDK_VERSION} ${runtimeLabel}`; +} + +export function createServices(name: string): Runtime { + return new Runtime(name); +} diff --git a/tests/harness/corpus-costs/8213-cycle-star-reexport/main.ts.json b/tests/harness/corpus-costs/8213-cycle-star-reexport/main.ts.json new file mode 100644 index 000000000..ef439d096 --- /dev/null +++ b/tests/harness/corpus-costs/8213-cycle-star-reexport/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4500 }