Skip to content

feat(code-splitting): wrap strict execution order modules on demand#10104

Merged
graphite-app[bot] merged 1 commit into
mainfrom
codex/vite-chunk-execution-order-plan
Jul 17, 2026
Merged

feat(code-splitting): wrap strict execution order modules on demand#10104
graphite-app[bot] merged 1 commit into
mainfrom
codex/vite-chunk-execution-order-plan

Conversation

@hyfdev

@hyfdev hyfdev commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds an opt-in on-demand planner for output.strictExecutionOrder while preserving both the flag-off path and strict mode's existing wrap-all default.

Configuration Execution-order behavior
strictExecutionOrder: false No execution-order wrapping
strictExecutionOrder: true Wrap every eligible module (wrap-all, the conservative default)
strictExecutionOrder: true + experimental.onDemandWrapping: true Derive a conservative wrapping plan from the generated chunk graph

The on-demand plan is deliberately conservative rather than minimal. It begins with predicted source-order versus chunk-order hazards, then closes over importers, trigger hosts, retained re-export paths, and cyclic chunks where eager evaluation cannot be proven safe.

This PR does not change the default behavior of strictExecutionOrder and does not enable strict execution order globally.

Fixes #10236.

Important

Strict-order output may change the entry-chunk shape. When an entry implementation must remain inert while another chunk is evaluated, Rolldown emits a small entry facade that calls the relevant init_*() function and moves the implementation into a separate chunk. Reviewers and downstream tooling should expect this facade/implementation split.

Why the analysis runs after chunking

Interop wrapping answers how a module is represented and remains a link-stage decision. Execution-order wrapping answers when an already-included module body may run, which depends on generated chunk placement, manual groups, facade elimination, shared entries, and chunk cycles.

This PR keeps link-owned WrapKind and user-code liveness immutable, computes an OrderWrapPlan after provisional chunking, and lowers that plan into a separate OrderWrapState. Lowering may add synthetic wrappers, init calls, runtime symbols, entry facades, and topology state; it cannot make an excluded user statement or binding live.

The on-demand path then projects imports added by lowering back into the chunk graph. If those imports create a new chunk cycle, every eligible module in the cyclic chunks is added to the plan and projection repeats until stable. The process is monotone and bounded by the module count.

Correctness hardening

  • Preserve init obligations through re-export barrels without reviving tree-shaken imports or unrelated exports.
  • Share init-obligation enumeration between emission, cross-chunk registration, and fixpoint projection so their record gates cannot drift.
  • Preserve the same link-stage tree-shaking result in wrap-all and on-demand, including consumer-local named and namespace re-export paths.
  • Keep cross-chunk init_* bindings live and deconflicted for both ESM and CJS output.
  • Restore user, dynamic, and emitted entry facades after strict lowering changes chunk topology.
  • Recompute topology-derived metadata and final output validation after lowering.

Tree-shaking parity here is specifically between wrap-all and on-demand. It does not claim byte identity with flag-off output and does not include the separate wrappedModuleTreeshaking work.

Review guide

  1. Start with internal-docs/code-splitting/design.md for the invariants, mode split, conservative decisions, and projected/omitted edge inventory.
  2. Read order_analysis.rs for source-order versus chunk-order prediction and the emergent-cycle fixpoint.
  3. Read order_wrap_state.rs and order_wrapping.rs for the late synthetic state and lowering boundary.
  4. Read esm_init_obligations.rs together with the module finalizers and cross-chunk-link pass for the shared emit/register/project contract.
  5. Review test(code-splitting): establish strict-order review baselines #10287 for config/variant normalization and the current-main output baseline.
  6. Review this PR's test diff for removal of every temporary expected-failure marker and the same-position snapshots of the final facade/runtime/chunk layouts.

Test-first sequence

The earlier test-first layers are already merged into main:

  1. test(code-splitting): add strict execution order regressions #10253 adds the fail-closed runtime-failure harness and core integration regressions.
  2. test(code-splitting): harden strict execution order coverage #10252 adds the later hardening fixtures and paired-mode coverage.

The remaining Graphite stack is:

  1. test(code-splitting): establish strict-order review baselines #10287 establishes the final config/variant review layout and current-main snapshots while preserving all expected-failure markers.
  2. feat(code-splitting): wrap strict execution order modules on demand #10104 (this PR) adds the implementation, removes all temporary failure markers, and records the implementation output.

The final tree contains 96 strict-enabled fixture configs and 200 named strict executions. Between #10287 and #10104, 21 config files only delete 44 expected-failure markers, and all 93 changed snapshots retain identical variant-heading order.

Validation

Local validation ran on rebased head 79efe909bb3f0bcd2abcc76dcd33e58069c3474c, based on main at c4e25974b8ff7598bf507ae4a2d7602cbcbc86fd.

  • The 86 changed fixtures passed twice on test(code-splitting): establish strict-order review baselines #10287 with the old implementation; the second pass was snapshot-stable.
  • All 96 strict-enabled fixture configs passed twice on this implementation, with a snapshot-stable final pass.
  • Full Rust workspace tests passed with just test-rust.
  • Rolldown Node tests passed: 793 main tests and 38 watcher tests.
  • The Rollup compatibility suite passed 1,213 tests with 0 failures.
  • cargo check --workspace --all-features --all-targets --locked and cargo fmt --all --check passed.
  • Clippy passed with --deny warnings for every changed Rust crate: rolldown, rolldown_common, and rolldown_ecmascript_utils.
  • just lint-node and just lint-repo passed.
  • Full-workspace just lint-rust is locally blocked on Rust 1.97 by clippy::ignore-without-reason in the unchanged rolldown_plugin_isolated_declaration/tests/mod.rs; the failing path is byte-identical to main.

Frozen closure evidence

The broader correctness campaign was frozen on one build of 42628c18b:

Fuzzer source and machine-readable evidence: https://github.com/hyf0/rolldown-order-fuzzer/tree/853fb775d1e949ced567deb0c163d044a031ba4f/.agents/evidence

Boundaries and known residuals

  • Top-level-await output remains legal, but this PR does not promise stronger TLA scheduling semantics than the default build.
  • External-module evaluation order remains outside the strict-order guarantee; generated output must still be legal.
  • Manual codeSplitting groups leak cross-entry execution without strictExecutionOrder (#9463 follow-up) #9998 is a separate bug in the non-strict/default code-splitting path and is not fixed here. The strict path covered by this PR passes its directed controls.
  • Camunda still emits two init_* functions with no call site (OverflowMenu$1 and RadioButton$1). They were harmless on all probed routes and have no synthetic reproduction.
  • The partial-forwarder per-obligation refinement is present in the shared contract, but no constructible corpus shape currently exercises that narrower branch directly.
  • On vue-vben-admin, the emergent-cycle fixpoint converged in two rounds and added 141 wrappers over the one-shot plan. ROLLDOWN_ORDER_DEBUG=1 reproduces the per-round trace.

@netlify

netlify Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit 1d86ffe
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a59ee2ef48fa80008865523

@hyfdev
hyfdev force-pushed the codex/vite-chunk-execution-order-plan branch 6 times, most recently from 4ce9346 to e6585a3 Compare July 3, 2026 07:12
@hyfdev
hyfdev force-pushed the codex/vite-chunk-execution-order-plan branch 5 times, most recently from 09e70de to 5a47f3b Compare July 8, 2026 15:27
@hyfdev
hyfdev force-pushed the codex/vite-chunk-execution-order-plan branch 3 times, most recently from f2aeed7 to 4df73b8 Compare July 12, 2026 12:29
@hyfdev
hyfdev force-pushed the codex/vite-chunk-execution-order-plan branch 2 times, most recently from e386c18 to e3e1baf Compare July 12, 2026 17:01
@hyfdev
hyfdev marked this pull request as ready for review July 13, 2026 02:15
Copilot AI review requested due to automatic review settings July 13, 2026 02:15

Copilot AI left a comment

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing codex/vite-chunk-execution-order-plan (220783a) with main (03e1e34)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@IWANABETHATGUY

Copy link
Copy Markdown
Member

I recommend splitting all newly added tests into a separate PR to make the PR review more manageable.

hyfdev added a commit that referenced this pull request Jul 16, 2026
… 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 added a commit that referenced this pull request Jul 16, 2026
…llector unification

#10104 replaced the finalizer's private wrapped-esm init-target collector with the
shared one, changing flag-off interop emission in two ways: star re-export
records now iterate sorted, non-ambiguous resolved exports (ambiguous-export
owners no longer receive a per-record init; order is name-order not hash-order),
and named/namespace specifiers are liveness-gated per importer (an `import * as
ns` consumer that only reads `ns.x` no longer inits every wrapped exporter).

Two directed flag-off fixtures (no strictExecutionOrder) execute under node and
observe side-effect order:
- namespace_liveness_gating: a wrapped barrel star-re-exports two side-effectful
  wrapped leaves; the entry reads only `ns.a`. Both leaves still run (A, B) via
  the barrel's own init, which calls init_leaf_a and init_leaf_b.
- ambiguous_star_reexport: two leaves both export `x` (ambiguous -> undefined);
  both owners still run (A, B) via the barrel's init.

Decision: every side effect still executes — just via the barrel's init rather
than redundant per-consumer init sites — and only ordering/bytes changed. The
collector unification narrowed redundant per-site inits without dropping
semantics. No regression found.
@IWANABETHATGUY

Copy link
Copy Markdown
Member

Took another pass over this after the two Fable reviews landed. The architecture holds up and I won't repeat what those already covered — everything below is stuff neither of them mentioned, from reading the diff at 220783a. Three of these I'd really like to see addressed before merge; the rest can go to #10294 or follow-ups.

The ones I'd fix before merge

1. The facade-split gate only sees pre-lowering import edgesorder_wrapping.rs:253

create_order_wrap_entry_facades builds imported_chunks from analysis.import_edges (line 214), which is the pre-lowering baseline. But lowering itself can create the first cross-chunk edge into an interop entry's chunk — e.g. an init_* forward through an excluded re-export hop (the Hole-2 shape; emergent_cycle_excluded_forwarder_import shows the linker minting exactly these baseline-invisible edges). When that emergent edge is acyclic, the fixpoint adds no at-risk modules and converges without ever revisiting the facade decision, so the entry keeps its inline trigger. Evaluating the importing chunk then runs the whole entry program early — which is the exact bug class this feature exists to prevent.

The interesting part is that analyze_execution_order already computes post-lowering edges each round and throws them away after cycle detection (order_analysis.rs:187-218). Returning the final round's edges instead of the baseline at order_analysis.rs:242 looks like it would close this. Worth noting the Tarjan assert proposed in the deep review wouldn't catch this one, since the missed edge is acyclic.

2. Deconflict doesn't see force-included runtime statementsdeconflict_chunk_symbols.rs:137

The other two consumers of statement inclusion both OR in the overlay (compute_cross_chunk_links.rs:390, module_finalizers/mod.rs:1622), but deconflict filters on raw stmt_info_included only. So a runtime helper that tree-shaking didn't include but order-wrapping forces (say __esmMin) gets rendered without its symbol ever being registered with the renamer. If a user module in the same chunk has a top-level __esmMin — single-chunk builds put the runtime right next to user code via order_wrapping.rs:451-456 — you get two same-named root declarations: a SyntaxError if the user binding is let/const, or the init wrappers calling the user's value if it's a var.

I couldn't find any other path that registers these symbols (synthetic statements only register the wrapper refs, and order_live_symbols only feeds the CJS captured-names set), so I think this one is real. It's also a nice illustration of why the overlay pattern makes me nervous — see the maintainability note below.

3. Moving the runtime sweep after exec-order assignment changes flag-off chunk orderfinalize_chunk_plan.rs:47

On main the sweep ran before exec-order assignment — there was even a comment saying it had to. Now exec orders are assigned first, and the Common-chunk comparator keys on modules[0]'s exec order (code_splitting.rs:187-216). The runtime module is always modules[0] with exec order 0, so when the sweep later strips it out of a still-live shared chunk, that chunk keeps an exec_order derived from a module that's no longer there. rebuild_sorted_chunk_idx_vec(true) just re-sorts the stale values, and renumber_live_chunks is strict-only.

Concretely: strict off, sweep fires (the #9374 walk-back shape), runtime shares chunk C with some user module. Main keyed C on that module's exec order; this PR keys it on 0, so C can sort ahead of chunks it used to follow. That feeds sorted_chunk_idx_vec and the cross-chunk import sort (compute_cross_chunk_links.rs:110/124), so entry chunks can emit import "./C.js" before a chunk that should run first — side effects execute in a different order than main, on the default path. The deep review noticed the benign name-slot half of this move (which I agree is a wash — tombstones sorted last anyway); this half is the one that bites.

Worth fixing, less urgent

4. dynamicImports metadata points at the implementation chunk, not what the code actually importscompute_cross_chunk_links.rs:358

The set records module_to_chunk[importee], but the finalizer rewrites the specifier via entry_module_to_entry_chunk — after a facade split those differ. Your own manual_group_phantom_dynamic snapshot shows it: group-l.js emits import("./m6.js"), but its metadata will list group-l.js and omit m6.js. Hashing is fine (placeholders come from rendered content), but modulepreload-style consumers and the devtools graph get the wrong direct edge. Recording entry_module_to_entry_chunk with a module_to_chunk fallback should do it.

5. restore_order_wrap_entry_facades revives every matching tombstone but wires only the firstorder_wrapping.rs:389

Both earlier reviews suggested pinning this with debug_assert!(len <= 1), but I don't think the invariant actually holds: emit the same module twice via this.emitFile with different names (FileEmitter::emit_chunk doesn't dedup) and you get two EmittedChunk facades, both of which the optimizer tombstones (same module means no export conflict, chunk_optimizer.rs:945-959). If that module lands in the plan, restore un-tombstones both and the second one renders as a stray near-empty entry asset. So rather than an assert, I'd only un-tombstone the facade we actually re-point to.

6. Dead importers can force runtime helpers into the outputorder_wrap_state.rs:33

populate_order_import_overlays walks every module with no is_included check, and required_runtime_helpers() folds all overlays — while its sibling live_symbols() carefully gates on overlay_is_live(key.importer). Net effect: a fully tree-shaken export-star barrel of a planned target mints a ReExport overlay, and forces_runtime_stmt ships a __reExport definition nobody calls. Bloat-only as far as I can tell, and gating the fold the same way live_symbols does looks safe — the overlays themselves need to stay, they feed the hop walk.

Maintainability

7. The liveness overlay pattern already claimed its first victim. Order-lowering's demand is OR-ed into consumers at five call sites instead of being folded into stmt_info_included/used_symbol_refs — and item 2 above is exactly the site that forgot. Since lowering runs while the builder is still mutable (seal is at mod.rs:150-152, after finalize_chunk_plan, and LinkStageOutput is per-bundle), folding the demand into the canonical sets before seal — or at least routing every read through one accessor — would make the next consumer safe by construction.

8. A few duplications that will drift:

  • The export-star WrapKind::Esm arm in module_finalizers/mod.rs:1753 is now token-for-token wrapped_esm_init_call_expr(importee.idx, SPAN, false, true) — this PR removed the last difference and the comment that used to justify the fork. Might as well call the helper before the two grow apart again (the helper already gained the esm_init_target indirection this arm mirrors by hand).
  • ensure_runtime_module_for_order_wraps mints the runtime chunk twice inline (order_wrapping.rs:419-441, 465-484), so the "rolldown-runtime" literal now lives in three places — and the only copy with the "Vite/plugin-rsc match on this name" warning is the one in code_splitting.rs:1071. One shared constructor would keep the contract in one place.
  • The 16-line order_live_symbols block is verbatim in generate_stage/mod.rs:180-195 and compute_cross_chunk_links.rs:261-276. Extract a helper — but don't reuse the first result: merge_cjs_namespace links canonical refs between the two computations, so the second recompute is load-bearing.

Perf quickies

  • add_wrapped_esm_init_targets_for_static_member_reads (esm_init_obligations.rs:359) rescans every included statement's refs per not-directly-used binding. Under wrap-all that's per wrapped module per link pass; and since the Emit call site isn't strict-gated, ns.x-only bindings of cross-chunk importees pay it on flag-off builds too, where main did O(1) pushes. A per-module member-read index keyed by object_ref would kill both.
  • The TopLevelImportReadDetector walk (ast_scanner/impl_visit.rs:98) is gated on strict only, but the bit it computes is read solely by the on-demand analysis (order_analysis.rs:1005) — wrap-all pays a per-module AST walk for nothing. Gate it on onDemandWrapping too.
  • create_order_wrap_entry_facades builds dynamic_target_modules_by_chunk (a full modules × records sweep, order_wrapping.rs:222-247) unconditionally, but only reads it behind !on_demand || ... — wrap-all computes it and throws it away.

Nits

  • detect_ineffective_dynamic_imports.rs:18 and order_wrapping.rs:272 inline matches!(..., Some(Removed)), which is !chunk_is_live() — the helper this same PR adds (and order_wrapping.rs uses a few lines below).
  • ReexportRetentionEvidence is only ever consumed via .any(); a bool argument would say the same thing.
  • OrderSyntheticStmt.chunk/.owner are written but only read by debug_asserts — placement truth already lives in synthetic_statements_by_chunk.
  • The // See internal-docs/devtools/implementation.md ... comment above trace_action_chunks_infos got dropped in the generate_stage/mod.rs reshuffle; the doc still pins that call's position in the action lifecycle, and AGENTS.md asks for the in-code reference.

One meta-note: I also chased a few things that turned out fine and might save the next reader time — the dead-specifier/ambiguous-star flag-off deviations the deep review asked fixtures for can't actually drop a side effect (side-effectful targets always force an earlier init through the barrel; the residual is byte-parity only, so a strict-gate on binding_is_opaque or one parity snapshot would settle it), and the recursive actual-order walk can't overflow on any input that wrap_module_recursively doesn't already blow up on first.

Nice work overall — the emit/register/project contract centralization in particular paid off; most of what I found lives in the seams around it, not in it.

@hyfdev

hyfdev commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Verified all three pre-merge items against the code — all real. Fixed as three stacked draft PRs continuing the follow-up stack, each with a directed fixture:

  • Item 3 → fix(code-splitting): re-derive chunk exec order after the runtime sweep #10334 (re-derive chunk exec order after the runtime sweep). Exactly the mechanism you described: the sweep's chunk.modules.retain(!= runtime) ran after the Common-chunk comparator had already keyed the shared chunk on the runtime's exec order 0. The exec-order assignment is now a reusable method and re-runs when the sweep removes the runtime from a live chunk. The fixture (topics/runtime/sweep_shared_chunk_exec_order) was snapshot-verified against main at the merge-base: the fixed branch reproduces main's chunk order.
  • Item 1 → fix(code-splitting): gate entry facades on post-lowering import edges #10335 (gate entry facades on post-lowering import edges). The gate now consumes the converged fixpoint round's edges instead of the pre-lowering baseline. One nuance from building the fixture (facade_gate_emergent_edge): the facade-decision gap is real and pinned (snapshot differentiator verified against the unfixed base), but an executable early-run does not appear constructible — a Hole-2 edge is projection-only (the real lowering never renders that forward; it exists for cycle detection), and a Hole-1 edge implies a baseline edge already exists, so the gate already split. Details and the attempt inventory are in the PR body; if a rendered baseline-invisible path is ever found, the fixture converts straight into an executed assertion.
  • Item 2 → fix(code-splitting): deconflict force-included runtime helper statements #10336 (deconflict force-included runtime helper statements). Deconflict now ORs in forces_runtime_stmt, mirroring the other two consumers. The fixture reproduces the failure on the unfixed base — two root var __esmMin, with init_late throwing TypeError: __esmMin is not a function — and executes correctly on the fix. One precision on the frame: a strict-order build relocates a co-hosted runtime to a standalone chunk (ensure_runtime_module_for_order_wraps) unless code splitting is disabled, so the constructible collision is codeSplitting: false; the fix itself is mode-independent. Your item-7 point (route every read through one accessor) is the right structural cure — kept this diff minimal and filed the accessor refactor with the rest.

Items 4–8, the perf quickies, and the nits are being recorded on #10294 so the follow-up ledger stays in one place. Two notes on the meta-note: the byte-parity conclusion on the collector unification matches what #10324's executing fixtures found independently (both leaves' effects still run via the barrel's own init), and agreed on the recursion-depth argument — dropping that item.

Full follow-up stack on this PR, bottom-up: #10320 #10321 #10322 #10323 #10324 #10334 #10335 #10336.

@graphite-app

graphite-app Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jul 17, 8:41 AM UTC: This pull request can not be added to the Graphite merge queue. Please try rebasing and resubmitting to merge when ready.
  • Jul 17, 8:41 AM UTC: Graphite disabled "merge when ready" on this PR due to: a merge conflict with the target branch; resolve the conflict and try again..
  • Jul 17, 8:55 AM UTC: hyfdev added this pull request to the Graphite merge queue.
  • Jul 17, 9:00 AM UTC: Merged by the Graphite merge queue.

…10104)

## Summary

This PR adds an opt-in on-demand planner for `output.strictExecutionOrder` while preserving both the flag-off path and strict mode's existing wrap-all default.

| Configuration | Execution-order behavior |
| --- | --- |
| `strictExecutionOrder: false` | No execution-order wrapping |
| `strictExecutionOrder: true` | Wrap every eligible module (wrap-all, the conservative default) |
| `strictExecutionOrder: true` + `experimental.onDemandWrapping: true` | Derive a conservative wrapping plan from the generated chunk graph |

The on-demand plan is deliberately conservative rather than minimal. It begins with predicted source-order versus chunk-order hazards, then closes over importers, trigger hosts, retained re-export paths, and cyclic chunks where eager evaluation cannot be proven safe.

This PR does not change the default behavior of `strictExecutionOrder` and does not enable strict execution order globally.

Fixes #10236.

> [!IMPORTANT]
> Strict-order output may change the entry-chunk shape. When an entry implementation must remain inert while another chunk is evaluated, Rolldown emits a small entry facade that calls the relevant `init_*()` function and moves the implementation into a separate chunk. Reviewers and downstream tooling should expect this facade/implementation split.

## Why the analysis runs after chunking

Interop wrapping answers how a module is represented and remains a link-stage decision. Execution-order wrapping answers when an already-included module body may run, which depends on generated chunk placement, manual groups, facade elimination, shared entries, and chunk cycles.

This PR keeps link-owned `WrapKind` and user-code liveness immutable, computes an `OrderWrapPlan` after provisional chunking, and lowers that plan into a separate `OrderWrapState`. Lowering may add synthetic wrappers, init calls, runtime symbols, entry facades, and topology state; it cannot make an excluded user statement or binding live.

The on-demand path then projects imports added by lowering back into the chunk graph. If those imports create a new chunk cycle, every eligible module in the cyclic chunks is added to the plan and projection repeats until stable. The process is monotone and bounded by the module count.

## Correctness hardening

- Preserve init obligations through re-export barrels without reviving tree-shaken imports or unrelated exports.
- Share init-obligation enumeration between emission, cross-chunk registration, and fixpoint projection so their record gates cannot drift.
- Preserve the same link-stage tree-shaking result in wrap-all and on-demand, including consumer-local named and namespace re-export paths.
- Keep cross-chunk `init_*` bindings live and deconflicted for both ESM and CJS output.
- Restore user, dynamic, and emitted entry facades after strict lowering changes chunk topology.
- Recompute topology-derived metadata and final output validation after lowering.

Tree-shaking parity here is specifically between wrap-all and on-demand. It does not claim byte identity with flag-off output and does not include the separate `wrappedModuleTreeshaking` work.

## Review guide

1. Start with `internal-docs/code-splitting/design.md` for the invariants, mode split, conservative decisions, and projected/omitted edge inventory.
2. Read `order_analysis.rs` for source-order versus chunk-order prediction and the emergent-cycle fixpoint.
3. Read `order_wrap_state.rs` and `order_wrapping.rs` for the late synthetic state and lowering boundary.
4. Read `esm_init_obligations.rs` together with the module finalizers and cross-chunk-link pass for the shared emit/register/project contract.
5. Review #10287 for config/variant normalization and the current-main output baseline.
6. Review this PR's test diff for removal of every temporary expected-failure marker and the same-position snapshots of the final facade/runtime/chunk layouts.

## Test-first sequence

The earlier test-first layers are already merged into `main`:

1. #10253 adds the fail-closed runtime-failure harness and core integration regressions.
2. #10252 adds the later hardening fixtures and paired-mode coverage.

The remaining Graphite stack is:

1. #10287 establishes the final config/variant review layout and current-`main` snapshots while preserving all expected-failure markers.
2. #10104 (this PR) adds the implementation, removes all temporary failure markers, and records the implementation output.

The final tree contains 96 strict-enabled fixture configs and 200 named strict executions. Between #10287 and #10104, 21 config files only delete 44 expected-failure markers, and all 93 changed snapshots retain identical variant-heading order.

## Validation

Local validation ran on rebased head `79efe909bb3f0bcd2abcc76dcd33e58069c3474c`, based on `main` at `c4e25974b8ff7598bf507ae4a2d7602cbcbc86fd`.

- The 86 changed fixtures passed twice on #10287 with the old implementation; the second pass was snapshot-stable.
- All 96 strict-enabled fixture configs passed twice on this implementation, with a snapshot-stable final pass.
- Full Rust workspace tests passed with `just test-rust`.
- Rolldown Node tests passed: 793 main tests and 38 watcher tests.
- The Rollup compatibility suite passed 1,213 tests with 0 failures.
- `cargo check --workspace --all-features --all-targets --locked` and `cargo fmt --all --check` passed.
- Clippy passed with `--deny warnings` for every changed Rust crate: `rolldown`, `rolldown_common`, and `rolldown_ecmascript_utils`.
- `just lint-node` and `just lint-repo` passed.
- Full-workspace `just lint-rust` is locally blocked on Rust 1.97 by `clippy::ignore-without-reason` in the unchanged `rolldown_plugin_isolated_declaration/tests/mod.rs`; the failing path is byte-identical to `main`.

## Frozen closure evidence

The broader correctness campaign was frozen on one build of `42628c18b`:

- Differential fuzzer full matrix: 3,000/3,000 passed (ESM/CJS/mixed inputs × wrap-all/on-demand × 500 cases).
- Directed barrel/definer family: 600/600 passed; the same generated shapes failed about 23.7% of the time before the fix.
- Directed eager-barrel family: 20/20 passed in wrap-all and 20/20 in on-demand.
- #9887 strict-path bracket passed on the branch and failed 20/20 on released `[email protected]`. This PR restores strict mode as an escape hatch; the non-strict path remains a separate issue, so this PR does not close #9887.
- #10236 CJS-output/on-demand bracket passed on the branch and failed all 8 directed seeds on released `[email protected]`.
- ComfyUI, Camunda, shadcn-admin, and vue-vben-admin passed the flag-off/on-demand/wrap-all browser-probed matrix.

Fuzzer source and machine-readable evidence: https://github.com/hyf0/rolldown-order-fuzzer/tree/853fb775d1e949ced567deb0c163d044a031ba4f/.agents/evidence

## Boundaries and known residuals

- Top-level-await output remains legal, but this PR does not promise stronger TLA scheduling semantics than the default build.
- External-module evaluation order remains outside the strict-order guarantee; generated output must still be legal.
- #9998 is a separate bug in the non-strict/default code-splitting path and is not fixed here. The strict path covered by this PR passes its directed controls.
- Camunda still emits two `init_*` functions with no call site (`OverflowMenu$1` and `RadioButton$1`). They were harmless on all probed routes and have no synthetic reproduction.
- The partial-forwarder per-obligation refinement is present in the shared contract, but no constructible corpus shape currently exercises that narrower branch directly.
- On vue-vben-admin, the emergent-cycle fixpoint converged in two rounds and added 141 wrappers over the one-shot plan. `ROLLDOWN_ORDER_DEBUG=1` reproduces the per-round trace.
@graphite-app
graphite-app Bot force-pushed the codex/vite-chunk-execution-order-plan branch from 2ee23cc to 1d86ffe Compare July 17, 2026 08:56
graphite-app Bot pushed a commit that referenced this pull request Jul 17, 2026
## Summary

- Add the circular ESM initialization graph reported in #10265, whose legacy init-call transfer failure is addressed by #10270.
- Exercise both strict modes: `strictExecutionOrder: true` by itself (wrap-all), and strict execution order with `experimental.onDemandWrapping: true`.
- Execute the generated entry in both cells and assert that the circular `init_a` / `init_b` path completes without calling an uninitialized wrapper.

## What this proves

The failure addressed by #10270 comes from the legacy `ensure_lazy_module_initialization_order()` transfer moving an `init_*()` call before a later wrapper assignment. With #10104's new strict implementation, that function is still called but returns before running the transfer logic; the wrap-all or on-demand execution-order analysis owns scheduling instead.

The exact circular graph executes successfully in both cells, demonstrating that this failure mode is absent from both new strict paths. Current `main` at `03e1e3422` also passes both strict configurations through the old implementation, so this is path-specific regression coverage rather than a `main`-fails / parent-passes claim.

## Scope

This conclusion is limited to strict execution order, in wrap-all and on-demand modes. Default and non-strict output still use the legacy transfer logic and are outside this test.

Refs #10265 and #10270.

## Validation

- `CARGO_TARGET_DIR=/tmp/codex-rolldown-pr10104-target just t-run crates/rolldown/tests/rolldown/issues/10265/_config.json` (twice; snapshot-stable)
- Both strict cells also pass on `main` at `03e1e3422`, with different generated layouts
- `just lint-node`
- `just lint-repo`
- `git diff --check`
@graphite-app
graphite-app Bot merged commit 1d86ffe into main Jul 17, 2026
34 checks passed
@graphite-app
graphite-app Bot deleted the codex/vite-chunk-execution-order-plan branch July 17, 2026 09:00
graphite-app Bot pushed a commit that referenced this pull request Jul 19, 2026
…ep (#10334)

## Summary

Flag-off builds could emit cross-chunk imports — and therefore run side effects — in a different order than `main`, on the default path. When the post-chunking runtime sweep drops the runtime module out of a shared chunk it co-hosts with user modules, that chunk was left with an execution order keyed on the removed runtime (exec order `0`) instead of its first surviving module, so it sorted ahead of chunks it should have followed. This restores `main`'s ordering by re-deriving chunk execution orders after the sweep.

## Verified mechanism

- Before #10104 (`1d86ffe06`), `sweep_unused_runtime_module` ran in `generate_chunks` **before** chunk exec-order assignment (there was a comment requiring it). #10104 moved the sweep into `finalize_chunk_plan`, which runs **after** assignment, so the regression is live on `main` today.
- The Common-chunk comparator keys on `modules[0]`'s exec order; `sort_chunk_modules` always makes the runtime (module exec order `0`) `modules[0]`. So a chunk hosting the runtime keys on `0`.
- When the walk-back (`find_entry_level_external_module`) invalidates the runtime demand (the #9374 star-re-export-to-external shape) and the sweep strips the runtime from a still-live shared chunk, the chunk keeps `exec_order` derived from the now-absent runtime. `rebuild_sorted_chunk_idx_vec(true)` only re-sorts the stale values; `renumber_live_chunks` is strict-only. That stale key feeds both `sorted_chunk_idx_vec` and the cross-chunk import sort (`compute_cross_chunk_links.rs:110/124`), so an entry emits `import "./C.js"` before a chunk that should run first.

## Fix

Chunk exec orders must already be assigned before order analysis runs (the on-demand actual-order simulation sorts sibling chunks by `exec_order`), so assignment cannot simply move after the sweep. Instead:

- Extract the assignment loop into `assign_chunk_exec_orders` (`code_splitting.rs`); keep the provisional call where it was in `generate_chunks`.
- Re-run it in `finalize_chunk_plan` when the sweep actually removed the runtime from a chunk — the same condition that already triggered `rebuild_sorted_chunk_idx_vec(true)` — then rebuild the sorted vec.

**Final ordering contract (documented on `assign_chunk_exec_orders`).** Flag-off, `analyze_execution_order` returns `None`, so no lowering and no `renumber_live_chunks` run; the re-run is the sole authority and reproduces exactly what `main` produced by sweeping before assignment. Under strict, the sweep block only runs when `required_runtime_helpers()` is empty (no order wrapper demands the runtime); `renumber_live_chunks` (strict-only, after lowering) merely *compacts* existing `exec_order` values to close tombstone gaps and never re-derives them from `modules[0]`. The re-run happens after lowering, so whichever of the two touches `exec_order` last leaves every live chunk both densely numbered and keyed on its correct lead module — they never contend.

## Behavior / snapshot impact

- **New fixture only.** `topics/runtime/sweep_shared_chunk_exec_order` — three entries; `re.js` (`export * from 'external'`) makes tree-shaking include the runtime for `__reExport`, and the runtime folds into `re.js`'s shared `{entry_a, entry_b}` chunk. The walk-back flattens the star re-export to a chunk-level `export * from "external"`, so the post-order sweep drops the runtime from that still-live shared chunk. `entry_a` also imports the earlier side-effect chunk `s_ac.js` (shared `{entry_a, entry_c}`).
- **Main-parity proof.** The fixture was built against three states. Pre-#10104 `main` (`ce98ae798`) emits `import "./s_ac.js"` before `import "./re.js"`; current `main` emits `import "./re.js"` first (the runtime-hosting chunk keeping stale exec order `0`) — a real side-effect-order change on the default path; this branch restores the pre-#10104 order. The committed snapshot is therefore the pre-regression output, not an expectation written to match the fix. Reverting only the two source files onto this branch reproduces the failure, so the fixture discriminates.
- **No existing snapshot changes.** No other fixture failed under `INSTA_UPDATE=no`, which is what a changed snapshot would do, so no existing snapshot changed.

## Validation

- `cargo test -p rolldown` — 1877 passed here against 1876 on `main`, the difference being exactly the new fixture. Both runs share the same 5 environmental failures (`cjs_module_lexer_compat` ×3 + `npm_packages/util_deprecate` need `pnpm install`; `test262_module_code` needs the submodule). Run with `INSTA_UPDATE=no`: `.insta.yaml` sets `update: 'always'`, so a local run without it rewrites snapshots in place and reports success regardless — worth knowing for anyone reproducing the revert-check above.
- `cargo fmt --all --check` — clean.
- `cargo clippy -p rolldown --all-targets -- -D warnings` — clean.

---

Follow-up to #10104; addresses item 3 of #10104 (comment)
graphite-app Bot pushed a commit that referenced this pull request Jul 20, 2026
…nts (#10336)

## Summary

Chunk deconfliction filtered each module's statements on raw `stmt_info_included`, so a runtime helper statement that tree-shaking excluded but strict order lowering force-includes (e.g. `__esmMin`) was rendered and symbol-assigned yet never registered with the renamer. When a user module co-hosted with the runtime declares a top-level binding of the same name, the two share the chunk's root scope: the user's initializer overwrites the runtime helper and the next wrapper's `__esmMin(...)` call throws. This ORs the order overlay into deconfliction's inclusion test, matching the two other consumers.

## Verified mechanism

- `compute_cross_chunk_links.rs:438` and `module_finalizers/mod.rs:1623` both OR `order_wrap_state.forces_runtime_stmt(...)` into their inclusion test; `deconflict_chunk_symbols.rs:137` filtered on `meta.stmt_info_included.has_bit(...)` alone.
- A single-chunk strict build (`codeSplitting: false`) places the runtime next to user code (`order_wrapping.rs:451-457`). Pure ESM demands no helper at link time, so tree-shaking drops `__esmMin` and strict order lowering force-includes it — the exact statement the deconflict filter dropped.

## Fix

`deconflict_chunk_symbols` already receives `order_wrap_state` and `link_output`, so the change is a one-line mirror of the existing two sites: `has_bit(idx) || forces_runtime_stmt(&link_output.runtime, module.idx, stmt_info)` (`forces_runtime_stmt` self-guards `module_idx == runtime.id()`, so it is inert for every non-runtime module).

**Item 7 (maintainability).** I kept the inline `||` rather than extracting a shared "is this statement rendered?" accessor: `forces_runtime_stmt` is already the shared half, the other two sites use this exact inline form, and a shared method would have to take `meta` + `stmt_idx` + `stmt_info` (coupling `OrderWrapState` to `LinkingMetadata`) and rewrite two already-correct sites — a larger diff than the reviewer's "keep the diff small" guidance wants for a fix. The three sites now read identically.

## Behavior / snapshot impact

- **Full suite: zero existing snapshots change.** No current fixture co-hosts a user binding named after a force-included runtime helper, so the added disjunct fires nowhere else.
- **One new fixture** `function/experimental/strict_execution_order/deconflict_forced_runtime_helper` (strict, `codeSplitting: false`): `helper.js` declares a top-level `__esmMin` (hoisted to a root-scope `var __esmMin` by its order wrapper) co-hosted with the runtime; `late.js` is ordered after it.
  - **Broken output verified on the unfixed base:** two `var __esmMin` at root — the runtime helper and helper.js's binding. `helper`'s init reassigns the shared binding to the user string, and `init_late = __esmMin(...)` then throws `TypeError: __esmMin is not a function` (the fixture fails to execute).
  - **Fixed:** deconflict registers the runtime statement, so the renamer renames the runtime helper to `__esmMin$1`; every wrapper calls `__esmMin$1(...)` and the user's `__esmMin` keeps its name. Snapshot pins the rename; `_test.mjs` asserts the executed result `{ helper: 'H:USERVAL', late: 'LATE:z' }`.

**Note on wrap-all vs on-demand, and the co-hosting mechanism.** The reproduction is a wrap-all strict build (`strictExecutionOrder` without `onDemandWrapping`) because it deterministically co-hosts the runtime with user code in one chunk. The collision needs the runtime co-hosted with a user module *at deconflict time*. `try_merge_runtime_chunk` can co-host the standalone runtime into a user consumer chunk even in multi-chunk builds, but under strict order wrapping `ensure_runtime_module_for_order_wraps` (`order_wrapping.rs:395-457`) relocates a co-hosted runtime (`modules.len() > 1`) back to a standalone chunk unless code splitting is disabled — so at deconflict time the runtime sits next to user code only in single-chunk (`codeSplitting: false`) builds. (This stack's own multi-chunk strict on-demand fixtures, e.g. `facade_gate_emergent_edge`, confirm it — they emit a standalone `rolldown-runtime.js` chunk.) In a single chunk on-demand finds no cross-chunk execution-order hazard, so it wraps nothing and demands no `__esmMin`; wrap-all is the frame that both co-hosts the runtime and force-includes the helper. The fix itself is mode-independent: `forces_runtime_stmt` keys on `required_runtime_helpers()`, covering on-demand and wrap-all and both the `__esmMin` and `__esm` (profiler_names) variants.

## Validation

- `cargo test -p rolldown` — 1872 passed (+1 new fixture, executed under node); only the 5 known environmental failures (`cjs_module_lexer_compat` ×3 + `npm_packages/util_deprecate` need `pnpm install`; `test262_module_code` needs the submodule). No existing snapshot changed (the local `symbols_ns2` drift is pre-existing `node_modules`-resolution noise, reproducible with this change reverted, and is not committed).
- `cargo fmt --all --check` — clean; `cargo clippy -p rolldown --all-targets -- -D warnings` — clean.

---

Follow-up to #10104; addresses item 2 (and the item-7 maintainability note) of #10104 (comment)
graphite-app Bot pushed a commit that referenced this pull request Jul 21, 2026
…r-sensitive (#10322)

## Summary

Fixes the class order-sensitivity gap found while reviewing #10104. A class can be side-effect-free for tree shaking while its definition still reads mutable global state from an extends expression, computed key, static initializer, or static block. Reordering that class across a module that patches the global can change behavior.

When Oxc certifies the whole class as side-effect-free, Rolldown now runs an independent definition-time visitor that collects only GlobalVarAccess and PureAnnotation reasons. It never invokes or short-circuits on tree-shaking analysis. Therefore a PureCJS target write remains tree-shaking-only, while its RHS and later siblings are still visited.

The visitor covers evaluated binding defaults, computed keys, assignment RHS, direct IIFEs, and nested classes. It skips deferred function and method bodies and instance initializers.

The tree-shaking channel is structurally unchanged: the original whole-class Oxc boolean remains its only input, and the new visitor can only add order-sensitive reasons.

## Regression coverage

- PureCJS-only and literal binding-default cases remain order-insensitive.
- PureCJS followed by a global read or pure-annotated call is order-sensitive in either operand order.
- Static-block binding defaults, computed member keys, assignment RHS, direct IIFEs, heritage expressions, and nested class-definition work are covered.
- Deferred arrow/function bodies, method bodies, and instance initializers remain order-insensitive.

## Validation

- cargo test -p rolldown --lib — 87 passed.
- cargo test -p rolldown — 87 unit and 49 integration tests passed; the only failure was the existing missing local test262 submodule.
- cargo clippy -p rolldown --lib --tests -- -D warnings — clean.
- cargo fmt --all -- --check — clean.

Follow-up to #10104; addresses Major 1 from #10104 (comment).

Draft, rebased directly onto main.
hyfdev added a commit that referenced this pull request Jul 21, 2026
…llector unification

#10104 replaced the finalizer's private wrapped-esm init-target collector with the
shared one, changing flag-off interop emission in two ways: star re-export
records now iterate sorted, non-ambiguous resolved exports (ambiguous-export
owners no longer receive a per-record init; order is name-order not hash-order),
and named/namespace specifiers are liveness-gated per importer (an `import * as
ns` consumer that only reads `ns.x` no longer inits every wrapped exporter).

Two directed flag-off fixtures (no strictExecutionOrder) execute under node and
observe side-effect order:
- namespace_liveness_gating: a wrapped barrel star-re-exports two side-effectful
  wrapped leaves; the entry reads only `ns.a`. Both leaves still run (A, B) via
  the barrel's own init, which calls init_leaf_a and init_leaf_b.
- ambiguous_star_reexport: two leaves both export `x` (ambiguous -> undefined);
  both owners still run (A, B) via the barrel's init.

Decision: every side effect still executes — just via the barrel's init rather
than redundant per-consumer init sites — and only ordering/bytes changed. The
collector unification narrowed redundant per-site inits without dropping
semantics. No regression found.
hyfdev added a commit that referenced this pull request Jul 21, 2026
#10104 changed flag-off wrapped-ESM init lowering while sharing strict-order obligation machinery. Pin three resulting paths without enabling strictExecutionOrder.

Keep immediate re-export barrels unwrapped and in a shared chunk while later require calls make wrapped leaf init symbols reachable. This reaches the shared collector and covers importer-local namespace liveness plus exclusion of ambiguous star exports.

Also cover codeSplitting: false with an excluded re-export to a TLA-tainted leaf. The carrier wrapper must await the forwarded init before executing its later statements.

All three generated outputs execute under Node.
hyfdev added a commit that referenced this pull request Jul 21, 2026
#10104 changed flag-off wrapped-ESM init lowering while sharing strict-order obligation machinery. Pin three resulting paths without enabling strictExecutionOrder.

Keep immediate re-export barrels unwrapped and in a shared chunk while later require calls make wrapped leaf init symbols reachable. This reaches the shared collector and covers importer-local namespace liveness plus exclusion of ambiguous star exports.

Also cover codeSplitting: false with an excluded re-export to a TLA-tainted leaf. The carrier wrapper must await the forwarded init before executing its later statements.

All three generated outputs execute under Node.
graphite-app Bot pushed a commit that referenced this pull request Jul 21, 2026
## Summary

Adds three executable **flag-off** regression fixtures for wrapped-ESM init emission changes that landed with #10104. None enables `strictExecutionOrder`, and this PR changes no production source.

The earlier version of this PR required each barrel directly, which wrapped the immediate importee and bypassed the shared init-target collector it claimed to cover. The two collector fixtures now keep the immediate barrel at `WrapKind::None` in a shared chunk, while later `require()` calls wrap the leaf owners and make their init symbols reachable from the main chunk.

## Coverage

### Importer-local namespace liveness

`namespace_liveness_gating` imports the barrel namespace but reads only `ns.a()`. A second entry places the unwrapped barrel in a shared chunk; later requires make both wrapped leaf init symbols reachable.

- Current output emits only `init_leaf_a()` at the namespace-import position. `init_leaf_b()` stays at its later explicit require position.
- Pre-#10104 output emitted `init_leaf_a(), init_leaf_b()` together at the namespace-import position.
- The shared barrel still evaluates both leaves exactly once; runtime order is `A, B, BEFORE_REQUIRE:a, MAIN`.

### Sorted, non-ambiguous star-export routing

`ambiguous_star_reexport` has two wrapped leaves that both export `x`. The entry uses a real `export * from` record; a dynamic CommonJS re-export keeps that record included, while the second entry keeps the immediate barrel unwrapped and in another chunk.

- Current output excludes ambiguous `x` from the collector's sorted, non-ambiguous export map and emits no owner init before `__reExport(...)`.
- Pre-#10104 output selected an ambiguous owner and emitted `init_mod_a()` before `__reExport(...)`.
- Runtime confirms `x` is absent and both owners still evaluate exactly once through the shared barrel.

### Excluded re-export to a TLA wrapper

`excluded_reexport_tla` uses `codeSplitting: false`, so an inline dynamic import wraps the barrel and its static dependency chain even with strict execution order disabled. With `moduleSideEffects: false`, the carrier's unused re-export is excluded while the same TLA leaf remains live through a separate `used` import.

- Current output emits `await init_leaf()` in the carrier's async wrapper before its later statement.
- Pre-#10104 output emitted bare `init_leaf()`, producing `CARRIER:undefined` before the leaf's top-level await completed.
- Current runtime order is `LEAF:before, LEAF:after, CARRIER:ready, BARREL:used:marker`.

## Behavior / snapshot impact

Tests only: two existing fixtures are corrected, one TLA fixture is added, and their generated snapshots are pinned. No existing production code or unrelated snapshot changes.

## Validation

- Rebased onto `main` at `cccf1d0d849c71e80c02fe3ff17a97192f3bf258`.
- Generated all three fixtures with the macOS native binding from current-main CI [run 29795703407](https://github.com/rolldown/rolldown/actions/runs/29795703407); every `_test.mjs` passed under Node.
- Compared each committed snapshot byte-for-byte with those generated assets after applying the integration harness's runtime-module hiding.
- Re-ran the same sources with the pre-#10104 binding from CI [run 29559371389](https://github.com/rolldown/rolldown/actions/runs/29559371389): both collector fixtures show the obsolete extra init, and the TLA fixture logs `CARRIER:undefined`.
- `vp fmt ... --check`, `typos`, JS syntax checks, JSON parsing, and `git diff --check` pass. The pre-commit `vp check --fix` hook also completed cleanly.

- CI [run 29819995357](https://github.com/rolldown/rolldown/actions/runs/29819995357): Repo, Rust, Node, Cargo, WASI, native-build, and Node 20/22/24 test jobs passed. Vite Test Ubuntu and the three Ubuntu Dev Server jobs stopped before tests because the Vite `rolldown-canary` branch no longer rebases cleanly onto `vite/main` (`pnpm-lock.yaml` conflict); this is unrelated Vite checkout noise.

## AI usage disclosure

Per the [AI Usage Policy](https://rolldown.rs/contribution-guide/#ai-usage-policy), OpenAI Codex was used to rebase and review the PR, design the corrected regression shapes, and verify generated output. I reviewed the final diff and validation evidence.

---

Follow-up to #10104; addresses Major 2b/2c from the review discussion: #10104 (comment)
graphite-app Bot pushed a commit that referenced this pull request Jul 22, 2026
…nistic and constant-time (#10320)

## Summary

Two independent, cheap fixes to the strict-execution-order cross-chunk link pass (review items 3 and 4):

- **Cross-target determinism.** `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 rename order (the `$1`/`$2` suffixes) in `deconflict_chunk_symbols` — could diverge between native and wasm32/WASI builds. Now iterated sorted by the owning `StmtInfoIdx`, pinning one order everywhere. It is the single hash-ordered site three review passes converged on; every other hash-ordered effect is already sorted before use.
- **Quadratic strict-mode hot loop.** `module_is_in_live_chunk` did a linear `chunk.modules.contains` scan, called for every canonical referenced symbol of every included statement under strict — quadratic on large chunks. Replaced with an O(1) `module_to_chunk` + liveness check.

The O(1) rewrite is verified semantically equivalent: `add_module_to_chunk` sets `module_to_chunk[m]` and pushes `m` onto `c.modules` together, and every site that later removes a module from a chunk's `modules` list preserves the invariant (`module_to_chunk[m] == Some(c) && c live ⟹ c.modules ∋ m`) by pairing the removal with clearing `module_to_chunk[m]` (unused-runtime sweep), reassigning it to the destination chunk via `add_module_to_chunk` (runtime relocation, runtime→target merge, `apply_common_chunk_merges`), or marking the emptied 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. The invariant is documented in the function's doc comment.

## Behavior / snapshot impact

**None.** Zero fixture snapshot changes. The determinism fix is invisible on native 64-bit (its value is native-vs-wasm parity, which the native suite cannot exercise); the O(1) change is a pure performance rewrite with identical results.

## Validation

- `cargo test -p rolldown` — 1868 passed; snapshot-stable (zero deltas). The only failures are 5 pre-existing environmental ones unrelated to this change (`cjs_module_lexer_compat` ×3 and `npm_packages/util_deprecate` need an installed `cjs-module-lexer`; `test262` needs the test262 submodule).
- `cargo fmt --all --check` — clean.
- `cargo clippy -p rolldown --all-targets -- -D warnings` — clean.

---

Follow-up to #10104; addresses the two "cheap pre-merge fixes" (items 3 and 4) from the review discussion: #10104 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants