Skip to content

compile: prune unused side-effect-free re-export subgraphs - #10245

Closed
proggeramlug wants to merge 5 commits into
PerryTS:mainfrom
proggeramlug:feat/10180-prune-unused-reexports
Closed

proggeramlug wants to merge 5 commits into
PerryTS:mainfrom
proggeramlug:feat/10180-prune-unused-reexports

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

OpenCode v1.18.30 collects unused siblings behind package barrels. This change removes 971 modules before HIR lowering/codegen: 7,897 → 6,926, including Remeda's generated mixed forwarding barrel (170 → 30 modules). Eager modules fall from 2,433 to 2,232. No version bump.

The issue's acceptance target remains unmet: the paired Bun build has 4,068 inputs, and OpenCode native CPU/.text measurements remain blocked by an existing CommonJS getter-forwarding linker failure. This PR provides the pruning implementation and measured progress; it should not automatically close #10180.

Behavior

  • Propagate named export demand to a fixed point through aliases and wildcard chains. Postpone unused re-exports only when the exporter and the target's complete static dependency tree permit omission.
  • Honor package sideEffects: false and supported glob arrays. When metadata is absent, conservatively prove inert initialization from the AST, including first-party/workspace modules. Explicit effectful or unknown contracts veto inference. Calls, property/binding reads, destructuring, classes, unknown loads, and unsupported constructs retain their modules.
  • Normalize import { x }; export { x } forwarding, including barrels containing bare imports and other re-exports. Preserve genuine bare imports. Require a pure, acyclic static tree before normalization; cyclic initialization entry points and effectful dependency order remain intact.
  • Preserve ordinary imports used by module code, live bindings, namespace consumers, dynamic-import targets, import attributes, and package aliases. Dynamic demand can reactivate postponed exports. Keep relative-import resolution consistent through source-visible symlink paths.
  • Track omitted sources and package manifests in cache invalidation. Report the pruned count in the compile summary. PERRY_NO_REEXPORT_PRUNE=1 disables pruning; PERRY_COLLECT_ONLY=1 writes audit.json and module-graph.json without codegen/linking. Both controls are documented.

Dynamic-only code remains compiled and available; this change does not split it into separate native binaries or deduplicate installed package versions.

OpenCode measurement

Windows x64, OpenCode tag v1.18.30, locked Bun 1.3.14 installation, built packages/app/dist, entry packages/opencode/src/index.ts, Bun platform condition, OPENCODE_MODELS_DEV=undefined, and the generated web-UI asset module. Paired collection with pruning disabled/enabled:

Metric Disabled Enabled
Collected modules 7,897 6,926
Eager modules 2,433 2,232
Deferred modules 5,464 4,694
Reported pruned modules 0 971
@ai-sdk/provider 648 148
@ai-sdk/provider-utils 1,012 808
remeda 170 30
effect 294 293
ai 227 225
openai 152 152

After normalizing the cache-local generated asset path, the enabled graph is a strict subset: 971 removed, zero added (12.3% fewer modules; 8.3% fewer eager modules). All 15 Prettier and 257 Babel modules are deferred in both graphs. TypeScript uses the native binding and has zero collected source modules. These are graph observations; running OpenCode --version remains unverified.

The Bun metafile has 4,068 inputs, including 24 Remeda inputs. Its build externalizes the generated asset module and platform-specific @opentui/core-* packages. The issue's reference is 4,063. Dynamic provider namespaces still retain their full export surfaces and installed dependency versions; Perry source files and Bun bundled distribution files also differ in granularity. The remaining gap is explicit, not an acceptance pass.

Native fixture measurement

Using the exact locked Remeda 2.26.0 package, import { sum } from 'remeda'; console.log(sum([1, 2, 3]));:

Metric Disabled Enabled
Modules, including entry 171 16
Pruned modules 0 155
Executable bytes 26,044,416 15,488,000
.text bytes (llvm-size) 20,084,230 12,212,998
stdout 6 6

Both native executables match Node 26.5.1. This fixture reduces .text by 39.2%; it is not an OpenCode size measurement.

Validation and limits

  • All 52 source-graph integration tests pass, including nine added in this update. They exercise audit membership and native execution against Node/pruning-disabled output: inert and effectful siblings, transitive effects, aliases/star chains, mixed barrels, cycles, dynamic namespaces/deferred init, live mutation, contracts, cache invalidation, attributes, and symlink-relative resolution. The new symlink case fails on the preceding build and passes with the correction.
  • Five pruning unit tests and four build-cache unit tests pass. The broader collector unit run passes 30/31; its Windows extended-path diagnostic assertion also fails with pruning disabled.
  • All 12 test_gap_dynamic_import* parity tests pass on Node 26.5.1, with zero skips, compile failures, or crashes. The full gap suite was not rerun for this update; snapshots are unchanged.
  • cargo check --locked -p perry --no-default-features --features dev-cli -j4, targeted rustfmt checks, git diff --check, file-size, test-registration, docs, and Node-version consistency checks pass. Targeted rustfmt is used because cargo fmt --all exceeds Windows' command-length limit.
  • Windows native tests use LLVM 22.1.8 and PERRY_RS4GC=0 for the existing WinEH/statepoint restriction (Windows: native-root stack walker so PERRY_RS4GC=1 works there (#7173) #7354). Compiler and full runtime/stdlib archives use the same explicit build stamp. The source-graph integration target is built through a temporary Cargo manifest pointing directly at the repository's actual test file, with CARGO_BIN_EXE_perry and PERRY_RUNTIME_DIR selecting that build.

The existing Babel blocker is independently reproducible with the final compiler and full archives: a CommonJS package exposing transformAsync through Object.defineProperty(exports, ..., { get: function () { return _transform.transformAsync; } }) fails to link its public function symbol with pruning both enabled and disabled. Node prints 42. The prior PR revision's full OpenCode build failed at this same getter-forwarded symbol. Therefore this update does not report OpenCode native CPU, binary size, or CLI parity as verified.

CI is not all green. Current failures report the existing raw thread_local! in unchanged crates/perry-runtime/src/regex/perex_owner.rs, unused functions in unchanged global_this_webassembly.rs, stale public benchmark inputs, and stale documentation gettext catalogs. Other checks may still be running; this PR does not claim a green full CI run.

Refs #10180

Summary by CodeRabbit

  • New Features

    • Unused re-exports are now pruned when their dependency trees are proven side-effect-free, including eligible first-party modules and packages without metadata.
    • Added collect-only mode to generate module-graph audit output without code generation or linking.
    • Compilation summaries now report the number of pruned re-exports.
  • Bug Fixes

    • Improved build-cache invalidation when configuration files or pruning settings change.
  • Documentation

    • Updated CLI documentation covering inferred purity, side-effect contracts, re-export pruning, collect-only mode, and audit files.

@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b03cb77f-0c1c-4be2-b65a-a810072140aa

📥 Commits

Reviewing files that changed from the base of the PR and between 432a2bc and 24b5305.

📒 Files selected for processing (4)
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/collect_modules/reexport_prune.rs
  • crates/perry/src/commands/compile/collect_modules/reexport_prune/scan.rs
  • crates/perry/tests/source_graph_export_regressions/issue_10180/purity.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The compiler now prunes unused re-exports when static dependency trees are proven side-effect-free through contracts or conservative purity analysis. It propagates demand, preserves unsafe cases, supports collect-only graph output, updates cache behavior, and adds regression coverage and documentation.

Changes

Re-export pruning

Layer / File(s) Summary
Purity and dependency analysis
crates/perry/src/commands/compile/collect_modules/reexport_prune/*, crates/perry/src/commands/compile/types.rs
Adds AST purity analysis, dependency scanning, sideEffects contract checks, glob matching, and ReexportPruner state.
Demand propagation and forwarding normalization
crates/perry/src/commands/compile/collect_modules/reexport_prune.rs, crates/perry/src/commands/compile/collect_modules/reexport_prune/forwarding.rs
Tracks named and namespace demand, settles re-export edges to a fixpoint, removes inactive edges, and normalizes qualifying forwarding barrels while preserving bare imports and cycle-sensitive imports.
Collection and compile integration
crates/perry/src/commands/compile/collect_modules.rs, crates/perry/src/commands/compile/collect_modules/walk.rs, crates/perry/src/commands/compile/run_pipeline.rs, crates/perry/src/commands/compile/bootstrap.rs
Integrates pruning into collection and recollection. Finalization writes module-graph data and collect-only builds return before code generation.
Cache and diagnostic controls
crates/perry/src/commands/compile/build_cache.rs
Keys the cache with PERRY_NO_REEXPORT_PRUNE, detects changed configuration paths, and treats PERRY_COLLECT_ONLY=1 as diagnostic mode.
Regression coverage and documentation
crates/perry/tests/source_graph_export_regressions/*, docs/src/cli/flags.md, changelog.d/10180-prune-unused-reexports.md
Adds coverage for purity, contracts, demand restoration, dynamic imports, cycles, forwarding, caching, and collect-only mode. Documents the updated pruning rules.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant CompilePipeline
  participant ModuleCollector
  participant ReexportPruner
  participant NativeModules
  CompilePipeline->>ModuleCollector: Collect modules
  ModuleCollector->>ReexportPruner: Record imports and re-export edges
  ReexportPruner->>ModuleCollector: Return newly activated paths
  ModuleCollector->>NativeModules: Remove inactive re-export edges
  CompilePipeline->>ReexportPruner: Write module graph in collect-only mode
Loading

Merge Risk: ⚪ Minimal · up to 24b53

The pruning change is ready to merge based on the available evidence; no concrete behavior regression remains identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.85% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 81 functions across 14 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: pruning unused side-effect-free re-export subgraphs during compilation.
Description check ✅ Passed The description provides a detailed summary, behavioral changes, issue reference, measurements, validation results, known limitations, and test coverage. It does not reproduce the template headings or…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed in merge train #10261: #10261. The merged main tree matches the validated train, and the fresh-head patch audit confirms the changes arrived.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant