journal: 2026-05-08 codegen split tidy
Records the four-phase reorganisation that moved 2470 lines out of lib.rs into purpose-named sibling submodules. Captures the visibility model (descendant-module privacy + selective pub(crate)), the deliberate non-extractions (lower_term, the app/call cluster), and the framing as a navigability tidy rather than a feature-driven change.
This commit is contained in:
+100
@@ -8351,3 +8351,103 @@ post-18g-tidy follow-ups: all closed. Decision 9 has been
|
||||
re-framed (commit `f10a77e`) to match the orchestrator's
|
||||
dual-allocator stance. Next iter is queue-driven; nothing
|
||||
load-bearing is open.
|
||||
|
||||
## 2026-05-08 — codegen split: lib.rs from 5295 → 2825 lines
|
||||
|
||||
User asked mid-session "wie groß ist die codebase mittlerweile?
|
||||
Kannst du sie noch kontrollieren?" The honest answer was that
|
||||
`crates/ailang-codegen/src/lib.rs` was the one file flagged for
|
||||
navigability — 5295 lines monolithic, every codegen concern
|
||||
sharing a single namespace. Decision delegated by the user
|
||||
("Das musst du selbst entscheiden"). With the 18g family closed
|
||||
and tests dense, the window for a mechanical move-only refactor
|
||||
was as good as it gets.
|
||||
|
||||
Executed as four sequential commits, full
|
||||
`cargo test --workspace` between each, no behaviour change.
|
||||
Each phase moved a coherent cluster into a sibling submodule
|
||||
under `crates/ailang-codegen/src/`:
|
||||
|
||||
- **Phase 1** (`84ba83d`): `synth.rs` (216 lines) +
|
||||
`subst.rs` (319 lines). The free-function tail of lib.rs:
|
||||
LLVM IR shaping helpers and the monomorphisation
|
||||
substitution pipeline.
|
||||
- **Phase 2** (`86989a7`): `drop.rs` (696 lines). All RC-
|
||||
allocator drop work — per-type drop fns, per-let-close
|
||||
drop dispatch, closure-pair drops.
|
||||
- **Phase 3** (`86406ac`): `match_lower.rs` (935 lines).
|
||||
`Term::Ctor`, `Term::ReuseAs`, `Term::Match` lowering. The
|
||||
`lower_match` body (≈500 lines) is the single largest
|
||||
method left in the project; moving it gave the biggest
|
||||
navigability win.
|
||||
- **Phase 4** (`bea5c92`): `lambda.rs` (447 lines).
|
||||
`Term::Lam` + closure machinery (capture analysis, env
|
||||
construction, deferred per-pair drop fns).
|
||||
|
||||
Final layout (lib.rs is now the Emitter setup + the
|
||||
`lower_term` mass dispatcher + the lookup helpers + tests):
|
||||
|
||||
```
|
||||
lib.rs 2825 Emitter struct, lifecycle, lower_term
|
||||
dispatcher, app/eq/effect lowering,
|
||||
lookup helpers, synth_arg_type,
|
||||
monomorphisation entry, tests.
|
||||
match_lower.rs 935 Term::Ctor / ReuseAs / Match.
|
||||
escape.rs 722 escape analysis (predates split).
|
||||
drop.rs 696 per-type + per-let-close drops.
|
||||
lambda.rs 447 Term::Lam + closure machinery.
|
||||
subst.rs 319 substitution + unification.
|
||||
synth.rs 216 IR shaping helpers + builtin types.
|
||||
```
|
||||
|
||||
### Visibility model
|
||||
|
||||
Rust's "private = visible to module + descendants" rule
|
||||
made the split cheap. The `Emitter` struct and its private
|
||||
fields stay private in `lib.rs`; submodules (drop, lambda,
|
||||
match_lower) can read those fields directly through normal
|
||||
descendant-module privacy. Only methods that lib.rs calls
|
||||
back into the submodule needed `pub(crate)` (the entry-point
|
||||
methods); helpers used only inside a submodule stayed
|
||||
private. Mirroring this, a small set of lib.rs-side helpers
|
||||
that submodules call back into were upgraded to
|
||||
`pub(crate)`: `start_block`, `lower_term`, `fresh_ssa`,
|
||||
`fresh_id`, `synth_arg_type`, `collect_owner_local_types`,
|
||||
`lookup_ctor_by_type`, `lookup_ctor_in_pattern`. No fields
|
||||
needed `pub(crate)`.
|
||||
|
||||
### What was deliberately not done
|
||||
|
||||
- **No behaviour change.** Not a single character of
|
||||
generated IR is different. The split is purely a
|
||||
reorganisation. (Verified by 66/66 e2e tests passing
|
||||
byte-identically across all four commits.)
|
||||
- **No method signatures changed.** Visibility upgrades only
|
||||
(`fn` → `pub(crate) fn`) on helpers crossed by the new
|
||||
module boundary. No parameter or return-type changes.
|
||||
- **No further extraction.** `lower_term` is 395 lines of
|
||||
pure dispatch into other methods; splitting its arms into
|
||||
per-shape files would force every shape's dispatcher to
|
||||
re-enter through a `pub(crate)` boundary and would split
|
||||
the term-shape ↔ lowered-shape correspondence across
|
||||
files. Kept whole. Same call for the
|
||||
`lower_app`/`lower_polymorphic_call`/`emit_call`/
|
||||
`emit_indirect_call` cluster — they are too tightly
|
||||
coupled to the `Emitter`'s dispatch state to extract
|
||||
cleanly.
|
||||
|
||||
### What this enables
|
||||
|
||||
The biggest concrete win is for `ailang-architect` and
|
||||
debugging conversations: a "what does the drop emission do?"
|
||||
question now reads one 696-line file instead of grepping
|
||||
through five thousand lines. Same for the closure machinery,
|
||||
the match lowering, and the substitution pipeline. Future
|
||||
iters that touch these subsystems get a smaller working
|
||||
context.
|
||||
|
||||
The split is not motivated by a coming feature, and no
|
||||
queued iter required it. It is a tidy paid for navigability
|
||||
alone — the kind of work that compounds across future
|
||||
sessions without showing up in any single one's bench
|
||||
numbers.
|
||||
|
||||
Reference in New Issue
Block a user