# `expr @ dtype` Implementation Plan

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

**Goal:** Implement `expr @ dtype` as postfix cast sugar (equivalent to `cast<dtype>(expr)`) across parsing, lowering diagnostics, and docs, including a warning for chained casts like `@f8 @f32`.

**Architecture:** Extend the chumsky Pratt parser atom rule to support postfix `@dtype` on arbitrary atoms, reusing the existing dtype parser. Preserve literal-specific `@dtype` handling for better diagnostics. In lowering, detect nested `Expr::Cast` chains and emit a soft diagnostic when multiple casts are stacked; keep runtime semantics unchanged.

**Tech Stack:** Rust, chumsky 0.12, existing Gonidium IR / diag framework, current test suite.

---

### Task 1: Parser support for postfix `@dtype`

**Files:**
- Modify: `src/parse/mod.rs`
- Reference: `src/parse/ast.rs`

**Step 1: Introduce `atom_base` and postfix cast loop**

- Refactor the atom definition so that:
  - `atom_base` is the existing choice of `cast | call | tuple | group | literals | const_sym | var`.
  - `atom` becomes `atom_base` followed by zero or more `@dtype` suffixes.
- Use the existing `literal_ann` helper (or an equivalent `(TokenKind::At, dtype)` parser) to recognise `@dtype` in the postfix position.
- For each parsed suffix, wrap the current `Spanned<Expr>` in `Expr::Cast(dtype, Box::new(inner))` and merge spans.

**Step 2: Keep literal inline `@dtype` behaviour**

- Ensure `lit_int`, `lit_float`, and `lit_complex` continue to use their existing inline annotation logic:
  - They should still produce `Expr::Lit*(_, Some(dtype))` when a literal is immediately followed by `@dtype`.
- Verify that the postfix cast loop does not interfere with tokens already consumed by literal annotations (i.e. only runs on remaining `@dtype` tokens after `atom_base` has been parsed).

**Step 3: Compile and run parser tests**

- Run: `cargo test tests::parse_dsl -- --nocapture` (or `cargo test parse_dsl` if you prefer the module filter).
- Confirm all existing parser tests still pass.

### Task 2: Lowering diagnostic for chained casts

**Files:**
- Modify: `src/ir/error.rs`
- Modify: `src/diag.rs`
- Modify: `src/ir/lower.rs`
- Add test: `tests/parse_dsl.rs` or a new dedicated lower test file (e.g. `tests/lower_cast_chain.rs`)

**Step 1: Add new diagnostic code**

- In `src/diag.rs`:
  - Add a new variant to `DiagCode`, e.g. `LowerRedundantCastChain`.
  - Set its default severity to `Warning`.
  - Assign a new short code (e.g. `L010`) and kebab-case string (e.g. `"lower-redundant-cast-chain"`).
- In `src/ir/error.rs`:
  - Add a constructor `lower_redundant_cast_chain(from: DType, to: DType, span: Span) -> Diag`.
  - Message example: `"multiple sequential casts: {from} -> {to}; consider casting directly to {to}"`.
  - Include a hint encouraging simplification (e.g. ``"simplify to `expr@{to}` if the intermediate cast is not needed"`).

**Step 2: Detect chained casts in lowering**

- In `lower_expr` and `lower_expr_collect`, inside the `Expr::Cast(dtype, child)` arm:
  - Before or after lowering `child`, inspect `child.node`.
  - If `matches!(child.node, Expr::Cast(inner_dtype, _))`, emit `lower_redundant_cast_chain(inner_dtype, *dtype, to_ir_span(expr.span))` via the appropriate context (`LowerCtx` / `LowerCtxCollect`).
  - Do not change the lowering behaviour: still emit a single `Node::Cast(*dtype, child_id)` as today.

**Step 3: Add tests for chained casts**

- Create a small test that parses and lowers a function containing chained casts, for example:
  - Source like: `||\n(1@i8)@i16`.
  - Or explicitly: `||\ncast<i16>(1@i8)`.
- Assert that:
  - Lowering succeeds.
  - The diagnostic list contains `DiagCode::LowerRedundantCastChain`.
- Run: `cargo test lower -- --nocapture` or a more targeted filter once the test is added.

### Task 3: Parser tests for postfix `@dtype`

**Files:**
- Modify: `tests/parse_dsl.rs`

**Step 1: Add structural parse tests**

- Add tests that:
  - Parse `(3//2)@f8` and assert the AST body is an `Expr::Cast` whose child is a `Binary(FloorDiv, ..)`.
  - Parse `|x: i32|\n x@i8` and assert the body is `Expr::Cast` over a `Var("x")`.
  - Parse `|x: i32|\n -x@i8` and assert the top node is `Expr::Unary(Neg, ...)` wrapping a `Cast`.

**Step 2: Add a parsed nested-cast test**

- Add a test for `1@i8@i16` (or a similar chained form) that asserts:
  - The outer node is `Expr::Cast(i16, inner)`.
  - `inner.node` is `Expr::Cast(i8, ...)`.

**Step 3: Run tests**

- Run: `cargo test tests::parse_dsl -- --nocapture`.

### Task 4: E2E behaviour check with demo-style expression

**Files:**
- Modify: `tests/e2e_parse_lower_interp.rs`

**Step 1: Add an end-to-end test**

- Use a small function similar to your demo:

  - Example:
    - `|a: f64, b: f64|\n -a + (3//2)@f8 + b`

- Flow:
  - `parse_dsl` → `lower` → `interp_eval`.
  - Check:
    - The program runs without errors.
    - The numeric result matches an equivalent Rust computation using the same promotion rules (e.g. floor division in `i64`, then cast to `f8`-like behaviour approximated via `f32`/`f64` as appropriate).

**Step 2: Run full test suite slice**

- Run: `cargo test e2e_parse_lower_interp -- --nocapture`.

### Task 5: Documentation updates

**Files:**
- Modify: `docs/grammar.md`
- Modify: `docs/type-system.md`

**Step 1: Update grammar**

- In `docs/grammar.md`:
  - Adjust the expression grammar to mention `expr @ dtype` postfix sugar in the expressions section.
  - Clarify that:
    - `@dtype` binds to the preceding expression at atom precedence.
    - It is conceptually equivalent to `cast<dtype>(expr)`.

**Step 2: Align type-system docs**

- In `docs/type-system.md`:
  - Update the description of `@dtype` so that it:
    - Covers both literals and arbitrary expressions.
    - Notes that literal-specific behaviour (default types, range and precision diagnostics) is applied to the literal before any further casts.

**Step 3: Spell-check and consistency**

- Quickly skim the updated sections for consistency with the existing terminology (DType, promotion, etc.).
