Skip to content
14 changes: 14 additions & 0 deletions quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,29 @@ Decided with the maintainer on 2026-10-23: the translator measures before it
builds. The benchmark and census come first; each spike's report rescopes or
deletes the quests that follow it.

Decided with the maintainer on 2026-10-24: the translator's output ranks correctness,
then speed, then the size of the consumer's final tree-shaken, minified JS; readability
is not a goal, and types must be solid at the API boundary ([design](/ts/README.md#design)).

## Required

- [Async and drop spike](/quest/m2/ts-async-spike.md) - generators from THIR against MIR state machines, deciding async, drop points, and the lowering
- [MIR lowering spike](/quest/m2/ts-mir-spike.md) - the whole lowering ported to MIR on two fixtures decides whether the translator moves
- [Fail the TS benchmark when Chrome hangs](/quest/m2/ts-bench-chrome-timeout.md) - a hung renderer fails the run loudly
- [A/B routes in the TS benchmark](/quest/m2/ts-bench-routes.md) - `--route name=dir` loads another translation for reproducible A/B runs
- [Stable names](/quest/m2/ts-stable-names.md) - generic instances and trait-impl methods always carry their suffix, so callers' names never shift
- [Diverging calls through closures and generics](/quest/m2/ts-never-edges.md) - closures returning `!` and generics at `!` pass `tsc`
- [Cheaper byte slices](/quest/m2/ts-byte-views.md) - splitting a `&[u8]` stops making a `Uint8Array` view per split
- [Wide-integer fast paths](/quest/m2/ts-wide-fast.md) - a fused `from_halves` and an unboxed limb product, kept only if measured faster
- [Error-path workload](/quest/m2/ts-bench-error-path.md) - the benchmark prices `?` on a frame split across reads
- [Drop in the TypeScript translator](/quest/m2/ts-drop.md) - a `Drop` type's `drop()` runs where Rust drops it
- [Async in the TypeScript translator](/quest/m2/ts-async.md) - 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
- [Collections](/quest/m2/ts-collections.md) - maps, sets, and queues keep Rust's key semantics
- [Tree-shaking spike](/quest/m2/ts-shake-spike.md) - classes, free functions, or free functions behind root facades, measured on speed, shaken size, and caller code
- [Tree-shakable runtime](/quest/m2/ts-runtime-shake.md) - `rt`'s adaptors and formatters become individually exported functions, kept only if no slower
- [Translate moq-net's model to TypeScript](/quest/m2/ts-model.md) - the driver grows from moq-net's codec to its model, for moq to compare against WASM
- [Delete the C++ generator's dead modes](/quest/m2/uniffi-cpp-dead-modes.md) - scaffolding mode and exception-style async go, with no consumer
- [Zero-copy byte returns](/quest/m2/ffi-bytes-return.md) - returned payloads reach the host without a copy, if the payload numbers justify it
- [Cheaper async](/quest/m2/ffi-async.md) - `RustFuture` exports cost fewer crossings, if the numbers show they matter
- [Namespaced FFI contract](/quest/m2/uniffi-contract.md) - bindings and a cdylib from different UniFFI forks refuse to load together
Expand Down
12 changes: 12 additions & 0 deletions quest/m2/ts-async.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,21 @@ machine its executor polls; JS promises can sit at the edge, for callers.
The [spike](/quest/m2/ts-async-spike.md) picks between generators and MIR state
machines; rewrite this plan to its report before starting.

Decided with the maintainer on 2026-10-24: the async spike
([#16](https://github.com/kixelated/rust2/pull/16)) merges with generators from
THIR, and the [MIR lowering spike](/quest/m2/ts-mir-spike.md) then decides
whether the whole lowering moves to MIR; rewrite this plan to its report too.
Also in scope, found since:

- `?` inside a function that returns `Poll`, as `ready!(..)?` writes it: about
224 sites in moq-net.
- A `()` future awaited as a statement fails `tsc`: `Future<void>` against
`Future<undefined>`.

Public API: the translator's output for async code.

## Required

- [Async and drop spike](/quest/m2/ts-async-spike.md) - the lowering this builds on
- [MIR lowering spike](/quest/m2/ts-mir-spike.md) - rewrite to the spike's report before starting
- [Drop in the TypeScript translator](/quest/m2/ts-drop.md) - a dropped future drops its state
15 changes: 15 additions & 0 deletions quest/m2/ts-bench-chrome-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# [XS] Fail the TS benchmark when Chrome hangs

## Goal

A hung Chromium renderer fails `just ts bench` loudly, naming the engine and what
it was running, instead of stalling the run until the job times out.

## Plan

Seen twice overnight, first in [#15](https://github.com/kixelated/rust2/pull/15):
the renderer sat idle and never posted a result, while a rerun passed. The limit
only turns a hang into a failure; it never retries, and if the hang's cause shows
up, fix it at the source.

Public API: none.
14 changes: 14 additions & 0 deletions quest/m2/ts-bench-error-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# [S] Error-path workload in the TS benchmark

## Goal

`just ts bench` times a codec path that fails and retries: a frame split across
reads, decoded again on `Short`, so the cost of `?` reusing the incoming `Err` is
measured against native, WASM, and hand-written routes.

## Plan

From [#15](https://github.com/kixelated/rust2/pull/15): every workload today
decodes a complete buffer, so no error path is priced.

Public API: none.
15 changes: 15 additions & 0 deletions quest/m2/ts-bench-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# [S] A/B routes in the TS benchmark

## Goal

`just ts bench --route name=dir` loads another translation as a route beside the
current one, so an A/B comparison of two translator versions is one reproducible
command.

## Plan

[#15](https://github.com/kixelated/rust2/pull/15) and
[#26](https://github.com/kixelated/rust2/pull/26) both patched `bench/main.ts` by
hand to time another translation (main's, or a rejected representation's).

Public API: a `just ts bench` option.
9 changes: 9 additions & 0 deletions quest/m2/ts-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ THIR does not say where drops happen; MIR does, after drop elaboration. The
beside the THIR lowering or move the lowering itself; rewrite this plan to its
report before starting.

Decided with the maintainer on 2026-10-24: the async spike
([#16](https://github.com/kixelated/rust2/pull/16)) merges with drop points from
THIR's scopes, and the [MIR lowering spike](/quest/m2/ts-mir-spike.md) then
decides whether drops move to MIR's elaboration; rewrite this plan to its report
too. Also in scope, found since: a temporary that drops and is held across an
`.await` is owned instead of refused (3 refusal sites), which needs declarations
at scope level.

Public API: the translator's output for `Drop` types.

## Required

- [Async and drop spike](/quest/m2/ts-async-spike.md) - where drop points come from
- [MIR lowering spike](/quest/m2/ts-mir-spike.md) - rewrite to the spike's report before starting
39 changes: 39 additions & 0 deletions quest/m2/ts-mir-spike.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [L] MIR lowering spike

## Goal

A report decides whether [rust2-ts](/ts/README.md) lowers from MIR instead of THIR.
A port of the whole lowering to MIR translates the codec and task fixtures (plain
code, drops, and async state machines), and is measured against the THIR lowering
on bundle size, `just ts bench` speed, exact drop traces (the task fixture's trace
diff), and how many census refusals move. If MIR wins, the translator moves to it;
otherwise the port is deleted.

## Plan

Decided with the maintainer on 2026-10-24: merge the async spike
([#16](https://github.com/kixelated/rust2/pull/16)) on THIR, then run this spike
over the whole lowering, on those two fixtures. The priorities that decide it are
the translator's ([design](/ts/README.md#design)): correctness first, speed second,
and bundle size third, with readable output not a goal. So the state machines'
1.4-2.6x speed weighs ahead of their roughly 18% larger bundle; measure the size
as the consumer's final minified JS.

Why, from #16:

- Its MIR state machines were 1.4-2.6x faster per handoff than the generators it
landed, and about 18% larger gzipped.
- THIR scope drops drew 10 review rounds of small drop-coverage bugs. MIR's drop
elaboration is exact by construction.

What #16 found against MIR, to answer rather than assume: its prototype
re-implemented places, rvalues, constants, closures, and `format_args!` at the MIR
level, and under wasm32's `panic=abort` MIR has no cleanup paths, so a JS
exception would skip drops that THIR's `finally` blocks run. That its output read as
basic blocks no longer counts against it.

Public API: none unless the translator moves; then its whole output.

## Required

- [Async and drop spike](/quest/m2/ts-async-spike.md) - the task fixture, its trace test, and the THIR lowering this compares against
17 changes: 17 additions & 0 deletions quest/m2/ts-never-edges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# [S] Diverging calls through closures and generics

## Goal

[rust2-ts](/ts/README.md) output passes `tsc` and matches native Rust in two cases
[#28](https://github.com/kixelated/rust2/pull/28) left: a call to a closure that
returns `!`, and a generic instantiated at `!`.

## Plan

- A closure returning `!` is an unannotated `const` arrow, so `tsc` does not see a
call to it diverge: a tail `f(b)` in a function with a result fails `tsc`.
- A generic instantiated at `!` (edition 2024's fallback, as in `id(panic!())`)
lowers its body with `Dest::Discard` and fails TS2534, and its argument is still
`undefined`.

Public API: the translator's output for these shapes, which fail `tsc` today.
24 changes: 24 additions & 0 deletions quest/m2/ts-runtime-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [M] Tree-shakable runtime

## Goal

[rust2-ts](/ts/README.md)'s runtime is free functions: `rt.Iter`'s adaptors,
`rt.Formatter`, and similar become individually exported functions, so a consumer's
bundler drops what translated code never calls. It lands only if `just ts bench`
measures it within noise of today's class form or faster, with a CI tree-shake test:
a consumer importing one function, asserting nothing unused survives in the final
minified JS.

## Plan

Decided with the maintainer on 2026-10-24: the runtime is always free functions,
whatever the [tree-shaking spike](/quest/m2/ts-shake-spike.md) picks for types,
because callers never touch `rt` directly. Bundle size counts the consumer's final
JS: a Bun production build (`bun build --minify`, which tree-shakes) of a small
consumer, raw and gzipped, using the benchmark's existing Bun setup.

Public API: the runtime's exports, which translated code calls.

## Related

- [Tree-shaking spike](/quest/m2/ts-shake-spike.md) - the same measurement for translated types
35 changes: 35 additions & 0 deletions quest/m2/ts-shake-spike.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# [M] Tree-shaking spike

## Goal

A report recommends the default shape [rust2-ts](/ts/README.md) emits for translated
types, from three built and measured:

1. Classes everywhere, today's baseline.
2. Free functions everywhere: plain objects plus exported functions.
3. Free functions inside, with a thin class facade only on root types.

Each is measured on `just ts bench` speed, on bundle size, and on what caller code
looks like. Bundle size is a Bun production build (`bun build --minify`, which
tree-shakes) of a small consumer that imports part of the API, raw and gzipped,
using the benchmark's existing Bun setup. The winner lands with a CI tree-shake test: a
consumer importing one function, asserting nothing unused survives.

## Plan

Decided with the maintainer on 2026-10-24: spike all three and measure. The
maintainer wants rust2-ts to produce libraries that don't force callers into
awkward code. A per-crate `ts.toml` option was considered and not chosen for the
spike. The ranking is the translator's ([design](/ts/README.md#design)):
correctness, then speed, then bundle size, which counts only the final minified
JS, so type-only constructs are free.

Survey how comparable libraries trade this off: RxJS's move to pipeable operators,
Zod 4's `zod/mini`, Valibot, Effect's dual APIs, date-fns, and wasm-bindgen's
output.

Public API: the translator's output shape for types, if the default changes.

## Related

- [Tree-shakable runtime](/quest/m2/ts-runtime-shake.md) - the runtime goes free functions whatever this picks
21 changes: 21 additions & 0 deletions quest/m2/ts-stable-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# [S] Stable names in the TypeScript translator

## Goal

A caller's translated names never change when unrelated code adds a generic
instance or a trait impl. Every generic instance always prints its type-argument
suffix (`foo$u8`, `get$Wrap_Pair`), and every trait-impl method always prints its
trait (`fmt$Display`), even when it is the only one.

## Plan

Decided with the maintainer on 2026-10-24: always suffix, in today's Rust-like
suffix format, not a hash. Today a generic function with one instance is `foo`
until a root reaches a second instance, and a type with only `Debug` gets `fmt`
until it also has `Display`; either change renames calls in code the new root
never reaches. Found in [#18](https://github.com/kixelated/rust2/pull/18) and
[#27](https://github.com/kixelated/rust2/pull/27), which notes that numbered `$2`
suffixes can shift the same way.

Public API: the translator's output names for generic instances and trait-impl
methods.
24 changes: 24 additions & 0 deletions quest/m2/ts-wide-fast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [S] Wide-integer fast paths

## Goal

Two fast paths for [rust2-ts](/ts/README.md)'s 64-bit integers (numbers while
safe, limbs past 2^53, per its [design](/ts/README.md#design)), each kept only if
the [TS benchmark](/ts/README.md#bench) measures it faster, with the wide
differential sweep still passing.

## Plan

From [#26](https://github.com/kixelated/rust2/pull/26)'s follow-ups:

- A fused `from_halves`: Chrome's 8-byte varint decode is 25-45% slower than
`bigint`'s, since it goes through `rt.u64.or(rt.u64.shl(hi, 32), lo)`.
- A product of two numbers past 2^53 boxes both factors into limbs before
multiplying; multiplying their halves directly would cut the allocations
`rescale` measures.

Public API: none; the runtime's internals.

## Related

- [A/B routes in the TS benchmark](/quest/m2/ts-bench-routes.md) - compares each path against the current one
17 changes: 17 additions & 0 deletions quest/m2/uniffi-cpp-dead-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# [S] Delete the C++ generator's dead modes

## Goal

[The fork](/uniffi/README.md)'s C++ generator drops two modes no consumer uses:
its scaffolding mode, and async under `error_style = "exceptions"`. The deletion
is listed under the fork README's "Carried changes".

## Plan

- Scaffolding mode (`--scaffolding`) has no consumer and has gone untested since
the import, which left out the C++ scaffolding tests.
- Exception-style async has no consumer: moq uses the expected style. Codex found
a cancel/free race and a result leak in it.

Public API: the generator's `--scaffolding` flag, and async under the exceptions
error style.
3 changes: 3 additions & 0 deletions quest/m3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ each a measurement that decides whether its follow-up lands.

## Required

- [No-C check](/quest/m3/bench-no-c.md) - the benchmark fails if the `moq-pure-crypto` corpus gains C through `links`, `cc`, or `cmake`
- [Steadier edit numbers](/quest/m3/bench-edit-noise.md) - moq's edit scenarios compare night to night within a few percent
- [Early cutoff for edit rebuilds](/quest/m3/edit-cutoff.md) - real moq-net edits through rust#156940's public API hash decide whether rust2 carries it, helps upstream, or stops
- [ra-jit in the build benchmark](/quest/m3/bench-ra-jit.md) - a variant hot-patching changed functions on moq-net's edit, with what it refuses
- [Relink without rebuilding](/quest/m3/relink.md) - measure upstream's skip of dependency-only rebuilds on moq's edits once it reaches nightly
16 changes: 16 additions & 0 deletions quest/m3/bench-no-c.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# [XS] No-C check in the build benchmark

## Goal

The [build-speed benchmark](/bench/build/README.md) fails if the `moq-pure-crypto`
corpus ([corpus.toml](/bench/build/corpus.toml)) gains a `links` key or a `cc` or `cmake` build dependency, so C cannot creep
back into moq-relay once moq adopts pure-Rust crypto.

## Plan

Decided with the maintainer on 2026-10-24, adopting
[#13](https://github.com/kixelated/rust2/pull/13)'s recommendations. This is not
the `pure` check, which forbids every build script: moq's graph keeps pure-Rust
ones (serde, proc-macro2, libc, rustls). It pairs with moq's crypto adoption.

Public API: none; a benchmark check.
27 changes: 27 additions & 0 deletions quest/m3/bench-ra-jit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# [S] ra-jit in the build benchmark

## Goal

The [build-speed benchmark](/bench/build/README.md) measures
[ra-jit](https://github.com/indexable-inc/ra-jit) as a variant on the moq-net edit
scenario, and records how much of moq-net it refuses or hands back to cargo.

## Plan

Decided with the maintainer on 2026-10-24, from build-speed research. ra-jit is a
daemon on a forked rust-analyzer that compiles only the changed functions through
Cranelift and patches them into the already-linked binary, so an edit has no SVH
cascade and no relink. It claims a 12-24x geometric mean, such as a nushell edit
going from 14.4 s to 0.85 s.

- Its `outline.rs` treats an `async fn` body as part of the signature, and moq-net
is mostly async, so record the share it hands back to cargo beside its times.
- It needs Rust 1.99.0 and several GB of daemon memory, so it runs as its own
toolchain variant.

Public API: a benchmark variant.

## Related

- [Early cutoff for edit rebuilds](/quest/m3/edit-cutoff.md) - the rustc-side route past dependent rebuilds
- [Relink without rebuilding](/quest/m3/relink.md) - upstream's route past the same rebuilds
Loading