Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2eebc50
chore(quest): claim quest/m3/ts-unsafe.md
kixelated Oct 28, 2026
e18624f
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 28, 2026
6902b87
feat(ts): translate raw pointers, NonNull, MaybeUninit, and unions as…
kixelated Oct 29, 2026
e96c445
feat(ts): refuse pointer reinterpretation, translate get_unchecked, a…
kixelated Oct 29, 2026
50a63a5
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 29, 2026
7ea69c9
docs(ts): document typed pointers, list what unsafe has left, and spl…
kixelated Oct 29, 2026
2a48ee1
fix(ts): copy raw reads deep, lay out uninit parts, and refuse pointe…
kixelated Oct 29, 2026
da85a2f
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 29, 2026
d508424
test(ts): keep the snapshot case's tuple apart from an array's copy name
kixelated Oct 29, 2026
c641fbb
docs(quest): widen the library-place quest to the refused replacement…
kixelated Oct 29, 2026
c5c2793
fix(ts): give a null as_ref the None of its Option, and write nothing…
kixelated Oct 29, 2026
9b1f1a3
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 29, 2026
0b23604
fix(ts): point a dangling pointer to a value of no bytes at one, and …
kixelated Oct 29, 2026
598c06b
fix(ts): refuse a dangling pointer to a value of no bytes that holds …
kixelated Oct 29, 2026
7417aea
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 29, 2026
1012256
docs(quest): list the library-place quest's sites as main's census fi…
kixelated Oct 29, 2026
9455557
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 29, 2026
71416da
test(ts): regenerate the pointer fixtures after #206's drops
kixelated Oct 29, 2026
d2343fd
Merge remote-tracking branch 'origin/main' into quest/m3/ts-unsafe
kixelated Oct 29, 2026
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: 3 additions & 1 deletion quest/m2/ts-model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Public API: the translator's output for moq-net's model.
- [Shared ownership](/quest/m2/ts-shared.md) - kio's waiters are `Arc`/`Weak` with observable counts, and its atomics and locks are plain values
- [Float formatting](/quest/m2/ts-model/floats.md) - `f64` formats and parses as std does
- [The rest of std the model reaches](/quest/m2/ts-model/std-rest.md) - `Vec` and `VecDeque` capacity, and `RandomState`
- [std gaps kio reaches](/quest/m2/ts-model/kio-std.md) - `array::from_fn`, `Default` of arrays and `()`, and maps of `Option`s
- [std gaps kio reaches](/quest/m2/ts-model/kio-std.md) - `array::from_fn`, `Default` of arrays and `()`, maps of `Option`s, and `trailing_zeros`
- [hashbrown as the runtime's map](/quest/m2/ts-model/hashbrown.md) - hashlink's `hashbrown` tables translate as `rt.HashMap`, with std's capacity
- [A write through a mutable reference to a library object](/quest/m2/ts-model/library-place.md) - `*r = v` out of an `Option` writes the slot, not a copy, and `mem::take` of a `&mut Vec` and writes through guards and map lookups translate
- [bytes' BytesMut, Buf, and BufMut](/quest/m2/ts-model/bytes.md) - map without config, so the model's `BytesMut` binding goes
- [Async in the TypeScript translator](/quest/m2/ts-async.md) - kio's producers and consumers are futures
27 changes: 27 additions & 0 deletions quest/m2/ts-model/hashbrown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# [M] hashbrown as the runtime's map

## Goal

rust2-ts translates `hashbrown`'s `HashMap`, `HashSet`, and `HashTable`, which hashlink
keeps its entries in, as it translates std's: an `rt.HashMap` and its runtime functions,
proven by differential tests against native Rust.

## Plan

Found by [#201](https://github.com/kixelated/rust2/pull/201) on 2026-10-28: with hashlink's
raw pointers translated, hashbrown is the model's largest source of refusals left
(251 lines of its census), its SIMD groups, raw allocation, and byte reinterpretation
being what JS has no counterpart of.

- Map it as std's map is: std's `HashMap` is hashbrown's, so the same runtime functions
stand in for both, with no config.
- `capacity` follows hashbrown's `capacity_to_buckets` and `allocation_size` exactly, as
[the rest of std](/quest/m2/ts-model/std-rest.md) decided for `Vec`'s capacity.
- `HashTable`'s methods take the hash and the equality from the caller, so it needs a
table keyed by the caller's hash, not the runtime's own.

Public API: the translator's output.

## Required

- [The rest of std the model reaches](/quest/m2/ts-model/std-rest.md) - `RandomState` and std's exact capacity, which hashbrown's default hasher and growth reuse
4 changes: 2 additions & 2 deletions quest/m2/ts-model/kio-std.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

rust2-ts translates the std that kio reaches and no other quest owns, proven by
differential tests against native Rust: `array::from_fn`, `Default` of arrays (kio's
`[Atomic<u64>; 8]::default()`) and of `()`, and a `HashMap` whose values are `Option`s
(`shared.rs`).
`[Atomic<u64>; 8]::default()`) and of `()`, a `HashMap` whose values are `Option`s
(`shared.rs`), and `trailing_zeros` of every integer width and `NonZero` (`task.rs`).

## Plan

Expand Down
38 changes: 38 additions & 0 deletions quest/m2/ts-model/library-place.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# [M] A write through a mutable reference to a library object

## Goal

A whole write through a `&mut` to a library object, such as a `Vec`, an `Arc`, or a
`BTreeSet`, writes the slot the reference points at, wherever the reference came from,
proven by differential tests against native Rust:

- `*r = v` where `r` came out of an `Option`, as in
`if let Some(r) = v.get_mut(0) { *r = Arc::new(1) }`, which translates today but
silently differs from native.
- The replacements refused today: `mem::take(v)` and `mem::replace` with `v: &mut Vec<_>`,
and `*r = v` through a `MutexGuard`'s deref or a map lookup.

## Plan

Found by [#201](https://github.com/kixelated/rust2/pull/201) on 2026-10-28, in safe code:
the translation assigns the local the `Option` was matched into, so the `Vec` keeps its old
`Arc`, whose count stays up. It silently differs from native, so it is fixed first.

Both have one root: a `&mut` to a library object is the object, which a write can't put
another object in place of, and so is one to the crate's own struct or enum. On main, at
moq `30f25c04`, moq-net's census refuses eight such sites, after
[#204](https://github.com/kixelated/rust2/pull/204) translated the write through a
closure's capture:

- "replacing the value behind a reference": a `Vec` at `ietf/publisher.rs:3794` (through a
`MutexGuard`), `lite/publisher.rs:4043`, and `model/broadcast.rs:1226`; a `BTreeSet` at
`lite/subscriber.rs:7620`; and the crate's own `Subscription` at
`ietf/publisher.rs:16954`, `TrackState` at `ietf/subscriber/tracks.rs:1118`, and `Parked`
at `model/expiry.rs:316`.
- "an assignment through a reference": an `Arc` at `model/frame.rs:752`.

- A whole write through such a reference goes through the `rt.Place` of where the object
is kept, as a `&mut` to a value JS copies does, or into the object in place where the
runtime type allows it. Where neither is known, the write stays refused with its span.

Public API: the translator's output.
3 changes: 2 additions & 1 deletion quest/m2/ts-model/std-rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

rust2-ts translates the std calls the model's census still refuses outside other quests:
`Vec`'s and `VecDeque`'s `capacity`, `reserve`, and `shrink_to`, and `RandomState` with
`BuildHasher::hash_one`, proven by differential tests against native Rust.
`BuildHasher::hash_one`, also where another crate names it, as hashbrown's and hashlink's
default hasher does, proven by differential tests against native Rust.

## Plan

Expand Down
30 changes: 25 additions & 5 deletions quest/m3/ts-unsafe.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,40 @@
## Goal

[rust2-ts](/ts/README.md) translates the `unsafe` operations it can give exact semantics in
JS, such as `get_unchecked`, `ptr::read` and `ptr::write` of a local or element, and
`MaybeUninit` written before it is read, so a dependency using only those translates from
source instead of being bound by hand.
JS, so a dependency using only those translates from source instead of being bound by
hand. What is left: pointers into a slice or `Vec`, `ManuallyDrop`, unions over a type
parameter, and a raw pointer to a value JS copies taken through a reference.

## Plan

Decided with the maintainer on 2026-10-27: a dependency containing `unsafe` is bound by
the consumer [at first](/ts/README.md#bindings), and moq gives its own `unsafe` safe
`cfg(rust2_ts)` twins. Both are stopgaps, since a stock crate should translate with no
annotations or config; this quest is what retires them. The census of moq-net's model
found `bytes`, `hashlink`, `slab`, and `smallvec` bound, and kio's lock-free `log`.
annotations or config; this quest is what retires them.

Decided with the maintainer on 2026-10-28, typed pointers: a raw pointer is what its
reference is, the object, an `rt.Place`, or `rt.NULL`, and a pointer into a slice or `Vec`
is `{ array, index }`. Byte reinterpretation, pointer-to-integer casts, and raw allocation
are refused with a span. [#201](https://github.com/kixelated/rust2/pull/201) landed `Box`
to raw and back, `NonNull`, `MaybeUninit`, raw reads and writes, `get_unchecked`, and
unions whose overlapping fields share a type, which took hashlink from 375 refusals to
none of its own `unsafe`.

- An `unsafe` operation outside the modeled set is refused with its span, as today.
- Undefined behavior in Rust need not be reproduced, but a translated program never
silently differs from native where Rust's behavior is defined.
- Element pointers, `{ array, index }`, with `add`, `offset`, `offset_from`, and
`slice::from_raw_parts` over them: smallvec, arrayvec, and moq-net's `frame.rs`.
- `ManuallyDrop<T>` as `T` with its drop skipped, as `MaybeUninit` is: crossbeam-epoch's
`Local` reaches it, though crossbeam's tagged pointers stay refused.
- A union over a type parameter, whose layout has no fixed offsets until an instance
fixes them (smallvec's `SmallVecData`): lay out each instance instead of the identity.
- `&raw const *r` where `r` is a `&T` to a value JS copies or a library object, such as
foldhash's seed and arrayvec's `as_ptr`: the reference is the value, not where it is
kept, so it needs the place the reference came from.
- A pointer to a part kept as an object is refused where the crate may put another
object there, judged by type over the whole program: a generic function writing a whole
`T` counts for every `T` its bounds allow. Judge by the instances translation reaches
if a consumer's pointer is refused only by that.

Public API: the translator's output.
16 changes: 11 additions & 5 deletions ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ would; the configured library is also translated, after type and borrow checking
what they reach. Every crate it depends on but std's translates with it as one program,
as far as the roots reach: each compiles with every function's MIR, and its types and
calls translate as the package's own do. They compile with `--cfg rust2_ts`, so a crate
can give translation a safe twin of its `unsafe`. Their `unsafe` is
refused where it is, as the package's is, unless a binding stands in for the item holding
it. Only the writes of the dependencies' functions translation reaches decide what it
can give translation a safe twin of its `unsafe`. Their `unsafe` translates as the
package's does, and what translation can't model is refused where it is, unless a binding
stands in for the item holding it. Only the writes of the dependencies' functions translation reaches decide what it
copies, so it runs again when it reaches one it didn't read. A construct outside the subset is a compile error with its span, and
then nothing is written. A type without a JS value is refused where a value of it is made
or received, such as a parameter, a field, or a call, rather than again at each variable
Expand Down Expand Up @@ -87,6 +87,9 @@ crate translation reaches is refused, and so are two versions of one crate it re
| `ControlFlow<B, C>` | `rt.Result<C, B>`, `Ok` to continue; what `?` branches on is the value it was applied to, but for a `Poll`'s `Pending` and `Ready(None)`, which continue as `Ok` of themselves |
| tuple, `[T; N]`, `&[T]`, `Vec<T>` | an array; for bytes, `Uint8Array` for `[u8; N]`, `rt.ByteSlice` for `&[u8]` and `&[u8; N]`, and `rt.ByteVec` for `Vec<u8>` |
| `&T`, `&mut T`, `Box<T>`, `Reverse<T>`, `NonZero<T>` | the value; a `&mut` to a value JS copies, such as a number, an `Option`, or a `String`, is an `rt.Place<T>` |
| `*const T`, `*mut T`, `NonNull<T>` | what a `&mut T` is: the object, or an `rt.Place<T>` to a value JS copies and to a library type such as a `Vec` or an `Arc`; `rt.NULL` for null and `rt.DANGLING` for dangling. A write of a whole object copies into it one object deep, as an assignment through a `&mut` does, so a pointer to a part kept as an object, such as a field of a tuple or struct type, is refused wherever the crate may put another object there; a read copies what the crate writes into in place |
| `MaybeUninit<T>` | `T`: `undefined` until written, or an object whose parts kept as objects are there, holding `undefined` |
| `union` | an object of slots named by offset, `at0` and `at8`, each holding the field part kept there; fields must overlap in the same type, or in a non-null pointer and an `Option` of it |
| an iterator | `rt.Iter<Item>`, whose adaptors are runtime functions: `rt.iterMap(rt.iterSlice(v), f)` |
| `a..b`, `a..=b` | `rt.Range<T>`, `rt.RangeInclusive<T>`, which are iterators too |
| `a..`, `..b`, `..=b`, `Bound<T>` | `{ start }`, `{ end }`, `{ end, inclusive: true }`; `rt.RangeBound<T>`, a union tagged by variant |
Expand Down Expand Up @@ -505,7 +508,8 @@ they need.

## Not yet

`unsafe` and raw pointers ([unsafe](/quest/m3/ts-unsafe.md)), and trait objects other than
Byte reinterpretation, pointer arithmetic and addresses, raw allocation, 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`, 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 boxed closure, binds to
hand-written TS ([bindings](#bindings)).
Expand Down Expand Up @@ -564,7 +568,9 @@ built natively for wasm32 and run beside its translation on the same inputs by
disagree with JS's, `vec![x; n]` of types whose `Clone` counts its clones, `&mut`
to values JS copies, of locals, fields, elements, and a map's values, passed on,
returned, kept in a struct, and captured, a `Vec`'s edits and sorts, with closures
that log what each is called with, and std's `Bound`, `Reverse`, and `NonZero`. Its
that log what each is called with, std's `Bound`, `Reverse`, and `NonZero`, and raw
pointers: a linked list of boxed nodes as `hashlink` keeps one, through a union and a
`MaybeUninit` entry, and pointers to locals, fields, and whole structs. Its
differential test runs through `caller.ts`, a
hand-written caller that also prints a `printable` error nothing translated prints,
and adds every 64- and 128-bit case over each pair of values next to a power of two,
Expand Down
2 changes: 0 additions & 2 deletions ts/fixtures/bind/refused.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ binding `raw::Tally::new`: rt/tally.ts would replace the runtime, rt.ts and rt/
binding `raw::checksum`: `checksum` binds the same item
binding `std::vec::Vec`: the type has a representation of its own
reading bind/missing.ts: No such file or directory (os error 2) at fixtures/bind/src/harness.rs:25:12
unsupported type `*const u32` at fixtures/bind/src/raw.rs:36:2
`raw::Held` binds without drop, so it would never drop its `raw::Tally` at fixtures/bind/src/harness.rs:61:13
a binding of `raw::value`, whose `N` one export for every instance can't recover from its arguments at fixtures/bind/src/raw.rs:65:1
a binding of `raw::zero`, whose `T` one export for every instance can't recover from its arguments at fixtures/bind/src/raw.rs:60:1
`raw::Held` binds without drop, so it would never drop its `raw::Tally` at fixtures/bind/src/harness.rs:61:6
unsupported call to `std::sync::Arc::<u32>::from_raw` at fixtures/bind/src/raw.rs:55:17
unsupported call to `std::sync::Arc::<u32>::into_raw` at fixtures/bind/src/raw.rs:42:11
a raw pointer dereferenced at fixtures/bind/src/raw.rs:48:12
an assignment through a reference to `blob::Blob` at fixtures/bind/src/harness.rs:68:2
1 change: 0 additions & 1 deletion ts/fixtures/crates/refused.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
unsupported call to `core::slice::<impl [u8]>::as_ptr` at fixtures/crates/dep/src/lib.rs:129:18
unsupported call to `std::ptr::const_ptr::<impl *const u8>::add` at fixtures/crates/dep/src/lib.rs:129:27
a raw pointer dereferenced at fixtures/crates/dep/src/lib.rs:129:11
25 changes: 25 additions & 0 deletions ts/fixtures/lang/expected/core/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { side, order } from "../keys.ts";
import type * as Hop from "../keys.Hop.ts";
import type * as Side from "../keys.Side.ts";
import type * as Way from "../keys.Way.ts";
import type * as Pair from "../pointers.Pair.ts";
import { from$From_Dir_u8 } from "../shapes.ts";
import type * as Dir from "../shapes.Dir.ts";
import type * as Meters from "../shapes.Meters.ts";
Expand Down Expand Up @@ -388,6 +389,13 @@ export function mapOr$Option_Ordering_u8_order(self: rt.Ordering | undefined, de
return order(self);
}

export function mapOr$Option_Pair_u8_copies_closure0(self: Pair.Pair | undefined, default_: rt.u8, f: (a0: Pair.Pair) => rt.u8): rt.u8 {
if (self === undefined) {
return default_;
}
return f(self);
}

export function mapOr$Option_Point_u8_known_closure0_closure0(self: Point.Point | undefined, default_: rt.u8, f: (a0: Point.Point) => rt.u8): rt.u8 {
if (self === undefined) {
return default_;
Expand Down Expand Up @@ -472,6 +480,13 @@ export function mapOr$Option_u32_u32_lent_closure4(self: rt.Place<rt.u32> | unde
return f(self);
}

export function mapOr$Option_u8_Arc_u8_u8_shared_closure0(self: [rt.u8, rt.Arc<rt.u8>] | undefined, default_: rt.u8, f: (a0: [rt.u8, rt.Arc<rt.u8>]) => rt.u8): rt.u8 {
if (self === undefined) {
return default_;
}
return f(self);
}

export function mapOr$Option_u8_Option_u8_anys_closure0(self: rt.u8 | undefined, default_: rt.u8 | undefined, f: (a0: rt.u8) => rt.u8 | undefined): rt.u8 | undefined {
if (self === undefined) {
return default_;
Expand Down Expand Up @@ -698,6 +713,16 @@ export function filter$Option_String_owned_closure1(self: string | undefined, pr
return undefined;
}

export function filter$Option_u32_sentinels_closure0(self: rt.u32 | undefined, predicate: (a0: rt.u32) => boolean): rt.u32 | undefined {
if (self !== undefined) {
const x: rt.u32 = self;
if (predicate(x)) {
return x;
}
}
return undefined;
}

export function filter$Option_u8_combinators_closure0(self: rt.u8 | undefined, predicate: (a0: rt.u8) => boolean): rt.u8 | undefined {
if (self !== undefined) {
const x: rt.u8 = self;
Expand Down
Loading