From 95f1d16893b61131cbbf26d099311f1a3dc0b07a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:10:45 -0700 Subject: [PATCH 1/5] quest: plan the overnight follow-ups Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn --- quest/m2/README.md | 8 +++++++ quest/m2/ts-async.md | 12 ++++++++++ quest/m2/ts-bench-chrome-timeout.md | 15 ++++++++++++ quest/m2/ts-bench-error-path.md | 14 +++++++++++ quest/m2/ts-bench-routes.md | 15 ++++++++++++ quest/m2/ts-drop.md | 9 ++++++++ quest/m2/ts-mir-spike.md | 36 +++++++++++++++++++++++++++++ quest/m2/ts-never-edges.md | 17 ++++++++++++++ quest/m2/ts-stable-names.md | 21 +++++++++++++++++ quest/m2/ts-wide-fast.md | 27 ++++++++++++++++++++++ quest/m2/uniffi-cpp-dead-modes.md | 17 ++++++++++++++ quest/m3/README.md | 1 + quest/m3/bench-no-c.md | 20 ++++++++++++++++ 13 files changed, 212 insertions(+) create mode 100644 quest/m2/ts-bench-chrome-timeout.md create mode 100644 quest/m2/ts-bench-error-path.md create mode 100644 quest/m2/ts-bench-routes.md create mode 100644 quest/m2/ts-mir-spike.md create mode 100644 quest/m2/ts-never-edges.md create mode 100644 quest/m2/ts-stable-names.md create mode 100644 quest/m2/ts-wide-fast.md create mode 100644 quest/m2/uniffi-cpp-dead-modes.md create mode 100644 quest/m3/bench-no-c.md diff --git a/quest/m2/README.md b/quest/m2/README.md index fb6b5868a..20d8d62ae 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -15,14 +15,22 @@ deletes the quests that follow it. ## 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 - [Wide integer spike](/quest/m2/ts-wide-ints.md) - `bigint` against boxed and unboxed hi/lo picks the 64- and 128-bit representation +- [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 - [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 - [Namespaced external types](/quest/m2/uniffi-namespaces.md) - two crates linked into one library can each export a same-named type in every generator +- [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 diff --git a/quest/m2/ts-async.md b/quest/m2/ts-async.md index 4ede3b7f2..c6335565f 100644 --- a/quest/m2/ts-async.md +++ b/quest/m2/ts-async.md @@ -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` against + `Future`. + 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 diff --git a/quest/m2/ts-bench-chrome-timeout.md b/quest/m2/ts-bench-chrome-timeout.md new file mode 100644 index 000000000..e9becc089 --- /dev/null +++ b/quest/m2/ts-bench-chrome-timeout.md @@ -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. diff --git a/quest/m2/ts-bench-error-path.md b/quest/m2/ts-bench-error-path.md new file mode 100644 index 000000000..7359f9bae --- /dev/null +++ b/quest/m2/ts-bench-error-path.md @@ -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. diff --git a/quest/m2/ts-bench-routes.md b/quest/m2/ts-bench-routes.md new file mode 100644 index 000000000..3882e8b03 --- /dev/null +++ b/quest/m2/ts-bench-routes.md @@ -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. diff --git a/quest/m2/ts-drop.md b/quest/m2/ts-drop.md index 2736f136a..edf7f1a8a 100644 --- a/quest/m2/ts-drop.md +++ b/quest/m2/ts-drop.md @@ -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 diff --git a/quest/m2/ts-mir-spike.md b/quest/m2/ts-mir-spike.md new file mode 100644 index 000000000..0554a5540 --- /dev/null +++ b/quest/m2/ts-mir-spike.md @@ -0,0 +1,36 @@ +# [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 +correctness first, bundle size second, and readability last. + +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; the resume dispatch jumps into the middle of loops, so the output read as +basic blocks; and under wasm32's `panic=abort` MIR has no cleanup paths, so a JS +exception would skip drops that THIR's `finally` blocks run. + +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 diff --git a/quest/m2/ts-never-edges.md b/quest/m2/ts-never-edges.md new file mode 100644 index 000000000..df060d381 --- /dev/null +++ b/quest/m2/ts-never-edges.md @@ -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. diff --git a/quest/m2/ts-stable-names.md b/quest/m2/ts-stable-names.md new file mode 100644 index 000000000..05040f6ae --- /dev/null +++ b/quest/m2/ts-stable-names.md @@ -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. diff --git a/quest/m2/ts-wide-fast.md b/quest/m2/ts-wide-fast.md new file mode 100644 index 000000000..e86680bee --- /dev/null +++ b/quest/m2/ts-wide-fast.md @@ -0,0 +1,27 @@ +# [S] Wide-integer fast paths + +## Goal + +Two fast paths for [rust2-ts](/ts/README.md)'s 64-bit integers, 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. + +## Required + +- [Wide integer spike](/quest/m2/ts-wide-ints.md) - the number-or-limbs representation these speed up + +## Related + +- [A/B routes in the TS benchmark](/quest/m2/ts-bench-routes.md) - compares each path against the current one diff --git a/quest/m2/uniffi-cpp-dead-modes.md b/quest/m2/uniffi-cpp-dead-modes.md new file mode 100644 index 000000000..01e488dc7 --- /dev/null +++ b/quest/m2/uniffi-cpp-dead-modes.md @@ -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. diff --git a/quest/m3/README.md b/quest/m3/README.md index fa98f31a7..cf670e290 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -8,5 +8,6 @@ each a measurement that decides whether its follow-up lands. ## Required - [Pure-Rust crypto](/quest/m3/pure-crypto.md) - moq-relay on graviola and RustCrypto instead of aws-lc, measured on build time and throughput +- [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 - [Relink without rebuilding](/quest/m3/relink.md) - measure upstream's skip of dependency-only rebuilds on moq's edits once it reaches nightly diff --git a/quest/m3/bench-no-c.md b/quest/m3/bench-no-c.md new file mode 100644 index 000000000..16d33e746 --- /dev/null +++ b/quest/m3/bench-no-c.md @@ -0,0 +1,20 @@ +# [XS] No-C check in the build benchmark + +## Goal + +The [build-speed benchmark](/bench/build/README.md) fails if the `moq-pure-crypto` +corpus 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. + +## Required + +- [Pure-Rust crypto](/quest/m3/pure-crypto.md) - adds the `moq-pure-crypto` corpus From 33d7dcf20b53462aa15699a645647ff022c0a16f Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:19:19 -0700 Subject: [PATCH 2/5] quest: add the early cutoff spike, with mbx's findings Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn --- quest/m3/edit-cutoff.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/quest/m3/edit-cutoff.md b/quest/m3/edit-cutoff.md index 7f990dad0..668472ba1 100644 --- a/quest/m3/edit-cutoff.md +++ b/quest/m3/edit-cutoff.md @@ -41,6 +41,28 @@ Research (2026-10-24, rustc at the pinned nightly 7df81381d): today. - #156940 is a draft, waiting on its author, and absent from the pinned nightly. +Research on [mbx](https://github.com/jdx/mr-boxington) (Mr Boxington, MIT, a cargo +and rustc wrapper cache), 2026-10-24: + +- mbx doesn't cut off dependents. Its keys hash each `--extern` artifact's bytes, + and for edited crates it relies on private rustc incremental state. Its + maintainers reproduced E0460 and rejected SVH patching as unsound in + [jdx/mr-boxington#645](https://github.com/jdx/mr-boxington/pull/645) (closed + unmerged): rustc's metadata format is unstable, and telling interface edits from + body edits needs span-aware normalization. +- Their numbers: with incremental on, dependents cost about 1.1 s of a 1.7 s edit, + and rustc incremental already cut one dependent from about 2.4 s to about 0.8 s. +- So the spike measures with incremental on, as the build benchmark does, and + counts only what incremental leaves. Report incremental on and off side by side. +- Classify each edit against mbx's list of interface-relevant bodies: generic, + `#[inline]`, and `const fn` bodies; `impl Trait` hidden types; new impls; plus + `async fn` bodies, whose coroutine MIR is always encoded. +- A cheap first data point: `mbx analyze` charges each dependent's rebuild to the + crate where the edit started. Run it on a few real moq-net edits before building + anything. +- rust2's edge: its own rustc driver could define an interface hash for the + pure-Rust path, which a wrapper cache can't. + The upper bound on the benchmark's moq-net edit is 3.9 s of 6.95 s. Its appended non-generic function overstates the benefit, which is why the report replays real edits. From 33fa929b071af01d689d719f2b7fccddb3f39dcc Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:56:38 -0700 Subject: [PATCH 3/5] quest: record the translator's output goals and plan tree-shaking Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn --- quest/m2/README.md | 6 ++++++ quest/m2/ts-mir-spike.md | 11 +++++++---- quest/m2/ts-runtime-shake.md | 23 +++++++++++++++++++++++ quest/m2/ts-shake-spike.md | 34 ++++++++++++++++++++++++++++++++++ ts/README.md | 11 +++++++++-- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 quest/m2/ts-runtime-shake.md create mode 100644 quest/m2/ts-shake-spike.md diff --git a/quest/m2/README.md b/quest/m2/README.md index 5201bd08b..743dee7de 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -12,6 +12,10 @@ 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 @@ -27,6 +31,8 @@ deletes the quests that follow 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 diff --git a/quest/m2/ts-mir-spike.md b/quest/m2/ts-mir-spike.md index 0554a5540..2b12d38a8 100644 --- a/quest/m2/ts-mir-spike.md +++ b/quest/m2/ts-mir-spike.md @@ -14,7 +14,10 @@ otherwise the port is deleted. 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 -correctness first, bundle size second, and readability last. +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: @@ -25,9 +28,9 @@ Why, from #16: 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; the resume dispatch jumps into the middle of loops, so the output read as -basic blocks; and under wasm32's `panic=abort` MIR has no cleanup paths, so a JS -exception would skip drops that THIR's `finally` blocks run. +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. diff --git a/quest/m2/ts-runtime-shake.md b/quest/m2/ts-runtime-shake.md new file mode 100644 index 000000000..c8ae1a760 --- /dev/null +++ b/quest/m2/ts-runtime-shake.md @@ -0,0 +1,23 @@ +# [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 after their bundler tree-shakes and minifies it. + +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 diff --git a/quest/m2/ts-shake-spike.md b/quest/m2/ts-shake-spike.md new file mode 100644 index 000000000..c918de77e --- /dev/null +++ b/quest/m2/ts-shake-spike.md @@ -0,0 +1,34 @@ +# [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 the consumer's final JS bundle after +esbuild and rollup tree-shake and minify a consumer that uses part of the API, and +on what caller code looks like. 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 diff --git a/ts/README.md b/ts/README.md index ac0ba8860..fe6efc7e0 100644 --- a/ts/README.md +++ b/ts/README.md @@ -1,7 +1,6 @@ # rust2-ts -A rustc driver that translates a subset of Rust to readable TypeScript with source -maps. No WASM: the output is plain ES modules that Node, Bun, and bundlers run as is. +A rustc driver that translates a subset of Rust to TypeScript with source maps. No WASM: the output is plain ES modules that Node, Bun, and bundlers run as is. ```bash rust2-ts --config ts.toml (--out dir | --census file.md) [--manifest-path Cargo.toml] [-- cargo args] @@ -121,6 +120,14 @@ Imports name the `.ts` file, which Node, Bun, and bundlers run as is. A `tsc` bu ## Design +Output is ranked correctness first, speed second, and bundle size third: the +consumer's final JS, after their bundler tree-shakes and minifies it, so type-only +constructs such as brand casts and annotations are free. Readable output is not a goal. +It stays `.ts`, so `tsc` keeps checking the internals, and minifying is the consumer's +bundler's job. Types must be solid at the API boundary: the `roots` items and +`printable` types use Rust's branded types (`rt.u64`), while internals cast as freely as +they need. + - 64- and 128-bit integers are numbers while they are safe integers, which most values are: lengths, IDs, sequence numbers, and timestamps. `just ts bench` measured it against `bigint`, against limbs for every value, and against unboxed halves, two From fee7dc5f95549ea39ba3904728f0f961ca5b9954 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:57:10 -0700 Subject: [PATCH 4/5] quest: measure the shake quests' bundle size with a Bun build Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn --- quest/m2/ts-runtime-shake.md | 3 ++- quest/m2/ts-shake-spike.md | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/quest/m2/ts-runtime-shake.md b/quest/m2/ts-runtime-shake.md index c8ae1a760..f23840bf3 100644 --- a/quest/m2/ts-runtime-shake.md +++ b/quest/m2/ts-runtime-shake.md @@ -14,7 +14,8 @@ minified JS. 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 after their bundler tree-shakes and minifies it. +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. diff --git a/quest/m2/ts-shake-spike.md b/quest/m2/ts-shake-spike.md index c918de77e..1626d3577 100644 --- a/quest/m2/ts-shake-spike.md +++ b/quest/m2/ts-shake-spike.md @@ -9,9 +9,10 @@ types, from three built and measured: 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 the consumer's final JS bundle after -esbuild and rollup tree-shake and minify a consumer that uses part of the API, and -on what caller code looks like. The winner lands with a CI tree-shake test: a +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 From eae358aaf83749cd554beb927017884a7efe2bca Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:09:26 -0700 Subject: [PATCH 5/5] quest: re-scope the early cutoff spike and add ra-jit to the build benchmark Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn --- quest/m3/README.md | 3 ++- quest/m3/bench-ra-jit.md | 27 ++++++++++++++++++++++++ quest/m3/edit-cutoff.md | 44 ++++++++++++++++++++++++---------------- quest/m3/relink.md | 3 ++- 4 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 quest/m3/bench-ra-jit.md diff --git a/quest/m3/README.md b/quest/m3/README.md index deaa38759..7bbc6efe6 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -9,5 +9,6 @@ each a measurement that decides whether its follow-up lands. - [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 decide how rust2 skips rebuilding dependents without waiting on cargo +- [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 diff --git a/quest/m3/bench-ra-jit.md b/quest/m3/bench-ra-jit.md new file mode 100644 index 000000000..8806b1b3d --- /dev/null +++ b/quest/m3/bench-ra-jit.md @@ -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 diff --git a/quest/m3/edit-cutoff.md b/quest/m3/edit-cutoff.md index 668472ba1..5fb5ca9ac 100644 --- a/quest/m3/edit-cutoff.md +++ b/quest/m3/edit-cutoff.md @@ -2,28 +2,33 @@ ## Goal -A report says what share of real moq-net edits could skip rebuilding their -dependents, and how many seconds that saves, under each rule below. It then picks -how rust2 gets early cutoff without waiting on -[rust-lang/cargo#17439](https://github.com/rust-lang/cargo/issues/17439): a -hash-patching `RUSTC_WRAPPER`, carrying -[rust-lang/rust#156940](https://github.com/rust-lang/rust/pull/156940) on a custom -toolchain, or helping upstream. +A report says how often real moq-net edits leave +[rust-lang/rust#156940](https://github.com/rust-lang/rust/pull/156940)'s public API +hash unchanged, so their dependents could skip rebuilding, and how many seconds that +saves with incremental on and off. It then picks: carry #156940 on a custom +toolchain, help upstream, or stop. The other route, +[rust-lang/cargo#17439](https://github.com/rust-lang/cargo/issues/17439), is a design +discussion. ## Plan Decided with the maintainer on 2026-10-24: measure real edits first, rather than -build. +build. Re-scoped the same day after build-speed research: a wrapper that patches the +SVH and `SourceFile` hashes out of an otherwise identical rmeta is no longer a +candidate, since tinymold already ran that spike +([tinyhumansai/tinymold#13](https://github.com/tinyhumansai/tinymold/pull/13), ADR +0007) with the same result: identical objects, a different rmeta, and E0460 +blocking reuse. -Replay moq-net's recent git history edit by edit. For each edit, check whether -dependents could be skipped under each rule: - -1. The upstream rmeta is identical apart from the SVH and the `SourceFile` hash - fields, which is what a wrapper could patch. -2. #156940's `-Z public-api-hash`, cherry-picked onto a scratch build of the - pinned nightly. - -Report edits to `async fn` bodies separately. +- Build #156940's `-Z public-api-hash` on a scratch toolchain from the pinned + nightly, and replay moq-net's git history through it edit by edit. +- Count how often the public hash stays unchanged, how often only span shifts break + it, and how often `async fn` bodies do. Report the seconds saved with incremental + on and off side by side. +- Check determinism: with no source change, are moq's rmeta bytes stable across + incremental sessions? headstart found def-ids encoded in a different order when + loaded from the incremental cache, which would also defeat cargo#17439's output + hashing. Research (2026-10-24, rustc at the pinned nightly 7df81381d): @@ -39,6 +44,10 @@ Research (2026-10-24, rustc at the pinned nightly 7df81381d): interface. - No build tool (Bazel rules_rust, buck2, sccache) gets early cutoff for Rust today. +- [headstart](https://github.com/PowderworksCode/headstart) emits early + interface-only metadata for clean builds; not chosen for now. +- The cg_clif performance goal speeds up the backend, which is 6% of moq's edit, so + it does not apply here. - #156940 is a draft, waiting on its author, and absent from the pinned nightly. Research on [mbx](https://github.com/jdx/mr-boxington) (Mr Boxington, MIT, a cargo @@ -72,3 +81,4 @@ Public API: none. ## Related - [Relink without rebuilding](/quest/m3/relink.md) - waits for the same lever upstream; this spike may change its plan +- [ra-jit in the build benchmark](/quest/m3/bench-ra-jit.md) - skips the SVH cascade another way, by patching changed functions into the linked binary diff --git a/quest/m3/relink.md b/quest/m3/relink.md index f71ab2446..d82bfcbf7 100644 --- a/quest/m3/relink.md +++ b/quest/m3/relink.md @@ -23,10 +23,11 @@ goal will not complete; rust#156940 is a draft, and cargo's early cutoff is a design discussion. The [early cutoff spike](/quest/m3/edit-cutoff.md) may change this plan: it -picks how rust2 gets early cutoff without waiting on cargo. +decides whether rust2 carries rust#156940, helps upstream, or stops. Public API: a benchmark variant. ## Related - [Early cutoff for edit rebuilds](/quest/m3/edit-cutoff.md) - measures the same lever on real edits, without waiting on upstream +- [ra-jit in the build benchmark](/quest/m3/bench-ra-jit.md) - another way past dependent rebuilds and the relink