read-only-derive
Internal proc-macro support crate for read-only.
This crate implements the #[embed] attribute macro that powers the safe,
composition-based readonly pattern exported by the public read-only crate.
It is intended primarily for maintainers, contributors, and anyone trying to understand how the macro parses Rust syntax trees and rewrites them into the generated readonly view type.
Relationship to read-only
- The root
read-onlycrate is the public user-facing crate. read-only-deriveis the proc-macro implementation crate.read_only::embedis re-exported from this crate.read_only::castdoes not live here; it is re-exported fromreadonly::make.
This split follows the common two-crate Rust pattern used by crates such as
serde / serde_derive.
What #[embed] Does
At a high level, the macro takes a named-field struct and splits it into two parts:
- readonly fields marked with
#[readonly] - all remaining fields
It then generates:
- a
ReadOnly{Type}struct containing the readonly fields - a rewritten original struct that embeds
read_only: ReadOnly{Type} - a
Deref<Target = ReadOnly{Type}>implementation for the rewritten original struct
This means consumers get direct readonly access syntax through deref, while the implementation remains composition-based rather than cast-based.
Example
Input:
use embed;
Expansion shape:
Parsing and Rewrite Rules
The macro operates on a parsed syn::DeriveInput.
Accepted input
Supported:
- named-field structs
- generics
- where-clauses
- field-level
#[readonly] - field types containing
Self
Rejected:
- enums
- unions
- tuple structs
- unit structs
- empty named structs
- structs with no
#[readonly]fields - structs with a field already named
read_only
Rewrite algorithm
The implementation in src/expand.rs follows roughly this sequence:
flowchart TD
A[Parse DeriveInput] --> B{Named struct?}
B -- no --> E1[Emit compile error]
B -- yes --> C{Has at least one field?}
C -- no --> E2[Emit compile error]
C -- yes --> D[Scan fields for #[readonly]]
D --> F{Any readonly fields?}
F -- no --> E3[Emit compile error]
F -- yes --> G{Field named read_only already exists?}
G -- yes --> E4[Emit compile error]
G -- no --> H[Split fields into readonly and remaining]
H --> I[Rewrite readonly field types containing Self]
I --> J[Generate ReadOnlyType struct]
J --> K[Generate outer struct with read_only field]
K --> L[Generate Deref impl]
Field handling rules
| Field kind | Result |
|---|---|
#[readonly] pub field: T |
moved into ReadOnly* and remains public there |
#[readonly] field: T |
moved into ReadOnly* and promoted to outer struct visibility |
non-#[readonly] field |
stays on outer struct |
field type using Self |
rewritten to the concrete outer type in readonly fields |
Why Self Replacement Exists
If a readonly field type refers to Self, that Self cannot remain unchanged
inside the generated ReadOnly* struct because the generated struct is a
different type.
Example input:
If left untouched, Self inside the generated readonly struct would refer to
ReadOnlyNode, not Node, which changes semantics.
The macro therefore rewrites readonly field types from:
to:
This is implemented through a small VisitMut
visitor that walks the type syntax tree and replaces path occurrences of
Self.
Error Handling Model
The macro uses two kinds of errors:
- hard structural errors that stop expansion immediately
- collected attribute-shape errors that are appended as compile errors
Hard structural errors include:
- wrong item kind
- wrong field style
- empty struct
- missing readonly fields
- conflicting
read_onlyfield name
Collected attribute errors currently come from malformed #[readonly] usage,
for example:
The macro strips the attribute and preserves the diagnostic by storing the
parser error and appending its to_compile_error() output to the final token
stream.
Why the Code Is Written This Way
The implementation may look a little unusual compared with some proc-macro crates that aggressively factor parsing into many helpers.
That is intentional.
The current style aims to keep the most important structural validation and
rewrite steps in a single visible control flow inside embed():
- validate input shape
- strip and classify readonly fields
- split field ownership
- rewrite
Self - emit expansion
This has a few benefits:
- maintainers can read the expansion logic top-to-bottom without jumping across many files
- structural error paths stay close to the logic that needs them
- macro compile tests map more directly to the expansion steps
It also ended up being helpful for coverage work. Some of the code shape was chosen to keep validation paths explicit and testable rather than hiding them behind layered helper abstractions that would leave more unreachable or hard-to-attribute coverage artifacts in proc-macro expansion code.
That does not mean the code is coverage-driven first; it means we preferred an implementation style that is both readable and easy to verify with compile tests.
Testing Strategy
This crate is exercised through the root workspace test suite:
tests/macro/pass/*.rs: passing macro expansion casestests/macro/fail/*.rs: compile-fail macro expansion cases with checked stderr snapshotstests/miri.rs: runtime coverage of the unsafe cast-based path in the root crate (cast), included because the public crate exposes both patterns
Useful commands:
Relevant Source Files
src/lib.rs: proc-macro entrypoint for#[embed]src/expand.rs: parsing, validation, tree rewriting, and token generation../tests/macro/: compile-time macro test suite
Related Reading
Publishing Note
This crate is intended to be published before the root read-only crate,
because the public crate depends on it as a versioned proc-macro dependency.