Repository navigation
[2.x] Detect cyclic defined types - #1211
Conversation
|
trevor-cortex
left a comment
There was a problem hiding this comment.
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, skippingenumValueNode.enum) andgetHasFiniteValueVisitor(inhabitedness check that treats options / non-fixed collections / alternative enum variants as recursion exits).isDefinedTypeCyclic(reachability search over the links visitor) andhasDefinedTypeFiniteValue, both guarded by a newCODAMA_ERROR__NODE_PATH_PROGRAM_MISSINGwhen the path can't resolve links.getByteSizeVisitor/getMaxByteSizeVisitornow key their memo and in-progress set byDefinedTypeNodeidentity instead ofidentifier, so a link to a same-named type in another program is no longer mistaken for a cycle.getInstructionDataFieldsscopes its "followed" set to the current branch so sibling fields linking the same type all expand.unwrapDefinedTypesVisitorkeeps 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.mdasks 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/*.mdin 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 —visitEnumTypereturnsfalsefor 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
getHasFiniteValueVisitorhas no memoization, unlike the byte-size visitors. Correctness-wise that's fine (afalseis 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.unwrapDefinedTypesVisitorwith an explicit type list is more conservative than strictly necessary (see inline comment onisCyclic). 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 fromvisitors-core.
70156ea to
8ae8301
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
Re-review
Re-fetched everything. All five points from the first pass are addressed, and the new code checks out:
getHasFiniteValueVisitornow cachestrueresults only, with a comment explaining whyfalsecan'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 naivefalsecache, so it's good to have it pinned.- Empty-enum nuance is now in the docs (
hasDefinedTypeFiniteValue, README) and covered by a test. assertNodePathHasProgrammoved toNodePath.tswith its own tests;pendingreplacesqueue.unwrapDefinedTypesVisitornow computes cycles over types-to-inline only (isInInlinedCycle), so['a']ona ↔ binlinesaand stops atlink(b), as the new test shows. I traced the tricky interactions: the search uses its ownlinksStack, so it doesn't disturb the main stack; the cache is keyed by originalDefinedTypeNodeidentity, which is what bothlinkablesand the filteredprogram.definedTypescarry; and a kept link to a cyclic type inside a type inlined across programs still gets qualified, because the earlyreturn linkhappens insidevisit(definedType.type, self)andinlineDefinedTypequalifies 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.
8ae8301 to
89c5b85
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
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.
89c5b85 to
76c0dff
Compare
297dd25 to
aa957c7
Compare
76c0dff to
9d286f5
Compare
9d286f5 to
ae9c46e
Compare
aa957c7 to
a98f6b8
Compare
ae9c46e to
c50d356
Compare
a98f6b8 to
8dff65d
Compare
c50d356 to
a9dfadc
Compare
3ed69d8 to
c89f907
Compare
46a0398 to
f9b2018
Compare
c89f907 to
d41ee16
Compare
f9b2018 to
bdccdf4
Compare

This PR adds helpers to detect cyclic defined types and fixes the existing cycle guards that got them wrong.
getDefinedTypeLinksVisitorlists the defined types a node links to, andgetHasFiniteValueVisitortells whether a type has a finite value.isDefinedTypeCyclicandhasDefinedTypeFiniteValueare built on those visitors and leaveLinkableDictionaryunchanged. Both throw the newCODAMA_ERROR__NODE_PATH_PROGRAM_MISSING(16), via the newassertNodePathHasProgram, when the path has no program to resolve links from.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.