uri: repaired and gap 9 (rest of URI slice 5) #132

Closed
opened 2026-10-05 21:07:23 +00:00 by orion · 8 comments
Collaborator

Context

Hosts are done; repaired and gap 9 are later.

Source

Acceptance criteria

  • As URI.md slice 5 specifies

Dependencies

None known.

Moved from the planning lists of the docs; the design record stays the specification.

## Context Hosts are done; `repaired` and gap 9 are later. ## Source - [URI.md slice 5](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/URI.md#L1996) ## Acceptance criteria - [ ] As URI.md slice 5 specifies ## Dependencies None known. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), source URI.md slice 5, not a duplicate. No dependencies. next: waiting (refine queued, ready buffer full) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), source URI.md slice 5, not a duplicate. No dependencies. next: waiting (refine queued, ready buffer full) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T07:39:00Z

[loop:torbscript] claim refine by session:loop-torbscript-20261024 until 2026-10-24T07:39:00Z
Author
Collaborator

[loop:torbscript] refine: requirements against docs/design/URI.md (slice 5, sections 6 and 8, gap 9 in section 13) and docs/language/types/data-or-capsule.md rule 6. Read-only analysis, no code.

[loop:torbscript] decided by the loop: one PR, two Conventional Commits, no seed change.

  • Why one PR: part A (repaired) is a pure function in std/uri plus tests and docs, part B (gap 9) is a rule of about 15 lines in compiler/src/semantics/checker/derive.trb plus a test and docs. Together roughly 1.5 h, inside the "one PR, one or two commits" size of the definition of ready. Splitting up front is not wanted while the epic rules say so.
  • Seed: does not touch the seed. repaired is a new std function (no syntax, no native, no driver flag, no std name the compiler looks up by string); gap 9 widens when a capsule derives Decode and nothing the compiler is built from relies on the refusal. So no two-commit seed dance.
  • Gap 9, which of the two fixes: URI.md names "the pair with String wins where there are several" or "a way to name the pair". Decision: String wins. It is the smaller rule, URI.md calls it right in every case in the repository, and a capsule that writes as text is what every format wants. It only turns a refusal into a derivation, so it is not a breaking change. Naming the pair stays open for a capsule with several non-String pairs (message unchanged for those).
  • Not in this issue: making UriReference.tryFrom(Path) infallible. It is still fallible today (a UNC server name IDNA refuses is InvalidHost), so Path does not gain a second pair now; gap 9 only has to be answered before that day, and this issue answers it.
  • repaired semantics (URI.md section 6 names "trimmed, backslashes turned, control characters dropped"; the table row that says a browser drops a NUL is not what WHATWG does, it percent-encodes it, and Uri already encodes it as %00). Decision: repaired does exactly what the WHATWG URL parser does before it parses, so the result is the address-bar case and nothing stricter:
    1. strip leading and trailing C0 controls and space (U+0000 to U+0020);
    2. remove every ASCII tab, line feed and carriage return anywhere in the text;
    3. turn \ into / in the part before the first ? or #, when the scheme is special (http, https, ws, wss, ftp, file, any case) or the text has no scheme; a \ in the query, the fragment or a non-special scheme (git+ssh:, mailto:, urn:) stays.
      Other C0 controls inside the text (a NUL) are left: Uri.tryFrom encodes them as WHATWG does. The doc comment in URI.md section 6 and the NUL row of its table are corrected to this.

Requirements

A. repaired (commit feat(uri): repaired(text) for the WHATWG repairs before parsing)

  • public fn repaired(text: String): String in a new std/uri/src/repaired.trb, public use repaired from "./repaired" in std/uri/src/lib.trb. Not in the prelude (URI.md section 16 q2: behind use ... from "std/uri").
  • Pure, total, idempotent: repaired(repaired(t)) == repaired(t). Works on bytes/code points without allocating per character more than the existing parser does.
  • Doc comment with a TorbScript example (Uri.tryFrom(repaired(userInput))?), per the torbscript skill canon.

B. Gap 9 (commit fix(checker): a capsule's pair with String is the Decode pair)

  • conversionPairOf in derive.trb returns the pair whose Source is String when conversionSourcesOf has several and one of them is String; with several and none String, still None and the message still lists them. derivesThroughPair, the lowering in ir/lower/encoding.trb and the diagnostic notes (conversionSourcesOf users) stay consistent: no case where the checker derives and the lowering sees no pair.
  • docs/language/types/data-or-capsule.md rule 6 and docs/design/URI.md gap 9 (closed, with date) and section 8's "second pair" paragraph describe the rule; docs/standard-library/uri.md documents repaired; URI.md slice 5 state becomes done, section 6's text and table row corrected as above, section 16 q6 untouched.
  • Regenerate skills/ (torb docs skill docs skills), commit with the docs.

Acceptance criteria

  • repaired(" http://example.com/a b ") is "http://example.com/a b"; Uri.tryFrom of it is http://example.com/a%20b.
  • repaired("http:\\\\example.com\\a\\b") is "http://example.com/a/b" (same for https, ws, wss, ftp, file, upper-case scheme).
  • repaired("ht\ttp://exa\nmple.com/\r") is "http://example.com/".
  • repaired("git+ssh://host\\path"), repaired("mailto:a\\b@example.com") and repaired("urn:x:a\\b") keep the backslash; repaired("http://h/a?q=\\x#f\\y") keeps the backslashes after ?.
  • A text with no scheme: repaired("\\\\server\\share") is "//server/share"; repaired("") and repaired(" ") are "".
  • A NUL in the middle stays: Uri.tryFrom(repaired("http://example.com/a\u0000b")) has path /a%00b (table row corrected).
  • Idempotent over all of the above; Uri.tryFrom(repaired("http://münchen.de\\x")) is http://xn--mnchen-3ya.de/x (IDNA from slice 5 part 1 plus repaired).
  • Gap 9: a capsule with TryFrom<String, F>+From<Self> on String and From<Title>/From<Self> on Title derives Decode/Encode through String (compile and decode round trip in a conformance program); a capsule with two non-String pairs still has no Decode and the message still says "converts both ways with A and B".
  • Path and UriReference keep their current pair behaviour (Path decodes from String); nothing in std/ loses or gains a derivation unintentionally (full suite green).

Test list

  • std/uri/tests/repaired.test.trb (new): one test per bullet above (trim, inner tab/newline, backslash for each special scheme and case, non-special scheme, query/fragment, no scheme, empty/blank, NUL, idempotence, the IDNA + backslash combination, repaired + Uri.tryFrom round trip).
  • compiler/tests/implementations.test.trb: the test "two types that convert both ways are two pairs" (line ~650) has String as one pair, so it would now derive. Rework it to two non-String sources (Title and a second type) so the message stays tested, and add: a capsule with String plus another pair derives Decode (no problems), and conversionPairOf picks String.
  • tests/conformance/encoding-*.trb: one program with the String-plus-other-pair capsule encoded and decoded as JSON in both VM and --native (tier B), next to the existing capsule cases.
  • Gates: sh tools/gates.sh a (touches compiler/src, so it bootstraps) and b is not needed unless the lowering in ir/lower/encoding.trb changes; run in forge CI only, per the contract.

Touches / risks

  • Seed: no. Syntax: no. Native: no. Lookup by string in the compiler: no.
  • Files: std/uri/src/repaired.trb, std/uri/src/lib.trb, std/uri/tests/repaired.test.trb, compiler/src/semantics/checker/derive.trb, compiler/tests/implementations.test.trb, one conformance program, docs/design/URI.md, docs/language/types/data-or-capsule.md, docs/standard-library/uri.md, skills/.
  • Risk: a std capsule that has String plus another pair and today relies on having no Decode (none known; CI shows it).
[loop:torbscript] refine: requirements against `docs/design/URI.md` (slice 5, sections 6 and 8, gap 9 in section 13) and `docs/language/types/data-or-capsule.md` rule 6. Read-only analysis, no code. [loop:torbscript] decided by the loop: one PR, two Conventional Commits, no seed change. - Why one PR: part A (`repaired`) is a pure function in `std/uri` plus tests and docs, part B (gap 9) is a rule of about 15 lines in `compiler/src/semantics/checker/derive.trb` plus a test and docs. Together roughly 1.5 h, inside the "one PR, one or two commits" size of the definition of ready. Splitting up front is not wanted while the epic rules say so. - Seed: **does not touch the seed.** `repaired` is a new `std` function (no syntax, no native, no driver flag, no std name the compiler looks up by string); gap 9 widens when a capsule derives `Decode` and nothing the compiler is built from relies on the refusal. So no two-commit seed dance. - Gap 9, which of the two fixes: URI.md names "the pair with `String` wins where there are several" or "a way to name the pair". Decision: **`String` wins.** It is the smaller rule, URI.md calls it right in every case in the repository, and a capsule that writes as text is what every format wants. It only turns a refusal into a derivation, so it is not a breaking change. Naming the pair stays open for a capsule with several non-`String` pairs (message unchanged for those). - Not in this issue: making `UriReference.tryFrom(Path)` infallible. It is still fallible today (a UNC server name IDNA refuses is `InvalidHost`), so `Path` does not gain a second pair now; gap 9 only has to be answered before that day, and this issue answers it. - `repaired` semantics (URI.md section 6 names "trimmed, backslashes turned, control characters dropped"; the table row that says a browser drops a NUL is not what WHATWG does, it percent-encodes it, and `Uri` already encodes it as `%00`). Decision: `repaired` does exactly what the WHATWG URL parser does *before* it parses, so the result is the address-bar case and nothing stricter: 1. strip leading and trailing C0 controls and space (U+0000 to U+0020); 2. remove every ASCII tab, line feed and carriage return anywhere in the text; 3. turn `\` into `/` in the part before the first `?` or `#`, when the scheme is special (`http`, `https`, `ws`, `wss`, `ftp`, `file`, any case) or the text has no scheme; a `\` in the query, the fragment or a non-special scheme (`git+ssh:`, `mailto:`, `urn:`) stays. Other C0 controls inside the text (a NUL) are left: `Uri.tryFrom` encodes them as WHATWG does. The doc comment in URI.md section 6 and the NUL row of its table are corrected to this. ## Requirements **A. `repaired`** (commit `feat(uri): repaired(text) for the WHATWG repairs before parsing`) - `public fn repaired(text: String): String` in a new `std/uri/src/repaired.trb`, `public use repaired from "./repaired"` in `std/uri/src/lib.trb`. Not in the prelude (URI.md section 16 q2: behind `use ... from "std/uri"`). - Pure, total, idempotent: `repaired(repaired(t)) == repaired(t)`. Works on bytes/code points without allocating per character more than the existing parser does. - Doc comment with a TorbScript example (`Uri.tryFrom(repaired(userInput))?`), per the `torbscript` skill canon. **B. Gap 9** (commit `fix(checker): a capsule's pair with String is the Decode pair`) - `conversionPairOf` in `derive.trb` returns the pair whose `Source` is `String` when `conversionSourcesOf` has several and one of them is `String`; with several and none `String`, still `None` and the message still lists them. `derivesThroughPair`, the lowering in `ir/lower/encoding.trb` and the diagnostic notes (`conversionSourcesOf` users) stay consistent: no case where the checker derives and the lowering sees no pair. - `docs/language/types/data-or-capsule.md` rule 6 and `docs/design/URI.md` gap 9 (closed, with date) and section 8's "second pair" paragraph describe the rule; `docs/standard-library/uri.md` documents `repaired`; URI.md slice 5 state becomes **done**, section 6's text and table row corrected as above, section 16 q6 untouched. - Regenerate `skills/` (`torb docs skill docs skills`), commit with the docs. ## Acceptance criteria - [ ] `repaired(" http://example.com/a b ")` is `"http://example.com/a b"`; `Uri.tryFrom` of it is `http://example.com/a%20b`. - [ ] `repaired("http:\\\\example.com\\a\\b")` is `"http://example.com/a/b"` (same for `https`, `ws`, `wss`, `ftp`, `file`, upper-case scheme). - [ ] `repaired("ht\ttp://exa\nmple.com/\r")` is `"http://example.com/"`. - [ ] `repaired("git+ssh://host\\path")`, `repaired("mailto:a\\b@example.com")` and `repaired("urn:x:a\\b")` keep the backslash; `repaired("http://h/a?q=\\x#f\\y")` keeps the backslashes after `?`. - [ ] A text with no scheme: `repaired("\\\\server\\share")` is `"//server/share"`; `repaired("")` and `repaired(" ")` are `""`. - [ ] A NUL in the middle stays: `Uri.tryFrom(repaired("http://example.com/a\u0000b"))` has path `/a%00b` (table row corrected). - [ ] Idempotent over all of the above; `Uri.tryFrom(repaired("http://münchen.de\\x"))` is `http://xn--mnchen-3ya.de/x` (IDNA from slice 5 part 1 plus `repaired`). - [ ] Gap 9: a capsule with `TryFrom<String, F>`+`From<Self>` on `String` **and** `From<Title>`/`From<Self>` on `Title` derives `Decode`/`Encode` through `String` (compile and decode round trip in a conformance program); a capsule with two non-`String` pairs still has no `Decode` and the message still says "converts both ways with `A` and `B`". - [ ] `Path` and `UriReference` keep their current pair behaviour (`Path` decodes from `String`); nothing in `std/` loses or gains a derivation unintentionally (full suite green). ## Test list - `std/uri/tests/repaired.test.trb` (new): one `test` per bullet above (trim, inner tab/newline, backslash for each special scheme and case, non-special scheme, query/fragment, no scheme, empty/blank, NUL, idempotence, the IDNA + backslash combination, repaired + `Uri.tryFrom` round trip). - `compiler/tests/implementations.test.trb`: the test "two types that convert both ways are two pairs" (line ~650) has `String` as one pair, so it would now derive. Rework it to two non-`String` sources (`Title` and a second type) so the message stays tested, and add: a capsule with `String` plus another pair derives `Decode` (no problems), and `conversionPairOf` picks `String`. - `tests/conformance/encoding-*.trb`: one program with the `String`-plus-other-pair capsule encoded and decoded as JSON in both VM and `--native` (tier B), next to the existing capsule cases. - Gates: `sh tools/gates.sh a` (touches `compiler/src`, so it bootstraps) and `b` is not needed unless the lowering in `ir/lower/encoding.trb` changes; run in forge CI only, per the contract. ## Touches / risks - Seed: no. Syntax: no. Native: no. Lookup by string in the compiler: no. - Files: `std/uri/src/repaired.trb`, `std/uri/src/lib.trb`, `std/uri/tests/repaired.test.trb`, `compiler/src/semantics/checker/derive.trb`, `compiler/tests/implementations.test.trb`, one conformance program, `docs/design/URI.md`, `docs/language/types/data-or-capsule.md`, `docs/standard-library/uri.md`, `skills/`. - Risk: a std capsule that has `String` plus another pair and today relies on having *no* `Decode` (none known; CI shows it).
Author
Collaborator

[loop:torbscript] refine: requirements, acceptance criteria and test list added above, decided by the loop (one PR, two commits, no seed change, String wins for gap 9), label ready set, next: implement

[loop:torbscript] refine: requirements, acceptance criteria and test list added above, decided by the loop (one PR, two commits, no seed change, String wins for gap 9), label `ready` set, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T07:48:27Z

[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T07:48:27Z
Author
Collaborator

[loop:torbscript] implement: checkpoint (session context limit), branch loop/132-uri-repaired pushed at 1a8c6ed1, no PR yet, next: continue implement.
Done: part A function std/uri/src/repaired.trb (trim U+0000..0020, drop tab/LF/CR, \ to / before first ?/# for special or no scheme) and public use repaired from "./repaired" in std/uri/src/lib.trb, as commit 1 feat(uri): repaired(text) for the WHATWG repairs before parsing. Not compiled (no toolchain on the box): CI-verified only.
Remaining: (1) std/uri/tests/repaired.test.trb per the test list (NUL literal is \u{0}, not \u0000); (2) part B in compiler/src/semantics/checker/derive.trb (conversionPairOf picks the String pair), compiler/tests/implementations.test.trb rework + new tests, conformance program; (3) docs: URI.md (slice 5 done, section 6 text/NUL row, gap 9, section 8), data-or-capsule.md rule 6, docs/standard-library/uri.md (document repaired, add std/uri/src/repaired.trb to source), regenerate skills/; commit 2 fix(checker): a capsule's pair with String is the Decode pair (tests and docs of A may go into commit 1 by amend/rebase); (4) push, open PR Closes #132, comment, ws wt rm. Worktree: code/wt/torbscript-language-132 of the orion workspace.

[loop:torbscript] implement: checkpoint (session context limit), branch `loop/132-uri-repaired` pushed at 1a8c6ed1, no PR yet, next: continue implement. Done: part A function `std/uri/src/repaired.trb` (trim U+0000..0020, drop tab/LF/CR, `\` to `/` before first `?`/`#` for special or no scheme) and `public use repaired from "./repaired"` in `std/uri/src/lib.trb`, as commit 1 `feat(uri): repaired(text) for the WHATWG repairs before parsing`. Not compiled (no toolchain on the box): CI-verified only. Remaining: (1) `std/uri/tests/repaired.test.trb` per the test list (NUL literal is `\u{0}`, not `\u0000`); (2) part B in `compiler/src/semantics/checker/derive.trb` (`conversionPairOf` picks the `String` pair), `compiler/tests/implementations.test.trb` rework + new tests, conformance program; (3) docs: URI.md (slice 5 done, section 6 text/NUL row, gap 9, section 8), data-or-capsule.md rule 6, `docs/standard-library/uri.md` (document `repaired`, add `std/uri/src/repaired.trb` to source), regenerate `skills/`; commit 2 `fix(checker): a capsule's pair with String is the Decode pair` (tests and docs of A may go into commit 1 by amend/rebase); (4) push, open PR `Closes #132`, comment, `ws wt rm`. Worktree: `code/wt/torbscript-language-132` of the orion workspace.
Author
Collaborator

[loop:torbscript] implement: checkpoint 2 (session context limit), branch loop/132-uri-repaired pushed at e048334c, no PR yet, next: continue implement.
Done (all CI-verified only, no toolchain on the box): std/uri/tests/repaired.test.trb (commit 24b52305, a fixup! of 1a8c6ed1); commit e048334c fix(checker): a capsule's pair with String is the Decode pair = conversionPairOf picks the String pair (and the notes use it), the reworked + new tests in compiler/tests/implementations.test.trb, conformance program tests/conformance/encoding-capsule-text-pair.trb with .exit and .allocations = unbudgeted: ... (count not measured; PR should say so).
Remaining: (1) docs: docs/design/URI.md (slice 5 done, section 6 text and NUL table row, gap 9 closed with date 2026-10-24, section 8 "second pair" paragraph, the Slug example at ~line 133 if it changes), docs/language/types/data-or-capsule.md rule 5/6 (String wins), docs/standard-library/uri.md (document repaired, add std/uri/src/repaired.trb to its source list), maybe docs/standard-library/encoding.md ("one conversion pair"); regenerate skills/ (torb docs skill docs skills, no toolchain: hand-edit the matching skills/** copies exactly as the generator would, or leave to CI --check and say so); put docs in commit 2. (2) git rebase -i --autosquash origin/main-style squash of 24b52305 into 1a8c6ed1 (git rebase --autosquash 5d7f7903), then git push --force-with-lease. (3) open PR Closes #132, comment, ws wt rm torbscript-language-132.

[loop:torbscript] implement: checkpoint 2 (session context limit), branch `loop/132-uri-repaired` pushed at e048334c, no PR yet, next: continue implement. Done (all CI-verified only, no toolchain on the box): `std/uri/tests/repaired.test.trb` (commit 24b52305, a `fixup!` of 1a8c6ed1); commit e048334c `fix(checker): a capsule's pair with String is the Decode pair` = `conversionPairOf` picks the `String` pair (and the notes use it), the reworked + new tests in `compiler/tests/implementations.test.trb`, conformance program `tests/conformance/encoding-capsule-text-pair.trb` with `.exit` and `.allocations` = `unbudgeted: ...` (count not measured; PR should say so). Remaining: (1) docs: `docs/design/URI.md` (slice 5 done, section 6 text and NUL table row, gap 9 closed with date 2026-10-24, section 8 "second pair" paragraph, the `Slug` example at ~line 133 if it changes), `docs/language/types/data-or-capsule.md` rule 5/6 (String wins), `docs/standard-library/uri.md` (document `repaired`, add `std/uri/src/repaired.trb` to its source list), maybe `docs/standard-library/encoding.md` ("one conversion pair"); regenerate `skills/` (`torb docs skill docs skills`, no toolchain: hand-edit the matching `skills/**` copies exactly as the generator would, or leave to CI `--check` and say so); put docs in commit 2. (2) `git rebase -i --autosquash origin/main`-style squash of 24b52305 into 1a8c6ed1 (`git rebase --autosquash 5d7f7903`), then `git push --force-with-lease`. (3) open PR `Closes #132`, comment, `ws wt rm torbscript-language-132`.
Author
Collaborator

[loop:torbscript] implement: PR 465 (6872007a), next: waiting (forge CI of 6872007a)

[loop:torbscript] implement: PR 465 (6872007a), next: waiting (forge CI of 6872007a)
argus closed this issue 2026-10-24 10:55:14 +00:00
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#132
No description provided.