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/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The fork's feature quests folded into the [exporter](/quest/m1/ffi-export/README
## Required

- [Byte-slice locals that crash the translator](/quest/m2/ts-scalar-ice.md) - `take_apart` never reaches the arm `find_scalars` promised it wouldn't
- [Replacing an indexed element whose drop may panic](/quest/m2/ts-index-cleanup.md) - `v[i] = x` type-checks and unwinds as Rust does when the old element's drop may panic
- [Byte slices without objects](/quest/m2/ts-byte-views/README.md) - reading through a translated cursor allocates no object per read
- [Async in the TypeScript translator](/quest/m2/ts-async.md) - moq-net's and kio's async code translates with Rust's lazy futures
- [Shared ownership](/quest/m2/ts-shared.md) - `Arc`/`Rc`/`Weak` keep exact counts, and locks and atomics become plain values
Expand Down
28 changes: 28 additions & 0 deletions quest/m2/ts-index-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# [S] Replacing an indexed element whose drop may panic

## Goal

rust2-ts translates `v[i] = x`, where dropping the old element may panic, into
TypeScript that `tsc` accepts and that drops and replaces as native Rust does on every
path, a panic in the old element's `Drop` included, proven by differential tests.

## Plan

Found while testing [trait objects](/quest/m3/ts-dyn.md). The drop-and-replace through
`IndexMut` lowers the borrowed element as `v[at2]`, where `at2` is a `const` the body
declares inside the `try`. The unwind cleanup in the `catch` replays the assignment as
`v[at2] = x`, out of `at2`'s scope, so `tsc` rejects the output. A `Vec` of a type with
a `Drop` impl reproduces it, no trait object needed:

```rust
let mut v = vec![Bomb::new()];
v[0] = Bomb::new();
```

kio's `self.children[index] = Some(Occupant { .. })` and `waiters[i] = Waiter::noop()`
have that shape.

- Fix it where the cleanup reads what the body spilled, so every temporary an aliased
place names is in scope in the `catch`, not by special-casing indexing.

Public API: none.
19 changes: 15 additions & 4 deletions quest/m3/ts-dyn.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@

## Goal

[rust2-ts](/ts/README.md) translates trait objects (`&dyn Trait` and `Box<dyn Trait>`), with
Rust's dispatch and drop order, proven by differential tests against native Rust, so a
consumer neither rewrites nor binds them.
[rust2-ts](/ts/README.md) translates the trait objects moq uses that it still refuses
(`Arc<dyn Trait>`, `Box<dyn FnMut>` and `&dyn Fn`, and other crates' traits), with Rust's
dispatch and drop order, proven by differential tests against native Rust, so a consumer
neither rewrites nor binds them.

## Plan

Decided with the maintainer on 2026-10-27: a stock crate translates with no annotations or
config, so trait objects move from never to not yet. Until this lands, moq rewrites
`dyn Expiry` as an enum and binds its boxed closures ([bindings](/ts/README.md#bindings)).

- `&dyn Debug`, `&dyn Display`, and `dyn Future` already translate; build on how they do.
- `&dyn Debug` and `&dyn Display` translate as functions writing their value, `dyn Future`
as an `rt.Future`, and `&dyn Trait`, `&mut dyn Trait`, and `Box<dyn Trait>` of the
crate's own traits as an object with the trait's methods, which a class per type wraps
the value in. Build the rest on those.
- `Arc<dyn Trait>`: an `Arc` unsized from an `Arc<T>` shares its count, so the wrapper
can't replace the `Arc`'s value; its clone, drop, and deref must still see one count.
`Arc<dyn Future>` has the same problem.
- Another crate's traits: `Fn*` objects can be the closure itself, as `&dyn Debug` is the
function writing it; `Iterator`, `Error`, `Any`, and `bytes::Buf` need their std
methods mapped onto the object.
- Generic traits and associated types name their arguments in the object's type.

Public API: the translator's output.
18 changes: 16 additions & 2 deletions ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ in `rt/str.ts`, collections in `rt/collections.ts`, ownership, atomics, and asyn
| `Ordering`, `DefaultHasher` | `rt.Ordering`, which is `-1`, `0`, or `1`; `rt.Hasher` |
| `Duration` | `rt.Duration`: `{ secs, nanos }`, std's whole seconds as an `rt.u64` and the nanoseconds past them, never changed once made |
| closure | an arrow function, with `[Symbol.dispose]` if it owns a value that drops |
| `&dyn Trait`, `&mut dyn Trait`, `Box<dyn Trait>` | an object with the trait's methods, its supertraits' included: `shape.area$Shape()` |
| constant | its value, inlined as Rust inlines it |
| a type that drops | a class with `[Symbol.dispose]`: its `Drop` impl, then its fields |
| a type's function | a function of the type's module, taking `self` first: `Decoder.slice(dec, 4)` after `import * as Decoder` |
Expand Down Expand Up @@ -232,6 +233,19 @@ panics leaves the rest to drop as Rust's does: a class's glue and the runtime's
go on to the rest of the value and its container, but a hash table leaks its other
entries, as std's does.

A trait object of the crate's own trait, behind `&`, `&mut`, or `Box`, is an object with
a method for each function a call can dispatch to, its supertraits' included, named for
its trait as an impl's function is and taking what follows `self`: `shape.area$Shape()`.
The trait's module exports its type, `Shape.Shape` from `dyns.Shape.ts`, which a TS
caller can implement. A coercion wraps the value in a class of its type's, `new
Shape.Square(sq)`, whose methods call that type's impl; one from a `&mut` to a value JS
copies holds its `rt.Place`, so `&mut self` writes where the value is. An upcast is the
same object. A `Box<dyn Trait>` drops what it holds through its `[Symbol.dispose]`, which
a TS caller's object may leave out. A trait object of another crate's trait, but for
`&dyn Debug`, `&dyn Display`, and `dyn Future`, of a generic trait or one with an
associated type, of a byte array, or behind any other pointer, such as an `Arc` or a
struct's last field, is refused.

An `async fn` returns a future without running anything: an instance of a class of its
own (`sumFuture`), the state machine MIR's coroutine transform makes. Its fields are the
arguments and the locals held across an `.await`, and its `poll` resumes at the state it
Expand Down Expand Up @@ -446,8 +460,8 @@ they need.
## Not yet

`unsafe` and raw pointers ([unsafe](/quest/m3/ts-unsafe.md)), and trait objects other than
`&dyn Debug`, `&dyn Display`, and `dyn Future` ([trait objects](/quest/m3/ts-dyn.md)), fail
translation. Until they translate, an item that needs one, such as a boxed closure, binds to
`&dyn Debug`, `&dyn Display`, `dyn Future`, and the crate's own traits' behind `&`, `&mut`,
or `Box` ([trait objects](/quest/m3/ts-dyn.md)), fail translation. Until they translate, an item that needs one, such as a boxed closure, binds to
hand-written TS ([bindings](#bindings)).

These fail translation: formatting and parsing an `f64`, and `f64::clamp`, whose panic formats them ([float formatting](/quest/m2/ts-model/floats.md)), `f32` ([`f32`](/quest/m3/ts-f32.md)), statics, async closures, a closure that owns a value
Expand Down
10 changes: 9 additions & 1 deletion ts/fixtures/lang/caller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,12 @@ const utf8 = [
[0x80, 0xf8, 0xff],
].map((bytes) => Uint8Array.of(op("utf8::decodes"), 0, ...bytes));

export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats];
/**
* `dyns::dyns` too short to run, with a `Tri` of each variant, at the ends of each side and
* count, and boxing each kind of shape.
*/
const dyns = [[], [1, 2], [3, 0, 7], [255, 1, 255, 0, 1, 2, 3, 4, 5], [0, 3, 2, 9, 255, 6, 128]].map((bytes) =>
Uint8Array.of(op("dyns::dyns"), 0, ...bytes),
);

export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...dyns];
90 changes: 90 additions & 0 deletions ts/fixtures/lang/expected/dyns.Named.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Generated by rust2-ts. Do not edit.

import * as rt from "./rt.ts";
import * as dyns_Square from "./dyns.Square.ts";
import * as dyns_Tri from "./dyns.Tri.ts";

export type Named =
| { label$Named: () => string; fmt$Debug: (a0: rt.Formatter) => rt.Result<undefined, rt.FmtError>; area$Shape: () => rt.u32; grow$Shape: (a0: rt.u8) => void; sides$Shape: () => rt.u8; intoArea$Shape: () => rt.u32; larger$Shape: (a0: rt.u32) => rt.u32; tally$Shape: (a0: rt.Place<rt.u32>) => void };

export class Square {
v: dyns_Square.Square;

constructor(v: dyns_Square.Square) {
this.v = v;
}

area$Shape(): rt.u32 {
return dyns_Square.area$Shape(this.v);
}

fmt$Debug(a: rt.Formatter): rt.Result<undefined, rt.FmtError> {
return dyns_Square.fmt$Debug(this.v, a);
}

grow$Shape(a: rt.u8): void {
return dyns_Square.grow$Shape(this.v, a);
}

intoArea$Shape(): rt.u32 {
return dyns_Square.intoArea$Shape(this.v);
}

label$Named(): string {
return dyns_Square.label$Named(this.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return dyns_Square.larger$Shape(this.v, a);
}

sides$Shape(): rt.u8 {
return dyns_Square.sides$Shape(this.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return dyns_Square.tally$Shape(this.v, a);
}
}

export class Tri {
v: dyns_Tri.Tri;

constructor(v: dyns_Tri.Tri) {
this.v = v;
}

area$Shape(): rt.u32 {
return dyns_Tri.area$Shape(this.v);
}

fmt$Debug(a: rt.Formatter): rt.Result<undefined, rt.FmtError> {
return dyns_Tri.fmt$Debug(this.v, a);
}

grow$Shape(a: rt.u8): void {
return dyns_Tri.grow$Shape(this, a);
}

intoArea$Shape(): rt.u32 {
return dyns_Tri.intoArea$Shape(this.v);
}

label$Named(): string {
return dyns_Tri.label$Named(this.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return dyns_Tri.larger$Shape(this.v, a);
}

sides$Shape(): rt.u8 {
return dyns_Tri.sides$Shape(this.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return dyns_Tri.tally$Shape(this.v, a);
}
}

//# sourceMappingURL=dyns.Named.ts.map
171 changes: 171 additions & 0 deletions ts/fixtures/lang/expected/dyns.Shape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Generated by rust2-ts. Do not edit.

import * as rt from "./rt.ts";
import { sides$Shape_u32, intoArea$Shape_u32, larger$Shape_u32, tally$Shape_u32, area$Shape_u32, grow$Shape_u32 } from "./dyns.ts";
import * as dyns_Square from "./dyns.Square.ts";
import * as dyns_Tri from "./dyns.Tri.ts";

export type Shape =
| { area$Shape: () => rt.u32; grow$Shape: (a0: rt.u8) => void; sides$Shape: () => rt.u8; intoArea$Shape: () => rt.u32; larger$Shape: (a0: rt.u32) => rt.u32; tally$Shape: (a0: rt.Place<rt.u32>) => void };

export class Square {
v: dyns_Square.Square;

constructor(v: dyns_Square.Square) {
this.v = v;
}

area$Shape(): rt.u32 {
return dyns_Square.area$Shape(this.v);
}

grow$Shape(a: rt.u8): void {
return dyns_Square.grow$Shape(this.v, a);
}

intoArea$Shape(): rt.u32 {
return dyns_Square.intoArea$Shape(this.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return dyns_Square.larger$Shape(this.v, a);
}

sides$Shape(): rt.u8 {
return dyns_Square.sides$Shape(this.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return dyns_Square.tally$Shape(this.v, a);
}
}

export class Tri {
v: dyns_Tri.Tri;

constructor(v: dyns_Tri.Tri) {
this.v = v;
}

area$Shape(): rt.u32 {
return dyns_Tri.area$Shape(this.v);
}

grow$Shape(a: rt.u8): void {
return dyns_Tri.grow$Shape(this, a);
}

intoArea$Shape(): rt.u32 {
return dyns_Tri.intoArea$Shape(this.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return dyns_Tri.larger$Shape(this.v, a);
}

sides$Shape(): rt.u8 {
return dyns_Tri.sides$Shape(this.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return dyns_Tri.tally$Shape(this.v, a);
}
}

export class Tri$Place {
v: rt.Place<dyns_Tri.Tri>;

constructor(v: rt.Place<dyns_Tri.Tri>) {
this.v = v;
}

area$Shape(): rt.u32 {
return dyns_Tri.area$Shape(this.v.v);
}

grow$Shape(a: rt.u8): void {
return dyns_Tri.grow$Shape(this.v, a);
}

intoArea$Shape(): rt.u32 {
return dyns_Tri.intoArea$Shape(this.v.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return dyns_Tri.larger$Shape(this.v.v, a);
}

sides$Shape(): rt.u8 {
return dyns_Tri.sides$Shape(this.v.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return dyns_Tri.tally$Shape(this.v.v, a);
}
}

export class u32 {
v: rt.u32;

constructor(v: rt.u32) {
this.v = v;
}

area$Shape(): rt.u32 {
return area$Shape_u32(this.v);
}

grow$Shape(a: rt.u8): void {
return grow$Shape_u32(this, a);
}

intoArea$Shape(): rt.u32 {
return intoArea$Shape_u32(this.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return larger$Shape_u32(this.v, a);
}

sides$Shape(): rt.u8 {
return sides$Shape_u32(this.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return tally$Shape_u32(this.v, a);
}
}

export class u32$Place {
v: rt.Place<rt.u32>;

constructor(v: rt.Place<rt.u32>) {
this.v = v;
}

area$Shape(): rt.u32 {
return area$Shape_u32(this.v.v);
}

grow$Shape(a: rt.u8): void {
return grow$Shape_u32(this.v, a);
}

intoArea$Shape(): rt.u32 {
return intoArea$Shape_u32(this.v.v);
}

larger$Shape(a: rt.u32): rt.u32 {
return larger$Shape_u32(this.v.v, a);
}

sides$Shape(): rt.u8 {
return sides$Shape_u32(this.v.v);
}

tally$Shape(a: rt.Place<rt.u32>): void {
return tally$Shape_u32(this.v.v, a);
}
}

//# sourceMappingURL=dyns.Shape.ts.map
14 changes: 14 additions & 0 deletions ts/fixtures/lang/expected/dyns.Shown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Generated by rust2-ts. Do not edit.

import * as rt from "./rt.ts";

/** Writes a `&dyn Debug` by calling its `fmt`, not through a placeholder. */
export interface Shown {
_0: rt.Fmt;
}

export function fmt$Debug(self: Shown, f: rt.Formatter): rt.Result<undefined, rt.FmtError> {
return self._0(f);
}

//# sourceMappingURL=dyns.Shown.ts.map
Loading