From 0dfb1fa1c534913844069f48c6345389fa651f05 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:37:03 -0800 Subject: [PATCH 1/3] chore(quest): claim quest/m3/ts-empty-places.md Quest-Claim: dfd60a80630f9b90 From bc5704f6f47c5aebbbff91afb19b8675c5ff6667 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:40:12 -0800 Subject: [PATCH 2/3] feat(ts): accept an empty iterator of mutable references as a place source Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-empty-places.md | 15 ------- ts/fixtures/lang/refs/expected/refs.ts | 54 +++++++++++++++++++++++--- ts/fixtures/lang/refs/src/refs.rs | 20 +++++++++- ts/src/lower/library.rs | 32 ++++++++------- ts/src/lower/mir/call.rs | 7 +++- 6 files changed, 91 insertions(+), 38 deletions(-) delete mode 100644 quest/m3/ts-empty-places.md diff --git a/quest/m3/README.md b/quest/m3/README.md index 239ea51a2..460b03e21 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -37,7 +37,6 @@ generic std gap is planned here even when moq never calls it. - [Whole writes into a lazy cell or thread-local key](/quest/m3/ts-lazy-writes.md) - a whole write through a `&mut` to a `LazyCell`, `LazyLock`, or `LocalKey` keeps its state - [Debug of a LazyCell or a LazyLock](/quest/m3/ts-lazy-debug.md) - `{:?}` of a `LazyCell` and a `LazyLock` writes its value or ``, as std's do - [Type errors at the Rust that made them](/quest/m3/ts-span-precision.md) - a `tsc` error on output names the tail expression or call argument that made it -- [An empty iterator of mutable references](/quest/m3/ts-empty-places.md) - `iter::empty::<&mut T>()` given to `zip` or `chain` translates - [An Option of numbers in the result slot](/quest/m3/ts-option-slot.md) - `varint_opt`'s `Option` comes back through the slot with no object per call - [Dropping a crate's generic type inside another](/quest/m3/ts-nested-generic-drop.md) - an `LruCache` holding a `LinkedHashMap` drops as rustc's glue does - [Fresh writes through a zip, a copy, and a repeat](/quest/m3/ts-fresh-shapes.md) - zipping a `Vec<[T; N]>`, reading a `T` out of `Zip::next`, and `copied` or `vec![x; n]` over a `Copy` type translate diff --git a/quest/m3/ts-empty-places.md b/quest/m3/ts-empty-places.md deleted file mode 100644 index 75b112948..000000000 --- a/quest/m3/ts-empty-places.md +++ /dev/null @@ -1,15 +0,0 @@ -# [XS] An empty iterator of mutable references - -## Goal - -[rust2-ts](/ts/README.md) translates `std::iter::empty::<&mut T>()` for a `T` JS copies, -as given to `zip` or `chain`, proven by a differential test against native Rust. - -## Plan - -Found by [#391](https://github.com/moq-dev/rust2/pull/391): the check that lets an -adaptor's argument lend places refuses `iter::empty`, though it yields nothing to write -through. Accept it in that check, so an empty iterator is a place source like a -`&mut` collection. - -Public API: the translator's output. diff --git a/ts/fixtures/lang/refs/expected/refs.ts b/ts/fixtures/lang/refs/expected/refs.ts index da8dcee57..d62c3f2c1 100644 --- a/ts/fixtures/lang/refs/expected/refs.ts +++ b/ts/fixtures/lang/refs/expected/refs.ts @@ -1388,8 +1388,8 @@ export function parity(b: rt.u8): rt.u8 | undefined { /** * A `&mut` to a `Vec`, an `Option`, or a `Result` of bytes that an adaptor is given, - * beside items of no places, lends each element as its place to write through. One a - * consumer is given compares as values. + * beside items of no places or an empty iterator, lends each element as its place to write + * through. One a consumer is given compares as values. */ export function adapted(data: rt.ByteSlice, out: rt.ByteVec): void { const v: rt.ByteVec = rt.byteSliceToByteVec(data); @@ -1439,6 +1439,50 @@ export function adapted(data: rt.ByteSlice, out: rt.ByteVec): void { } _76.v = rt.u8WrappingAdd(_76.v, 5); } + rt.iterEmpty(); + const _84: rt.Iter> = rt.iterChain(rt.iterEmpty() as rt.Iter>, rt.iterSliceMut(rt.byteVecView(v))); + rt.iterEmpty(); + const iter6: rt.Iter> = rt.iterChain(_84, rt.iterEmpty() as rt.Iter>); + while (true) { + const _89: rt.Place | rt.Done = iter6.next(); + if (_89 === rt.DONE) { + break; + } + _89.v = rt.intRotateLeft(_89.v, 3, 8, false); + } + const _97: rt.Iter> = rt.iterSliceMut(rt.byteSliceView(rt.byteVecSlice(v))); + rt.iterEmpty(); + const iter7: rt.Iter<[rt.Place, rt.Place]> = rt.iterZip(_97, rt.iterEmpty() as rt.Iter>, "len"); + while (true) { + const _102: [rt.Place, rt.Place] | rt.Done = iter7.next(); + if (_102 === rt.DONE) { + break; + } + const swapped2 = _102[0].v; + _102[0].v = _102[1].v; + _102[1].v = swapped2; + } + rt.iterEmpty(); + const iter8: rt.Iter<[rt.Place, rt.Place]> = rt.iterZip(rt.iterEmpty() as rt.Iter>, rt.iterOption(rt.optionAsMut(rt.place(() => opt, (value7) => opt = value7))), "len"); + while (true) { + const _113: [rt.Place, rt.Place] | rt.Done = iter8.next(); + if (_113 === rt.DONE) { + break; + } + _113[0].v = _113[0].v ^ _113[1].v; + _113[1].v = 0; + } + const lists: rt.Iter> = rt.listIter(rt.listNew()); + const owned: rt.Iter> = rt.listIter(rt.listNew()); + rt.iterEmpty() as rt.Iter>; + const iter9: rt.Iter> = rt.iterChain(rt.iterChain(rt.iterChain(rt.iterEmpty() as rt.Iter>, rt.iterSliceMut(rt.byteVecView(v))), lists), owned); + while (true) { + const _128: rt.Place | rt.Done = iter9.next(); + if (_128 === rt.DONE) { + break; + } + _128.v = rt.u8WrappingSub(_128.v, 1); + } rt.iterExtendBytes(out, rt.iterSlice(rt.byteVecView(v)), undefined, true); rt.iterExtendBytes(out, rt.iterOption(opt), undefined, true); rt.iterExtendBytes(out, rt.iterChain(rt.iterResult(res), rt.iterOption(err$1nqzzwu(asRef$0ssaoss(res)))), undefined, true); @@ -1446,10 +1490,10 @@ export function adapted(data: rt.ByteSlice, out: rt.ByteVec): void { let other: rt.u8 | undefined = option_map$0cs071d(opt, (x: rt.u8): rt.u8 => { return x ^ 1; }); - const _114: boolean = rt.iterEq(rt.iterOption(opt), rt.iterOption(rt.optionAsMut(rt.place(() => other, (value7) => other = value7))), (a04: rt.u8, a14: rt.Place): boolean => { + const _166: boolean = rt.iterEq(rt.iterOption(opt), rt.iterOption(rt.optionAsMut(rt.place(() => other, (value8) => other = value8))), (a04: rt.u8, a14: rt.Place): boolean => { return a04 === a14.v; }, true); - const _118: boolean = rt.iterEq(rt.iterResult(res), rt.iterOption(rt.optionAsMut(rt.place(() => opt, (value8) => opt = value8))), (a05: rt.u8, a15: rt.Place): boolean => { + const _170: boolean = rt.iterEq(rt.iterResult(res), rt.iterOption(rt.optionAsMut(rt.place(() => opt, (value9) => opt = value9))), (a05: rt.u8, a15: rt.Place): boolean => { return a05 === a15.v; }, true); rt.iterExtendBytes(out, rt.iterSlice(rt.bytesMap([rt.iterEq(rt.iterSlice(rt.byteSliceView(rt.byteVecSlice(copy))), rt.iterSliceMut(rt.byteVecView(v)), (a0: rt.u8, a1: rt.Place): boolean => { @@ -1458,7 +1502,7 @@ export function adapted(data: rt.ByteSlice, out: rt.ByteVec): void { return a02 === a12.v; }, true), rt.iterEq(rt.iterSkip(rt.iterSlice(rt.byteSliceView(rt.byteVecSlice(copy))), 1), rt.iterSliceMut(rt.byteVecView(v)), (a03: rt.u8, a13: rt.Place): boolean => { return a03 === a13.v; - }, true), _114, _118, !rt.iterEq(rt.iterSlice(rt.byteSliceView(data)), rt.iterOption(rt.resultAsMut(rt.place(() => res, (value9) => res = value9))), (a06: rt.u8, a16: rt.Place): boolean => { + }, true), _166, _170, !rt.iterEq(rt.iterSlice(rt.byteSliceView(data)), rt.iterOption(rt.resultAsMut(rt.place(() => res, (value10) => res = value10))), (a06: rt.u8, a16: rt.Place): boolean => { return a06 === a16.v; }, true)], (a07: boolean): rt.u8 => { return a07 ? 1 : 0; diff --git a/ts/fixtures/lang/refs/src/refs.rs b/ts/fixtures/lang/refs/src/refs.rs index 31aae4b1b..c6cc0a013 100644 --- a/ts/fixtures/lang/refs/src/refs.rs +++ b/ts/fixtures/lang/refs/src/refs.rs @@ -935,8 +935,8 @@ fn parity(b: u8) -> Result { } /// A `&mut` to a `Vec`, an `Option`, or a `Result` of bytes that an adaptor is given, -/// beside items of no places, lends each element as its place to write through. One a -/// consumer is given compares as values. +/// beside items of no places or an empty iterator, lends each element as its place to write +/// through. One a consumer is given compares as values. pub fn adapted(data: &[u8], out: &mut Vec) { let mut v = data.to_vec(); let mut opt = data.first().copied(); @@ -957,6 +957,22 @@ pub fn adapted(data: &[u8], out: &mut Vec) { for x in None.into_iter().chain(&mut v).chain(&mut opt).chain(&mut res) { *x = x.wrapping_add(5); } + // An empty iterator lends nothing, on either side. + for x in std::iter::empty().chain(&mut v).chain(std::iter::empty()) { + *x = x.rotate_left(3); + } + for (x, y) in v.iter_mut().zip(std::iter::empty::<&mut u8>()) { + std::mem::swap(x, y); + } + for (x, y) in std::iter::empty::<&mut u8>().zip(&mut opt) { + *x ^= *y; + *y = 0; + } + let lists = >::default(); + let owned = >::default(); + for x in std::iter::Empty::default().chain(&mut v).chain(lists).chain(owned) { + *x = x.wrapping_sub(1); + } out.extend(&v); out.extend(opt); out.extend(res.iter().chain(res.as_ref().err())); diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index 92e7462da..78ab2e7e7 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -420,6 +420,24 @@ pub fn empty_iter(tcx: TyCtxt<'_>, did: DefId) -> bool { is_std(tcx, did) && std_path(tcx, did) == ["iter", "sources", "empty", "Empty"] } +/// Whether `did` is a list's `Iter`, `IterMut`, or `IntoIter`. +fn list_iter(tcx: TyCtxt<'_>, did: DefId) -> bool { + is_std(tcx, did) + && matches!( + std_path(tcx, did) + .iter() + .map(String::as_str) + .collect::>() + .as_slice(), + ["collections", "linked_list", "Iter" | "IterMut" | "IntoIter"] + ) +} + +/// Whether `Default::default()` of `ty` is an iterator that yields nothing. +pub fn empty_default(tcx: TyCtxt<'_>, ty: Ty<'_>) -> bool { + matches!(ty.kind(), ty::Adt(adt, _) if empty_iter(tcx, adt.did()) || list_iter(tcx, adt.did())) +} + /// Whether a `ty`'s value is a JS string: a `str`, a `String`, a `Box`, or a `Cow`. pub fn string<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> bool { match ty.kind() { @@ -3333,19 +3351,7 @@ pub fn default_value<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span, _ if nothing(tcx, ty) => Expr::undefined(), ty::Adt(adt, _) if empty_iter(tcx, adt.did()) => b.zero_sized_std(ty, Vec::new(), span).ok()?, // A list's iterators, over an empty list. - ty::Adt(adt, _) - if is_std(tcx, adt.did()) - && matches!( - std_path(tcx, adt.did()) - .iter() - .map(String::as_str) - .collect::>() - .as_slice(), - ["collections", "linked_list", "Iter" | "IterMut" | "IntoIter"] - ) => - { - Expr::rt("listIter", vec![Expr::rt("listNew", Vec::new())]) - } + ty::Adt(adt, _) if list_iter(tcx, adt.did()) => Expr::rt("listIter", vec![Expr::rt("listNew", Vec::new())]), ty::Tuple(parts) => Expr::Array(parts.iter().map(|t| part(b, t, out)).collect()), ty::Array(elem, n) => { let n = n.try_to_target_usize(tcx)?; diff --git a/ts/src/lower/mir/call.rs b/ts/src/lower/mir/call.rs index 5717d0991..43d764954 100644 --- a/ts/src/lower/mir/call.rs +++ b/ts/src/lower/mir/call.rs @@ -381,8 +381,9 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { // Mappings that lend an element JS copies as its place, as a map's `get_mut` does, the // item a `Peekable` peeked, the place a `&mut dyn Any` wraps, a `RefCell` key's value, // a `Result`'s payload, the value of an `Arc`, a lock, or a cell, what a write guard - // lends its map or its leak, the `&mut` an iterator's `by_ref` is given, or what `zip` - // makes of the collection it is given by `into_iter`, as the free `zip` does of either. + // lends its map or its leak, the `&mut` an iterator's `by_ref` is given, what `zip` + // makes of the collection it is given by `into_iter`, as the free `zip` does of either, + // or an empty iterator, made or defaulted, which lends nothing. let lends = match library::key(tcx, def_id) { Some((owner, name)) => match (owner.as_str(), name.as_str()) { ("slice", "get_mut" | "first_mut" | "last_mut" | "iter_mut" | "split_first_mut" | "split_last_mut") @@ -396,6 +397,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { | ("RefMut", "leak") | ("RefCell", "undo_leak") | ("Iterator", "by_ref") + | ("empty", "empty") | ( "RefMut" | "MutexGuard" | "MappedMutexGuard" | "RwLockWriteGuard" | "MappedRwLockWriteGuard", "map" | "filter_map" | "try_map" | "map_split", @@ -403,6 +405,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { ("IntoIterator", "into_iter") => gargs.types().next().is_some_and(into_places), ("Iterator", "zip") => gargs.types().nth(1).is_some_and(into_places), ("zip", "zip") => gargs.types().take(2).any(into_places), + ("Default", "default") => gargs.types().next().is_some_and(|ty| library::empty_default(tcx, ty)), _ => false, }, None => false, From 18ab8cb69c88f9d8a20f5ad50c5a62022c6d5b21 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:44:53 -0800 Subject: [PATCH 3/3] test(ts): translate a list's empty IterMut of bytes rather than refuse it Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.txt | 1 - ts/fixtures/reject/expected.txt | 1 - ts/fixtures/reject/src/values.rs | 6 ++---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ts/fixtures/reject/census.txt b/ts/fixtures/reject/census.txt index 5b9a5691e..4595d504c 100644 --- a/ts/fixtures/reject/census.txt +++ b/ts/fixtures/reject/census.txt @@ -243,7 +243,6 @@ fixtures/reject/src/values.rs:float+1:2: arithmetic on `f16` fixtures/reject/src/values.rs:generic_unit+1:2: `values::Maybe<()>` is not supported: its field `value` is `Option`, typed `T | undefined`, whose `None` would be `rt.None` as `Option<()>` fixtures/reject/src/values.rs:lists+1:4: `std::collections::LinkedList::::retain` with a mutable reference to a value JS copies, which its mapping would copy fixtures/reject/src/values.rs:lists+2:4: `clone_from` of `std::collections::LinkedList`, whose items' `clone_from` is their own, which is not supported yet -fixtures/reject/src/values.rs:lists+3:2: `std::default::Default::default` with a mutable reference to a value JS copies, which its mapping would copy fixtures/reject/src/values.rs:manual_impls+2:2: unsupported call to ` as std::fmt::Debug>::fmt` fixtures/reject/src/values.rs:manual_impls+2:25: unsupported call to ` as std::cmp::PartialEq>::eq` fixtures/reject/src/values.rs:manual_impls+2:34: a constant of type `std::mem::ManuallyDrop` diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index bab20641e..df1fd4e97 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -243,7 +243,6 @@ fixtures/reject/src/values.rs:float+1:2: arithmetic on `f16` fixtures/reject/src/values.rs:generic_unit+1:2: `values::Maybe<()>` is not supported: its field `value` is `Option`, typed `T | undefined`, whose `None` would be `rt.None` as `Option<()>` fixtures/reject/src/values.rs:lists+1:4: `std::collections::LinkedList::::retain` with a mutable reference to a value JS copies, which its mapping would copy fixtures/reject/src/values.rs:lists+2:4: `clone_from` of `std::collections::LinkedList`, whose items' `clone_from` is their own, which is not supported yet -fixtures/reject/src/values.rs:lists+3:2: `std::default::Default::default` with a mutable reference to a value JS copies, which its mapping would copy fixtures/reject/src/values.rs:manual_impls+2:11: unsupported call to ` as std::fmt::Debug>::fmt` fixtures/reject/src/values.rs:manual_impls+2:25: unsupported call to ` as std::cmp::PartialEq>::eq` fixtures/reject/src/values.rs:manual_impls+2:34: a constant of type `std::mem::ManuallyDrop` diff --git a/ts/fixtures/reject/src/values.rs b/ts/fixtures/reject/src/values.rs index 9f6823bfd..2882aa0da 100644 --- a/ts/fixtures/reject/src/values.rs +++ b/ts/fixtures/reject/src/values.rs @@ -87,12 +87,10 @@ fn reclone(a: Option>) -> Option> { } /// `LinkedList`'s unstable `retain`, which no mapping covers, as none does its other -/// unstable methods; its `clone_from` of items whose own `clone_from` it would call; and -/// an empty `IterMut` of bytes, whose places nothing makes. -pub fn lists(l: &mut std::collections::LinkedList, r: &mut std::collections::LinkedList) -> usize { +/// unstable methods; and its `clone_from` of items whose own `clone_from` it would call. +pub fn lists(l: &mut std::collections::LinkedList, r: &mut std::collections::LinkedList) { l.retain(|x| *x > 1); r.clone_from(&std::collections::LinkedList::new()); - std::collections::linked_list::IterMut::::default().count() } /// `ManuallyDrop`'s impls beyond holding a value: compared, formatted, and a constant.