Skip to content

feat(ts): keep a Cow<str>'s variant - #567

Merged
moq-bot[bot] merged 7 commits into
mainfrom
quest/m3/ts-cow-str-variant
Nov 4, 2026
Merged

moq-bot[bot] merged 7 commits into
mainfrom
quest/m3/ts-cow-str-variant

Conversation

@kixelated

@kixelated kixelated commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

rust2-ts kept a Cow<str> as a plain JS string, so it kept no variant: matching Cow::Borrowed/Owned was refused, to_mut of a borrowed one stayed borrowed, and String::from_utf8_lossy lost whether std borrowed or replaced. Quest: quest/m3/ts-cow-str-variant.md, found translating from_utf8_lossy in #537.

Approach

A runtime wrapper, as the maintainer's 2026-11-03 direction in #555 favors over a whole-program pass. It has the same shape as Cow<[T]>: rt.StrCow = string | rt.Borrowed<string>. The owned variant is the plain string a String already is, and the borrowed one is wrapped. rt.Borrowed<S> now takes the slice type, so one class serves both.

  • Matching, field reads, and construction go through the Cow<[T]> paths (cow_borrowed, cow_made, cow_borrows, cow_deref).
  • to_mut stores the text, now owned, in the Cow's place, then lends that place as the String.
  • into_owned and Deref use rt.strCowStr, Clone uses rt.strCowClone, and +/+= use rt.strCowAdd, which takes either side whole when the other is empty, as std does.
  • From borrows the text of a &str/&String and owns a String. Default and collect give owned values.
  • String::from_utf8_lossy borrows valid UTF-8 and owns replaced text. The unchecked decodes moved to rt.strFromUtf8Unchecked.
  • Comparison, ordering, hashing, Display/Debug, join/concat, String's extend/collect/unzip of Cow<str> items, and to_string deref first.
  • Lookups and B-tree ranges of a Cow-keyed collection by a str (or slice) pass that value as is: the Cow comparisons read a plain string or array as an owned Cow. A hash collection hashes every Cow<str> key, so an owned key (a primitive) and an equal borrowed one are one key.
  • Cow::is_borrowed/is_owned are unstable and stay refused, now by the generic unsupported-call refusal, as the slice ones already were.
  • Found along the way: a Result rebuilt around a written payload lost TS narrowing, so a second write through one &mut failed tsc. This was already true on main, for example two pushes into a Result<String, _>'s Ok. to_mut made it show up in the existing exclaimed test. That store now emits { ok: true, value } instead of rt.Ok(value), which keeps the narrowing. Typing rt.Ok narrowly instead also narrowed every initializer and broke lang/shapes.

Differential tests against native Rust: a new cows::variants case covers construction, conversions, clones, to_mut, +/+= across empty and non-empty sides of each variant, matches by reference, in place, by &mut passed on, and by value, Option, Vec, fields, join/concat/collect/extend/unzip, and B-tree and hash keys with lookups and ranges by &str. utf8::lossy now checks from_utf8_lossy's variant. The reject fixture's cow_matched case moved into lang/cows.

Measurements

moq's translated codec (just ts moq at the pin) and the bench's codec and task translations are byte-identical to main apart from the runtime, since none of them uses Cow<str>. Bundle size is 65,586 B on both. just ts bench --route main=<main's translation>, geometric mean over 68 codec workloads:

engine translated ÷ main A/A ÷ translated (noise)
Node 24 (V8) 0.992 0.990
Bun 1.4 (JSC) 1.007 0.993
Chromium 155 0.998 1.014
Firefox 158 1.005 1.007

The cost lands only on code that uses a Cow<str>: an rt.Borrowed allocation for each borrowed one made, and an instanceof on each read.

Impact

  • Breaking: a Cow<str> in the translated API is an rt.StrCow rather than string. Read it with rt.strCowStr(c), and pass new rt.Borrowed(s) or a string.
  • Breaking: rt.Borrowed<S> takes the borrowed slice type, so a Cow<[T]>'s is rt.Borrowed<T[]>. rt.Cow<T> is unchanged.
  • Breaking: rt.stringFromUtf8Lossy returns an rt.StrCow. The plain decode is rt.strFromUtf8Unchecked.
  • New: rt.StrCow, rt.strCowStr, rt.strCowClone, rt.strCowAdd, rt.STRING_COW_PART. rt.cowBy and rt.hashCow are generic over the borrowed type.
  • ts/MIGRATE.md has the entry.

Alternatives

  • A whole-program pass keeping plain strings where nothing observes the variant: free where unobserved, but it has to follow values through generics, fields, collections, and calls. The maintainer's feat(ts): a Vec as rt.Vec, holding std's exact capacity #555 direction prefers wrappers converted at API boundaries.
  • Wrapping the owned variant and leaving borrowed ones plain: decode, which borrows, would allocate nothing. But to_mut's &mut String would then need a place inside the wrapper, which would diverge from Cow<[T]>. With measured parity on moq, the uniform shape won.

Follow-ups

None.

Review

mechanical fix by codex / Codex: Merged base and mechanically re·solved four conflicts, preserving Cow&lt;str> and byte-helper changes; passed cargo build --locked -p rust2-ts, cargo test --locked -p rust2-ts (104 tests), cargo fmt --all --check in ts/ with pinned nightly, and tsc -p ts/runtime. (head ca4e6e3)

Codex (quest review), gpt-6.1-sol, medium effort, head b4fc8806566e8e572a1d72ca39d7f5d06abd3b46, round 2: no findings.

  • Round 1 (head c58846f): "Cow-keyed B-tree range/range_mut by str refused". Fixed: lookup values and range bounds pass as is. Also fixed the hash-key split between variants that this surfaced. Both are covered differentially.

Checks run locally: just ci passes on the reviewed head, and just ts moq passes.

🤖 Generated with Claude Code

kixelated and others added 6 commits November 3, 2026 21:10
A Cow<str> is an rt.StrCow: the string it owns, or an rt.Borrowed of the
one it borrows, as a Cow<[T]> keeps its variant. Matching Borrowed and
Owned, to_mut, into_owned, +, From, Clone, and from_utf8_lossy keep
std's variant. A Result rebuilt around a written payload is an object
literal, so TS keeps the variant its test narrowed it to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A str or slice a Cow-keyed collection is looked up or ranged by stands
in as it is, which the Cow's comparisons read as an owned Cow. A hash
collection hashes every Cow<str> key, so an owned one, a JS string,
and an equal borrowed one are one key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title A string Cow's variant feat(ts): keep a Cow<str>'s variant Nov 4, 2026
@kixelated
kixelated marked this pull request as ready for review November 4, 2026 06:09
@pullfrog

pullfrog Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

This run was cancelled before it finished.

Pullfrog  | View workflow run | via Pullfrog

@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ac1ccdde-ee1c-4219-8f71-33544733b3c0
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated
kixelated added this pull request to the merge queue Nov 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 4, 2026
@moq-bot

moq-bot Bot commented Nov 4, 2026

Copy link
Copy Markdown

quest land watch: the merge queue removed this pull request at b4fc880 because it conflicts with main (merge_conflict).

The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs.

(written by quest land watch)

@moq-bot
moq-bot Bot enabled auto-merge November 4, 2026 06:27
@moq-bot

moq-bot Bot commented Nov 4, 2026

Copy link
Copy Markdown

quest land watch: armed auto-merge on this pull request at ca4e6e3, so it joins the back of the merge queue once its required checks pass, with the land fixer's fix (codex / Codex): Merged base and mechanically re·solved four conflicts, preserving Cow&lt;str> and byte-helper changes; passed cargo build --locked -p rust2-ts, cargo test --locked -p rust2-ts (104 tests), cargo fmt --all --check in ts/ with pinned nightly, and tsc -p ts/runtime..

(written by quest land watch)

@moq-bot
moq-bot Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit dbbbe52 Nov 4, 2026
7 checks passed
@moq-bot
moq-bot Bot deleted the quest/m3/ts-cow-str-variant branch November 4, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant