Skip to content

fix(code-splitting): gate entry-level transitive init registration on strict order#10323

Draft
hyfdev wants to merge 5 commits into
mainfrom
codex/strict-entry-transitive-init-gate
Draft

fix(code-splitting): gate entry-level transitive init registration on strict order#10323
hyfdev wants to merge 5 commits into
mainfrom
codex/strict-entry-transitive-init-gate

Conversation

@hyfdev

@hyfdev hyfdev commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Addresses review Major 2a. The entry/facade-chunk loop in compute_cross_chunk_links called add_transitive_esm_init_depended_symbols for the entry module with no strict gate, but legacy interop transitive_esm_init_targets exist flag-off too. For an interop-wrapped entry rendered behind a facade chunk, with an excluded re-export whose wrapped targets share the entry's host chunk, the facade would gain a dead cross-chunk init_* import that the pre-#10104 base never emitted.

Gate this entry-level call on is_strict_execution_order_enabled(). Strict still needs it (it feeds order-wrapped entries' prologue init imports). The per-module call in add_module_esm_init_depended_symbols stays ungated — it is provably inert flag-off, since legacy targets are same-chunk-only and same-chunk targets never become cross-chunk imports.

Behavior / snapshot impact

None. Full suite runs with zero snapshot changes vs the ungated base. For a non-facade entry the entry chunk is the host chunk, so the registered targets are same-chunk and never turn into cross-chunk imports; only a facade entry (entry module hosted elsewhere) could differ. I verified this symmetrically: the gated build matches the committed (ungated) snapshots on every existing fixture.

On the flag-off regression fixture

The gate lands as hardening; I could not construct the exact deviation flag-off after four genuine attempts, and the reason is structural:

  • The dead import only appears when the entry chunk is a facade (entry module hosted in a different chunk), so the same-host wrapped target becomes cross-chunk.
  • Flag-off, the advancedChunks-captured-entry facade does not form: analyze_execution_order returns None when strict is off (order_analysis.rs:107), so create_order_wrap_entry_facades never runs. Capturing an entry into a group flag-off merges the entry into that chunk (confirmed empirically) rather than creating a facade.
  • The pre-existing flag-off facade paths (dynamic-entry / emitted-chunk) require the module to be deferred, which makes it concatenate, not wrap — so there is no init_* for the entry, and no transitive registration.
  • Wrapping the entry via interop (require) or a cycle materializes / co-locates it in a way that either un-excludes the re-export or removes the facade.

Attempts tried (all empirically built and inspected): (1) TLA entry captured into a group → concatenated, no wrapper; (2) require-cycle single entry → wrapped but no facade (merged); (3) two-entry CJS-driven shared host → merged, re-export materialized; (4) dynamic-entry facade over a shared host → concatenated. The strict-mode facade-creation machinery is exactly what breaks this tension, and it does not run flag-off — so the deviation is a latent shape the gate defends against rather than one reachable today.

Validation

  • cargo test -p rolldown — 1868 passed, snapshot-stable, zero deltas. (Same 5 pre-existing environmental failures: cjs_module_lexer_compat ×3 / npm_packages need cjs-module-lexer; test262 needs the submodule.)
  • cargo fmt --all --check — clean.
  • cargo clippy -p rolldown --all-targets -- -D warnings — clean.

Follow-up to #10104; addresses Major 2a from the review discussion: #10104 (comment)

Draft, stacked on codex/strict-class-def-order-sensitivity.

hyfdev added 5 commits July 15, 2026 20:29
…nistic and constant-time

Two independent fixes to the strict-execution-order cross-chunk link pass:

- add_transitive_esm_init_depended_symbols iterated an FxHashMap via values(),
  whose bucket order follows FxHash layout. FxHash is unseeded but hashes
  differently on 32-bit vs 64-bit, so that order (which flows into the chunk's
  depended_symbols FxIndexSet and thence into imported-symbol $1/$2 rename order
  in deconflict_chunk_symbols) could diverge between native and wasm32/WASI
  builds. Iterate sorted by the owning StmtInfoIdx to pin one order everywhere.

- module_is_in_live_chunk did a linear chunk.modules.contains scan, called for
  every canonical referenced symbol of every included statement under strict,
  making the pass quadratic on large chunks. Replace with an O(1)
  module_to_chunk + liveness check. Sound because add_module_to_chunk sets both
  together and every module-removal site pairs the removal with clearing
  module_to_chunk, reassigning it to the destination chunk, or marking the chunk
  Removed; no site leaves module_to_chunk pointed at a still-live chunk that no
  longer contains the module, so no removed-module set is needed.
…der strict order

The emergent-cycle projector's soundness rests on mirroring the linker's three
init registration paths. One consumption disjunct
(referenced_symbols_by_entry_point_chunk) has no projection counterpart, so a
future projector hole would surface only as a fuzzer-caught
`init_* is not a function` runtime crash.

Add a debug-only, strict-gated check that re-runs Tarjan over the FINAL
imports_from_other_chunks chunk graph and asserts the exact obligation
close_cyclic_chunk_members discharges: every module hosted in a nontrivial SCC
(>=2 chunks, or a self-loop) that is both order-sensitive AND order-wrap-eligible
is order-wrapped in the plan. This turns any future under-projection into a
deterministic debug/CI build failure instead of a runtime crash. It is a
structural invariant like the existing empty-facade zero-static-indegree assert,
not the internal semantic verifier design.md rejects.

The obligation is deliberately is_order_sensitive && is_order_wrap_eligible, not
raw is_order_wrap_eligible: a side-effect-free, import-free eligible leaf (e.g.
`export const x = 1`) is never wrapped by the projector, yet can be co-hosted in
a chunk that lands in a cycle because of its siblings' cross-chunk imports, and
leaving it unwrapped is correct. Reuses is_order_sensitive and
is_order_wrap_eligible (visibility widened to pub(super)) instead of duplicating
their conditions. Zero fixtures trip the assert; zero snapshot changes.
…r-sensitive

is_order_sensitive() collects GlobalVarAccess/PureAnnotation reasons only in the
expression arms the analyzer handles itself. Three oxc-delegate arms returned a
bare tree-shaking boolean and dropped the reasons: the analyze_decl catch-all
(class declarations), export default class, and the analyze_expr catch-all
(class expressions). So `var x = Object.freeze({})` was order-sensitive while
`class C { static x = Object.freeze({}) }` was not — a module whose only
eval-time work is a whitelisted-global read in a class position could be silently
reordered across a module that patches that global (polyfill-then-use hazard).
On-demand wrapping widened the blast radius from import-free leaves to every
planless module.

When oxc reports no side effect for a class declaration/expression, additionally
collect order-sensitive reasons from the class's definition-time-evaluated
positions: heritage (extends), class/element decorators, computed keys, static
field/accessor initializers, and static blocks — routed through the existing
analyze_expr/analyze_stmt machinery. Instance (non-static) field initializers and
method bodies run at construction/call time, not definition time, so they are
excluded.

The tree-shaking channel is byte-for-byte unaffected: base facts stay
from_tree_shaking_side_effect(<same node>.may_have_side_effects), and reasons are
harvested (order-sensitive channel only) exclusively when oxc already certified
the class side-effect-free. Adds unit tests for both the newly-sensitive
positions and the excluded construction-time ones. Zero existing fixture
snapshots change; the hazard was latent.
… strict order

The entry/facade-chunk loop called add_transitive_esm_init_depended_symbols for
the entry module with no strict gate, but legacy interop
transitive_esm_init_targets exist flag-off too. For an interop-wrapped entry
rendered behind a facade chunk, with an excluded re-export whose wrapped targets
share the entry's host chunk, the facade would gain a dead cross-chunk init_*
import that the pre-#10104 base never emitted.

Gate this entry-level call on is_strict_execution_order_enabled(). Strict still
needs it (it feeds order-wrapped entries' prologue init imports). The per-module
call in add_module_esm_init_depended_symbols stays ungated: it is provably inert
flag-off, since legacy targets are same-chunk-only and same-chunk targets never
become cross-chunk imports.

Behavior/snapshot impact: none. Full suite runs with zero snapshot changes vs
the ungated base — for a non-facade entry the entry chunk equals the host chunk,
so the registered targets are same-chunk and never turn into cross-chunk imports;
only a facade entry (entry module hosted elsewhere) would differ.
@hyfdev
hyfdev force-pushed the codex/strict-class-def-order-sensitivity branch from 4709a6b to 4f804f3 Compare July 16, 2026 15:14
@hyfdev
hyfdev force-pushed the codex/strict-entry-transitive-init-gate branch from 4306dd7 to 3bbc10b Compare July 16, 2026 15:14
@hyfdev
hyfdev force-pushed the codex/strict-class-def-order-sensitivity branch 3 times, most recently from b8df48d to 3b7f8b7 Compare July 20, 2026 14:02
@graphite-app
graphite-app Bot force-pushed the codex/strict-class-def-order-sensitivity branch from 3b7f8b7 to ff32edb Compare July 21, 2026 01:50
Base automatically changed from codex/strict-class-def-order-sensitivity to main July 21, 2026 01:55
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.

1 participant