From 8dbe033de6cce82db1495b2aaeb7ba224c14ac58 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:03:22 -0800 Subject: [PATCH 1/2] chore(quest): claim quest/m3/ts-reassigned-pop.md Quest-Claim: dfa8e9cb30a2e271 From 93d612731043965449433c4c8158da6a64b11396 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:18:49 -0800 Subject: [PATCH 2/2] fix(ts): emit a borrowed place's held temporaries before an edit reads it Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-reassigned-pop.md | 30 --------- .../lang/pointers/expected/pointers.ts | 3 +- ts/fixtures/lang/strs/expected/strs.ts | 64 ++++++++++++------- ts/fixtures/lang/strs/src/strs.rs | 9 ++- ts/src/lower/mir/call.rs | 21 +++--- 6 files changed, 59 insertions(+), 69 deletions(-) delete mode 100644 quest/m3/ts-reassigned-pop.md diff --git a/quest/m3/README.md b/quest/m3/README.md index ced9594b0..1cf37e4a4 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -36,7 +36,6 @@ 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 -- [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 - [Whether a type needs drop](/quest/m3/ts-needs-drop.md) - `mem::needs_drop::()` translates as rustc's constant for each instance - [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 diff --git a/quest/m3/ts-reassigned-pop.md b/quest/m3/ts-reassigned-pop.md deleted file mode 100644 index 74a7f0ec3..000000000 --- a/quest/m3/ts-reassigned-pop.md +++ /dev/null @@ -1,30 +0,0 @@ -# [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/ts/fixtures/lang/pointers/expected/pointers.ts b/ts/fixtures/lang/pointers/expected/pointers.ts index bf78056d8..5fe81058c 100644 --- a/ts/fixtures/lang/pointers/expected/pointers.ts +++ b/ts/fixtures/lang/pointers/expected/pointers.ts @@ -437,8 +437,9 @@ export function cells(data: rt.ByteSlice, out: rt.ByteVec): void { _67._1 = _67._1 ^ 128; rt.iterExtendBytes(out, rt.iterSlice(Uint8Array.of(before2._0, before2._1, between._0, between._1, pair.v._0, pair.v._1)), undefined, true); } else if (_23 === 3) { + const _85: rt.char = rt.u8Add(97, rt.u8Rem(n, 26)) as rt.char; const _83: rt.Place = text; - _83.v = _83.v + String.fromCodePoint(rt.u8Add(97, rt.u8Rem(n, 26)) as rt.char); + _83.v = _83.v + String.fromCodePoint(_85); const _91: rt.Place = text; _91.v = _91.v + "!"; rt.iterExtendBytes(out, rt.iterSlice(rt.byteSliceView(rt.stringAsBytes(text.v))), undefined, true); diff --git a/ts/fixtures/lang/strs/expected/strs.ts b/ts/fixtures/lang/strs/expected/strs.ts index 03f93827d..2c5b96488 100644 --- a/ts/fixtures/lang/strs/expected/strs.ts +++ b/ts/fixtures/lang/strs/expected/strs.ts @@ -678,25 +678,41 @@ export function edits(kind: rt.u8, dataData: Uint8Array, dataStart: number, data const b: rt.u8 = _25[2]; const _39: rt.u8 = rt.u8Div(op, 64); if (_39 === 0) { - edit(rt.place(() => named.name, (value2) => named.name = value2), op, a, b, out); + edit(rt.place(() => named.name, (value10) => named.name = value10), op, a, b, out); } else if (_39 === 1) { - const _42: rt.u8 = rt.u8Rem(op, 6); + const _42: rt.u8 = rt.u8Rem(op, 8); if (_42 === 0) { - s = s + "Ć©/"; + const _44: rt.Place = rt.place(() => s, (value9) => s = value9); + _44.v = _44.v + "Ć©/"; } else if (_42 === 1) { const _47: rt.usize = offset(s, a); - s = rt.stringTruncate(s, _47); + const _46: rt.Place = rt.place(() => s, (value8) => s = value8); + _46.v = rt.stringTruncate(_46.v, _47); } else if (_42 === 2) { - const edit2 = rt.stringPop(s); - s = edit2[0]; - putChar(out, edit2[1]); + const _51: rt.Place = rt.place(() => s, (value7) => s = value7); + const edit4 = rt.stringPop(_51.v); + _51.v = edit4[0]; + putChar(out, edit4[1]); } else if (_42 === 3) { const _53: rt.usize = offset(s, a); - s = rt.stringInsertStr(s, _53, "šŸ˜€"); + const _56: string = "šŸ˜€"; + const _52: rt.Place = rt.place(() => s, (value6) => s = value6); + _52.v = rt.stringInsertStr(_52.v, _53, _56); } else if (_42 === 4) { + const _60: rt.Place = rt.place(() => s, (value5) => s = value5); const _61: rt.usize = offset(s, a); - const edit_ = rt.stringSplitOff(s, _61); - s = edit_[0]; + const edit3 = rt.stringSplitOff(_60.v, _61); + _60.v = edit3[0]; + put(out, edit3[1]); + } else if (_42 === 5) { + const _66: rt.Place = rt.place(() => s, (value4) => s = value4); + const edit2 = rt.stringRemove(_66.v, 0); + _66.v = edit2[0]; + putChar(out, edit2[1]); + } else if (_42 === 6) { + const _70: rt.Place = rt.place(() => s, (value3) => s = value3); + const edit_ = rt.stringSplitOff(_70.v, 1); + _70.v = edit_[0]; put(out, edit_[1]); } else { s = ""; @@ -707,7 +723,8 @@ export function edits(kind: rt.u8, dataData: Uint8Array, dataStart: number, data put(out, s); put(out, named.name); } - put(out, s + "šŸ˜€" + named.name); + s = s + "šŸ˜€" + named.name; + put(out, s); } /** @@ -1447,8 +1464,8 @@ export function hinted(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { const m: rt.u32 = _7[1]; const sub: rt.usize = rt.usizeDiv(option_unwrapOr$0r2bmpr(rt.byteSliceGet(data, 1), 0) as rt.usize, [0, 5, 8, 9, 2147483645, 2147483648, 4294967295].length); let t: string = "aĆ©"; - const _25: rt.Place = rt.place(() => t, (value) => t = value); const _26: string = text(option_unwrapOr$1rbuegs(rt.byteSliceGetRange(data, 2), rt.byteSliceFrom(Uint8Array.of()))); + const _25: rt.Place = rt.place(() => t, (value) => t = value); _25.v = _25.v + _26; const calls: rt.Cell = new rt.Cell(0); const f: (a0: rt.u32) => rt.char = (_2: rt.u32): rt.char => { @@ -1465,20 +1482,20 @@ export function hinted(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { }; const _38: rt.u8 = rt.u8Rem(kind, 10); if (_38 === 0) { - const _40: rt.Place = rt.place(() => t, (value13) => t = value13); const _41: rt.Iter = rt.iterMap(new rt.Range(0, n), f); + const _40: rt.Place = rt.place(() => t, (value13) => t = value13); _40.v = rt.stringExtendChars(_40.v, _41); } else if (_38 === 1) { t = rt.stringExtendChars("", rt.iterMap(rt.iterChain(new rt.RangeInclusive(0, n, rt.u32Step), new rt.Range(0, n)), f)); } else if (_38 === 2) { - const _48: rt.Place = rt.place(() => t, (value12) => t = value12); const _49: rt.Iter = rt.iterSkip(rt.iterMap(new rt.Range(0, rt.u64Or(rt.u64Shl(n as rt.u64, 8), m as rt.u64), rt.u64Step), (i4: rt.u64): rt.char => { return f(rt.u64Low(i4)); }), m as rt.usize); + const _48: rt.Place = rt.place(() => t, (value12) => t = value12); _48.v = rt.stringExtendChars(_48.v, _49); } else if (_38 === 3) { - const _61: rt.Place = rt.place(() => t, (value11) => t = value11); const _62: rt.Iter = rt.iterTake(rt.iterMap(new rt.Range(0, n), f), m as rt.usize, undefined, true); + const _61: rt.Place = rt.place(() => t, (value11) => t = value11); _61.v = rt.stringExtendChars(_61.v, _62); } else if (_38 === 4) { const _66: rt.Place = rt.place(() => t, (value10) => t = value10); @@ -1492,45 +1509,45 @@ export function hinted(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { _66.v = rt.stringExtendChars(_66.v, _67); } else if (_38 === 5) { if (rt.u32Rem(m, 2) === 0) { - const _78: rt.Place = rt.place(() => t, (value9) => t = value9); const _79: rt.Iter = rt.iterChain(rt.iterMap(new rt.Range(0, n), f), rt.iterMap(new rt.Range(0, m), f)); + const _78: rt.Place = rt.place(() => t, (value9) => t = value9); _78.v = rt.stringExtendChars(_78.v, _79); } else { - const _84: rt.Place = rt.place(() => t, (value8) => t = value8); const _85: rt.Iter = rt.iterMap(rt.iterZip(new rt.Range(0, n), new rt.Range(0, m), [false, false]), (_211: [rt.u32, rt.u32]): rt.char => { return f(_211[0]); }); + const _84: rt.Place = rt.place(() => t, (value8) => t = value8); _84.v = rt.stringExtendChars(_84.v, _85); } } else if (_38 === 6) { - const _91: rt.Place = rt.place(() => t, (value7) => t = value7); const _92: rt.Iter = rt.iterFlatMap(new rt.Range(0, n), (i2: rt.u32): rt.char[] => { return [f(i2), f(i2)]; }, (value: rt.char[]) => rt.iterSlice(value), false, 2); + const _91: rt.Place = rt.place(() => t, (value7) => t = value7); _91.v = rt.stringExtendChars(_91.v, _92); } else if (_38 === 7) { - const _96: rt.Place = rt.place(() => t, (value6) => t = value6); const _97: rt.Iter = rt.iterChain(rt.iterChain(rt.iterChain(rt.iterChain(rt.iterMap(new rt.Range(0, rt.u32SaturatingSub(n, 5)), f), rt.iterOnce(120)), rt.iterOption(121)), rt.strChars("Ć©šŸ˜€")), rt.iterSlice([122])); + const _96: rt.Place = rt.place(() => t, (value6) => t = value6); _96.v = rt.stringExtendChars(_96.v, _97); } else if (_38 === 8) { const _112: rt.usize = rt.usizeRem(sub, 3); if (_112 === 0) { - const _114: rt.Place = rt.place(() => t, (value5) => t = value5); const _115: rt.Iter = rt.iterMap(new rt.Range({ hi: 0x10000000, lo: 0 } as rt.u64, rt.u64Add({ hi: 0x10000000, lo: 0 } as rt.u64, n as rt.u64), rt.u64Step), (_210: rt.u64): rt.char => { return f(0); }); + const _114: rt.Place = rt.place(() => t, (value5) => t = value5); _114.v = rt.stringExtendChars(_114.v, _115); } else if (_112 === 1) { - const _128: rt.Place = rt.place(() => t, (value4) => t = value4); const _129: rt.Iter = rt.iterMap(new rt.Range(rt.i128Neg(rt.i128Shl(1, 100)), rt.i128Add(rt.i128Neg(rt.i128Shl(1, 100)), n as rt.i128), rt.i128Step), (_27: rt.i128): rt.char => { return f(0); }); + const _128: rt.Place = rt.place(() => t, (value4) => t = value4); _128.v = rt.stringExtendChars(_128.v, _129); } else { - const _146: rt.Place = rt.place(() => t, (value3) => t = value3); const _147: rt.Iter = rt.iterMap(new rt.RangeInclusive({ hi: 0x10000000, lo: 0 } as rt.u64, rt.u64Add({ hi: 0x10000000, lo: 0 } as rt.u64, n as rt.u64), rt.u64Step), (_24: rt.u64): rt.char => { return f(0); }); + const _146: rt.Place = rt.place(() => t, (value3) => t = value3); _146.v = rt.stringExtendChars(_146.v, _147); } } else { @@ -1538,8 +1555,9 @@ export function hinted(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { return _23[1]; })))); putChar(out, rt.peekablePeek(chars)); + const _173: rt.Peekable = chars; const _172: rt.Place = rt.place(() => t, (value2) => t = value2); - _172.v = rt.stringExtendChars(_172.v, chars); + _172.v = rt.stringExtendChars(_172.v, _173); } put(out, t); } diff --git a/ts/fixtures/lang/strs/src/strs.rs b/ts/fixtures/lang/strs/src/strs.rs index debec6bb1..996d23f67 100644 --- a/ts/fixtures/lang/strs/src/strs.rs +++ b/ts/fixtures/lang/strs/src/strs.rs @@ -497,13 +497,16 @@ pub fn edits(kind: u8, data: &[u8], out: &mut Vec) { let [op, a, b] = [op[0], *op.get(1).unwrap_or(&0), *op.get(2).unwrap_or(&0x10)]; match op / 64 { 0 => edit(&mut named.name, op, a, b, out), - // On the local itself, with no `&mut` between. - 1 => match op % 6 { + // On the local itself, with no `&mut` between, which the `+` after the loop assigns whole. + 1 => match op % 8 { 0 => s.push_str("Ć©/"), 1 => s.truncate(offset(&s, a)), 2 => put_char(out, s.pop()), 3 => s.insert_str(offset(&s, a), "šŸ˜€"), 4 => put(out, &s.split_off(offset(&s, a))), + // Of a constant offset, which reads nothing between the borrow and the edit. + 5 => put_char(out, Some(s.remove(0))), + 6 => put(out, &s.split_off(1)), _ => s.clear(), }, _ => edit(&mut s, op, a, b, out), @@ -512,7 +515,7 @@ pub fn edits(kind: u8, data: &[u8], out: &mut Vec) { put(out, &named.name); } // `+` of the local itself, moved. - let s = s + "šŸ˜€" + &named.name; + s = s + "šŸ˜€" + &named.name; put(out, &s); } diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index 404adc8d2..c13e5d1d5 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -827,8 +827,6 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { // A `Cow<[T]>` that borrows takes a clone of its slice, whose `Vec` the `&mut` lends. ("Cow", "to_mut") if let ty::Slice(elem) = targ(0)?.kind() => { let place = self.borrowed_place(&args[0].node, span, out)?; - // Read twice, by the clone and by the store. - self.materialize(&place.0, out); let owned = library::cow_cloned(self.b, "IntoOwned", place.0.clone(), *elem, span); let owned = self.path(owned, "owned", out); self.set_borrowed(&place, owned.clone(), span, out); @@ -1187,7 +1185,8 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// reference, which a store resolves: an alias of a borrowed place, not a copy of a /// reference, which holds the object it points at, or an `rt.Place`, whose `v` it is, /// or else the object it points at, written in place. A `[u8; N]`'s is a copy of its - /// bytes instead, which the store writes in place. + /// bytes instead, which the store writes in place. What it reads that is held back is + /// emitted, as an edit reads it and then writes it, where MIR reads it once. fn borrowed_place(&mut self, op: &Operand<'tcx>, span: Span, out: &mut Vec) -> Option> { let (Operand::Copy(place) | Operand::Move(place)) = op else { return None; @@ -1202,23 +1201,25 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let slice = self.slice_path(slice, out); return Some((Expr::rt("byteSliceToArray", vec![slice]), Some(deref))); } - match &self.slots[place.local] { + let borrowed = match &self.slots[place.local] { Slot::Alias(Some(target)) if assignable(target) && self.alias_of_place(place.local) => { - Some((target.clone(), Some(deref))) + (target.clone(), Some(deref)) } _ if self.b.t.place_pointee(self.mir.local_decls[place.local].ty).is_some() => { - Some((self.place_expr(deref, span, out), Some(deref))) + (self.place_expr(deref, span, out), Some(deref)) } _ if self.in_object(deref) => { let target = self.place_expr(deref, span, out); - Some((self.path(target, "object", out), Some(deref))) + (self.path(target, "object", out), Some(deref)) } _ => { let pointee = self.mir.local_decls[place.local].ty.peel_refs(); let err = self.err(span, format!("replacing the value behind a reference to `{pointee}`")); - Some((err, None)) + (err, None) } - } + }; + self.materialize(&borrowed.0, out); + Some(borrowed) } /// `slice::from_mut`, `array::from_mut`, or with `cell`, `Cell::from_mut` of `op`, a @@ -1449,8 +1450,6 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let Some(payload) = payload else { return Some(Some((target, true))); }; - // The test reads it before either branch, so what it reads is set before both. - self.materialize(&target, out); let whole = tcx.mk_place_deref(args[0].node.place()?); let option_ty = whole.ty(&self.mir.local_decls, tcx).ty; // What it inserts, made before the `Option` is read, as Rust makes the argument.