|
| 1 | +# Analyser readability review (`ndc_analyser`) |
| 2 | + |
| 3 | +> Point-in-time review captured alongside the LSP work (PR #164). File/line |
| 4 | +> references are a snapshot and will drift as the code changes; treat them as |
| 5 | +> starting points, not exact coordinates. |
| 6 | +
|
| 7 | +## Context |
| 8 | + |
| 9 | +The semantic analyser is the hardest part of the project to hold in one's head. |
| 10 | +This review answers two questions, with the upcoming DefId/LSP-resolution work in |
| 11 | +mind: (1) are there any *big* issues that only a major refactor can fix, and (2) |
| 12 | +what *incremental* readability improvements are worth making? It proposes no |
| 13 | +behaviour changes β it is an assessment plus a backlog. Sizes at time of writing: |
| 14 | +`analyser.rs` ~930 lines, `scope.rs` ~1432 (~1038 production + ~394 tests), |
| 15 | +`lib.rs` 5. |
| 16 | + |
| 17 | +## Headline verdict |
| 18 | + |
| 19 | +**No mandatory major refactor.** The architecture is sound for the language's feature |
| 20 | +set: `Analyser` is a thin client over `ScopeTree`; types flow through side tables |
| 21 | +(`expr_types`, `inferred_return_types`) plus in-place AST annotation (`resolved`, |
| 22 | +`captures`, `inferred_type`); the compiler consumes the annotated AST. Nothing is |
| 23 | +boxed-in. The difficulty is **accumulated local complexity**, which is entirely |
| 24 | +addressable incrementally. |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## Part 1 β Big issues |
| 29 | + |
| 30 | +### The one architectural theme: `ScopeTree` conflates resolution + slot allocation |
| 31 | + |
| 32 | +`ScopeTree` (`scope.rs`) does two jobs at once: **lexical name resolution** (which |
| 33 | +declaration a name means) and **VM stack-slot allocation** (the concrete `usize` |
| 34 | +`ResolvedVar::{Local,Upvalue,Global}{slot}`). This entanglement is the root of both |
| 35 | +the intricacy (the `base_offset` / `function_scope_idx` / upvalue-hoisting math |
| 36 | +threaded through every lookup) and the "slot isn't a stable identity" limitation that |
| 37 | +bit the LSP. |
| 38 | + |
| 39 | +**Feasibility (investigated):** |
| 40 | +- The compiler is already **semi-independent** of analyser slots: it keeps its own |
| 41 | + `num_locals` and only `max`es it against declared slots (`compiler.rs:513,758,792,811`), |
| 42 | + and allocates its own temporaries (`compiler.rs:280-282,483-484`). So *local* slot |
| 43 | + numbering could plausibly move to the compiler. |
| 44 | +- **Globals** are purely positional in the `FunctionRegistry` iteration order |
| 45 | + (`interpreter/src/lib.rs:221-229`, `scope.rs:370-384`, `vm.rs:204`) β moving their |
| 46 | + assignment needs a stable nameβslot map handed compiler-side. Contained, not hard. |
| 47 | +- **Upvalues/captures are the hard, tightly-coupled part** and *not cleanly |
| 48 | + separable*: the analyser computes `CaptureSource::{Local,Upvalue}(index)` |
| 49 | + (`scope.rs:474-480,988-1025`), the compiler embeds it verbatim into `OpCode::Closure` |
| 50 | + (`compiler.rs:736-742`), and the VM indexes `upvalues[slot]` directly (`vm.rs:491-532`). |
| 51 | + Critically, **discovering captures *is* a name-resolution activity** (you must |
| 52 | + resolve names across function boundaries to know what escapes), and the index *is* |
| 53 | + the layout β so "separating resolution from layout" buys little here. |
| 54 | +- Other consumers: REPL resume leans on `Compiler::num_locals` (`interpreter/src/lib.rs` |
| 55 | + ~251/257/276); the LSP reads `ResolvedVar` but not slot numbers. Coupling surface is |
| 56 | + small (~6 files, ~70 lines). |
| 57 | + |
| 58 | +**Verdict: feasible but low-ROI β recommend shelving.** It would mostly relocate |
| 59 | +local-slot bookkeeping; it would *not* simplify the genuinely hard code (upvalue |
| 60 | +hoisting, overload resolution β both intrinsic). It also touches the runtime hot path |
| 61 | +(closure creation, REPL resume) for moderate risk. The planned **DefId side-table** |
| 62 | +gives the LSP the stable identity it needs *without* this refactor, and would be the |
| 63 | +natural seam if this is ever revisited. **Do DefId first; reconsider this only if a |
| 64 | +concrete need appears.** |
| 65 | + |
| 66 | +### Not-big, but worth knowing |
| 67 | +- `resolve_call` / `scalar_walk` (the 5-case overload + tuple-broadcast cascade, |
| 68 | + `scope.rs:531-715`) is the most complex algorithm, but the complexity is *intrinsic* |
| 69 | + (overloading Γ vectorization Γ closures). It is well-documented; it can be made more |
| 70 | + readable (Part 2) but not fundamentally simpler without dropping features. |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Part 2 β Incremental readability backlog (prioritized) |
| 75 | + |
| 76 | +All behaviour-preserving. Ordered by (value Γ· risk). Each is independently shippable. |
| 77 | + |
| 78 | +### Batch 1 β High value, near-zero risk (pure moves/renames/docs) |
| 79 | +1. **Extract big `analyse_inner` arms into methods.** `analyse_inner` is ~367 lines |
| 80 | + (`analyser.rs:104-470`). Move `FunctionDeclaration` (`282-371`, ~90 lines) β |
| 81 | + `analyse_function_declaration`, `OpAssignment` (`194-281`, ~88 lines) β |
| 82 | + `analyse_op_assignment`, `Assignment` (`169-193`) β `analyse_assignment`. Leaves the |
| 83 | + dispatcher a scannable table of one-liners. **Biggest single win.** |
| 84 | +2. **Fix `span` shadowing** in `resolve_lvalue_declarative` (`analyser.rs:721-757`): the |
| 85 | + `Lvalue::Identifier { span, .. }` destructure shadows the method's `span` param β |
| 86 | + rename one. Genuine footgun. |
| 87 | +3. **Module-level orientation docs.** Add a short "how analysis works" header to |
| 88 | + `analyser.rs` and `scope.rs` (two-phase function pre-registration; slot numbering & |
| 89 | + `base_offset`; upvalue hoisting; the 5-case resolution). Document the `base_offset` / |
| 90 | + `function_scope_idx` / `env_scopes` invariants once at their definitions |
| 91 | + (`scope.rs:128-135`). Highest orientation ROI. |
| 92 | +4. **Fix the `TOOD` typo** (`analyser.rs:538`) and capture the "get this from the AST |
| 93 | + when the parser adds it" note as a real TODO.md entry / issue. |
| 94 | + |
| 95 | +### Batch 2 β Dedupe tricky logic (low risk, removes copy-paste) |
| 96 | +5. **Unify "widen binding or error".** The same widen-then-check-annotation block |
| 97 | + appears 3Γ: `analyser.rs:178-189` (Assignment), `244-255` (OpAssignment ident), |
| 98 | + `265-272` (OpAssignment index). Extract one helper |
| 99 | + `widen_binding(target, widened, value_type, span)`. |
| 100 | +6. **Extract the upvalue-chain follower** in `scope.rs`. The `CaptureSource::Local | |
| 101 | + Upvalue` walk is duplicated in `get_type` (`387-413`) and `get_binding_mut` |
| 102 | + (`898-932`), and echoed in `hoist_upvalue` (`988-1025`). A `follow_upvalue_chain` |
| 103 | + helper removes the worst `scope.rs` duplication. |
| 104 | +7. **Naming pass.** `sig`/`type_sig` β consistent `arg_types`; `loose` β |
| 105 | + `compatible_candidates`; `scope_ptr` β `scope_idx`; `env_scopes` β |
| 106 | + `crossed_fn_boundaries` (+ doc). Cheap, high comprehension value. |
| 107 | + |
| 108 | +### Batch 3 β Structural tidy (small risk, needs tests first) |
| 109 | +8. **Collapse dual error storage.** `Analyser.errors` (`analyser.rs:37`) duplicates |
| 110 | + `AnalysisResult.errors`, reconciled in `take_result` (`60-64`). Emit straight into |
| 111 | + `result.errors` and drop the field (check `emit`/`emit_external`/`has_errors`). |
| 112 | +9. **Split `scalar_walk`** (`scope.rs:639-715`): factor the per-scope body |
| 113 | + (find-exact / collect-loose / collect-all-by-name) into a `scan_scope` helper used by |
| 114 | + both the loop and the global-scope fallback, removing the duplicated fallback block |
| 115 | + (`647-651` vs `683-688`). |
| 116 | +10. **Extract `resolve_lvalue_declarative`'s Sequence arm** (`analyser.rs:~762-819`) |
| 117 | + into `resolve_sequence_lvalue`; it's long, nested, and has a shadow of `found_type`. |
| 118 | +11. *(Optional)* **Error-constructor boilerplate** (`analyser.rs:~853-929`): 11 |
| 119 | + `Self { text: format!(...), span }` constructors β a tiny macro or `new(span, msg)` |
| 120 | + helper trims repetition. Low priority (currently readable). |
| 121 | + |
| 122 | +### Supporting: characterization tests (do before Batch 3) |
| 123 | +`scope.rs` tests (~`1039-1432`, 20 cases) cover scope/upvalue mechanics well but **omit |
| 124 | +`resolve_call`'s vec/dynamic paths** (`resolve_vec`, `VecResolution`, `Binding::Dynamic`, |
| 125 | +`dynamic_return_type`, `extend_dedup`). Add characterization tests for the 5 resolution |
| 126 | +cases first β they document behaviour *and* de-risk items 8β10. |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## Recommended sequence & verification |
| 131 | + |
| 132 | +If/when executed: Batch 1 β Batch 2 β (add resolution tests) β Batch 3, one small PR |
| 133 | +per item, each gated on `cargo test` (workspace), `cargo clippy` clean, `cargo fmt`. The |
| 134 | +functional suite (`tests/functional`) plus the `scope.rs` unit tests are the safety net; |
| 135 | +the new characterization tests harden the riskiest area before it's touched. No item |
| 136 | +changes runtime behaviour, so a green suite is sufficient verification. |
0 commit comments