Skip to main content

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 insert block — added lines the generator never emits;
  • replaced code, wrapped in a replace block — 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 codea plain own-line // comment — no markup
swap one generated linea 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 impla replace block whose recorded original is the whole item
add lines the generator doesn't emitan insert block
remove generated code / take over a whole conceptnot 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 @doc comment 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 happenedWhat to do
…which no longer exists in the regenerated codethe 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 changedsame-keyed items (e.g. two impl Foo blocks) were added/removed, so "the Nth one" is ambiguousre-place by hand
…whose generated code changed (comment)the statement your comment annotated changed or is no longer uniquere-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 appliedre-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 blocka separate comment/insert block pointed into a span your replace block overridesmove that comment/block inside the replace block
It is a trailing (end-of-line) commenttrailing comments aren't carriedmove 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.