diff --git a/quest/m3/README.md b/quest/m3/README.md index ccb9886f1..61d0892cb 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -37,7 +37,7 @@ generic std gap is planned here even when moq never calls it. - [A vector's exact capacity as it shrinks](/quest/m3/ts-vec-exact-capacity.md) - a deque made from a `Vec` splits its slices where std's does - [A B-tree's searches call Ord as std's do](/quest/m3/ts-btree-search-order.md) - a B-tree's lookups and inserts call a key's own `Ord` in std's order and number - [Rc and Arc of a slice](/quest/m3/ts-shared-slices.md) - `Rc<[T]>` and `Arc<[T]>` from a `Vec`, a slice, or an iterator translate -- [Adding to a String](/quest/m3/ts-string-add.md) - `String + &str` translates as `+=` does +- [Popping a String that is reassigned elsewhere](/quest/m3/ts-reassigned-pop.md) - `pop`, `remove`, and `split_off` of a local some other path assigns whole translate - [Operator trait methods called by path](/quest/m3/ts-operator-paths.md) - `::add_assign` and every other `std::ops` operator method translate called by path or as a value - [The rest of a vector's methods](/quest/m3/ts-vec-rest.md) - `append`, `resize`, `split_off`, `try_reserve`, and `Vec::from(VecDeque)` translate - [A reversed zip's per-pair cost](/quest/m3/ts-zip-rev-overhead.md) - a reversed by-index zip costs per pair what a forward one does, or the gap is explained diff --git a/quest/m3/ts-operator-paths.md b/quest/m3/ts-operator-paths.md index e7b07b1e9..b75b0b09f 100644 --- a/quest/m3/ts-operator-paths.md +++ b/quest/m3/ts-operator-paths.md @@ -21,7 +21,3 @@ Lowering std's primitive impls from their MIR may cover most of these, since the the operator itself. Measure that against a mapping per trait. Public API: the translator's output. - -## Related - -- [Adding to a String](/quest/m3/ts-string-add.md) - `String + &str`, an operator impl of a std type rather than a primitive's diff --git a/quest/m3/ts-reassigned-pop.md b/quest/m3/ts-reassigned-pop.md new file mode 100644 index 000000000..74a7f0ec3 --- /dev/null +++ b/quest/m3/ts-reassigned-pop.md @@ -0,0 +1,30 @@ +# [S] Popping a String that is reassigned elsewhere + +## Goal + +[rust2-ts](/ts/README.md) translates `String::pop`, `remove`, and `split_off` on a local +that another path assigns whole, proven by a differential test against native Rust. + +## Plan + +Found on 2026-11-02 testing [`String + &str`](https://github.com/moq-dev/rust2/pull/479): +with one arm `s = String::from("x")`, another arm's `s.pop()` reads `s` but writes the +result through `_3.v`, an `rt.Place` it never declares, so `tsc` fails. `truncate` and +`insert` in the same spot declare the place and pass. + +```rust +pub fn local(op: u8) -> Option { + let mut s = String::new(); + match op { + 1 => return s.pop(), + _ => s = String::from("x"), + } + None +} +``` + +The read and the write of the borrowed place disagree on whether the `&mut` is an alias of +`s` or its `rt.Place`; look for every edit that reads through `borrowed_place` and writes +through `store`. + +Public API: the translator's output. diff --git a/quest/m3/ts-string-add.md b/quest/m3/ts-string-add.md deleted file mode 100644 index 8e6bc28d5..000000000 --- a/quest/m3/ts-string-add.md +++ /dev/null @@ -1,13 +0,0 @@ -# [XS] Adding to a String - -## Goal - -[rust2-ts](/ts/README.md) translates `String + &str`, which moves the `String` and pushes -onto it, as `+=` already does, proven by a differential test against native Rust. - -## Plan - -Found on 2026-11-02 writing a scratch test for `&mut dyn Debug`: `a + &b` with `a: String` -is an "unsupported call to `>::add`", while `+=` maps. - -Public API: the translator's output. diff --git a/ts/README.md b/ts/README.md index 287504ae2..26837d324 100644 --- a/ts/README.md +++ b/ts/README.md @@ -315,7 +315,8 @@ one `&s[a..b]` or `&s[a..=b]` takes, which reads the string up to it and panics does inside a char, where `get` gives `None`. A JS string can't change in place, so `String`'s edits, `push_str`, `truncate`, `insert`, `retain`, `replace_range`, and the rest, assign the edited one to the local or field the `&mut` borrows, taking and checking byte offsets as std does, and `retain` then -drops its closure. A `&mut str` is +drops its closure. `+`, of a `String` or a `Cow`, is JS's `+` of the moved text, as +`+=` is. A `&mut str` is the `rt.Place` of its `String` wherever that is, and a part of one, from `&mut s[a..b]`, `get_mut`, or `split_at_mut`, is `rt.strPart(p, a, b)`, a place of its own that writes the whole string anew, checking its offsets as `&s[a..b]` does. `as_bytes_mut`, diff --git a/ts/fixtures/lang/cows/expected/cows.ts b/ts/fixtures/lang/cows/expected/cows.ts index 9f31ae5a2..32f534fbc 100644 --- a/ts/fixtures/lang/cows/expected/cows.ts +++ b/ts/fixtures/lang/cows/expected/cows.ts @@ -63,27 +63,32 @@ export function cows(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { put(out, _124); } put(out, c2); + let joined: string = cow("", owned) + first + cow(s, !owned); + const _145: string = cow("", !owned); + joined = joined + _145; + joined = joined + "+"; + put(out, joined); if ((kind & 64) === 0) { r = rt.Ok(cow(first, owned)); } else { r = rt.Err(kind); } - const _138: rt.Result = exclaimed(r); - if (_138.ok) { - put(out, _138.value); + const _156: rt.Result = exclaimed(r); + if (_156.ok) { + put(out, _156.value); } else { - rt.byteVecPush(out, _138.error); + rt.byteVecPush(out, _156.error); } } else if (_14 === 2) { const a: string = cow(s, owned); const b: string = cow(first, !owned); const iter2: rt.Iter = rt.iterSlice([a === first, a === first, a === s, first === a, s === a, a === b, a !== b, rt.cmpStr(a, b) < 0, rt.cmpStr(a, b) >= 0, rt.cmpStr(a, b) === 1]); while (true) { - const _187: boolean | rt.Done = iter2.next(); - if (_187 === rt.DONE) { + const _205: boolean | rt.Done = iter2.next(); + if (_205 === rt.DONE) { break; } - rt.byteVecPush(out, _187 ? 1 : 0); + rt.byteVecPush(out, _205 ? 1 : 0); } rt.byteVecPush(out, rt.ordMax(a, b, rt.cmpLt(rt.cmpStr)) === b ? 1 : 0); } else if (_14 === 3) { @@ -96,45 +101,45 @@ export function cows(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { rt.byteVecPush(out, Named.cmp$cmp_Ord(n, m2) > 0 ? 1 : 0); rt.byteVecPush(out, Named.partialCmp$cmp_PartialOrd(n, m2) === -1 ? 1 : 0); put(out, Named.intoOwned(n).name); - const _252: Named.Named = Named.default$Default(); - const _254: Named.Named = Named.new("", kind); - const args4: rt.Fmt[] = [(f: rt.Formatter) => Named.fmt$fmt_Debug(_252, f), (f: rt.Formatter) => Named.fmt$fmt_Debug(_254, f)]; + const _270: Named.Named = Named.default$Default(); + const _272: Named.Named = Named.new("", kind); + const args4: rt.Fmt[] = [(f: rt.Formatter) => Named.fmt$fmt_Debug(_270, f), (f: rt.Formatter) => Named.fmt$fmt_Debug(_272, f)]; put(out, rt.fmtFormat([args4[0], " ", args4[1]])); } else if (_14 === 4) { const counts: rt.BTreeMap = rt.btreeNew(rt.cmpStr) as rt.BTreeMap; const iter3: rt.Iter<[rt.usize, string]> = rt.iterEnumerate(rt.strSplit(s, "/")); while (true) { - const _268: [rt.usize, string] | rt.Done = iter3.next(); - if (_268 === rt.DONE) { + const _286: [rt.usize, string] | rt.Done = iter3.next(); + if (_286 === rt.DONE) { break; } - const _276: string = cow(_268[1], rt.usizeRem(_268[0], 2) === 1 === owned); - const _273: rt.Place = rt.entryOrInsertMut(rt.entry(counts, _276), 0); - _273.v = rt.u8Add(_273.v, 1); + const _294: string = cow(_286[1], rt.usizeRem(_286[0], 2) === 1 === owned); + const _291: rt.Place = rt.entryOrInsertMut(rt.entry(counts, _294), 0); + _291.v = rt.u8Add(_291.v, 1); } const iter4: rt.Iter<[string, rt.u8]> = rt.btreeIter(counts); while (true) { - const _285: [string, rt.u8] | rt.Done = iter4.next(); - if (_285 === rt.DONE) { + const _303: [string, rt.u8] | rt.Done = iter4.next(); + if (_303 === rt.DONE) { break; } - put(out, _285[0]); - rt.byteVecPush(out, _285[1]); + put(out, _303[0]); + rt.byteVecPush(out, _303[1]); } rt.byteVecPush(out, option_unwrapOr$0r2bmpr(rt.btreeGet(counts, first), 255)); const names: rt.HashSet = rt.hashNew(rt.hashing(Named.hash$Hash_hash_DefaultHasher as (a0: Named.Named, a1: rt.Hasher) => undefined, Named.eq$cmp_PartialEq)) as rt.HashSet; const iter5: rt.Iter = rt.strSplit(s, "/"); while (true) { - const _305: string | rt.Done = iter5.next(); - if (_305 === rt.DONE) { + const _323: string | rt.Done = iter5.next(); + if (_323 === rt.DONE) { break; } - const _311: Named.Named = Named.new(_305, 0); - rt.hashAdd(names, _311); + const _329: Named.Named = Named.new(_323, 0); + rt.hashAdd(names, _329); } rt.byteVecPush(out, rt.hashLen(names) & 0xff); - const _321: Named.Named = Named.new(first, 0); - rt.byteVecPush(out, rt.hashContains(names, _321) ? 1 : 0); + const _339: Named.Named = Named.new(first, 0); + rt.byteVecPush(out, rt.hashContains(names, _339) ? 1 : 0); } else { const c4: string = cow(s, owned); const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtPad(f, c4), (f: rt.Formatter) => rt.fmtStr(c4, f)]; diff --git a/ts/fixtures/lang/cows/src/cows.rs b/ts/fixtures/lang/cows/src/cows.rs index b228616db..3332cc753 100644 --- a/ts/fixtures/lang/cows/src/cows.rs +++ b/ts/fixtures/lang/cows/src/cows.rs @@ -108,6 +108,12 @@ pub fn cows(kind: u8, data: &[u8], out: &mut Vec) { put(out, c); } put(out, &c.into_owned()); + // `+` and `+=` of a `&str` and of another `Cow`, which borrow the right side when the + // left is empty. + let mut joined = cow("", owned) + first + cow(&s, !owned); + joined += cow("", !owned); + joined += "+"; + put(out, &joined); let r = match kind & 0x40 { 0 => Ok(cow(first, owned)), _ => Err(kind), diff --git a/ts/fixtures/lang/strs/expected/strs.ts b/ts/fixtures/lang/strs/expected/strs.ts index 624b86bdd..03f93827d 100644 --- a/ts/fixtures/lang/strs/expected/strs.ts +++ b/ts/fixtures/lang/strs/expected/strs.ts @@ -535,14 +535,27 @@ export function edit(s: rt.Place, op: rt.u8, a: rt.u8, b: rt.u8, out: rt if (_24 === 0) { s.v = s.v + piece; } else if (_24 === 1) { - s.v = s.v + piece; + const _28: rt.u8 = rt.u8Rem(b, 4); + if (_28 === 0) { + s.v = s.v + piece; + } else if (_28 === 1) { + const taken2 = s.v; + s.v = ""; + s.v = taken2 + piece; + } else if (_28 === 2) { + const taken = s.v; + s.v = ""; + s.v = taken + "" + piece + piece; + } else { + s.v = rt.stringWithCapacity(a as rt.usize) + s.v + ""; + } } else if (_24 === 2) { if (!(rt.u8Rem(a, 4) === 0)) { - const _35: rt.usize = rt.u8Rem(b, 16) as rt.usize; - if (!(_35 < 16)) { - rt.panic("index out of bounds: the len is " + 16 + " but the index is " + _35); + const _59: rt.usize = rt.u8Rem(b, 16) as rt.usize; + if (!(_59 < 16)) { + rt.panic("index out of bounds: the len is " + 16 + " but the index is " + _59); } - s.v = s.v + String.fromCodePoint([47, 92, 97, 48, 57, 127, 128, 233, 2047, 2048, 55295, 57344, 65535, 65536, 128512, 1114111][_35]); + s.v = s.v + String.fromCodePoint([47, 92, 97, 48, 57, 127, 128, 233, 2047, 2048, 55295, 57344, 65535, 65536, 128512, 1114111][_59]); } else { s.v = ""; } @@ -557,11 +570,11 @@ export function edit(s: rt.Place, op: rt.u8, a: rt.u8, b: rt.u8, out: rt s.v = edit2[0]; putChar(out, edit2[1]); } else if (_24 === 6) { - const _44: rt.usize = rt.u8Rem(b, 16) as rt.usize; - if (!(_44 < 16)) { - rt.panic("index out of bounds: the len is " + 16 + " but the index is " + _44); + const _68: rt.usize = rt.u8Rem(b, 16) as rt.usize; + if (!(_68 < 16)) { + rt.panic("index out of bounds: the len is " + 16 + " but the index is " + _68); } - s.v = rt.stringInsertStr(s.v, i, String.fromCodePoint([47, 92, 97, 48, 57, 127, 128, 233, 2047, 2048, 55295, 57344, 65535, 65536, 128512, 1114111][_44])); + s.v = rt.stringInsertStr(s.v, i, String.fromCodePoint([47, 92, 97, 48, 57, 127, 128, 233, 2047, 2048, 55295, 57344, 65535, 65536, 128512, 1114111][_68])); } else if (_24 === 7) { s.v = rt.stringInsertStr(s.v, i, piece); } else if (_24 === 8) { @@ -581,48 +594,48 @@ export function edit(s: rt.Place, op: rt.u8, a: rt.u8, b: rt.u8, out: rt s.v = edit[0]; put(out, edit[1]); } else if (_24 === 10) { - const _67: rt.u8 = rt.u8Rem(b, 7); - if (_67 === 0) { - const _69: rt.Range = new rt.Range(i, j); - s.v = rt.stringReplaceRange(s.v, _69, piece); - } else if (_67 === 1) { - const _72: rt.RangeFrom = new rt.RangeFrom(i, rt.usizeStep); - s.v = rt.stringReplaceRange(s.v, _72, piece); - } else if (_67 === 2) { - const _75: { end: rt.usize } = { end: j }; - s.v = rt.stringReplaceRange(s.v, _75, piece); - } else if (_67 === 3) { + const _91: rt.u8 = rt.u8Rem(b, 7); + if (_91 === 0) { + const _93: rt.Range = new rt.Range(i, j); + s.v = rt.stringReplaceRange(s.v, _93, piece); + } else if (_91 === 1) { + const _96: rt.RangeFrom = new rt.RangeFrom(i, rt.usizeStep); + s.v = rt.stringReplaceRange(s.v, _96, piece); + } else if (_91 === 2) { + const _99: { end: rt.usize } = { end: j }; + s.v = rt.stringReplaceRange(s.v, _99, piece); + } else if (_91 === 3) { s.v = rt.stringReplaceRange(s.v, undefined, piece); - } else if (_67 === 4) { - const _80: rt.RangeInclusive = new rt.RangeInclusive(i, j, rt.usizeStep); - s.v = rt.stringReplaceRange(s.v, _80, piece); - } else if (_67 === 5) { - const _83: { end: rt.usize; inclusive: true } = { end: j, inclusive: true }; - s.v = rt.stringReplaceRange(s.v, _83, piece); + } else if (_91 === 4) { + const _104: rt.RangeInclusive = new rt.RangeInclusive(i, j, rt.usizeStep); + s.v = rt.stringReplaceRange(s.v, _104, piece); + } else if (_91 === 5) { + const _107: { end: rt.usize; inclusive: true } = { end: j, inclusive: true }; + s.v = rt.stringReplaceRange(s.v, _107, piece); } else { - const _86: [rt.RangeBound, rt.RangeBound] = [{ tag: "Excluded", _0: i } as rt.RangeBound, { tag: "Included", _0: j } as rt.RangeBound]; - s.v = rt.stringReplaceRange(s.v, _86, piece); + const _110: [rt.RangeBound, rt.RangeBound] = [{ tag: "Excluded", _0: i } as rt.RangeBound, { tag: "Included", _0: j } as rt.RangeBound]; + s.v = rt.stringReplaceRange(s.v, _110, piece); } } else if (_24 === 11) { - const _91: rt.u8 = rt.u8Rem(b, 5); - if (_91 === 0) { + const _115: rt.u8 = rt.u8Rem(b, 5); + if (_115 === 0) { s.v = rt.stringExtendFromWithin(s.v, new rt.Range(i, j)); - } else if (_91 === 1) { + } else if (_115 === 1) { s.v = rt.stringExtendFromWithin(s.v, new rt.RangeFrom(i, rt.usizeStep)); - } else if (_91 === 2) { + } else if (_115 === 2) { s.v = rt.stringExtendFromWithin(s.v, { end: j, inclusive: true }); - } else if (_91 === 3) { + } else if (_115 === 3) { s.v = rt.stringExtendFromWithin(s.v, [{ tag: "Included", _0: i } as rt.RangeBound, { tag: "Excluded", _0: j } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]); } else { s.v = rt.stringExtendFromWithin(s.v, undefined); } } else if (_24 === 12) { - const _99: rt.u8 = rt.u8Rem(a, 4); - if (_99 === 0) { + const _123: rt.u8 = rt.u8Rem(a, 4); + if (_123 === 0) { rt.stringReserve(s.v, rt.usizeSub(4294967295, b as rt.usize)); - } else if (_99 === 1) { - const _104: rt.usize = rt.usizeAdd(rt.usizeAdd(rt.usizeSub(2147483647, rt.stringLen(s.v)), 1), rt.u8Rem(b, 2) as rt.usize); - rt.stringReserve(s.v, _104); + } else if (_123 === 1) { + const _128: rt.usize = rt.usizeAdd(rt.usizeAdd(rt.usizeSub(2147483647, rt.stringLen(s.v)), 1), rt.u8Rem(b, 2) as rt.usize); + rt.stringReserve(s.v, _128); } else { rt.stringReserve(s.v, a as rt.usize); rt.stringReserve(s.v, b as rt.usize); @@ -694,6 +707,7 @@ export function edits(kind: rt.u8, dataData: Uint8Array, dataStart: number, data put(out, s); put(out, named.name); } + put(out, s + "😀" + named.name); } /** diff --git a/ts/fixtures/lang/strs/src/strs.rs b/ts/fixtures/lang/strs/src/strs.rs index aa100ec4e..debec6bb1 100644 --- a/ts/fixtures/lang/strs/src/strs.rs +++ b/ts/fixtures/lang/strs/src/strs.rs @@ -419,7 +419,14 @@ fn edit(s: &mut String, op: u8, a: u8, b: u8, out: &mut Vec) { let (i, j) = (offset(s, a), offset(s, b)); match op % 14 { 0 => s.push_str(&piece), - 1 => *s += &piece, + // `+=`, and `+`, which moves the `String` it pushes onto: of a `&str`, and through + // deref of a `&String`, a `&&str`, and the `&mut String` itself, chained. + 1 => match b % 4 { + 0 => *s += &piece, + 1 => *s = std::mem::take(s) + &piece, + 2 => *s = std::mem::take(s) + "" + &&*piece + &piece, + _ => *s = String::with_capacity(usize::from(a)) + s + "", + }, 2 if a % 4 == 0 => s.clear(), 2 => s.push(ALPHABET[usize::from(b % 16)]), 3 => s.truncate(i), @@ -504,6 +511,9 @@ pub fn edits(kind: u8, data: &[u8], out: &mut Vec) { put(out, &s); put(out, &named.name); } + // `+` of the local itself, moved. + let s = s + "😀" + &named.name; + put(out, &s); } /// Where `bool`, `char`, and `NonZero` parse or fail: case, whitespace, each UTF-8 width, diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index f81938327..de33b1969 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -2309,6 +2309,11 @@ fn mapping<'tcx>( // A `Box` is its text, as a `String` is. ("String", "as_str" | "into_boxed_str") | ("str", "into_string") => args.pop()?, ("String", "new") => Expr::Str(String::new()), + // `String + &str` and `Cow`'s, which push onto the moved text as `+=` does. + ("Add", "add") if string(tcx, targ(gargs, 0)) => { + let text = args.pop()?; + args.pop()?.binary("+", text) + } ("String", "with_capacity") => Expr::rt("stringWithCapacity", args), // Of the place the `&mut` is, which it writes as it drops, and of a range, as `Vec::drain`. ("String", "drain") => { diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index 5a203ab0f..279396210 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -614,9 +614,11 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { ); done(next, true) } - // A JS string can't change in place, so what the `&mut` borrows gets a new one. + // A JS string can't change in place, so what the `&mut` borrows gets a new one. A + // `Cow` is its text, as a `String` is. ("String", "push" | "push_str") | ("AddAssign", "add_assign") - if owner == "String" || targ(0).is_some_and(|t| self.b.t.std_of(t) == Some(Std::String)) => + if owner == "String" + || (targ(0).is_some_and(|t| library::string(tcx, t)) && !self.b.t.translated(def_id, gargs)) => { let place = self.borrowed_place(&args[0].node, span, out)?; let text = self.operand(&args[1].node, span, out);