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
4 changes: 2 additions & 2 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ Decided with the maintainer on 2026-10-25, from the
[spike](https://github.com/kixelated/rust2/pull/41): the [exporter](/ffi/README.md), which
turns a crate's `pub` API into bindings with no attributes, replaces the UniFFI fork. The
fork stays in maintenance and keeps building moq's bindings until then; its feature quests
are folded into the [exporter's](/quest/m1/ffi-export.md). The
are folded into the [exporter's](/quest/m1/ffi-export/README.md). The
[binding benchmark](/bench/ffi/README.md) continues as the exporter's baseline.

## Required

- [Waiting: publish the UniFFI fork](/quest/m1/uniffi-release.md) - the fork publishes to crates.io under `rust2-*` names, once the maintainer decides it should
- [Exporter](/quest/m1/ffi-export.md) - moq's bindings exported from its crates' `pub` API, replacing the fork
- [Exporter](/quest/m1/ffi-export/README.md) - moq's bindings exported from its crates' `pub` API, replacing the fork
- [Land main through quest land](/quest/m1/land.md) - every queued PR lands as one tested batch through the shared gate
54 changes: 0 additions & 54 deletions quest/m1/ffi-export.md

This file was deleted.

45 changes: 45 additions & 0 deletions quest/m1/ffi-export/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Exporter: moq's bindings from its crates' pub API

## Goal

[rust2-ffi](/ffi/README.md) replaces [the UniFFI fork](/uniffi/README.md) for moq: it exports
what moq's bindings need from moq's own crates, with no attributes and no hand-written wrapper
beyond what the census shows is genuinely new API, and generates the languages moq ships on its
C ABI. moq-ffi shrinks to that remainder. The fork stays in maintenance, building moq's bindings
until then.

## Plan

Decided with the maintainer on 2026-10-25, from the spike
([kixelated/rust2#41](https://github.com/kixelated/rust2/pull/41)): adopt the exporter, and fold
the fork's paused feature quests into it.

Decided with the maintainer on 2026-10-27: this is an epic, with each item a child ranked by
what the census refuses and what moq waits on first, then the runtime, then the generators.
What stays here is the end-to-end: moq-ffi shrinks to its remainder and takes the stand-in
package's place.

Every generator on the C ABI:

- is measured on the [binding benchmark](/bench/ffi/README.md);
- keeps the C ABI's bulk and zero-copy payloads, with no per-byte loop on either side;
- reports its generated size, raw and gzipped, against the fork's (Python 653 KiB, Kotlin
914 KiB, Swift 684 KiB for moq-ffi);
- wraps the C ABI's futures in the language's native future. Those futures already take the
result in the poll that finishes them, so a cancel never loses a frame, cost three crossings
when ready at once, and reuse a repeated waker. The native future must latch a wake that
lands between `PENDING` and parking, prove no loss on cancel in a test that forces that
window, and pick a semantic for concurrent pollers. A hot read still makes a future handle
per read; measure it before adding a per-handle poll. Decided with the maintainer on
2026-10-27: async lands with each generator, not as its own quest.

## Required

- [Paths without a lifetime](/quest/m1/ffi-export/paths.md) - moq's owned `Path` exports, and its excludes go
- [Instances the config can't name yet](/quest/m1/ffi-export/instance-keys.md) - `&T` keys, `IntoIterator` as a `Vec`, `serde_json::Value`, and `Catalog<()>`
- [Closures lent for the call](/quest/m1/ffi-export/lent-closures.md) - a callback borrowed for one call, as `modify`'s `Guard` needs
- [One library from several crates](/quest/m1/ffi-export/crates.md) - the C names the several-crate export still can't give
- [Driving sessions and origins](/quest/m1/ffi-export/driving.md) - how a binding polls moq's `Driver`
- [A contract symbol](/quest/m1/ffi-export/contract.md) - a header and a library from different builds refuse to mix
- [C++ bindings](/quest/m1/ffi-export/cpp.md) - C++17, 20, and 23 over the C ABI, with native futures
- [Kotlin bindings](/quest/m1/ffi-export/kotlin.md) - JNI or Panama over the C ABI, with native futures
13 changes: 13 additions & 0 deletions quest/m1/ffi-export/contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# [S] A contract symbol

## Goal

Each build exports a contract symbol that bindings check when they load, so a header and a
library from different builds refuse to mix.

## Plan

It replaces the fork's namespaced contract and per-method checksums, which the export never
had.

Public API: one exported symbol per library.
12 changes: 12 additions & 0 deletions quest/m1/ffi-export/cpp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# [L] C++ bindings

## Goal

The exporter generates C++ over its one C ABI, per standard (17, 20, 23), compiled with
GCC, Clang, and MSVC, with and without exceptions and RTTI, including OBS's C17 and C++17
builds, meeting the [epic's](/quest/m1/ffi-export/README.md) rules for every generator,
native futures included.

## Plan

Public API: a new generator.
17 changes: 17 additions & 0 deletions quest/m1/ffi-export/crates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# [M] One library from several crates

## Goal

moq-net, moq-pattern, moq-clock, moq-tokio, hang, and moq-mux export into one library
with every C name the bindings need, so nothing is excluded for a name clash.

## Plan

They export through an empty package standing in for moq-ffi's remainder, which
`just ffi standin` writes. Left:

- moq-clock's C names (`moq_now`, `moq_origin`) wait for its merge into kio.
- A field and a method of one name, such as moq-tokio's `Backoff::initial`, claim one C
name that `rename` cannot split, so [the config](/ffi/moq/ffi.toml) excludes both.

Public API: the exporter's config and the C names it emits.
17 changes: 17 additions & 0 deletions quest/m1/ffi-export/driving.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# [M] Driving sessions and origins

## Goal

A binding runs moq's sessions and origins: each runs a `Driver` polled with a time and a
`kio::Waiter`, and the exporter gives bindings a way to drive it that every generator can
use.

## Plan

Open: export that poll, generate a thread that drives it, or export moq-tokio's runtime
helpers. Go's cancel contract (moq's `go/wrapper/doc.go` ends the stream) is decided
with it. `time::run` at `origin::Driver` was left out of
[#126](https://github.com/kixelated/rust2/pull/126) because it runs on an ambient runtime
C does not have.

Public API: the exporter's C ABI for drivers.
22 changes: 22 additions & 0 deletions quest/m1/ffi-export/instance-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# [M] Instances the config can't name yet

## Goal

The exporter's `instances` key names the instances moq-ffi still wraps by hand: a `&T`
argument (`with_publisher` at `&origin::Producer`, as moq-ffi passes it),
`impl IntoIterator<Item = X>` exported as a `Vec` of `X`, `serde_json::Value` as the `T` of
`catalog::Producer`'s `json_snapshot` and `json_stream`, and `DEFAULT_NAME` on
`hang::Catalog<()>`.

## Plan

Left out of [#126](https://github.com/kixelated/rust2/pull/126), which exports generic
functions at config-named instances; decided with the maintainer on 2026-10-27 to plan
them all.

- `IntoIterator` as a `Vec` needs `Vec` of records to map first.
- `serde_json::Value` crosses the C ABI as something every generator can read; decide
its shape (JSON text is the obvious one) when the quest starts.
- Report moq-ffi's coverage and the census before and after.

Public API: the exporter's config and the C names it emits.
14 changes: 14 additions & 0 deletions quest/m1/ffi-export/kotlin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# [L] Kotlin bindings

## Goal

The exporter generates Kotlin over its C ABI through JNI or Panama, meeting the
[epic's](/quest/m1/ffi-export/README.md) rules for every generator, native futures
included.

## Plan

Not JNA: the fork measured 3.2 us per call through it against 7 ns for upstream's JNI
bindgen.

Public API: a new generator.
19 changes: 19 additions & 0 deletions quest/m1/ffi-export/lent-closures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# [M] Closures lent for the call

## Goal

The exporter exports a function that takes a closure it calls only during the call, as a
C callback with a context pointer, so moq's `catalog::Producer` can be changed through a
lent closure (`mutate`) where `modify` returns a borrowing `Guard` the C ABI can't hold.

## Plan

Left out of [#126](https://github.com/kixelated/rust2/pull/126); decided with the
maintainer on 2026-10-27 to plan it.

- A closure the callee keeps past the call stays refused; only a lent one exports.
- A panic or an error in the callback must not unwind across the C ABI.
- Decide whether `mutate` is moq's API or the exporter's rewrite of `modify`; moq's own
change belongs in moq's tree.

Public API: the exporter's C ABI for callbacks.
14 changes: 14 additions & 0 deletions quest/m1/ffi-export/paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# [S] Paths without a lifetime

## Goal

The exporter exports moq's owned `Path`: the census exports `Path::new`, `strip_prefix`,
and `Relative::new`, [the exclude list](/ffi/moq/ffi.toml) drops `AsPath` and the `borrow`
methods, and only `Path::from_static`'s `'static` borrow is refused.

## Plan

[moq-dev/moq#6843](https://github.com/moq-dev/moq/pull/6843), which makes `Path` owned,
merged on 2026-10-27. Bump the moq pin past it and report the census before and after.

Public API: the C names moq's bindings get.
4 changes: 3 additions & 1 deletion quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ runtime such as Node or Bun calls Rust through [rust2-ffi](/ffi/README.md), so N
Bun matter only as proxies for V8 and JavaScriptCore, and a speed call that splits them
from the browsers goes to the browsers.

The fork's feature quests folded into the [exporter](/quest/m1/ffi-export.md);
The fork's feature quests folded into the [exporter](/quest/m1/ffi-export/README.md);
[m1](/quest/m1/README.md#plan) records why.

## Required

- [A tuple's parts read after their variables change](/quest/m2/ts-swap-parts.md) - `(a, b) = (b, a)` swaps instead of copying one into both
- [Fixture cases by name](/quest/m2/ts-harness-names.md) - adding a harness case never renumbers another
- [Byte slices without objects](/quest/m2/ts-byte-views/README.md) - reading through a translated cursor allocates no object per read
- [Drop in the TypeScript translator](/quest/m2/ts-drop.md) - every value that drops in moq-net and kio drops where Rust drops it
- [Async in the TypeScript translator](/quest/m2/ts-async.md) - moq-net's and kio's async code translates with Rust's lazy futures
Expand Down
5 changes: 5 additions & 0 deletions quest/m2/ts-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ entries that drop, sorted by the crate's own `Ord`; a `collect` in place of a `z
whose second iterator drops; `successors` of items that drop, which holds the next one;
and `DebugList::entries` of items that drop.

Next, from [#115](https://github.com/kixelated/rust2/pull/115): `rt.iterFromFn` and
`rt.iterSuccessors` take droppers for their closure and for `successors`' pending item,
as std's fields drop them, which lifts both refusals and the reject fixture's second
refusal of dropping the `Take` around them.

Watch the cost: a function whose cleanup does something JS can see sets a variable
before each call that can panic and wraps its body in a `try`; measure any change on
the [TS benchmark](/ts/README.md#bench).
Expand Down
24 changes: 24 additions & 0 deletions quest/m2/ts-harness-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [S] Fixture cases by name

## Goal

Adding a module to a TS fixture's differential harness never renumbers another's, so two
PRs that each add one merge without editing the dispatch, and a targeted run in
`caller.ts` can't land on the wrong module.

## Plan

Found on 2026-10-27: the lang fixture dispatches by hand-numbered `match op % N` in
`ts/fixtures/lang/src/harness.rs`, which `caller.ts` targets by number. Every concurrent
ts PR that day collided on the numbers and on `N`, and one collision routed
`durations`' targeted cases to another module, which only review caught.

Decided with the maintainer on 2026-10-27: m2, after the swap fix, since it slows every
ts PR now.

- Name each case, or derive the dispatch from one list, so the count follows it.
- Keep each case's share of the random run and the harness's coverage; a dispatch change
must not shrink what the differential run reaches.
- Apply it to every fixture with the same pattern.

Public API: none.
4 changes: 3 additions & 1 deletion quest/m2/ts-model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Decided with the maintainer on 2026-10-27, from a census of the model's 22 publi
- Each child measures this target by the census of `ts/moq/model.toml`, rooted at those
types with the boundaries bound, which nightly reports beside the whole crate.
- The end-to-end work stays here: the model's roots, its differential harness, and the
`frames.ts` workload.
`frames.ts` workload. moq writes the binding files `model.toml` names, in its own tree.

Public API: the translator's output for moq-net's model.

Expand All @@ -49,4 +49,6 @@ Public API: the translator's output for moq-net's model.
- [Translate safe dependencies with the package](/quest/m2/ts-model/crates.md) - kio, moq-clock, moq-pattern, and rangemap translate as one program with moq-net
- [Std gaps the model reaches](/quest/m2/ts-model/std-more.md) - ranges, wrappers, and `Vec`'s editing methods
- [Options of values that can be undefined](/quest/m2/ts-model/nested-option.md) - `Option<Option<T>>` and `Option<()>` keep `None` apart from `Some(None)`
- [Std's mutable references to values JS copies](/quest/m2/ts-model/std-places.md) - `as_mut`, `get_or_insert`, `iter_mut`, and the rest give an `rt.Place`
- [The rest of std the model reaches](/quest/m2/ts-model/std-rest.md) - the std calls the census still refuses
- [Async in the TypeScript translator](/quest/m2/ts-async.md) - kio's producers and consumers are futures
24 changes: 24 additions & 0 deletions quest/m2/ts-model/std-places.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [M] Std's mutable references to values JS copies

## Goal

rust2-ts translates the std methods that hand out a `&mut` to a value JS copies (a
number, an `Option` of one, a `Box`), giving an `rt.Place` where
[#128](https://github.com/kixelated/rust2/pull/128)'s guard refuses them now, proven by
differential tests against native Rust.

## Plan

Found by #128 on 2026-10-27: its guard refused 74 std-made `&mut`s in moq-net, most from
`Option::as_mut`, `get_or_insert*`, `insert`, `slice::get_mut` and `last_mut`,
`iter_mut` and `values_mut`, `Entry::and_modify`, `retain_mut`, and `dedup_by_key`.
Also refused: an `Option<&mut T>` passed through `map`, `map_or`, `filter`, and
`and_then` closures, and comparing `Option`s or tuples of Places through the runtime's key
functions.

- Rerun the model's census first: [#129](https://github.com/kixelated/rust2/pull/129) now
lowers `Option`'s combinators from core's MIR, so some of these may already translate.
- Code that needs no Place still gets none; measure on the
[TS benchmark](/ts/README.md#bench).

Public API: the translator's output.
25 changes: 25 additions & 0 deletions quest/m2/ts-model/std-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# [M] The rest of std the model reaches

## Goal

rust2-ts translates the std calls the model's census still refuses after
[std gaps](/quest/m2/ts-model/std-more.md) and [core's MIR](https://github.com/kixelated/rust2/pull/129),
outside [Places](/quest/m2/ts-model/std-places.md), proven by differential tests against
native Rust.

## Plan

Found by [#124](https://github.com/kixelated/rust2/pull/124) and #129 on 2026-10-27:
iterator `min_by_key` and `max_by_key`, `slice::split_last`, integer `to_le_bytes`,
`Box<T>::clone`, `retain` and `sort` of byte vectors, `Debug` of `Reverse` and `Bound`,
slicing by `..=`, and `Option`'s and `Result`'s `PartialEq` and `From` impls and the std
calls core's bodies reach (`Default::default`, a payload's `Clone`).

- Rerun the model's census first, since #115, #128, and #129 moved it, and take what it
refuses in std rather than only this list.

Public API: the translator's output.

## Required

- [Std gaps the model reaches](/quest/m2/ts-model/std-more.md) - ranges, wrappers, and `Vec`'s editing methods
24 changes: 24 additions & 0 deletions quest/m2/ts-swap-parts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [S] A tuple's parts read after their variables change

## Goal

rust2-ts translates a tuple built from variables and read after one of them is
reassigned with the value it held when built, so `(a, b) = (b, a)` on two `&[u8]` in a
loop swaps them instead of translating to `a = b; b = a;`, proven by a differential test
against native Rust.

## Plan

Found by [#130](https://github.com/kixelated/rust2/pull/130) on 2026-10-27: a swap in its
`splits` fixture gave 74 differences in about 344k cases, so the committed fixture leaves
the pattern out. The cause is on main: the `var_parts` arm of `place_expr` in
`ts/src/lower/mir/place.rs` reads a once-built tuple's field as the operand it was built
from, even when that operand is a variable reassigned before the read.

Decided with the maintainer on 2026-10-27: a silent miscompile, so it ranks first in m2.

- Fix it where the parts are recorded or read, for every operand kind, not only byte
slices; check the other readers of `var_parts`.
- Put the swap back in the lang fixture, inside a loop.

Public API: the translator's output.
Loading