diff --git a/quest/m3/README.md b/quest/m3/README.md index 9c5583c69..ad08cc06c 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -36,6 +36,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 - [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 - [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 diff --git a/quest/m3/ts-dyn.md b/quest/m3/ts-dyn.md index a8548dfcd..bef2ca0ae 100644 --- a/quest/m3/ts-dyn.md +++ b/quest/m3/ts-dyn.md @@ -14,7 +14,7 @@ Decided with the maintainer on 2026-10-27: a stock crate translates with no anno config, so trait objects move from never to not yet. - `dyn Debug` and `dyn Display` translate as functions writing their value, behind `&`, - `Box`, `Arc`, and `Rc`, `dyn Future` + `&mut`, `Box`, `Arc`, and `Rc`, `dyn Future` as an `rt.Future`, `dyn Any` as the class a downcast checks for, `dyn Fn*` as the closure itself, and the translated crates' traits as an object with the trait's methods. Build the rest on those. diff --git a/quest/m3/ts-string-add.md b/quest/m3/ts-string-add.md new file mode 100644 index 000000000..8e6bc28d5 --- /dev/null +++ b/quest/m3/ts-string-add.md @@ -0,0 +1,13 @@ +# [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 0e7692f93..98ec9b600 100644 --- a/ts/README.md +++ b/ts/README.md @@ -229,7 +229,8 @@ translate, as do `Display` and `Debug` impls, derived and thiserror's included. runtime ports `core::fmt`, so the text matches native Rust's byte for byte. A placeholder becomes a function writing its value, and `fmt::Arguments` an array of literal pieces and such functions: `rt.fmtWriteFmt(f, ["code=", (f) => rt.u16Display(code, f)])`. A `&dyn -Debug` or `&dyn Display` is such a function too, and so is a `Box` of one, `rt.fmtOwned` +Debug` or `&dyn Display` is such a function too, and so is a `&mut` one, which reads +through the `rt.Place` of a value JS copies as it writes, a `Box` of one, `rt.fmtOwned` of the value, which drops it as the box drops, and an `Arc` or `Rc` of one, which the last pointer drops, as an `Arc` of the crate's own trait object does. Format specs take every option but `{:p}`: fill, alignment, `+`, `#`, `0`, `x?` and `X?`, a width and a precision, each also from an @@ -475,8 +476,8 @@ a call calls as it does the closure, and which a box drops as it does the closur that `Fn` or `FnMut` calls, boxed as a `dyn FnOnce`, is `rt.once` of it, which drops it after the call, as an `FnOnce` closure's body drops what it owns, and a box of a reference to one is `rt.lent` of it, which never drops it. A trait object of std's -or a mapped crate's trait other than `dyn Debug` and `dyn Display` behind `&`, `Box`, -`Arc`, or `Rc`, `dyn Future`, +or a mapped crate's trait other than `dyn Debug` and `dyn Display` behind `&`, `&mut`, +`Box`, `Arc`, or `Rc`, `dyn Future`, `dyn Any`, `dyn Buf`, and the function traits, of a generic trait or one with an associated type, of a byte array, or behind any other pointer, such as a `Weak` or a struct's last field, is refused, and so are an upcast to `dyn Any` and @@ -856,7 +857,7 @@ they need. Byte reinterpretation, pointer addresses, pointer arithmetic on a value JS keeps as an object (other than wrapping arithmetic on a null or dangling pointer), memory from `alloc` other than an array of values JS copies, and unions whose fields overlap in different types ([unsafe](/quest/m3/ts-unsafe.md)), and trait objects other than -`&dyn Debug`, `&dyn Display`, `dyn Future`, `dyn Any`, `dyn Fn*`, `dyn Buf`, and the translated crates' traits' +`dyn Debug`, `dyn Display`, `dyn Future`, `dyn Any`, `dyn Fn*`, `dyn Buf`, and the translated crates' traits' behind `&`, `&mut`, `Box`, or `Arc` ([trait objects](/quest/m3/ts-dyn.md)), fail translation. Until they translate, an item that needs one, such as a `dyn Error`, binds to hand-written TS ([bindings](#bindings)). diff --git a/ts/fixtures/lang/expected/fmt.Gauge.ts b/ts/fixtures/lang/expected/fmt.Gauge.ts new file mode 100644 index 000000000..8d22218ab --- /dev/null +++ b/ts/fixtures/lang/expected/fmt.Gauge.ts @@ -0,0 +1,16 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** Writes what its cell holds as it is written. */ +export interface Gauge { + _0: rt.Cell; +} + +export function fmt$fmt_Debug(self: Gauge, f: rt.Formatter): rt.Result { + const _5: rt.DebugTuple = rt.fmtDebugTuple(f, "Gauge"); + const _9: rt.u8 = self._0.v; + return rt.debugTupleFinish(rt.debugTupleField(_5, (f: rt.Formatter) => rt.u8Debug(_9, f))); +} + +//# sourceMappingURL=fmt.Gauge.ts.map diff --git a/ts/fixtures/lang/expected/fmt.ts b/ts/fixtures/lang/expected/fmt.ts index 80617720d..723e2dd50 100644 --- a/ts/fixtures/lang/expected/fmt.ts +++ b/ts/fixtures/lang/expected/fmt.ts @@ -10,6 +10,7 @@ import * as Capped from "./fmt.Capped.ts"; import * as Clip from "./fmt.Clip.ts"; import * as Dropper from "./fmt.Dropper.ts"; import * as Fails from "./fmt.Fails.ts"; +import * as fmt_Gauge from "./fmt.Gauge.ts"; import * as Hex from "./fmt.Hex.ts"; import * as Kind from "./fmt.Kind.ts"; import * as Manual from "./fmt.Manual.ts"; @@ -2199,7 +2200,7 @@ export function writes(data: rt.ByteSlice, out: rt.ByteVec): void { } export function objects(data: rt.ByteSlice, out: rt.ByteVec): void { - let _218: boolean = false; + let _372: boolean = false; const log: rt.Arc> = new rt.Arc(new rt.RefCell(new rt.ByteVec())); const dropper: (a0: rt.u8) => Dropper.Dropper = (tag: rt.u8): Dropper.Dropper => { return new Dropper.Dropper(tag, rt.arcClone(log)); @@ -2210,8 +2211,8 @@ export function objects(data: rt.ByteSlice, out: rt.ByteVec): void { _6.value = new rt.ByteVec(); const tags: rt.ByteVec = taken; rt.refMutRelease(_6); - const args18: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtList(rt.byteVecView(tags), f, rt.u8Debug)]; - put(out2, rt.fmtFormat([args18[0], " "])); + const args32: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtList(rt.byteVecView(tags), f, rt.u8Debug)]; + put(out2, rt.fmtFormat([args32[0], " "])); }; const shown: rt.Fmt[] = []; const written: rt.Fmt[] = []; @@ -2259,7 +2260,8 @@ export function objects(data: rt.ByteSlice, out: rt.ByteVec): void { rt.dropAll(shown); log[Symbol.dispose](); } else { - const _121: rt.Arc<[rt.u8, rt.u8]> = new rt.Arc([data.data[data.start], data.data[data.start + 1]] as [rt.u8, rt.u8]); + const b: rt.u8 = data.data[data.start + 1]; + const _121: rt.Arc<[rt.u8, rt.u8]> = new rt.Arc([data.data[data.start], b] as [rt.u8, rt.u8]); const shared: rt.Arc = new rt.ArcDyn(_121, rt.fmtOwned(_121.value, (value: [rt.u8, rt.u8], f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.u8Debug(value[0], f), (f: rt.Formatter) => rt.u8Debug(value[1], f)], f))); const kept: rt.Arc = rt.arcClone(shared); const _135: rt.usize = kept.strong; @@ -2268,15 +2270,15 @@ export function objects(data: rt.ByteSlice, out: rt.ByteVec): void { const counted: rt.Arc = new rt.Arc(dropper(data.data[data.start])); const _149: rt.Arc = rt.arcClone(counted); const asDisplay: rt.Arc = new rt.ArcDyn(_149, rt.fmtOwned(_149.value, (value: Dropper.Dropper, f: rt.Formatter) => Dropper.fmt$fmt_Display(value, f), rt.drop)); - _218 = true; + _372 = true; const asDebug: rt.Arc = new rt.ArcDyn(counted, rt.fmtOwned(counted.value, (value: Dropper.Dropper, f: rt.Formatter) => Dropper.fmt$fmt_Debug(value, f), rt.drop)); const _160: rt.usize = asDebug.strong; const args12: rt.Fmt[] = [(f: rt.Formatter) => asDisplay.value(f), (f: rt.Formatter) => rt.usizeDisplay(_160, f)]; put(out, rt.fmtFormat([rt.fmtSpec({ align: "<", width: 5 }, args12[0]), "| ", args12[1], " "])); asDisplay[Symbol.dispose](); dropped(out); - if (rt.u8Rem(data.data[data.start + 1], 2) === 0) { - _218 = false; + if (rt.u8Rem(b, 2) === 0) { + _372 = false; asDebug[Symbol.dispose](); } else { const args14: rt.Fmt[] = [(f: rt.Formatter) => asDebug.value(f)]; @@ -2286,11 +2288,57 @@ export function objects(data: rt.ByteSlice, out: rt.ByteVec): void { shared[Symbol.dispose](); const _198: rt.usize = kept.strong; const args16: rt.Fmt[] = [(f: rt.Formatter) => kept.value(f), (f: rt.Formatter) => rt.usizeDisplay(_198, f)]; - put(out, rt.fmtFormat([args16[0], " ", args16[1]])); - if (_218) { + put(out, rt.fmtFormat([args16[0], " ", args16[1], " "])); + let n: rt.u8 = rt.u8WrappingMul(data.data[data.start], 3); + const args18: rt.Fmt[] = [(f: rt.Formatter) => rt.u8Display(b, f)]; + let text: string = rt.fmtFormat(["t", args18[0]]); + const owned: Dropper.Dropper = dropper(b); + const part5 = rt.u8WrappingAdd(b, 1); + let boxed: rt.Fmt = rt.fmtOwned(dropper(part5), (value: Dropper.Dropper, f: rt.Formatter) => Dropper.fmt$fmt_Debug(value, f), rt.drop); + const cell: rt.Cell = new rt.Cell(data.data[data.start]); + const pair: [fmt_Gauge.Gauge, rt.u8] = [{ _0: cell }, b]; + const _237: rt.Place = rt.place(() => n, (value) => n = value); + const arg = rt.place(() => text, (value2) => text = value2); + const _238: rt.Fmt = (f: rt.Formatter) => rt.fmtStr(arg.v, f); + const _240: rt.Fmt = (f: rt.Formatter) => Dropper.fmt$fmt_Debug(owned, f); + const arg2 = rt.place(() => boxed, (value3) => boxed = value3); + const objects: rt.Fmt[] = [(f: rt.Formatter) => rt.u8Debug(_237.v, f), _238, _240, (f: rt.Formatter) => arg2.v(f), (f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => fmt_Gauge.fmt$fmt_Debug(pair[0], f), (f: rt.Formatter) => rt.u8Debug(pair[1], f)], f)]; + const iter4: rt.Iter> = rt.iterSliceMut(objects); + while (true) { + const _251: rt.Place | rt.Done = iter4.next(); + if (_251 === rt.DONE) { + break; + } + const args20: rt.Fmt[] = [(f: rt.Formatter) => _251.v(f)]; + put(out, rt.fmtFormat([args20[0], " ", rt.fmtSpec({ alternate: true }, args20[0]), " "])); + } + const _269: rt.u8 = rt.u8WrappingAdd(cell.v, 1); + rt.cellSet(cell, _269); + const again: rt.Fmt = objects[rt.idx(objects, 4)]; + const args22: rt.Fmt[] = [(f: rt.Formatter) => again(f)]; + put(out, rt.fmtFormat([args22[0], " "])); + const args24: rt.Fmt[] = [(f: rt.Formatter) => again(f)]; + put(out, rt.fmtFormat([args24[0], " "])); + n = rt.u8WrappingAdd(n, 1); + const _304: rt.Place = rt.place(() => n, (value4) => n = value4); + const part6 = (f: rt.Formatter) => rt.u8Display(_304.v, f); + const args26: rt.Fmt[] = [(f: rt.Formatter) => part6(f)]; + put(out, rt.fmtFormat(["[", rt.fmtSpec({ align: ">", width: 5 }, args26[0]), "] "])); + const part7 = (f: rt.Formatter) => Dropper.fmt$fmt_Display(owned, f); + const args28: rt.Fmt[] = [(f: rt.Formatter) => part7(f)]; + put(out, rt.fmtFormat(["[", rt.fmtSpec({ align: "^", width: 6 }, args28[0]), "] "])); + text = text + "!"; + const arg3 = rt.place(() => text, (value5) => text = value5); + const part8 = (f: rt.Formatter) => rt.fmtPad(f, arg3.v); + const args30: rt.Fmt[] = [(f: rt.Formatter) => part8(f)]; + put(out, rt.fmtFormat([args30[0], " "])); + rt.drop(boxed); + dropped(out); + owned[Symbol.dispose](); + if (_372) { asDebug[Symbol.dispose](); } - _218 = false; + _372 = false; kept[Symbol.dispose](); rt.drop(up); rt.dropAll(shown); diff --git a/ts/fixtures/lang/src/fmt.rs b/ts/fixtures/lang/src/fmt.rs index 736897f03..0a700239b 100644 --- a/ts/fixtures/lang/src/fmt.rs +++ b/ts/fixtures/lang/src/fmt.rs @@ -5,7 +5,7 @@ #![allow(dead_code, reason = "fields only `Debug` reads")] -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; use std::fmt::{self, Debug, Display}; use std::ops::{Range, RangeToInclusive}; @@ -1118,6 +1118,15 @@ pub fn writes(data: &[u8], out: &mut Vec) { put(out, &twice.0); } +/// Writes what its cell holds as it is written. +struct Gauge<'a>(&'a Cell); + +impl Debug for Gauge<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Gauge").field(&self.0.get()).finish() + } +} + /// Logs its tag as it drops, and writes it padded by `Display`. struct Dropper { tag: u8, @@ -1196,5 +1205,36 @@ pub fn objects(data: &[u8], out: &mut Vec) { } dropped(out); drop(shared); - put(out, &format!("{kept:?} {}", Arc::strong_count(&kept))); + put(out, &format!("{kept:?} {} ", Arc::strong_count(&kept))); + + // Mutably borrowed, written as a shared borrow would be, from where the value is: a + // number and a string JS copies, the crate's type, a box of one, and a `Cell` set + // while borrowed. + let mut n = a.wrapping_mul(3); + let mut text = format!("t{b}"); + let mut owned = dropper(*b); + let mut boxed: Box = Box::new(dropper(b.wrapping_add(1))); + let cell = Cell::new(*a); + let mut pair = (Gauge(&cell), *b); + { + let mut objects: Vec<&mut dyn Debug> = vec![&mut n, &mut text, &mut owned, &mut boxed, &mut pair]; + for o in objects.iter_mut() { + put(out, &format!("{o:?} {o:#?} ")); + } + cell.set(cell.get().wrapping_add(1)); + let again: &mut dyn Debug = &mut *objects[4]; + put(out, &format!("{again:?} ")); + let shared: &dyn Debug = again; + put(out, &format!("{shared:?} ")); + } + n = n.wrapping_add(1); + let shown: &mut dyn Display = &mut n; + put(out, &format!("[{shown:>5}] ")); + let written: &mut dyn Display = &mut owned; + put(out, &format!("[{written:^6}] ")); + text.push('!'); + let last: &mut dyn Display = &mut text; + put(out, &format!("{last} ")); + drop(boxed); + dropped(out); } diff --git a/ts/src/lower/fmt.rs b/ts/src/lower/fmt.rs index dc7ddcda5..140e54a09 100644 --- a/ts/src/lower/fmt.rs +++ b/ts/src/lower/fmt.rs @@ -104,7 +104,7 @@ fn formatter() -> Type { impl<'b, 'a, 'tcx> Body<'b, 'a, 'tcx> { /// The formatting trait of a `&dyn Debug` or the like. pub fn fmt_object(&self, ty: Ty<'tcx>) -> Option { - let ty::Ref(_, inner, rustc_ast::Mutability::Not) = ty.kind() else { + let ty::Ref(_, inner, _) = ty.kind() else { return None; }; let ty::Dynamic(preds, ..) = inner.kind() else {