text: normalization data and natives, teach the compiler (commit 1 of 2 for NFC and NFKC) #362

Closed
opened 2026-10-13 22:53:26 +00:00 by orion · 5 comments
Collaborator

Context

Commit 1 of 2 for normalization in std/text (the declaration is #363). CLAUDE.md "Seed and breaking changes": a new native the std declares has to be known to the seed first. This issue builds the normalization data, the runtime functions and the natives rows, and declares nothing in std/; the declaration waits for a published seed that contains this merge (as #299 / #54 did for Buffer).

Part of the chain of #36 (full Unicode tables). Source: FIRST-PARTY.md gap 23 (NFKC for SASLprep), DNS.md section 3 ("NFC normalization"), ROADMAP "the full Unicode tables".

Depends on

#36 (generator, pinned Unicode 17.0.0 data, tools/fetch-unicode.sh).

Decisions (refinement)

  • Four forms, all with the same runtime entry shape torb_text torb_text_to_nfc(torb_text) (also _nfd, _nfkc, _nfkd); natives String.toNfc, String.toNfd, String.toNfkc, String.toNfkd (naming follows toUpperCase; acronyms are written Nfc like Uri, Dns, Tls). They are methods of String, no free functions and no form: parameter.
  • Data: Canonical_Combining_Class, the canonical and compatibility decomposition mappings (UnicodeData.txt fields 3 and 5), Full_Composition_Exclusion (DerivedNormalizationProps.txt); Hangul syllables are computed, not tabled. Generator inputs added to tools/unicode/SHA256SUMS.
  • Algorithm: UAX #15 as written (decompose recursively with a per-text buffer, canonical ordering by a stable insertion sort of runs of non-zero class, then composition for NFC/NFKC). A quick check: an ASCII-only text is returned as it is (the same storage, no copy), and so is any text whose every code point has NFC_QC=Yes/ccc=0 - the generator adds a quick-check bit for this.
  • Layout: two-stage index to a record {ccc, flags, decomposition offset}, decomposition strings in one uint32_t pool, composition as a sorted pair table with binary search. Budget: all normalization data at most 100 KiB; over budget the implementer compacts (pool deduplication, 16-bit pool entries for the BMP) before anything else.

Acceptance criteria

  • The generator emits the normalization tables into runtime/unicode_data.c (or a second generated unit, runtime/unicode_normalization.c, if the size rule of #36 asks for an on-demand object); reproducible as in #36.
  • torb_text_to_nfc/nfd/nfkc/nfkd in runtime/, with ready rows in compiler/src/backend/c/natives.trb for String.toNfc etc. (ownership like String.toUpperCase); runtime/include/torb_natives.h and runtime/machine_natives.c regenerated; the VM calls the same functions.
  • A runtime C test (runtime/tests/) runs the whole NormalizationTest.txt of the pinned version (all four forms, parts 0 to 3, the invariants of the file's header) when the file is present - CI fetches it with tools/fetch-unicode.sh - and a committed excerpt of at least the edge cases (Hangul, ccc reordering, singletons, compatibility, astral) when it is not.
  • A test in compiler/tests/natives.test.trb that each of the four rows exists, is ready and names its symbol; nativeSymbols() of the VM lists them.
  • Idempotence property over the test file: nf(nf(x)) == nf(x); NFC of an ASCII text returns the same storage (a runtime test on the pointer).
  • Size, measured in forge CI and stated (native release hello-world before and after; bytes of the normalization data from the object); the numbers go into runtime/README.md and the PR body. The rule of #36 applies: a program that does not use them must not grow by more than 4 KiB.
  • No change under std/, seed/, tests/conformance/. Tier A green; tier B green (runtime touched).
  • After the merge the seed with this commit is published (forge Actions -> seed, or the nightly). Not part of this PR; #363 stays not ready until seeds.txt lists a seed at or after the merge commit.

Seed impact

Yes: four new natives. This is commit 1 of the two-commit rule; the std declaration is the next issue.

Size

One PR, two Conventional Commits: feat(text): normalization data in the generated Unicode tables and feat(runtime): NFC, NFD, NFKC and NFKD on texts, and their natives rows.

## Context Commit 1 of 2 for normalization in `std/text` (the declaration is #363). `CLAUDE.md` "Seed and breaking changes": a new native the std declares has to be known to the seed first. This issue builds the normalization data, the runtime functions and the natives rows, and **declares nothing in `std/`**; the declaration waits for a published seed that contains this merge (as #299 / #54 did for `Buffer`). Part of the chain of #36 (full Unicode tables). Source: [FIRST-PARTY.md gap 23](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/FIRST-PARTY.md#L283) (NFKC for SASLprep), [DNS.md section 3](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/DNS.md#L119) ("NFC normalization"), ROADMAP "the full Unicode tables". ## Depends on #36 (generator, pinned Unicode 17.0.0 data, `tools/fetch-unicode.sh`). ## Decisions (refinement) - Four forms, all with the same runtime entry shape `torb_text torb_text_to_nfc(torb_text)` (also `_nfd`, `_nfkc`, `_nfkd`); natives `String.toNfc`, `String.toNfd`, `String.toNfkc`, `String.toNfkd` (naming follows `toUpperCase`; acronyms are written `Nfc` like `Uri`, `Dns`, `Tls`). They are methods of `String`, no free functions and no `form:` parameter. - Data: `Canonical_Combining_Class`, the canonical and compatibility decomposition mappings (`UnicodeData.txt` fields 3 and 5), `Full_Composition_Exclusion` (`DerivedNormalizationProps.txt`); Hangul syllables are computed, not tabled. Generator inputs added to `tools/unicode/SHA256SUMS`. - Algorithm: UAX #15 as written (decompose recursively with a per-text buffer, canonical ordering by a stable insertion sort of runs of non-zero class, then composition for NFC/NFKC). A **quick check**: an ASCII-only text is returned as it is (the same storage, no copy), and so is any text whose every code point has `NFC_QC=Yes`/`ccc=0` - the generator adds a `quick-check` bit for this. - Layout: two-stage index to a record `{ccc, flags, decomposition offset}`, decomposition strings in one `uint32_t` pool, composition as a sorted pair table with binary search. Budget: all normalization data at most **100 KiB**; over budget the implementer compacts (pool deduplication, 16-bit pool entries for the BMP) before anything else. ## Acceptance criteria - [ ] The generator emits the normalization tables into `runtime/unicode_data.c` (or a second generated unit, `runtime/unicode_normalization.c`, if the size rule of #36 asks for an on-demand object); reproducible as in #36. - [ ] `torb_text_to_nfc/nfd/nfkc/nfkd` in `runtime/`, with ready rows in `compiler/src/backend/c/natives.trb` for `String.toNfc` etc. (ownership like `String.toUpperCase`); `runtime/include/torb_natives.h` and `runtime/machine_natives.c` regenerated; the VM calls the same functions. - [ ] A runtime C test (`runtime/tests/`) runs the **whole `NormalizationTest.txt`** of the pinned version (all four forms, parts 0 to 3, the invariants of the file's header) when the file is present - CI fetches it with `tools/fetch-unicode.sh` - and a committed excerpt of at least the edge cases (Hangul, ccc reordering, singletons, compatibility, astral) when it is not. - [ ] A test in `compiler/tests/natives.test.trb` that each of the four rows exists, is ready and names its symbol; `nativeSymbols()` of the VM lists them. - [ ] Idempotence property over the test file: `nf(nf(x)) == nf(x)`; NFC of an ASCII text returns the same storage (a runtime test on the pointer). - [ ] **Size, measured in forge CI and stated** (native release hello-world before and after; bytes of the normalization data from the object); the numbers go into `runtime/README.md` and the PR body. The rule of #36 applies: a program that does not use them must not grow by more than 4 KiB. - [ ] No change under `std/`, `seed/`, `tests/conformance/`. Tier A green; tier B green (runtime touched). - [ ] After the merge the seed with this commit is published (forge Actions -> `seed`, or the nightly). **Not part of this PR**; #363 stays not `ready` until `seeds.txt` lists a seed at or after the merge commit. ## Seed impact Yes: four new natives. This is commit 1 of the two-commit rule; the std declaration is the next issue. ## Size One PR, two Conventional Commits: `feat(text): normalization data in the generated Unicode tables` and `feat(runtime): NFC, NFD, NFKC and NFKD on texts, and their natives rows`.
Author
Collaborator

[loop:torbscript] refine: body written by the refinement of #36 (goal, criteria, decisions, seed impact, size are in it). next: waiting (depends on #36 being merged)

[loop:torbscript] refine: body written by the refinement of #36 (goal, criteria, decisions, seed impact, size are in it). next: waiting (depends on #36 being merged)
Author
Collaborator

[loop:torbscript] refine: #36 landed (c3eee74a31); the body meets the definition of ready, label ready set. next: implement

[loop:torbscript] refine: #36 landed (https://git.torb.dev/torbscript/language/commit/c3eee74a3123d9cd8030449fbc076666b1d93d23); the body meets the definition of ready, label `ready` set. next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261015 until 2026-10-15T05:52Z

[loop:torbscript] claim implement by session:loop-torbscript-20261015 until 2026-10-15T05:52Z
Author
Collaborator

[loop:torbscript] implement: PR #369 (two commits, Closes #362), Argus requested; CI green on c4e52a6f (tier A/B, gates, whole NormalizationTest.txt: 302 runtime tests, 0 failed); hello-world release +192 bytes (bound 4096), normalization data 86,946 bytes (budget 100 KiB). Tokens: ~1.2M over the whole implement run. next: review

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/369 (two commits, Closes #362), Argus requested; CI green on c4e52a6f (tier A/B, gates, whole NormalizationTest.txt: 302 runtime tests, 0 failed); hello-world release +192 bytes (bound 4096), normalization data 86,946 bytes (budget 100 KiB). Tokens: ~1.2M over the whole implement run. next: review
argus closed this issue 2026-10-15 01:18:15 +00:00
Author
Collaborator

[loop:torbscript] close: landed by the merge gate as e14d7addbf (PR #369); issue closed. #363 stays not ready until a published seed contains this commit. next: done

[loop:torbscript] close: landed by the merge gate as https://git.torb.dev/torbscript/language/commit/e14d7addbf2f2616a594d7535742eae562cb58f5 (PR https://git.torb.dev/torbscript/language/pulls/369); issue closed. #363 stays not ready until a published seed contains this commit. next: done
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#362
No description provided.