Skip to content

chore - make a module split free by keying identity on the exported path #1505

Description

@dannymeijer

What

Reorganising a module today invalidates every consumer of the declarations that moved, even when the public API is byte-for-byte unchanged and every moved item is re-exported under its original path.

Splitting foo.incn into foo/mod.incn plus foo/detail.incn, with foo/mod.incn re-exporting everything, changes nothing a source-level consumer can see. It still renames every symbol that moved, still changes the unit's semantic digest, and still forces every dependent to recompile.

Why it happens

Two independent encodings of the declaring module path reach the final symbol, and both are ours:

  1. RFC 120's incan-v1 projection encodes the complete CanonicalSymbolId, and encode_origin writes SymbolOrigin::Module(path) segments into the payload. Ordinary source functions emit as fn __incan_v1_<payload>; the codegen snapshot suite asserts this.
  2. Emitted Rust mirrors Incan modules one-to-one (use crate::functions::col;, with the __INCAN_INSERT_MODS__ insertion point), so rustc mangles a def path we chose. There is no #[no_mangle] or #[export_name] in the emitter.

CanonicalSymbolId::origin also reaches the semantic digest through Body::canonical, so the unit's own identity moves too — not only its symbol.

Worth being precise about whose constraint this is: rustc imposes neither encoding. The genuinely external part is narrower — rustc has no "the dependency changed but my output need not", so any change to an upstream rlib recompiles downstream. That means holding symbol names stable while still producing a different rlib buys nothing. The property only pays if the split produces an identical unit, which means the Incan module structure must not reach the artifact at all.

What would fix it

Key identity on the exported path rather than the declaring path, at both layers:

  • emit flat, with item names derived from the exported path;
  • key SymbolOrigin on the exported path so the semantic digest does not move either.

A split preserving the public API then becomes invisible end to end: the upstream unit does not rebake, so no dependent ever sees a changed rlib.

Why this is not a small change

  • Two declarations named foo in different submodules collide at a flat root and need a disambiguator that is not the declaring path.
  • Backtraces, debug info, and incan inspect rust output all get less legible when the symbol stops naming where the code lives.
  • A declaration exported under two paths, or none, needs a defined answer.
  • SymbolOrigin is specified by RFC 120, which is closed. This is a superseding RFC, not a patch.

Evidence

Pinned by moving_a_declaration_into_a_submodule_changes_its_emitted_symbol in crates/incan_semantics_core/src/emitted_symbol.rs, and stated on encode_incan_symbol_identity so the property is met where it is created.

Recommendation

Not now. The cost is concentrated in naming and debuggability, and the benefit only materialises for projects that reorganise modules often. Nothing measured so far says that is common enough to pay for. Filed so the option is on record with its real price rather than rediscovered as a defect — a module split rebaking dependents is correct behaviour under the current model, not a bug.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    • Status
      Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions