regex: Unicode categories for \p{...} and case folding from the tables (slice of #366) #374

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

Context

Part of #366 (declarations: #373). std/regex keeps hand tables in std/regex/src/characters.trb: \p{...} knows Any, L, Lu, Ll, N, Nd, Z, Zs, Latin, Greek and Cyrillic, and (?i) folds by foldOrbit, a hand list over ASCII, Latin-1, Latin Extended-A, Greek and Cyrillic (TEXT-FORMATS.md, "Unicode as far as the tables reach"). With Char.category() and Char.nextCaseVariant() it reads the real data.

Depends on

The std/text declarations of #373 (Char.category(), GeneralCategory, Char.nextCaseVariant()), merged. A published seed is not needed beyond what that issue needed: this change is std only.

Decisions (refinement)

  • \p{X} takes the Unicode abbreviations (Lu, Nd, Zs, ...) and the seven groups L, M, N, P, S, Z, C, with \P{X} and the negations the parser accepts today; no long names, as in RE2. Names and groups follow RE2: C is Cc, Cf, Co and Cs, and \p{Cs} is accepted and matches nothing because a Char is never a surrogate. Cn (unassigned) is an accepted extension that RE2 lacks and is not part of C, so a pattern ported from RE2 means the same.
  • \d, \w, \s, \b and the POSIX classes stay ASCII, as in RE2; Any stays.
  • The scripts Latin, Greek and Cyrillic keep their hand tables: script data is not in the pinned input set of the tables (Scripts.txt arrives with #364 for five scripts only). They are the only hand tables left, and the docs say so.
  • (?i) is the simple case folding orbit of every code point, Char.nextCaseVariant() followed until it returns to the start, and no longer bounded by a foldLimit. (?i)\p{Lu} matches a, as in RE2.
  • How a category class is represented is decided with numbers, not by taste: the two candidates are (a) ranges built by scanning code points with Char.category() (about 1.1 million lookups per class, probably too slow in the VM), and (b) a category mask on CharacterSet checked at match time, with (?i) as a flag that tests the orbit of the input character at match time instead of adding the partners of every range at compile time. The recommendation is (b); the implementer measures both in forge CI and states the numbers. Budget set here: compiling a pattern with \p{L} takes at most 1 s in the VM and at most 50 ms natively, and no regex test grows by more than 10% of its time because of the tables.

Acceptance criteria

  • unicodeSet answers every abbreviation and group above through Char.category(); letterSet, upperSet, lowerSet, decimalSet, numberSet, the Z set and foldOrbit with its hand data are deleted from std/regex/src/characters.trb.
  • New regex tests (VM and native): \p{Lu}, \p{Ll}, \p{Nd} on Arabic-Indic and fullwidth digits, \p{Pd}, \p{Sc} on $, € and ₿, \p{M} on a combining mark, \p{L} on Hebrew, CJK and Hangul text, \P{L}, [^\p{L}\d], \p{Cs} matching nothing, \p{Cn} on U+0378, an unknown name still refused with the same error; (?i) on Armenian, Cherokee, Georgian, Dž, Σσς, the Kelvin sign, and (?i)[a-z] matching K Kelvin sign as today.
  • RE2's search tests (std/regex/tests/search-cases.trb) still pass in full (1808 of 1808, as TEXT-FORMATS.md states); the other regex tests pass unchanged.
  • The measured numbers of the decision above are in the PR, and the decision is recorded in a comment of this issue by the implementer if it picks (a).
  • docs/design/TEXT-FORMATS.md ("Unicode as far as the tables reach") and docs/standard-library/ regex page say which names and folding are supported; the skills are regenerated (torb docs skill docs skills).
  • Tier A and B green in forge CI.

Seed impact

None: std/regex only, no native, no name the compiler looks up by string.

Size

One PR, one Conventional Commit: feat(regex): Unicode categories and case folding from the tables. If the mask representation grows CharacterSet beyond a reviewable diff, two commits: refactor(regex): sets with a category mask and the switch.

## Context Part of #366 (declarations: #373). `std/regex` keeps hand tables in `std/regex/src/characters.trb`: `\p{...}` knows `Any`, `L`, `Lu`, `Ll`, `N`, `Nd`, `Z`, `Zs`, `Latin`, `Greek` and `Cyrillic`, and `(?i)` folds by `foldOrbit`, a hand list over ASCII, Latin-1, Latin Extended-A, Greek and Cyrillic ([TEXT-FORMATS.md, "Unicode as far as the tables reach"](https://git.torb.dev/torbscript/language/src/commit/740a15aad627ea4ff37f857a5a68d780c2b56aed/docs/design/TEXT-FORMATS.md#L111)). With `Char.category()` and `Char.nextCaseVariant()` it reads the real data. ## Depends on The `std/text` declarations of #373 (`Char.category()`, `GeneralCategory`, `Char.nextCaseVariant()`), **merged**. A published seed is not needed beyond what that issue needed: this change is std only. ## Decisions (refinement) - **`\p{X}` takes the Unicode abbreviations** (`Lu`, `Nd`, `Zs`, ...) and the seven groups `L`, `M`, `N`, `P`, `S`, `Z`, `C`, with `\P{X}` and the negations the parser accepts today; no long names, as in RE2. Names and groups follow RE2: `C` is `Cc`, `Cf`, `Co` and `Cs`, and `\p{Cs}` is accepted and matches nothing because a `Char` is never a surrogate. `Cn` (unassigned) is an accepted extension that RE2 lacks and is not part of `C`, so a pattern ported from RE2 means the same. - **`\d`, `\w`, `\s`, `\b` and the POSIX classes stay ASCII**, as in RE2; `Any` stays. - **The scripts `Latin`, `Greek` and `Cyrillic` keep their hand tables**: script data is not in the pinned input set of the tables (`Scripts.txt` arrives with #364 for five scripts only). They are the only hand tables left, and the docs say so. - **`(?i)` is the simple case folding orbit of every code point**, `Char.nextCaseVariant()` followed until it returns to the start, and no longer bounded by a `foldLimit`. `(?i)\p{Lu}` matches `a`, as in RE2. - **How a category class is represented is decided with numbers**, not by taste: the two candidates are (a) ranges built by scanning code points with `Char.category()` (about 1.1 million lookups per class, probably too slow in the VM), and (b) a category mask on `CharacterSet` checked at match time, with `(?i)` as a flag that tests the orbit of the input character at match time instead of adding the partners of every range at compile time. The recommendation is (b); the implementer measures both in forge CI and states the numbers. **Budget set here**: compiling a pattern with `\p{L}` takes at most 1 s in the VM and at most 50 ms natively, and no regex test grows by more than 10% of its time because of the tables. ## Acceptance criteria - [ ] `unicodeSet` answers every abbreviation and group above through `Char.category()`; `letterSet`, `upperSet`, `lowerSet`, `decimalSet`, `numberSet`, the `Z` set and `foldOrbit` with its hand data are deleted from `std/regex/src/characters.trb`. - [ ] New regex tests (VM and native): `\p{Lu}`, `\p{Ll}`, `\p{Nd}` on Arabic-Indic and fullwidth digits, `\p{Pd}`, `\p{Sc}` on `$`, `€` and `₿`, `\p{M}` on a combining mark, `\p{L}` on Hebrew, CJK and Hangul text, `\P{L}`, `[^\p{L}\d]`, `\p{Cs}` matching nothing, `\p{Cn}` on U+0378, an unknown name still refused with the same error; `(?i)` on Armenian, Cherokee, Georgian, `Dž`, `Σσς`, the Kelvin sign, and `(?i)[a-z]` matching `K` Kelvin sign as today. - [ ] RE2's search tests (`std/regex/tests/search-cases.trb`) still pass in full (1808 of 1808, as `TEXT-FORMATS.md` states); the other regex tests pass unchanged. - [ ] The measured numbers of the decision above are in the PR, and the decision is recorded in a comment of this issue by the implementer if it picks (a). - [ ] `docs/design/TEXT-FORMATS.md` ("Unicode as far as the tables reach") and `docs/standard-library/` regex page say which names and folding are supported; the skills are regenerated (`torb docs skill docs skills`). - [ ] Tier A and B green in forge CI. ## Seed impact None: `std/regex` only, no native, no name the compiler looks up by string. ## Size One PR, one Conventional Commit: `feat(regex): Unicode categories and case folding from the tables`. If the mask representation grows `CharacterSet` beyond a reviewable diff, two commits: `refactor(regex): sets with a category mask` and the switch.
Author
Collaborator

[loop:torbscript] refine: body written by the refinement of #366 (decisions: RE2 names and groups, scripts keep their tables, (?i) by case orbit, representation chosen by measurement with a stated budget). next: waiting (depends on #373 merged)

[loop:torbscript] refine: body written by the refinement of #366 (decisions: RE2 names and groups, scripts keep their tables, `(?i)` by case orbit, representation chosen by measurement with a stated budget). next: waiting (depends on #373 merged)
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#374
No description provided.