fix: settle what a case boundary is, and how a branch is named - #189
Merged
Conversation
CanonicalWords was not a fixed point. Feeding a canonical back through it could
change it: CanonicalWords("Aℤ") is "aℤ", and CanonicalWords("aℤ") is "a_ℤ".
wordBoundary asked unicode.IsUpper while irverify.isCased asks whether
lowercasing changes the value, and the two disagree about a rune that is
uppercase with no lowercase form of its own — double-struck ℤ, GREEK UPSILON
WITH HOOK ϒ, the Roman numerals. The grammar split on such a rune, lowercasing
left it unchanged, so it still looked like a boundary in the output and the
second pass split it again. The consequence is worse than the drift: segmentation
came to depend on the casing of the source spelling rather than on its words, so
COUNTℤ was one word and countℤ two, for names an emitter is meant to treat alike.
The grammar now asks the same question the verifier does — does lowercasing
change this rune — so producer and checker agree by construction. It also takes
in the titlecase letters, which are a boundary and which IsUpper reports false
for, so xDžy is two words rather than one.
No golden moved: no corpus name carries such a rune. The decision is pinned by
six conformance rows instead, and ir-design §3.2 now says which definition the
contract means, since it defined a word without saying what case means for a
letter that has only one.
Closes #187
canonicalCases pins the grammar's answers, and a table is the right shape for that: which words userID splits into is a specification decision. But a table only covers the rows someone thought to write, and the defect fixed in the commit before this one sat in a spelling no row contained. This adds the claims that hold for every input rather than a listed one: the grammar is a fixed point, its output is the word sequence ir.Naming.Canonical promises, and the word runes of that output are the lowercased word runes of the input in order — so a word going missing is visible, which no shape check can see. It is committed after the fix rather than before so no commit in history is red, but it was written first and reddened on three of its own seeds. Against the table as it stood before this branch, reinstating the defect leaves the table green and this target red, which is the whole claim. A grammar mishandling a single script does the same, and is the class a list of Latin examples cannot reach at all. None of this says the grammar is right — a property computed through the grammar moves with it. The table says what the answer should be, these say what any answer must satisfy, and irverify says every Naming in a Document agrees with whatever the grammar answered. The seeds are adversarial rather than drawn from real specs because the gate runs a fuzz target's seeds without searching. What ships as standing coverage is exactly those spellings, and the doc comment says so rather than leaving a reader to assume the target explores on every run. Closes #186
An inline oneOf/anyOf branch whose pointer an outside $ref also names took its hint from whichever lowering interned the node first. The composition derived variant_0; hoistSubSchema, reached through the reference, derived the pointer's last segment — the bare ordinal "0". Both are valid hints and nothing compared them, so the same components in two declaration orders compiled to two different documents, silently. The $ref-branch half of this already agreed, because a $ref branch has a target both paths read a name from. An inline branch has none, so the two genuinely held different information: the composition knew the branch's ordinal within the union, and the pointer walk knew only the segment, which is that ordinal with nothing to say it is one. The composition's spelling wins and the pointer walk now derives it. "0" is not a name an emitter can build an identifier from, and it is the same information either way — the ordinal — so the only question was which of them says what it means. Both now go through one derivation, so they cannot drift apart again. The corpus carries the shape for the first time. The two-order oracle detects this class already and no committed spec put it in reach; the case declares the reference before the union deliberately, since interning through the reference is the path that was wrong, so a single-order golden can see it too. Reverting the fix now fails the conformance case and the corpus sweep, where before it changed output that every check accepted. Closes #181
Both are the same kind of rule as the ones around them: learned by shipping the opposite here, and cheap to follow once written down. A line a new guard forces you onto may already carry a recorded decision. #185 tightened a check, which made one Naming literal a violation, and settled the choice without knowing #184 had framed it, answered it, and asked for its own change. It took #188 to correct. And a fixture's declaration order is part of the test. A golden declaring things in the order the correct lowering already produced cannot see an order-dependent fix: reverting it leaves the golden green. The inline-branch case on this branch did exactly that until its two components were swapped.
The commit that settled what "upper" means applied one test to both boundary rules. They ask different questions, and a mutation sweep caught the second going unverified — reverting it changed no test. The camel-case rule asks whether a rune is a case *transition*, so it must ask whether lowercasing changes it; that is what makes the grammar a fixed point. The acronym-tail rule asks whether a rune belongs to a run of capitals, which is about the letter's form. Answering it with the transition test lost ℤServer's boundary — ℤ is a capital whether or not lowercasing would change it — and answering it with the uppercase category alone lost the titlecase forms, which IsUpper reports false for. It now takes both, so ℤServer is ℤ_server and DžBc is dž_bc. Three conformance rows pin it, and reverting either half of the definition now reddens them. No golden moved: no corpus name carries such a rune.
Both documents recorded #73 as answered on the grounds that the grammars landed in the framework "rather than in `ir` as its text proposed". Half of that is no longer true: the naming grammar moved to `ir` in #185, and `irverify` validates against it — which is #73's first acceptance bullet met as its own text proposed. Its second bullet is untouched. #73 also asks for the `t/prim/<kind>` constructor in `ir`, and that is still in `compilers/compile`, so the issue stays open rather than being closed on a summary that no longer describes it. The ID grammar staying in the framework is not a departure: a compiler's path is its own and nothing in `ir` can compute one, which is why only the shape moved.
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
Two naming decisions the compiler was making inconsistently, and the mechanism
that found one of them.
A canonical name drifted on a second pass.
CanonicalWords("Aℤ")was"aℤ",and
CanonicalWords("aℤ")was"a_ℤ".wordBoundaryaskedunicode.IsUpperwhile
irverify.isCasedasks whether lowercasing changes the value, and the twodisagree about a rune that is uppercase with no lowercase form of its own —
double-struck
ℤ, GREEK UPSILON WITH HOOKϒ, the Roman numerals. The grammarsplit on such a rune, lowercasing left it unchanged, so it still looked like a
boundary and the next pass split it again. The consequence is worse than drift:
segmentation came to depend on the casing of the source spelling rather than on
its words, so
COUNTℤwas one word andcountℤtwo, for names an emitter ismeant to treat alike.
The two boundary rules turn out to ask different questions, and settling that is
the fix. A camel-case boundary is a case transition, so it asks whether
lowercasing changes the rune — the same test the verifier applies, so producer
and checker now agree by construction. An acronym tail is the last of a run of
capitals, which is about the letter's form:
ℤServerisℤ_serverwhether ornot lowercasing would touch
ℤ, andDžBcisdž_bcbecause a titlecase letterbelongs to a run too and
IsUpperreports false for it.An inline union branch was named by whichever lowering arrived first. A
oneOf/anyOfbranch whose pointer an outside$refalso names tookvariant_0from the composition or
0from the pointer walk, depending on declarationorder — two different documents from the same components, silently. The
$refhalf of this already agreed, because a
$refbranch has a target both paths reada name from; an inline branch has only its position. The composition's spelling
wins, both paths now go through one derivation, and
0is not a name an emittercan build an identifier from.
The grammar had no validation beyond a table of examples.
canonicalCasespins the answers, which is the right shape for a specification decision, but a
table only covers the rows someone wrote — and the defect above sat in a spelling
no row contained. A fuzz target now asserts what must hold for any input: the
grammar is a fixed point, its output is the word sequence
Naming.Canonicalpromises, and the word runes of that output are the lowercased word runes of the
input in order.
Test plan
gofmt,go vet,golangci-lint,go build, andscripts/check-coverage.shat exactly 100% of 3963 statements.
grammar dropping case, dropping single-rune words, or mishandling one script,
the branch hint reverting on either path, and the pointer parse widening or
narrowing. The sweep earned its place — it found the acronym-tail change
unverified, which is what surfaced that the two rules ask different questions.
stood before this branch, reinstating the case defect leaves it green and the
target red. A grammar mishandling a single script does the same, and is a class
a list of Latin examples cannot reach.
oracle detects this class already and no committed spec put it in reach. The
case declares the reference before the union deliberately, since interning
through the reference is the path that was wrong — otherwise a single-order
golden cannot see the fix at all, which it could not until the two were swapped.
decision is pinned by nine conformance rows instead, and
ir-design.md§3.2 nowsays which definition the contract means for each rule.
Notes
CLAUDE.mdgains two rules, both paid for by the previous two branches: check thetracker before settling a decision on a line a new guard forced you onto (#185
settled one that #184 had already framed, and needed #188 to correct), and treat a
fixture's own declaration order as part of the test.
Applying the first of those to this branch turned up two stale claims, corrected
here rather than left: #73's proposal for the naming grammar was
irand the docsrecorded the opposite, and #42's reproduction no longer holds. Both are commented
on their issues; #73 stays open for the half it still asks for.
Closes #181
Closes #186
Closes #187