Skip to content

[2.x] Detect cyclic defined types - #1211

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

lorisleiva merged 1 commit into
mainfrom
10-06-detect_cyclic_defined_types

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

This PR adds helpers to detect cyclic defined types and fixes the existing cycle guards that got them wrong.

// list = struct { next: option<link(list)> }, loop = struct { next: link(loop) }
isDefinedTypeCyclic([root, program, list], linkables);       // true
hasDefinedTypeFiniteValue([root, program, list], linkables); // true: None stops the recursion
hasDefinedTypeFiniteValue([root, program, loop], linkables); // false
  • New visitors: getDefinedTypeLinksVisitor lists the defined types a node links to, and getHasFiniteValueVisitor tells whether a type has a finite value.
  • New helpers: isDefinedTypeCyclic and hasDefinedTypeFiniteValue are built on those visitors and leave LinkableDictionary unchanged. Both throw the new CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING (16), via the new assertNodePathHasProgram, when the path has no program to resolve links from.
  • Byte-size visitors: they track the types being visited by node rather than by name, so a link to a same-named type of another program no longer counts as a cycle.
  • getInstructionDataFields: sibling fields linking the same type are now all expanded. Previously only the first one was.
  • unwrapDefinedTypesVisitor: keeps defined types in a cycle made only of types to inline, and the links to them. Previously it recursed forever. A cycle going through a type that is not inlined ends at its link, so its other types are still inlined.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: bdccdf4

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 first-class cycle detection for defined types and fixes the places that got it wrong:

  • getDefinedTypeLinksVisitor (lists resolved link targets one hop out, skipping enumValueNode.enum) and getHasFiniteValueVisitor (inhabitedness check that treats options / non-fixed collections / alternative enum variants as recursion exits).
  • isDefinedTypeCyclic (reachability search over the links visitor) and hasDefinedTypeFiniteValue, both guarded by a new CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING when the path can't resolve links.
  • getByteSizeVisitor / getMaxByteSizeVisitor now key their memo and in-progress set by DefinedTypeNode identity instead of identifier, so a link to a same-named type in another program is no longer mistaken for a cycle.
  • getInstructionDataFields scopes its "followed" set to the current branch so sibling fields linking the same type all expand.
  • unwrapDefinedTypesVisitor keeps cyclic types (and links to them) instead of recursing forever.

I traced the visitor plumbing (extendVisitor → interceptVisitor this propagation, NodeStack.visitPath + recordNodeStackVisitor pushing the last node back) and the self/stack usage in the new visitors is correct. The byte-size memoization-under-cycle reasoning still holds with node keys: anything memoized as null because it hit an in-progress type is itself on that cycle, so the cached value is genuinely right in every context. Tests are thorough, including the cross-program same-name cases and the "recorded later" cases that confirm LinkableDictionary isn't mutated.

Things to check before merging

  • Changeset. CONTRIBUTING.md asks for one on any user-facing change, and this PR adds public API (four exports + an error code) and changes the behavior of three existing visitors/helpers. I don't see a .changeset/*.md in the file list. If the 2.x line handles this differently (e.g. everything rolls into a seeded major changeset), ignore this.
  • Doc nuance in hasDefinedTypeFiniteValue: "Only cyclic types can lack one" isn't quite true — visitEnumType returns false for an enum with zero variants (correctly: it's uninhabited). Tiny wording thing, but worth a tweak if you want the doc to be exact.

Notes for other reviewers / non-blocking

  • getHasFiniteValueVisitor has no memoization, unlike the byte-size visitors. Correctness-wise that's fine (a false is context-dependent on the in-progress set, so it can't be cached naively), but it means repeated shared sub-types get re-walked on every branch — exponential in the pathological "every level has two fields linking the next level" case. Not a problem today since nothing hot calls it; I left an inline note on the safe memoization option if it becomes one.
  • unwrapDefinedTypesVisitor with an explicit type list is more conservative than strictly necessary (see inline comment on isCyclic). Behaviour for '*' is exactly right; the explicit-list case is a judgment call and the new test enshrines the conservative choice, so just flagging it as deliberate-or-not.
  • Nothing else of substance. Error code 16 sits in the general range alongside LINKED_NODE_NOT_FOUND (4), which is the existing precedent for errors thrown from visitors-core.

Comment thread packages/visitors-core/src/getHasFiniteValueVisitor.ts
Comment thread packages/visitors-core/src/getHasFiniteValueVisitor.ts
Comment thread packages/visitors-core/src/isDefinedTypeCyclic.ts Outdated
Comment thread packages/visitors-core/src/isDefinedTypeCyclic.ts Outdated
Comment thread packages/visitors/src/unwrapDefinedTypesVisitor.ts Outdated
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch from 70156ea to 8ae8301 Compare October 6, 2026 15:51

@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 everything. All five points from the first pass are addressed, and the new code checks out:

  • getHasFiniteValueVisitor now caches true results only, with a comment explaining why false can't be cached. The new "first reached within a cycle it does not need" test (forest → tree → branch → tree) is exactly the case that would break a naive false cache, so it's good to have it pinned.
  • Empty-enum nuance is now in the docs (hasDefinedTypeFiniteValue, README) and covered by a test.
  • assertNodePathHasProgram moved to NodePath.ts with its own tests; pending replaces queue.
  • unwrapDefinedTypesVisitor now computes cycles over types-to-inline only (isInInlinedCycle), so ['a'] on a ↔ b inlines a and stops at link(b), as the new test shows. I traced the tricky interactions: the search uses its own linksStack, so it doesn't disturb the main stack; the cache is keyed by original DefinedTypeNode identity, which is what both linkables and the filtered program.definedTypes carry; and a kept link to a cyclic type inside a type inlined across programs still gets qualified, because the early return link happens inside visit(definedType.type, self) and inlineDefinedType qualifies the result afterwards.

Nothing blocking. Two small non-blocking notes below (one inline). Still no .changeset/*.md in the diff — given this targets a stacked 10-05-… 2.x branch I'm assuming it rolls into the 2.x release changeset; ignore if so.

Comment thread packages/visitors/src/unwrapDefinedTypesVisitor.ts

@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 (3)

Re-fetched all diffs. The only change since my last pass is a two-line addition to the getHasFiniteValueVisitor doc comment, explaining why a link back to a type being visited yields false on its own and calling out the empty-enum case. That reads well and matches the behaviour and tests.

Everything else is byte-for-byte what I approved last time, so nothing new to add — the open non-blocking notes (optional shouldFollow on isDefinedTypeCyclic to dedupe isInInlinedCycle, changeset if the 2.x stack needs one per PR) still stand as take-it-or-leave-it. Good to merge from my side.

@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch from 9d286f5 to ae9c46e Compare October 9, 2026 09:12
@lorisleiva
lorisleiva force-pushed the 10-05-port_the_dynamic_client_to_codama_v2 branch from aa957c7 to a98f6b8 Compare October 9, 2026 09:12
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch from ae9c46e to c50d356 Compare October 9, 2026 09:13
@lorisleiva
lorisleiva force-pushed the 10-05-port_the_dynamic_client_to_codama_v2 branch from a98f6b8 to 8dff65d Compare October 9, 2026 09:13
@lorisleiva
lorisleiva force-pushed the 10-06-detect_cyclic_defined_types branch from c50d356 to a9dfadc Compare October 9, 2026 09:14
@lorisleiva
lorisleiva force-pushed the 10-05-port_the_dynamic_client_to_codama_v2 branch 2 times, most recently from 3ed69d8 to c89f907 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-05-port_the_dynamic_client_to_codama_v2 branch from c89f907 to d41ee16 Compare October 9, 2026 09:16
Base automatically changed from 10-05-port_the_dynamic_client_to_codama_v2 to main 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 marked this pull request as ready for review October 9, 2026 09:17
@lorisleiva
lorisleiva merged commit 8b44950 into main Oct 9, 2026
0 of 2 checks passed
@lorisleiva
lorisleiva deleted the 10-06-detect_cyclic_defined_types branch October 9, 2026 09:17
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