Navigability groundwork from the architecture review — zero behaviour change. - docs/MODULES.md: the repo map. Area -> directory -> key files -> re-mint?, plus per-feature touch points (tree-shaking, direct-linking, numeric fl/fx, inlining, multimethods, deps) and where a given clojure.core fn lives. Answers "where does X live / what's related to Y" in one read. - docs/rfc/README.md: index the 7 RFCs; flags RFC 0007's stale "no code yet" status (direct-linking + tree-shaking shipped) and the undocumented inlining/numeric work. - CLAUDE.md: document the var-deref calling convention (public defns reached from the .ss runtime by string lookup aren't dead), the def-var! native pattern, and the overlay shadowing rule; point at MODULES.md. - REFACTOR_PLAN.md: the prioritized, risk-tiered plan (working doc for this branch).
117 lines
7.5 KiB
Markdown
117 lines
7.5 KiB
Markdown
# Architecture refactor plan
|
||
|
||
Working doc for the `spike/arch-refactor` branch. Goal: make the codebase easier
|
||
to understand and safely modify — optimized for both human and LLM maintainers.
|
||
Derived from a four-part review (compiler core, Chez runtime, build/tooling,
|
||
top-level organization) plus call-graph analysis of the compiler.
|
||
|
||
The codebase is in better shape than its 58-file runtime suggests: `rt.ss` is a
|
||
real load-order map, the `passes/` split is clean, filename prefixes encode tier
|
||
order, and the Makefile self-documents the gate. The work below targets the few
|
||
genuine liabilities.
|
||
|
||
## The re-mint constraint shapes risk
|
||
|
||
- **Seed sources** (`jolt-core/jolt/*.clj`, `jolt-core/clojure/core/*.clj`,
|
||
`host/chez/reader.ss`): a change needs `make remint` + the byte-fixpoint
|
||
`make selfhost`. Behaviour-preserving refactors are *verified* by selfhost, but
|
||
every touch costs a re-mint. Higher risk, batch them.
|
||
- **Runtime `.ss` + runtime-loaded `.clj`** (`build.ss`, `emit-image.ss`,
|
||
`main.clj`, the dispatcher shims, …): no re-mint; verified by `make test` +
|
||
`make shakesmoke`. Lower risk.
|
||
- **Docs / renames of non-seed files**: zero behaviour risk.
|
||
|
||
## Tier 0 — navigability (zero risk, highest LLM value, do first)
|
||
|
||
1. **`docs/MODULES.md` — the repo map.** One table: area → directory → key files →
|
||
re-mint? Plus a per-feature "touch points" list for cross-cutting features
|
||
(tree-shaking, direct-linking, numeric fl/fx, multimethods, the deps resolver).
|
||
Collapses the current 3–4-source lookup ("where does X live?") into one read.
|
||
2. **`docs/rfc/README.md`** — index the 7 RFCs (number, title, status), mirroring
|
||
`docs/spec/README.md`.
|
||
3. **Document the shipped-but-undocumented compiler features**: IR inlining and
|
||
fl*/fx* numeric lowering (the `passes/` pipeline). A short "compilation passes"
|
||
doc or an RFC, referenced from RFC 0007.
|
||
4. **CLAUDE.md — one "Invariants you must preserve" block**: the `var-deref`
|
||
calling convention (the compiler is reached from `.ss` by string lookup), the
|
||
`def-var!` native-binding pattern, and the seed dual-home shadowing rule (today
|
||
only in `seed-overlay-registry.md`).
|
||
5. **First-line purpose comment on every `.ss`** (audit the laggards) and note the
|
||
reader's surprising location (`host/chez/reader.ss`, Scheme, re-mint applies).
|
||
|
||
## Tier 1 — runtime structure (no re-mint; verify with `make test` + shakesmoke)
|
||
|
||
6. **Registry pattern for the core dispatchers — the biggest runtime win.**
|
||
`jolt-pr-str`, `jolt-pr-readable`, `jolt=2`, `jolt-get`, `jolt-class`,
|
||
`jolt-hash` are extended by `set!`-capture-and-rebind across ~8 files (e.g.
|
||
`jolt-pr-str` is wrapped 9× in 8 files). The registry pattern already exists in
|
||
tree (`register-str-render!`, `register-instance-check-arm!`) and is strictly
|
||
better: type-disjoint arms gathered in one walker, load-order-independent,
|
||
greppable. Add `register-pr-arm!` / `register-eq-arm!` / `register-get-arm!` /
|
||
`register-hash-arm!` / `register-class-arm!`; convert the ~35 `set!` sites to
|
||
one-line registrations. Eliminates "read 8 files to understand one dispatcher."
|
||
7. **Extract a `host/chez/dce.ss` module for tree-shaking.** The DCE logic is split
|
||
across `emit-image.ss` (the `dce-*` helpers + record producer) and `build.ss`
|
||
(`bld-shake-all` + the manifest splice). Pull every `dce-*` def + `bld-shake-all`
|
||
(→ `dce-shake`) into `dce.ss`; give the record a named accessor API
|
||
(`dce-rec-keep?`/`-fqn`/`-refs`/`-str`) instead of `(vector-ref r 0..3)`; split
|
||
`bld-shake-all`'s five jobs (root-seed, edge-build, BFS, bail-detect, partition)
|
||
into named steps. This is recently-added code and the loosest contract in the
|
||
build.
|
||
8. **Tag the runtime manifest.** `bld-emit-runtime` decides "splice shaken core" /
|
||
"drop compiler" by substring-matching `(load "…seed/prelude.ss")` strings.
|
||
Replace the manifest with tagged entries (`(prelude)`/`(image)`/`(compile-eval)`/
|
||
`(load path)`) and dispatch on the tag — removes a silent-failure coupling.
|
||
9. **`build-binary` options map.** It takes 8 positional args ending in two bare
|
||
booleans (`direct-link?` `tree-shake?`); a swap compiles and misbehaves silently.
|
||
Pass one options map from `main.clj`; new flags become additive.
|
||
10. **`dynamic-wind` the compiler-global set/reset** in `build-binary` so
|
||
`set-optimize!`/`set-direct-link!` always revert (today they leak on a build
|
||
error; harmless for the CLI, a trap for any in-process caller).
|
||
11. **Re-split `host-static-{,statics,objects}.ss`** on a real axis — static
|
||
methods/fields vs host object classes — instead of the current "grew too big"
|
||
chain (one file is 502 lines, the largest non-seed runtime file). Move the
|
||
System-property/env plumbing out to `io.ss` / a `host-system.ss`.
|
||
12. **Consolidate the seq/transducer/parity natives** and dissolve
|
||
`natives-parity.ss` (a literal leftovers file). One `natives-transduce.ss` for
|
||
`td-*`/`into-xform`/`transduce`/`sequence`/`cat`; relocate parity's hash /
|
||
macroexpand / reader-conditional pieces to their real homes.
|
||
13. **Smaller renames/moves:** `dynamic-vars.ss` (5 constants) vs `dyn-binding.ss`
|
||
(the binding stack) — rename the former; pull the `format` engine out of
|
||
`natives-misc.ss` into `natives-format.ss`.
|
||
|
||
## Tier 2 — compiler core (seed sources; re-mint + selfhost + corpus)
|
||
|
||
14. **Split the success-type checker out of `types.clj`** (716 lines = inference +
|
||
checker + driver). Move the checker (`check-*`, the error-domain predicates) to
|
||
`jolt.passes.types.check`. Highest reduction in edit blast-radius in the
|
||
compiler — inference and checker stop being able to break each other.
|
||
15. **Decompose `infer`'s `:invoke` arm** (≈120 lines, 8 hand-coded call patterns)
|
||
into named `infer-<pattern>` helpers (as `numeric.clj`'s `an-invoke` already
|
||
does), and add `ty`/`nd` accessors for the positional `[type node]` tuple
|
||
(a transposition is currently silent and type-correct).
|
||
16. **Single-source the const-keyword-lookup recognizer.** It's implemented three
|
||
times divergently (`inline.clj`, `types.clj` ×2 arms, `backend_scheme.clj`).
|
||
Lift one predicate into `jolt.passes.fold` (the shared-predicate home) for the
|
||
two analysis copies; the backend's value-emission copy can stay.
|
||
17. **Co-locate the numeric op tables.** `backend_scheme.clj` (`dbl-ops`/`lng-ops`,
|
||
the Scheme strings) and `numeric.clj` (`dbl-spec`/`lng-spec`, specializable
|
||
names) must agree or `emit-numeric` splices a `nil` op string. Cross-link or
|
||
share the name→op table.
|
||
18. **Collapse `local-escapes?`'s mechanical recursion** (≈12 of its arms are just
|
||
"does any child escape" = `reduce-ir-children`); keep only the binder/lookup
|
||
arms explicit. Removes ~50 lines and a new-op soundness hazard. Fold
|
||
`recur-kinds`/`recur-arg-lists` (numeric) into one `recur-tails`.
|
||
19. **Touch-when-nearby:** drop the dead `form-char?` refer in `analyzer.clj:21`;
|
||
standardize on `(get node :k)` in the pass files; promote a few inline-comment
|
||
soundness arguments to function docstrings (`an-invoke` `:wild`, `dbl/lng-spec`).
|
||
|
||
## Sequencing
|
||
|
||
Tier 0 lands immediately (docs only). Tier 1 is the bulk of the maintainability win
|
||
and carries no re-mint — do 6 (registry) and 7–10 (DCE module + build hygiene)
|
||
first; they target the code most likely to be edited and most recently grown.
|
||
Tier 2 batches into one or two re-mints once Tier 1 is stable, each verified by
|
||
`make selfhost` + `make corpus`.
|
||
|
||
Every code change in Tiers 1–2 is behaviour-preserving and gated; no feature work.
|