text: Char.category(), GeneralCategory and case folding in std/text (commit 2 of 2 for #366) #373

Open
opened 2026-10-15 22:08:28 +00:00 by orion · 1 comment
Collaborator

Context

Commit 2 of 2 for #366. std/text gets the public general category of a Char and the case folding that the natives of #372 answer, with the docs and conformance programs that go with them. std/regex and std/markdown switch to them in two later issues. The text of #366 left the shape of the API to refinement: it is internal API design under the existing naming conventions (the toUpperCase/toLowerCase pair exists on Char and on String), decided below.

Depends on

#372 (the natives rows), merged and in a published seed (seeds.txt on the seeds release lists a seed at or after the merge commit). Until then this issue is not ready.

Decisions (refinement)

  • Char.category(): GeneralCategory, a closed public enum in std/text with the 29 categories a scalar value can have; Cs (surrogate) has no case because a Char is never a surrogate. A public enum never grows within 1.x (RELEASE.md section 3 and the policy row); the Unicode Stability Policy fixes the set of values, so the policy holds. It is a method on Char (methods are the preferred API style, #146), not a set of isPunctuation()-style predicates on Char: one lookup answers every question, and Char.isLetter() stays as the fast path that exists.
  • Cases are the long Unicode names in PascalCase, as the other public enums of std name theirs: UppercaseLetter, LowercaseLetter, TitlecaseLetter, ModifierLetter, OtherLetter, NonspacingMark, SpacingMark, EnclosingMark, DecimalNumber, LetterNumber, OtherNumber, ConnectorPunctuation, DashPunctuation, OpenPunctuation, ClosePunctuation, InitialPunctuation, FinalPunctuation, OtherPunctuation, MathSymbol, CurrencySymbol, ModifierSymbol, OtherSymbol, SpaceSeparator, LineSeparator, ParagraphSeparator, Control, Format, PrivateUse, Unassigned. with Equals, Hash, Show.
  • No fixed case values: rawValue() would publish a number nobody should rely on. Char.category() is ordinary TorbScript over a private native Char.categoryCode(): Int (a match over the runtime's numbers, Cs = 27 unreachable). If the manifest cannot name a private native of Char, the implementer makes it a documented native fn of the same name and reports it in the PR.
  • Members of GeneralCategory: abbreviation(): String ("Lu"), static fn fromAbbreviation(text: String): Self? (None for a name that is no category, and for "Cs"), and the seven groups of the standard as predicates: isLetter(), isMark(), isNumber(), isPunctuation(), isSymbol(), isSeparator(), isOther(). Regex classes (\p{L}) and CommonMark ("P or S") are written against exactly these.
  • Case folding follows the pair that exists for case mapping, on both types: Char.toCaseFolded(): Self (simple folding, one code point in and out) and String.toCaseFolded(): Self (full folding, ß becomes ss); not Char.foldCase(). The name says what comes out; caseless comparison is a.toCaseFolded() == b.toCaseFolded(), which is not toLowerCase() (ς and σ, Cherokee, ẞ).
  • Char.nextCaseVariant(): Self, the cyclic successor in the class of code points that fold alike (K -> k -> Kelvin sign -> K); it is public because std/regex is a separate package and a private native cannot be reached from it. Doc comment names it as the primitive for case-insensitive matching.
  • No caseVariants() list and no isPunctuation() on Char: additions are cheap, a public commitment is not, and the callers (the next two issues) need neither.

Acceptance criteria

  • std/text/src/lib.trb: GeneralCategory with the members above; Char.category(), private native fn categoryCode(), native fn toCaseFolded() and native fn nextCaseVariant() on Char, native fn toCaseFolded() on String, each with a doc comment in the style of the neighbours (# Examples; # Pitfalls: simple against full folding, folding is not lower-casing, no locale/Turkish tailoring, normalization is a separate question).
  • Every GeneralCategory case is reached and maps to the right abbreviation: a conformance program tests/conformance/character-category.trb (+ .expected, .allocations), identical in the VM and natively, with one code point per category ('A', 'a', Dž U+01C5, ʰ U+02B0, א U+05D0, U+0301, U+0903, U+20DD, ٣ U+0663, Ⅷ U+2167, ½, _, -, (, ), «, », !, +, $, ^, ©, space, U+2028, U+2029, U+0000, U+00AD, U+E000, U+0378), fromAbbreviation of every abbreviation and of "Cs", "xx" and the empty text, the group predicates, and a sweep of the Basic Multilingual Plane that isLetter() agrees with category().isLetter() on every code point.
  • A conformance program tests/conformance/character-case-folding.trb (+ files), VM and native identical: K/k/Kelvin sign cycle through nextCaseVariant, Σ/σ/ς, ß and ẞ (Char answers itself or its simple folding, String answers ss), ffi, İ (no Turkic folding), Armenian, Cherokee, Georgian, ASCII text (storage shared, no allocation), the empty text, bytes that are no character, idempotence on a mixed text.
  • docs/standard-library/text.md, the status lines of docs/ROADMAP.md and CONCEPT.md ("Case mapping and character classification read the Unicode Character Database") say what is built; build/release/torb docs skill docs skills regenerated and committed (--check is a tier A gate).
  • Tier A and B green in forge CI.

Seed impact

No new native here (the rows of #372 are known to the seed once it is published), but a std name is added: the declarations are what commit 1 made safe. Nothing under seed/; no seed refresh needed after this merge. The compiler looks up none of the new names by string.

Size

One PR, one Conventional Commit: feat(text): Char.category and case folding.

Part of #366.

## Context Commit 2 of 2 for #366. `std/text` gets the public general category of a `Char` and the case folding that the natives of #372 answer, with the docs and conformance programs that go with them. `std/regex` and `std/markdown` switch to them in two later issues. The text of #366 left the shape of the API to refinement: it is internal API design under the existing naming conventions (the `toUpperCase`/`toLowerCase` pair exists on `Char` and on `String`), decided below. ## Depends on #372 (the natives rows), **merged and in a published seed** (`seeds.txt` on the `seeds` release lists a seed at or after the merge commit). Until then this issue is not `ready`. ## Decisions (refinement) - **`Char.category(): GeneralCategory`, a closed public enum in `std/text`** with the 29 categories a scalar value can have; `Cs` (surrogate) has no case because a `Char` is never a surrogate. A public enum never grows within 1.x ([RELEASE.md section 3 and the policy row](https://git.torb.dev/torbscript/language/src/commit/740a15aad627ea4ff37f857a5a68d780c2b56aed/docs/design/RELEASE.md#L118)); the Unicode Stability Policy fixes the set of values, so the policy holds. It is a method on `Char` (methods are the preferred API style, #146), not a set of `isPunctuation()`-style predicates on `Char`: one lookup answers every question, and `Char.isLetter()` stays as the fast path that exists. - **Cases are the long Unicode names in PascalCase**, as the other public enums of `std` name theirs: `UppercaseLetter`, `LowercaseLetter`, `TitlecaseLetter`, `ModifierLetter`, `OtherLetter`, `NonspacingMark`, `SpacingMark`, `EnclosingMark`, `DecimalNumber`, `LetterNumber`, `OtherNumber`, `ConnectorPunctuation`, `DashPunctuation`, `OpenPunctuation`, `ClosePunctuation`, `InitialPunctuation`, `FinalPunctuation`, `OtherPunctuation`, `MathSymbol`, `CurrencySymbol`, `ModifierSymbol`, `OtherSymbol`, `SpaceSeparator`, `LineSeparator`, `ParagraphSeparator`, `Control`, `Format`, `PrivateUse`, `Unassigned`. `with Equals, Hash, Show`. - **No fixed case values**: `rawValue()` would publish a number nobody should rely on. `Char.category()` is ordinary TorbScript over a **private** native `Char.categoryCode(): Int` (a `match` over the runtime's numbers, `Cs` = 27 unreachable). If the manifest cannot name a private native of `Char`, the implementer makes it a documented `native fn` of the same name and reports it in the PR. - **Members of `GeneralCategory`**: `abbreviation(): String` (`"Lu"`), `static fn fromAbbreviation(text: String): Self?` (`None` for a name that is no category, and for `"Cs"`), and the seven groups of the standard as predicates: `isLetter()`, `isMark()`, `isNumber()`, `isPunctuation()`, `isSymbol()`, `isSeparator()`, `isOther()`. Regex classes (`\p{L}`) and CommonMark ("P or S") are written against exactly these. - **Case folding follows the pair that exists for case mapping, on both types**: `Char.toCaseFolded(): Self` (simple folding, one code point in and out) and `String.toCaseFolded(): Self` (full folding, `ß` becomes `ss`); not `Char.foldCase()`. The name says what comes out; caseless comparison is `a.toCaseFolded() == b.toCaseFolded()`, which is not `toLowerCase()` (`ς` and `σ`, Cherokee, `ẞ`). - **`Char.nextCaseVariant(): Self`**, the cyclic successor in the class of code points that fold alike (`K` -> `k` -> Kelvin sign -> `K`); it is public because `std/regex` is a separate package and a private native cannot be reached from it. Doc comment names it as the primitive for case-insensitive matching. - No `caseVariants()` list and no `isPunctuation()` on `Char`: additions are cheap, a public commitment is not, and the callers (the next two issues) need neither. ## Acceptance criteria - [ ] `std/text/src/lib.trb`: `GeneralCategory` with the members above; `Char.category()`, private `native fn categoryCode()`, `native fn toCaseFolded()` and `native fn nextCaseVariant()` on `Char`, `native fn toCaseFolded()` on `String`, each with a doc comment in the style of the neighbours (`# Examples`; `# Pitfalls`: simple against full folding, folding is not lower-casing, no locale/Turkish tailoring, normalization is a separate question). - [ ] Every `GeneralCategory` case is reached and maps to the right abbreviation: a conformance program `tests/conformance/character-category.trb` (+ `.expected`, `.allocations`), identical in the VM and natively, with one code point per category (`'A'`, `'a'`, `Dž` U+01C5, `ʰ` U+02B0, `א` U+05D0, U+0301, U+0903, U+20DD, `٣` U+0663, `Ⅷ` U+2167, `½`, `_`, `-`, `(`, `)`, `«`, `»`, `!`, `+`, `$`, `^`, `©`, space, U+2028, U+2029, U+0000, U+00AD, U+E000, U+0378), `fromAbbreviation` of every abbreviation and of `"Cs"`, `"xx"` and the empty text, the group predicates, and a sweep of the Basic Multilingual Plane that `isLetter()` agrees with `category().isLetter()` on every code point. - [ ] A conformance program `tests/conformance/character-case-folding.trb` (+ files), VM and native identical: `K`/`k`/Kelvin sign cycle through `nextCaseVariant`, `Σ`/`σ`/`ς`, `ß` and `ẞ` (`Char` answers itself or its simple folding, `String` answers `ss`), `ffi`, `İ` (no Turkic folding), Armenian, Cherokee, Georgian, ASCII text (storage shared, no allocation), the empty text, bytes that are no character, idempotence on a mixed text. - [ ] `docs/standard-library/text.md`, the status lines of `docs/ROADMAP.md` and `CONCEPT.md` ("Case mapping and character classification read the Unicode Character Database") say what is built; `build/release/torb docs skill docs skills` regenerated and committed (`--check` is a tier A gate). - [ ] Tier A and B green in forge CI. ## Seed impact No new native here (the rows of #372 are known to the seed once it is published), but a std name is added: the declarations are what commit 1 made safe. Nothing under `seed/`; no seed refresh needed after this merge. The compiler looks up none of the new names by string. ## Size One PR, one Conventional Commit: `feat(text): Char.category and case folding`. Part of #366.
Author
Collaborator

[loop:torbscript] refine: body written by the refinement of #366 (API shape decided there: Char.category() with a closed GeneralCategory of 29 cases, toCaseFolded() on Char and String, nextCaseVariant(); goal, criteria, seed impact, size). next: waiting (depends on #372 merged and a published seed containing it)

[loop:torbscript] refine: body written by the refinement of #366 (API shape decided there: `Char.category()` with a closed `GeneralCategory` of 29 cases, `toCaseFolded()` on `Char` and `String`, `nextCaseVariant()`; goal, criteria, seed impact, size). next: waiting (depends on #372 merged and a published seed containing it)
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#373
No description provided.