From df639ece7e29e4ace2c08c872be891a34a1a9d13 Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Wed, 4 Nov 2026 23:27:19 +0300 Subject: [PATCH 01/10] fix(ir/irverify): read blank names as Unicode defines them isInvisible approximated Default_Ignorable_Code_Point as space, control, Cf and Other_Default_Ignorable_Code_Point, wrong in both directions. It missed Variation_Selector (U+FE00..FE0F, U+E0100..E01EF, U+180B..180D, U+180F), so an entry made only of selectors was not blank. It counted the Cf characters Unicode takes out of the property because they render: Prepended_Concatenation_Mark, the interlinear annotation characters U+FFF9..FFFB and the Egyptian hieroglyph format controls U+13430..1343F, so an entry made of those was reported blank. Add Variation_Selector and subtract the other three. Package unicode has a table for every term but the two ranges, which are named constants. Both callers of isBlankName change with it: the alias rule (ir/naming-alias-blank) and the string-list rules (ir/path-segment-blank, ir/set-entry-blank). No IR shape changes. --- docs/ir/naming.md | 8 ++++++-- ir/irverify/naming.go | 27 ++++++++++++++++++++++++--- ir/irverify/naming_alias_test.go | 32 +++++++++++++++++++++++++++++--- ir/irverify/stringlists_test.go | 4 ++-- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/docs/ir/naming.md b/docs/ir/naming.md index 3a6f4a2eb..1edd99794 100644 --- a/docs/ir/naming.md +++ b/docs/ir/naming.md @@ -88,8 +88,12 @@ no content rule touches it, because it records what the spec said rather than de What is left is decidable without any grammar, and `irverify` holds an alias to exactly that much: - **Every entry names something** (`ir/naming-alias-blank`). An entry whose every rune is one - Unicode classifies as invisible — a space, a control, a format character, or a default-ignorable - one — matches nothing under any grammar. `""`, `" "`, `"\u200b"` and `"\u3164"` are alike here. + Unicode classifies as invisible — white space, a control, or a `Default_Ignorable_Code_Point` + (most format characters, the variation selectors, and fillers such as U+3164) — matches nothing + under any grammar. `""`, `" "`, `"\u200b"`, `"\u3164"` and `"\ufe0f"` are alike here. The format + characters Unicode takes out of that property because they render — the prepended concatenation + marks, the interlinear annotation characters and the Egyptian hieroglyph format controls — are + visible, so an entry made of them is not blank, as U+2800 is not. An invisible rune sitting *beside* a visible one is a different question and is not asked: whether `com.example.User` is a legal name is decidable only under the grammar of the format it will be matched against, which the IR does not know. diff --git a/ir/irverify/naming.go b/ir/irverify/naming.go index dfc9f4739..e1135cba3 100644 --- a/ir/irverify/naming.go +++ b/ir/irverify/naming.go @@ -319,12 +319,33 @@ func isBlankName(s string) bool { return true } -// isInvisible reports whether Unicode classifies r as carrying no visible mark. +// isInvisible reports whether Unicode classifies r as carrying no visible mark: +// white space, a control, or Default_Ignorable_Code_Point. The package unicode +// has no table for that property, so it is built from its terms as +// DerivedCoreProperties.txt defines it, Other_Default_Ignorable_Code_Point, +// Cf and Variation_Selector less Prepended_Concatenation_Mark and the two +// ranges unicode lacks a table for, the interlinear annotation characters and +// the Egyptian hieroglyph format controls, which are Cf and render. func isInvisible(r rune) bool { - return unicode.IsSpace(r) || unicode.IsControl(r) || unicode.Is(unicode.Cf, r) || - unicode.Is(unicode.Other_Default_Ignorable_Code_Point, r) + switch { + case unicode.IsSpace(r), unicode.IsControl(r): + return true + case r >= interlinearFirst && r <= interlinearLast, + r >= egyptianFormatFirst && r <= egyptianFormatLast, + unicode.Is(unicode.Prepended_Concatenation_Mark, r): + return false + } + return unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Other_Default_Ignorable_Code_Point, r) || + unicode.Is(unicode.Variation_Selector, r) } +// The Cf ranges Unicode takes out of Default_Ignorable_Code_Point that package +// unicode has no table for: U+FFF9..U+FFFB and U+13430..U+1343F. +const ( + interlinearFirst, interlinearLast = '\uFFF9', '\uFFFB' + egyptianFormatFirst, egyptianFormatLast = '\U00013430', '\U0001343F' +) + // isCased reports whether s still carries casing an emitter should own. The test // is lowercase-idempotence, not unicode.IsUpper: a compiler neutralizes names // with strings.ToLower, so a rune that has no lowercase form (double-struck ℤ, diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index 280b43412..d7698c4b0 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -70,7 +70,12 @@ func TestVerify_BlankAliasIsAViolation(t *testing.T) { "nul": "\x00", "hangul filler": "\u3164", "jamo filler": "\u115f", - "invisible mix": "\u200b\t\ufeff\u3164", + // Variation_Selector: Mn, so neither Cf nor Other_Default_Ignorable. + "variation selector": "\ufe0f", + "variation selector supplement": "\U000e0100", + "mongolian selector": "\u180b", + "mongolian selector four": "\u180f", + "invisible mix": "\u200b\t\ufeff\u3164", } for name, alias := range blanks { t.Run(name, func(t *testing.T) { @@ -95,10 +100,31 @@ func TestVerify_InvisibleRuneBesideAVisibleOneIsNotBlank(t *testing.T) { // TestVerify_BlankLookingGraphicAliasIsNotBlank holds the other side. U+2800 // BRAILLE PATTERN BLANK renders as whitespace but is a graphic character // Unicode does not classify as invisible, so the rule leaves it alone rather -// than deciding on its own account what looks empty. +// than deciding on its own account what looks empty. So are the format +// characters Unicode takes out of Default_Ignorable_Code_Point because they +// render: a prepended concatenation mark, an interlinear annotation character +// and an Egyptian hieroglyph format control. func TestVerify_BlankLookingGraphicAliasIsNotBlank(t *testing.T) { t.Parallel() - assert.Empty(t, aliasViolations(t, "\u2800")) + graphics := map[string]string{ + "braille blank": "\u2800", + "arabic number sign": "\u0600", + "arabic end of ayah": "\u06dd", + "syriac abbreviation": "\u070f", + "arabic pound mark": "\u0890", + "arabic disputed end": "\u08e2", + "kaithi number sign": "\U000110bd", + "annotation anchor": "\ufff9", + "annotation end": "\ufffb", + "egyptian format": "\U00013430", + "egyptian last": "\U0001343f", + } + for name, alias := range graphics { + t.Run(name, func(t *testing.T) { + t.Parallel() + assert.Empty(t, aliasViolations(t, alias)) + }) + } } // TestVerify_IllFormedAliasIsAViolation covers the rule every channel shares, diff --git a/ir/irverify/stringlists_test.go b/ir/irverify/stringlists_test.go index 1bca23e6f..23cafa71a 100644 --- a/ir/irverify/stringlists_test.go +++ b/ir/irverify/stringlists_test.go @@ -114,10 +114,10 @@ func TestVerify_BlankListEntryIsAViolation(t *testing.T) { code, message = "ir/set-entry-blank", "set entry is blank, so it names nothing" } doc := heldListDoc() - list.set(doc, []string{"", " ", "\u3164", "acme"}) + list.set(doc, []string{"", " ", "\u3164", "\ufe0f", "acme", "\u0600", "\ufff9", "\U00013430"}) got := verifyDeclared(doc) - require.Len(t, got, 3, "one violation per blank entry") + require.Len(t, got, 4, "one violation per blank entry, none for a visible format character") for i, v := range got { assert.Equal(t, code, v.Code) assert.Equal(t, fmt.Sprintf("%s[%d]", list.path, i), v.Path) From ee3de624e85a9385941f963a745297764a32db97 Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Thu, 5 Nov 2026 01:33:42 +0300 Subject: [PATCH 02/10] test(ir/irverify): pin the blank rule's Unicode boundary A test now holds the interlinear exclusion's lower bound, with U+FFF8 as the witness directly below it. The test's comment no longer calls the format characters graphic. The constants name the Unicode version they hold for, and say to recheck them when unicode.Version moves. --- ir/irverify/naming.go | 6 ++++-- ir/irverify/naming_alias_test.go | 16 +++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ir/irverify/naming.go b/ir/irverify/naming.go index e1135cba3..ad79dc52d 100644 --- a/ir/irverify/naming.go +++ b/ir/irverify/naming.go @@ -339,8 +339,10 @@ func isInvisible(r rune) bool { unicode.Is(unicode.Variation_Selector, r) } -// The Cf ranges Unicode takes out of Default_Ignorable_Code_Point that package -// unicode has no table for: U+FFF9..U+FFFB and U+13430..U+1343F. +// The Cf ranges Unicode 17.0.0 takes out of Default_Ignorable_Code_Point that +// package unicode has no table for: U+FFF9..U+FFFB and U+13430..U+1343F. The +// second grew at Unicode 15.0, so a Go release that moves unicode.Version needs +// both rechecked against DerivedCoreProperties.txt. const ( interlinearFirst, interlinearLast = '\uFFF9', '\uFFFB' egyptianFormatFirst, egyptianFormatLast = '\U00013430', '\U0001343F' diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index d7698c4b0..b35ff16cb 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -75,7 +75,9 @@ func TestVerify_BlankAliasIsAViolation(t *testing.T) { "variation selector supplement": "\U000e0100", "mongolian selector": "\u180b", "mongolian selector four": "\u180f", - "invisible mix": "\u200b\t\ufeff\u3164", + // Other_Default_Ignorable, directly below the excluded U+FFF9. + "reserved before annotation": "\ufff8", + "invisible mix": "\u200b\t\ufeff\u3164", } for name, alias := range blanks { t.Run(name, func(t *testing.T) { @@ -100,13 +102,13 @@ func TestVerify_InvisibleRuneBesideAVisibleOneIsNotBlank(t *testing.T) { // TestVerify_BlankLookingGraphicAliasIsNotBlank holds the other side. U+2800 // BRAILLE PATTERN BLANK renders as whitespace but is a graphic character // Unicode does not classify as invisible, so the rule leaves it alone rather -// than deciding on its own account what looks empty. So are the format -// characters Unicode takes out of Default_Ignorable_Code_Point because they -// render: a prepended concatenation mark, an interlinear annotation character -// and an Egyptian hieroglyph format control. +// than deciding on its own account what looks empty. It leaves alone, too, the +// format characters Unicode takes out of Default_Ignorable_Code_Point because +// they render: a prepended concatenation mark, an interlinear annotation +// character and an Egyptian hieroglyph format control. func TestVerify_BlankLookingGraphicAliasIsNotBlank(t *testing.T) { t.Parallel() - graphics := map[string]string{ + visible := map[string]string{ "braille blank": "\u2800", "arabic number sign": "\u0600", "arabic end of ayah": "\u06dd", @@ -119,7 +121,7 @@ func TestVerify_BlankLookingGraphicAliasIsNotBlank(t *testing.T) { "egyptian format": "\U00013430", "egyptian last": "\U0001343f", } - for name, alias := range graphics { + for name, alias := range visible { t.Run(name, func(t *testing.T) { t.Parallel() assert.Empty(t, aliasViolations(t, alias)) From d2bb7cf5856c8133005ea9d7f6cadd9ef24ee677 Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Thu, 5 Nov 2026 03:37:08 +0300 Subject: [PATCH 03/10] refactor(ir/irverify): keep the isInvisible case on one line The two range tests move into locals so the case stays on one line, as the styleguide's rule 1.8 asks. The predicate answers the same on every code point. --- ir/irverify/naming.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ir/irverify/naming.go b/ir/irverify/naming.go index ad79dc52d..9e3638130 100644 --- a/ir/irverify/naming.go +++ b/ir/irverify/naming.go @@ -327,12 +327,12 @@ func isBlankName(s string) bool { // ranges unicode lacks a table for, the interlinear annotation characters and // the Egyptian hieroglyph format controls, which are Cf and render. func isInvisible(r rune) bool { + interlinear := r >= interlinearFirst && r <= interlinearLast + egyptianFormat := r >= egyptianFormatFirst && r <= egyptianFormatLast switch { case unicode.IsSpace(r), unicode.IsControl(r): return true - case r >= interlinearFirst && r <= interlinearLast, - r >= egyptianFormatFirst && r <= egyptianFormatLast, - unicode.Is(unicode.Prepended_Concatenation_Mark, r): + case interlinear, egyptianFormat, unicode.Is(unicode.Prepended_Concatenation_Mark, r): return false } return unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Other_Default_Ignorable_Code_Point, r) || From 61c5fbc371273cc315260820292444cdc1128309 Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Thu, 5 Nov 2026 23:26:53 +0300 Subject: [PATCH 04/10] test(ir/irverify): hold each invisibility bound's outer side Rows just outside the interlinear and Egyptian format ranges and a private use character keep Variation_Selector and Cf from widening unseen. The test is renamed for what its rows share, and a second pins the ranges to unicode.Version, which no table of package unicode carries. --- ir/irverify/naming.go | 17 ++++++++--------- ir/irverify/naming_alias_test.go | 31 +++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/ir/irverify/naming.go b/ir/irverify/naming.go index 9e3638130..2f5077b04 100644 --- a/ir/irverify/naming.go +++ b/ir/irverify/naming.go @@ -327,6 +327,14 @@ func isBlankName(s string) bool { // ranges unicode lacks a table for, the interlinear annotation characters and // the Egyptian hieroglyph format controls, which are Cf and render. func isInvisible(r rune) bool { + // The Cf ranges Unicode 17.0.0 takes out of Default_Ignorable_Code_Point that + // package unicode has no table for: U+FFF9..U+FFFB and U+13430..U+1343F. + // TestIsInvisible_RangesHoldForThisUnicode fails when a Go release moves + // unicode.Version, so that move rechecks both. + const ( + interlinearFirst, interlinearLast = '\uFFF9', '\uFFFB' + egyptianFormatFirst, egyptianFormatLast = '\U00013430', '\U0001343F' + ) interlinear := r >= interlinearFirst && r <= interlinearLast egyptianFormat := r >= egyptianFormatFirst && r <= egyptianFormatLast switch { @@ -339,15 +347,6 @@ func isInvisible(r rune) bool { unicode.Is(unicode.Variation_Selector, r) } -// The Cf ranges Unicode 17.0.0 takes out of Default_Ignorable_Code_Point that -// package unicode has no table for: U+FFF9..U+FFFB and U+13430..U+1343F. The -// second grew at Unicode 15.0, so a Go release that moves unicode.Version needs -// both rechecked against DerivedCoreProperties.txt. -const ( - interlinearFirst, interlinearLast = '\uFFF9', '\uFFFB' - egyptianFormatFirst, egyptianFormatLast = '\U00013430', '\U0001343F' -) - // isCased reports whether s still carries casing an emitter should own. The test // is lowercase-idempotence, not unicode.IsUpper: a compiler neutralizes names // with strings.ToLower, so a rune that has no lowercase form (double-struck ℤ, diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index b35ff16cb..c52f44dbc 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "testing" + "unicode" "unicode/utf8" "github.com/stretchr/testify/assert" @@ -99,14 +100,16 @@ func TestVerify_InvisibleRuneBesideAVisibleOneIsNotBlank(t *testing.T) { assert.Empty(t, aliasViolations(t, "com.example.\u200bUser", " padded ")) } -// TestVerify_BlankLookingGraphicAliasIsNotBlank holds the other side. U+2800 -// BRAILLE PATTERN BLANK renders as whitespace but is a graphic character -// Unicode does not classify as invisible, so the rule leaves it alone rather -// than deciding on its own account what looks empty. It leaves alone, too, the -// format characters Unicode takes out of Default_Ignorable_Code_Point because -// they render: a prepended concatenation mark, an interlinear annotation -// character and an Egyptian hieroglyph format control. -func TestVerify_BlankLookingGraphicAliasIsNotBlank(t *testing.T) { +// TestVerify_VisibleAliasIsNotBlank holds the other side. U+2800 +// BRAILLE PATTERN BLANK renders as whitespace but Unicode does not classify it +// as invisible, so the rule leaves it alone rather than deciding on its own +// account what looks empty. It leaves alone, too, the format characters Unicode +// takes out of Default_Ignorable_Code_Point because they render: a prepended +// concatenation mark, an interlinear annotation character and an Egyptian +// hieroglyph format control. The rows just outside those ranges, a character +// from the object replacement to a private use one, keep each bound from +// widening into a class beside it. +func TestVerify_VisibleAliasIsNotBlank(t *testing.T) { t.Parallel() visible := map[string]string{ "braille blank": "\u2800", @@ -120,6 +123,10 @@ func TestVerify_BlankLookingGraphicAliasIsNotBlank(t *testing.T) { "annotation end": "\ufffb", "egyptian format": "\U00013430", "egyptian last": "\U0001343f", + "object replacement": "\ufffc", + "egyptian before": "\U0001342f", + "egyptian mirror": "\U00013440", + "private use": "\ue000", } for name, alias := range visible { t.Run(name, func(t *testing.T) { @@ -129,6 +136,14 @@ func TestVerify_BlankLookingGraphicAliasIsNotBlank(t *testing.T) { } } +// TestIsInvisible_RangesHoldForThisUnicode fails when a Go release moves +// unicode.Version, since isInvisible holds two ranges no table of package unicode +// carries. Recheck them against DerivedCoreProperties.txt, then move the version here. +func TestIsInvisible_RangesHoldForThisUnicode(t *testing.T) { + t.Parallel() + assert.Equal(t, "17.0.0", unicode.Version) +} + // TestVerify_IllFormedAliasIsAViolation covers the rule every channel shares, // now checkUTF8's rather than the alias switch's own: the bytes end in a // multibyte sequence that never completes, and a Document refuses to encode a From 822120e9832c8e79f6936ea8b579ba521bb3a07c Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Fri, 6 Nov 2026 00:39:52 +0300 Subject: [PATCH 05/10] test(ir/irverify): state what the invisibility tests hold The visible-alias rows keep a term from widening over a bound, not a bound from widening: an exclusion moved outward can only answer false, so a false row cannot see it. Say so in the test's comment. Rename the unicode.Version pin to say the ranges were checked, not that they hold, and have its failure say what to recheck. Record beside the constants why the second range stops at U+1343F where DerivedCoreProperties.txt's header writes U+13440. Correct a comment on the blank-list test that still counted one visible entry. --- ir/irverify/naming.go | 6 ++++-- ir/irverify/naming_alias_test.go | 13 +++++++------ ir/irverify/stringlists_test.go | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ir/irverify/naming.go b/ir/irverify/naming.go index 2f5077b04..516d7c00b 100644 --- a/ir/irverify/naming.go +++ b/ir/irverify/naming.go @@ -329,8 +329,10 @@ func isBlankName(s string) bool { func isInvisible(r rune) bool { // The Cf ranges Unicode 17.0.0 takes out of Default_Ignorable_Code_Point that // package unicode has no table for: U+FFF9..U+FFFB and U+13430..U+1343F. - // TestIsInvisible_RangesHoldForThisUnicode fails when a Go release moves - // unicode.Version, so that move rechecks both. + // DerivedCoreProperties.txt's header writes the second as 13430..13440, but + // U+13440 is Mn and in no added term, so subtracting it changes nothing. + // TestIsInvisible_RangesWereCheckedAgainstThisUnicode fails when a Go release + // moves unicode.Version, so that move rechecks both. const ( interlinearFirst, interlinearLast = '\uFFF9', '\uFFFB' egyptianFormatFirst, egyptianFormatLast = '\U00013430', '\U0001343F' diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index c52f44dbc..3738257b6 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -106,9 +106,9 @@ func TestVerify_InvisibleRuneBesideAVisibleOneIsNotBlank(t *testing.T) { // account what looks empty. It leaves alone, too, the format characters Unicode // takes out of Default_Ignorable_Code_Point because they render: a prepended // concatenation mark, an interlinear annotation character and an Egyptian -// hieroglyph format control. The rows just outside those ranges, a character -// from the object replacement to a private use one, keep each bound from -// widening into a class beside it. +// hieroglyph format control. The rows beside those ranges and the private use one +// hold a term from widening over them: U+13440 from Mn, U+E000 from C, U+FFFC +// from So, U+1342F from Lo. func TestVerify_VisibleAliasIsNotBlank(t *testing.T) { t.Parallel() visible := map[string]string{ @@ -136,12 +136,13 @@ func TestVerify_VisibleAliasIsNotBlank(t *testing.T) { } } -// TestIsInvisible_RangesHoldForThisUnicode fails when a Go release moves +// TestIsInvisible_RangesWereCheckedAgainstThisUnicode fails when a Go release moves // unicode.Version, since isInvisible holds two ranges no table of package unicode // carries. Recheck them against DerivedCoreProperties.txt, then move the version here. -func TestIsInvisible_RangesHoldForThisUnicode(t *testing.T) { +func TestIsInvisible_RangesWereCheckedAgainstThisUnicode(t *testing.T) { t.Parallel() - assert.Equal(t, "17.0.0", unicode.Version) + assert.Equal(t, "17.0.0", unicode.Version, + "recheck isInvisible's ranges against DerivedCoreProperties.txt, then move this version") } // TestVerify_IllFormedAliasIsAViolation covers the rule every channel shares, diff --git a/ir/irverify/stringlists_test.go b/ir/irverify/stringlists_test.go index 23cafa71a..7295f8afd 100644 --- a/ir/irverify/stringlists_test.go +++ b/ir/irverify/stringlists_test.go @@ -102,7 +102,7 @@ func TestVerify_HeldListFixturesMatchTheRules(t *testing.T) { // TestVerify_BlankListEntryIsAViolation holds each path and set list to its // blank rule. An entry made only of runes Unicode calls invisible is as blank as -// "", and each is reported at the entry, so the visible one beside them is not. +// "", and each is reported at the entry, so the visible ones beside them are not. func TestVerify_BlankListEntryIsAViolation(t *testing.T) { t.Parallel() require.Empty(t, verifyDeclared(heldListDoc()), "the fixture must draw nothing of its own") From ee41a8229a9ef59979ead63649a6d746f91747bd Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Fri, 6 Nov 2026 01:45:32 +0300 Subject: [PATCH 06/10] docs(ir/irverify): say what the blank rules take as invisible The blank-name comments and docs/ir/naming.md said Unicode classifies white space as invisible, but U+1680 OGHAM SPACE MARK renders a visible line. The rules take white space, controls and Default_Ignorable_Code_Point as one set the package chose, with U+1680 counting as white space; say so. No behaviour changes. --- docs/ir/naming.md | 15 ++++++++------- ir/irverify/naming.go | 12 +++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/ir/naming.md b/docs/ir/naming.md index 1edd99794..d4e567b03 100644 --- a/docs/ir/naming.md +++ b/docs/ir/naming.md @@ -87,13 +87,14 @@ lossless-by-default rules out. `Source` is the internal precedent: it carries `U no content rule touches it, because it records what the spec said rather than deciding a spelling. What is left is decidable without any grammar, and `irverify` holds an alias to exactly that much: -- **Every entry names something** (`ir/naming-alias-blank`). An entry whose every rune is one - Unicode classifies as invisible — white space, a control, or a `Default_Ignorable_Code_Point` - (most format characters, the variation selectors, and fillers such as U+3164) — matches nothing - under any grammar. `""`, `" "`, `"\u200b"`, `"\u3164"` and `"\ufe0f"` are alike here. The format - characters Unicode takes out of that property because they render — the prepended concatenation - marks, the interlinear annotation characters and the Egyptian hieroglyph format controls — are - visible, so an entry made of them is not blank, as U+2800 is not. +- **Every entry names something** (`ir/naming-alias-blank`). An entry whose every rune is white + space, a control, or a `Default_Ignorable_Code_Point` (most format characters, the variation + selectors, and fillers such as U+3164) matches nothing under any grammar, U+1680 OGHAM SPACE MARK + counting with white space though it renders a line. `""`, `" "`, `"\u200b"`, `"\u3164"` and + `"\ufe0f"` are alike here. The format characters Unicode takes out of that property because they + render — the prepended concatenation marks, the interlinear annotation characters and the + Egyptian hieroglyph format controls — are visible, so an entry made of them is not blank, as + U+2800 is not. An invisible rune sitting *beside* a visible one is a different question and is not asked: whether `com.example.User` is a legal name is decidable only under the grammar of the format it will be matched against, which the IR does not know. diff --git a/ir/irverify/naming.go b/ir/irverify/naming.go index 516d7c00b..2d5eed2fa 100644 --- a/ir/irverify/naming.go +++ b/ir/irverify/naming.go @@ -300,8 +300,8 @@ func isWordSequence(s string) bool { } // isBlankName reports whether s holds no rune a name could be made of. It takes -// as invisible only what Unicode itself classifies so, needing no format's -// grammar. +// as invisible only what Unicode's own properties list (isInvisible), needing no +// format's grammar. // // strings.TrimSpace is not that test: IsSpace misses the zero-width joiners, // the soft hyphen and the BOM (all Cf), and space, control and Cf all miss @@ -319,9 +319,11 @@ func isBlankName(s string) bool { return true } -// isInvisible reports whether Unicode classifies r as carrying no visible mark: -// white space, a control, or Default_Ignorable_Code_Point. The package unicode -// has no table for that property, so it is built from its terms as +// isInvisible reports whether r is white space, a control, or +// Default_Ignorable_Code_Point, the set the blank rules take as carrying no +// visible mark. U+1680 OGHAM SPACE MARK renders a line and stays in, as white +// space (GitHub #832). The package unicode has no table for that property, so it +// is built from its terms as // DerivedCoreProperties.txt defines it, Other_Default_Ignorable_Code_Point, // Cf and Variation_Selector less Prepended_Concatenation_Mark and the two // ranges unicode lacks a table for, the interlinear annotation characters and From ccb528d4c572fab6a1e33113d0bc0db8161fd182 Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Fri, 6 Nov 2026 02:03:29 +0300 Subject: [PATCH 07/10] test(ir/irverify): say what the blank tests mean by invisible --- ir/irverify/naming_alias_test.go | 8 ++++---- ir/irverify/stringlists_test.go | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index 3738257b6..7e280877e 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -49,10 +49,10 @@ func TestVerify_NoAliasesIsClean(t *testing.T) { } // TestVerify_BlankAliasIsAViolation covers the whole of what "names nothing" -// means. An entry made only of runes Unicode calls invisible is as blank as "", -// and testing emptiness alone — or trimming only what unicode.IsSpace reports — -// would let most of these through the one rule that exists to catch an entry -// matching nothing. +// means. An entry made only of runes the rule takes as invisible is as blank as +// "", and testing emptiness alone — or trimming only what unicode.IsSpace +// reports — would let most of these through the one rule that exists to catch +// an entry matching nothing. func TestVerify_BlankAliasIsAViolation(t *testing.T) { t.Parallel() blanks := map[string]string{ diff --git a/ir/irverify/stringlists_test.go b/ir/irverify/stringlists_test.go index 7295f8afd..703c26433 100644 --- a/ir/irverify/stringlists_test.go +++ b/ir/irverify/stringlists_test.go @@ -101,8 +101,9 @@ func TestVerify_HeldListFixturesMatchTheRules(t *testing.T) { } // TestVerify_BlankListEntryIsAViolation holds each path and set list to its -// blank rule. An entry made only of runes Unicode calls invisible is as blank as -// "", and each is reported at the entry, so the visible ones beside them are not. +// blank rule. An entry made only of runes the rule takes as invisible is as +// blank as "", and each is reported at the entry, so the visible ones beside +// them are not. func TestVerify_BlankListEntryIsAViolation(t *testing.T) { t.Parallel() require.Empty(t, verifyDeclared(heldListDoc()), "the fixture must draw nothing of its own") From 33119b0500783ca16d32ecf8fe84a6ecf152c54f Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Fri, 6 Nov 2026 02:03:29 +0300 Subject: [PATCH 08/10] test(ir/irverify): hold U+1680 to the blank rule --- ir/irverify/naming_alias_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index 7e280877e..66c638e9b 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -79,6 +79,8 @@ func TestVerify_BlankAliasIsAViolation(t *testing.T) { // Other_Default_Ignorable, directly below the excluded U+FFF9. "reserved before annotation": "\ufff8", "invisible mix": "\u200b\t\ufeff\u3164", + // White_Space, though it renders a line. + "ogham space mark": "\u1680", } for name, alias := range blanks { t.Run(name, func(t *testing.T) { From 9f205632ffa37cbd6b97b7be4a30816d3e37ce8a Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Fri, 6 Nov 2026 12:12:03 +0300 Subject: [PATCH 09/10] test(ir/irverify): witness Zl, Zp and DEL as blank White_Space reaches beyond Zs and Cc: U+2028 and U+2029 are neither, so the rows only the space term held were all Zs. Controls past C0 had no witness either, so narrowing the control term to C0 stayed green. Add a row for each of U+2028, U+2029, DEL and a C1 control. --- ir/irverify/naming_alias_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index 66c638e9b..13bcf6fa8 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -81,6 +81,12 @@ func TestVerify_BlankAliasIsAViolation(t *testing.T) { "invisible mix": "\u200b\t\ufeff\u3164", // White_Space, though it renders a line. "ogham space mark": "\u1680", + // White_Space in neither Zs nor Cc. + "line separator": "\u2028", + "paragraph separator": "\u2029", + // Controls past C0, which only their own term holds. + "delete": "\x7f", + "c1 control": "\u0080", } for name, alias := range blanks { t.Run(name, func(t *testing.T) { From 57484050f2f5de1ed404c7e7334750a2ae5835a6 Mon Sep 17 00:00:00 2001 From: Fuad Daoud Date: Fri, 6 Nov 2026 13:06:45 +0300 Subject: [PATCH 10/10] test(ir/irverify): witness Mn in the default-ignorable term --- ir/irverify/naming_alias_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ir/irverify/naming_alias_test.go b/ir/irverify/naming_alias_test.go index 13bcf6fa8..d2fd9424c 100644 --- a/ir/irverify/naming_alias_test.go +++ b/ir/irverify/naming_alias_test.go @@ -87,6 +87,8 @@ func TestVerify_BlankAliasIsAViolation(t *testing.T) { // Controls past C0, which only their own term holds. "delete": "\x7f", "c1 control": "\u0080", + // Other_Default_Ignorable in Mn, which no other term holds. + "combining grapheme joiner": "\u034f", } for name, alias := range blanks { t.Run(name, func(t *testing.T) {