Skip to content

refactor(ir): give the shared document walk one home - #249

Open
OmarAlJarrah wants to merge 2 commits into
mainfrom
refactor/shared-document-walk
Open

refactor(ir): give the shared document walk one home#249
OmarAlJarrah wants to merge 2 commits into
mainfrom
refactor/shared-document-walk

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

Three pieces of document-inspection mechanism were written more than once across
pass, ir/irverify and compilers/compile, because there was no package below
all three to hold them. There is one: ir is Layer 0, imports only the stdlib,
and already owns traversal.

One walk. The bounded, cycle-guarded reflection walk existed twice — 141
lines in pass/refs.go, 128 in ir/irverify/refs.go — with mapEntry and
orderedEntries byte-for-byte identical and the rest the same logic spelled
twice: the pointer/struct/sequence/map arms, the embedded-field path rule, the
depth cap and its rationale, and the byte-slice skip. Both comments already
cross-referenced the other to say the two had to agree.

It is now ir.WalkValues, with ir.MaxWalkDepth and ir.MapKeySuffix beside
it. irverify's visit(v, path) bool signature is the one that survived, since it
subsumes pass's isRef predicate and per-struct hook — both are closures over
one visitor now, and neither package keeps a walker of its own.

One isNilTypeDef. It existed three times with identical bodies. It asks a
question about ir.TypeDef, so it is ir.IsNilTypeDef.

One registry derivation. irverify.refKindByType enumerated the
registry-bearing ID classes by hand and needed TestStringTypes_AreAllClassified
to stop it drifting; pass.documentRegistries derived the same set from
Document's own shape. The derivation moves to ir.DocumentRegistries, both
checkers use it, and the hand-written table and its guard test are deleted. A
registry added to Document is now checked by both the moment it exists.

Behaviour is unchanged: same codes, same messages, same paths, same ordering. No
golden or conformance fixture moved.

Truncated walks are no longer silent (#55)

checkNaming discarded its walk-truncation flag, and so did checkIndices,
checkProvenance and checkRawPayloads — the reference walk was the only one
that reported anything. That was benign by coincidence rather than by design: a
pruning walk reaches a subset of what the unpruned reference walk reaches, so the
reference walk trips the cap first. The coincidence holds only until the
reference walk gains pruning of its own, and a walk over a document nested past
the cap would then check a prefix of the names and say nothing about the rest.

Every walking check now returns the flag, and Verify folds them into a single
ir/walk-truncated violation — a too-deep document is one fact about the
document, not one fact per walk that noticed it. A drift guard holds the shape
from both sides: a function that calls ir.WalkValues must hand a bool back to
its caller, and a function shaped like a walking check must appear in the list
Verify runs.

This is the last open item on #55. Item 1 (forEachGroupOperation truncating
silently) was fixed in #136 and item 3 (TestIR_NoFloatFields hand-maintaining
the kind list) in #149, so the issue closes here.

Deliberately out of scope

The integer-index carriers — Service.Servers, Channel.Servers,
HTTPBinding.SuccessStatus — stay enumerated by hand in both checkers. Nothing
can derive them: an index is an int like any other and reflection has nothing
to key on, which is the reason the two checks exist separately in the first
place. integerFields in ir/irverify/indices_test.go remains the guard that
fails when the IR grows an integer field nobody has classified, and the code now
says so where a reader will reach it.

Test plan

  • gofmt -l, go vet ./..., golangci-lint run (0 issues), ./scripts/check-coverage.sh — 100% of 4466 statements.
  • The walk's guarantees are now tested where the walk lives (ir/walk_test.go), rather than once per copy. Each guarantee was proven by planting the defect and watching the test redden:
    • byte-sequence skip removed → TestWalkValues_ByteSequencesAreNotDescendedInto
    • cycle guard removed → TestWalkValues_SharedPointerIsDescendedIntoOnce, TestWalkValues_CyclicPointerGraphTerminates
    • map entries left in Go's iteration order → TestWalkValues_MapEntriesAreVisitedInRenderedKeyOrder
    • embedded-field elision removed → TestWalkValues_PathsSpellFieldsIndicesAndKeys
    • key.PkgPath() == "" dropped from the registry derivation → TestDocumentRegistries_DerivedFromDocumentShape
    • typed nil unscreened → TestIsNilTypeDef_ScreensBothSpellingsOfNil
  • The truncation fix likewise: restoring checkNaming's dropped flag reddens TestWalkChecks_EachReportsTruncation, and making Verify swallow every flag reddens TestVerify_ReportsTruncationOnce.
  • The drift guard was checked against both additions it advertises catching: a planted function that walks without returning a bool, and a planted check-shaped function absent from the list — each fails with the message naming it.

The bounded, cycle-guarded reflection walk over an ir.Document was
written twice — once in pass/refs.go, once in ir/irverify/refs.go —
with two of its helpers byte-for-byte identical and the rest the same
logic spelled twice: the pointer/struct/sequence/map arms, the
embedded-field path rule, the depth cap and its rationale, and the
byte-slice skip. Both comments already cross-referenced the other to
say the two must agree, which is agreement maintained by hand.

ir is the package below both — Layer 0, stdlib-only, and already the
owner of traversal — so the walk lives there now as ir.WalkValues,
with its bound as ir.MaxWalkDepth and its map-key marker as
ir.MapKeySuffix. irverify's visit(v, path) bool signature is the one
that survived: pass's isRef predicate and per-struct hook are closures
over it, so neither package keeps a walker of its own.

Two smaller duplications go with it:

  - isNilTypeDef existed three times (pass, ir/irverify,
    compilers/compile) with identical bodies. It asks a question about
    ir.TypeDef, so it is now ir.IsNilTypeDef.

  - irverify.refKindByType enumerated the registry-bearing ID classes
    by hand and needed TestStringTypes_AreAllClassified to stop it
    drifting, while pass derived the same set from Document's own
    shape. That derivation moves to ir.DocumentRegistries and both
    checkers use it; the hand-written table and its guard test are
    deleted, and a registry added to Document is checked by both the
    moment it exists.

The integer-index carriers stay written by hand in both checkers,
because nothing can derive them: an index is an int like any other and
reflection has nothing to key on. irverify's integerFields guard,
which fails when the IR grows an integer field nobody has classified,
is what keeps that list honest.

Codes, messages, paths and ordering are unchanged; no golden or
conformance fixture moved.

This also fixes the walking checks that discarded their truncation
flag. checkNaming dropped it outright, and checkIndices,
checkProvenance and checkRawPayloads dropped it too — only the
reference walk reported one. That was benign by coincidence: a pruning
walk reaches a subset of what the unpruned reference walk does, so the
reference walk trips the cap first, and the coincidence holds only
until it gains pruning of its own. Every walking check now returns the
flag and Verify folds them into a single ir/walk-truncated violation,
a too-deep document being one fact about the document rather than one
per walk that noticed it. A drift guard holds the shape from both
sides: a function that calls ir.WalkValues must hand a bool back, and
one shaped like a walking check must be in the list Verify runs.

Closes #247
Closes #55
Review follow-up on the walk consolidation.

The root label "doc" was the one piece of the path grammar still
written by hand, in seven places across the two checkers. It is
load-bearing for the property the shared walk exists to give — a
caller running both checkers can only dedupe one defect if both spell
it as one location — so it is now ir.DocumentPath, used at every
document-rooted walk and by the truncation reports that name the
document itself.

Nothing else in the walk was left hand-maintained; a sweep for
reflect.ValueOf outside ir confirms the two checkers keep no traversal
of their own.

Also from the same pass, none of it behavioural:

  - WalkValues now states that visit is required and what path is for.
  - Registry.Label says how it is derived (the declaring Document
    field's name, lowercased), and DocumentRegistries says what a nil
    document yields and why that is the answer a report-only caller
    wants.
  - TestDocumentRegistries_CoverAWholeRegistry named no claim; it is
    TestRegistry_HasResolvesOnlyDeclaredIDs.
  - A loop of NotEqual asserting one absent path became NotContains.
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.

1 participant