ir.CanonicalWords is documented as a fixed point, and
FuzzCanonicalWords_Properties/assertIdempotent asserts it. Feeding a
canonical back in can still change it:
CanonicalWords("ℤℤA") == "ℤℤa"
CanonicalWords("ℤℤa") == "ℤ_ℤa"
"Aℤ" — the shape #187 named — is a fixed point today, so #187's own case is
fixed. This is the same mechanism surviving at a different position: there the
caseless capital followed the cased one, here it precedes it.
Why
The acronym-tail rule in wordBoundary (ir/naming.go:97) splits at the last
capital of a run when the next rune is lowercase:
case isCapital(prev) && isCapital(r) && i+1 < len(runes) && unicode.IsLower(runes[i+1]):
On "ℤℤA" nothing follows the run in lowercase, so the first pass produces one
word and lowercases it to "ℤℤa". But lowercasing is what supplies the missing
lowercase rune: ℤ has no lowercase form and survives as a capital, while A
becomes a. The second pass now sees isCapital('ℤ') && isCapital('ℤ') with a
lowercase a following, fires the tail rule, and yields "ℤ_ℤa".
ir/naming.go:124-126 states the invariant this breaks, and states it as the
reason the two rules are safe together:
Both stay idempotent: after one pass the only capitals left are the ones
lowercasing does not change, and the tail rule needs a lowercase letter
following, which such a run does not produce on its own.
The run does not produce it — the rune after the run does, by being lowercased.
The reasoning holds for a caseless capital at the end of a name and not for one
before a cased letter.
Why it matters
Same consequence #187 recorded: the segmentation depends on the casing of the
source spelling rather than on its words, and Naming.Canonical is ABI precisely
so an emitter never has to know which spelling produced it. Canonical == CanonicalWords(Source) still holds, so irverify stays clean and nothing in the
current suite goes red — the seed "ℤℤa" is already in adversarialRunes and
passes, because the input that breaks the property is the uppercase spelling
one mutation away from it.
Reproducer
ir/testdata/fuzz/FuzzCanonicalWords_Properties/<name>:
go test fuzz v1
string("ℤℤA")
Found by a bounded -fuzz run over the existing target. The file is deliberately
not committed yet — it would redden go test ./ir for everyone before the fix
lands; commit it with the fix so the case stays pinned.
What to decide
Whether the tail rule should ask carriesCase of the rune it splits before (so a
run of caseless capitals is never a tail), or whether it should look at what the
run will become after lowercasing rather than at the source runes. The first is
the smaller change and matches the direction #187 chose for the lower→Upper rule;
it costs "ℤServer" its split, which may or may not be wanted.
ir.CanonicalWordsis documented as a fixed point, andFuzzCanonicalWords_Properties/assertIdempotentasserts it. Feeding acanonical back in can still change it:
"Aℤ"— the shape #187 named — is a fixed point today, so #187's own case isfixed. This is the same mechanism surviving at a different position: there the
caseless capital followed the cased one, here it precedes it.
Why
The acronym-tail rule in
wordBoundary(ir/naming.go:97) splits at the lastcapital of a run when the next rune is lowercase:
On
"ℤℤA"nothing follows the run in lowercase, so the first pass produces oneword and lowercases it to
"ℤℤa". But lowercasing is what supplies the missinglowercase rune:
ℤhas no lowercase form and survives as a capital, whileAbecomes
a. The second pass now seesisCapital('ℤ') && isCapital('ℤ')with alowercase
afollowing, fires the tail rule, and yields"ℤ_ℤa".ir/naming.go:124-126states the invariant this breaks, and states it as thereason the two rules are safe together:
The run does not produce it — the rune after the run does, by being lowercased.
The reasoning holds for a caseless capital at the end of a name and not for one
before a cased letter.
Why it matters
Same consequence #187 recorded: the segmentation depends on the casing of the
source spelling rather than on its words, and
Naming.Canonicalis ABI preciselyso an emitter never has to know which spelling produced it.
Canonical == CanonicalWords(Source)still holds, soirverifystays clean and nothing in thecurrent suite goes red — the seed
"ℤℤa"is already inadversarialRunesandpasses, because the input that breaks the property is the uppercase spelling
one mutation away from it.
Reproducer
Found by a bounded
-fuzzrun over the existing target. The file is deliberatelynot committed yet — it would redden
go test ./irfor everyone before the fixlands; commit it with the fix so the case stays pinned.
What to decide
Whether the tail rule should ask
carriesCaseof the rune it splits before (so arun of caseless capitals is never a tail), or whether it should look at what the
run will become after lowercasing rather than at the source runes. The first is
the smaller change and matches the direction #187 chose for the lower→Upper rule;
it costs
"ℤServer"its split, which may or may not be wanted.