# REPL CLI Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Implement a CLI REPL for Gonidium that lets users type single-line input declarations, assignments, and expressions, see types and values immediately, and reuse the existing parse/lower/opt/diag pipeline. The REPL will run as a new `gonidium repl` subcommand and use a reusable session engine that can later back a TUI.

**Architecture:** Introduce a `repl` module in the library that owns the session model (inputs/locals/diagnostics) and a pure line processor, plus a small CLI harness in `src/bin/gonidium.rs` that wires stdin/stdout to the engine and prints a simple header before each prompt. Tests will exercise the line processor directly (not the interactive loop) so behaviour is deterministic and automation-friendly.

**Tech Stack:** Rust, existing Gonidium library APIs (`parse_dsl_with_errors`, `lower_with_config_collect`, `optimize_with_info`, `interp_eval`, `Diag`, `DiagConfig`), `bpaf` for CLI subcommands, standard IO traits (`BufRead`, `Write`) for REPL IO.

---

### Task 1: Sketch the REPL engine API

**Files:**
- Modify: `src/lib.rs` (public re-export of REPL types/functions)
- Create: `src/repl.rs` (core REPL engine module)

**Step 1: Define core REPL types**

- Add a new module `repl` to the library, with:
  - `pub struct ReplConfig` (flags like `opt_enabled: bool` and maybe future diag options).
  - `pub struct ReplSession` holding:
    - `inputs: Vec<(String, DType)>` and a fast lookup map.
    - `locals: HashMap<String, (DType, Option<Value>)>`.
    - `last_diags: Vec<Diag>`.
    - `diag_cfg: DiagConfig`.
  - `pub enum ReplLineKind` or similar (InputDecl / Assign / Expr) if helpful.

**Step 2: Define processing result structure**

- In `src/repl.rs`, sketch `pub struct ReplOutput` containing:
  - Something like `pub header: ReplHeader` (snapshot for inputs/locals/last diag summary).
  - `pub messages: Vec<String>` (user-facing lines to print for this input, including value/expression output and any diagnostics).
  - A small enum flag to indicate success vs error (for tests).

**Step 3: Public entry points**

- Expose from `src/lib.rs`:
  - `pub fn repl_session(config: ReplConfig) -> ReplSession`.
  - `impl ReplSession { pub fn process_line(&mut self, line: &str) -> ReplOutput }`.

Expected: no full implementation yet; just type signatures and basic struct fields so later tasks can fill in details.

---

### Task 2: Implement header snapshot and summarisation

**Files:**
- Modify: `src/repl.rs`

**Step 1: Implement header type**

- Add `pub struct ReplHeader` with fields:
  - `pub inputs_summary: String` (e.g. `"inputs: x: f32, y: f64"` or `"inputs: (none)"`).
  - `pub locals_summary: String` (recent locals, similar format).
  - `pub last_diag_summary: String` (e.g. `"last diag: L007 lower-implicit-promotion (Info)"` or `"last diag: none"`).

**Step 2: Implement header builder**

- On `ReplSession`, implement an internal method `fn make_header(&self) -> ReplHeader` that:
  - Builds `inputs_summary` from `self.inputs` in declaration order.
  - Builds `locals_summary` from the most recent N locals (cap N to something like 5).
  - Inspects `self.last_diags` to pick:
    - If empty: `"last diag: none"`.
    - Else: highest severity diag (by `Severity`), using `DiagCode::short_code()` and `DiagCode::as_str()` to format a short message.

**Step 3: Add to `ReplOutput`**

- Ensure `ReplOutput` always carries a `ReplHeader` built from the session *after* processing the line (even if the line failed).

---

### Task 3: Line classification and parsing

**Files:**
- Modify: `src/repl.rs`

**Step 1: Classify input line**

- In `ReplSession::process_line`, first trim the line and:
  - If empty: return a `ReplOutput` with header only (no messages).
  - If it matches `name: dtype` with no `=`:
    - Classify as InputDecl.
  - Else if it matches `name = expr` (first `=` split):
    - Classify as Assign.
  - Else:
    - Classify as bare Expr.

**Step 2: Parse dtype-only lines**

- For InputDecl:
  - Reuse the existing dtype parser if exposed via library; if not, create a tiny helper that routes through the `parse` module’s dtype parser to obtain a `DType`.
  - On success:
    - Insert/update in `self.inputs`.
    - Produce a message like: `"input x: f32"`.
  - On failure:
    - Do not mutate session.
    - Produce error message(s) and push an appropriate `Diag` into `last_diags`.

**Step 3: Build synthetic function source for expressions**

- For Assign and Expr cases:
  - Construct a minimal synthetic Gonidium source string representing a single function:
    - Use `|inputs...|` header with current `self.inputs`.
    - Add lines for current `self.locals` if necessary, or rely on single-expression semantics (v1 can start with just parameters and this line).
    - Append either:
      - `name = expr` and `name` as the final expression for Assign, or
      - The bare expression for Expr.
  - This source string will go through the usual `parse_dsl_with_errors` and `lower_with_config_collect`.

---

### Task 4: Wire parse → lower → opt → type info into the REPL engine

**Files:**
- Modify: `src/repl.rs`

**Step 1: Integrate parsing with diagnostics**

- In `process_line` for Assign/Expr:
  - Call `parse_dsl_with_errors` on the synthetic source.
  - If it returns `Err(diags)`:
    - Convert each `Diag` into readable strings using `report_string_default` (or a compact variant).
    - Store them into `self.last_diags`.
    - Fill `ReplOutput.messages` with these strings.
    - Return early without mutating `inputs`/`locals`.

**Step 2: Integrate lowering and diag printing**

- Call `lower_with_config_collect` with `self.diag_cfg`.
  - For each `diag` in `lower_result.diags`:
    - Print according to `self.diag_cfg.severity(code)` (reuse behaviour from the existing CLI).
    - Collect into `self.last_diags`.
  - For each `error` in `lower_result.errors`:
    - Render and collect similarly, then abort processing of this line (no session mutation).

**Step 3: Optional optimisation**

- If `self.config.opt_enabled` is true:
  - Call `optimize_with_info` on the `dag` as in `src/bin/gonidium.rs`.
  - Append optimisation diags into `self.last_diags` and into the output messages.
  - Replace `dag` with the optimised one on success.

---

### Task 5: Compute dtype and “simplified expression + dtype”

**Files:**
- Modify: `src/repl.rs`

**Step 1: Identify the output node and dtype**

- After lowering/optimisation, find the function’s single output node for this line:
  - For Assign: the assigned variable’s node.
  - For Expr: the final expression node.
- Get its `DType` from the `dag.dtypes` map.

**Step 2: Produce simplified expression string**

- Use existing IR/opt facilities to render a human-readable simplified expression:
  - Either reuse functions from `backend` or `opt` modules that can pretty-print a node expression, or:
  - Write a small helper that walks the DAG to reconstruct an infix expression string in a simple style.
- Combine expression string and dtype into `"expr @ dtype"` as described in the REPL design doc.

**Step 3: Attach to `ReplOutput`**

- For all successful lines (even when interpreter will not run), push this `"expr @ dtype"` string into `ReplOutput.messages` as the primary result representation.

---

### Task 6: Hook interpreter evaluation for closed expressions

**Files:**
- Modify: `src/repl.rs`

**Step 1: Determine whether expression is evaluable**

- Decide minimal v1 rule:
  - Expression is evaluable only if:
    - All referenced inputs have concrete `Value`s (for now we can assume none, or treat them as non-evaluable).
    - All referenced locals with `value` fields are `Some`.
  - Start with a conservative rule: only allow evaluation of expressions built entirely from literals and locals that already have values.

**Step 2: Invoke `interp_eval` when allowed**

- If evaluable:
  - Construct input vector for `interp_eval` consistent with `dag.params`.
  - Call `interp_eval` and:
    - On success:
      - Format each `Value` with its `DType` using a helper similar to `format_value_with_dtype` from `src/bin/gonidium.rs`.
      - Push `"value@dtype"` strings into `ReplOutput.messages` (either alongside or instead of the `"expr @ dtype"` line, following the design doc).
      - Update the corresponding local’s `value` if this was an assignment.
    - On error:
      - Print `interp error: ...` string.
      - Do not mutate locals.

**Step 3: Decide ordering with expression output**

- Implement the default ordering from the design:
  - If interpreter ran: `value@dtype` is the main line (can keep `"expr @ dtype"` as an additional line if helpful).
  - If interpreter did not run: only `"expr @ dtype"`.

---

### Task 7: Expose REPL from the CLI binary

**Files:**
- Modify: `src/bin/gonidium.rs`

**Step 1: Extend CLI parser with a `repl` subcommand**

- Update `Run` and `cli_parser()`:
  - Introduce a new `Repl` struct (even if empty for now).
  - Add `repl_parser().command("repl")` next to `"run"`.
  - Adjust `main` to match on either `Run` or `Repl`.

**Step 2: Implement the interactive loop**

- For the `repl` branch:
  - Create a `ReplSession` with default config.
  - Use `stdin().lock()` and `stdout()` to implement:
    - Loop:
      - Build header via `session.make_header()`.
      - Print the header block (three lines: inputs, locals, last diag).
      - Print prompt string (`">> "`).
      - Read a single line from stdin.
      - If EOF or line equals `:q` / `:quit`, break.
      - Call `session.process_line(&line)` and print each `message` line.

**Step 3: Error handling and exit codes**

- Keep REPL always returning exit code 0 unless a fatal IO error occurs.
- Individual line errors are shown as messages but do not abort the loop.

---

### Task 8: Add tests for the REPL engine

**Files:**
- Create: `tests/repl_session.rs`

**Step 1: Write basic session tests**

- Import `ReplSession` and `ReplConfig` from the library.
- Write tests such as:
  - `repl_declares_inputs`: send `x: f32`, inspect session header and ensure inputs contain `x: f32`.
  - `repl_assignment_sets_local_dtype`: with `x: f32` declared, send `t1 = x + 1`, assert the reported dtype and locals map.
  - `repl_expr_prints_simplified_expr`: with no values, send `1 + 0`, assert that `"1+0 @ f64"` (or the chosen simplified form) appears in `messages`.

**Step 2: Test diagnostics behaviour**

- Craft an expression that triggers:
  - An implicit promotion diag.
  - A float precision loss warning.
- Assert that:
  - `last_diag_summary` reflects the expected short code and severity.
  - `messages` contain human-readable text for the diagnostic.

**Step 3: Avoid flaky IO**

- Tests should call `ReplSession::process_line` directly, not launch the CLI binary, so they run fast and deterministically.

---

### Task 9: Update documentation

**Files:**
- Modify: `README.md`
- Optionally modify: `docs/grammar.md` or `docs/diagnostics.md` (if you want to reference REPL usage)

**Step 1: Document the REPL command**

- In `README.md`, add a short section:
  - How to start REPL: `gonidium repl`.
  - Example session with inputs, assignment, and expression outputs.
  - Show `value@dtype` printing and one example of a diagnostic appearing.

**Step 2: Cross-link from design doc**

- At the end of `docs/plans/2026-02-25-repl-design.md`, optionally add a brief note linking to the implementation plan file, so future readers can jump from design to plan.
