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
2 changes: 1 addition & 1 deletion quest/m2/ts-model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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
- [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
- [Floats](/quest/m2/ts-model/floats.md) - `f64` translates, so `Bandwidth::scaled` and `Duration::mul_f64` do
- [Float formatting](/quest/m2/ts-model/floats.md) - `f64` formats and parses as std does
- [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
- [Well-known crates without config](/quest/m2/ts-model/stock.md) - rust2-ts maps `bytes`, `rand`, and `tracing` itself, so `model.toml` stops binding and ignoring them
Expand Down
41 changes: 16 additions & 25 deletions quest/m2/ts-model/floats.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
# [L] Floats
# [M] Float formatting

## Goal

[rust2-ts](/ts/README.md) translates `f64`: its arithmetic, comparisons, `as` casts to and
from every integer width, its methods and constants, `str::parse`, its formatting, and
`Duration`'s `f64` methods, proven by differential tests against native Rust. moq-net's
`Bandwidth::scaled`, its probe thresholds, and its `Duration::mul_f64` jitter translate.
[rust2-ts](/ts/README.md) formats and parses `f64` as std does, proven by differential
tests against native Rust: `Display`, `Debug` (which switches to an exponent), a precision,
and `{:e}`/`{:E}`, byte for byte as `core::fmt`, and `str::parse::<f64>` by std's grammar,
not `Number`'s. `f64::clamp`, whose panic prints its bounds with `Debug`, translates with
them.

## Plan

Decided with the maintainer on 2026-10-27, from
[#135](https://github.com/kixelated/rust2/pull/135):
[#135](https://github.com/kixelated/rust2/pull/135): this quest owns float formatting; the
other types' format specs already translate. The rest of `f64`, its arithmetic, casts,
methods, and `Duration`'s `f64` methods, landed in
[#165](https://github.com/kixelated/rust2/pull/165).

- `f64` here, since moq-net uses it; [`f32`](/quest/m3/ts-f32.md) follows in m3.
- An `f64` is a JS number. Every operation std specifies exactly matches native bit for
bit, including where JS differs: `round` rounds half away from zero, `max` and `min`
ignore a NaN, a cast to an integer saturates and sends NaN to 0, and a 64- or 128-bit
integer rounds to the nearest `f64` once. `str::parse` follows std's grammar, not
`Number`'s.
- The functions std leaves to the platform's libm (`sin`, `exp`, `ln`, `powf`, `powi`, and
the rest) map to `Math.*`. Their tests compare within a small ULP bound the README
states, not an exact libm port. `mul_add` is specified as one rounding, which `Math`
has no cheap form of, so it is refused with a span until a consumer needs it.
- `to_bits` and `from_bits` go through a `DataView`. JS does not guarantee a NaN's
payload, so the tests cover payloads in Node and Chrome and the README notes it.
- This quest owns float formatting byte for byte as `core::fmt`: `Display`, `Debug`
(which switches to an exponent), a precision, and `{:e}`/`{:E}`.
The other types' format specs already translate.
- `Duration`'s `f64` methods (`as_secs_f64`, `from_secs_f64`, `mul_f64`, `div_f64`,
`div_duration_f64`, and the `try_` forms) port std's integer algorithms, with its
panics and `Err`s at `u64::MAX` seconds, negative values, and NaN.
- Report the model's census before and after.
- `Display` and `Debug` without a precision print the shortest digits that read back as
the value. JS's `Number.prototype.toString` picks the same digits, which leaves only the
layout, `1e-7` against `0.0000001`, to port.
- A precision rounds the exact decimal value half to even, where `toFixed` rounds half up,
so it needs the exact expansion rather than `toFixed`.
- `str::parse` takes std's grammar (`inf`, `NaN`, no hex, no `_`) and its errors.

Public API: the translator's output.
6 changes: 3 additions & 3 deletions quest/m3/ts-f32.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ native Rust.
## Plan

Decided with the maintainer on 2026-10-27: rust2 aims to support all of std, so this is
planned without a moq consumer. The decisions of [floats](/quest/m2/ts-model/floats.md)
carry over.
planned without a moq consumer. The decisions of [float formatting](/quest/m2/ts-model/floats.md)
and of `f64`, which [#165](https://github.com/kixelated/rust2/pull/165) landed, carry over.

- An `f32` is a JS number rounded with `Math.fround` after every operation. A 64- or
128-bit integer rounds to `f32` once, never through `f64`, which would round twice.
Expand All @@ -21,4 +21,4 @@ Public API: the translator's output.

## Required

- [Floats](/quest/m2/ts-model/floats.md) - `f64` translates, with the formatting and casts `f32` builds on
- [Float formatting](/quest/m2/ts-model/floats.md) - `f64` formats as std does, which `f32`'s formatting builds on
17 changes: 15 additions & 2 deletions ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ and each type a module of its own beside it, holding the type and its functions
from. `rt.ts` beside them is the
runtime: what std does that JS does not, with iterators in `rt/iter.ts`, `str`'s searches
in `rt/str.ts`, collections in `rt/collections.ts`, ownership and async in
`rt/task.ts`, and `Duration` in `rt/time.ts`. What translates from std's own MIR,
`rt/task.ts`, `f64`'s methods in `rt/float.ts`, and `Duration` in `rt/time.ts`. What translates from std's own MIR,
`Option`'s, `Result`'s, and `Poll`'s methods, goes in a module under its crate's name, as
`core/option.ts` holds `map$Option_u64_u64_rescale_closure0`.

Expand All @@ -71,6 +71,7 @@ in `rt/str.ts`, collections in `rt/collections.ts`, ownership and async in
| `char` | `rt.char`: a `number` branded so, its Unicode scalar value |
| `u8`-`u32`, `i8`-`i32`, `usize`, `isize` | `rt.u8` and so on: a `number` branded with its type, checked: overflow panics |
| `u64`, `i64`, `u128`, `i128` | `rt.u64` and so on: a branded `number` while a safe integer, 32-bit limbs past 2^53, checked the same way |
| `f64` | `rt.f64`: a `number` branded so, which is the same IEEE 754 double |
| `struct` | a plain object, typed by an interface; a class if it may drop |
| `enum` without data | a union of strings: `"Lite01" \| "Lite02"`, or `never` without variants |
| `enum` | a union of objects tagged by variant |
Expand Down Expand Up @@ -119,6 +120,18 @@ comparisons are runtime calls, `rt.u64Add(a, b)` and `rt.u64Lt(a, b)`, and `x as
is `rt.u64Low(x)`. `((hi as u64) << 32) | lo as u64` of halves of at most 32 bits is
one call, `rt.u64FromHalves(hi, lo)`.

An `f64` is a JS number, so its operators are JS's, which round as Rust's do. Where
JS differs from std, the runtime has std's: `round` halves away from zero, `max` and
`min` ignore a NaN, and a cast to an integer truncates, saturates at the type's bounds,
and sends NaN to 0. A 64- or 128-bit integer `as f64` rounds once, to the nearest. The
functions std leaves to the platform's libm, such as `sin`, `exp`, `ln`, `powf`, and
`hypot`, are `Math`'s, which the tests find within 2 ULPs of native Rust's in Node;
`mul_add`, which rounds once, is refused. `Duration`'s `f64` methods run std's integer
algorithm, to the nearest nanosecond with its errors and panics. JS doesn't say which
NaN an operation makes, so a NaN's payload and sign are the engine's: Node and Chrome
keep them through `from_bits` and `to_bits`, but V8 quiets a signaling NaN stored in an
array, as a `Vec<f64>` holds it.

`format!`, `write!`, `panic!`, `assert!`, and the other macros of `format_args!`
translate, as do `Display` and `Debug` impls, derived and thiserror's included. The
runtime ports `core::fmt`, so the text matches native Rust's byte for byte. A placeholder
Expand Down Expand Up @@ -435,7 +448,7 @@ they need.
Until they translate, an item that needs one, such as a boxed future, binds to
hand-written TS ([bindings](#bindings)).

These fail translation: floats ([Floats](/quest/m2/ts-model/floats.md)), statics, async closures, a closure that owns a value
These fail translation: formatting and parsing an `f64`, and `f64::clamp`, whose panic formats them ([float formatting](/quest/m2/ts-model/floats.md)), `f32` ([`f32`](/quest/m3/ts-f32.md)), statics, async closures, a closure that owns a value
that drops given to a std function that drops it beside another value that drops, as
`for_each` of items that drop does, or keeps it other than in an iterator adaptor, and
values that drop in some iterators ([drop order](/quest/m3/ts-drop-order.md)), a `Cow` of other than `str`,
Expand Down
52 changes: 51 additions & 1 deletion ts/fixtures/lang/caller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,56 @@ function wide(n: bigint): Uint8Array {
return bytes;
}

/** A double's bits, big-endian, as `floats` takes them. */
function double(x: number | bigint): number[] {
const bytes = new Uint8Array(8);
const view = new DataView(bytes.buffer);
if (typeof x === "bigint") view.setBigUint64(0, x);
else view.setFloat64(0, x);
return [...bytes];
}

/**
* `floats`' operands: each side of a half, of 2^52 and 2^53, where each integer type
* saturates, of a nanosecond and `u64::MAX` seconds, subnormals, infinities, and NaNs,
* one of them negative and one with a payload.
*/
const doubles: (number | bigint)[] = [
0, -0, 0.5, -0.5, 1.5, 2.5, -2.5, 0.49999999999999994, 1, -1, 3, 0.1, 1 / 3, 2 ** 52 - 0.5, 2 ** 52, 2 ** 53, 2 ** 53 + 2,
255.5, -128.5, 2147483647.5, -2147483648.5, 4294967295.9, 2 ** 63, -(2 ** 63), -(2 ** 63) - 2048, 2 ** 64, 2 ** 64 - 2048,
2 ** 127, 2 ** 128, -(2 ** 128), 1e-9, 0.5e-9, 1.5e-9, 0.999999999, 0.9999999995, 1.0000000005, 4.656612873077393e-10,
1e-310, 5e-324, 2.2250738585072014e-308, Number.MAX_VALUE, -Number.MAX_VALUE, Infinity, -Infinity, NaN,
0xfff8000000000000n, 0x7ff0000000000001n,
];
/** Integers whose `f64` rounds, as `floats`' kind 2 takes them: 16 bytes of a `u128`. */
const wides = [0n, 1n, (1n << 53n) + 1n, (1n << 53n) + 3n, (1n << 54n) + 2n, (1n << 63n) + 1025n, (1n << 64n) - 1n, (1n << 64n) - 1025n, (1n << 64n) + (1n << 11n) + 1n, (1n << 127n) + 1n, (1n << 128n) - 1n, (1n << 128n) - (1n << 74n), -(1n << 100n) - 1n];
const floats = [
...Array.from({ length: 10 }, (_, kind) => doubles.flatMap((a) => doubles.map((b) => Uint8Array.of(58, kind, ...double(a), ...double(b), 1)))).flat(),
// `from_secs_f64`, `mul_f64`, and `div_f64` panicking, and `Duration::MAX.mul_f64`.
...[18, 28, 38].flatMap((kind) => doubles.map((a) => Uint8Array.of(58, kind, ...double(a), ...double(a), 3))),
...wides.map((v) => {
const bytes = new Uint8Array(16);
new DataView(bytes.buffer).setBigUint64(0, BigInt.asUintN(64, v >> 64n));
new DataView(bytes.buffer).setBigUint64(8, BigInt.asUintN(64, v));
return Uint8Array.of(58, 2, ...bytes);
}),
];

/** How many ULPs from native Rust's each `f64` of `floats`' kind 7 may be, which Rust leaves to libm. */
const ULPS = 2n;

export function within(input: Uint8Array, native: Uint8Array, translated: Uint8Array): boolean {
if (input[0] % 59 !== 58 || input[1] % 10 !== 7 || native.length !== translated.length) return false;
const [a, b] = [native, translated].map((v) => new DataView(v.buffer, v.byteOffset, v.length));
// Bits as a line through zero, so the distance between two is how many doubles apart they are.
const line = (v: bigint) => (v < 0n ? -(v & 0x7fffffffffffffffn) : v);
for (let i = 0; i < native.length; i += 8) {
const d = line(a.getBigInt64(i)) - line(b.getBigInt64(i));
if (d > ULPS || d < -ULPS) return false;
}
return true;
}

/**
* `utf8::decodes` of raw bytes: a byte order mark, which std keeps, and the sequences at
* each edge of what it takes: surrogates, overlongs, past U+10FFFF, and cut short at the
Expand All @@ -81,4 +131,4 @@ const utf8 = [
[0x80, 0xf8, 0xff],
].map((bytes) => Uint8Array.of(57, 0, ...bytes));

export const cases: Uint8Array[] = [...loops, ...durations, ...utf8];
export const cases: Uint8Array[] = [...loops, ...durations, ...utf8, ...floats];
21 changes: 21 additions & 0 deletions ts/fixtures/lang/expected/core/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export function unwrapOr$Option_char(self: rt.char | undefined, default_: rt.cha
return self;
}

export function unwrapOr$Option_f64(self: rt.f64 | undefined, default_: rt.f64): rt.f64 {
if (self === undefined) {
return default_;
}
return self;
}

export function unwrapOr$Option_i64(self: rt.i64 | undefined, default_: rt.i64): rt.i64 {
if (self === undefined) {
return default_;
Expand Down Expand Up @@ -254,6 +261,13 @@ export function mapOr$Option_Nibble_u8_sets_closure4(self: Nibble.Nibble | undef
return f(self);
}

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

export function mapOr$Option_Ordering_u8_order(self: rt.Ordering | undefined, default_: rt.u8, _: (a0: rt.Ordering) => rt.u8): rt.u8 {
if (self === undefined) {
return default_;
Expand Down Expand Up @@ -324,6 +338,13 @@ export function mapOr$Option_u8_bool_take_largest_closure0(self: rt.u8 | undefin
return f(self);
}

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

export function mapOr$Option_u8_u8_iters_closure3(self: rt.u8 | undefined, default_: rt.u8, f: (a0: rt.u8) => rt.u8): rt.u8 {
if (self === undefined) {
return default_;
Expand Down
22 changes: 22 additions & 0 deletions ts/fixtures/lang/expected/floats.Gauge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Generated by rust2-ts. Do not edit.

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

export function eq$PartialEq(self: Gauge, other: Gauge): boolean {
if (!(self.level === other.level)) {
return false;
}
return self.peak === other.peak;
}

export function default$Default(): Gauge {
return { level: 0.0, peak: undefined };
}

/** A struct with an `f64` field, changed in place through a `&mut`. */
export interface Gauge {
level: rt.f64;
peak: rt.f64 | undefined;
}

//# sourceMappingURL=floats.Gauge.ts.map
Loading