Skip to content

[2.x] Support recursive defined types in dynamic codecs - #1212

Merged
lorisleiva merged 1 commit into
mainfrom
10-06-support_recursive_defined_types_in_dynamic_codecs
Oct 9, 2026
Merged

lorisleiva merged 1 commit into
mainfrom
10-06-support_recursive_defined_types_in_dynamic_codecs

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

This PR lets dynamic codecs handle defined types that link back to themselves, such as linked lists and trees. Creating their codecs used to overflow the call stack.

// list = struct { value: u8, next: option<link(list)> }
const codec = getNodeValueCodec([root, program, list]);
codec.encode({ next: { __option: 'Some', value: { next: null, value: 2 } }, value: 1 }); // 0x01010200
  • Lazy links back: a link back to a type whose codec is still being created gets a lazy codec, which defers to the finished codec when first used. Every other node gets the same codec as before.
  • Types with no finite value: creating the codec of a type whose every value would nest another one, e.g. loop = struct { next: link(loop) }, throws the new CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE (17) with { name, path }.
  • Fixed-size items: a link back to its own type is variable-size, so it cannot be the item of a fixed option or a zeroable option.
  • Other packages: dynamic-parsers, dynamic-address-resolution and dynamic-instructions create their codecs through this visitor, so they handle recursive types too. A parser test covers a recursive account.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 557ab3d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Adds recursive defined type support to the dynamic codec visitor. visitDefinedType registers the type in a definedTypesInProgress map (keyed by node identity) for the duration of its creation; visitDefinedTypeLink now resolves the linked path itself and, when the target is in progress, returns a getLazyCodec that defers to the finished codec on first use. Before handing out a lazy codec, hasDefinedTypeFiniteValue (from @codama/visitors-core) rejects types with no finite value with the new CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE (17). Link transforms still apply on top of the lazy codec via the existing intercept layer, so size prefixes etc. work at every depth.

I walked through the key paths and they hold up:

  • Identity-keyed lookup via the resolved path's last node correctly distinguishes same-named types across programs (covered by test).
  • Mutual recursion through intermediate types (person ↔ friends) works because only the type whose codec is actually in progress gets the lazy treatment; intermediates are created normally.
  • The finite check is anchored on the back-linked type, so usesLoop → loop correctly reports loop, and a type like a = struct { x: link(b) }, b = struct { z: link(a) } is correctly rejected as a.
  • inProgress is a per-call object captured by the lazy closure, so it stays valid after the map entry is deleted in finally, and a throw mid-creation cleans up properly for reused visitors (e.g. in dynamic-parsers).
  • Verified the expected hex in all four happy-path tests by hand.

Things to watch

  • Changeset: @codama/dynamic-codecs and @codama/errors are both public and this is a user-facing feature + new error code, but there's no .changeset/*.md in the diff. If a lower PR in the stack already covers this concern, ignore; otherwise one is needed before merge per CONTRIBUTING.
  • Finite check semantics: hasDefinedTypeFiniteValue also returns false for types containing a variant-less enum, so e.g. a = struct { next: option<link(a)>, e: enum {} } throws DEFINED_TYPE_HAS_NO_FINITE_VALUE with a message that blames nesting. The type genuinely has no values so the error code is defensible, just noting the message is slightly off in that corner — and that the non-recursive struct { e: enum {} } doesn't throw at all. Not blocking.
  • Minor nits inline: optional caching of the finite check, a small readability tweak in visitDefinedType, and the untested invariant-violation branch.

For subsequent reviewers

hasDefinedTypeFiniteValue / getHasFiniteValueVisitor are not in this diff — they land in a lower PR of the stack. Their rules (options/enums/non-fixed collections stop recursion; transforms don't) are what make the "lazy codecs never recurse at creation time" guarantee hold, so review those together with this one.

Comment thread packages/dynamic-codecs/src/codecs.ts Outdated
Comment thread packages/dynamic-codecs/src/codecs.ts Outdated
Comment thread packages/dynamic-codecs/src/codecs.ts
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch from 8ae8301 to 89c5b85 Compare October 6, 2026 19:53
@lorisleiva
lorisleiva force-pushed the 10-06-support_recursive_defined_types_in_dynamic_codecs branch from 3b7d3df to 8618f69 Compare October 6, 2026 19:53

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review

Re-fetched the diff. All three points from my previous pass are addressed:

  • Finite check caching: finiteDefinedTypes set on the visitor, keyed by node identity, populated only on success — so a failing type still throws on every attempt, which is the right behaviour. The check and the lazy codec creation are now neatly bundled in getRecursiveCodec.
  • visitDefinedType readability: inProgress is now explicitly typed and the codec is returned from a local rather than through narrowing across the try/finally.
  • Invariant-violation test: the new sentinelTransformNode(constantValueNode(definedTypeLinkNode('list'), …)) case covers the branch and locks in the message. The option wrapper keeps the type finite so the check passes, and the sentinel's getConstantBytes then hits the lazy codec before inProgress.codec is set — exactly the path the comment describes.

Logic in visitDefinedTypeLink / getRecursiveCodec is otherwise unchanged from what I verified last time. Still LGTM.

Still outstanding

  • Changeset: still no .changeset/*.md in this PR. @codama/dynamic-codecs is public and gains a user-facing feature; @codama/errors gains a new code (and is in the fixed group, so it'll bump with the core packages regardless). If the lower detect_cyclic_defined_types PR already adds one that mentions dynamic-codecs, fine — otherwise it needs one before merge.

@lorisleiva
lorisleiva force-pushed the 10-06-support_recursive_defined_types_in_dynamic_codecs branch from a9f4701 to ea6f5df Compare October 9, 2026 09:13
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch 2 times, most recently from c50d356 to a9dfadc Compare October 9, 2026 09:14
@lorisleiva
lorisleiva force-pushed the 10-06-support_recursive_defined_types_in_dynamic_codecs branch 2 times, most recently from 6510101 to f925e67 Compare October 9, 2026 09:15
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch 2 times, most recently from 46a0398 to f9b2018 Compare October 9, 2026 09:16
@lorisleiva
lorisleiva force-pushed the 10-06-support_recursive_defined_types_in_dynamic_codecs branch from f925e67 to 9e3d79c Compare October 9, 2026 09:16
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch from f9b2018 to bdccdf4 Compare October 9, 2026 09:17
@lorisleiva
lorisleiva force-pushed the 10-06-support_recursive_defined_types_in_dynamic_codecs branch from 9e3d79c to 228ce02 Compare October 9, 2026 09:17
Base automatically changed from 10-06-detect_cyclic_defined_types to main October 9, 2026 09:17
@lorisleiva
lorisleiva force-pushed the 10-06-support_recursive_defined_types_in_dynamic_codecs branch from 228ce02 to 557ab3d Compare October 9, 2026 09:17
@lorisleiva
lorisleiva marked this pull request as ready for review October 9, 2026 09:18
@lorisleiva
lorisleiva merged commit 30ce4a8 into main Oct 9, 2026
0 of 2 checks passed
@lorisleiva
lorisleiva deleted the 10-06-support_recursive_defined_types_in_dynamic_codecs branch October 9, 2026 09:18
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.

2 participants