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
1 change: 1 addition & 0 deletions quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion quest/m3/ts-dyn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions quest/m3/ts-string-add.md
Original file line number Diff line number Diff line change
@@ -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 `<String as Add<&str>>::add`", while `+=` maps.

Public API: the translator's output.
9 changes: 5 additions & 4 deletions ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)).

Expand Down
16 changes: 16 additions & 0 deletions ts/fixtures/lang/expected/fmt.Gauge.ts
Original file line number Diff line number Diff line change
@@ -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<rt.u8>;
}

export function fmt$fmt_Debug(self: Gauge, f: rt.Formatter): rt.Result<undefined, rt.FmtError> {
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
68 changes: 58 additions & 10 deletions ts/fixtures/lang/expected/fmt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<rt.RefCell<rt.ByteVec>> = 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));
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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<rt.Fmt> = 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.Fmt> = rt.arcClone(shared);
const _135: rt.usize = kept.strong;
Expand All @@ -2268,15 +2270,15 @@ export function objects(data: rt.ByteSlice, out: rt.ByteVec): void {
const counted: rt.Arc<Dropper.Dropper> = new rt.Arc(dropper(data.data[data.start]));
const _149: rt.Arc<Dropper.Dropper> = rt.arcClone(counted);
const asDisplay: rt.Arc<rt.Fmt> = 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<rt.Fmt> = 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)];
Expand All @@ -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<rt.u8> = new rt.Cell(data.data[data.start]);
const pair: [fmt_Gauge.Gauge, rt.u8] = [{ _0: cell }, b];
const _237: rt.Place<rt.u8> = 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.Place<rt.Fmt>> = rt.iterSliceMut(objects);
while (true) {
const _251: rt.Place<rt.Fmt> | 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.u8> = 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);
Expand Down
44 changes: 42 additions & 2 deletions ts/fixtures/lang/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1118,6 +1118,15 @@ pub fn writes(data: &[u8], out: &mut Vec<u8>) {
put(out, &twice.0);
}

/// Writes what its cell holds as it is written.
struct Gauge<'a>(&'a Cell<u8>);

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,
Expand Down Expand Up @@ -1196,5 +1205,36 @@ pub fn objects(data: &[u8], out: &mut Vec<u8>) {
}
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<dyn Debug> = 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);
}
2 changes: 1 addition & 1 deletion ts/src/lower/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Trait> {
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 {
Expand Down