refactor(ir): give the shared document walk one home - #249
Open
OmarAlJarrah wants to merge 2 commits into
Open
Conversation
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.
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
Three pieces of document-inspection mechanism were written more than once across
pass,ir/irverifyandcompilers/compile, because there was no package belowall three to hold them. There is one:
iris 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 inir/irverify/refs.go— withmapEntryandorderedEntriesbyte-for-byte identical and the rest the same logic spelledtwice: 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, withir.MaxWalkDepthandir.MapKeySuffixbesideit. irverify's
visit(v, path) boolsignature is the one that survived, since itsubsumes pass's
isRefpredicate and per-struct hook — both are closures overone visitor now, and neither package keeps a walker of its own.
One
isNilTypeDef. It existed three times with identical bodies. It asks aquestion about
ir.TypeDef, so it isir.IsNilTypeDef.One registry derivation.
irverify.refKindByTypeenumerated theregistry-bearing ID classes by hand and needed
TestStringTypes_AreAllClassifiedto stop it drifting;
pass.documentRegistriesderived the same set fromDocument's own shape. The derivation moves toir.DocumentRegistries, bothcheckers use it, and the hand-written table and its guard test are deleted. A
registry added to
Documentis 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)
checkNamingdiscarded its walk-truncation flag, and so didcheckIndices,checkProvenanceandcheckRawPayloads— the reference walk was the only onethat 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
Verifyfolds them into a singleir/walk-truncatedviolation — a too-deep document is one fact about thedocument, not one fact per walk that noticed it. A drift guard holds the shape
from both sides: a function that calls
ir.WalkValuesmust hand a bool back toits caller, and a function shaped like a walking check must appear in the list
Verifyruns.This is the last open item on #55. Item 1 (
forEachGroupOperationtruncatingsilently) was fixed in #136 and item 3 (
TestIR_NoFloatFieldshand-maintainingthe 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. Nothingcan derive them: an index is an
intlike any other and reflection has nothingto key on, which is the reason the two checks exist separately in the first
place.
integerFieldsinir/irverify/indices_test.goremains the guard thatfails 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.ir/walk_test.go), rather than once per copy. Each guarantee was proven by planting the defect and watching the test redden:TestWalkValues_ByteSequencesAreNotDescendedIntoTestWalkValues_SharedPointerIsDescendedIntoOnce,TestWalkValues_CyclicPointerGraphTerminatesTestWalkValues_MapEntriesAreVisitedInRenderedKeyOrderTestWalkValues_PathsSpellFieldsIndicesAndKeyskey.PkgPath() == ""dropped from the registry derivation →TestDocumentRegistries_DerivedFromDocumentShapeTestIsNilTypeDef_ScreensBothSpellingsOfNilcheckNaming's dropped flag reddensTestWalkChecks_EachReportsTruncation, and makingVerifyswallow every flag reddensTestVerify_ReportsTruncationOnce.