# Requirements
What `ohno_macros` has to deliver. Written before the rewrite, from the previous
implementation, the crate docs, and the tests in `crates/ohno`.
The authority for behavior is `crates/ohno/tests/**` (integration tests) and
`crates/ohno/tests/ui/**` (compile-fail snapshots): they are the only tests that
compile what the macros produce. The crate's own unit tests and expansion
snapshots pin the shape of the tokens each phase emits, which is a regression
net rather than a statement of what the crate owes.
## Public surface
Three entry points, re-exported by `ohno` as `ohno::Error`, `ohno::enrich_err`
and `ohno::error`.
| `Error` | derive | `error`, `display`, `no_constructors`, `no_debug`, `from` |
| `enrich_err` | attribute | none |
| `error` | attribute | none |
The helper attribute list is part of the surface: everything in it appears in
the derive's rustdoc, and anything not in it is not inert on a field.
Generated code names the crate as `ohno`. Renaming the package in `Cargo.toml`
is not supported.
## R1 — `#[derive(Error)]`
### R1.1 Input
Structs only. Named, tuple, and (through `#[ohno::error]`) unit structs that
have already been rewritten into tuple structs.
- An enum is rejected.
- A unit struct under the derive alone is rejected: there is no room for a core.
### R1.2 Picking the error field
Exactly one field holds the `OhnoCore`. It is found in this order:
1. A field carrying the reserved `#[doc]` marker that `#[ohno::error]` writes.
2. A field carrying `#[error]`.
3. The single field whose type path ends in `OhnoCore`.
Step 3 reads the last path segment and resolves nothing, so a core reached
through a type alias or a renamed import is invisible to it and has to be
marked. A marked field's type is never checked — `rustc` resolves it.
Rejected:
- `#[error]` with any argument: `#[error(x)]`, `#[error()]`, `#[error = "x"]`.
- Two fields marked, or one field marked twice.
- `#[error]` on a struct that already carries the generated marker.
- No marker and no `OhnoCore` field; no marker and several of them.
Full statement of the rules, including why the marker is a doc comment with a
nonce: `error_error.md`.
### R1.3 Generated items
For every accepted input:
| `Display` | the core, plus the `#[display(...)]` message when present |
| `std::error::Error` | `source()` delegates to the core |
| `ohno::Enrichable` | `add_enrichment` delegates to the core |
| `ohno::ErrorExt` | `message()` and `backtrace()` delegate to the core |
| `From<std::convert::Infallible>` | always; the body is `unreachable!` |
| `Debug` | unless `#[no_debug]`; prints every field, including the core |
| constructors | unless `#[no_constructors]` |
| `From<T>` | one per type listed in `#[from(...)]` |
All of them carry the input's generics, type generics, and where clause.
### R1.4 Constructors
Two associated functions, both `pub(crate)`:
- `new(...)` — one parameter per non-core field, each `impl Into<FieldType>`;
the core is `OhnoCore::default()`.
- `caused_by(..., error)` — the same parameters plus a trailing
`impl Into<Box<dyn std::error::Error + Send + Sync>>`; the core is
`OhnoCore::from(error)`.
For a tuple struct the parameters are named `param_0`, `param_1`, … by field
index, skipping the core, and the field order of the struct is preserved.
`pub(crate)` is deliberate and documented: an error type that needs a public
constructor declares one by hand. `#[no_constructors]` suppresses both.
### R1.5 `#[display(...)]`
`#[display("template")]` or `#[display("template", arg1, arg2)]`. The template
is a `format!` string.
| `{name}` | the field called `name` |
| `{0}` | tuple field 0 |
| `{name:spec}` | the field, with `spec` as the format spec |
| `{}` | the next positional argument |
| `{{`, `}}` | a literal brace |
Positional arguments are implicitly scoped to `self`: a field is written by its
bare name, and the argument's leftmost term is the one that has to name a field
or call a method of `self`.
The scoped argument is emitted as `&(self.<arg>)` — the parentheses are
load-bearing, so that `count as u64` casts the field rather than a reference to
it.
Referenceable fields are every field the user wrote; the field injected by
`#[ohno::error]` is excluded, a core the user declared is not. Raw identifiers
keep their `r#`.
Rejected, by the macro rather than by the expansion:
- A placeholder naming something that is not a field.
- `{}` with no argument left, or an argument no `{}` consumes.
- An argument written with a `self.` prefix.
- An argument rooted in anything other than a field or method of `self`.
- An unbalanced `{` or `}`.
Diagnostics point at what the user wrote, and any diagnostic spanning more than
one token is built with `syn::Error::new_spanned` so it renders identically on
every toolchain. The exact messages are pinned by `crates/ohno/tests/ui/*.stderr`.
Full statement, including the argument-rooting rules: `error_display.md`.
### R1.6 `#[from(...)]`
`#[from(Type1, Type2)]` generates one `From` per type. A type may carry field
expressions: `#[from(std::io::Error(kind: ErrorKind::Io, message: "…"))]`, keyed
by field name for a named struct and by index for a tuple struct.
The core field is built with `OhnoCore::from(error)`. Every other field takes
its expression if one was given and `Default::default()` otherwise.
Rejected: `#[from]`, `#[from()]`, `#[from = "…"]`, and a non-integer key for a
tuple field. Several `#[from(...)]` attributes on one struct accumulate.
### R1.7 `#[no_debug]` and `#[no_constructors]`
Bare markers on the struct. `#[no_debug]` suppresses the generated `Debug` so a
hand-written one can stand; without it, a manual `#[derive(Debug)]` collides.
`#[no_constructors]` suppresses `new` and `caused_by`, and is rejected under
`#[ohno::error]`.
## R2 — `#[ohno::error]`
Rewrites a struct, then applies `#[derive(ohno::Error)]` to it.
- Named struct: appends a field `ohno_core: ohno::OhnoCore`, renamed
`ohno_core_1`, `ohno_core_2`, … on collision.
- Tuple struct: appends `ohno::OhnoCore` as the last field.
- Unit struct: rewritten as a tuple struct holding the core.
- Anything that is not a struct is rejected.
The added field carries the reserved doc marker, which is what makes it findable
in R1.2 without listing a helper attribute that would then show up in rustdoc.
The attribute runs before it adds anything, so it can tell a hand-written marker
from its own and rejects both `#[error]` on any field and a hand-written
reserved marker. Doc comments and other attributes on the struct survive.
## R3 — `#[enrich_err(...)]`
Wraps a function body so that an `Err` gains an enrichment entry carrying a
message, `file!()` and `line!()`.
Arguments:
| none | `"error in function <name>"` |
| `"text"` with no braces | the literal |
| `"text {field}"` | `format!` of the literal |
| `"text {}", expr, …` | `format!` of the literal and arguments |
The first token has to be a string literal; anything else is rejected. A
function with no return type is rejected.
The body is rewritten into an immediately-invoked closure so that `?` inside it
still returns from the closure, `.await`ed when the function is `async`. Every
part of the signature has to survive: visibility, `const`, `unsafe`, `extern`
ABI, generics, lifetimes, bounds, where clauses, `impl Trait` and `dyn Trait`
parameters, `self` receivers in every form, doc comments, and other attributes.
`const` survives faithfully enough that a `const fn` is then rejected by `rustc`
rather than by the macro, which `design.md` records under Limits.
## R4 — Diagnostics
A macro reports what it can rather than emitting code that fails to compile.
Errors reach the user as `compile_error!` at a span in their own source, never
as a panic and never as a `rustc` error pointing into generated code. A few
inputs survive from the implementation this replaces where that does not hold;
`design.md` records them under Limits.
Where an input breaks several rules, all of them are reported at once rather
than one per compile cycle.
Wherever a diagnostic covers more than one token it is spanned with
`syn::Error::new_spanned`.
## R5 — Quality gates
The crate has to keep passing what the workspace already runs against it:
`cargo check`, `clippy` at workspace lint level, `cargo doc`, `cargo fmt
--check`, `cargo machete`, license boilerplate, and `cargo mutants` on
`validate.rs` and `display/`, where the rules live. The `ohno` crate's doc tests
and examples exercise the macros and have to keep compiling.