Skip to content

Re-land the RcStr carrier: the interpreter's string index reads a carried ASCII fact instead of testing per call - #9256

Merged
briansrls merged 4 commits into
mainfrom
session/sunny-cat-490
Aug 26, 2026
Merged

briansrls merged 4 commits into
mainfrom
session/sunny-cat-490

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

#9212 deleted the _ascii_aware triplet in v1_rt because its precomputed
is_ascii parameter had no producer anywhere in main's reachable history --
the doc comment named "the RcStr carrier fact" and no RcStr existed. It
closed with a declared residual: a single call cannot be O(1) without the
whole-string ASCII fact, that fact needs a carrier on the string value, and
that carrier is the class's next-rung trigger. This lands the carrier, so the
parameter's absent producer is not re-created -- the fact lives on the value.

v1_rt::RcStr (authored at src/v1/runtime_rust.dag rt_string_carrier) is
Rc<str> plus the ASCII flag, computed once in RcStr::new and read by
RcStr::char_at / substring / string_length. The flag has exactly one
producer and a private field, so "carrier says ASCII, content is not" has no
constructor (DESIGN §4b: structurally impossible, not validated). The free
char_at/substring/string_length stay exactly as #9212 left them -- they
are the entry points for emitted code holding a bare &str, which has no fact
to read and must test; the carrier methods fall back to them whenever the flag
is false, so semantics are theirs by construction.

v1_interpreter.rs carries Value::Str(RcStr) and routes every string
position primitive through it: s[i] (eval_index), s[a..b]
(eval_slice), .char_at(), .substring(), char_at(), substring(),
string_length(), length() and native_len -- eight arms plus the length
helper, over three primitives. expect_value_str hands those arms the carrier
itself rather than an owned String, so a read-only index no longer pays an
O(n) .to_string() either.

WHAT THE COST CHANGE IS, at the honest grain: a single ASCII index goes from
O(min(pos, n)) to O(1), so a left-to-right walk over an ASCII string goes from
O(n^2) to O(n). It is NOT constant-time for non-ASCII text -- the carrier
answers only "is the byte offset the code-point offset", and where it is not,
the walk is the same chars() walk as before. The remaining residual is the
re-index-from-zero shape itself; a cursor surface is its next rung, and the
char_at doc comment now says that instead of naming a carrier that does not
exist.

Evidence, by execution (remote, cargo test -p v1-compiler --lib):
v1_interpreter::rc_str_carrier_tests -- 5 passed, 0 failed. Four of them are
differential against the free functions the carrier shadows (the pre-carrier
semantics), over every index of a 5-byte/4-code-point string, so a carrier that
took the byte path over multibyte text disagrees at the first non-ASCII code
point. The fifth states that string's code-point answers absolutely, so the
control survives a change to the free functions.

The floor witness test.claim.char_at_unicode_witness now runs through the
carrier: its char_at/string_length calls reach free_call.char_at /
free_call.string_length, which are two of the arms rerouted here, and its
RED (a byte-offset implementation returning U+00A9 at index 1 of "a" U+00E9
"b") is unchanged and still authorable.

…ried ASCII fact instead of testing per call

#9212 deleted the `_ascii_aware` triplet in `v1_rt` because its precomputed
`is_ascii` parameter had no producer anywhere in main's reachable history --
the doc comment named "the `RcStr` carrier fact" and no `RcStr` existed. It
closed with a declared residual: a single call cannot be O(1) without the
whole-string ASCII fact, that fact needs a carrier on the string value, and
that carrier is the class's next-rung trigger. This lands the carrier, so the
parameter's absent producer is not re-created -- the fact lives on the value.

`v1_rt::RcStr` (authored at `src/v1/runtime_rust.dag` `rt_string_carrier`) is
`Rc<str>` plus the ASCII flag, computed once in `RcStr::new` and read by
`RcStr::char_at` / `substring` / `string_length`. The flag has exactly one
producer and a private field, so "carrier says ASCII, content is not" has no
constructor (DESIGN §4b: structurally impossible, not validated). The free
`char_at`/`substring`/`string_length` stay exactly as #9212 left them -- they
are the entry points for emitted code holding a bare `&str`, which has no fact
to read and must test; the carrier methods fall back to them whenever the flag
is false, so semantics are theirs by construction.

`v1_interpreter.rs` carries `Value::Str(RcStr)` and routes every string
position primitive through it: `s[i]` (`eval_index`), `s[a..b]`
(`eval_slice`), `.char_at()`, `.substring()`, `char_at()`, `substring()`,
`string_length()`, `length()` and `native_len` -- eight arms plus the length
helper, over three primitives. `expect_value_str` hands those arms the carrier
itself rather than an owned `String`, so a read-only index no longer pays an
O(n) `.to_string()` either.

WHAT THE COST CHANGE IS, at the honest grain: a single ASCII index goes from
O(min(pos, n)) to O(1), so a left-to-right walk over an ASCII string goes from
O(n^2) to O(n). It is NOT constant-time for non-ASCII text -- the carrier
answers only "is the byte offset the code-point offset", and where it is not,
the walk is the same `chars()` walk as before. The remaining residual is the
re-index-from-zero shape itself; a cursor surface is its next rung, and the
`char_at` doc comment now says that instead of naming a carrier that does not
exist.

Evidence, by execution (remote, `cargo test -p v1-compiler --lib`):
`v1_interpreter::rc_str_carrier_tests` -- 5 passed, 0 failed. Four of them are
differential against the free functions the carrier shadows (the pre-carrier
semantics), over every index of a 5-byte/4-code-point string, so a carrier that
took the byte path over multibyte text disagrees at the first non-ASCII code
point. The fifth states that string's code-point answers absolutely, so the
control survives a change to the free functions.

The floor witness `test.claim.char_at_unicode_witness` now runs through the
carrier: its `char_at`/`string_length` calls reach `free_call.char_at` /
`free_call.string_length`, which are two of the arms rerouted here, and its
RED (a byte-offset implementation returning U+00A9 at index 1 of "a" U+00E9
"b") is unchanged and still authorable.
@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

PERTURBATION RECEIPT — the four differential tests are discriminating by execution, not by name.

Perturbation: both ASCII guards in RcStr::char_at / RcStr::substring replaced by if false and RcStr::string_length's by if true, so the byte path is taken unconditionally. Rebuilt remotely from that source, cargo test -p v1-compiler --lib rc_str_carrier_tests:

test result: FAILED. 1 passed; 4 failed; 0 ignored; 612 filtered out
  • carrier_char_at_agrees_with_the_free_function_it_shadows — RED: char_at disagrees at 2 on "abéc", left "Ã" right "é". That is exactly the byte-offset misread the carrier must not produce.
  • carrier_string_length_agrees_with_the_free_function_it_shadows — RED: left 5 (bytes) right 4 (code points).
  • carrier_indexes_code_points_not_bytes_on_multibyte_text — RED, same 5-vs-4.
  • carrier_substring_agrees_with_the_free_function_it_shadows — RED, panicking inside v1_rt on the non-char-boundary slice.
  • carrier_records_ascii_ness_of_its_own_content — GREEN, and that is correct rather than a gap: the perturbation is downstream of RcStr::new, so the flag it asserts is genuinely unchanged. A test that reddened here would be reddening on something it does not measure.

That last RED is also a partial answer to the OPEN QUESTION #9212 left behind. There, substring(s, 0, 3) on the same string under an unconditional byte path did NOT panic, and the PR flagged the possibility that witness failures were being swallowed somewhere. On the carrier path the same slice DOES panic, loudly, through the Rust test harness. This does not settle the .dag-witness half of that question — a different harness and a different call path — but it removes the carrier path from the list of places where that panic goes missing.

Brian Searls added 2 commits August 26, 2026 01:27
`--required-regen` refused with `generated surface drift:
v1_compiler_runtime_rust.rs`, and it was right. When the carrier landed I
edited `char_at`'s doc comment in `runtime_rust.dag` and mirrored the four new
literals into the stage0 transliteration BY HAND, splicing them as a
right-branching `concat(concat(concat(L, a), b), c)` subtree hanging off one
element of the chain. The emitter builds one flat left-fold spine over the
whole literal sequence. Both expressions evaluate to the same string -- which
is exactly why nothing else caught it -- but the mirror is compared BYTE-WISE
against a fresh emit, so the shape is the artifact.

Re-emitted `rt_string_ops` as a strict left fold over its 56 literals in order.

Verified against the emitter rather than against my reading of it: a remote
`--required-regen` was run with the pre-fix tree and its candidate artifact
diffed against the committed mirror. The candidate differs on exactly one line,
`rt_string_ops`'s body, and the line this commit installs is byte-identical to
the candidate's -- same 4845 bytes, compared programmatically, not eyeballed.
No other file and no other line drifts, so the carrier's own `rt_string_carrier`
and the rewired `rust_runtime_source` were already in the emitted shape.

The lesson is the repository's own: a generated mirror is not hand-editable
even when the hand edit is semantically correct. What made this recoverable is
that the gate compares bytes and refuses, so the wrong shape could not merge
quietly.
…t refuses it

review 56017 (REQUEST_CHANGES) found that nothing in this PR classifies a
hand-written v1 interpreter change against the seed's admission rules. That gap
was real and this commit closes it.

The receipt lands in `gunbc.v1_maintenance_standing`, which is the authority
that governs v1 changes -- a purpose test, four recorded admission shapes, and
five refused classes that DOMINATE every admission. It does not land in the
form the review named. That form ("a deleted scaffold path, census shrink, or
explicit lane/ROADMAP-row deferral") appears nowhere in DESIGN.md: `hand-Rust`
0 occurrences, `census shrink` 0, `ROADMAP-row` 0. The obligation was real and
the cited authority was not, so writing the receipt in the invented vocabulary
would have been the authority-substitution failure DESIGN's failure-mode list
names -- a fact filed in a carrier that does not govern the operation.

What the row says, and the part that matters is not the admission:

- PURPOSE TEST: satisfied. The interpreter is the engine the v2 self-host
  program runs on, so its per-call string cost is that program's cost. The
  defect is a cost SHAPE (an ASCII walk was O(n^2) because each index re-tested
  a prefix), and DESIGN section 6 states a proven cost-shape defect is always
  fixed regardless of realized n. Recorded instance:
  BehaviorPreservingRedundancyRemoval, with preservation proven by the
  differential tests and the perturbation RED already on the PR.

- PUBLICSURFACEGROWTH, one of the five refused classes, LITERALLY FIRES: the
  emitted seed's exported declarations gain `pub struct RcStr`, and the rung
  note defines that class as a diff over exactly that surface. The row says so
  in those words rather than routing around it, because an admission that omits
  the dominating class is not a classification. The argument for admitting
  anyway is that the class exists to stop the seed ACCUMULATING CAPABILITY and
  none is accumulated -- `RcStr::char_at` returns the same value at every index
  as the `v1_rt::char_at` it shadows, `Value::Str` already exported a string
  payload, and nothing is expressible after this change that was not before, so
  the exported type is the mechanism of a REMOVAL. The counter-argument is
  recorded beside it: `pub struct` in the seed is exactly what the class names,
  and the capability-versus-mechanism distinction is authored, not derived.

- WHO CAN OVERTURN IT: a reviewer or the operator. The carrier already declares
  itself mitigatable -- nothing mechanically refuses a v1 change in a refused
  class -- so this is a judgment on the record, not a wall.

Receipt that the row is well-formed: `gunbc run --entry
dag/gunbc/v1_maintenance_standing.dag` reaches evaluation and stops at
`NoSuchFunction { name: "main" }`, the expected result for a data-only module.
No parse or type diagnostic.
@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressing review 56017 (REQUEST_CHANGES). The finding is half right, and the half that is right is now closed in 850120e.

RIGHT: nothing in this PR classified the change against v1's admission rules. That was a real gap. src/v1 is semantics-frozen with maintenance active, every change owes a classification, and I had left mine implicit — worse, the earlier APPROVE (review 55963) asserted admissibility on my behalf, which is not a receipt I authored.

WRONG: the authority named. The review requires "DESIGN's required hand-Rust receipt: a deleted scaffold path, census shrink, or explicit lane/ROADMAP-row deferral." Measured against DESIGN.md on this head:

hand-Rust: 0 occurrences    census shrink: 0    ROADMAP-row: 0

None of those three forms exists in the document the review attributes them to. The rule that actually governs is the carrier gunbc.v1_maintenance_standing — a purpose test, four recorded admission shapes, and five refused classes that dominate every admission. Filing the receipt in the review's vocabulary would have put a fact in a carrier that does not govern the operation, which is the authority-substitution failure DESIGN's own failure-mode list names. So the receipt lands in the carrier that owns v1 admissions, as v1_maintenance_string_carrier_admission_note.

What writing it actually surfaced — this is the part worth reading. PublicSurfaceGrowth is one of the five refused classes, refusals dominate admissions, and it literally fires here: the emitted seed's exported declarations gain pub struct RcStr, and the carrier's own rung note defines that class as a diff over exactly that surface. The row says so in those words. The argument for admitting anyway is that the class exists to stop the seed accumulating capability, and none is accumulated — RcStr::char_at returns the same value at every index as the v1_rt::char_at it shadows, Value::Str already exported a string payload, and nothing is expressible after this change that was not expressible before, so the exported type is the mechanism of a removal. The counter-argument is recorded beside it rather than omitted: pub struct in the seed is exactly what the class names, and capability-versus-mechanism is authored, not derived.

That judgment is a reviewer's or the operator's to overturn — the carrier declares itself mitigatable, so nothing mechanically refuses a v1 change in a refused class. I am not claiming it is settled; I am putting it on the record where it can be rejected.

On the narrower point that "modeling the generated RcStr source in runtime_rust.dag does not discharge the separate handwritten interpreter expansion" — agreed, and it was never offered as discharging it. runtime_rust.dag is the single authority for the carrier's source; the interpreter routing is a separate change and is what the admission row classifies.

Receipt that the new row is well-formed: gunbc run --entry dag/gunbc/v1_maintenance_standing.dag reaches evaluation and stops at NoSuchFunction { name: "main" } — the expected result for a data-only module, with no parse or type diagnostic.

— sent from sunny-cat-490

# Conflicts:
#	dag/gunbc/v1_maintenance_standing.dag
@briansrls
briansrls merged commit 9a7f0a8 into main Aug 26, 2026
4 of 6 checks passed
@briansrls
briansrls deleted the session/sunny-cat-490 branch August 26, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant