terni 0.7.0

Ternary error handling: Success, Partial with measured loss, Failure. Because computation is not binary.
Documentation
# The `eh!` Macro

The block that tries extra hard. Roll+Loss.

**10+** Success — zero loss, clean hit.
**7-9** Partial — you get the value, but something was lost. The loss is measured.
**6-** Failure — the MC makes a move. The cost carries.

The design descends from PbtA (Powered by the Apocalypse). The 7-9 result — success with complications — is the design innovation that PbtA contributed to game design. `eh!` encodes that structure in a proc macro.

`eh` — the shrug. For the engineer who's been here before.
`eh` — extra hard. For the engineer reading the docs.
`eh!` — the proc macro. For the compiler.

## Usage

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn pipeline(input: i32) -> Imperfect<i32, String, ConvergenceLoss> {
    eh! {
        let a = step_one(input)?;
        let b = step_two(a)?;
        b + 1
    }
}
```

Every `expr?` inside `eh!` routes through the `IntoEh` trait:

- **`Imperfect` values**: extracted via `Eh::eh()`, loss accumulated into a hidden context.
- **`Result` values**: passed through unchanged, no loss.

The final expression is wrapped with accumulated loss:
- All success, no loss: returns `Success(value)`
- Any loss accumulated: returns `Partial(value, accumulated_loss)`
- Any `?` hits `Failure` or `Err`: returns `Failure(error, accumulated_loss)`

## Mixing `Imperfect` and `Result`

Both types work inside `eh!`:

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn mixed() -> Imperfect<String, String, ConvergenceLoss> {
    eh! {
        let data = Imperfect::<Vec<u8>, String, ConvergenceLoss>::Success(vec![72, 105])?;
        let text: String = Ok::<String, String>(String::from_utf8_lossy(&data).into())?;
        text
    }
}
```

The `IntoEh` trait handles dispatch at compile time. Zero-cost: monomorphized away.

## Nested Blocks

Each `eh!` block gets its own accumulation context:

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn outer() -> Imperfect<i32, String, ConvergenceLoss> {
    eh! {
        let inner: Imperfect<i32, String, ConvergenceLoss> = eh! {
            let x = Imperfect::<i32, String, ConvergenceLoss>::Partial(10, ConvergenceLoss::new(2))?;
            x + 1
        };
        let v = inner?;
        v + 5
    }
}
```

The inner block produces `Partial(11, ConvergenceLoss(2))`. The outer block's `?` extracts the value and accumulates the inner loss.

## Recovery

Add a `recover` branch to handle partial results. If the body completes with accumulated loss (Partial), the recovery closure runs with the value and loss. The closure transforms the value; the loss stays unchanged. Success passes through untouched.

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn adjusted(input: i32) -> Imperfect<i32, String, ConvergenceLoss> {
    eh! {
        let a = step_one(input)?;
        let b = step_two(a)?;
        b + 1

        recover |value, loss| {
            // 7-9: you got it, it cost something
            eprintln!("lost {} steps", loss.steps());
            value * 2  // adjust the value based on what was lost
        }
    }
}
```

The `recover` closure receives `(value, loss)` — the value from the body and the accumulated loss. It returns a new value. The loss itself is fact, not something you edit.

If no loss accumulated (Success), the `recover` branch is never executed.

## Rescue

Add a `rescue` branch to handle failures. If any `?` in the body hits `Failure`, the rescue closure runs with the error. The accumulated loss from the try body carries into the rescue. The result is always `Partial` — the failure happened.

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn resilient(input: i32) -> Imperfect<i32, String, ConvergenceLoss> {
    eh! {
        let a = step_one(input)?;
        let b = step_two(a)?;
        b + 1

        rescue |e| {
            // 6-: the MC makes a move
            eprintln!("failed: {}", e);
            0  // fallback value
        }
    }
}
```

Without `rescue`, a `Failure` propagates as `Failure(error, accumulated_loss)`. With `rescue`, it becomes `Partial(rescued_value, accumulated_loss)`.

The rescue closure receives the error value. Use it or ignore it:

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn with_error_info() -> Imperfect<String, String, ConvergenceLoss> {
    eh! {
        let val = might_fail()?;
        format!("got: {}", val)

        rescue |e| {
            format!("rescued from: {}", e)
        }
    }
}
```

If no failure occurs, the `rescue` branch is never executed.

## Full PbtA Block

Both branches are optional and independent. Use one, both, or neither. When both are present, `recover` comes before `rescue`:

```rust
use terni::{eh, Imperfect, ConvergenceLoss};

fn full_pbta(input: i32) -> Imperfect<i32, String, ConvergenceLoss> {
    eh! {
        let a = step_one(input)?;
        step_two(a)

        // 7-9: you got it, it cost something
        recover |value, loss| {
            adjust(value, &loss)
        }

        // 6-: the MC makes a move
        rescue |error| {
            fallback(error)
        }
    }
}

// 10+: clean hit. no handler needed.
```

## How It Works

The `eh!` proc macro rewrites the block:

1. Creates a hidden `Eh` context (`__eh_ctx`)
2. Wraps the block body in a closure returning `Result`
3. Rewrites every `expr?` to `IntoEh::into_eh(expr, &mut __eh_ctx)?`
4. Wraps the final expression in `Ok(...)`
5. Matches the closure result: `Ok` checks accumulated loss, `Err` calls `failure_with_loss()`
6. With `recover`: `Ok` with loss runs the recover closure with `(value, loss)`, returns `Partial(new_value, loss)`
7. With `rescue`: `Err` builds a `Failure`, then calls `unwrap_or_else()` with the rescue closure — always producing `Partial`

## Limitations

**`return` returns from the block, not the enclosing function.** The macro wraps your code in a closure. `return` exits that closure, not your function. Use `?` for early exit from `eh!` blocks.

**No `async` support.** The closure wrapper doesn't interact well with `.await`. This needs design work and will come in a future release.

**`?` in closures accumulates to the same context.** If you use `?` inside a closure within `eh!`, loss accumulates into the outer block's context. This is usually what you want.

**`__eh_ctx` name collision.** The macro generates an internal variable named `__eh_ctx`. If your code uses a variable with this exact name, it will collide. The double-underscore prefix makes this unlikely in practice, but it is not hygienically scoped (proc macros operate at call-site span).

**`recover` / `rescue` as identifiers.** The parser detects `recover` and `rescue` keywords by scanning for a top-level identifier followed by `|`. If you have a variable named `recover` or `rescue` followed by a bitwise OR (`|`), the parser will misidentify it as a branch keyword. Rename the variable to avoid ambiguity.

**`needless_question_mark` clippy lint.** When the tail expression of an `eh!` block is `expr?`, the macro expansion produces `Ok(IntoEh::into_eh(expr, ctx)?)` which clippy flags as redundant. This is inherent to the rewriting strategy and does not affect correctness. Suppress with `#[allow(clippy::needless_question_mark)]` on the enclosing function if needed.

## The `IntoEh` Trait

The trait that makes `eh!` work:

```rust
pub trait IntoEh<T, E, L: Loss> {
    fn into_eh(self, ctx: &mut Eh<L>) -> Result<T, E>;
}
```

Implemented for:
- `Imperfect<T, E, L>` -- extracts value, accumulates loss into context
- `Result<T, E>` -- passes through unchanged

You can implement `IntoEh` for your own types to make them work with `eh!`.

## Feature Flag

The macro is behind the `macros` feature, which is on by default:

```toml
[dependencies]
terni = "0.5"  # macros included

# or explicitly:
terni = { version = "0.5", features = ["macros"] }

# without macros:
terni = { version = "0.5", default-features = false }
```