Preserving edits to generated code
Everything under a generated crate's src/generated/** is tool-owned and rewritten on every
export. Three kinds of hand edits survive a regeneration anyway:
- comments you add on their own line — carried automatically, no markup needed;
- inserted code, wrapped in an
insertblock — added lines the generator never emits; - replaced code, wrapped in a
replaceblock — your version swapped in over a recorded copy of the generated code it overrides.
Anything else — an untagged code edit — is clobbered by the next export, exactly as before. The tags are the opt-in: explicit, reviewable in diffs, and impossible to lose silently.
The whole feature follows one contract, never silent: every preserved comment or block either
lands back in the regenerated file, or is trapped in a compile_error! block so the generated
crate fails to build until you review it. There is no third outcome. --no-preserve-comments
disables the entire overlay (comments and code blocks alike; see
command line flags).
Which mechanism for which edit
| You want to… | Use |
|---|---|
| annotate generated code | a plain own-line // comment — no markup |
| swap one generated line | a replace block (one-line recorded original) |
| swap a run of lines (e.g. most of a function body) | a replace block |
replace a whole function or impl | a replace block whose recorded original is the whole item |
| add lines the generator doesn't emit | an insert block |
| remove generated code / take over a whole concept | not a preservation feature — turn off codegen for that piece instead: @custom_serialize / @custom_deserialize (see the comment DSL) or an _CDDL_CODEGEN_EXTERN_TYPE_ extern type, and hand-write it in user-owned space |
Comments
Add a //, ///, or /* */ comment on its own line anywhere under src/generated/** and
regenerate: the comment is re-anchored onto the fresh output by symbol identity (which named item
it sits in or above) and token equality — never a textual diff, so rustfmt reflow can't break
it. A comment on an unchanged item transfers; a comment above a still-present item re-attaches
above it; a comment on a statement inside a changed body re-attaches if that exact statement
still appears exactly once on both sides.
Scope notes:
- Trailing (end-of-line) comments are not carried — one fails loudly with a hint to move it to its own line.
- Doc text on tool-documented items is tool-owned:
///flows from your CDDL and the@doccomment DSL, so edit it there. A hand edit to a tool-emitted doc block is dropped on regeneration (the one deliberate silent-drop class); a doc comment you add where the tool does not document is preserved like any other comment.
Insert blocks — adding code
Wrap the added lines in a pair of own-line tag comments:
// cddl-codegen:insert-start
self.extra_validation()?;
// cddl-codegen:insert-end
The whole block — tags, code, any comments inside it — travels as one verbatim unit, anchored by
the code that follows it, through the same tiers as a comment. The inserted code must be balanced
({}/()/[]) and is otherwise arbitrary: statements inside a function, an extra field in a
struct definition, even a whole new item between two generated ones.
Replace blocks — swapping code
A replace block has three sections: your code, the marker replaces, and a //-commented copy
of the generated code you overrode (the recorded original):
// cddl-codegen:replace-start
let len = custom_len_logic(&self.items);
// cddl-codegen:replaces
// serializer.write_array(cbor_event::Len::Len(self.items.len() as u64))?;
// cddl-codegen:replace-end
To author one: wrap the region, move the original generated lines under replaces and comment
them out (prefix each with // ), and write your replacement above the marker. Exact whitespace
in the recorded original doesn't matter — it is matched token-wise, not textually.
The recorded original is what makes this work, doing three jobs at once:
- Placement. On regeneration the recorded original is uncommented, tokenized, and located in the regenerated item; your block is spliced in over that exact spot. No positional guessing — the block finds its place even when unrelated code moved.
- Drift detection. If the generator's output for that region changed — you edited the CDDL,
or a tool upgrade emits different code — the recorded original no longer matches, and the block
is trapped in a
compile_error!showing what you overrode. A stale override can never silently keep suppressing new generated behavior; you re-review, update your code, re-record the original, and delete the error block. - Review record. Every override is visible in your VCS diff next to the code it replaced.
The same syntax scales from one line to a whole item: record a single statement, a run of
statements, an entire member function, or a whole impl — whatever the recorded original spans
(within one top-level item) is what your code replaces. Both your code and the recorded original
must be delimiter-balanced; an empty recorded original is an error (there'd be nothing to match
against).
Generated comments inside the replaced span are replaced with it; comments outside survive. A plain comment of yours whose anchor falls inside a span some replace block deletes fails loudly — move it into the block.
Imports and your block's code. The generated struct modules import the collection helper types
(BTreeMap, OrderedHashMap, NonEmptyVec, NonEmptyMap) only when the file — or one of its
descendant modules — references them (see Output format). Your inserted/replaced
code is not scanned for these — an import is kept iff the generated code names it. So if your
block references e.g. BTreeMap in a file whose generated code doesn't, name it by a fully
qualified path (std::collections::BTreeMap) rather than relying on a use that won't be there.
When preservation fails: the two failure channels
Malformed tags abort the export (a hard error naming the file, before anything is written).
This covers: an unclosed block, an orphaned insert-end/replaces/replace-end, unbalanced
delimiters, an empty or unlexable recorded original, code lines under replaces (every line
there must be //-commented), a recorded original spanning multiple top-level items — and any
unrecognized own-line comment starting with // cddl-codegen:. That last rule is what keeps the
never-silent contract airtight: a typo'd tag can never demote your block to plain text and let
half of it be clobbered. Fix the tag structure (or pass --no-preserve-comments to clobber
pristine) and re-run.
Placement failures trap the payload in the output. When the file regenerates but a comment or block can't be safely re-placed, the export succeeds and the affected payload lands at the top of the file as:
// cddl-codegen:unpreserved-comment (delete this block after review)
compile_error!("cddl-codegen could not preserve a user code block across regeneration.\n<reason>\nOriginal code block:\n<your full block, verbatim>");
The generated crate fails to build with your original text in the error message; the block carries forward verbatim across further regenerations until you delete it, so nothing is ever lost. The reasons you'll see, and what they mean:
| Reason (abbreviated) | What happened | What to do |
|---|---|---|
…which no longer exists in the regenerated code | the item holding your edit vanished (removed/renamed in the CDDL) | re-place the edit by hand where it now belongs, or discard it |
…the number of same-named items changed | same-keyed items (e.g. two impl Foo blocks) were added/removed, so "the Nth one" is ambiguous | re-place by hand |
…whose generated code changed (comment) | the statement your comment annotated changed or is no longer unique | re-place by hand against the new code |
The generated code for …changed, so its recorded original no longer appears (drift) | the generator now emits different code where your replace block applied | re-review: update your replacement for the new behavior, re-record the new original under replaces |
…recorded original is not unique within … / …appears more than once… | the recorded original matches zero-or-multiple spots ambiguously (e.g. a duplicated statement was deleted or added) | include more surrounding lines in the recorded original so it's unique, or re-place by hand |
Its anchor lies inside code replaced by a …replace-start block | a separate comment/insert block pointed into a span your replace block overrides | move that comment/block inside the replace block |
It is a trailing (end-of-line) comment | trailing comments aren't carried | move it to its own line |
Guarantees and residual limits
Running the tool twice equals running it once: re-exporting an unchanged spec reproduces the same file byte-for-byte, blocks included. The overlay never touches code tokens outside your tagged regions — its only effects are inserting your comments/blocks and removing exactly the span a recorded original identifies.
Known residual limits, by design:
- A generator-emitted
//comment whose text changes between tool versions is indistinguishable from a user comment and fails loudly once per upgrade — noisy but safe. - Preservation input must be lexable Rust: if a hand edit leaves an unterminated string/comment, the export is a hard error naming the file rather than a silent clobber.
- A file that stops being generated (a removed/renamed type or scope) is orphaned on disk with a stderr warning; edits there are not carried anywhere.