From 45833befaa35d7913ce89ddd936c5599b0a16f29 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:57:36 -0700 Subject: [PATCH 1/4] chore(quest): claim quest/m2/ts-model/floats.md Quest-Claim: 2ce0a055a10071e6 From cce70b30debc38916d5e55c9ba725c7c1af7b4d0 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:22:51 -0700 Subject: [PATCH 2/4] feat(ts): translate f64 and Duration's f64 methods Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 2 +- quest/m2/ts-model/floats.md | 41 ++- quest/m3/ts-f32.md | 6 +- ts/README.md | 17 +- ts/fixtures/lang/caller.ts | 52 +++- ts/fixtures/lang/expected/core/option.ts | 21 ++ ts/fixtures/lang/expected/floats.Gauge.ts | 22 ++ ts/fixtures/lang/expected/floats.ts | 250 ++++++++++++++++++ ts/fixtures/lang/expected/harness.ts | 5 +- ts/fixtures/lang/src/floats.rs | 301 ++++++++++++++++++++++ ts/fixtures/lang/src/harness.rs | 7 +- ts/fixtures/lang/src/lib.rs | 11 +- ts/fixtures/reject/census.md | 50 ++-- ts/fixtures/reject/expected.txt | 10 +- ts/fixtures/reject/src/lib.rs | 16 +- ts/fixtures/reject/ts.toml | 2 + ts/runtime/rt.ts | 1 + ts/runtime/rt/float.ts | 211 +++++++++++++++ ts/runtime/rt/int.ts | 62 +++++ ts/runtime/rt/time.ts | 88 ++++++- ts/src/lower/collections.rs | 7 +- ts/src/lower/consts.rs | 4 + ts/src/lower/float.rs | 115 +++++++++ ts/src/lower/fmt.rs | 2 + ts/src/lower/iter.rs | 12 + ts/src/lower/library.rs | 16 +- ts/src/lower/mir/mod.rs | 6 +- ts/src/lower/mir/place.rs | 1 + ts/src/lower/mir/rvalue.rs | 2 +- ts/src/lower/mod.rs | 3 + ts/src/lower/num.rs | 10 + ts/src/lower/time.rs | 6 + ts/src/program.rs | 1 + ts/tests/differential.ts | 7 +- ts/tests/nan.ts | 28 ++ ts/tests/runtime.rs | 11 + 36 files changed, 1324 insertions(+), 82 deletions(-) create mode 100644 ts/fixtures/lang/expected/floats.Gauge.ts create mode 100644 ts/fixtures/lang/expected/floats.ts create mode 100644 ts/fixtures/lang/src/floats.rs create mode 100644 ts/runtime/rt/float.ts create mode 100644 ts/src/lower/float.rs create mode 100644 ts/tests/nan.ts diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index 7b35fc781..204f376d5 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -50,7 +50,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 - [Std gaps the model reaches](/quest/m2/ts-model/std-more.md) - ranges, wrappers, and `Vec`'s editing methods -- [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 diff --git a/quest/m2/ts-model/floats.md b/quest/m2/ts-model/floats.md index d0f61c3a4..ef0dee83f 100644 --- a/quest/m2/ts-model/floats.md +++ b/quest/m2/ts-model/floats.md @@ -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::` 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. diff --git a/quest/m3/ts-f32.md b/quest/m3/ts-f32.md index 02de5f77e..890dfc45a 100644 --- a/quest/m3/ts-f32.md +++ b/quest/m3/ts-f32.md @@ -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. @@ -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 diff --git a/ts/README.md b/ts/README.md index ef5bf02c8..2bf907074 100644 --- a/ts/README.md +++ b/ts/README.md @@ -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 and `Result`'s combinators, goes in a module under its crate's name, as `core/option.ts` holds `map$Option_u64_u64_rescale_closure0`. @@ -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 | @@ -117,6 +118,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` 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 @@ -426,7 +439,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 std's iterators or collections, and values that drop in some iterators ([Drop](/quest/m2/ts-drop.md)), a `Cow` of other than `str`, a collection holding a type that can be `undefined`, as `()` and `Option` can, a std diff --git a/ts/fixtures/lang/caller.ts b/ts/fixtures/lang/caller.ts index 6d81f0c79..663c4ebb0 100644 --- a/ts/fixtures/lang/caller.ts +++ b/ts/fixtures/lang/caller.ts @@ -63,4 +63,54 @@ function wide(n: bigint): Uint8Array { return bytes; } -export const cases: Uint8Array[] = [...loops, ...durations]; +/** 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(54, 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(54, 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(54, 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] % 56 !== 54 || 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; +} + +export const cases: Uint8Array[] = [...loops, ...durations, ...floats]; diff --git a/ts/fixtures/lang/expected/core/option.ts b/ts/fixtures/lang/expected/core/option.ts index ca2ea7157..060771a12 100644 --- a/ts/fixtures/lang/expected/core/option.ts +++ b/ts/fixtures/lang/expected/core/option.ts @@ -44,6 +44,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_; @@ -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_; @@ -317,6 +331,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_; diff --git a/ts/fixtures/lang/expected/floats.Gauge.ts b/ts/fixtures/lang/expected/floats.Gauge.ts new file mode 100644 index 000000000..7528f2d7d --- /dev/null +++ b/ts/fixtures/lang/expected/floats.Gauge.ts @@ -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 diff --git a/ts/fixtures/lang/expected/floats.ts b/ts/fixtures/lang/expected/floats.ts new file mode 100644 index 000000000..91ce70e2a --- /dev/null +++ b/ts/fixtures/lang/expected/floats.ts @@ -0,0 +1,250 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { unwrapOr$Option_f64, mapOr$Option_Ordering_u8_floats_closure0, mapOr$Option_u8_u64_floats_closure1 } from "./core/option.ts"; +import * as Gauge from "./floats.Gauge.ts"; +import { put } from "./fmt.ts"; + +/** An `f64` from the front of `data`, its bits big-endian and zero-padded. */ +export function take(data: rt.ByteSlice): [rt.f64, rt.ByteSlice] { + const buf: Uint8Array = rt.bytes(8); + const n: rt.usize = rt.min(data.length, rt.byteSliceFrom(buf).length); + rt.byteSliceCopyFromSlice(rt.byteSliceRange(rt.byteSliceFrom(buf), 0, n), rt.byteSliceRange(data, 0, n)); + return [rt.f64FromBits(rt.u64FromBeBytes(buf)), rt.byteSliceRange(data, n)] as [rt.f64, rt.ByteSlice]; +} + +/** `x`'s bits, any NaN as `f64::NAN`'s. */ +export function putF64(out: rt.ByteVec, x: rt.f64): void { + let x2!: rt.f64; + if (!Number.isNaN(x)) { + x2 = x; + } else { + x2 = NaN; + } + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.f64ToBits(x2)))); +} + +export function putDuration(out: rt.ByteVec, d: rt.Duration): void { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.durationAsSecs(d)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.durationSubsecNanos(d)))); +} + +/** What `try_from_secs_f64` made, or its error printed both ways. */ +export function putSecs(out: rt.ByteVec, d: rt.Result): void { + if (d.ok) { + putDuration(out, d.value); + } else { + const e: rt.TryFromFloatSecsError = d.error; + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtTryFromFloatSecsErrorDisplay(e, f), (f: rt.Formatter) => rt.fmtTryFromFloatSecsErrorDebug(e, f)]; + put(out, rt.fmtFormat([args2[0], "|", args2[1], "|", rt.fmtSpec({ alternate: true }, args2[1])])); + } +} + +export function raise(level: rt.Place, by: rt.f64): void { + level.v = level.v + by; + level.v = level.v * 2.0; +} + +export function floats(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { + let zeros!: boolean; + let _242!: rt.f64; + const _6: [rt.f64, rt.ByteSlice] = take(data); + const a: rt.f64 = _6[0]; + const _9: [rt.f64, rt.ByteSlice] = take(_6[1]); + const b: rt.f64 = _9[0]; + const _10: rt.u8 = rt.u8Rem(kind, 10); + if (_10 === 0) { + const iter: rt.Iter = rt.iterSlice([a + b, a - b, a * b, a / b, a % b, -a, a + 1.5, 0.1 * a]); + while (true) { + const _23: rt.f64 | rt.Done = iter.next(); + if (_23 === rt.DONE) { + break; + } + putF64(out, _23); + } + const iter2: rt.Iter = rt.iterSlice([a === b, a !== b, a < b, a <= b, a > b, a >= b, a === 0.0, a < b, a >= b]); + while (true) { + const _45: boolean | rt.Done = iter2.next(); + if (_45 === rt.DONE) { + break; + } + rt.byteVecPush(out, _45 ? 1 : 0); + } + let c: rt.f64 = a; + c = c + b; + c = c - 0.25; + c = c * b; + c = c / 3.0; + c = c % a; + putF64(out, c); + } else if (_10 === 1) { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u8ToBeBytes(rt.u8FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u16ToBeBytes(rt.u16FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.u32FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.u64FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u128ToBeBytes(rt.u128FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.usizeToBeBytes(rt.usizeFromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i8ToBeBytes(rt.i8FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i16ToBeBytes(rt.i16FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i32ToBeBytes(rt.i32FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i64ToBeBytes(rt.i64FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i128ToBeBytes(rt.i128FromF64(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.isizeToBeBytes(rt.isizeFromF64(a)))); + } else if (_10 === 2) { + const wide: Uint8Array = rt.bytes(16); + const n: rt.usize = rt.min(data.length, 16); + rt.byteSliceCopyFromSlice(rt.byteSliceRange(rt.byteSliceFrom(wide), 0, n), rt.byteSliceRange(data, 0, n)); + const v: rt.u128 = rt.u128FromBeBytes(wide); + const iter3: rt.Iter = rt.iterSlice([(rt.u128Low(v) & 0xff) as rt.f64, (rt.u128Low(v) & 0xffff) as rt.f64, rt.u128Low(v) as rt.f64, rt.u64ToF64(rt.u64Cast(v)), rt.u128ToF64(v), rt.u128Low(v) as rt.f64, (rt.u128Low(v) << 24) >> 24 as rt.f64, (rt.u128Low(v) << 16) >> 16 as rt.f64, (rt.u128Low(v) | 0) as rt.f64, rt.i64ToF64(rt.i64Cast(v)), rt.i128ToF64(rt.i128Cast(v)), (rt.u128Low(v) | 0) as rt.f64, rt.u128Low(v) as rt.f64, (rt.u128Low(v) << 16) >> 16 as rt.f64, rt.u128And(v, 1) === 1 ? 1.0 : 0.0, (rt.u128Low(v) & 0xff) as rt.f64, 1.8446744073709552e19, -9.223372036854776e18, 3.402823669209385e38]); + while (true) { + const _163: rt.f64 | rt.Done = iter3.next(); + if (_163 === rt.DONE) { + break; + } + putF64(out, _163); + } + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.u64FromF64(rt.u64ToF64(rt.u64Cast(v)))))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i128ToBeBytes(rt.i128FromF64(rt.i128ToF64(rt.i128Cast(v)))))); + } else if (_10 === 3) { + const iter4: rt.Iter = rt.iterSlice([Math.floor(a), Math.ceil(a), Math.trunc(a), rt.f64Round(a), rt.f64RoundTiesEven(a), rt.f64Fract(a), Math.abs(a), rt.f64Signum(a), Math.sqrt(a), 1 / a, a * 57.29577951308232, a * 0.017453292519943295]); + while (true) { + const _197: rt.f64 | rt.Done = iter4.next(); + if (_197 === rt.DONE) { + break; + } + putF64(out, _197); + } + } else if (_10 === 4) { + const iter5: rt.Iter = rt.iterSlice([Number.isNaN(a), Number.isFinite(a), rt.f64IsInfinite(a), rt.f64IsNormal(a), rt.f64IsSubnormal(a), rt.f64IsSignPositive(a), rt.f64IsSignNegative(a)]); + while (true) { + const _212: boolean | rt.Done = iter5.next(); + if (_212 === rt.DONE) { + break; + } + rt.byteVecPush(out, _212 ? 1 : 0); + } + const iter6: rt.Iter = rt.iterSlice([rt.f64Copysign(a, b), rt.f64Copysign(1.5, a), rt.f64NextUp(a), rt.f64NextDown(a)]); + while (true) { + const _225: rt.f64 | rt.Done = iter6.next(); + if (_225 === rt.DONE) { + break; + } + putF64(out, _225); + } + } else if (_10 === 5) { + if (!(a === 0.0)) { + zeros = false; + } else { + zeros = b === 0.0; + } + const iter7: rt.Iter = rt.iterSlice([rt.f64Max(a, b), rt.f64Min(a, b)]); + while (true) { + const _237: rt.f64 | rt.Done = iter7.next(); + if (_237 === rt.DONE) { + break; + } + const x5: rt.f64 = _237; + if (!zeros) { + _242 = x5; + } else { + _242 = 0.0; + } + putF64(out, _242); + } + const iter8: rt.Iter = rt.iterSlice([rt.f64Midpoint(a, b), rt.f64DivEuclid(a, b), rt.f64RemEuclid(a, b)]); + while (true) { + const _250: rt.f64 | rt.Done = iter8.next(); + if (_250 === rt.DONE) { + break; + } + putF64(out, _250); + } + rt.byteVecPush(out, rt.f64TotalCmp(a, b) & 0xff); + rt.byteVecPush(out, mapOr$Option_Ordering_u8_floats_closure0(rt.f64PartialCmp(a, b), 9, (o: rt.Ordering): rt.u8 => { + return o & 0xff; + })); + } else if (_10 === 6) { + if (!Number.isNaN(a)) { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.f64ToBits(a)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.f64ToBeBytes(a))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.f64ToLeBytes(a))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.f64ToLeBytes(a))); + const be: Uint8Array = rt.f64ToBeBytes(a); + const le: Uint8Array = rt.f64ToLeBytes(a); + const iter9: rt.Iter = rt.iterSlice([rt.f64FromBeBytes(be), rt.f64FromLeBytes(le), rt.f64FromLeBytes(le)]); + while (true) { + const _292: rt.f64 | rt.Done = iter9.next(); + if (_292 === rt.DONE) { + break; + } + putF64(out, _292); + } + } + putF64(out, rt.f64FromBits(rt.u64Xor(rt.f64ToBits(a), 1))); + } else if (_10 === 7) { + const n2: rt.i32 = rt.i32Rem(rt.u64Low(rt.f64ToBits(b)) | 0, 64); + const _310: [rt.f64, rt.f64] = rt.f64SinCos(a); + const s: rt.f64 = _310[0]; + const c2: rt.f64 = _310[1]; + const iter10: rt.Iter = rt.iterSlice([Math.sin(a), Math.cos(a), Math.tan(a), Math.asin(a), Math.acos(a), Math.atan(a), Math.atan2(a, b), Math.sinh(a), Math.cosh(a), Math.tanh(a), Math.asinh(a), Math.acosh(a), rt.f64Atanh(a), Math.exp(a), Math.pow(2, a), Math.expm1(a), Math.log(a), Math.log(a) / Math.log(b), Math.log2(a), Math.log10(a), Math.log1p(a), rt.f64Powf(a, b), rt.f64Powi(a, n2), Math.cbrt(a), Math.hypot(a, b), s, c2]); + while (true) { + const _339: rt.f64 | rt.Done = iter10.next(); + if (_339 === rt.DONE) { + break; + } + putF64(out, _339); + } + } else if (_10 === 8) { + const d: rt.Duration = rt.durationNew(rt.u64Shl(mapOr$Option_u8_u64_floats_closure1(rt.byteSliceFirst(_9[1]), 0, (_2: rt.u8): rt.u64 => { + return _2 as rt.u64; + }), 20), 123456789); + putF64(out, rt.durationAsSecsF64(d)); + const _355: rt.Duration = rt.durationFromMillis(1500); + putF64(out, rt.durationDivDurationF64(d, _355)); + putF64(out, rt.durationDivDurationF64({ secs: 0, nanos: 0 }, { secs: 0, nanos: 0 })); + putSecs(out, rt.durationTryFromSecsF64(a)); + putSecs(out, rt.durationTryFromSecsF64(Math.abs(a))); + putSecs(out, rt.durationTryFromSecsF64(Math.abs(a) * 1e-9)); + const _367: rt.u8 = rt.u8Rem(rt.u8Div(kind, 10), 4); + if (_367 === 0) { + putDuration(out, rt.durationFromSecsF64(a)); + } else if (_367 === 1) { + putDuration(out, rt.durationMulF64(d, a)); + } else if (_367 === 2) { + putDuration(out, rt.durationDivF64(d, a)); + } else { + putDuration(out, rt.durationMulF64({ secs: { hi: 0xffffffff, lo: 0xffffffff } as rt.u64, nanos: 999999999 }, b)); + } + } else { + const iter11: rt.Iter = rt.iterSlice([1.7976931348623157e308, -1.7976931348623157e308, 2.2250738585072014e-308, 2.220446049250313e-16, Infinity, -Infinity, NaN, -0.0, 3.141592653589793, 6.283185307179586, 2.718281828459045, 1.4142135623730951, 0.7071067811865476, 0.6931471805599453, 2.302585092994046, 1.4426950408889634, 0.4342944819032518, 1.5707963267948966, 0.3183098861837907, 1e-300, 123456789.125, 0.5]); + while (true) { + const _377: rt.f64 | rt.Done = iter11.next(); + if (_377 === rt.DONE) { + break; + } + putF64(out, _377); + } + const v2: rt.f64[] = [a, b, 0.5]; + putF64(out, rt.iterSum(rt.iterSlice(v2), rt.f64Add, -0.0)); + putF64(out, rt.iterProduct(rt.iterSlice(v2), rt.f64Mul, 1.0)); + putF64(out, rt.iterSum(rt.iterSlice([]), rt.f64Add, -0.0)); + putF64(out, rt.iterSum(rt.iterSlice([-0.0, -0.0]), rt.f64Add, -0.0)); + putF64(out, 0.0); + const g: Gauge.Gauge = { level: a, peak: b }; + raise(rt.place(() => g.level, (value) => g.level = value), b); + if (g.peak !== undefined) { + const _409: rt.f64 = rt.f64Max(g.peak, g.level); + g.peak = _409; + } + putF64(out, g.level); + putF64(out, unwrapOr$Option_f64(g.peak, -1.0)); + const _422: Gauge.Gauge = Gauge.default$Default(); + rt.byteVecPush(out, Gauge.eq$PartialEq(g, _422) ? 1 : 0); + rt.byteVecPush(out, g.peak === b ? 1 : 0); + let local: rt.f64 = a; + raise(rt.place(() => local, (value2) => local = value2), 1.0); + putF64(out, local); + } +} + +//# sourceMappingURL=floats.ts.map diff --git a/ts/fixtures/lang/expected/harness.ts b/ts/fixtures/lang/expected/harness.ts index b84374bba..3e5fe978d 100644 --- a/ts/fixtures/lang/expected/harness.ts +++ b/ts/fixtures/lang/expected/harness.ts @@ -7,6 +7,7 @@ import { hashMaps, sets, btreeMaps, deques, vecs, stores } from "./collections.t import { combinators } from "./combinators.ts"; import { cows } from "./cows.ts"; import { durations } from "./durations.ts"; +import { floats } from "./floats.ts"; import { ints, strings, derived, written, counts, rounding, panics } from "./fmt.ts"; import { methods } from "./generics.ts"; import { u8Op, u16Op, u32Op, u64Op, u128Op, usizeOp, i8Op, i16Op, i32Op, i64Op, i128Op, isizeOp, i8Neg, i16Neg, i32Neg, i64Neg, i128Neg, isizeNeg, cast, convert } from "./ints.ts"; @@ -27,7 +28,7 @@ export function run(input: rt.ByteSlice): rt.ByteVec { const _10: [rt.u8, rt.u8] = [input.data[input.start], input.data[input.start + 1]]; const op: rt.u8 = _10[0]; const kind: rt.u8 = _10[1]; - const _14: rt.u8 = rt.u8Rem(op, 55); + const _14: rt.u8 = rt.u8Rem(op, 56); if (_14 === 0) { u8Op(kind, new rt.ByteSlice(input.data, input.start + 2, input.length - 2), out); } else if (_14 === 1) { @@ -153,6 +154,8 @@ export function run(input: rt.ByteSlice): rt.ByteVec { counts(new rt.ByteSlice(input.data, input.start + 2, input.length - 2), out); } else if (_14 === 53) { rounding(out); + } else if (_14 === 54) { + floats(kind, new rt.ByteSlice(input.data, input.start + 2, input.length - 2), out); } else { skips(kind, new rt.ByteSlice(input.data, input.start + 2, input.length - 2), out); } diff --git a/ts/fixtures/lang/src/floats.rs b/ts/fixtures/lang/src/floats.rs new file mode 100644 index 000000000..bc35fb4c2 --- /dev/null +++ b/ts/fixtures/lang/src/floats.rs @@ -0,0 +1,301 @@ +//! `f64`: arithmetic and comparisons, `as` casts to and from every integer width, its +//! methods and constants, sums, places, and `Duration`'s `f64` methods. Operands are +//! bits, so NaNs, infinities, and subnormals come up; `caller.ts` names the values random +//! bits rarely make. A NaN result is written as one NaN, since neither side specifies +//! which NaN an operation makes, and the functions std leaves to libm, kind 7, agree +//! within the ULPs `caller.ts` allows. + +use std::f64::consts; +use std::time::Duration; + +use crate::fmt::put; + +/// An `f64` from the front of `data`, its bits big-endian and zero-padded. +fn take(data: &[u8]) -> (f64, &[u8]) { + let mut buf = [0u8; 8]; + let n = data.len().min(buf.len()); + buf[..n].copy_from_slice(&data[..n]); + (f64::from_bits(u64::from_be_bytes(buf)), &data[n..]) +} + +/// `x`'s bits, any NaN as `f64::NAN`'s. +fn put_f64(out: &mut Vec, x: f64) { + let x = if x.is_nan() { f64::NAN } else { x }; + out.extend_from_slice(&x.to_bits().to_be_bytes()); +} + +fn put_duration(out: &mut Vec, d: Duration) { + out.extend_from_slice(&d.as_secs().to_be_bytes()); + out.extend_from_slice(&d.subsec_nanos().to_be_bytes()); +} + +/// What `try_from_secs_f64` made, or its error printed both ways. +fn put_secs(out: &mut Vec, d: Result) { + match d { + Ok(d) => put_duration(out, d), + Err(e) => put(out, &format!("{e}|{e:?}|{e:#?}")), + } +} + +/// A struct with an `f64` field, changed in place through a `&mut`. +#[derive(Clone, Copy, PartialEq, Default)] +struct Gauge { + level: f64, + peak: Option, +} + +fn raise(level: &mut f64, by: f64) { + *level += by; + *level *= 2.0; +} + +/// Each float constant std has, in a `const` of the crate's. +const HALF: f64 = 0.5; +const CONSTANTS: [f64; 22] = [ + f64::MAX, + f64::MIN, + f64::MIN_POSITIVE, + f64::EPSILON, + f64::INFINITY, + f64::NEG_INFINITY, + f64::NAN, + -0.0, + consts::PI, + consts::TAU, + consts::E, + consts::SQRT_2, + consts::FRAC_1_SQRT_2, + consts::LN_2, + consts::LN_10, + consts::LOG2_E, + consts::LOG10_E, + consts::FRAC_PI_2, + consts::FRAC_1_PI, + 1e-300, + 123456789.125, + HALF, +]; + +pub fn floats(kind: u8, data: &[u8], out: &mut Vec) { + let (a, rest) = take(data); + let (b, rest) = take(rest); + match kind % 10 { + 0 => { + // Arithmetic, which JS rounds as Rust does, and comparisons, NaN's included. + for x in [a + b, a - b, a * b, a / b, a % b, -a, a + 1.5, 0.1 * a] { + put_f64(out, x); + } + for bit in [ + a == b, + a != b, + a < b, + a <= b, + a > b, + a >= b, + a == 0.0, + a.lt(&b), + (&a).ge(&&b), + ] { + out.push(bit as u8); + } + let mut c = a; + c += b; + c -= 0.25; + c *= b; + c /= 3.0; + c %= a; + put_f64(out, c); + } + 1 => { + // To every integer type: truncated, saturated at its bounds, and 0 for NaN. + out.extend_from_slice(&(a as u8).to_be_bytes()); + out.extend_from_slice(&(a as u16).to_be_bytes()); + out.extend_from_slice(&(a as u32).to_be_bytes()); + out.extend_from_slice(&(a as u64).to_be_bytes()); + out.extend_from_slice(&(a as u128).to_be_bytes()); + out.extend_from_slice(&(a as usize).to_be_bytes()); + out.extend_from_slice(&(a as i8).to_be_bytes()); + out.extend_from_slice(&(a as i16).to_be_bytes()); + out.extend_from_slice(&(a as i32).to_be_bytes()); + out.extend_from_slice(&(a as i64).to_be_bytes()); + out.extend_from_slice(&(a as i128).to_be_bytes()); + out.extend_from_slice(&(a as isize).to_be_bytes()); + } + 2 => { + // From every integer type, rounded once to the nearest, ties to even. + let mut wide = [0u8; 16]; + let n = data.len().min(16); + wide[..n].copy_from_slice(&data[..n]); + let v = u128::from_be_bytes(wide); + for x in [ + v as u8 as f64, + v as u16 as f64, + v as u32 as f64, + v as u64 as f64, + v as f64, + v as usize as f64, + v as i8 as f64, + v as i16 as f64, + v as i32 as f64, + v as i64 as f64, + v as i128 as f64, + v as isize as f64, + f64::from(v as u32), + f64::from(v as i16), + f64::from(v & 1 == 1), + (v as u8).into(), + u64::MAX as f64, + i64::MIN as f64, + u128::MAX as f64, + ] { + put_f64(out, x); + } + // And back, where the round trip saturates. + out.extend_from_slice(&((v as u64) as f64 as u64).to_be_bytes()); + out.extend_from_slice(&((v as i128) as f64 as i128).to_be_bytes()); + } + 3 => { + // Rounding, which JS's `Math.round` does half up rather than away from zero. + for x in [ + a.floor(), + a.ceil(), + a.trunc(), + a.round(), + a.round_ties_even(), + a.fract(), + a.abs(), + a.signum(), + a.sqrt(), + a.recip(), + a.to_degrees(), + a.to_radians(), + ] { + put_f64(out, x); + } + } + 4 => { + // The sign bit and the class, and the doubles next to `a`. + let bits = [ + a.is_nan(), + a.is_finite(), + a.is_infinite(), + a.is_normal(), + a.is_subnormal(), + a.is_sign_positive(), + a.is_sign_negative(), + ]; + for bit in bits { + out.push(bit as u8); + } + for x in [a.copysign(b), 1.5f64.copysign(a), a.next_up(), a.next_down()] { + put_f64(out, x); + } + } + 5 => { + // Two operands. std leaves which zero `max` and `min` pick between 0.0 and -0.0. + let zeros = a == 0.0 && b == 0.0; + for x in [a.max(b), a.min(b)] { + put_f64(out, if zeros { 0.0 } else { x }); + } + for x in [a.midpoint(b), a.div_euclid(b), a.rem_euclid(b)] { + put_f64(out, x); + } + out.push(a.total_cmp(&b) as i8 as u8); + out.push(a.partial_cmp(&b).map_or(9, |o| o as i8 as u8)); + } + 6 => { + // Bits and bytes. A NaN's are the engine's, so only a number's round trip. + if !a.is_nan() { + out.extend_from_slice(&a.to_bits().to_be_bytes()); + out.extend_from_slice(&a.to_be_bytes()); + out.extend_from_slice(&a.to_le_bytes()); + out.extend_from_slice(&a.to_ne_bytes()); + let be = a.to_be_bytes(); + let le = a.to_le_bytes(); + for x in [f64::from_be_bytes(be), f64::from_le_bytes(le), f64::from_ne_bytes(le)] { + put_f64(out, x); + } + } + put_f64(out, f64::from_bits(a.to_bits() ^ 1)); + } + 7 => { + // What std leaves to libm, within the ULPs `caller.ts` allows. + let n = (b.to_bits() as i32) % 64; + let (s, c) = a.sin_cos(); + for x in [ + a.sin(), + a.cos(), + a.tan(), + a.asin(), + a.acos(), + a.atan(), + a.atan2(b), + a.sinh(), + a.cosh(), + a.tanh(), + a.asinh(), + a.acosh(), + a.atanh(), + a.exp(), + a.exp2(), + a.exp_m1(), + a.ln(), + a.log(b), + a.log2(), + a.log10(), + a.ln_1p(), + a.powf(b), + a.powi(n), + a.cbrt(), + a.hypot(b), + s, + c, + ] { + put_f64(out, x); + } + } + 8 => { + // `Duration`'s `f64` methods, through std's integer algorithm, with its errors and panics. + let d = Duration::new(rest.first().map_or(0, |&s| u64::from(s)) << 20, 123_456_789); + put_f64(out, d.as_secs_f64()); + put_f64(out, d.div_duration_f64(Duration::from_millis(1500))); + put_f64(out, Duration::ZERO.div_duration_f64(Duration::ZERO)); + put_secs(out, Duration::try_from_secs_f64(a)); + put_secs(out, Duration::try_from_secs_f64(a.abs())); + put_secs(out, Duration::try_from_secs_f64(a.abs() * 1e-9)); + match kind / 10 % 4 { + 0 => put_duration(out, Duration::from_secs_f64(a)), + 1 => put_duration(out, d.mul_f64(a)), + 2 => put_duration(out, d.div_f64(a)), + _ => put_duration(out, Duration::MAX.mul_f64(b)), + } + } + _ => { + // Constants, sums and products, `Default`, places, and fields. + for x in CONSTANTS { + put_f64(out, x); + } + let v = [a, b, 0.5]; + put_f64(out, v.iter().sum()); + put_f64(out, v.into_iter().product()); + put_f64(out, [-0.0f64; 0].iter().sum()); + put_f64(out, [-0.0f64, -0.0].into_iter().sum::()); + put_f64(out, f64::default()); + let mut g = Gauge { + level: a, + peak: Some(b), + }; + raise(&mut g.level, b); + if let Some(peak) = &mut g.peak { + *peak = peak.max(g.level); + } + put_f64(out, g.level); + put_f64(out, g.peak.unwrap_or(-1.0)); + out.push((g == Gauge::default()) as u8); + out.push((g.peak == Some(b)) as u8); + let mut local = a; + raise(&mut local, 1.0); + put_f64(out, local); + } + } +} diff --git a/ts/fixtures/lang/src/harness.rs b/ts/fixtures/lang/src/harness.rs index a463eb2ee..a9a8fe87a 100644 --- a/ts/fixtures/lang/src/harness.rs +++ b/ts/fixtures/lang/src/harness.rs @@ -2,8 +2,8 @@ //! case and the rest is its input. use crate::{ - chars, chunks, collections, combinators, cows, durations, fmt, generics, ints, iters, keys, nested, refs, shapes, - splits, strs, + chars, chunks, collections, combinators, cows, durations, floats, fmt, generics, ints, iters, keys, nested, refs, + shapes, splits, strs, }; /// The native side's entry: [`run`], except that a case starting with 0xff prints a @@ -22,7 +22,7 @@ pub fn run(input: &[u8]) -> Vec { return out; }; let (op, kind) = (*op, *kind); - match op % 55 { + match op % 56 { 0 => ints::u8_op(kind, data, &mut out), 1 => ints::u16_op(kind, data, &mut out), 2 => ints::u32_op(kind, data, &mut out), @@ -87,6 +87,7 @@ pub fn run(input: &[u8]) -> Vec { 51 => refs::refs(data, &mut out), 52 => fmt::counts(data, &mut out), 53 => fmt::rounding(&mut out), + 54 => floats::floats(kind, data, &mut out), _ => iters::skips(kind, data, &mut out), } out diff --git a/ts/fixtures/lang/src/lib.rs b/ts/fixtures/lang/src/lib.rs index 029e20f99..c2bff3f91 100644 --- a/ts/fixtures/lang/src/lib.rs +++ b/ts/fixtures/lang/src/lib.rs @@ -1,9 +1,9 @@ //! The language the driver translates, exercised against native Rust: every integer -//! width's arithmetic and casts, `char`s and the `str` methods over them, `Cow`, -//! `Duration`, control flow, patterns, byte slices split apart, `Copy` aggregates, -//! closures, traits, generic types, formatting, collections, `&mut` to values JS -//! copies, the standard library calls it maps, `Option`'s and `Result`'s combinators -//! from core's MIR, and `Option`s of values that can already be `undefined`. +//! width's arithmetic and casts, `f64`, `char`s and the `str` methods over them, +//! `Cow`, `Duration`, control flow, patterns, byte slices split apart, `Copy` +//! aggregates, closures, traits, generic types, formatting, collections, `&mut` to +//! values JS copies, the standard library calls it maps, `Option`'s and `Result`'s +//! combinators from core's MIR, and `Option`s of values that can already be `undefined`. //! //! `harness::run` is the entry point both sides of the differential test run, and //! `fmt::Fault` an error only `caller.ts` prints. @@ -14,6 +14,7 @@ mod collections; mod combinators; mod cows; mod durations; +mod floats; mod fmt; mod generics; pub mod harness; diff --git a/ts/fixtures/reject/census.md b/ts/fixtures/reject/census.md index 67d64b2cc..2fa76ffb8 100644 --- a/ts/fixtures/reject/census.md +++ b/ts/fixtures/reject/census.md @@ -1,14 +1,14 @@ # Census of `reject-fixture` -- Refusals: 78 -- Function instances: 76 +- Refusals: 80 +- Function instances: 78 - Generic functions no instance reached, lowered over their type parameters: 4 ## By construct | Count | Construct | First | | ---: | --- | --- | -| 11 | unsupported call to `_` | fixtures/reject/src/lib.rs:28:4 | +| 12 | unsupported call to `_` | fixtures/reject/src/lib.rs:28:4 | | 5 | passing `_`, a closure that owns a value that drops, to `_`, which is not supported yet | fixtures/reject/src/lib.rs:61:11 | | 4 | a variant of `_`, which is not supported: either is the same JS string | fixtures/reject/src/lib.rs:398:2 | | 4 | an assignment through a reference to `_` | fixtures/reject/src/lib.rs:115:2 | @@ -31,10 +31,11 @@ | 1 | `_` of `_`, which drops `_`, which is not supported yet | fixtures/reject/src/lib.rs:67:13 | | 1 | `_` through a held type's own, which is not supported yet | fixtures/reject/src/lib.rs:342:2 | | 1 | `_`, which is not supported yet | fixtures/reject/src/lib.rs:318:9 | +| 1 | `_`, which rounds once, where JS would round twice | fixtures/reject/src/lib.rs:472:4 | | 1 | `fold` of `_`, which folds a `flatten` whose source and iterators both drop, which is not supported yet | fixtures/reject/src/lib.rs:235:26 | -| 1 | a `_` cast from `_` to `_` | fixtures/reject/src/lib.rs:110:2 | | 1 | a `zip` of `_` and `_`, which std reads by index, of items that drop, which is not supported yet | fixtures/reject/src/lib.rs:240:30 | | 1 | a `zip` of `_`, whose closures std calls one item further through `next` than through `fold` | fixtures/reject/src/lib.rs:186:51 | +| 1 | a cast from `_` to `_` | fixtures/reject/src/lib.rs:110:2 | | 1 | a coercion to `_` | fixtures/reject/src/lib.rs:365:2 | | 1 | a constant of type `_` | fixtures/reject/src/lib.rs:94:2 | | 1 | a copy of a closure that changes what it captured | fixtures/reject/src/lib.rs:276:14 | @@ -58,7 +59,7 @@ The type or module each refused type or path belongs to. | ---: | --- | --- | | 7 | `std::borrow::Cow` | fixtures/reject/src/lib.rs:398:2 | | 6 | `Option` | fixtures/reject/src/lib.rs:23:15 | -| 6 | `f64` | fixtures/reject/src/lib.rs:7:14 | +| 6 | `f32` | fixtures/reject/src/lib.rs:7:14 | | 5 | `Iterator` | fixtures/reject/src/lib.rs:48:42 | | 5 | `{closure}` | fixtures/reject/src/lib.rs:61:11 | | 3 | `std::iter::Map` | fixtures/reject/src/lib.rs:61:18 | @@ -68,6 +69,7 @@ The type or module each refused type or path belongs to. | 2 | `Meters` | fixtures/reject/src/lib.rs:99:2 | | 2 | `Noisy` | fixtures/reject/src/lib.rs:67:13 | | 2 | `Poll` | fixtures/reject/src/lib.rs:119:20 | +| 2 | `f64` | fixtures/reject/src/lib.rs:472:4 | | 2 | `std::collections::HashMap` | fixtures/reject/src/lib.rs:67:13 | | 2 | `std::iter::Iterator` | fixtures/reject/src/lib.rs:266:16 | | 2 | `std::iter::Take` | fixtures/reject/src/lib.rs:390:79 | @@ -80,7 +82,6 @@ The type or module each refused type or path belongs to. | 1 | `Entry` | fixtures/reject/src/lib.rs:381:13 | | 1 | `FusedIterator` | fixtures/reject/src/lib.rs:227:1 | | 1 | `Hasher` | fixtures/reject/src/lib.rs:318:9 | -| 1 | `IntToFloat` | fixtures/reject/src/lib.rs:110:2 | | 1 | `Maybe` | fixtures/reject/src/lib.rs:38:2 | | 1 | `T \| undefined` | fixtures/reject/src/lib.rs:38:2 | | 1 | `[u16; 1]` | fixtures/reject/src/lib.rs:259:22 | @@ -111,7 +112,7 @@ The type or module each refused type or path belongs to. | Count | Construct | Type or path | First | | ---: | --- | --- | --- | | 4 | a variant of `_`, which is not supported: either is the same JS string | `std::borrow::Cow` | fixtures/reject/src/lib.rs:398:2 | -| 3 | unsupported type `_` | `f64` | fixtures/reject/src/lib.rs:7:14 | +| 3 | unsupported type `_` | `f32` | fixtures/reject/src/lib.rs:7:14 | | 2 | `_` is not supported: it would be pending two ways with one value | `Poll` | fixtures/reject/src/lib.rs:119:20 | | 2 | `_` is not supported: its `None` and `Some` would both be `rt.None` | `Option` | fixtures/reject/src/lib.rs:23:15 | | 2 | an assignment through a reference to `_` | `std::pin::Pin` | fixtures/reject/src/lib.rs:130:2 | @@ -131,23 +132,24 @@ The type or module each refused type or path belongs to. | 1 | `_` with a mutable reference to a value JS copies, which its mapping would copy | `std::iter::IntoIterator::into_iter` | fixtures/reject/src/lib.rs:17:2 | | 1 | `_` with a mutable reference to a value JS copies, which its mapping would copy | `std::slice::last_mut` | fixtures/reject/src/lib.rs:424:21 | | 1 | `_`, which is not supported yet | `Hasher::write` | fixtures/reject/src/lib.rs:318:9 | +| 1 | `_`, which rounds once, where JS would round twice | `f64::mul_add` | fixtures/reject/src/lib.rs:472:4 | | 1 | `fold` of `_`, which folds a `flatten` whose source and iterators both drop, which is not supported yet | `std::iter::Flatten` | fixtures/reject/src/lib.rs:235:26 | -| 1 | a `_` cast from `_` to `_` | `IntToFloat`, `u64`, `f64` | fixtures/reject/src/lib.rs:110:2 | | 1 | a `zip` of `_` and `_`, which std reads by index, of items that drop, which is not supported yet | `std::iter::Map`, `std::ops::Range` | fixtures/reject/src/lib.rs:240:30 | | 1 | a `zip` of `_`, whose closures std calls one item further through `next` than through `fold` | `std::iter::Chain` | fixtures/reject/src/lib.rs:186:51 | +| 1 | a cast from `_` to `_` | `u64`, `f32` | fixtures/reject/src/lib.rs:110:2 | | 1 | a coercion to `_` | `std::sync::Arc` | fixtures/reject/src/lib.rs:365:2 | | 1 | a constant of type `_` | `&u8` | fixtures/reject/src/lib.rs:94:2 | | 1 | a field of `_` | `std::ops::RangeFrom` | fixtures/reject/src/lib.rs:282:48 | | 1 | a field of `_` | `std::ops::RangeTo` | fixtures/reject/src/lib.rs:282:7 | | 1 | a mutable borrow of a subslice of `_`, which JS would copy, having no views | `u32` | fixtures/reject/src/lib.rs:245:13 | -| 1 | a scalar constant of type `_` | `f64` | fixtures/reject/src/lib.rs:8:2 | +| 1 | a scalar constant of type `_` | `f32` | fixtures/reject/src/lib.rs:8:2 | | 1 | a zero-sized constant of type `_` | `{async closure}` | fixtures/reject/src/lib.rs:76:2 | | 1 | an assignment through a reference to `_` | `std::sync::Arc` | fixtures/reject/src/lib.rs:115:2 | | 1 | an assignment through a reference to `_` | `std::sync::MutexGuard` | fixtures/reject/src/lib.rs:135:2 | | 1 | an iterator of the crate implementing `_`, which is not supported yet | `DoubleEndedIterator` | fixtures/reject/src/lib.rs:173:1 | | 1 | an iterator of the crate implementing `_`, which is not supported yet | `FusedIterator` | fixtures/reject/src/lib.rs:227:1 | | 1 | an iterator that owns `_`, which drops, which is not supported yet | `Noisy` | fixtures/reject/src/lib.rs:392:13 | -| 1 | arithmetic on `_` | `f64` | fixtures/reject/src/lib.rs:8:2 | +| 1 | arithmetic on `_` | `f32` | fixtures/reject/src/lib.rs:8:2 | | 1 | collecting `_`, whose `None` is `rt.None`, which is not supported yet | `Option` | fixtures/reject/src/lib.rs:43:43 | | 1 | comparing `_` as a key, which is not supported yet | `std::vec::Vec` | fixtures/reject/src/lib.rs:292:52 | | 1 | constructing `_` | `std::borrow::Cow` | fixtures/reject/src/lib.rs:416:53 | @@ -163,6 +165,7 @@ The type or module each refused type or path belongs to. | 1 | passing `_`, a closure that owns a value that drops, to `_`, which is not supported yet | `{closure}`, `from_fn::from_fn` | fixtures/reject/src/lib.rs:391:13 | | 1 | passing `_`, a closure that owns a value that drops, to `_`, which is not supported yet | `{closure}`, `successors::successors` | fixtures/reject/src/lib.rs:390:15 | | 1 | replacing the value behind a reference to `_` | `[u16; 1]` | fixtures/reject/src/lib.rs:259:22 | +| 1 | unsupported call to `_` | `::fmt` | fixtures/reject/src/lib.rs:477:2 | | 1 | unsupported call to `_` | `::deref` | fixtures/reject/src/lib.rs:410:2 | | 1 | unsupported call to `_` | `::eq` | fixtures/reject/src/lib.rs:448:2 | | 1 | unsupported call to `_` | `::cycle` | fixtures/reject/src/lib.rs:140:11 | @@ -176,7 +179,7 @@ The type or module each refused type or path belongs to. ## Spans -
unsupported call to `_` (11) +
unsupported call to `_` (12) - fixtures/reject/src/lib.rs:28:4: unsupported call to `std::slice::::sort` - fixtures/reject/src/lib.rs:60:29: unsupported call to `std::mem::size_of_val::` @@ -189,6 +192,7 @@ The type or module each refused type or path belongs to. - fixtures/reject/src/lib.rs:448:2: unsupported call to ` as std::cmp::PartialEq>::eq` - fixtures/reject/src/lib.rs:467:17: unsupported call to ` as std::iter::Iterator>::sum::` - fixtures/reject/src/lib.rs:467:38: unsupported call to ` as std::iter::Iterator>::sum::` +- fixtures/reject/src/lib.rs:477:2: unsupported call to `::fmt`
@@ -222,10 +226,10 @@ The type or module each refused type or path belongs to.
unsupported type `_` (4) -- fixtures/reject/src/lib.rs:7:14: unsupported type `f64` -- fixtures/reject/src/lib.rs:7:25: unsupported type `f64` +- fixtures/reject/src/lib.rs:7:14: unsupported type `f32` +- fixtures/reject/src/lib.rs:7:25: unsupported type `f32` - fixtures/reject/src/lib.rs:11:16: unsupported type `dyn std::ops::Fn(u8) -> u8` -- fixtures/reject/src/lib.rs:108:51: unsupported type `f64` +- fixtures/reject/src/lib.rs:108:51: unsupported type `f32`
@@ -351,15 +355,15 @@ The type or module each refused type or path belongs to.
-
`fold` of `_`, which folds a `flatten` whose source and iterators both drop, which is not supported yet (1) +
`_`, which rounds once, where JS would round twice (1) -- fixtures/reject/src/lib.rs:235:26: `fold` of `std::iter::Flatten>>`, which folds a `flatten` whose source and iterators both drop, which is not supported yet +- fixtures/reject/src/lib.rs:472:4: `f64::mul_add`, which rounds once, where JS would round twice
-
a `_` cast from `_` to `_` (1) +
`fold` of `_`, which folds a `flatten` whose source and iterators both drop, which is not supported yet (1) -- fixtures/reject/src/lib.rs:110:2: a `IntToFloat` cast from `u64` to `f64` +- fixtures/reject/src/lib.rs:235:26: `fold` of `std::iter::Flatten>>`, which folds a `flatten` whose source and iterators both drop, which is not supported yet
@@ -375,6 +379,12 @@ The type or module each refused type or path belongs to.
+
a cast from `_` to `_` (1) + +- fixtures/reject/src/lib.rs:110:2: a cast from `u64` to `f32` + +
+
a coercion to `_` (1) - fixtures/reject/src/lib.rs:365:2: a coercion to `std::sync::Arc<[u8]>` @@ -413,7 +423,7 @@ The type or module each refused type or path belongs to.
a scalar constant of type `_` (1) -- fixtures/reject/src/lib.rs:8:2: a scalar constant of type `f64` +- fixtures/reject/src/lib.rs:8:2: a scalar constant of type `f32`
@@ -431,7 +441,7 @@ The type or module each refused type or path belongs to.
arithmetic on `_` (1) -- fixtures/reject/src/lib.rs:8:2: arithmetic on `f64` +- fixtures/reject/src/lib.rs:8:2: arithmetic on `f32`
diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index b7335e933..d7af10d9a 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -1,10 +1,10 @@ a root must not be generic at fixtures/reject/src/lib.rs:108:1 no item `missing` in the crate a printable type must implement `Display` or `Debug` at fixtures/reject/src/lib.rs:106:1 -unsupported type `f64` at fixtures/reject/src/lib.rs:7:14 -a scalar constant of type `f64` at fixtures/reject/src/lib.rs:8:2 -arithmetic on `f64` at fixtures/reject/src/lib.rs:8:2 -unsupported type `f64` at fixtures/reject/src/lib.rs:7:25 +unsupported type `f32` at fixtures/reject/src/lib.rs:7:14 +a scalar constant of type `f32` at fixtures/reject/src/lib.rs:8:2 +arithmetic on `f32` at fixtures/reject/src/lib.rs:8:2 +unsupported type `f32` at fixtures/reject/src/lib.rs:7:25 unsupported type `dyn std::ops::Fn(u8) -> u8` at fixtures/reject/src/lib.rs:11:16 `std::iter::IntoIterator::into_iter` with a mutable reference to a value JS copies, which its mapping would copy at fixtures/reject/src/lib.rs:17:11 `Option>>` is not supported: its `None` and `Some` would both be `rt.None` at fixtures/reject/src/lib.rs:24:2 @@ -70,6 +70,8 @@ unsupported call to ` as std::iter::Iterator>::sum::::and_modify` with a mutable reference to a value JS copies, which its mapping would copy at fixtures/reject/src/lib.rs:431:15 a mutable reference into a `Result`'s payload, passed on or kept at fixtures/reject/src/lib.rs:441:12 unsupported call to ` as std::cmp::PartialEq>::eq` at fixtures/reject/src/lib.rs:448:2 +`f64::mul_add`, which rounds once, where JS would round twice at fixtures/reject/src/lib.rs:472:4 +unsupported call to `::fmt` at fixtures/reject/src/lib.rs:477:11 overriding `Iterator::fold`, which std's adaptors call in place of `next` or beside it at fixtures/reject/src/lib.rs:153:2 an iterator of the crate implementing `DoubleEndedIterator`, which is not supported yet at fixtures/reject/src/lib.rs:173:1 overriding `Iterator::size_hint`, which std's adaptors call in place of `next` or beside it at fixtures/reject/src/lib.rs:207:2 diff --git a/ts/fixtures/reject/src/lib.rs b/ts/fixtures/reject/src/lib.rs index 85de41a3d..55046f771 100644 --- a/ts/fixtures/reject/src/lib.rs +++ b/ts/fixtures/reject/src/lib.rs @@ -4,7 +4,7 @@ #![allow(dead_code, reason = "each item is a translation root, not called from Rust")] #![feature(cow_is_borrowed)] -pub fn float(a: f64) -> f64 { +pub fn float(a: f32) -> f32 { a * 2.0 } @@ -105,9 +105,9 @@ pub fn replaced(a: &mut Meters) -> Meters { pub struct Meters(pub u64); -pub fn generic>(t: &mut T) -> f64 { +pub fn generic>(t: &mut T) -> f32 { // A census lowers this over `T`: what hinges on `T` waits for an instance, the float does not. - (*t).into() as f64 + (*t).into() as f32 } /// Every clone of an `Arc` is one JS object: replacing the payload would reach them all. @@ -466,3 +466,13 @@ impl std::iter::Sum for u64 { pub fn parts_summed(a: Vec, b: Vec) -> (std::time::Duration, u64) { (a.into_iter().sum(), b.into_iter().sum()) } + +/// One rounding, which JS has no cheap form of. +pub fn fma(a: f64) -> f64 { + a.mul_add(2.0, 1.0) +} + +/// Float formatting is not translated yet. +pub fn float_printed(a: f64) -> String { + format!("{a}") +} diff --git a/ts/fixtures/reject/ts.toml b/ts/fixtures/reject/ts.toml index f6fe558a6..47a2c52f7 100644 --- a/ts/fixtures/reject/ts.toml +++ b/ts/fixtures/reject/ts.toml @@ -56,6 +56,8 @@ roots = [ "modified", "ok_passed", "places_equal", + "fma", + "float_printed", "missing", ] printable = ["Meters"] diff --git a/ts/runtime/rt.ts b/ts/runtime/rt.ts index 2743d7ce3..3cd4f11ce 100644 --- a/ts/runtime/rt.ts +++ b/ts/runtime/rt.ts @@ -9,6 +9,7 @@ import { type char, type TryFromIntError, type u32, u8Debug, usizeDebug, usizeDi import type { Pending } from "./rt/task.ts"; export * from "./rt/collections.ts"; +export * from "./rt/float.ts"; export * from "./rt/int.ts"; export * from "./rt/iter.ts"; export * from "./rt/str.ts"; diff --git a/ts/runtime/rt/float.ts b/ts/runtime/rt/float.ts new file mode 100644 index 000000000..4845dc33f --- /dev/null +++ b/ts/runtime/rt/float.ts @@ -0,0 +1,211 @@ +// `f64`, for rust2-ts output: a number, which is the same IEEE 754 double, so its +// operators are JS's. These are its methods where JS differs from std or has none, each +// its own function, `f64Round`, so a bundler drops every one translated code never +// calls. `rt.ts` re-exports all of it. +// +// JS doesn't specify which NaN an operation makes, so a NaN's payload and sign are the +// engine's: `to_bits` reads what it keeps. + +import type { Ordering } from "./collections.ts"; +import { type f64, type u64, u64FromHalves } from "./int.ts"; + +/** Eight bytes that hold a double while its bits are read or written. */ +const view = new DataView(new ArrayBuffer(8)); +const TWO32 = 4294967296; +const TWO52 = 4503599627370496; +/** `f64::MIN_POSITIVE`, the least normal double. */ +const MIN_POSITIVE = 2.2250738585072014e-308; + +/** `f64::round`: to the nearest integer, half away from zero. */ +export function f64Round(x: f64): f64 { + const t = Math.trunc(x); + // `x - t` is exact, and NaN for an infinity, which `t` already is. + return Math.abs(x - t) >= 0.5 ? t + Math.sign(x) : t; +} + +/** `f64::round_ties_even`: to the nearest integer, half to even. */ +export function f64RoundTiesEven(x: f64): f64 { + const a = Math.abs(x); + // Every double from 2^52 up is an integer; below it, adding 2^52 rounds to one, ties to even. + if (!(a < TWO52)) return x; + const r = a + TWO52 - TWO52; + return x < 0 ? -r : x === 0 ? x : r; +} + +/** `f64::fract`: `x - x.trunc()`. */ +export const f64Fract = (x: f64): f64 => x - Math.trunc(x); + +/** `f64::is_sign_negative`: whether the sign bit is set, as it is for -0.0. */ +export function f64IsSignNegative(x: f64): boolean { + if (x === x) return x < 0 || 1 / x < 0; + view.setFloat64(0, x); + return view.getUint8(0) >= 0x80; +} + +/** `f64::is_sign_positive`: whether the sign bit is clear. */ +export const f64IsSignPositive = (x: f64): boolean => !f64IsSignNegative(x); + +/** `f64::signum`: 1.0 or -1.0 by the sign bit, so -1.0 for -0.0, and NaN for NaN. */ +export const f64Signum = (x: f64): f64 => (x !== x ? NaN : f64IsSignNegative(x) ? -1 : 1); + +/** `f64::copysign`: `x`'s magnitude with `y`'s sign bit. */ +export function f64Copysign(x: f64, y: f64): f64 { + const negative = f64IsSignNegative(y); + if (x === x) return negative ? -Math.abs(x) : Math.abs(x); + view.setFloat64(0, x); + const top = view.getUint8(0) & 0x7f; + view.setUint8(0, negative ? top | 0x80 : top); + return view.getFloat64(0); +} + +/** `f64::is_infinite`. */ +export const f64IsInfinite = (x: f64): boolean => Math.abs(x) === Infinity; + +/** `f64::is_normal`: neither zero, subnormal, infinite, nor NaN. */ +export function f64IsNormal(x: f64): boolean { + const a = Math.abs(x); + return a >= MIN_POSITIVE && a < Infinity; +} + +/** `f64::is_subnormal`. */ +export function f64IsSubnormal(x: f64): boolean { + const a = Math.abs(x); + return a > 0 && a < MIN_POSITIVE; +} + +/** `x`'s bits moved by one toward +infinity if `up`, from a finite or infinite non-NaN. */ +function step(x: number, up: boolean): number { + if (x === 0) return up ? 5e-324 : -5e-324; + view.setFloat64(0, x); + let hi = view.getUint32(0); + let lo = view.getUint32(4); + // The magnitude grows if it moves away from zero. + if (up === x > 0) { + lo = (lo + 1) >>> 0; + if (lo === 0) hi++; + } else { + if (lo === 0) hi--; + lo = (lo - 1) >>> 0; + } + view.setUint32(0, hi); + view.setUint32(4, lo); + return view.getFloat64(0); +} + +/** `f64::next_up`: the least double greater than `x`. */ +export const f64NextUp = (x: f64): f64 => (x !== x || x === Infinity ? x : step(x, true)); + +/** `f64::next_down`: the greatest double less than `x`. */ +export const f64NextDown = (x: f64): f64 => (x !== x || x === -Infinity ? x : step(x, false)); + +/** `f64::max`: the greater, ignoring a NaN. */ +export const f64Max = (a: f64, b: f64): f64 => (a !== a ? b : b !== b ? a : Math.max(a, b)); + +/** `f64::min`: the lesser, ignoring a NaN. */ +export const f64Min = (a: f64, b: f64): f64 => (a !== a ? b : b !== b ? a : Math.min(a, b)); + +/** `f64::midpoint`: halved before the sum where the sum could overflow. */ +export function f64Midpoint(a: f64, b: f64): f64 { + const hi = Number.MAX_VALUE * 0.5; + return Math.abs(a) <= hi && Math.abs(b) <= hi ? (a + b) * 0.5 : a * 0.5 + b * 0.5; +} + +/** `f64::to_bits`. */ +export function f64ToBits(x: f64): u64 { + view.setFloat64(0, x); + return u64FromHalves(view.getUint32(0), view.getUint32(4)); +} + +/** `f64::from_bits`. */ +export function f64FromBits(v: u64): f64 { + view.setUint32(0, typeof v === "number" ? Math.floor(v / TWO32) : v.hi); + view.setUint32(4, typeof v === "number" ? v >>> 0 : v.lo); + return view.getFloat64(0); +} + +function toBytes(x: number, little: boolean): Uint8Array { + const out = new Uint8Array(8); + new DataView(out.buffer).setFloat64(0, x, little); + return out; +} + +function fromBytes(bytes: Uint8Array, little: boolean): number { + return new DataView(bytes.buffer, bytes.byteOffset, 8).getFloat64(0, little); +} + +/** `f64::to_be_bytes`. */ +export const f64ToBeBytes = (x: f64): Uint8Array => toBytes(x, false); + +/** `f64::to_le_bytes`, and `to_ne_bytes`, as wasm32 is little-endian. */ +export const f64ToLeBytes = (x: f64): Uint8Array => toBytes(x, true); + +/** `f64::from_be_bytes`. */ +export const f64FromBeBytes = (bytes: Uint8Array): f64 => fromBytes(bytes, false); + +/** `f64::from_le_bytes`, and `from_ne_bytes`, as wasm32 is little-endian. */ +export const f64FromLeBytes = (bytes: Uint8Array): f64 => fromBytes(bytes, true); + +/** `f64::total_cmp`: IEEE 754's total order, by the bits, with a negative's flipped. */ +export function f64TotalCmp(a: f64, b: f64): Ordering { + view.setFloat64(0, a); + let ah = view.getInt32(0); + let al = view.getUint32(4); + if (ah < 0) { + ah ^= 0x7fffffff; + al = ~al >>> 0; + } + view.setFloat64(0, b); + let bh = view.getInt32(0); + let bl = view.getUint32(4); + if (bh < 0) { + bh ^= 0x7fffffff; + bl = ~bl >>> 0; + } + return ah < bh ? -1 : ah > bh ? 1 : al < bl ? -1 : al > bl ? 1 : 0; +} + +/** `f64`'s `PartialOrd::partial_cmp`: `undefined`, which is `None`, if either is NaN. */ +export const f64PartialCmp = (a: f64, b: f64): Ordering | undefined => (a < b ? -1 : a > b ? 1 : a === b ? 0 : undefined); + +/** `f64::powi`, multiplied by squaring as compiler-builtins' `__powidf2` is. */ +export function f64Powi(x: f64, n: number): f64 { + let a = x; + let pow = Math.abs(n); + let mul = 1; + for (;;) { + if (pow & 1) mul *= a; + // `>>>` keeps 2^31, the magnitude of `i32::MIN`, unsigned. + pow >>>= 1; + if (pow === 0) break; + a *= a; + } + return n < 0 ? 1 / mul : mul; +} + +/** `f64::powf`, which is libm's `pow`: 1 for a base of 1, and for -1 to an infinite power, where `Math.pow` is NaN. */ +export const f64Powf = (x: f64, y: f64): f64 => (x === 1 || (x === -1 && Math.abs(y) === Infinity) ? 1 : Math.pow(x, y)); + +/** `f64::sin_cos`. */ +export const f64SinCos = (x: f64): [f64, f64] => [Math.sin(x), Math.cos(x)]; + +/** `f64::atanh`, as std computes it rather than libm. */ +export const f64Atanh = (x: f64): f64 => 0.5 * Math.log1p((2 * x) / (1 - x)); + +/** `f64::div_euclid`. */ +export function f64DivEuclid(x: f64, rhs: f64): f64 { + const q = Math.trunc(x / rhs); + if (x % rhs < 0) return rhs > 0 ? q - 1 : q + 1; + return q; +} + +/** `f64::rem_euclid`. */ +export function f64RemEuclid(x: f64, rhs: f64): f64 { + const r = x % rhs; + return r < 0 ? r + Math.abs(rhs) : r; +} + +/** `f64`'s `+`, which `Iterator::sum` folds with. */ +export const f64Add = (a: f64, b: f64): f64 => a + b; + +/** `f64`'s `*`, which `Iterator::product` folds with. */ +export const f64Mul = (a: f64, b: f64): f64 => a * b; diff --git a/ts/runtime/rt/int.ts b/ts/runtime/rt/int.ts index 176f7f1cc..13a620191 100644 --- a/ts/runtime/rt/int.ts +++ b/ts/runtime/rt/int.ts @@ -44,6 +44,8 @@ export type i16 = Num<"i16">; export type i32 = Num<"i32">; /** 32 bits, as on wasm32. */ export type isize = Num<"isize">; +/** Rust's `f64`: a number, which is the same IEEE 754 double. */ +export type f64 = Num<"f64">; /** Rust's `char`: a Unicode scalar value, which is at most 0x10ffff and no surrogate. */ export type char = Num<"char">; /** @@ -1606,3 +1608,63 @@ export const i128Max = (a: i128, b: i128): i128 => (lt(b, a) ? a : b); export const i128Min = (a: i128, b: i128): i128 => (lt(b, a) ? b : a); /** `i128`'s `Step`, which ranges step by: `n` moved by `by`, and whether that overflowed. */ export const i128Step: Step = (n, by) => stepped(sum128(n, Math.abs(by), by < 0 ? -1 : 1, true)) as [i128, boolean]; + +// Casts between integers and `f64`. An integer of at most 32 bits is already its `f64`; +// a wider one rounds to the nearest, ties to even, once. An `f64` to an integer +// truncates toward zero, saturates at the type's bounds, and sends NaN to 0. + +/** `x as T` into a type of at most 32 bits, whose bounds are int32s or uint32s. */ +function narrowFromF64(x: number, min: number, max: number): number { + // NaN fails both comparisons; `+ 0` turns a truncated -0 into 0. + return x >= min ? (x <= max ? Math.trunc(x) + 0 : max) : x < min ? min : 0; +} + +/** `x as T` into a 64- or 128-bit type. */ +function wideFromF64(x: number, bits: number, signed: boolean): Wide { + const t = Math.trunc(x) + 0; + if (fits(t, signed)) return t; + if (t !== t) return 0; + // Every integer past 2^53 is exact in a double, and so is each type's bound. + const bound = 2 ** (signed ? bits - 1 : bits); + const min = signed ? -bound : 0; + if (t >= bound) return of((1n << BigInt(signed ? bits - 1 : bits)) - 1n, bits, signed); + if (t < min) return of(BigInt(min), bits, signed); + if (bits === 64) { + const hi = Math.floor(t / TWO32); + return join(hi, t - hi * TWO32); + } + return of(BigInt(t), bits, signed); +} + +/** `x as u8`. */ +export const u8FromF64 = (x: f64): u8 => narrowFromF64(x, 0, 0xff); +/** `x as u16`. */ +export const u16FromF64 = (x: f64): u16 => narrowFromF64(x, 0, 0xffff); +/** `x as u32`. */ +export const u32FromF64 = (x: f64): u32 => narrowFromF64(x, 0, 0xffffffff); +/** `x as usize`. */ +export const usizeFromF64: (x: f64) => usize = u32FromF64 as never; +/** `x as i8`. */ +export const i8FromF64 = (x: f64): i8 => narrowFromF64(x, -0x80, 0x7f); +/** `x as i16`. */ +export const i16FromF64 = (x: f64): i16 => narrowFromF64(x, -0x8000, 0x7fff); +/** `x as i32`. */ +export const i32FromF64 = (x: f64): i32 => narrowFromF64(x, -0x80000000, 0x7fffffff); +/** `x as isize`. */ +export const isizeFromF64: (x: f64) => isize = i32FromF64 as never; +/** `x as u64`. */ +export const u64FromF64 = (x: f64): u64 => wideFromF64(x, 64, false) as u64; +/** `x as i64`. */ +export const i64FromF64 = (x: f64): i64 => wideFromF64(x, 64, true) as i64; +/** `x as u128`. */ +export const u128FromF64 = (x: f64): u128 => wideFromF64(x, 128, false) as u128; +/** `x as i128`. */ +export const i128FromF64 = (x: f64): i128 => wideFromF64(x, 128, true) as i128; +/** `a as f64`: limbs' one sum is exact before it rounds. */ +export const u64ToF64 = (a: u64): f64 => (typeof a === "number" ? (a as number) : a.hi * TWO32 + a.lo); +/** `a as f64`: limbs' one sum is exact before it rounds. */ +export const i64ToF64 = (a: i64): f64 => (typeof a === "number" ? (a as number) : a.hi * TWO32 + a.lo); +/** `a as f64`: a bigint converts to the nearest double. */ +export const u128ToF64 = (a: u128): f64 => (typeof a === "number" ? (a as number) : Number(exact(a))); +/** `a as f64`: a bigint converts to the nearest double. */ +export const i128ToF64 = (a: i128): f64 => (typeof a === "number" ? (a as number) : Number(exact(a))); diff --git a/ts/runtime/rt/time.ts b/ts/runtime/rt/time.ts index a82b4eeef..c8510c1ed 100644 --- a/ts/runtime/rt/time.ts +++ b/ts/runtime/rt/time.ts @@ -5,9 +5,9 @@ // A `Duration` never changes once made, so values share it. Its arithmetic is std's: a // sum past `u64::MAX` seconds is `None`, saturates, or panics with std's message. -import { type FmtResult, type Formatter, fmtPad, panic } from "../rt.ts"; +import { Err, type FmtResult, type Formatter, fmtDebugStructFields, fmtPad, Ok, panic, type Result } from "../rt.ts"; import { cmp, type Hasher, hashInt, type Ordering } from "./collections.ts"; -import { type u32, type u64, type u128, u64CheckedAdd, u64CheckedMul, u64CheckedSub, u64Div, u64Eq, u64Rem, u64TryFrom, u128Add, u128Cast, u128Div, u128Mul, u128Rem } from "./int.ts"; +import { type f64, type u32, type u64, type u128, u64CheckedAdd, u64CheckedMul, u64CheckedSub, u64Div, u64Eq, u64Of, u64Rem, u64ToF64, u64TryFrom, u128Add, u128Cast, u128Div, u128Mul, u128Rem } from "./int.ts"; import { type Iter, iterFold } from "./iter.ts"; /** `core::time::Duration`: whole seconds, and the nanoseconds past them, below 10^9. */ @@ -194,6 +194,90 @@ export function durationAbsDiff(a: Duration, b: Duration): Duration { return durationCheckedSub(a, b) ?? (durationCheckedSub(b, a) as Duration); } +/** `core::time::TryFromFloatSecsError`: why seconds made no `Duration`. */ +export type TryFromFloatSecsError = { readonly kind: "Negative" | "OverflowOrNan" }; + +/** `Duration::as_secs_f64`. */ +export function durationAsSecsF64(d: Duration): f64 { + return u64ToF64(d.secs) + d.nanos / NANOS_PER_SEC; +} + +/** `Duration::div_duration_f64`: their nanoseconds, each rounded to an `f64`, divided. */ +export function durationDivDurationF64(a: Duration, b: Duration): f64 { + return (u64ToF64(a.secs) * NANOS_PER_SEC + a.nanos) / (u64ToF64(b.secs) * NANOS_PER_SEC + b.nanos); +} + +/** `n`, the nanoseconds past a second times 2^`shift`, rounded to the nearest, half to even. */ +function roundNanos(n: bigint, shift: bigint): number { + const nanos = Number(n >> shift); + const rem = n & ((1n << shift) - 1n); + const half = 1n << (shift - 1n); + return rem > half || (rem === half && (nanos & 1) === 1) ? nanos + 1 : nanos; +} + +/** + * `Duration::try_from_secs_f64`: `secs` to the nearest nanosecond, half to even, by + * std's integer algorithm on its bits. + */ +export function durationTryFromSecsF64(secs: f64): Result { + if (secs < 0) return Err({ kind: "Negative" }); + const bits = f64Bits(secs); + const mantMask = (1n << 52n) - 1n; + const mant = (bits & mantMask) | (mantMask + 1n); + const exp = Number((bits >> 52n) & 0x7ffn) - 1023; + let whole: bigint; + let nanos: number; + if (exp < -31) { + // Less than a nanosecond, which can't round up to one. + [whole, nanos] = [0n, 0]; + } else if (exp < 0) { + nanos = roundNanos(BigInt(NANOS_PER_SEC) * (mant << BigInt(44 + exp)), 96n); + [whole, nanos] = nanos === NANOS_PER_SEC ? [1n, 0] : [0n, nanos]; + } else if (exp < 52) { + whole = mant >> BigInt(52 - exp); + nanos = roundNanos(BigInt(NANOS_PER_SEC) * ((mant << BigInt(exp)) & mantMask), 52n); + if (nanos === NANOS_PER_SEC) [whole, nanos] = [whole + 1n, 0]; + } else if (exp < 64) { + [whole, nanos] = [mant << BigInt(exp - 52), 0]; + } else { + return Err({ kind: "OverflowOrNan" }); + } + return Ok({ secs: u64Of(whole), nanos }); +} + +/** Eight bytes that hold a double while its bits are read. */ +const view = new DataView(new ArrayBuffer(8)); + +/** A double's bits. */ +function f64Bits(x: number): bigint { + view.setFloat64(0, x); + return view.getBigUint64(0); +} + +/** `Duration::from_secs_f64`, panicking where `try_from_secs_f64` errs. */ +export function durationFromSecsF64(secs: f64): Duration { + const r = durationTryFromSecsF64(secs); + return r.ok ? r.value : panic(tryFromFloatSecsErrorText(r.error)); +} + +/** `Duration::mul_f64`: through seconds as an `f64`, as std computes it. */ +export const durationMulF64 = (d: Duration, rhs: f64): Duration => durationFromSecsF64(rhs * durationAsSecsF64(d)); + +/** `Duration::div_f64`: through seconds as an `f64`, as std computes it. */ +export const durationDivF64 = (d: Duration, rhs: f64): Duration => durationFromSecsF64(durationAsSecsF64(d) / rhs); + +function tryFromFloatSecsErrorText(e: TryFromFloatSecsError): string { + const why = e.kind === "Negative" ? "value is negative" : "value is either too big or NaN"; + return `cannot convert float seconds to Duration: ${why}`; +} + +/** `Display` of `TryFromFloatSecsError`. */ +export const fmtTryFromFloatSecsErrorDisplay = (e: TryFromFloatSecsError, f: Formatter): FmtResult => fmtPad(f, tryFromFloatSecsErrorText(e)); + +/** `Debug` of `TryFromFloatSecsError`. */ +export const fmtTryFromFloatSecsErrorDebug = (e: TryFromFloatSecsError, f: Formatter): FmtResult => + fmtDebugStructFields(f, "TryFromFloatSecsError", ["kind"], [(f) => f.out.writeStr(e.kind)]); + /** `a + b`, panicking past `u64::MAX` seconds. */ export function durationAdd(a: Duration, b: Duration): Duration { return durationCheckedAdd(a, b) ?? panic("adding durations should not exceed u64::MAX"); diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 64f078257..aad874f8b 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -926,7 +926,8 @@ fn std_key<'tcx>(b: &Body<'_, '_, 'tcx>, ty: Ty<'tcx>) -> bool { std_key(b, t) || b.t.deferred(t) || matches!(t.kind(), ty::Adt(adt, _) if b.t.own(adt.did())) }; match ty.kind() { - ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Str => true, + // A float is no key, but its `PartialEq` and `PartialOrd` are the runtime's. + ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Float(_) | ty::Str => true, ty::Tuple(tys) => !tys.is_empty() && tys.iter().all(held), ty::Adt(..) if let Some(inner) = option_of(b, ty).or_else(|| arc_of(b, ty)) => held(inner), ty::Adt(..) => library::string(b.t.tcx, ty) || matches!(b.t.std_of(ty), Some(Std::Ordering | Std::Duration)), @@ -951,6 +952,8 @@ fn cmp_fn<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, partial: bool, span: S let ty = peel(b, ty); match ty.kind() { ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) => Expr::Rt("cmp".into()), + // `None` for a NaN: only `PartialOrd`, since a float is no `Ord`. + ty::Float(_) if partial => Expr::Rt("f64PartialCmp".into()), ty::Str => Expr::Rt("cmpStr".into()), ty::Adt(..) if library::string(tcx, ty) => Expr::Rt("cmpStr".into()), ty::Adt(..) if b.t.std_of(ty) == Some(Std::Ordering) => Expr::Rt("cmp".into()), @@ -989,7 +992,7 @@ fn eq_fn<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Expr { { int.func("eq") } - ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Str => Expr::Rt("same".into()), + ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Float(_) | ty::Str => Expr::Rt("same".into()), ty::Adt(..) if library::string(tcx, ty) || b.t.std_of(ty) == Some(Std::Ordering) => Expr::Rt("same".into()), ty::Adt(..) if b.t.std_of(ty) == Some(Std::Duration) => Expr::Rt("durationEq".into()), ty::Tuple(tys) if !tys.is_empty() => { diff --git a/ts/src/lower/consts.rs b/ts/src/lower/consts.rs index ca357511c..e3e8aecdb 100644 --- a/ts/src/lower/consts.rs +++ b/ts/src/lower/consts.rs @@ -41,6 +41,10 @@ impl<'b, 'a, 'tcx> Body<'b, 'a, 'tcx> { }; int.lit(value) } + _ if super::float::is(ty) => match leaf(valtree) { + Some(scalar) => super::float::lit(f64::from_bits(scalar.to_u64())), + None => self.err(span, "a float constant without a value"), + }, ty::Char => match leaf(valtree) { Some(scalar) => Expr::lit(scalar.to_u32().to_string()), None => self.err(span, "a char constant without a value"), diff --git a/ts/src/lower/float.rs b/ts/src/lower/float.rs new file mode 100644 index 000000000..3c4cdea4f --- /dev/null +++ b/ts/src/lower/float.rs @@ -0,0 +1,115 @@ +//! `f64`: a JS number, which is the same IEEE 754 double, so its arithmetic and +//! comparisons are JS's operators, rounded as Rust's are. Where JS differs from std, as +//! in rounding half away from zero, `max` and `min` around NaN, and casts that saturate, +//! the runtime has std's. The functions std leaves to the platform's libm are `Math`'s. + +use rustc_middle::mir::{BinOp, UnOp}; +use rustc_middle::ty::{self, Ty}; +use rustc_span::Span; + +use super::body::Body; +use super::num::{Int, Operand}; +use crate::ts::{Expr, Type}; + +/// The TypeScript type of an `f64`. +pub fn ty() -> Type { + Type::Rt("f64", Vec::new()) +} + +/// Whether `ty` is `f64`. The other float types have no JS value yet. +pub fn is(ty: Ty<'_>) -> bool { + matches!(ty.kind(), ty::Float(ty::FloatTy::F64)) +} + +/// `v` as a literal: the shortest digits that read back as it, as Rust's `Debug` prints them. +pub fn lit(v: f64) -> Expr { + match v { + _ if v.is_nan() => Expr::lit("NaN"), + f64::INFINITY => Expr::lit("Infinity"), + f64::NEG_INFINITY => Expr::lit("-Infinity"), + _ => Expr::lit(format!("{v:?}")), + } +} + +/// `l op r` of two `f64`s, which JS's operators round as Rust's do. +pub fn binary<'tcx>(b: &mut Body<'_, '_, 'tcx>, op: BinOp, l: Expr, r: Expr, span: Span) -> Expr { + let js = match op { + BinOp::Add => "+", + BinOp::Sub => "-", + BinOp::Mul => "*", + BinOp::Div => "/", + // Both are C's `fmod`: exact, with the dividend's sign. + BinOp::Rem => "%", + BinOp::Eq => "===", + BinOp::Ne => "!==", + BinOp::Lt => "<", + BinOp::Le => "<=", + BinOp::Gt => ">", + BinOp::Ge => ">=", + _ => return b.err(span, format!("`{op:?}` on `f64`")), + }; + l.binary(js, r) +} + +pub fn unary<'tcx>(b: &mut Body<'_, '_, 'tcx>, op: UnOp, value: Expr, span: Span) -> Expr { + match op { + UnOp::Neg => Expr::unary("-", value), + _ => b.err(span, format!("`{op:?}` on `f64`")), + } +} + +/// `value as to` where either side is a float. +pub fn cast<'tcx>(b: &mut Body<'_, '_, 'tcx>, value: Operand, from: Ty<'tcx>, to: Ty<'tcx>, span: Span) -> Expr { + match (Int::of(from), Int::of(to)) { + _ if is(from) && is(to) => value.expr, + // `From`, which `as` has no form of. + _ if from.is_bool() && is(to) => Expr::Cond(Box::new(value.expr), Box::new(lit(1.0)), Box::new(lit(0.0))), + // An integer to the nearest `f64`, rounded once: exact up to 2^53, which narrow ones are. + (Some(int), None) if is(to) => match value.value { + Some(c) if int.signed => lit(c as f64), + Some(c) => lit(c as u128 as f64), + None if int.big() => int.method("toF64", vec![value.expr]), + None => Expr::As(Box::new(value.expr), ty()), + }, + // Truncated, saturated at the type's bounds, and 0 for NaN. + (None, Some(int)) if is(from) => int.method("fromF64", vec![value.expr]), + _ => b.err(span, format!("a cast from `{from}` to `{to}`")), + } +} + +/// A call of `f64`'s own `name`, or `None` if it is not mapped. +pub fn method<'tcx>(b: &mut Body<'_, '_, 'tcx>, name: &str, args: Vec, span: Span) -> Option { + let math = |f: &str, args: Vec| Expr::ident("Math").member(f).call(args); + let rt = |f: &str, args: Vec| Expr::rt_of("f64", f).call(args); + let one = |args: Vec| <[Expr; 1]>::try_from(args).ok().map(|[x]| x); + Some(match name { + // Exact in JS as in std. + "floor" | "ceil" | "trunc" | "abs" | "sqrt" => math(name, args), + // libm's in Rust, the engine's in JS: each within a few ULPs, the README says how many. + "exp" | "log2" | "log10" | "cbrt" | "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "sinh" | "cosh" + | "tanh" | "asinh" | "acosh" | "atan2" | "hypot" => math(name, args), + "ln" => math("log", args), + "exp_m1" => math("expm1", args), + "ln_1p" => math("log1p", args), + "exp2" => math("pow", vec![Expr::lit("2"), one(args)?]), + "log" => { + let [x, base] = <[Expr; 2]>::try_from(args).ok()?; + math("log", vec![x]).binary("/", math("log", vec![base])) + } + "is_nan" => Expr::ident("Number").member("isNaN").call(args), + "is_finite" => Expr::ident("Number").member("isFinite").call(args), + "recip" => Expr::lit("1").binary("/", one(args)?), + // std multiplies by these constants, rounded once. + "to_degrees" => one(args)?.binary("*", lit(180.0 / std::f64::consts::PI)), + "to_radians" => one(args)?.binary("*", lit(std::f64::consts::PI / 180.0)), + "round" | "round_ties_even" | "fract" | "signum" | "copysign" | "is_infinite" | "is_normal" + | "is_subnormal" | "is_sign_positive" | "is_sign_negative" | "next_up" | "next_down" | "max" | "min" + | "midpoint" | "to_bits" | "from_bits" | "to_be_bytes" | "to_le_bytes" | "from_be_bytes" | "from_le_bytes" + | "total_cmp" | "powi" | "powf" | "sin_cos" | "atanh" | "div_euclid" | "rem_euclid" => rt(&super::camel(name), args), + // wasm32 is little-endian. + "to_ne_bytes" => rt("toLeBytes", args), + "from_ne_bytes" => rt("fromLeBytes", args), + "mul_add" => b.err(span, "`f64::mul_add`, which rounds once, where JS would round twice"), + _ => return None, + }) +} diff --git a/ts/src/lower/fmt.rs b/ts/src/lower/fmt.rs index 3cbd5edd4..6b0b81a40 100644 --- a/ts/src/lower/fmt.rs +++ b/ts/src/lower/fmt.rs @@ -294,6 +294,8 @@ pub fn std_impl<'tcx>( (Std::FmtError, Trait::Display) => rt("errorDisplay", vec![value, f]), (Std::TryFromIntError, Trait::Debug) => rt("tryFromIntErrorDebug", vec![value, f]), (Std::TryFromIntError, Trait::Display) => rt("tryFromIntErrorDisplay", vec![value, f]), + (Std::TryFromFloatSecsError, Trait::Debug) => rt("tryFromFloatSecsErrorDebug", vec![value, f]), + (Std::TryFromFloatSecsError, Trait::Display) => rt("tryFromFloatSecsErrorDisplay", vec![value, f]), (Std::FromUtf8Error, Trait::Debug) => rt("fromUtf8ErrorDebug", vec![value, f]), (Std::FromUtf8Error, Trait::Display) => rt("fromUtf8ErrorDisplay", vec![value, f]), (Std::Duration, Trait::Debug) => Expr::rt("durationDebug", vec![value, f]), diff --git a/ts/src/lower/iter.rs b/ts/src/lower/iter.rs index 697543a88..04cabf455 100644 --- a/ts/src/lower/iter.rs +++ b/ts/src/lower/iter.rs @@ -398,6 +398,18 @@ pub fn call<'tcx>( let dropper = iter_dropper(b, self_ty, span); Some(Expr::rt("durationSum", tail(vec![recv.clone()], vec![dropper]))) } + // std's float sum starts from -0.0, so a sum of none, or of -0.0s, is -0.0. + "sum" | "product" if super::float::is(targ(1)?) => { + let (op, unit) = match name { + "sum" => ("add", -0.0), + _ => ("mul", 1.0), + }; + let dropper = iter_dropper(b, self_ty, span); + method( + name, + tail(vec![Expr::rt_of("f64", op), super::float::lit(unit)], vec![dropper]), + ) + } "sum" | "product" => { let int = Int::of(targ(1)?)?; let (op, unit) = match name { diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index 15896e753..88f5f304e 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -33,6 +33,7 @@ pub enum Std { Box, FromUtf8Error, TryFromIntError, + TryFromFloatSecsError, /// `rt.Formatter`. Formatter, /// `rt.Arguments`: what `format_args!` makes. @@ -158,6 +159,7 @@ impl Std { match tcx.item_name(did).as_str() { "FromUtf8Error" => return Some(Self::FromUtf8Error), "TryFromIntError" => return Some(Self::TryFromIntError), + "TryFromFloatSecsError" => return Some(Self::TryFromFloatSecsError), "PoisonError" => return Some(Self::PoisonError), _ => {} } @@ -625,7 +627,7 @@ pub fn key(tcx: TyCtxt<'_>, def_id: DefId) -> Option<(String, String)> { ty::Str => "str".into(), ty::Bool => "bool".into(), ty::Char => "char".into(), - ty::Int(_) | ty::Uint(_) => self_ty.to_string(), + ty::Int(_) | ty::Uint(_) | ty::Float(_) => self_ty.to_string(), _ => return None, }; return Some((owner, name)); @@ -722,6 +724,9 @@ fn mapping<'tcx>( }; return Some(int.method(method, args)); } + if owner == "f64" { + return super::float::method(b, &name, args, span); + } let self_ty = || gargs.types().next(); let is_u8 = |t: Ty<'tcx>| matches!(t.kind(), ty::Uint(ty::UintTy::U8)); @@ -746,9 +751,9 @@ fn mapping<'tcx>( if to == from || (string(tcx, to) && string(tcx, from.peel_refs())) { return Some(value); } - // Each `From` between integers and `char`, or from `bool`, keeps the value, as - // `as` then does. - let scalar = |t: Ty<'tcx>| Int::of(t).is_some() || t.is_char(); + // Each `From` between integers, `char`, and `f64`, or from `bool`, keeps the + // value, as `as` then does. + let scalar = |t: Ty<'tcx>| Int::of(t).is_some() || t.is_char() || super::float::is(t); if !scalar(to) || !(scalar(from) || from.is_bool()) { return None; } @@ -1295,7 +1300,7 @@ fn int_named(name: &str) -> Option { } fn primitive(ty: Ty<'_>) -> bool { - matches!(ty.kind(), ty::Bool | ty::Int(_) | ty::Uint(_) | ty::Char) + matches!(ty.kind(), ty::Bool | ty::Int(_) | ty::Uint(_) | ty::Char) || super::float::is(ty) } /// A `Vec`'s elements as a slice: an `rt.ByteSlice` of a byte vector, or the array itself. @@ -1345,6 +1350,7 @@ pub fn default_value<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>) -> Option Expr::lit("false"), ty::Int(_) | ty::Uint(_) => Int::of(ty)?.lit(0), + _ if super::float::is(ty) => super::float::lit(0.0), ty::Char => Expr::lit("0"), ty::Ref(_, inner, _) => match inner.kind() { ty::Slice(elem) if matches!(elem.kind(), ty::Uint(ty::UintTy::U8)) => Expr::rt( diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index 085f546c4..dec05bf28 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -3552,9 +3552,11 @@ pub fn fold(e: Expr) -> Expr { _ => None, }; if let (Some(a), Some(b)) = (num(&l), num(&r)) { - // Integer arithmetic JS does exactly, in 32 bits for the bitwise operators. + // Integer arithmetic JS does exactly, in 32 bits for the bitwise operators. An + // `f64` literal is spelled with a point or an exponent, and its -0.0 is no integer. + let int = |e: &Expr| matches!(e, Expr::Lit(t) if !t.contains(['.', 'e'])); let small = |v: f64| v.fract() == 0.0 && v.abs() < 2f64.powi(31); - if small(a) && small(b) { + if int(&l) && int(&r) && small(a) && small(b) { let (x, y) = (a as i64, b as i64); let int = match op { "<<" if (0..31).contains(&y) => Some((x as i32).wrapping_shl(y as u32) as i64), diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index 80191db6e..22f3a55d8 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -977,6 +977,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { ConstValue::Scalar(Scalar::Int(int)) => match ty.kind() { ty::Bool => Expr::lit((int.to_bits_unchecked() != 0).to_string()), ty::Char => Expr::lit(int.to_u32().to_string()), + _ if crate::lower::float::is(ty) => crate::lower::float::lit(f64::from_bits(int.to_u64())), _ => match Int::of(ty) { Some(i) => { let size = rustc_abi::Size::from_bits(i.bits); diff --git a/ts/src/lower/mir/rvalue.rs b/ts/src/lower/mir/rvalue.rs index e90f57a0c..5709c28fa 100644 --- a/ts/src/lower/mir/rvalue.rs +++ b/ts/src/lower/mir/rvalue.rs @@ -189,7 +189,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { let tcx = self.tcx; let from = op.ty(&self.mir.local_decls, tcx); match kind { - CastKind::IntToInt => { + CastKind::IntToInt | CastKind::IntToFloat | CastKind::FloatToInt | CastKind::FloatToFloat => { let value = self.operand(op, span, out); let operand = num::Operand { expr: value, diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index 3980fe5f9..af2dd7001 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -12,6 +12,7 @@ mod collections; mod consts; mod copy; mod drop; +mod float; mod fmt; mod iter; mod library; @@ -1020,6 +1021,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { match ty.kind() { ty::Bool => Type::Builtin("boolean"), ty::Int(_) | ty::Uint(_) => num::Int::of(ty).unwrap().ty(), + _ if float::is(ty) => float::ty(), ty::Char => Type::Rt("char", Vec::new()), ty::Str => Type::Builtin("string"), ty::Never => Type::Builtin("never"), @@ -1191,6 +1193,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { library::Std::Box => self.ty(args.type_at(0), span), library::Std::FromUtf8Error => Type::Rt("FromUtf8Error", vec![]), library::Std::TryFromIntError => Type::Rt("TryFromIntError", vec![]), + library::Std::TryFromFloatSecsError => Type::Rt("TryFromFloatSecsError", vec![]), library::Std::Formatter => Type::Rt("Formatter", vec![]), library::Std::Arguments => Type::Rt("Arguments", vec![]), library::Std::FmtError => Type::Rt("FmtError", vec![]), diff --git a/ts/src/lower/num.rs b/ts/src/lower/num.rs index 6305e6b4e..f4458d260 100644 --- a/ts/src/lower/num.rs +++ b/ts/src/lower/num.rs @@ -12,6 +12,7 @@ use rustc_middle::ty::{self, Ty}; use rustc_span::Span; use super::body::Body; +use super::float; use crate::ts::{Expr, Key, Type}; /// An integer type, by width and signedness. `usize` and `isize` are 32 bits. @@ -217,6 +218,9 @@ pub fn binary<'tcx>( span: Span, ) -> Expr { let (lty, rty) = (lty.peel_refs(), rty.peel_refs()); + if float::is(lty) { + return float::binary(b, op, l.expr, r.expr, span); + } let compare = match op { BinOp::Eq => Some("==="), BinOp::Ne => Some("!=="), @@ -340,6 +344,9 @@ pub fn binary<'tcx>( pub fn unary<'tcx>(b: &mut Body<'_, '_, 'tcx>, op: UnOp, value: Expr, ty: Ty<'tcx>, span: Span) -> Expr { let ty = ty.peel_refs(); + if float::is(ty) { + return float::unary(b, op, value, span); + } match (op, ty.kind()) { (UnOp::Not, ty::Bool) => value.not(), (UnOp::Not, ty::Int(_) | ty::Uint(_)) => { @@ -363,6 +370,9 @@ pub fn unary<'tcx>(b: &mut Body<'_, '_, 'tcx>, op: UnOp, value: Expr, ty: Ty<'tc /// `value as to`. pub fn cast<'tcx>(b: &mut Body<'_, '_, 'tcx>, value: Operand, from: Ty<'tcx>, to: Ty<'tcx>, span: Span) -> Expr { + if from.is_floating_point() || to.is_floating_point() { + return float::cast(b, value, from, to, span); + } // Only a `u8` casts to a `char`, whose scalar value it is. if let (ty::Char, ty::Uint(ty::UintTy::U8)) = (to.kind(), from.kind()) { return Expr::As(Box::new(value.expr), Type::Rt("char", Vec::new())); diff --git a/ts/src/lower/time.rs b/ts/src/lower/time.rs index f396aae50..2f96fa6ea 100644 --- a/ts/src/lower/time.rs +++ b/ts/src/lower/time.rs @@ -54,6 +54,12 @@ pub fn call<'tcx>( "saturating_sub" => "durationSaturatingSub", "saturating_mul" => "durationSaturatingMul", "abs_diff" => "durationAbsDiff", + "as_secs_f64" => "durationAsSecsF64", + "from_secs_f64" => "durationFromSecsF64", + "try_from_secs_f64" => "durationTryFromSecsF64", + "mul_f64" => "durationMulF64", + "div_f64" => "durationDivF64", + "div_duration_f64" => "durationDivDurationF64", _ => return Ok(None), }, ("Add", "add") if is(0) => "durationAdd", diff --git a/ts/src/program.rs b/ts/src/program.rs index eb17d11d5..70b813a67 100644 --- a/ts/src/program.rs +++ b/ts/src/program.rs @@ -11,6 +11,7 @@ use crate::ts::{self, FieldId, Function, Item, SymId}; pub const RUNTIME: &[(&str, &str)] = &[ ("rt.ts", include_str!("../runtime/rt.ts")), ("rt/collections.ts", include_str!("../runtime/rt/collections.ts")), + ("rt/float.ts", include_str!("../runtime/rt/float.ts")), ("rt/int.ts", include_str!("../runtime/rt/int.ts")), ("rt/iter.ts", include_str!("../runtime/rt/iter.ts")), ("rt/str.ts", include_str!("../runtime/rt/str.ts")), diff --git a/ts/tests/differential.ts b/ts/tests/differential.ts index b7c12d6ab..d45ff8053 100644 --- a/ts/tests/differential.ts +++ b/ts/tests/differential.ts @@ -7,7 +7,9 @@ // random bytes rarely make: every pair of values next to a power of two. // // A harness, or a hand-written caller wrapping one, exports `run(input: rt.ByteSlice): -// Vec`, and may export `cases`, inputs it names, which run with the generated ones. +// Vec`, and may export `cases`, inputs it names, which run with the generated ones, +// and `within(input, native, translated)`, which accepts outputs that differ only where +// Rust leaves the result to the platform, as libm's are. // The wasm build exports `input`, `run`, and `output` around the crate's entry, which // `wasm.ts` calls. A panic traps natively and throws `Panic` in // TypeScript, and the two agree only if their messages do. @@ -188,7 +190,8 @@ for (const input of cases) { continue; } if (called === "panic") panics++; - if (show(want) !== show(got)) { + const close = want instanceof Uint8Array && got instanceof Uint8Array && harness.within?.(input, want, got) === true; + if (show(want) !== show(got) && !close) { failures++; if (failures <= 20) { console.error(`input ${show(input)}\nnative ${display(want)}\nts ${display(got)}\n`); diff --git a/ts/tests/nan.ts b/ts/tests/nan.ts new file mode 100644 index 000000000..b25d9927b --- /dev/null +++ b/ts/tests/nan.ts @@ -0,0 +1,28 @@ +// A NaN's payload and sign, which JS leaves to the engine: what this engine keeps through +// `from_bits` and `to_bits`, the sign tests, and `copysign`, and that it quiets a +// signaling NaN stored in an array, as V8 does, which the README notes. +// +// usage: node nan.ts + +import { strict as assert } from "node:assert"; + +import * as rt from "../runtime/rt.ts"; + +/** A `u64`'s value. */ +function big(v: rt.u64): bigint { + return typeof v === "number" ? BigInt(v) : (BigInt(v.hi) << 32n) + BigInt(v.lo); +} + +const QUIET = 0x0008000000000000n; +for (const bits of [0x7ff8000000000000n, 0xfff8000000000000n, 0x7ff8000000000123n, 0xfffc0000deadbeefn, 0x7ff0000000000001n, 0xfff4000000000abcn]) { + const x = rt.f64FromBits(rt.u64Of(bits)); + const negative = bits >> 63n === 1n; + assert.equal(big(rt.f64ToBits(x)), bits, `${bits.toString(16)} round trips`); + assert.equal(rt.f64IsSignNegative(x), negative); + assert.equal(rt.f64IsSignPositive(x), !negative); + assert.equal(big(rt.f64ToBits(rt.f64Copysign(x, -1))), bits | (1n << 63n)); + assert.equal(big(rt.f64ToBits(rt.f64Copysign(x, 1))), bits & ~(1n << 63n)); + // A double array holds a NaN quieted, its payload otherwise kept. + const held = [x, 1.5]; + assert.equal(big(rt.f64ToBits(held[0])), bits | QUIET, `${bits.toString(16)} held in an array`); +} diff --git a/ts/tests/runtime.rs b/ts/tests/runtime.rs index c79761277..07259ff17 100644 --- a/ts/tests/runtime.rs +++ b/ts/tests/runtime.rs @@ -14,6 +14,17 @@ fn spawn_budget() { assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); } +/// A NaN keeps its payload and sign through `f64`'s bits, which `nan.ts` checks in Node. +#[test] +fn nan_payloads() { + let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("nan.ts"); + let output = Command::new("node") + .arg(script) + .output() + .expect("running node, which the dev shell provides"); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); +} + /// The runtime's B-tree runs `btree.ts`'s script of random operations as std's does. #[test] fn btree_script() { From ad176000d4ded6ed2d325d2335baa07c4410ea1d Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:31:36 -0700 Subject: [PATCH 3/4] fix(ts): keep f64's runtime shakeable, and escape the globals it calls Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/reject/census.md | 7 +++---- ts/runtime/rt/float.ts | 11 +++++++++-- ts/runtime/rt/time.ts | 8 +++----- ts/src/census.rs | 2 ++ ts/src/lower/mod.rs | 5 +++++ 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/ts/fixtures/reject/census.md b/ts/fixtures/reject/census.md index 2fa76ffb8..86c52834d 100644 --- a/ts/fixtures/reject/census.md +++ b/ts/fixtures/reject/census.md @@ -31,7 +31,7 @@ | 1 | `_` of `_`, which drops `_`, which is not supported yet | fixtures/reject/src/lib.rs:67:13 | | 1 | `_` through a held type's own, which is not supported yet | fixtures/reject/src/lib.rs:342:2 | | 1 | `_`, which is not supported yet | fixtures/reject/src/lib.rs:318:9 | -| 1 | `_`, which rounds once, where JS would round twice | fixtures/reject/src/lib.rs:472:4 | +| 1 | `f64::mul_add`, which rounds once, where JS would round twice | fixtures/reject/src/lib.rs:472:4 | | 1 | `fold` of `_`, which folds a `flatten` whose source and iterators both drop, which is not supported yet | fixtures/reject/src/lib.rs:235:26 | | 1 | a `zip` of `_` and `_`, which std reads by index, of items that drop, which is not supported yet | fixtures/reject/src/lib.rs:240:30 | | 1 | a `zip` of `_`, whose closures std calls one item further through `next` than through `fold` | fixtures/reject/src/lib.rs:186:51 | @@ -69,7 +69,6 @@ The type or module each refused type or path belongs to. | 2 | `Meters` | fixtures/reject/src/lib.rs:99:2 | | 2 | `Noisy` | fixtures/reject/src/lib.rs:67:13 | | 2 | `Poll` | fixtures/reject/src/lib.rs:119:20 | -| 2 | `f64` | fixtures/reject/src/lib.rs:472:4 | | 2 | `std::collections::HashMap` | fixtures/reject/src/lib.rs:67:13 | | 2 | `std::iter::Iterator` | fixtures/reject/src/lib.rs:266:16 | | 2 | `std::iter::Take` | fixtures/reject/src/lib.rs:390:79 | @@ -86,6 +85,7 @@ The type or module each refused type or path belongs to. | 1 | `T \| undefined` | fixtures/reject/src/lib.rs:38:2 | | 1 | `[u16; 1]` | fixtures/reject/src/lib.rs:259:22 | | 1 | `dyn std::ops::Fn(u8) -> u8` | fixtures/reject/src/lib.rs:11:16 | +| 1 | `f64` | fixtures/reject/src/lib.rs:477:2 | | 1 | `from_fn` | fixtures/reject/src/lib.rs:391:13 | | 1 | `into_keys` | fixtures/reject/src/lib.rs:67:13 | | 1 | `std::collections::hash_map::Entry` | fixtures/reject/src/lib.rs:431:15 | @@ -132,7 +132,6 @@ The type or module each refused type or path belongs to. | 1 | `_` with a mutable reference to a value JS copies, which its mapping would copy | `std::iter::IntoIterator::into_iter` | fixtures/reject/src/lib.rs:17:2 | | 1 | `_` with a mutable reference to a value JS copies, which its mapping would copy | `std::slice::last_mut` | fixtures/reject/src/lib.rs:424:21 | | 1 | `_`, which is not supported yet | `Hasher::write` | fixtures/reject/src/lib.rs:318:9 | -| 1 | `_`, which rounds once, where JS would round twice | `f64::mul_add` | fixtures/reject/src/lib.rs:472:4 | | 1 | `fold` of `_`, which folds a `flatten` whose source and iterators both drop, which is not supported yet | `std::iter::Flatten` | fixtures/reject/src/lib.rs:235:26 | | 1 | a `zip` of `_` and `_`, which std reads by index, of items that drop, which is not supported yet | `std::iter::Map`, `std::ops::Range` | fixtures/reject/src/lib.rs:240:30 | | 1 | a `zip` of `_`, whose closures std calls one item further through `next` than through `fold` | `std::iter::Chain` | fixtures/reject/src/lib.rs:186:51 | @@ -355,7 +354,7 @@ The type or module each refused type or path belongs to.
-
`_`, which rounds once, where JS would round twice (1) +
`f64::mul_add`, which rounds once, where JS would round twice (1) - fixtures/reject/src/lib.rs:472:4: `f64::mul_add`, which rounds once, where JS would round twice diff --git a/ts/runtime/rt/float.ts b/ts/runtime/rt/float.ts index 4845dc33f..21a961e12 100644 --- a/ts/runtime/rt/float.ts +++ b/ts/runtime/rt/float.ts @@ -9,8 +9,9 @@ import type { Ordering } from "./collections.ts"; import { type f64, type u64, u64FromHalves } from "./int.ts"; -/** Eight bytes that hold a double while its bits are read or written. */ -const view = new DataView(new ArrayBuffer(8)); +/** Eight bytes that hold a double while its bits are read or written, made on first use so a bundler drops them unused. */ +let scratch: DataView | undefined; +const bytes = (): DataView => (scratch ??= new DataView(new ArrayBuffer(8))); const TWO32 = 4294967296; const TWO52 = 4503599627370496; /** `f64::MIN_POSITIVE`, the least normal double. */ @@ -38,6 +39,7 @@ export const f64Fract = (x: f64): f64 => x - Math.trunc(x); /** `f64::is_sign_negative`: whether the sign bit is set, as it is for -0.0. */ export function f64IsSignNegative(x: f64): boolean { if (x === x) return x < 0 || 1 / x < 0; + const view = bytes(); view.setFloat64(0, x); return view.getUint8(0) >= 0x80; } @@ -52,6 +54,7 @@ export const f64Signum = (x: f64): f64 => (x !== x ? NaN : f64IsSignNegative(x) export function f64Copysign(x: f64, y: f64): f64 { const negative = f64IsSignNegative(y); if (x === x) return negative ? -Math.abs(x) : Math.abs(x); + const view = bytes(); view.setFloat64(0, x); const top = view.getUint8(0) & 0x7f; view.setUint8(0, negative ? top | 0x80 : top); @@ -76,6 +79,7 @@ export function f64IsSubnormal(x: f64): boolean { /** `x`'s bits moved by one toward +infinity if `up`, from a finite or infinite non-NaN. */ function step(x: number, up: boolean): number { if (x === 0) return up ? 5e-324 : -5e-324; + const view = bytes(); view.setFloat64(0, x); let hi = view.getUint32(0); let lo = view.getUint32(4); @@ -112,12 +116,14 @@ export function f64Midpoint(a: f64, b: f64): f64 { /** `f64::to_bits`. */ export function f64ToBits(x: f64): u64 { + const view = bytes(); view.setFloat64(0, x); return u64FromHalves(view.getUint32(0), view.getUint32(4)); } /** `f64::from_bits`. */ export function f64FromBits(v: u64): f64 { + const view = bytes(); view.setUint32(0, typeof v === "number" ? Math.floor(v / TWO32) : v.hi); view.setUint32(4, typeof v === "number" ? v >>> 0 : v.lo); return view.getFloat64(0); @@ -147,6 +153,7 @@ export const f64FromLeBytes = (bytes: Uint8Array): f64 => fromBytes(bytes, true) /** `f64::total_cmp`: IEEE 754's total order, by the bits, with a negative's flipped. */ export function f64TotalCmp(a: f64, b: f64): Ordering { + const view = bytes(); view.setFloat64(0, a); let ah = view.getInt32(0); let al = view.getUint32(4); diff --git a/ts/runtime/rt/time.ts b/ts/runtime/rt/time.ts index c8510c1ed..6d3718f11 100644 --- a/ts/runtime/rt/time.ts +++ b/ts/runtime/rt/time.ts @@ -7,6 +7,7 @@ import { Err, type FmtResult, type Formatter, fmtDebugStructFields, fmtPad, Ok, panic, type Result } from "../rt.ts"; import { cmp, type Hasher, hashInt, type Ordering } from "./collections.ts"; +import { f64ToBits } from "./float.ts"; import { type f64, type u32, type u64, type u128, u64CheckedAdd, u64CheckedMul, u64CheckedSub, u64Div, u64Eq, u64Of, u64Rem, u64ToF64, u64TryFrom, u128Add, u128Cast, u128Div, u128Mul, u128Rem } from "./int.ts"; import { type Iter, iterFold } from "./iter.ts"; @@ -245,13 +246,10 @@ export function durationTryFromSecsF64(secs: f64): Result String { "rt", // Globals the output calls. "Array", + "Infinity", + "Math", + "NaN", + "Number", "Object", + "String", "Uint8Array", ]; match RESERVED.contains(&name) { From 734a45416ca5905d0ce96575f5aed55d6da1e85e Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:40:38 -0700 Subject: [PATCH 4/4] fix(ts): keep a NaN constant's sign and payload Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/expected/floats.ts | 10 ++++++---- ts/fixtures/lang/src/floats.rs | 4 ++++ ts/src/lower/float.rs | 9 +++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ts/fixtures/lang/expected/floats.ts b/ts/fixtures/lang/expected/floats.ts index 91ce70e2a..1053ebf81 100644 --- a/ts/fixtures/lang/expected/floats.ts +++ b/ts/fixtures/lang/expected/floats.ts @@ -224,6 +224,8 @@ export function floats(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { } putF64(out, _377); } + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.f64ToBits(rt.f64FromBits({ hi: 0xfff80000, lo: 0x123 } as rt.u64))))); + rt.byteVecPush(out, rt.f64IsSignNegative(rt.f64FromBits({ hi: 0xfff80000, lo: 0x123 } as rt.u64)) ? 1 : 0); const v2: rt.f64[] = [a, b, 0.5]; putF64(out, rt.iterSum(rt.iterSlice(v2), rt.f64Add, -0.0)); putF64(out, rt.iterProduct(rt.iterSlice(v2), rt.f64Mul, 1.0)); @@ -233,13 +235,13 @@ export function floats(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { const g: Gauge.Gauge = { level: a, peak: b }; raise(rt.place(() => g.level, (value) => g.level = value), b); if (g.peak !== undefined) { - const _409: rt.f64 = rt.f64Max(g.peak, g.level); - g.peak = _409; + const _417: rt.f64 = rt.f64Max(g.peak, g.level); + g.peak = _417; } putF64(out, g.level); putF64(out, unwrapOr$Option_f64(g.peak, -1.0)); - const _422: Gauge.Gauge = Gauge.default$Default(); - rt.byteVecPush(out, Gauge.eq$PartialEq(g, _422) ? 1 : 0); + const _430: Gauge.Gauge = Gauge.default$Default(); + rt.byteVecPush(out, Gauge.eq$PartialEq(g, _430) ? 1 : 0); rt.byteVecPush(out, g.peak === b ? 1 : 0); let local: rt.f64 = a; raise(rt.place(() => local, (value2) => local = value2), 1.0); diff --git a/ts/fixtures/lang/src/floats.rs b/ts/fixtures/lang/src/floats.rs index bc35fb4c2..35e539347 100644 --- a/ts/fixtures/lang/src/floats.rs +++ b/ts/fixtures/lang/src/floats.rs @@ -51,6 +51,8 @@ fn raise(level: &mut f64, by: f64) { /// Each float constant std has, in a `const` of the crate's. const HALF: f64 = 0.5; +/// A NaN whose sign and payload a constant keeps, which only its bits make. +const PAYLOAD: f64 = f64::from_bits(0xfff8_0000_0000_0123); const CONSTANTS: [f64; 22] = [ f64::MAX, f64::MIN, @@ -275,6 +277,8 @@ pub fn floats(kind: u8, data: &[u8], out: &mut Vec) { for x in CONSTANTS { put_f64(out, x); } + out.extend_from_slice(&PAYLOAD.to_bits().to_be_bytes()); + out.push(PAYLOAD.is_sign_negative() as u8); let v = [a, b, 0.5]; put_f64(out, v.iter().sum()); put_f64(out, v.into_iter().product()); diff --git a/ts/src/lower/float.rs b/ts/src/lower/float.rs index 3c4cdea4f..f861db89a 100644 --- a/ts/src/lower/float.rs +++ b/ts/src/lower/float.rs @@ -22,8 +22,17 @@ pub fn is(ty: Ty<'_>) -> bool { } /// `v` as a literal: the shortest digits that read back as it, as Rust's `Debug` prints them. +/// A NaN other than `f64::NAN` is made from its bits, which keeps its sign and payload. pub fn lit(v: f64) -> Expr { match v { + _ if v.is_nan() && v.to_bits() != f64::NAN.to_bits() => { + let u64 = Int { + bits: 64, + signed: false, + pointer: false, + }; + Expr::rt("f64FromBits", vec![u64.lit(i128::from(v.to_bits()))]) + } _ if v.is_nan() => Expr::lit("NaN"), f64::INFINITY => Expr::lit("Infinity"), f64::NEG_INFINITY => Expr::lit("-Infinity"),