Read structure off the compiler's walk, not a reflective scan - #97
Merged
Conversation
Delete UnboundSymbols and DefinitionGraph: cycle detection moves into TypeEnvironment's own descent, Expression::parameters() derives from diagnose()'s recorded references, and the persisted-tree guard goes with the scan it kept honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CoversClass scopes each test's recorded coverage, so the Diagnosis-level assertions were never credited to TypeEnvironment's cycle branch and infection ran only the direct TypeEnvironmentTest tests against its mutants. Add close-range kills: the cycle-branch read observed through a recovery-enabled compiler, chain trimming and pop precision, parameters() dropping a definition name, and parameters() being dialect-relative. The nullsafe-strictness mutant on the cycle branch is equivalent ($reads is provably non-null there) and is ignored in infection.json5. Also document the other observable parameters() shifts in the changelog: dialect relativity, first-read order, and the diagnose() cost — Program::$references is the cheap answer for compiled expressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
UnboundSymbolswalked the source tree by reflecting over public properties to answer structural questions — which symbols are free, which definitions reference which — while the compiler answered the same questions a second time by actually compiling. Two descriptions of one structure, and the reflective one was the guessed one: a child in a private property or a composite type missing from its hardcoded allowlist (MatchPattern,MatchArm) silently disappeared from parameter discovery and cycle detection. This PR deletes the shadow copy. Every structural fact now comes from the compiler's own descent, which each source compiler already states by compiling its children — the machinery #90 (recorded reads), #91 (the walk survives faults), and #92 (reads as the runtime contract) built.Three things fall out of
UnboundSymbols, one per call site:Cycle detection moves into the descent.
TypeEnvironmentalready carried an in-progress chain as a recursion backstop; it is now the sole detector, andDefinitionGraph's upfront graph pass (withErrorRecovery's poisoning machinery) is deleted. The same broken expression, both channels:Before,
compile()refused with the unlocated wrapperThe definition graph is not well-founded; evaluation would recurse without terminating.(path: null— the one refusal that named no node). Now it refuses withCyclic symbol definition: a → b → a.at$.children[0].node.children[0].node— the reference that closed the cycle, two definition hops below the root. The chain is trimmed to start where the cycle does, so a definition merely passed through on the way in (dependant → a → b → a) reportsa → b → a, not itself.Expression::parameters()derives from the compiler's recorded reads (viadiagnose(), which records references even through parts that refuse), filtered of defined names. One semantic improvement, in the same direction #92 set: a declared input read only through a definition now counts. With['gross' => turnover * rate]defined,gross + 1reported[]before (the reflective scan never descended into definitions) and reports['turnover', 'rate']now — exactly the bindings the call will demand.The persisted-tree guard in
compileOwnedSymbolis deleted. Its whole job was catching disagreement between the compiler and the reflective scan; with one walk there is nothing to disagree. A symbol a host compiler constructs without persisting is no longer refused — resolution itself records it, so it shows up inparameters()andProgram::$referencesand cannot hide. TheSourceCompilationTest"cannot hide a dependency" test now asserts that stronger property.Behavioral changes to be aware of:
a → b + c, b → a, c → bread fromcreportsb → a → bandc → b → a → c.UnboundSymbolsis breaking — it shipped in 0.6.x.DefinitionGraphwas unreleased, so its changelog entries are edited to describe the final state.Evidence: PHPStan clean, 1,172 tests at 100% line coverage, infection MSI 100%. Net −796 lines.
🤖 Generated with Claude Code