Skip to content

fix(archtest): make the recursion pin see methods - #331

Merged
OmarAlJarrah merged 2 commits into
mainfrom
fix/archtest-recursion-pin-methods
Aug 9, 2026
Merged

fix(archtest): make the recursion pin see methods#331
OmarAlJarrah merged 2 commits into
mainfrom
fix/archtest-recursion-pin-methods

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

loweringCallGraph in internal/archtest/recursion_test.go built its graph from top-level functions with no receiver, so every method in the four lowering packages was absent from it. anchorWalk.walk and anchorWalk.walkMapping in compilers/openapi/internal/schema/schema.go are mutually recursive — the mapping walk descends into values through walk, and walk dispatches mappings to walkMapping — 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:

  • Methods are nodes, keyed <receiver type>.<method>. The graph refuses a name collision rather than resolving it, and walk exists both as a method on anchorWalk and 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.
  • A selector on the declaration's own receiver is an edge. w.walkMapping is how one method names another, and the receiver's type is the one thing the parser can resolve without a type checker.
  • A self call is an edge, and the pin reads a self-loop as a set of one. Without this a directly recursive method would still come out dependency-free, which is a different edge from the sibling call and not covered by fixing it.

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.Slice is 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 by charge, which refuses past maxDynamicAnchorDepth or a spent node budget and records the refusal so the caller learns the index is partial.
  • {bodyParts} — bounded by maxPartCompositionDepth.

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 (dynamicAnchors calls w.walk; LowerService calls groups.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 needs go/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_RefusesWhatIsNotATypeNamego/parser accepts a receiver go/types would reject, so the guard declOf leans on is reachable from a source file.
  • TestLoweringCallGraph_RecordsAMethodEdge — the three anchorWalk edges plus charge, 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:

planted defect reddens
methods skipped again (fn.Recv != nilcontinue) TestLoweringRecursion_IsOnlyTheKnownCycles, TestLoweringCallGraph_RecordsAMethodEdge
receiver selectors not read as edges the two above, plus TestCalleesOf_ResolvesAMethodThroughItsReceiver
self edges excluded again the three above, plus TestCalleesOf_SeparatesAValueFromWhatIsNotOne
receiverTypeName invents a name instead of refusing TestReceiverTypeName_RefusesWhatIsNotATypeName
a one-parameter generic receiver left unwrapped TestDeclOf_..., TestCalleesOf_ResolvesAMethodThroughItsReceiver

And 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.go each redden TestLoweringRecursion_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

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
OmarAlJarrah merged commit 0d01d5d into main Aug 9, 2026
1 check passed
@OmarAlJarrah
OmarAlJarrah deleted the fix/archtest-recursion-pin-methods branch August 9, 2026 11:09
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.
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.

archtest: the recursion pin reads free functions only

1 participant