Skip to content

Compile import cycles whose class is reached only after initialization - #1495

Open
cramforce wants to merge 4 commits into
mainfrom
opt/cycle-class-tdz
Open

cramforce wants to merge 4 commits into
mainfrom
opt/cycle-class-tdz

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Two modules that import each other, where a class in one can be reached from code that runs before the class statement, were refused with SC1016 ("the class … can be accessed before its declaration runs"). The usual case is a class that is only used after both modules finished initializing, which Node runs normally. Node throws a temporal-dead-zone ReferenceError only when the class is actually reached early.

Classes of an import cycle now get the same initialization flag that a module's own early-reached classes already had (registerClassInitFlag). The class statement sets the flag, and every reference from code that can run earlier (new, the class value, instanceof, static members) checks it first, throwing Node's ReferenceError: Cannot access 'X' before initialization.

Two details:

  • The class's references in other modules name it through their import binding, so the reference scan now resolves import aliases. Before this, only same-module references were checked.
  • An extends clause in another module that can run before the class statement keeps the refusal. Node evaluates the clause when that module's class statement runs, but a compiled subclass is static and has nothing to check. This is the existing cycle-class-early diagnostics fixture, whose output is unchanged.

Evidence

  • 6440-cycle-class-late-use/: a logger-style cycle. One module declares a class and builds it from an arrow in a module-level object; the other reaches it only from functions called after initialization. Main already compiled this shape; it is a regression check for the late case.
  • 6441-cycle-class-early-access/: the other module constructs the class during its own initialization (Node's ReferenceError, caught and printed), and the class works afterwards. Main refused this program.
  • 6442-cycle-class-factory/: construction from a factory in the other module after both settled, with instanceof. Main also compiled this shape; it pins the late case through a factory.
  • Both lanes: Node and native agree. Existing cycle corpus programs and the diagnostics suite pass unchanged.
  • Zero-cost gate against main (0afea26): PASS, all 3252 programs (bench, self, corpus) byte-identical; bench geomean +0.03%, self −0.69%, both within tolerance.
  • Lint, format, test:ts7 --baselines-only, and the self-hosting IR build are clean.

🤖 Generated with Claude Code

A class of an import cycle that code in the other module can reach before the class statement was refused (SC1016). It now gets the initialization flag a module's own early-reached classes already had: references from code that can run earlier check it, so Node's ReferenceError appears only when the class is actually reached early. The reference scan resolves other modules' import bindings. An `extends` clause in another module that can run early keeps the refusal, since a compiled subclass is static.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 10:09am UTC

Comment thread packages/compiler/src/frontend/lowering/cycle-initialization.ts
Comment thread packages/compiler/src/frontend/lowering/cycle-initialization.ts Outdated
Comment thread packages/compiler/src/frontend/lowering/cycle-initialization.ts Outdated
The references of a cycle class were found by their spelling, so a class
imported under another name (`import { Sink as Alias }`) was never seen as
reached early, and neither was a let or const imported under another
name. An `extends` clause only counted when its expression was the bare
class name. A static method read as a value (`const f = Sink.m`) built a
closure that no check covered.

The early-binding scan and the class reference scan now also match each
file's local import names that resolve to the binding, and record them:
Node's ReferenceError names the binding the access spells, so the check
names the alias. The heritage check walks the whole `extends` expression.
A closure of a flagged class's static method checks the class like a
static call does.
Comment on lines +100 to +101
if (info !== undefined && !info.generic && heritage === null)
registerClassInitFlag(lowerer, binding, info, true);

@vercel vercel Bot Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An early Sink.name read on a newly accepted cycle class is folded to a string literal without checking initialization, returning the class name instead of throwing Node’s ReferenceError.

Fix on Vercel

This branch was successfully deployed

1 active deployment
Preview — 62aa9afd Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants