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).
7.5 KiB
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 needsmake remint+ the byte-fixpointmake 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 bymake test+make shakesmoke. Lower risk. - Docs / renames of non-seed files: zero behaviour risk.
Tier 0 — navigability (zero risk, highest LLM value, do first)
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.docs/rfc/README.md— index the 7 RFCs (number, title, status), mirroringdocs/spec/README.md.- 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. - CLAUDE.md — one "Invariants you must preserve" block: the
var-derefcalling convention (the compiler is reached from.ssby string lookup), thedef-var!native-binding pattern, and the seed dual-home shadowing rule (today only inseed-overlay-registry.md). - 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)
- Registry pattern for the core dispatchers — the biggest runtime win.
jolt-pr-str,jolt-pr-readable,jolt=2,jolt-get,jolt-class,jolt-hashare extended byset!-capture-and-rebind across ~8 files (e.g.jolt-pr-stris 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. Addregister-pr-arm!/register-eq-arm!/register-get-arm!/register-hash-arm!/register-class-arm!; convert the ~35set!sites to one-line registrations. Eliminates "read 8 files to understand one dispatcher." - Extract a
host/chez/dce.ssmodule for tree-shaking. The DCE logic is split acrossemit-image.ss(thedce-*helpers + record producer) andbuild.ss(bld-shake-all+ the manifest splice). Pull everydce-*def +bld-shake-all(→dce-shake) intodce.ss; give the record a named accessor API (dce-rec-keep?/-fqn/-refs/-str) instead of(vector-ref r 0..3); splitbld-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. - Tag the runtime manifest.
bld-emit-runtimedecides "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. build-binaryoptions 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 frommain.clj; new flags become additive.dynamic-windthe compiler-global set/reset inbuild-binarysoset-optimize!/set-direct-link!always revert (today they leak on a build error; harmless for the CLI, a trap for any in-process caller).- Re-split
host-static-{,statics,objects}.sson 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 toio.ss/ ahost-system.ss. - Consolidate the seq/transducer/parity natives and dissolve
natives-parity.ss(a literal leftovers file). Onenatives-transduce.ssfortd-*/into-xform/transduce/sequence/cat; relocate parity's hash / macroexpand / reader-conditional pieces to their real homes. - Smaller renames/moves:
dynamic-vars.ss(5 constants) vsdyn-binding.ss(the binding stack) — rename the former; pull theformatengine out ofnatives-misc.ssintonatives-format.ss.
Tier 2 — compiler core (seed sources; re-mint + selfhost + corpus)
- Split the success-type checker out of
types.clj(716 lines = inference + checker + driver). Move the checker (check-*, the error-domain predicates) tojolt.passes.types.check. Highest reduction in edit blast-radius in the compiler — inference and checker stop being able to break each other. - Decompose
infer's:invokearm (≈120 lines, 8 hand-coded call patterns) into namedinfer-<pattern>helpers (asnumeric.clj'san-invokealready does), and addty/ndaccessors for the positional[type node]tuple (a transposition is currently silent and type-correct). - 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 intojolt.passes.fold(the shared-predicate home) for the two analysis copies; the backend's value-emission copy can stay. - Co-locate the numeric op tables.
backend_scheme.clj(dbl-ops/lng-ops, the Scheme strings) andnumeric.clj(dbl-spec/lng-spec, specializable names) must agree oremit-numericsplices anilop string. Cross-link or share the name→op table. - 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. Foldrecur-kinds/recur-arg-lists(numeric) into onerecur-tails. - Touch-when-nearby: drop the dead
form-char?refer inanalyzer.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.