Skip to content

feat(deps): multi-version mangling — Level 1 fallback#19

Merged
Sunrisepeak merged 1 commit intomainfrom
feat/multi-version-mangling
May 9, 2026
Merged

feat(deps): multi-version mangling — Level 1 fallback#19
Sunrisepeak merged 1 commit intomainfrom
feat/multi-version-mangling

Conversation

@Sunrisepeak
Copy link
Copy Markdown
Member

Summary

Layer 1 of the 0.0.3 dep-resolution work. Wraps up the three-step strategy on main along with PR #17 (transitive walker) and PR #18 (SemVer merge).

When the SemVer merger can't find a single version satisfying every consumer (typically =0.0.1=0.0.2 or any genuine cross-major split), the resolver no longer hard-errors. Instead it stages the secondary version's source under target/.mangled/<consumer>/..., rewrites (export )?module X; / (export )?import X; to a mangled name (<X>__v<M>_<m>_<p>__mcpp), and the direct consumer is staged + rewritten too so its import points at the mangled BMI. Both BMIs coexist in the build graph at distinct module names; C++23 module attachment handles ABI isolation, so no namespace mangling is needed.

Pieces

  • src/pm/mangle.cppm — pure helper (name format + regex-free source rewriter), 11 unit tests
  • cli.cppm resolver conflict branch — invokes mangling on try_merge_semver failure; tracks consumerDepIndex per WorkItem; renames the staged secondary's [package].name so the modgraph validator's prefix check passes
  • build/plan.cppm + ninja_backend.cppm — basename-collision detection: when two compile units across packages share a filename (e.g. cmdline 0.0.1 and staged-mangled 0.0.2 both have parse.cppm), colliding objects get obj/<sanitized-pkg>/<file>; .ddi placement follows. Single-package builds keep the original layout
  • tests/e2e/33_multi_version_mangling.sh — Y-shape: libA→cmdline 0.0.1, libB→cmdline 0.0.2; build succeeds with both BMIs on disk
  • tests/e2e/32_semver_merge.sh — irreconcilable case dropped (now handled by Level 1)

MVP scope

  • Conflicting consumer must be a dep (not the main package)
  • Secondary version must be a leaf (no own transitive deps)

Both limits surface as clear error messages with the workaround (pin one version explicitly). Recursive mangling for deeper graphs is a follow-up.

Test plan

  • tests/unit/test_mangle.cpp — 11 tests for name format + rewriter
  • tests/e2e/33_multi_version_mangling.sh (new)
  • tests/e2e/32_semver_merge.sh (updated)
  • tests/e2e/31_transitive_deps.sh still green
  • CI green on linux x86_64 self-host

… coexistence

When SemVer merge fails (typically two non-overlapping pins like
=0.0.1 ⨯ =0.0.2 across two consumers of the same package), instead of
hard-erroring the resolver now stages the secondary version's source
under `target/.mangled/<consumer>/...` and rewrites its module
declarations to a mangled name (`<X>__v<M>_<m>_<p>__mcpp`). The
direct consumer's source is staged + rewritten too so its `import X;`
points at the mangled secondary BMI. Both BMIs coexist in the build
graph at distinct module names; C++23 module attachment isolates
their internal symbols, so no namespace mangling is needed.

Touches:
- `src/pm/mangle.cppm` (new) — pure helper: name format + a regex-free
  source rewriter that handles `(export )?module X;`,
  `(export )?module X:Y;`, `(export )?import X;`, `import X:Y;`. Bare
  partition imports (`import :Y;`) and the global module fragment
  (`module ;`) are left intact. 11 new unit tests in
  `tests/unit/test_mangle.cpp`.
- `src/cli.cppm` — resolver conflict branch: when `try_merge_semver`
  fails, run mangling instead of returning. WorkItem gains a
  `consumerDepIndex` field so the fallback can locate the consumer
  manifest for staging. Resolved record uses the mangled module name
  as `[package].name` so the modgraph validator's "module must be
  prefixed by package name" check passes.
- `src/build/plan.cppm` + `ninja_backend.cppm` — when two compile
  units share a basename across packages (e.g. cmdline 0.0.1 and
  staged-mangled 0.0.2 both have `parse.cppm`), the colliding object
  paths get a per-package subdirectory (`obj/<sanitized-pkg>/<file>`).
  `.ddi` placement follows. Single-package builds keep the original
  `obj/<file>` layout so cache hashes don't churn.
- `tests/e2e/33_multi_version_mangling.sh` (new) — Y-shape graph:
  libA pinned to cmdline 0.0.1, libB pinned to cmdline 0.0.2; the
  build succeeds with both BMIs (`mcpplibs.cmdline.gcm` +
  `mcpplibs.cmdline__v0_0_2__mcpp.gcm`) on disk and the runtime
  output matches.
- `tests/e2e/32_semver_merge.sh` — drop the previously-irreconcilable
  case; that's now Level 1's territory and is covered by test 33.

MVP scope (clear errors out of bounds):
- Conflicting consumer must be a dep, not the main package.
- Secondary version must be a leaf (no own transitive deps).
Both surfaces are documented in the error message so users know
which limit they hit and have a workaround (pin one version).

CHANGELOG 0.0.3 entry: covers all three resolution levels (transitive
walker / SemVer merge / mangling) and the obj-path-namespacing
side-fix to the build backend.
@Sunrisepeak Sunrisepeak merged commit bb8ea8a into main May 9, 2026
1 check passed
@Sunrisepeak Sunrisepeak mentioned this pull request May 9, 2026
3 tasks
Sunrisepeak added a commit that referenced this pull request May 9, 2026
- mcpp.toml + MCPP_VERSION → 0.0.3
- CHANGELOG: lock [Unreleased] → [0.0.3] — 2026-05-10

Three-tier dependency resolution lands in this release: transitive
walker (#17), SemVer merge / Level 2 (#18), multi-version mangling /
Level 1 (#19). See the 0.0.3 entry for the full breakdown.
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