fix(archtest): make the recursion pin see methods - #331
Merged
Conversation
Clean merge — this branch touches only internal/archtest/recursion_test.go and nothing on main moved it. Worth checking anyway, because the pin reads the whole tree: a recursive method added to a lowering package since this branch opened would have to join the pinned set. None was. TestLoweringRecursion_IsOnlyTheKnownCycles passes against the merged tree with the same five sets, so the two the widening admits are still the only ones it admits.
OmarAlJarrah
added a commit
that referenced
this pull request
Aug 9, 2026
Clean merge, and a tree that compiled nowhere: #328 consolidated the openapi test scaffolding into internal/openapitest, and this branch's new tests call six of those helpers by their old package-local names. Git reconciles both edits without a conflict, so the failure only shows on a build. Repointed the eight call sites — componentSpec, requireNoErrorDiags, propsByWire, emptyEitherSchema — across conformance_test.go, compose_test.go, schema_test.go and schema_internal_test.go. Signatures are unchanged, so these are renames. The archtest pin composed on its own: #331 widened loweringRecursions to see methods and this branch adds the four-function nullability cycle, and the merged order already satisfies #331's length-then-first-member comparator. TestLoweringRecursion_IsOnlyTheKnownCycles passes on the result, which is what checks that rather than the eye.
OmarAlJarrah
added a commit
that referenced
this pull request
Aug 9, 2026
Clean merge, and a tree that compiled nowhere: #328 consolidated the openapi test scaffolding into internal/openapitest, and this branch's new tests call six of those helpers by their old package-local names. Git reconciles both edits without a conflict, so the failure only shows on a build. Resolved by repointing the eight call sites — componentSpec, requireNoErrorDiags, propsByWire, emptyEitherSchema — across conformance_test.go, compose_test.go, schema_test.go and schema_internal_test.go. The signatures are unchanged, so these are renames. The archtest pin composed on its own: #331 widened loweringRecursions to see methods and this branch adds the four-function nullability cycle, and the merged order already satisfies #331's length-then-first-member comparator. TestLoweringRecursion_IsOnlyTheKnownCycles passing on the result is what checks that, rather than the eye.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
loweringCallGraphininternal/archtest/recursion_test.gobuilt its graph from top-level functions with no receiver, so every method in the four lowering packages was absent from it.anchorWalk.walkandanchorWalk.walkMappingincompilers/openapi/internal/schema/schema.goare mutually recursive — the mapping walk descends into values throughwalk, andwalkdispatches mappings towalkMapping— and the pin said nothing about them. Planting the same cycle as free functions reddened the test; planting it as methods did not.That matters because this pin is what enforces the repo's rule that recursion is permitted only against an explicit depth counter. A blind spot the size of "methods" means no recursive method anywhere in the lowering is held to it.
Three changes, all in the one test file:
<receiver type>.<method>. The graph refuses a name collision rather than resolving it, andwalkexists both as a method onanchorWalkand as a name a free function may take, so the two have to stay distinguishable. Pointer, value, and generic receivers all resolve to the bare type name.w.walkMappingis how one method names another, and the receiver's type is the one thing the parser can resolve without a type checker.The comparator that orders the pinned sets also gained a tie-break on first member. There is more than one pair in the answer now and
sort.Sliceis not stable, so length alone would have left the two pairs in whichever order the sort happened to land them.What the widened pin catches that the old one did not
Run against the whole tree, it adds exactly two entries, and both are already bounded — the codebase complies with the rule the pin enforces, and no exemption was needed anywhere:
{anchorWalk.walk, anchorWalk.walkMapping}— bounded bycharge, which refuses pastmaxDynamicAnchorDepthor a spent node budget and records the refusal so the caller learns the index is partial.{bodyParts}— bounded bymaxPartCompositionDepth.The two sets pinned before are unchanged, which is the property #210 was accepted on: a real answer can still be told from a re-baselined one.
What the pin still cannot see
Stated so the boundary is honest rather than implied. A selector resolves only when its base is the declaring method's own receiver. A free function calling a method on a local (
dynamicAnchorscallsw.walk;LowerServicecallsgroups.group), a method calling one on a field (w.view.MappingPairs), and any call through an interface are all still invisible. None of them closes a cycle today, so this is an unheld invariant rather than a wrong answer, and resolving them needsgo/types— matching on a bare method name instead is exactly the resolution the one-namespace rule refuses. Filed as #323.Test plan
TestCalleesOf_ResolvesAMethodThroughItsReceiver— planted sources for both shapes (a sibling call and a self call), pointer/value/generic receivers, a method value handed on, and the three negatives that define the boundary.TestDeclOf_NamesAMethodByItsReceiverType— the key a method gets, over every receiver form Go allows.TestReceiverTypeName_RefusesWhatIsNotATypeName—go/parseraccepts a receivergo/typeswould reject, so the guarddeclOfleans on is reachable from a source file.TestLoweringCallGraph_RecordsAMethodEdge— the threeanchorWalkedges pluscharge, asserted on the tree rather than a planted source.TestLoweringRecursion_IsOnlyTheKnownCycles— re-pinned, with the reason each new set is allowed to recurse.Every part of the change was proved to bite by planting its opposite and watching the suite go red:
fn.Recv != nil→continue)TestLoweringRecursion_IsOnlyTheKnownCycles,TestLoweringCallGraph_RecordsAMethodEdgeTestCalleesOf_ResolvesAMethodThroughItsReceiverTestCalleesOf_SeparatesAValueFromWhatIsNotOnereceiverTypeNameinvents a name instead of refusingTestReceiverTypeName_RefusesWhatIsNotATypeNameTestDeclOf_...,TestCalleesOf_ResolvesAMethodThroughItsReceiverAnd against the tree, the issue's own acceptance criterion: a mutually recursive method pair, a directly self-recursive method, and a directly self-recursive free function appended to
compilers/openapi/internal/operation/operations.goeach reddenTestLoweringRecursion_IsOnlyTheKnownCycles. All three left it green before this change. The control is a different shape — a mutually recursive free-function pair, the one case the old pin did read — and it reddens on both sides, which is what says this widens the pin rather than repairing something wholly broken. A self-recursive free function is not that control: self edges were missing for free functions too, so it was invisible before and is caught now, like the two method shapes.Full gate passes.
Closes #224