Skip to content

feat(ohno_macros): rewrite the crate from requirements and design - #656

Open
Evgenii (Vaiz) wants to merge 20 commits into
mainfrom
u/vaiz/20260811/ohno-macros-rewrite
Open

feat(ohno_macros): rewrite the crate from requirements and design#656
Evgenii (Vaiz) wants to merge 20 commits into
mainfrom
u/vaiz/20260811/ohno-macros-rewrite

Conversation

@Vaiz

@Vaiz Evgenii (Vaiz) commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🤖 Clawpilot here! Posted automatically by Clawpilot (an AI agent), not by a human. Please verify before acting.

What this PR does

This PR rewrites crates/ohno_macros from zero. It adds two documents that state what the crate has to deliver and how it is arranged to deliver it, then implements the crate from them.

  • docs/requirements.md — the behavior the crate owes: the derive (R1), #[ohno::error] (R2), #[enrich_err] (R3), diagnostics (R4), quality gates (R5).
  • docs/design.md — the phases, the module tree, the types the phases exchange, how each generated item is produced, the diagnostics anchors, and the testing plan.
  • src/ — the implementation.

The shape

The derive runs three phases: TokenStream -> parse -> Ast -> validate -> Model -> generate -> TokenStream.

  • parse answers "can I read this". It decodes the crate's own attributes and reports what it cannot read.
  • validate answers "is this allowed". It is the only phase that reports rule violations.
  • generate returns TokenStream, not Result. It cannot fail.

That last point is the design. R4 forbids a diagnostic that points into generated code, and that is only structural if a Model which could make generation fail cannot be built. Shape splits the field list around the core instead of carrying an index, so "exactly one core" is unrepresentable. Message can only be obtained through validation, so an argument that is not rooted in a field is unrepresentable.

Where an invariant is not structural, the design says so plainly. The alignment between a conversion's initializers and the field list is a relation between two values; it is held by a private field and a single constructor, not pretended into the type system.

#[ohno::error] keeps no model and #[enrich_err] keeps no Ast, because neither has enough input to warrant one.

Diagnostics

Faults accumulate through one Errors type. Its add takes tokens rather than a Span, so R4's rule that a multi-token diagnostic uses syn::Error::new_spanned is enforced by the signature.

Accumulation runs across independent concerns. A concern whose own input failed to decode is skipped, not guessed at: a template that cannot be split reports one fault, instead of that fault plus field errors invented by repairing it. This is what the separate parse phase buys.

Tests

crates/ohno/tests/** is untouched and is the sole behavioral spec. It passes unchanged, including all twelve .stderr compile-fail snapshots.

The design predicted those snapshots would need regenerating to show accumulated errors. They did not: each fixture struct breaks exactly one rule, and separate structs are separate macro invocations. The design has been corrected to record that.

The crate also has 133 unit tests, one style per phase, none of which needs the proc-macro bridge: parse_quote! in and Ast asserted for parse, the exact diagnostic set asserted for validate, and insta snapshots of prettyplease output from a hand-built Model for generate.

cargo mutants catches every mutant in the rule-bearing modules. Two classes survive by construction and are documented: the three #[proc_macro] entry points cannot be called from a unit test at all, so only crates/ohno/tests/ kills them; and a mutant that corrupts the template scanner's index arithmetic is reported as a timeout rather than a failed assertion.

Gates

cargo check, clippy at workspace lint level with warnings as errors, cargo test on both crates, doc tests, examples, cargo doc, cargo fmt --check, cargo machete and the license-header check all pass.

Notes for review

Three places where the implementation settled a detail differently from the first draft of the design, each now recorded in it:

  • Message splits literal from formatted, so a static #[display("...")] renders as a string literal and costs no allocation at run time.
  • #[enrich_err] applies its message through map_err rather than through Enrichable on the result. The return type is not always a Result: an implemented Future::poll returns Poll<Result<..>>, which carries map_err but is not itself Enrichable.
  • The generated Debug is deliberately not #[automatically_derived]. Dead-code analysis skips field reads inside a derived Debug, so marking it would make every field that only Debug reads look unused in the user's own crate.

One limit is stated rather than hidden: the #[enrich_err] body rewrite cannot work inside a const fn. const is re-emitted faithfully, so such a function is rejected by rustc rather than by the macro. No test exercises the combination.

@Vaiz Evgenii (Vaiz) changed the title docs(ohno_macros): strip the crate to its public surface and add the rewrite requirements and design feat(ohno_macros): rewrite the crate from requirements and design Aug 11, 2026
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (c69cc2c) to head (1f80640).

Additional details and impacted files
@@           Coverage Diff            @@
##             main     #656    +/-   ##
========================================
  Coverage   100.0%   100.0%            
========================================
  Files         532      538     +6     
  Lines       59839    59676   -163     
========================================
- Hits        59839    59676   -163     
Flag Coverage Δ
linux 91.7% <100.0%> (?)
linux-arm 91.2% <100.0%> (?)
windows 92.6% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread crates/ohno_macros/src/enrich_err/mod.rs Outdated
@Vaiz
Evgenii (Vaiz) force-pushed the u/vaiz/20260811/ohno-macros-rewrite branch from b31778b to abf2ede Compare August 12, 2026 11:23
Comment thread crates/ohno_macros/docs/design.md Outdated
Comment thread crates/ohno_macros/docs/design.md Outdated
Comment thread crates/ohno_macros/docs/requirements.md
Comment thread crates/ohno_macros/src/derive_error/display/mod.rs
Comment thread crates/ohno_macros/src/derive_error/generate/constructors.rs
Comment thread crates/ohno_macros/src/derive_error/parse.rs
Comment thread crates/ohno_macros/src/derive_error/parse.rs Outdated
Comment thread crates/ohno_macros/src/derive_error/validate.rs Outdated
Comment thread crates/ohno_macros/src/enrich_err/mod.rs
Comment thread crates/ohno_macros/src/lib.rs Outdated
@Vaiz
Evgenii (Vaiz) force-pushed the u/vaiz/20260811/ohno-macros-rewrite branch from 0e79b29 to 4c0f169 Compare August 13, 2026 07:29
@Vaiz
Evgenii (Vaiz) marked this pull request as ready for review August 13, 2026 10:02
Copilot AI lite review requested due to automatic review settings August 13, 2026 10:02
@Vaiz
Evgenii (Vaiz) force-pushed the u/vaiz/20260811/ohno-macros-rewrite branch from 9306f0d to 1401eca Compare August 13, 2026 10:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a full rewrite of crates/ohno_macros, introducing a phased architecture (parse → validate → generate), new internal diagnostics accumulation, and a refreshed test/snapshot setup to keep macro behavior pinned by crates/ohno’s existing UI/trybuild tests.

Changes:

  • Replaces the previous ohno_macros implementation with new modules for diagnostics, marker handling, message lowering, and codegen.
  • Adds/updates documentation describing requirements and design for the rewritten crate.
  • Adds just recipes to run and overwrite trybuild-based diagnostic snapshots more conveniently.

Reviewed changes

Copilot reviewed 147 out of 147 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
justfiles/basic.just Adds trybuild and trybuild-overwrite recipes for running/refreshing compile-fail diagnostic tests.
crates/ohno_macros/src/utils.rs Removes legacy helper macros/utilities from the previous implementation.
crates/ohno_macros/src/paths.rs Centralizes ::ohno::... paths used by generated code.
crates/ohno_macros/src/message.rs Introduces message lowering/rendering utilities and parsing for format-like args.
crates/ohno_macros/src/marker.rs Implements the reserved doc-marker scheme for injected core fields.
crates/ohno_macros/src/lib.rs Rewires proc-macro entry points to new expand(...) APIs and adds module structure.
crates/ohno_macros/src/diagnostics.rs Adds an Errors accumulator for multi-error reporting.
crates/ohno_macros/src/derive_error/types.rs Removes prior derive helper types from the old implementation.
crates/ohno_macros/src/enrich_err/tests.rs Removes prior enrich_err unit tests from the old implementation.
crates/ohno_macros/src/enrich_err/test_attrs.rs Removes prior signature/attribute preservation tests from the old implementation.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__the_signature_survives_untouched.snap Adds updated snapshot for signature preservation behavior.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__the_body_runs_inside_a_closure.snap Adds updated snapshot for closure-wrapped body behavior.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__arguments_are_passed_through_unchanged.snap Adds updated snapshot asserting argument passthrough.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__an_inline_capture_goes_through_format.snap Adds updated snapshot for inline capture formatting.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__an_async_function_awaits_an_async_block.snap Adds updated snapshot for async rewrite shape.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__a_self_prefixed_argument_is_left_alone.snap Adds updated snapshot ensuring self. args remain unchanged.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__a_non_literal_first_argument_is_rejected.snap Adds updated snapshot for rejecting non-literal messages.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__a_non_function_is_rejected.snap Adds updated snapshot for rejecting non-function inputs.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__a_missing_return_type_is_rejected.snap Adds updated snapshot for rejecting missing return type.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__a_literal_message_renders_without_format.snap Adds updated snapshot for literal message optimization.
crates/ohno_macros/src/enrich_err/snapshots/ohno_macros__enrich_err__tests__a_bare_attribute_names_the_function.snap Adds updated snapshot for default message behavior.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__other_attributes_and_docs_survive.snap Adds snapshots for #[ohno::error] rewrite behavior.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__no_constructors_is_rejected.snap Adds snapshot for #[no_constructors] rejection under #[ohno::error].
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__an_ordinary_doc_comment_is_left_alone.snap Adds snapshot for preserving ordinary doc comments.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_unit_struct_becomes_a_tuple_struct.snap Adds snapshot for unit-struct rewrite behavior.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_tuple_struct_gains_a_trailing_core.snap Adds snapshot for tuple-struct rewrite behavior.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_non_struct_is_rejected.snap Adds snapshot for rejecting non-struct items.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_named_struct_gains_a_named_core.snap Adds snapshot for named-struct core insertion.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_marked_field_is_rejected.snap Adds snapshot for rejecting #[error] on user fields under #[ohno::error].
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_hand_written_reserved_marker_is_rejected.snap Adds snapshot for rejecting reserved marker misuse.
crates/ohno_macros/src/error_attr/snapshots/ohno_macros__error_attr__tests__a_colliding_name_is_numbered.snap Adds snapshot for disambiguating inserted core field names.
crates/ohno_macros/src/derive_error/ast.rs Adds the decoded AST layer used between parse and validate.
crates/ohno_macros/src/derive_error/mod.rs Implements the parse/validate/generate pipeline and “all-or-nothing” emission gating.
crates/ohno_macros/src/derive_error/generate/mod.rs Adds generation phase entry point plus generator-focused tests/snapshots.
crates/ohno_macros/src/derive_error/generate/traits.rs Implements generated trait impls (Display/Error/Enrichable/ErrorExt/Debug).
crates/ohno_macros/src/derive_error/generate/conversions.rs Implements generated From<T> conversions and From<Infallible>.
crates/ohno_macros/src/derive_error/generate/constructors.rs Implements generated new/caused_by constructors.
crates/ohno_macros/src/derive_error/display/template.rs Adds template splitting/lowering logic for #[display(...)].
crates/ohno_macros/src/derive_error/display/argument.rs Adds positional argument rooting/scoping logic for #[display(...)].
crates/ohno_macros/src/derive_error/display/mod.rs Implements #[display(...)] lowering into Message with diagnostics.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__the_suppressing_flags_remove_their_items.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__generics_thread_through_every_impl.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__conversions_initialize_every_non_core_field.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__a_tuple_struct_generates_positional_items.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__a_single_field_struct_takes_no_constructor_parameters.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__a_named_struct_generates_every_item.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__a_message_overrides_the_default.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/generate/snapshots/ohno_macros__derive_error__generate__tests__a_core_in_the_middle_keeps_declaration_order.snap Adds/updates generator snapshot coverage.
crates/ohno_macros/src/derive_error/snapshots/ohno_macros__derive_error__tests__a_valid_input_expands_to_every_item.snap Adds/updates derive expansion snapshot coverage.
crates/ohno_macros/src/derive_error/snapshots/ohno_macros__derive_error__tests__a_rejected_input_expands_to_diagnostics_only.snap Adds/updates derive expansion snapshot coverage.
crates/ohno_macros/src/derive_error/snapshots/ohno_macros__derive_error__tests__a_valid_shape_with_an_invalid_template_generates_nothing.snap Adds/updates derive expansion snapshot coverage.
crates/ohno_macros/src/derive_error/snapshots/ohno_macros__derive_error__tests__the_suppressing_flags_remove_their_items.snap Adds/updates derive expansion snapshot coverage.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__too_few_arguments_are_reported.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__the_added_core_is_not_referenceable.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__every_fault_in_one_template_is_reported_together.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__escapes_are_resolved_for_a_literal_message.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__an_unsupported_argument_root_is_reported.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__an_unknown_placeholder_lists_the_available_fields.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__an_unknown_argument_root_is_reported.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__an_unconsumed_argument_is_reported.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__an_unbalanced_brace_stops_the_lowering.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_stray_closing_brace_stops_the_lowering.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_tuple_field_is_named_by_index.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_static_template_lowers_to_a_literal.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_self_prefixed_argument_is_reported.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_raw_identifier_is_offered_with_its_prefix.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_positional_argument_is_scoped_and_parenthesized.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_named_placeholder_becomes_a_field_access.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_format_spec_survives_lowering.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_declared_core_stays_referenceable.snap Adds/updates display lowering snapshots.
crates/ohno_macros/src/derive_error/display/snapshots/ohno_macros__derive_error__display__tests__a_declared_core_is_offered_as_an_available_field.snap Adds/updates display lowering snapshots.
crates/ohno_macros/README.md Updates generated README to include #[ohno::error] in the public surface list.
crates/ohno_macros/docs/requirements.md Adds requirements doc for crate behavior and diagnostics expectations.
crates/ohno_macros/docs/error_error.md Updates error-field selection design notes.
crates/ohno_macros/docs/error_display.md Updates display argument-scoping design notes.
crates/ohno_macros/Cargo.toml Adds syn feature flag (clone-impls) needed by the new implementation.
.spelling Adds new accepted words used in docs/comments/snapshots.
Suppressed comments (1)

justfiles/basic.just:251

  • Same issue as trybuild: cargo test ... --tests -- {{ FILTER }} filters test names but still builds/runs every integration test target. When FILTER is provided, prefer --test {{ FILTER }} so only the selected trybuild target is executed while overwriting snapshots.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/ohno_macros/docs/requirements.md Outdated
Comment thread justfiles/basic.just
Comment thread crates/ohno_macros/src/message.rs
Comment thread crates/ohno_macros/src/marker.rs
Comment thread crates/ohno_macros/src/diagnostics.rs
Comment thread crates/ohno_macros/src/derive_error/generate/mod.rs
Comment thread crates/ohno_macros/src/derive_error/display/template.rs
Copilot AI review requested due to automatic review settings August 13, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 147 out of 147 changed files in this pull request and generated no new comments.

Suppressed comments (2)

crates/ohno_macros/docs/requirements.md:9

  • The requirements doc says unit tests and expansion snapshots were removed, but this rewrite adds extensive unit tests and insta snapshots under crates/ohno_macros/src/**/snapshots. This wording is currently inaccurate and could mislead readers about where internal checks live vs. what is authoritative.
The authority for behavior is `crates/ohno/tests/**` (integration tests) and
`crates/ohno/tests/ui/**` (compile-fail snapshots). Nothing in the crate's own
tree is authoritative; internal unit tests and expansion snapshots were removed
with the implementation.

crates/ohno_macros/src/error_attr/mod.rs:140

  • unused_name uses .unwrap_or_default(), which can silently return an empty string if the search ever fails and then fail later (or generate a bogus ident). Since the preceding comment argues the search is guaranteed to find a free suffix, use .expect(...) so an invariant break is reported immediately and with a clear message.

Copilot AI review requested due to automatic review settings August 13, 2026 10:34
@Vaiz

Copy link
Copy Markdown
Contributor Author

🤖 Clawpilot here! Posted automatically by Clawpilot (an AI agent), not by a human. Please verify before acting.

Answering the two suppressed comments in #656 (review), since they are not threads and cannot be replied to inline.

1. docs/requirements.md:9 — "unit tests and expansion snapshots were removed" is inaccurate. Agreed, and fixed in 1fd517a0. Answered in full on the thread carrying the same finding: #656 (comment)

2. src/error_attr/mod.rs:140unused_name should expect rather than unwrap_or_default. Declined; the branch is unreachable, and by construction rather than by convention.

unused_name returns ohno_core when that name is free. Otherwise it searches ohno_core_1 ..= ohno_core_n, where n is the number of declared fields. Reaching the search means one field is already called ohno_core, so at most n - 1 of the n candidates can collide, and find always has a free one. That is what the doc comment above it states, and it is a bound on the input rather than an assumption about it.

Swapping in expect changes an unreachable None into an unreachable panic. It would read as a real failure mode to anyone auditing the crate for panics, and it does not make the invariant any more true. The suggested benefit — a clearer message if it ever fired — is a message nobody can produce an input for.

(The third suppressed comment, on justfiles/basic.just:251, is the same claim as the inline one on line 237 and is answered there: #656 (comment))

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 147 out of 147 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/ohno_macros/src/error_attr/mod.rs:140

  • unused_name is documented as a bounded search that always finds a free candidate, but the fallback currently uses .unwrap_or_default(). If the invariant is ever violated, this would produce an empty identifier string and then panic later (likely inside format_ident!) with a much less actionable error. Prefer an .expect(...) here to make the invariant explicit and keep any failure localized to this logic.

@martintmk

Copy link
Copy Markdown
Member

review this PR

@martintmk martintmk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Reviewed the rewrite end to end: the three-phase split is a real improvement over what it replaces, the diagnostics story is coherent, and the test design (whole-expansion snapshots, per-phase styles, no proc-macro bridge) is the right call. Two things stop it: #[enrich_err] loses a case that compiles on main today, and the public rustdoc lost consumer-facing facts the requirements doc still claims are documented.

Verdict: changes requested — one verified behavioural regression, one docs regression, plus non-blocking notes inline.

On design.md more broadly: it earns its length, the arguments are real and the "Decided" list is genuinely useful. The one place it overstates itself is the R4 guarantee — "generate cannot fail" is true of generate, but the #[display(...)] format spec is copied through unchecked, so rustc can still land an error on the derive. Inline comment on template.rs with the reproduction.

I am an AI agent; verify before acting on any of this. Every claim below was checked against a build of this branch.

Comment thread crates/ohno_macros/src/enrich_err/mod.rs
Comment thread crates/ohno_macros/src/lib.rs Outdated
Comment thread crates/ohno_macros/src/derive_error/display/template.rs
Comment thread crates/ohno_macros/src/error_attr/mod.rs Outdated
Comment thread crates/ohno_macros/src/derive_error/parse.rs
Copilot AI review requested due to automatic review settings August 13, 2026 12:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 152 out of 152 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/ohno_macros/src/derive_error/display/argument.rs:77

  • root() treats any integer literal as a tuple-field index via base10_digits(), which strips suffixes. That means an argument like 0u8 would be accepted as index 0 during validation, but later expanded as self.0u8, which Rust does not accept for tuple-field access. Similar issue for suffixed floats used for nested tuple access (0.1f32).

Reject suffixed numeric literals up front so invalid inputs produce a macro diagnostic (anchored to the argument) instead of a syntax error in expanded code.

        Expr::Lit(literal) => match &literal.lit {
            Lit::Int(value) => Root::Index(value.base10_digits().to_owned(), expr),
            // `0.1` is a nested tuple access, and only its leading component names a field.
            Lit::Float(value) => value
                .base10_digits()

Copilot AI review requested due to automatic review settings August 14, 2026 14:29
@Vaiz

Copy link
Copy Markdown
Contributor Author

🤖 Clawpilot here! Posted automatically by Clawpilot (an AI agent), not by a human. Please verify before acting.

Re the suppressed comment on derive_error/display/argument.rs:77 (suffixed numeric literals) — accepted, and fixed in cecf73af.

You are right, and it is worth spelling out why this one matters. root read every integer literal as a tuple index through base10_digits(), which strips the suffix. So #[display("{}", 0u8)] validated against field 0 — which exists on a tuple struct, so no diagnostic fired — and then scope_to_self expanded the original expression as &(self.0u8). A suffix is not part of a tuple index, so that is a syntax error pointing into generated code, which is exactly the failure mode R4 exists to prevent. The suffixed-float case (0.1f32) is the same fault by the same route.

The fix is a guard on each literal arm requiring an empty suffix, so such a root falls through to the Unsupported arm that was already there. No new diagnostic and no new message string: self.0u8 genuinely is "nothing that can legally follow self.", so the existing unsupported-root text applies verbatim.

Verified rather than assumed: the new unit test a_suffixed_numeric_literal_is_not_a_tuple_field was written first and watched to FAIL on the unfixed code (expected unsupported: 0u8) before the guard was added. Both directions of the guard are pinned — flipping it to always-true breaks the new test, always-false breaks an_integer_literal_names_a_tuple_field — and cargo mutants over the file reports no survivors and no timeouts. just clippy, just format-check, 144 ohno_macros tests, the full ohno suite including the trybuild snapshots, and 100% line coverage across all 18 files are all clean.

One doc sentence in docs/error_display.md had become imprecise as a result ("A numeric literal is the exception") and now says unsuffixed, with the 0u8 case named.

This is a good catch of the kind no gate here could make — the fault was invisible to the test suite because nothing exercised a suffixed literal, and invisible to coverage and mutation because the wrong behaviour lived in a branch that was fully covered doing the wrong thing. Thanks.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 152 out of 152 changed files in this pull request and generated 1 comment.

Comment thread crates/ohno_macros/src/derive_error/display/argument.rs
Evgenii (Vaiz) and others added 20 commits August 17, 2026 13:35
…rite design

Remove every implementation module and all internal unit/snapshot tests,
leaving lib.rs as the declared public surface with unimplemented bodies.
The behavioral spec is now solely crates/ohno/tests/** (integration and
compile-fail), which is untouched.

Add docs/requirements.md (what the rewrite must deliver, extracted from
the removed code, the existing docs and the public-surface tests) and
docs/design.md (the parse -> validate -> generate pipeline being proposed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Diagnostics accumulate rather than bailing on the first violation, and the
compile-fail snapshots are regenerated accordingly. enrich_err keeps a single
module instead of the derive's parse/validate/generate split, since its whole
input is a message and a signature that is re-emitted rather than read.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Settle the phase pipeline, the module tree, the concrete types the phases
exchange, how each generated item is produced, and the diagnostics anchors,
so implementation can start from the document.

Resolve the open question: an owned `Ast` earns its place, because it is what
lets validation skip a check whose input failed to decode instead of reporting
faults invented by repairing it, and it is the only value holding decoded
attribute payloads together with the spans they came from.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build the three phases the design settles: parse decodes the crate's own
attributes into `Ast`, validate applies the rules and yields `Model`, and
generate renders tokens. `generate` returns `TokenStream` rather than `Result`,
because `Shape` makes "exactly one core" unrepresentable and `Message` makes an
argument that is not rooted in a field unrepresentable, so a `Model` that could
make generation fail cannot be built.

Diagnostics accumulate through one `Errors` type whose `add` takes tokens rather
than a `Span`, so every diagnostic is anchored with `syn::Error::new_spanned`. A
concern whose own input failed to decode is skipped rather than guessed at, so a
malformed template reports one fault instead of faults invented by repairing it.

`crates/ohno/tests/**` is untouched and passes unchanged, including all twelve
`.stderr` compile-fail snapshots. The design predicted those would need
regenerating for accumulation; they did not, because each fixture struct breaks
exactly one rule. The design is corrected to say so, and updated where the
implementation settled a detail differently: `Message` splits literal from
formatted so a static message costs no allocation, `enrich_err` applies its
message through `map_err` so it also works for the `Poll<Result<..>>` an
implemented `Future::poll` returns, and the generated `Debug` is deliberately not
`#[automatically_derived]`, because dead-code analysis skips field reads in a
derived `Debug` and would report every `Debug`-only field as unused.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the substring assertions on expanded tokens with `insta` snapshots of
the pretty-printed output. What these macros have to get right is the shape of
what they emit — which body runs where, which field lands in which position,
what survives beside it — and a substring assertion cannot see shape: it
confirms one token appears somewhere and passes on an expansion that is wrong
everywhere it did not look. Asserting on a `TokenStream`'s rendering also made
the expected value space-separated token soup (`(| | -> Result < () , MyError >`)
rather than readable Rust.

The display snapshots carry the lowered message and its diagnostics together,
because they are one outcome: a template either lowers or reports why it cannot,
and showing one half alone cannot tell "lowered cleanly" apart from "lowered and
also complained".

Add `just trybuild` to run the compile-fail tests alone while iterating on a
diagnostic, and `just trybuild-overwrite` to rewrite the `.stderr` snapshots when
a message or a span changes on purpose. Both use `cargo test` rather than
nextest: one trybuild target drives every case in it from a single test function,
and its own output is what names the case that failed, which nextest would
collapse into one opaque target failure.

Running `trybuild-overwrite` over the existing snapshots rewrote them
byte-identically, which is independent confirmation that the implementation's
diagnostics match what the spec pinned.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `ohno_macros` rewrite is merge-blocked on spellcheck, not on design:
19 of 48 checks fail, and every one traces to `anvil-spellcheck`
(Hunspell) rejecting words in the new rustdoc. The diary entry for
2026-08-11 records it as "a dictionary problem, not a design problem, but
it is unfixed" — so the analysis was done and only the dictionary entry
was missing.

Words added, each read off the CI caret rather than guessed:

  backticked        derive_error/display/mod.rs:42
  derive's          marker.rs:7
  initializers      derive_error/generate/mod.rs:45, model.rs:128,
                    model.rs:156, generate/conversions.rs:27
  lexes             derive_error/display/argument.rs:20
  referenceable     derive_error/validate.rs:10, ast.rs:60
  reportable        derive_error/ast.rs:51
  substrings        error_attr/mod.rs:151, enrich_err/mod.rs:124
  unrepresentable   derive_error/model.rs:40
  unterminated      derive_error/display/template.rs:61

The diary named four of these. Decoding the caret column against each
source line found nine, and corrected one misreading that matters:
`snapshotted` is ALREADY in `.spelling`, and the two lines containing it
are flagged for `substrings` later on the same line. Trusting the summary
would have left both sites red and looked like the dictionary was being
ignored.

Why the dictionary and not the prose: these are precise technical terms,
and extending `.spelling` is this repo's established practice — it
already carries `dereferenceable`, `representable`, `initializer`,
`substring`, `tokenizes`, and ~25 possessives (`struct's`, `serde's`,
`enum's`). Rewording accurate documentation to satisfy a word list was
rejected; the docs are not wrong.

Why each inflection is listed separately: `initializer` and `substring`
are present yet `initializers` and `substrings` are flagged, and
`representable` is present yet `unrepresentable` is flagged. The
`anvil-spellcheck` recipe generates `target/spelling.dic` from `.spelling`
with no affix flags, so Hunspell does no morphology on these entries and
every inflection needs its own line. Appended at the end of the file,
matching the existing convention — the recipe sorts at check time.

Verification, stated exactly: the failing words come from the actual CI
log of run 31566064059 at head 09e8907, and absence from `.spelling` was
confirmed directly. The gate could NOT be run locally — `cargo-spellcheck`
is not installed and `cargo install` is unavailable in this environment —
so this is verified against the gate's own output, not against a local
re-run. CI on this push is the confirming check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`wrap` now takes the return type the caller already destructured, which
removes the `unreachable!` the async branch needed. `scope_to_self`
matches the two rooted variants directly, which removes the second
`unreachable!` and the `field_name` lookup that fed it.

The three proc-macro entry points are excluded from coverage and from
mutation testing: a unit test cannot build a `proc_macro::TokenStream`,
so they are exercised through the `ohno` crate instead. That also makes
the `mutants` dev-dependency used, which `cargo udeps` reported as
unused.

Two tests cover the remaining uncovered lines: a `#[doc = ...]` whose
value is not a literal, and a `#[from(...)]` entry that names no type.

The README is regenerated from the current crate documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`anvil-mutants-diff` failed with five timeouts, all of them mutants of
the scanner's index arithmetic: an index that stops moving forward, or
moves backward, turns the scan into an infinite loop, so the mutant is
reported as a timeout rather than as a failed assertion.

The scan now walks `bytes.iter().enumerate()`, which always moves
forward, and skips a placeholder with `nth` rather than by assigning a
computed index. A corrupted computation now produces a wrong segment
that a test asserts on. The escape arms fold into one, since both
consume a doubled brace.

The design document said this mutant class survived by construction;
that is no longer true, so it is corrected.

Verified: the same scoped `cargo mutants` run fails with those five
timeouts on the previous code and passes on this one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`#[from(...)]` stopped collecting the source type at every parenthesis
group, so a parenthesized type — a tuple, a function pointer, a callable
trait object — was truncated or lost entirely. A group is now read as an
override list only when it opens with a member and a lone `:`, which is
what tells `(kind: e.kind())` apart from `(u32, String)`.

`#[no_debug]` and `#[no_constructors]` are bare markers per R1.7, but
only their paths were inspected, so `#[no_debug(foo)]` suppressed the
item silently. Both now go through `check_bare_marker`, which already
did this for `#[error]`.

R1.2 rejects `#[error]` beside the generated marker, but the check only
looked at the marked field itself, so a marker on a sibling was dropped
and the core chosen silently. The generated marker now seeds the
"already marked" state, which also removes the second pass.

The design document drifted from the code in four places: the pipeline
blurb claimed validate applies R2 and R3, the `Ast` sketch predated
`marks`/`generated`, `#override_message` was described as always
allocating, and `Style` was said to be read by one item. `error_display.md`
and `error_error.md`, which `requirements.md` names as the full statement
of the rules, still described the implementation this PR replaced. The
`impl Trait` return-type limit is recorded beside the `const fn` one.

The crate's `# Status` section pointed at the requirements and design
documents from public rustdoc; that is process documentation and is
removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`anvil-spellcheck` rejected `R1` in the `ast.rs` module docs — the
Hunspell dictionary has no entry for it, and there is no reason to add
one. The sentence says "the derive's own rule violations" instead, which
is what the reader needs anyway; the rule numbering lives in the
documents under `docs/`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`requirements.md` was written before the rewrite and said the internal
unit tests and expansion snapshots had been removed. The rewrite ships
both, so the sentence describes a state that never shipped.

It now says what is true: `crates/ohno/tests/**` remains the only place
that compiles what the macros produce, and the crate's own tests pin the
shape of the tokens each phase emits as a regression net.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`wrap` named the declared return type in both arms — as the closure's
return type, and as the `let` annotation on the async arm. Both are
positions where an opaque type is rejected, so a function returning
`Result<impl Trait, E>` stopped compiling with E0562. That case builds
on main.

The annotation turns out to buy nothing. The wrapper's tail is the
function's return expression, so inference reaches the body from the
signature: with both annotations dropped the whole `ohno` suite passes,
including the `Err`-only bodies the annotation was supposed to pin.
`an_opaque_ok_type_is_supported` covers the case that regressed and
fails with E0562 without this change. The eight `enrich_err` snapshots
change by exactly the dropped annotation.

`wrap` no longer needs the type, so it takes only the function; the
presence check stays in `expand`, which still has to reject a function
with no return type at all.

The derive's rustdoc regains two facts a consumer cannot get elsewhere:
that the generated constructors are `pub(crate)` even on a `pub` error
type and that `#[no_constructors]` is rejected under `#[ohno::error]`,
and that an existing manual `#[derive(Debug, Error)]` now collides.
`requirements.md` R1.4 already claims the first is documented.

A second `#[display(...)]` silently replaced the first while a second
`#[from(...)]` accumulates; it is now reported, which is what the
parse/validate split exists to do.

`unused_name` uses `expect` with the reason in the message, as
AGENTS.md asks, rather than `unwrap_or_default` on an unreachable branch.

`design.md` records that a `#[display(...)]` format spec is carried
through unchecked, so a spec referring to another argument is reported
against the derive. That is the one place the guarantee is not
structural, and the document previously stated it without qualification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A `#[display(...)]` positional argument written as a bare call — `describe()` —
was rooted as unsupported, so the diagnostic promising "a field or method of
`self`" rejected the very thing it named. A call in leftmost position is now a
method root: it is prefixed with `self.` and looked up as no field.

A type whose every field is generated offered "available fields: " with nothing
after it. It now says the error type has no fields that can be referenced.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A `#[from(...)]` source type was collected token by token up to the next comma,
so a comma inside a generic argument list ended the type early and
`#[from(PairError<u32, String>)]` failed to parse. The type is now parsed
speculatively by `syn`, which knows where a type ends; the token walk remains
for the one case `syn` cannot decide, a `(member: expression)` override list
following a plain path, which reads as a parenthesized generic argument list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `#[display(...)]` format spec limit was filed under `#[enrich_err(...)]`,
which is not the macro it describes, and the paragraph explaining the wrapper's
return type had drifted away from the code block it explains. Both limits now
sit in a `Limits` section, which is what `requirements.md` and `template.rs`
already point at, and the return type paragraph is back beside its code block.

R4 and R5 now say what the code does: the two `rustc`-reported exceptions, and
that mutation testing covers `validate.rs` and `display/`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ocals

`From<T>` bound each data initializer to its own `__ohno_field_N` local, so a
generated name was in scope while the next initializer was evaluated. An
initializer naming an outer item the derive happened to shadow read the
generated local instead — and when that outer item is a `const`, the `let`
becomes a constant pattern and generated code fails to compile, which is the
outcome R4 exists to prevent. The old struct-literal expansion had no locals,
so this was a regression.

The initializers are now evaluated into one tuple, whose elements are all
evaluated before its binding exists, and the fields read tuple indices. The two
affected snapshots change by exactly that and nothing else.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment explaining the `Expr::Call` arm was duplicated above `Expr::Index`,
where it describes nothing — indexing is not a call. It was left behind when
the `Call` arm was moved into place.

A test asserting an opaque `Ok` type uses `unwrap` rather than `expect`, which
is what AGENTS.md asks for in test code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The speculative parse added a token-by-token fallback for the case `syn` was
said not to decide, a `(member: expression)` override list following a plain
path. That justification was wrong: `syn` stops at the group on its own, so the
fallback was reachable only to produce an error. CI proved it — three mutants of
its loop condition survived and the crate's line coverage fell to 99.8%, both
pointing at code no test could reach.

`source_type` now rejects an entry that opens with an override list, which is
the one thing a type cannot start with, and hands the rest to `syn`. All 143
crate tests and the `ohno` suite pass unchanged, mutation testing on the file is
clean, and coverage is back to 100%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
R4 and the Limits section both said there were exactly two inputs that reach
`rustc` instead of a macro. There is a third: `#[enrich_err(...)]` checks that a
return type is present, not what it is, so a function returning a plain value
meets the generated `map_err` at the compiler. Recorded, with why it cannot be
decided syntactically. Neither document counts any more.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`root` read every integer literal as a tuple index through `base10_digits`,
which drops the suffix. `#[display("{}", 0u8)]` therefore validated against
field `0` and then expanded to `self.0u8`, which is not a tuple field access,
so the input reached `rustc` as a syntax error in generated code instead of a
macro diagnostic. The same held for a suffixed float used for nested access.

A suffix is now required to be absent for both literal kinds, so such a root
falls through to the existing unsupported-root diagnostic. No new message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 17, 2026 12:39
@Vaiz
Evgenii (Vaiz) force-pushed the u/vaiz/20260811/ohno-macros-rewrite branch from cecf73a to 1f80640 Compare August 17, 2026 12:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 152 out of 152 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/ohno_macros/src/message.rs:54

  • Message::render rebuilds string literals with Span::call_site(), and enrich_err::parse_message/display::lower convert LitStr into String early. This drops the user-authored literal span, so any compiler diagnostics originating from the generated format! (e.g., bad captures like {missing} or invalid format specs) will point at the macro expansion/call-site instead of the attribute argument that caused the error. If the design goal is for diagnostics to be anchored in user code, preserve the original LitStr span through lowering (e.g., store the span in Message and use it when constructing LitStr in render).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants