Repository navigation
Rewrite stored source under proved obligations - #94
Closed
robertvansteen wants to merge 1 commit into
Closed
robertvansteen wants to merge 1 commit into
robertvansteen wants to merge 1 commit into
Conversation
Add Superscript\Axiom\Rewrite: a rule set applied bottom-up over an immutable Source tree, returning the new tree plus a report of every site a rule fired, every site a rule was refused, and every shape the walk could not see inside. A rule owns matching and replacement for the exact classes it visits and writes no traversal; descent belongs to the toolkit, is exhaustive over the core node set by law, and joins host arms through Extension::sourceDescenders(). A class no extension claims is an opaque leaf: never descended, never rewritten, always reported. Nothing is applied that was not proved. Every replacement compiles against the expression's own scope and must certify the type it replaces, or refuse identically; a rule may claim verdict preservation, checked against a host bindings corpus. A broken obligation refuses that one site and reports it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Kept as a design reference: this will be proven inside a host application first — module-local, against the current released model — and extracted here once the API has real migration rules behind it. 🤖 Generated with Claude Code |
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.
Hosts that store expressions eventually have to change them in bulk, and today the only options are editing a corpus by hand (a migration nobody finishes) or a script (a migration nobody can prove). This adds
Superscript\Axiom\Rewrite: a rule set applied bottom-up over an immutableSourcetree, where every replacement must discharge an obligation before it is applied, and everything the run did — and everything it could not see — is reported.Stacks on #93 (
demanded-inputs); it targets that branch and should merge after it. Review only the last commit.One expression, end to end
Read the report and take nothing — that is the dry run:
Then take the tree:
$run->changedistrue,$run->source->describe()is(roof > 0.25) && flag, and$run->expression()puts it back under the original dialect, definitions, declarations and boundary. There is no mode flag — report-only is this same run with the tree ignored.Now the same rule against a stored
!!countwherecountis a Number:$run->changedisfalseand$run->sourceis the very tree that went in. Simplifying would have handed back a certified Number program in place of a refusal — one the author never wrote and the checker never blessed. One site is refused; the rest of the run proceeds.What is in the box
Sourceclasses it visits —identifier(),visits(),rewrite(Source): ?Source,preserves()— and writes no traversal. Structural knowledge stays in rules rather than becoming a method every host node must implement forever. Dispatch is exact-class indexed, so a node costs one array lookup however many rules a run carries.CoreSourceDescendershas a descend-and-rebuild arm per core node (and per match pattern), andCoreDescentExhaustivenessTestreflects oversrc/Sourcesand fails if a class exists without one. Untouched subtrees come back as the same instance, so$run->changedis answered by identity.Extension::sourceDescenders()is never descended, never rewritten — not even by a rule naming its own class, since a rule cannot be trusted to rebuild a shape the toolkit cannot take apart — and it is named in the report (opaque Acme\Sources\LookupSource at $.right: LookupSource). A silent skip would let a host add a node class, forget its arm, and read "no rewrites needed" as "nothing to do".BindingsCorpusand compares answers, naming the case that disagrees. A claim with no oracle is reported unchecked — which neither blocks a rewrite nor counts as evidence for one.RemoveDoubleNegationis neutral because core's!is the only row for its symbol (Boolean in, Boolean out), so a!!xthat compiles has an operand ofBooleanor, lifted,Boolean?, and on both the operation is total and involutive.RemoveRedundantDefaultis the other shape — one whose applicability matching cannot settle:x ?? 0is the identity exactly whenxcan never be absent, which is a fact about types, so it proposes the removal everywhere and lets type preservation decide (NumberagainstNumber?refuses the ones absence can reach).The namespaced-symbol migration is wire-level, not a Source rule
A natural question is whether the namespaced-symbol migration (the wire format this branch retires) is expressible here. It is not, and the reason is structural rather than a gap in the toolkit:
SymbolSourceno longer carries a namespace and its constructor rejects a dotted name outright, so stored{"name": "turnover", "namespace": "customer"}cannot be hydrated into anySource. There is no input tree for a rule to match. The migration belongs in the host's deserializer, which choosesMemberAccessSource(SymbolSource('customer'), 'turnover')before a tree exists. A migration is a rewrite rule only once both shapes are representable; this one changes the node set rather than moving within it.docs/rewriting.mdstates the distinction so the next such change is triaged in the right place.Notes for the reviewer
$.left.operand,$.arms[1].expression), deliberately not the$.children[0].nodelanguage compilation speaks. Those indices number the children a compiler recorded, and a compiler records what it needs — a wildcard arm records no pattern child — so the same arm sits at a different index depending on which patterns precede it. A coordinate into stored source must name the same node before anything is compiled.Extensiongains one hook (sourceDescenders()) with the same exact, unranked ownership assourceCompilers(), so one package declares both how its node compiles and how a rewrite reaches through it.CONTEXT.mdgains one bullet: the file already names every seam a host implements, and rewrite rules, descent arms and the opaque leaf are one.Validation
composer testgreen: PHPStan max clean, 1290 tests / 4911 assertions at 100% line coverage, Infection at MSI 100%.vendor/bin/pint --testclean on touched files,git diff --checkclean.