Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) - `<u8 as AddAssign>::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
Expand Down
4 changes: 0 additions & 4 deletions quest/m3/ts-operator-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 30 additions & 0 deletions quest/m3/ts-reassigned-pop.md
Original file line number Diff line number Diff line change
@@ -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<char> {
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.
13 changes: 0 additions & 13 deletions quest/m3/ts-string-add.md

This file was deleted.

3 changes: 2 additions & 1 deletion ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>`, 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`,
Expand Down
55 changes: 30 additions & 25 deletions ts/fixtures/lang/cows/expected/cows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, rt.u8> = exclaimed(r);
if (_138.ok) {
put(out, _138.value);
const _156: rt.Result<string, rt.u8> = 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<boolean> = 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) {
Expand All @@ -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<string, rt.u8> = rt.btreeNew(rt.cmpStr) as rt.BTreeMap<string, rt.u8>;
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.u8> = 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.u8> = 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<Named.Named> = rt.hashNew(rt.hashing(Named.hash$Hash_hash_DefaultHasher as (a0: Named.Named, a1: rt.Hasher) => undefined, Named.eq$cmp_PartialEq)) as rt.HashSet<Named.Named>;
const iter5: rt.Iter<string> = 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)];
Expand Down
6 changes: 6 additions & 0 deletions ts/fixtures/lang/cows/src/cows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ pub fn cows(kind: u8, data: &[u8], out: &mut Vec<u8>) {
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),
Expand Down
90 changes: 52 additions & 38 deletions ts/fixtures/lang/strs/expected/strs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,14 +535,27 @@ export function edit(s: rt.Place<string>, 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 = "";
}
Expand All @@ -557,11 +570,11 @@ export function edit(s: rt.Place<string>, 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) {
Expand All @@ -581,48 +594,48 @@ export function edit(s: rt.Place<string>, 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<rt.usize> = new rt.Range(i, j);
s.v = rt.stringReplaceRange(s.v, _69, piece);
} else if (_67 === 1) {
const _72: rt.RangeFrom<rt.usize> = 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<rt.usize> = new rt.Range(i, j);
s.v = rt.stringReplaceRange(s.v, _93, piece);
} else if (_91 === 1) {
const _96: rt.RangeFrom<rt.usize> = 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<rt.usize> = 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<rt.usize> = 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.usize>, rt.RangeBound<rt.usize>] = [{ tag: "Excluded", _0: i } as rt.RangeBound<rt.usize>, { tag: "Included", _0: j } as rt.RangeBound<rt.usize>];
s.v = rt.stringReplaceRange(s.v, _86, piece);
const _110: [rt.RangeBound<rt.usize>, rt.RangeBound<rt.usize>] = [{ tag: "Excluded", _0: i } as rt.RangeBound<rt.usize>, { tag: "Included", _0: j } as rt.RangeBound<rt.usize>];
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<rt.usize>, { tag: "Excluded", _0: j } as rt.RangeBound<rt.usize>] as [rt.RangeBound<rt.usize>, rt.RangeBound<rt.usize>]);
} 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);
Expand Down Expand Up @@ -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);
}

/**
Expand Down
12 changes: 11 additions & 1 deletion ts/fixtures/lang/strs/src/strs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,14 @@ fn edit(s: &mut String, op: u8, a: u8, b: u8, out: &mut Vec<u8>) {
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),
Expand Down Expand Up @@ -504,6 +511,9 @@ pub fn edits(kind: u8, data: &[u8], out: &mut Vec<u8>) {
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,
Expand Down
5 changes: 5 additions & 0 deletions ts/src/lower/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2309,6 +2309,11 @@ fn mapping<'tcx>(
// A `Box<str>` 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<str>`'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") => {
Expand Down
6 changes: 4 additions & 2 deletions ts/src/lower/mir/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>` 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);
Expand Down