# `expr @ dtype` Cast Sugar Design

Date: 2026-02-25

## Goals

- Make `@dtype` a uniform **postfix cast sugar** over arbitrary expressions, not just literals.
- Keep the mental model: **`expr @ dtype` is syntax sugar for `cast<dtype>(expr)`**.
- Preserve existing literal behaviour and diagnostics (`8@i8` range checks, default literal types).
- Emit a warning when chaining multiple casts in a row like `expr @ f8 @ f32`, with a suggestion to simplify.

## Surface Syntax

### Current

- Grammar in `docs/grammar.md` currently describes:
  - `literal = literal '@' dtype | int_lit | float_lit | complex_lit | bool_lit`
- Parser implementation in `src/parse/mod.rs`:
  - Recognises numeric literals (`Int/Float/Complex`) with an optional inline `@dtype` annotation, e.g. `8@i8`.
  - Does **not** support `@dtype` as a general postfix operator on arbitrary expressions.

### Proposed

- Treat `@dtype` as a **postfix cast sugar** at the atom level:

  - Extend the Pratt atom from:
    - `atom = cast | call | group | tuple | literal | const_sym | ident`
  - To:
    - `atom_base = cast | call | group | tuple | literal | const_sym | ident`
    - `atom      = atom_base ('@' dtype)*`

- Parsing behaviour:

  - `a@i8` → `Expr::Cast(i8, Var("a"))`
  - `(3//2)@f8` → `Expr::Cast(f8, Binary(FloorDiv, 3, 2))`
  - `-x@i8`:
    - `x@i8` is parsed as `Expr::Cast(i8, Var("x"))`
    - Unary `-` then wraps it: `Expr::Unary(Neg, Cast(i8, x))` → semantics `-(x@i8)`

- Chaining:

  - `expr @ f8 @ f32` is parsed as:
    - `Expr::Cast(f32, Expr::Cast(f8, expr))`
  - This is allowed but considered potentially redundant; lowering will emit a soft diagnostic (see below).

### Literals

- Keep the existing literal-specific `@dtype` handling:

  - Numeric literals remain parsed as `Expr::LitInt(_, Option<DType>)` / `Expr::LitFloat(_, Option<DType>)` /
    `Expr::LitComplex(_, _, Option<DType>)`.
  - The inline annotation on literals continues to drive:
    - Default dtype selection.
    - Range checks for integer literals.
    - Precision-loss diagnostics for float/complex literals.

- Semantics wise:

  - `8@i8` continues to behave like a constant whose dtype is exactly `i8`, with range checks at lowering time.
  - The user mental model can still be “`8@i8` is a casted literal”, but implementation keeps the specialised literal
    node for better diagnostics and const-folding.

## Semantics and Lowering

### Existing

- `Expr::Cast(dtype, child)` in `src/ir/lower.rs`:
  - Lowers `child` first, then pushes a `Node::Cast(dtype, child_id)` with result dtype `dtype`.
- `Expr::Lit*` with annotations:
  - Drive dtype and diagnostics directly at literal lowering time.

### With postfix `@dtype`

- New `Expr::Cast` nodes from postfix `@dtype` behave exactly like explicit `cast<dtype>(...)`:

  - There is **no semantic difference** between:
    - `cast<f8>(3//2)` and `(3//2)@f8`
    - `cast<i8>(x)` and `x@i8`

- Lowering does not need special handling for `@`; it only sees more `Expr::Cast` nodes.

### Chained casts warning

- Pattern:

  - An expression of the shape `Expr::Cast(d2, child)` where `child.node` is also `Expr::Cast(d1, _)`.
  - This covers both:
    - `expr @ f8 @ f32` (via postfix sugar).
    - `cast<f32>(cast<f8>(expr))` (explicit nested casts).

- Diagnostic:

  - Phase: lowering (`src/ir/lower.rs` + `src/ir/error.rs`).
  - New code: `LowerRedundantCastChain` (default severity: Warning).
  - Message (example):
    - `"multiple sequential casts: f8 -> f32; consider casting directly to f32 or keeping only the final cast"`
  - Span:
    - Use the outer cast span (covers the full `expr @ f8 @ f32` surface).
  - Hint:
    - `"simplify to `expr@f32` if the intermediate cast is not required"`

- Behaviour:

  - This is a **non-fatal** diagnostic; lowering and evaluation proceed as normal.
  - Users can still deliberately use multiple casts; they will just see a warning they can silence via `DiagConfig`.

## Documentation Updates

- `docs/grammar.md`:

  - Update the `literal` rule comment to no longer imply `@dtype` is tied only to literals.
  - Add a short subsection under “Expressions”:

    - Describe `expr @ dtype` as postfix syntax sugar for `cast<dtype>(expr)`.
    - Note that it binds tighter than any infix operator (implemented as part of `atom`), and after unary operators.

- `docs/type-system.md`:

  - Clarify that:
    - `@dtype` is *conceptually* a cast, applied either to literals or to arbitrary expressions.
    - Literal-specific rules (default dtype, range/precision diagnostics) still apply to the literal *before* any
      further casts.

## Tests

- Parser tests (`tests/parse_dsl.rs`):

  - Add coverage for:
    - `(3//2)@f8`
    - `a@i8`
    - `-x@i8`
    - Chained syntax `1@i8@i16` parses as nested `Expr::Cast`.

- Lowering / e2e tests:

  - Ensure `-a + (3//2)@f8 + b` parses and lowers successfully.
  - Add a test that `expr @ f8 @ f32` triggers `LowerRedundantCastChain` (or equivalent) with `Warning` severity.
