From 40c68b3fee59c014a4525c9b9109772c71f33172 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:32:56 -0700 Subject: [PATCH 1/5] chore(quest): claim quest/m3/ts-f32.md Quest-Claim: 82ac1b971f0533f4 From d4707890c5a371951ecb42dc67ece5e5b5e8140a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:05:37 -0700 Subject: [PATCH 2/5] feat(ts): translate f32 as f64 An f32 is a number a single holds exactly, each arithmetic result rounded with Math.fround. Casts to and from f64 and integers round once, formatting prints the shortest digits that read back as the single, and parsing rounds once to the nearest single. Duration's f32 methods and bytes' get_f32/put_f32 map. Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-f32.md | 21 - quest/m3/ts-unsafe.md | 3 +- ts/README.md | 27 +- ts/fixtures/lang/caller.ts | 79 +++- ts/fixtures/lang/expected/core/option.ts | 7 + ts/fixtures/lang/expected/harness.ts | 5 +- ts/fixtures/lang/expected/singles.Gauge.ts | 22 + ts/fixtures/lang/expected/singles.Reading.ts | 20 + ts/fixtures/lang/expected/singles.ts | 442 ++++++++++++++++++ ts/fixtures/lang/src/harness.rs | 5 +- ts/fixtures/lang/src/lib.rs | 1 + ts/fixtures/lang/src/singles.rs | 451 +++++++++++++++++++ ts/fixtures/reject/census.md | 22 +- ts/fixtures/reject/expected.txt | 8 +- ts/fixtures/reject/src/lib.rs | 8 +- ts/fixtures/stock/src/buf.rs | 6 +- ts/runtime/rt/float.ts | 345 +++++++++++--- ts/runtime/rt/int.ts | 31 +- ts/runtime/rt/time.ts | 28 +- ts/src/lower/collections.rs | 2 +- ts/src/lower/consts.rs | 4 +- ts/src/lower/float.rs | 212 ++++++--- ts/src/lower/fmt.rs | 4 +- ts/src/lower/iter.rs | 4 +- ts/src/lower/library.rs | 20 +- ts/src/lower/mir/place.rs | 2 +- ts/src/lower/mod.rs | 2 +- ts/src/lower/num.rs | 8 +- ts/src/lower/stock.rs | 8 + ts/src/lower/time.rs | 6 + ts/tests/nan.ts | 13 + ts/tests/runtime.rs | 2 +- 33 files changed, 1620 insertions(+), 199 deletions(-) delete mode 100644 quest/m3/ts-f32.md create mode 100644 ts/fixtures/lang/expected/singles.Gauge.ts create mode 100644 ts/fixtures/lang/expected/singles.Reading.ts create mode 100644 ts/fixtures/lang/expected/singles.ts create mode 100644 ts/fixtures/lang/src/singles.rs diff --git a/quest/m3/README.md b/quest/m3/README.md index e2b7da99a..c23450291 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -18,7 +18,6 @@ generic std gap is planned here even when moq never calls it. - [A raw pointer's staleness bound by its liveness](/quest/m3/ts-pointer-liveness.md) - a pointer dead before its variable is assigned again, as in an earlier loop iteration, translates - [Writes into fresh memory before any pointer to it](/quest/m3/ts-fresh-writes.md) - a whole write filling a fresh allocation or `MaybeUninit` before any pointer into it no longer refuses later pointers, as kio's `Segment::new` slots - [Unsafe code the translator can model](/quest/m3/ts-unsafe.md) - a dependency using only `unsafe` the translator can model translates from source -- [f32](/quest/m3/ts-f32.md) - `f32` and `Duration`'s `f32` methods translate as `f64` does - [The capacity of a BytesMut](/quest/m3/ts-bytes-capacity.md) - `BytesMut::capacity` and what reads it translate with `bytes`' values - [BufMut's provided writes through a &mut](/quest/m3/ts-buf-mut-forward.md) - `put`, `put_bytes`, `put_uint`, and `put_int` of a `&mut Vec` or `&mut BytesMut` translate - [Trait objects](/quest/m3/ts-dyn.md) - `dyn Error`, `dyn Iterator`, `dyn Buf`, and `Arc` translate, so a consumer neither rewrites nor binds them diff --git a/quest/m3/ts-f32.md b/quest/m3/ts-f32.md deleted file mode 100644 index 822de200d..000000000 --- a/quest/m3/ts-f32.md +++ /dev/null @@ -1,21 +0,0 @@ -# [M] f32 - -## Goal - -[rust2-ts](/ts/README.md) translates `f32` as it translates `f64`, including casts between -them, its formatting, and `Duration`'s `f32` methods, proven by differential tests against -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 `f64` and its formatting, which -[#165](https://github.com/moq-dev/rust2/pull/165) and [#260](https://github.com/moq-dev/rust2/pull/260) -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. -- Formatting prints the shortest digits that round-trip as an `f32`, as `core::fmt` does, - which JS's number formatting cannot give. - -Public API: the translator's output. diff --git a/quest/m3/ts-unsafe.md b/quest/m3/ts-unsafe.md index 9c78b8a37..898a9dcac 100644 --- a/quest/m3/ts-unsafe.md +++ b/quest/m3/ts-unsafe.md @@ -41,8 +41,7 @@ none of its own `unsafe`. ([#222](https://github.com/moq-dev/rust2/pull/222)): the bytes keep their type, so this reinterprets nothing. Those getters also need `<[u8]>::get` and `get_mut` of a range, and each integer's `from_le_bytes` and `from_ne_bytes`, and `chunks_vectored` - needs `std::io::IoSlice`: 184 of moq-net's 196 refusals in `bytes`, the rest being 12 - of [`f32`](/quest/m3/ts-f32.md). + needs `std::io::IoSlice`: all 184 of moq-net's refusals in `bytes`. - `BinaryHeap` and `PeekMut` translate from std's MIR, whose `Hole` moves elements through raw pointers into the `Vec`'s storage, replacing `rt.BinaryHeap` and `rt.PeekMut`, the hand-written port of std's sifts that diff --git a/ts/README.md b/ts/README.md index 7578507f4..6762c3c76 100644 --- a/ts/README.md +++ b/ts/README.md @@ -67,7 +67,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`, hashing in `rt/hash.ts`, ownership, atomics, and async in -`rt/task.ts`, `f64`'s methods in `rt/float.ts`, and `Duration` in `rt/time.ts`. What translates from std's own MIR, +`rt/task.ts`, `f64`'s and `f32`'s methods in `rt/float.ts`, and `Duration` in `rt/time.ts`. What translates from std's own MIR, `Option`'s, `Result`'s, and `Poll`'s methods, `==` of a `Result` and of std's errors, `cmp::max` and `min`, and the `str::parse` and `from_str_radix` of an integer or a `NonZero` and `bool`'s `str::parse`, goes in a module under its crate's name, as `core/option.ts` holds `map$1cf8u0y`. A dependency's modules go @@ -82,6 +82,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | `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 | +| `f32` | `rt.f32`: a `number` branded so, which a single holds exactly | | `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 | @@ -167,6 +168,18 @@ NaN an operation makes, so a NaN's payload and sign are the engine's: Node and C keep them through `from_bits` and `to_bits`, but V8 quiets a signaling NaN stored in an array, as a `Vec` holds it. +An `f32` is a JS number a single holds exactly. Each arithmetic result, and each of +`sqrt`, `recip`, and libm's functions, rounds to a single with `Math.fround`, which gives +the single nearest the exact value, since a double holds the exact result closely enough; +`%`, comparisons, and negation need no rounding. Its methods compute in singles where std +does, as `div_euclid`, `powi`, and `atanh` do, and in doubles where std does, as wasm32's +`midpoint` does. A cast to `f64` is the same number, and `f64 as f32` is `Math.fround`; +an integer rounds to a single once, never through the nearest double, which could round +twice. A NaN keeps its sign and payload at the top of a double NaN's, so `from_bits` and +`to_bits` keep a signaling one, which JS's conversion from a single would quiet. +`Duration`'s `f32` methods are std's: `as_secs_f32` and `div_duration_f32` round each step +to a single, and the rest go through `f64` as std's do. + `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 @@ -174,11 +187,13 @@ becomes a function writing its value, and `fmt::Arguments` an array of literal p and such functions: `rt.fmtWriteFmt(f, ["code=", (f) => rt.u16Display(code, f)])`. A `&dyn Debug` or `&dyn Display` is such a function too. Format specs take every option but `{:p}`: fill, alignment, `+`, `#`, `0`, `x?` and `X?`, a width and a precision, each also from an -argument, and `x`, `X`, `o`, `b`, `e`, and `E` for integers, `e` and `E` for `f64`. An `f64` +argument, and `x`, `X`, `o`, `b`, `e`, and `E` for integers, `e` and `E` for floats. An `f64` prints std's digits: the shortest that read back as it, the greater of two as close where JS's would take the even one, or its exact value rounded half to even to a precision, -where `toFixed` rounds half up. `str::parse::` takes std's grammar, not `Number`'s, -and rounds to the nearest double. The `str::parse` and `from_str_radix` of an integer or a +where `toFixed` rounds half up. An `f32` prints the shortest digits that read back as the +single, found as Dragon does, which JS has no function for. `str::parse` of a float takes +std's grammar, not `Number`'s, and rounds to the nearest of the type once, so an `f32` +never rounds through a double. The `str::parse` and `from_str_radix` of an integer or a `NonZero` are core's own, over the string's UTF-8 bytes; their `ParseIntError` is `rt.ParseIntError`, `{ kind }`, whose `IntErrorKind` is the variant's name. `bool`'s is core's too, failing with an `rt.ParseBoolError`; a `char`'s is `rt.charFromStr`, failing with @@ -380,7 +395,7 @@ of them: `split_off`, `try_reclaim`, `try_unsplit`, and `spare_capacity_mut`, which read it. - `Buf` and `BufMut` of a `Bytes`, a `BytesMut`, a `&[u8]`, and a `Vec` map: the required methods, `has_remaining`, `copy_to_slice`, `copy_to_bytes`, each `get_` and - `put_` of an integer or `f64`, `put_slice`, `put_bytes`, and `put`, whose panics are + `put_` of an integer or a float, `put_slice`, `put_bytes`, and `put`, whose panics are `bytes`'. `put` of the crate's own `Buf` into a `Vec` reads it through its methods as `bytes` does, and into a `BytesMut` is refused, as `bytes` picks those methods by its capacity. The provided `copy_to_bytes` of the crate's own `Buf`, whose `BytesMut` has @@ -610,7 +625,7 @@ fields overlap in different types ([unsafe](/quest/m3/ts-unsafe.md)), and trait behind `&`, `&mut`, `Box`, or `Arc` ([trait objects](/quest/m3/ts-dyn.md)), fail translation. Until they translate, an item that needs one, such as a `dyn Error`, binds to hand-written TS ([bindings](#bindings)). -These fail translation: `f32` ([`f32`](/quest/m3/ts-f32.md)), async closures, a `Poll` of a `Poll` of a `Poll`, a future whose output can be pending, an instance of a generic type whose field is a `Poll` of a type parameter that can be pending, a closure that owns a value +These fail translation: `f16` and `f128`, async closures, a `Poll` of a `Poll` of a `Poll`, a future whose output can be pending, an instance of a generic type whose field is a `Poll` of a type parameter that can be pending, a closure that owns a value that drops given to a std function that drops it beside another value that drops, as `for_each` of items that drop does, or keeps it other than in an iterator adaptor, and values that drop in some iterators ([drop order](/quest/m3/ts-drop-order.md)), a `Cow` of other than `str`, diff --git a/ts/fixtures/lang/caller.ts b/ts/fixtures/lang/caller.ts index 235b0e200..cdd10e882 100644 --- a/ts/fixtures/lang/caller.ts +++ b/ts/fixtures/lang/caller.ts @@ -137,16 +137,81 @@ const parsed = [ `1${"0".repeat(800)}e-800`, `${"9".repeat(800)}e-1100`, "1e65535", `1e-65536`, `0.${"0".repeat(70000)}1e70000`, ].map((text) => Uint8Array.of(op("floats::parses"), 0, ...new TextEncoder().encode(text))); -/** How many ULPs from native Rust's each `f64` of `floats`' kind 7 may be, which Rust leaves to libm. */ +/** A single's bits, big-endian, as `singles` takes them. */ +function single(x: number | bigint): number[] { + const bytes = new Uint8Array(4); + const view = new DataView(bytes.buffer); + if (typeof x === "bigint") view.setUint32(0, Number(x)); + else view.setFloat32(0, x); + return [...bytes]; +} + +/** + * `singles`' operands: each side of a half, of 2^23 and 2^24, where each integer type + * saturates, where a single's range and its normals end, the bounds of `Debug`'s + * exponent, infinities, and NaNs, one negative, one with a payload, and one signaling. + */ +const singlesValues: (number | bigint)[] = [ + 0, -0, 0.5, -0.5, 1.5, 2.5, -2.5, 0.49999997, 1, -1, 3, 0.1, 1 / 3, 2 ** 23 - 0.5, 2 ** 23, 2 ** 24, 2 ** 24 + 2, 255.5, -128.5, + 2147483520, 2 ** 31, -(2 ** 31), 4294967040, 2 ** 32, 2 ** 63, -(2 ** 63), 2 ** 64, 2 ** 127, 1e-9, 0.99999994, 1.0000001, 1e-4, 1e16, + 1e-40, 2 ** -149, 2 ** -126, 3.4028234663852886e38, -3.4028234663852886e38, Infinity, -Infinity, NaN, 0xffc00000n, 0x7fc00123n, 0x7f800001n, +]; +const singlesOp = op("singles::singles"); +const singlesCases = [ + ...Array.from({ length: 10 }, (_, kind) => singlesValues.flatMap((a) => singlesValues.map((b) => Uint8Array.of(singlesOp, kind, ...single(a), ...single(b), 1)))).flat(), + // `from_secs_f32`, `mul_f32`, and `div_f32` panicking, and `Duration::MAX.mul_f32`. + ...[18, 28, 38].flatMap((kind) => singlesValues.map((a) => Uint8Array.of(singlesOp, kind, ...single(a), ...single(a), 3))), + // Integers whose `f32` rounds once where through an `f64` it would round twice: past + // 2^53, a bit just past halfway between two singles. + ...wides.concat([(1n << 60n) + (1n << 36n) + 1n, (1n << 64n) - (1n << 39n) - 1n, (1n << 127n) + (1n << 103n) + 1n, (1n << 24n) + 1n]).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(singlesOp, 2, ...bytes); + }), +]; + +/** `singles::formats`' singles: ties at each precision, carries through nines, and the shortest digits at each end. */ +const singlesFormatted: (number | bigint)[] = [ + ...singlesValues, + 0.125, 0.375, 3.5, 9.5, 99.5, 0.95, 9.995, 1.25, 1.35, 2.675, 9.999999e-5, 1e15, 9999999e9, 1e-7, 123456.79, 16777215, 7.006492e-46, 1.17549421e-38, 8388609, +]; +const singlesFormatsOp = op("singles::formats"); +const singlesParsesOp = op("singles::parses"); +const singlesFormats = [ + ...singlesFormatted.flatMap((a) => Array.from({ length: 12 }, (_, p) => Uint8Array.of(singlesFormatsOp, 0, ...single(a), p))), + ...singlesFormatted.flatMap((a) => [1, 3, 4].map((kind) => Uint8Array.of(singlesFormatsOp, kind, ...single(a), 0))), + ...singlesFormatted.flatMap((a) => [[1, 2], [2, 1], [NaN, 1], [1, NaN], [-0, 0], [0, -0]].map(([b, c]) => Uint8Array.of(singlesFormatsOp, 2, ...single(a), ...single(b), ...single(c)))), + ...singlesValues.map((a) => Uint8Array.of(singlesParsesOp, 2, ...single(a))), +]; + +/** + * `singles::parses`' texts: std's grammar, past each end of a single's range, and + * decimals within a hair of halfway between two singles, which rounding through the + * nearest double would round twice. + */ +const singlesParsed = [ + "", "+", "-", ".", "e5", "1e", "1.e5", ".5", "+1", "1_000", " 1", "1e40", "-1e40", "1e-50", "-0", "inf", "-infinity", "NaN", "nan1", + "3.4028235e38", "3.4028236e38", "3.40282357e38", "3.4028235677973366e38", "3.4028235677973367e38", "1.4e-45", "7e-46", + "7.006492321624085354618647916449580656401e-46", "7.006492321624085354618647916449580656402e-46", "1.1754942e-38", "1.17549435e-38", + "16777217", "16777217.000000000001", "16777218.99999999999", "16777219", "1.00000005960464477539062499", "1.000000059604644775390625", + "1.00000005960464477539062501", "1.0000000596046448", "0.1", "1e10", "1234567e-10", "12345678e-10", "9007199254740993", + `1${"0".repeat(60)}e-60`, `0.${"0".repeat(60)}1e61`, "1e65535", "1e-65536", +].map((text) => Uint8Array.of(singlesParsesOp, 0, ...new TextEncoder().encode(text))); + +/** How many ULPs from native Rust's each float of `floats`' and `singles`' 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] % count !== floatsOp || input[1] % 10 !== 7 || native.length !== translated.length) return false; + const width = input[0] % count === floatsOp ? 8 : input[0] % count === singlesOp ? 4 : 0; + if (width === 0 || 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)); + const read = (v: DataView, i: number) => (width === 8 ? v.getBigInt64(i) : BigInt(v.getInt32(i))); + // Bits as a line through zero, so the distance between two is how many floats apart they are. + const magnitude = (1n << BigInt(width * 8 - 1)) - 1n; + const line = (v: bigint) => (v < 0n ? -(v & magnitude) : v); + for (let i = 0; i < native.length; i += width) { + const d = line(read(a, i)) - line(read(b, i)); if (d > ULPS || d < -ULPS) return false; } return true; @@ -404,4 +469,4 @@ const iters = Array.from({ length: 12 }, (_, kind) => [[], [2, 7, 9, 14], [0, 2, const hintedOp = op("strs::hinted"); const hinted = Array.from({ length: 10 * 7 * 21 }, (_, i) => Uint8Array.of(hintedOp, i % 10, Math.floor(i / 10) % 7, Math.floor(i / 70))); -export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...formats, ...parsed, ...dyns, ...cells, ...shared, ...locks, ...onces, ...bounds, ...unsafeCells, ...edits, ...subslices, ...consumers, ...closures, ...ranges, ...reducers, ...sliceEdits, ...drains, ...iters, ...hinted]; +export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...formats, ...parsed, ...singlesCases, ...singlesFormats, ...singlesParsed, ...dyns, ...cells, ...shared, ...locks, ...onces, ...bounds, ...unsafeCells, ...edits, ...subslices, ...consumers, ...closures, ...ranges, ...reducers, ...sliceEdits, ...drains, ...iters, ...hinted]; diff --git a/ts/fixtures/lang/expected/core/option.ts b/ts/fixtures/lang/expected/core/option.ts index 968055a4f..3e0f951b6 100644 --- a/ts/fixtures/lang/expected/core/option.ts +++ b/ts/fixtures/lang/expected/core/option.ts @@ -85,6 +85,13 @@ export function unwrapOr$0k15iai(self: rt.char | undefined, default_: rt.char): return self; } +export function unwrapOr$19h8ynp(self: rt.f32 | undefined, default_: rt.f32): rt.f32 { + if (self === undefined) { + return default_; + } + return self; +} + export function unwrapOr$1ayoj7n(self: rt.f64 | undefined, default_: rt.f64): rt.f64 { if (self === undefined) { return default_; diff --git a/ts/fixtures/lang/expected/harness.ts b/ts/fixtures/lang/expected/harness.ts index 22487a7bf..1c5052f6e 100644 --- a/ts/fixtures/lang/expected/harness.ts +++ b/ts/fixtures/lang/expected/harness.ts @@ -34,6 +34,7 @@ import { orders, bytes as orders_bytes } from "./orders.ts"; import { lists, shared as pointers_shared, places, unchecked, nested as pointers_nested, copies as pointers_copies, sentinels, cells as pointers_cells, rebound, writers, made } from "./pointers.ts"; import { refs, toRefs } from "./refs.ts"; import { copies as shapes_copies, order, discriminants, loops, patterns, closures, library, flows } from "./shapes.ts"; +import { singles, formats as singles_formats, parses as singles_parses } from "./singles.ts"; import { parts, chunked, subslices, drains, ranges as slices_ranges, edits as slices_edits } from "./slices.ts"; import { splits } from "./splits.ts"; import { statics, cells as statics_cells, onces } from "./statics.ts"; @@ -90,7 +91,7 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) refs(data21, out21); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["fmt::counts", (_2121: rt.u8, data22: rt.ByteSlice, out22: rt.ByteVec): void => { counts(data22, out22); - }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["fmt::rounding", (_2122: rt.u8, _396: rt.ByteSlice, out23: rt.ByteVec): void => { + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["fmt::rounding", (_2122: rt.u8, _3100: rt.ByteSlice, out23: rt.ByteVec): void => { rounding(out23); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["iters::skips", skips] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["wrappers::wrappers", wrappers] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["edits::edits", edits] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["utf8::decodes", decodes] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["floats::floats", floats] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["atomics::atomics", atomics] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["keys::overrides", overrides] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["dyns::dyns", (_2123: rt.u8, data23: rt.ByteSlice, out24: rt.ByteVec): void => { dyns(data23, out24); @@ -180,7 +181,7 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) made(data65, out66); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["arrays::as_slices", (_2166: rt.u8, data66: rt.ByteSlice, out67: rt.ByteVec): void => { asSlices(data66, out67); - }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["ints::widenings", widenings] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void]]; + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["ints::widenings", widenings] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["singles::singles", singles] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["singles::formats", singles_formats] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["singles::parses", singles_parses] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void]]; } /** Run one case. */ diff --git a/ts/fixtures/lang/expected/singles.Gauge.ts b/ts/fixtures/lang/expected/singles.Gauge.ts new file mode 100644 index 000000000..0b4ff6798 --- /dev/null +++ b/ts/fixtures/lang/expected/singles.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 `f32` field, changed in place through a `&mut`. */ +export interface Gauge { + level: rt.f32; + peak: rt.f32 | undefined; +} + +//# sourceMappingURL=singles.Gauge.ts.map diff --git a/ts/fixtures/lang/expected/singles.Reading.ts b/ts/fixtures/lang/expected/singles.Reading.ts new file mode 100644 index 000000000..390fa8908 --- /dev/null +++ b/ts/fixtures/lang/expected/singles.Reading.ts @@ -0,0 +1,20 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +export function fmt$Debug(self: Reading, f: rt.Formatter): rt.Result { + const _3: string = "Reading"; + const _4: string = "level"; + const _5: rt.Fmt = (f: rt.Formatter) => rt.f32Debug(self.level, f); + const _7: string = "history"; + const _8: rt.Fmt = (f: rt.Formatter) => rt.fmtList(self.history, f, rt.f32Debug); + return rt.debugStructFinish(rt.debugStructField(rt.debugStructField(rt.fmtDebugStruct(f, _3), _4, _5), _7, _8)); +} + +/** A reading whose derived `Debug` writes each `f32` with the placeholder's options. */ +export interface Reading { + level: rt.f32; + history: rt.f32[]; +} + +//# sourceMappingURL=singles.Reading.ts.map diff --git a/ts/fixtures/lang/expected/singles.ts b/ts/fixtures/lang/expected/singles.ts new file mode 100644 index 000000000..4c8be24be --- /dev/null +++ b/ts/fixtures/lang/expected/singles.ts @@ -0,0 +1,442 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { unwrapOr$19h8ynp, unwrapOr$0r2bmpr, mapOr$1e4a1e0, mapOr$15zxk73 } from "./core/option.ts"; +import { put } from "./fmt.ts"; +import * as Gauge from "./singles.Gauge.ts"; +import * as Reading from "./singles.Reading.ts"; + +/** The bits of an `f32` from the front of `data`, big-endian and zero-padded. */ +export function takeBits(data: rt.ByteSlice): [rt.u32, rt.ByteSlice] { + const buf: Uint8Array = rt.bytes(4); + 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.u32FromBeBytes(buf), rt.byteSliceRange(data, n)] as [rt.u32, rt.ByteSlice]; +} + +/** An `f32` from the front of `data`. */ +export function take(data: rt.ByteSlice): [rt.f32, rt.ByteSlice] { + const _4: [rt.u32, rt.ByteSlice] = takeBits(data); + return [rt.f32FromBits(_4[0]), _4[1]] as [rt.f32, rt.ByteSlice]; +} + +/** `x`'s bits, any NaN as `f32::NAN`'s. */ +export function putF32(out: rt.ByteVec, x: rt.f32): void { + let x2!: rt.f32; + if (!Number.isNaN(x)) { + x2 = x; + } else { + x2 = NaN; + } + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(x2)))); +} + +/** `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_f32` made, or its error printed. */ +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]])); + } +} + +export function raise(level: rt.Place, by: rt.f32): void { + level.v = Math.fround(level.v + by); + level.v = Math.fround(level.v * 2.0); +} + +export function singles(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { + let zeros!: boolean; + let _300!: rt.f32; + const _6: [rt.f32, rt.ByteSlice] = take(data); + const a: rt.f32 = _6[0]; + const _9: [rt.f32, rt.ByteSlice] = take(_6[1]); + const b: rt.f32 = _9[0]; + const _10: rt.u8 = rt.u8Rem(kind, 10); + if (_10 === 0) { + const iter: rt.Iter = rt.iterSlice([Math.fround(a + b), Math.fround(a - b), Math.fround(a * b), Math.fround(a / b), a % b, -a, Math.fround(a + 1.5), Math.fround(0.10000000149011612 * a), Math.fround(Math.fround(a * b) + a), Math.fround(Math.fround(a - b) / Math.fround(a + b))]); + while (true) { + const _28: rt.f32 | rt.Done = iter.next(); + if (_28 === rt.DONE) { + break; + } + putF32(out, _28); + } + const _35: boolean = a === b; + const _36: boolean = a !== b; + const _37: boolean = a < b; + const _38: boolean = a <= b; + const _39: boolean = a > b; + const _40: boolean = a >= b; + const _41: boolean = a === 0.0; + const _42: boolean = rt.f32PartialCmp(a, b) === -1; + const order = rt.f32PartialCmp(a, b); + const iter2: rt.Iter = rt.iterSlice([_35, _36, _37, _38, _39, _40, _41, _42, order !== undefined && order >= 0]); + while (true) { + const _50: boolean | rt.Done = iter2.next(); + if (_50 === rt.DONE) { + break; + } + rt.byteVecPush(out, _50 ? 1 : 0); + } + let c: rt.f32 = a; + c = Math.fround(c + b); + c = Math.fround(c - 0.25); + c = Math.fround(c * b); + c = Math.fround(c / 3.0); + c = c % a; + putF32(out, c); + const _61: [rt.f64, rt.f64] = [a as rt.f64, b as rt.f64]; + const wa: rt.f64 = _61[0]; + const wb: rt.f64 = _61[1]; + const iter3: rt.Iter = rt.iterSlice([wa, wa * wb, wa / wb, wa + 1e-30]); + while (true) { + const _70: rt.f64 | rt.Done = iter3.next(); + if (_70 === rt.DONE) { + break; + } + putF64(out, _70); + } + const iter4: rt.Iter = rt.iterSlice([Math.fround(wa * wb), Math.fround(wa / wb), Math.fround(wa + 1e-30), Math.fround(wa + wb)]); + while (true) { + const _86: rt.f32 | rt.Done = iter4.next(); + if (_86 === rt.DONE) { + break; + } + putF32(out, _86); + } + } else if (_10 === 1) { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u8ToBeBytes(rt.u8FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u16ToBeBytes(rt.u16FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.u32FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.u64FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u128ToBeBytes(rt.u128FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.usizeToBeBytes(rt.usizeFromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i8ToBeBytes(rt.i8FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i16ToBeBytes(rt.i16FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i32ToBeBytes(rt.i32FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i64ToBeBytes(rt.i64FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i128ToBeBytes(rt.i128FromF64(a as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.isizeToBeBytes(rt.isizeFromF64(a as rt.f64)))); + } 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 w: rt.f64 = rt.f64FromBits(rt.u64Cast(v)); + const iter5: rt.Iter = rt.iterSlice([Math.fround(rt.u128Low(v) & 0xff), Math.fround(rt.u128Low(v) & 0xffff), Math.fround(rt.u128Low(v)), rt.u64ToF32(rt.u64Cast(v)), rt.u128ToF32(v), Math.fround(rt.u128Low(v)), Math.fround((rt.u128Low(v) << 24) >> 24), Math.fround((rt.u128Low(v) << 16) >> 16), Math.fround(rt.u128Low(v) | 0), rt.i64ToF32(rt.i64Cast(v)), rt.i128ToF32(rt.i128Cast(v)), Math.fround(rt.u128Low(v) | 0), rt.u64ToF32(rt.u64Cast(rt.u128Shr(v, 64))), rt.i64ToF32(rt.i64Cast(rt.u128Shr(v, 64))), Math.fround(rt.u128Low(v) & 0xffff), Math.fround((rt.u128Low(v) << 16) >> 16), rt.u128And(v, 1) === 1 ? 1.0 : 0.0, Math.fround(rt.u128Low(v) & 0xff), Math.fround(w), 1.8446744073709552e19, -9.223372036854776e18, Infinity, 2147483648.0]); + while (true) { + const _214: rt.f32 | rt.Done = iter5.next(); + if (_214 === rt.DONE) { + break; + } + putF32(out, _214); + } + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.u64FromF64(rt.u64ToF32(rt.u64Cast(v)) as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.i128ToBeBytes(rt.i128FromF64(rt.i128ToF32(rt.i128Cast(v)) as rt.f64)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.u32FromF64(Math.fround(rt.u128Low(v)) as rt.f64)))); + } else if (_10 === 3) { + const iter6: rt.Iter = rt.iterSlice([Math.floor(a), Math.ceil(a), Math.trunc(a), rt.f32Round(a), rt.f32RoundTiesEven(a), rt.f32Fract(a), Math.abs(a), rt.f32Signum(a), Math.fround(Math.sqrt(a)), Math.fround(1 / a), Math.fround(a * 57.295780181884766), Math.fround(a * 0.01745329238474369)]); + while (true) { + const _255: rt.f32 | rt.Done = iter6.next(); + if (_255 === rt.DONE) { + break; + } + putF32(out, _255); + } + } else if (_10 === 4) { + const iter7: rt.Iter = rt.iterSlice([Number.isNaN(a), Number.isFinite(a), rt.f32IsInfinite(a), rt.f32IsNormal(a), rt.f32IsSubnormal(a), rt.f32IsSignPositive(a), rt.f32IsSignNegative(a)]); + while (true) { + const _270: boolean | rt.Done = iter7.next(); + if (_270 === rt.DONE) { + break; + } + rt.byteVecPush(out, _270 ? 1 : 0); + } + const iter8: rt.Iter = rt.iterSlice([rt.f32Copysign(a, b), rt.f32Copysign(1.5, a), rt.f32NextUp(a), rt.f32NextDown(a)]); + while (true) { + const _283: rt.f32 | rt.Done = iter8.next(); + if (_283 === rt.DONE) { + break; + } + putF32(out, _283); + } + } else if (_10 === 5) { + if (!(a === 0.0)) { + zeros = false; + } else { + zeros = b === 0.0; + } + const iter9: rt.Iter = rt.iterSlice([rt.f32Max(a, b), rt.f32Min(a, b)]); + while (true) { + const _295: rt.f32 | rt.Done = iter9.next(); + if (_295 === rt.DONE) { + break; + } + const x7: rt.f32 = _295; + if (!zeros) { + _300 = x7; + } else { + _300 = 0.0; + } + putF32(out, _300); + } + const _304: rt.f32 = rt.f32Midpoint(a, b); + const _305: rt.f32 = rt.f32DivEuclid(a, b); + const _306: rt.f32 = rt.f32RemEuclid(a, b); + const _308: rt.i32 = rt.i32Rem(rt.f32ToBits(b) | 0, 300); + const iter10: rt.Iter = rt.iterSlice([_304, _305, _306, rt.f32Powi(a, _308)]); + while (true) { + const _316: rt.f32 | rt.Done = iter10.next(); + if (_316 === rt.DONE) { + break; + } + putF32(out, _316); + } + rt.byteVecPush(out, rt.f32TotalCmp(a, b) & 0xff); + rt.byteVecPush(out, mapOr$1e4a1e0(rt.f32PartialCmp(a, b), 9, (o: rt.Ordering): rt.u8 => { + return o & 0xff; + })); + } else if (_10 === 6) { + const bits2: rt.u32 = takeBits(data)[0]; + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(rt.f32FromBits(bits2))))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.f32ToBeBytes(rt.f32FromBits(bits2)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.f32ToLeBytes(rt.f32FromBits(bits2)))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(rt.f32FromBeBytes(rt.u32ToBeBytes(bits2)))))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(rt.f32FromLeBytes(rt.u32ToBeBytes(bits2).reverse()))))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.f32ToLeBytes(rt.f32FromLeBytes(rt.u32ToBeBytes(bits2).reverse())))); + rt.byteVecPush(out, rt.f32IsSignNegative(rt.f32FromBits(bits2)) ? 1 : 0); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(rt.f32Copysign(rt.f32FromBits(bits2), -1.0))))); + if (!Number.isNaN(a)) { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(a)))); + } + putF32(out, rt.f32FromBits((rt.f32ToBits(a) ^ 1) >>> 0)); + } else if (_10 === 7) { + const _393: [rt.f32, rt.f32] = rt.f32SinCos(a); + const s: rt.f32 = _393[0]; + const c2: rt.f32 = _393[1]; + const iter11: rt.Iter = rt.iterSlice([Math.fround(Math.sin(a)), Math.fround(Math.cos(a)), Math.fround(Math.tan(a)), Math.fround(Math.asin(a)), Math.fround(Math.acos(a)), Math.fround(Math.atan(a)), Math.fround(Math.atan2(a, b)), Math.fround(Math.sinh(a)), Math.fround(Math.cosh(a)), Math.fround(Math.tanh(a)), Math.fround(Math.asinh(a)), Math.fround(Math.acosh(a)), rt.f32Atanh(a), Math.fround(Math.exp(a)), Math.fround(Math.pow(2, a)), Math.fround(Math.expm1(a)), Math.fround(Math.log(a)), Math.fround(Math.fround(Math.log(a)) / Math.fround(Math.log(b))), Math.fround(Math.log2(a)), Math.fround(Math.log10(a)), Math.fround(Math.log1p(a)), rt.f32Powf(a, b), Math.fround(Math.cbrt(a)), Math.fround(Math.hypot(a, b)), s, c2]); + while (true) { + const _421: rt.f32 | rt.Done = iter11.next(); + if (_421 === rt.DONE) { + break; + } + putF32(out, _421); + } + } else if (_10 === 8) { + const d: rt.Duration = rt.durationNew(rt.u64Shl(mapOr$15zxk73(rt.byteSliceFirst(_9[1]), 0, (_2: rt.u8): rt.u64 => { + return _2 as rt.u64; + }), 20), 123456789); + putF32(out, rt.durationAsSecsF32(d)); + putF32(out, rt.durationAsSecsF32(rt.durationNew(rt.f32ToBits(b) as rt.u64, rt.u32Rem(rt.f32ToBits(a), 1000000000)))); + const _446: rt.Duration = rt.durationFromMillis(1500); + putF32(out, rt.durationDivDurationF32(d, _446)); + putF32(out, rt.durationDivDurationF32({ secs: 0, nanos: 0 }, { secs: 0, nanos: 0 })); + putSecs(out, rt.durationTryFromSecsF32(a)); + putSecs(out, rt.durationTryFromSecsF32(Math.abs(a))); + putSecs(out, rt.durationTryFromSecsF32(Math.fround(Math.abs(a) * 9.999999717180685e-10))); + const _458: rt.u8 = rt.u8Rem(rt.u8Div(kind, 10), 4); + if (_458 === 0) { + putDuration(out, rt.durationFromSecsF32(a)); + } else if (_458 === 1) { + putDuration(out, rt.durationMulF32(d, a)); + } else if (_458 === 2) { + putDuration(out, rt.durationDivF32(d, a)); + } else { + putDuration(out, rt.durationMulF32({ secs: { hi: 0xffffffff, lo: 0xffffffff } as rt.u64, nanos: 999999999 }, b)); + } + } else { + const iter12: rt.Iter = rt.iterSlice([3.4028234663852886e38, -3.4028234663852886e38, 1.1754943508222875e-38, 1.1920928955078125e-7, Infinity, -Infinity, NaN, -0.0, 3.1415927410125732, 6.2831854820251465, 2.7182817459106445, 1.4142135381698608, 0.7071067690849304, 0.6931471824645996, 2.3025851249694824, 1.4426950216293335, 0.4342944920063019, 0.31830987334251404, 9.99994610111476e-41, 0.10000000149011612, 16777216.0]); + while (true) { + const _468: rt.f32 | rt.Done = iter12.next(); + if (_468 === rt.DONE) { + break; + } + putF32(out, _468); + } + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(rt.f32FromBits(4290773283))))); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(rt.f32ToBits(rt.f32FromBits(2139095041))))); + rt.byteVecPush(out, rt.f32IsSignNegative(rt.f32FromBits(4290773283)) ? 1 : 0); + const v2: rt.f32[] = [a, b, 0.10000000149011612]; + putF32(out, rt.iterSum(rt.iterSlice(v2), rt.f32Add, -0.0)); + putF32(out, rt.iterProduct(rt.iterSlice(v2), rt.f32Mul, 1.0)); + const _499: rt.f32[] = []; + putF32(out, rt.iterSum(rt.iterSlice(_499), rt.f32Add, -0.0)); + putF32(out, rt.iterSum(rt.iterSlice([-0.0, -0.0]), rt.f32Add, -0.0)); + putF32(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 _513: rt.f32 = rt.f32Max(g.peak, g.level); + g.peak = _513; + } + putF32(out, g.level); + putF32(out, unwrapOr$19h8ynp(g.peak, -1.0)); + const _526: Gauge.Gauge = Gauge.default$Default(); + rt.byteVecPush(out, Gauge.eq$PartialEq(g, _526) ? 1 : 0); + rt.byteVecPush(out, g.peak === b ? 1 : 0); + let local: rt.f32 = a; + raise(rt.place(() => local, (value2) => local = value2), 0.10000000149011612); + putF32(out, local); + } +} + +/** xorshift32, for the singles and decimals a case makes from its seed. */ +export function step(state: rt.Place): rt.u32 { + state.v = (state.v ^ ((state.v << 13) >>> 0)) >>> 0; + state.v = (state.v ^ (state.v >>> 17)) >>> 0; + state.v = (state.v ^ ((state.v << 5) >>> 0)) >>> 0; + return state.v; +} + +/** + * `f32`'s `Display`, `Debug`, `LowerExp`, and `UpperExp`, with a precision from the byte + * after the single, and every other option. + */ +export function formats(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { + const _6: [rt.f32, rt.ByteSlice] = take(data); + const a: rt.f32 = _6[0]; + const p: rt.usize = unwrapOr$0r2bmpr(rt.byteSliceFirst(_6[1]), 0) as rt.usize; + b0: { + const w: rt.usize = rt.usizeDiv(p, 4); + const _13: rt.u8 = rt.u8Rem(kind, 5); + if (_13 === 0) { + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.f32Display(a, f), (f: rt.Formatter) => rt.f32Debug(a, f), (f: rt.Formatter) => rt.f32LowerExp(a, f), (f: rt.Formatter) => rt.f32UpperExp(a, f), rt.fmtCount(p)]; + put(out, rt.fmtFormat([args2[0], " ", args2[1], " ", args2[2], " ", args2[3], " ", rt.fmtSpec({ precision: rt.fmtCountOf(args2[4]) }, args2[0]), " ", rt.fmtSpec({ precision: rt.fmtCountOf(args2[4]) }, args2[1]), " ", rt.fmtSpec({ precision: rt.fmtCountOf(args2[4]) }, args2[2]), " ", rt.fmtSpec({ precision: rt.fmtCountOf(args2[4]) }, args2[3]), " ", rt.fmtSpec({ plus: true }, args2[0]), " ", rt.fmtSpec({ plus: true }, args2[1]), " ", rt.fmtSpec({ plus: true, precision: rt.fmtCountOf(args2[4]) }, args2[2])])); + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.f32Display(a, f), (f: rt.Formatter) => rt.f32LowerExp(a, f), (f: rt.Formatter) => rt.f32Debug(a, f), (f: rt.Formatter) => rt.f32UpperExp(a, f), rt.fmtCount(w), rt.fmtCount(p)]; + put(out, rt.fmtFormat([rt.fmtSpec({ zero: true, width: 8, precision: 3 }, args4[0]), "|", rt.fmtSpec({ plus: true, zero: true, width: 12 }, args4[1]), "|", rt.fmtSpec({ align: "<", width: 9 }, args4[0]), "|", rt.fmtSpec({ align: "^", width: 12 }, args4[2]), "|", rt.fmtSpec({ fill: "*", align: ">", width: 10, precision: 1 }, args4[0]), "|", rt.fmtSpec({ alternate: true }, args4[2]), "|", rt.fmtSpec({ fill: "-", align: "^", plus: true, width: 15, precision: 2 }, args4[3]), "|", rt.fmtSpec({ zero: true, width: 10 }, args4[0]), "|", rt.fmtSpec({ align: ">", width: rt.fmtCountOf(args4[4]), precision: rt.fmtCountOf(args4[5]) }, args4[0])])); + put(out, rt.fmtToString((f: rt.Formatter) => rt.f32Display(a, f))); + } else if (_13 === 1) { + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.f32Display(a, f), (f: rt.Formatter) => rt.f32LowerExp(a, f), (f: rt.Formatter) => rt.f32Debug(a, f)]; + put(out, rt.fmtFormat([rt.fmtSpec({ precision: 160 }, args6[0]), " ", rt.fmtSpec({ precision: 120 }, args6[1]), " ", rt.fmtSpec({ precision: 160 }, args6[2])])); + } else if (_13 === 2) { + const _72: [rt.f32, rt.ByteSlice] = take(_6[1]); + const b: rt.f32 = _72[0]; + const c: rt.f32 = take(_72[1])[0]; + putF32(out, rt.f32Clamp(a, b, c)); + } else if (_13 === 3) { + const _78: rt.f32[] = [Math.fround(a * 0.5), -0.0, 9.999999747378752e-5, 1.0000000272564224e16, NaN]; + const part = { level: a, history: _78 }; + const args8: rt.Fmt[] = [(f: rt.Formatter) => Reading.fmt$Debug(part, f)]; + put(out, rt.fmtFormat([args8[0], "|", rt.fmtSpec({ precision: 2 }, args8[0]), "|", rt.fmtSpec({ plus: true, zero: true, width: 12 }, args8[0])])); + } else { + let state: rt.u32 = (takeBits(data)[0] | 1) >>> 0; + let iterStart: rt.i32 = 0; + const iterEnd: rt.i32 = 64; + while (iterStart++ < iterEnd) { + const x: rt.f32 = rt.f32FromBits(step(rt.place(() => state, (value) => state = value))); + const args10: rt.Fmt[] = [(f: rt.Formatter) => rt.f32Display(x, f), (f: rt.Formatter) => rt.f32Debug(x, f), (f: rt.Formatter) => rt.f32LowerExp(x, f)]; + put(out, rt.fmtFormat([args10[0], " ", args10[1], " ", args10[2]])); + } + let iterStart2: rt.i32 = 0; + const iterEnd2: rt.i32 = 64; + while (true) { + if (!(iterStart2++ < iterEnd2)) { + break b0; + } + const r2: rt.u32 = step(rt.place(() => state, (value2) => state = value2)); + const x2: rt.f32 = Math.fround(Math.fround(r2 >>> 8) / Math.fround(rt.u32Shl(1, rt.u32Add(rt.u32Rem(r2, 16), 1)))); + const args12: rt.Fmt[] = [(f: rt.Formatter) => rt.f32Debug(x2, f), (f: rt.Formatter) => rt.f32LowerExp(x2, f)]; + put(out, rt.fmtFormat([args12[0], " ", args12[1]])); + } + } + } +} + +/** + * `str::parse::`, with its errors, of the input as text, of bytes mapped into the + * grammar's alphabet, or of decimals made from a seed. + */ +export function parses(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { + let _108!: rt.usize; + b0: { + const _4: rt.u8 = rt.u8Rem(kind, 3); + if (_4 === 0) { + const _6: rt.Result = rt.strFromUtf8(data); + if (_6.ok) { + one(_6.value, out); + } + } else if (_4 === 1) { + let s: string = ""; + for (const b of rt.byteSliceView(data)) { + const _21: rt.ByteSlice = rt.byteSliceFrom(Uint8Array.of(48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 46, 101, 69, 43, 45, 95, 105, 110, 102, 73, 78, 70, 116, 121, 84, 89, 97, 32)); + const _22: rt.usize = rt.usizeRem(b as rt.usize, rt.byteSliceFrom(Uint8Array.of(48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 46, 101, 69, 43, 45, 95, 105, 110, 102, 73, 78, 70, 116, 121, 84, 89, 97, 32)).length); + rt.idx(_21, _22); + s = s + String.fromCodePoint(_21.data[_21.start + _22] as rt.char); + } + one(s, out); + } else { + let state: rt.u32 = (takeBits(data)[0] | 1) >>> 0; + let iterStart: rt.i32 = 0; + const iterEnd: rt.i32 = 16; + while (iterStart++ < iterEnd) { + const x: rt.f32 = rt.f32FromBits((step(rt.place(() => state, (value) => state = value)) & 2147483647) >>> 0); + const half: rt.f64 = (((x as rt.f64)) + ((rt.f32NextUp(x) as rt.f64))) / 2.0; + const p: rt.usize = rt.u32Add(rt.u32Rem(step(rt.place(() => state, (value2) => state = value2)), 30), 8) as rt.usize; + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.f64LowerExp(half, f), rt.fmtCount(p)]; + one(rt.fmtFormat([rt.fmtSpec({ precision: rt.fmtCountOf(args2[1]) }, args2[0])]), out); + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.f64LowerExp(half, f)]; + one(rt.fmtFormat([args4[0]]), out); + } + let iterStart2: rt.i32 = 0; + const iterEnd2: rt.i32 = 16; + while (true) { + if (!(iterStart2++ < iterEnd2)) { + break b0; + } + const r: rt.u32 = step(rt.place(() => state, (value3) => state = value3)); + const len: rt.usize = rt.u32Add(rt.u32Rem(r, 20), 1) as rt.usize; + const point: rt.usize = rt.usizeRem(r >>> 8 as rt.usize, rt.usizeAdd(len, 1)); + let s2: string = ""; + let iterStart3: rt.usize = 0; + const iterEnd3: rt.usize = len; + while ((_108 = iterStart3++) < iterEnd3) { + if (_108 === point) { + s2 = s2 + "."; + } + const _117: rt.char = rt.u8Add(48, rt.u32Rem(step(rt.place(() => state, (value4) => state = value4)), 10) & 0xff) as rt.char; + s2 = s2 + String.fromCodePoint(_117); + } + const _133: rt.i64 = rt.i64Sub(rt.u32Rem(r >>> 16, 100) as rt.i64, 50); + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtPad(f, s2), (f: rt.Formatter) => rt.i64Display(_133, f)]; + one(rt.fmtFormat([args6[0], "e", args6[1]]), out); + } + } + } +} + +/** What parsing `s` makes: the single's bits and its `Debug`, or the error printed. */ +export function one(s: string, out: rt.ByteVec): void { + const _4: rt.Result = rt.f32FromStr(s); + if (_4.ok) { + const v: rt.f32 = _4.value; + putF32(out, v); + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.f32Debug(v, f)]; + put(out, rt.fmtFormat([args2[0]])); + } else { + const e: rt.ParseFloatError = _4.error; + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtParseFloatErrorDisplay(e, f), (f: rt.Formatter) => rt.fmtParseFloatErrorDebug(e, f)]; + put(out, rt.fmtFormat([args4[0], "|", args4[1]])); + } + rt.byteVecPush(out, rt.f32FromStr(s).ok ? 1 : 0); +} + +//# sourceMappingURL=singles.ts.map diff --git a/ts/fixtures/lang/src/harness.rs b/ts/fixtures/lang/src/harness.rs index 3313e4eca..644ad86e0 100644 --- a/ts/fixtures/lang/src/harness.rs +++ b/ts/fixtures/lang/src/harness.rs @@ -4,7 +4,7 @@ use crate::{ arrays, atomics, borrows, bounds, captures, cells, chars, chunks, collections, combinators, counts, cows, defaults, durations, dyns, edits, errors, floats, fmt, generics, hashes, holds, ints, iters, keys, locals, locks, nested, - objects, orders, pointers, refs, shapes, slices, splits, statics, strs, utf8, wrappers, + objects, orders, pointers, refs, shapes, singles, slices, splits, statics, strs, utf8, wrappers, }; /// The native side's entry: [`run`], except that a case starting with 0xff prints a @@ -154,6 +154,9 @@ fn cases() -> Vec<(&'static str, Case)> { ("pointers::made", |_, data, out| pointers::made(data, out)), ("arrays::as_slices", |_, data, out| arrays::as_slices(data, out)), ("ints::widenings", ints::widenings), + ("singles::singles", singles::singles), + ("singles::formats", singles::formats), + ("singles::parses", singles::parses), ] } diff --git a/ts/fixtures/lang/src/lib.rs b/ts/fixtures/lang/src/lib.rs index c1784c0b8..0edc66b9a 100644 --- a/ts/fixtures/lang/src/lib.rs +++ b/ts/fixtures/lang/src/lib.rs @@ -51,6 +51,7 @@ mod orders; mod pointers; mod refs; mod shapes; +mod singles; mod slices; mod splits; mod statics; diff --git a/ts/fixtures/lang/src/singles.rs b/ts/fixtures/lang/src/singles.rs new file mode 100644 index 000000000..358f9a2a7 --- /dev/null +++ b/ts/fixtures/lang/src/singles.rs @@ -0,0 +1,451 @@ +//! `f32`, as `floats` has `f64`: arithmetic, which rounds each result to a single, casts +//! to and from `f64` and every integer width, its methods and constants, sums, places, +//! `Duration`'s `f32` methods, and formatting and parsing. 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::f32::consts; +use std::time::Duration; + +use crate::fmt::put; + +/// The bits of an `f32` from the front of `data`, big-endian and zero-padded. +fn take_bits(data: &[u8]) -> (u32, &[u8]) { + let mut buf = [0u8; 4]; + let n = data.len().min(buf.len()); + buf[..n].copy_from_slice(&data[..n]); + (u32::from_be_bytes(buf), &data[n..]) +} + +/// An `f32` from the front of `data`. +fn take(data: &[u8]) -> (f32, &[u8]) { + let (bits, rest) = take_bits(data); + (f32::from_bits(bits), rest) +} + +/// `x`'s bits, any NaN as `f32::NAN`'s. +fn put_f32(out: &mut Vec, x: f32) { + let x = if x.is_nan() { f32::NAN } else { x }; + out.extend_from_slice(&x.to_bits().to_be_bytes()); +} + +/// `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_f32` made, or its error printed. +fn put_secs(out: &mut Vec, d: Result) { + match d { + Ok(d) => put_duration(out, d), + Err(e) => put(out, &format!("{e}|{e:?}")), + } +} + +/// A struct with an `f32` field, changed in place through a `&mut`. +#[derive(Clone, Copy, PartialEq, Default)] +struct Gauge { + level: f32, + peak: Option, +} + +fn raise(level: &mut f32, by: f32) { + *level += by; + *level *= 2.0; +} + +/// A NaN whose sign and payload a constant keeps, which only its bits make. +const PAYLOAD: f32 = f32::from_bits(0xffc0_0123); +/// A signaling NaN, which JS's conversion from a single would quiet. +const SIGNALING: f32 = f32::from_bits(0x7f80_0001); +const CONSTANTS: [f32; 21] = [ + f32::MAX, + f32::MIN, + f32::MIN_POSITIVE, + f32::EPSILON, + f32::INFINITY, + f32::NEG_INFINITY, + f32::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_1_PI, + 1e-40, + 0.1, + 16_777_216.0, +]; + +pub fn singles(kind: u8, data: &[u8], out: &mut Vec) { + let (a, rest) = take(data); + let (b, rest) = take(rest); + match kind % 10 { + 0 => { + // Arithmetic, each result a single, 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, a * b + a, (a - b) / (a + b)] { + put_f32(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_f32(out, c); + // Through `f64` and back: exact one way, rounded once the other. + let (wa, wb) = (a as f64, f64::from(b)); + for x in [wa, wa * wb, wa / wb, wa + 1e-30] { + put_f64(out, x); + } + for x in [(wa * wb) as f32, (wa / wb) as f32, (wa + 1e-30) as f32, (wa + wb) as f32] { + put_f32(out, x); + } + } + 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 and from `f64`, rounded once to the nearest, ties to + // even, where through the nearest `f64` would round twice. + 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); + let w = f64::from_bits(v as u64); + for x in [ + v as u8 as f32, + v as u16 as f32, + v as u32 as f32, + v as u64 as f32, + v as f32, + v as usize as f32, + v as i8 as f32, + v as i16 as f32, + v as i32 as f32, + v as i64 as f32, + v as i128 as f32, + v as isize as f32, + (v >> 64) as u64 as f32, + (v >> 64) as i64 as f32, + f32::from(v as u16), + f32::from(v as i16), + f32::from(v & 1 == 1), + (v as u8).into(), + w as f32, + u64::MAX as f32, + i64::MIN as f32, + u128::MAX as f32, + i32::MAX as f32, + ] { + put_f32(out, x); + } + // And back, where the round trip saturates. + out.extend_from_slice(&((v as u64) as f32 as u64).to_be_bytes()); + out.extend_from_slice(&((v as i128) as f32 as i128).to_be_bytes()); + out.extend_from_slice(&((v as u32) as f32 as u32).to_be_bytes()); + } + 3 => { + // Rounding, and the methods that round once to a single. + 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_f32(out, x); + } + } + 4 => { + // The sign bit and the class, and the singles 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.5f32.copysign(a), a.next_up(), a.next_down()] { + put_f32(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_f32(out, if zeros { 0.0 } else { x }); + } + for x in [a.midpoint(b), a.div_euclid(b), a.rem_euclid(b), a.powi((b.to_bits() as i32) % 300)] { + put_f32(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 sign and payload kept from the bits they're made of. + let bits = take_bits(data).0; + out.extend_from_slice(&f32::from_bits(bits).to_bits().to_be_bytes()); + out.extend_from_slice(&f32::from_bits(bits).to_be_bytes()); + out.extend_from_slice(&f32::from_bits(bits).to_le_bytes()); + out.extend_from_slice(&f32::from_be_bytes(bits.to_be_bytes()).to_bits().to_be_bytes()); + out.extend_from_slice(&f32::from_le_bytes(bits.to_le_bytes()).to_bits().to_be_bytes()); + out.extend_from_slice(&f32::from_ne_bytes(bits.to_le_bytes()).to_ne_bytes()); + out.push(f32::from_bits(bits).is_sign_negative() as u8); + out.extend_from_slice(&f32::from_bits(bits).copysign(-1.0).to_bits().to_be_bytes()); + if !a.is_nan() { + out.extend_from_slice(&a.to_bits().to_be_bytes()); + } + put_f32(out, f32::from_bits(a.to_bits() ^ 1)); + } + 7 => { + // What std leaves to libm, within the ULPs `caller.ts` allows. + 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.cbrt(), + a.hypot(b), + s, + c, + ] { + put_f32(out, x); + } + } + 8 => { + // `Duration`'s `f32` methods, with their errors and panics. + let d = Duration::new(rest.first().map_or(0, |&s| u64::from(s)) << 20, 123_456_789); + put_f32(out, d.as_secs_f32()); + put_f32(out, Duration::new(u64::from(b.to_bits()), a.to_bits() % 1_000_000_000).as_secs_f32()); + put_f32(out, d.div_duration_f32(Duration::from_millis(1500))); + put_f32(out, Duration::ZERO.div_duration_f32(Duration::ZERO)); + put_secs(out, Duration::try_from_secs_f32(a)); + put_secs(out, Duration::try_from_secs_f32(a.abs())); + put_secs(out, Duration::try_from_secs_f32(a.abs() * 1e-9)); + match kind / 10 % 4 { + 0 => put_duration(out, Duration::from_secs_f32(a)), + 1 => put_duration(out, d.mul_f32(a)), + 2 => put_duration(out, d.div_f32(a)), + _ => put_duration(out, Duration::MAX.mul_f32(b)), + } + } + _ => { + // Constants, sums and products, `Default`, places, and fields. + for x in CONSTANTS { + put_f32(out, x); + } + out.extend_from_slice(&PAYLOAD.to_bits().to_be_bytes()); + out.extend_from_slice(&SIGNALING.to_bits().to_be_bytes()); + out.push(PAYLOAD.is_sign_negative() as u8); + let v = [a, b, 0.1]; + put_f32(out, v.iter().sum()); + put_f32(out, v.into_iter().product()); + put_f32(out, [-0.0f32; 0].iter().sum()); + put_f32(out, [-0.0f32, -0.0].into_iter().sum::()); + put_f32(out, f32::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_f32(out, g.level); + put_f32(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, 0.1); + put_f32(out, local); + } + } +} + +/// A reading whose derived `Debug` writes each `f32` with the placeholder's options. +#[derive(Debug)] +struct Reading { + level: f32, + history: Vec, +} + +/// xorshift32, for the singles and decimals a case makes from its seed. +fn step(state: &mut u32) -> u32 { + *state ^= *state << 13; + *state ^= *state >> 17; + *state ^= *state << 5; + *state +} + +/// `f32`'s `Display`, `Debug`, `LowerExp`, and `UpperExp`, with a precision from the byte +/// after the single, and every other option. +pub fn formats(kind: u8, data: &[u8], out: &mut Vec) { + let (a, rest) = take(data); + let p = usize::from(rest.first().copied().unwrap_or(0)); + let w = p / 4; + match kind % 5 { + 0 => { + put( + out, + &format!("{a} {a:?} {a:e} {a:E} {a:.p$} {a:.p$?} {a:.p$e} {a:.p$E} {a:+} {a:+?} {a:+.p$e}"), + ); + put( + out, + &format!("{a:08.3}|{a:+012e}|{a:<9}|{a:^12?}|{a:*>10.1}|{a:#?}|{a:-^+15.2E}|{a:010}|{a:>w$.p$}"), + ); + put(out, &a.to_string()); + } + 1 => { + // Every digit a single has: a subnormal's run to 149 places. + put(out, &format!("{a:.160} {a:.120e} {a:.160?}")); + } + 2 => { + // The panic prints both bounds with `Debug`. + let (b, rest) = take(rest); + let (c, _) = take(rest); + put_f32(out, a.clamp(b, c)); + } + 3 => { + let r = Reading { + level: a, + history: vec![a * 0.5, -0.0, 1e-4, 1e16, f32::NAN], + }; + put(out, &format!("{r:?}|{r:.2?}|{r:+012?}")); + } + _ => { + // The shortest digits of many singles, of every exponent, then of ones with a + // full significand and a few binary digits past the point, which can end half a + // digit past two as short and as close, where std takes the greater. + let mut state = take_bits(data).0 | 1; + for _ in 0..64 { + let x = f32::from_bits(step(&mut state)); + put(out, &format!("{x} {x:?} {x:e}")); + } + for _ in 0..64 { + let r = step(&mut state); + let x = (r >> 8) as f32 / (1u32 << (r % 16 + 1)) as f32; + put(out, &format!("{x:?} {x:e}")); + } + } + } +} + +/// `str::parse::`, with its errors, of the input as text, of bytes mapped into the +/// grammar's alphabet, or of decimals made from a seed. +pub fn parses(kind: u8, data: &[u8], out: &mut Vec) { + const ALPHABET: &[u8] = b"0123456789012345678901234567890.eE+-_infINFtyTYa "; + match kind % 3 { + 0 => { + if let Ok(s) = std::str::from_utf8(data) { + one(s, out); + } + } + 1 => { + let mut s = String::new(); + for &b in data { + s.push(char::from(ALPHABET[usize::from(b) % ALPHABET.len()])); + } + one(&s, out); + } + _ => { + // Halfway between two singles, which a double holds exactly, written to a few + // digits short of exact and just past: the nearest double of each lands on the + // halfway point, so rounding through one would round twice. + let mut state = take_bits(data).0 | 1; + for _ in 0..16 { + let x = f32::from_bits(step(&mut state) & 0x7fff_ffff); + let half = (f64::from(x) + f64::from(x.next_up())) / 2.0; + let p = (step(&mut state) % 30 + 8) as usize; + one(&format!("{half:.p$e}"), out); + one(&format!("{half:e}"), out); + } + // Up to 20 digits with a point among them, and an exponent within a few of each + // end of a single's range. + for _ in 0..16 { + let r = step(&mut state); + let len = (r % 20 + 1) as usize; + let point = (r >> 8) as usize % (len + 1); + let mut s = String::new(); + for i in 0..len { + if i == point { + s.push('.'); + } + s.push(char::from(b'0' + (step(&mut state) % 10) as u8)); + } + one(&format!("{s}e{}", ((r >> 16) % 100) as i64 - 50), out); + } + } + } +} + +/// What parsing `s` makes: the single's bits and its `Debug`, or the error printed. +fn one(s: &str, out: &mut Vec) { + match s.parse::() { + Ok(v) => { + put_f32(out, v); + put(out, &format!("{v:?}")); + } + Err(e) => put(out, &format!("{e}|{e:?}")), + } + out.push(::from_str(s).is_ok() as u8); +} diff --git a/ts/fixtures/reject/census.md b/ts/fixtures/reject/census.md index 84d19070e..efe0c0bcb 100644 --- a/ts/fixtures/reject/census.md +++ b/ts/fixtures/reject/census.md @@ -85,7 +85,7 @@ The type or module each refused type or path belongs to. | 16 | `(u32, u32)` | fixtures/reject/src/lib.rs:661:10 | | 7 | `Option` | fixtures/reject/src/lib.rs:16:15 | | 7 | `std::borrow::Cow` | fixtures/reject/src/lib.rs:377:2 | -| 6 | `f32` | fixtures/reject/src/lib.rs:7:14 | +| 6 | `f16` | fixtures/reject/src/lib.rs:7:14 | | 5 | `Gap` | fixtures/reject/src/lib.rs:1001:10 | | 5 | `u32` | fixtures/reject/src/lib.rs:608:27 | | 4 | `std::cell::UnsafeCell` | fixtures/reject/src/lib.rs:740:15 | @@ -174,7 +174,7 @@ The type or module each refused type or path belongs to. | 6 | a raw pointer to `_` in a variable assigned again while it may point there | `(u32, u32)` | fixtures/reject/src/lib.rs:669:10 | | 4 | a variant of `_`, which is not supported: either is the same JS string | `std::borrow::Cow` | fixtures/reject/src/lib.rs:377:2 | | 3 | a raw pointer to `_` in `_`, which the crate may put another object in place of | `(u32, u32)`, `Gap` | fixtures/reject/src/lib.rs:1001:10 | -| 3 | unsupported type `_` | `f32` | fixtures/reject/src/lib.rs:7:14 | +| 3 | unsupported type `_` | `f16` | fixtures/reject/src/lib.rs:7:14 | | 2 | `_` is not supported: its `None` and `Some` would both be `rt.None` | `Option` | fixtures/reject/src/lib.rs:16:15 | | 2 | `_`, a trait object of another crate's trait, which is not supported yet | `dyn std::iter::Iterator` | fixtures/reject/src/lib.rs:11:16 | | 2 | `_`, a trait object of the generic trait `_`, which is not supported yet | `dyn Scaled`, `Scaled` | fixtures/reject/src/lib.rs:510:23 | @@ -209,7 +209,7 @@ The type or module each refused type or path belongs to. | 1 | `next_if_eq` of `_` items with a `_`, which is not supported yet | `std::string::String`, `str` | fixtures/reject/src/lib.rs:904:27 | | 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:231: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:182:51 | -| 1 | a cast from `_` to `_` | `u64`, `f32` | fixtures/reject/src/lib.rs:87:2 | +| 1 | a cast from `_` to `_` | `u64`, `f16` | fixtures/reject/src/lib.rs:87:2 | | 1 | a cast from a pointer to `_` to one to `_`, which reads the bytes of one type as another | `u32`, `u8` | fixtures/reject/src/lib.rs:608:27 | | 1 | a coercion to `_` | `std::sync::Arc` | fixtures/reject/src/lib.rs:336:2 | | 1 | a coercion to `_`, which is not supported yet | `&Tail` | fixtures/reject/src/lib.rs:537:30 | @@ -226,14 +226,14 @@ The type or module each refused type or path belongs to. | 1 | a raw pointer to `_` in `_`, which the crate may put another object in place of | `(u32, u32)`, `std::cell::UnsafeCell` | fixtures/reject/src/lib.rs:740:15 | | 1 | a raw pointer to `_` in `_`, which the crate may put another object in place of | `std::cell::UnsafeCell`, `Hits` | fixtures/reject/src/lib.rs:773:21 | | 1 | a raw pointer to `_` in an element, a payload, or a union field, which is not supported yet | `(u32, u32)` | fixtures/reject/src/lib.rs:732:10 | -| 1 | a scalar constant of type `_` | `f32` | fixtures/reject/src/lib.rs:8:2 | +| 1 | a scalar constant of type `_` | `f16` | fixtures/reject/src/lib.rs:8:2 | | 1 | a trait object of `_`, which is not supported yet | `[u8; 4]` | fixtures/reject/src/lib.rs:527:26 | | 1 | a zero-sized constant of type `_` | `{async closure}` | fixtures/reject/src/lib.rs:62:2 | | 1 | an assignment through a reference to `_` | `std::sync::MutexGuard` | fixtures/reject/src/lib.rs:102:2 | | 1 | an iterator of the crate implementing `_`, which is not supported yet | `DoubleEndedIterator` | fixtures/reject/src/lib.rs:169:1 | | 1 | an iterator of the crate implementing `_`, which is not supported yet | `FusedIterator` | fixtures/reject/src/lib.rs:223:1 | | 1 | an upcast from `_` to `_`, which is not supported yet | `dyn Keyed`, `dyn std::any::Any` | fixtures/reject/src/lib.rs:585:32 | -| 1 | arithmetic on `_` | `f32` | fixtures/reject/src/lib.rs:8:2 | +| 1 | arithmetic on `_` | `f16` | 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:36:43 | | 1 | comparing `_` as a key, which is not supported yet | `std::collections::BTreeSet` | fixtures/reject/src/lib.rs:263:75 | | 1 | comparing pointers to `_` in an atomic, which JS has no address of | `u32` | fixtures/reject/src/lib.rs:835:15 | @@ -383,9 +383,9 @@ The type or module each refused type or path belongs to.
unsupported type `_` (3) -- 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:85:51: unsupported type `f32` +- fixtures/reject/src/lib.rs:7:14: unsupported type `f16` +- fixtures/reject/src/lib.rs:7:25: unsupported type `f16` +- fixtures/reject/src/lib.rs:85:51: unsupported type `f16`
@@ -581,7 +581,7 @@ The type or module each refused type or path belongs to.
a cast from `_` to `_` (1) -- fixtures/reject/src/lib.rs:87:2: a cast from `u64` to `f32` +- fixtures/reject/src/lib.rs:87:2: a cast from `u64` to `f16`
@@ -653,7 +653,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 `f32` +- fixtures/reject/src/lib.rs:8:2: a scalar constant of type `f16`
@@ -677,7 +677,7 @@ The type or module each refused type or path belongs to.
arithmetic on `_` (1) -- fixtures/reject/src/lib.rs:8:2: arithmetic on `f32` +- fixtures/reject/src/lib.rs:8:2: arithmetic on `f16`
diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index f183bad35..891158e06 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -3,10 +3,10 @@ no item `missing` in the crate no item `bound_ranges` in the crate a root must not be generic at fixtures/reject/src/lib.rs:85:1 a printable type must implement `Display` or `Debug` at fixtures/reject/src/lib.rs:83:1 -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 `f16` at fixtures/reject/src/lib.rs:7:14 +a scalar constant of type `f16` at fixtures/reject/src/lib.rs:8:2 +arithmetic on `f16` at fixtures/reject/src/lib.rs:8:2 +unsupported type `f16` at fixtures/reject/src/lib.rs:7:25 `dyn std::iter::Iterator`, a trait object of another crate's trait, which is not supported yet at fixtures/reject/src/lib.rs:11:16 `dyn std::iter::Iterator`, a trait object of another crate's trait, which is not supported yet at fixtures/reject/src/lib.rs:12:4 `Option>>` is not supported: its `None` and `Some` would both be `rt.None` at fixtures/reject/src/lib.rs:17:2 diff --git a/ts/fixtures/reject/src/lib.rs b/ts/fixtures/reject/src/lib.rs index 18b45ed73..8ceef80b3 100644 --- a/ts/fixtures/reject/src/lib.rs +++ b/ts/fixtures/reject/src/lib.rs @@ -2,9 +2,9 @@ //! output is written. #![allow(dead_code, reason = "each item is a translation root, not called from Rust")] -#![feature(cow_is_borrowed)] +#![feature(cow_is_borrowed, f16)] -pub fn float(a: f32) -> f32 { +pub fn float(a: f16) -> f16 { a * 2.0 } @@ -82,9 +82,9 @@ pub fn statics() -> u8 { pub struct Meters(pub u64); -pub fn generic>(t: &mut T) -> f32 { +pub fn generic>(t: &mut T) -> f16 { // A census lowers this over `T`: what hinges on `T` waits for an instance, the float does not. - (*t).into() as f32 + (*t).into() as f16 } /// `Ready(Some(Ready(Pending)))` would be `rt.OuterPending`, as the outer `Pending` is. diff --git a/ts/fixtures/stock/src/buf.rs b/ts/fixtures/stock/src/buf.rs index 3ba9eb2d9..c49bf8bed 100644 --- a/ts/fixtures/stock/src/buf.rs +++ b/ts/fixtures/stock/src/buf.rs @@ -100,7 +100,7 @@ impl Checked for B { /// A scalar read off the front of a buffer by `n`'s pick, as a `u128`'s bytes. fn read(buf: &mut B, n: usize) -> u128 { - match n % 14 { + match n % 15 { 0 => buf.get_u8().into(), 1 => buf.get_i8() as u128, 2 => buf.get_u16().into(), @@ -114,6 +114,7 @@ fn read(buf: &mut B, n: usize) -> u128 { 10 => buf.get_uint(n % 10).into(), 11 => buf.get_int_le(n % 10) as u128, 12 => buf.get_f64().to_bits().into(), + 13 => buf.get_f32_le().to_bits().into(), _ => { let mut dst = [0; 3]; buf.copy_to_slice(&mut dst[..n % 4]); @@ -296,7 +297,7 @@ fn write_all(buf: &mut B, data: &[u8]) { data = rest; let (op, n) = (*op, *n); let v = u64::from(n) * 0x0101_0101_0101_0101; - match op % 15 { + match op % 16 { 0 => buf.put_u8(n), 1 => buf.put_i8(n as i8), 2 => buf.put_u16(v as u16), @@ -311,6 +312,7 @@ fn write_all(buf: &mut B, data: &[u8]) { 11 => buf.put_slice(&[op, n]), 12 => buf.put_bytes(n, n as usize % 7), 13 => buf.put(Bytes::from_static(b"put")), + 14 => buf.put_f32(f32::from(n) / 3.0), _ => buf.put_f64(f64::from(n) / 3.0), } } diff --git a/ts/runtime/rt/float.ts b/ts/runtime/rt/float.ts index b2a89d5d4..6208766d0 100644 --- a/ts/runtime/rt/float.ts +++ b/ts/runtime/rt/float.ts @@ -1,14 +1,19 @@ -// `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. +// `f64` and `f32`, for rust2-ts output. An `f64` is a number, which is the same IEEE 754 +// double, so its operators are JS's. An `f32` is a number that a single holds exactly, +// each operation's result rounded to one by `Math.fround`, which rounds as Rust does: a +// double holds the exact sum, product, or quotient of two singles closely enough that +// rounding it again gives the single nearest the exact value. These are their 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. +// engine's: `to_bits` reads what it keeps. An `f32` NaN is the double NaN with its sign +// and its payload at the top, which a signaling one keeps, where JS's conversion from a +// single would quiet it. import { Err, type FmtResult, type Formatter, fmtPadFormatted, fmtToString, Ok, panic, type Result } from "../rt.ts"; import type { Ordering } from "./collections.ts"; -import { type f64, type u64, u64FromHalves } from "./int.ts"; +import { type f32, type f64, type u32, type u64, u64FromHalves } from "./int.ts"; /** 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; @@ -175,61 +180,217 @@ export function f64TotalCmp(a: f64, b: f64): Ordering { /** `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 { +/** The identity, as an `f64` rounds what JS computes. */ +const double = (x: number): number => x; + +/** `powi`, multiplied by squaring as compiler-builtins' `__powidf2` is, each product rounded by `round`. */ +function powi(x: number, n: number, round: (x: number) => number): number { let a = x; let pow = Math.abs(n); let mul = 1; for (;;) { - if (pow & 1) mul *= a; + if (pow & 1) mul = round(mul * a); // `>>>` keeps 2^31, the magnitude of `i32::MIN`, unsigned. pow >>>= 1; if (pow === 0) break; - a *= a; + a = round(a * a); } - return n < 0 ? 1 / mul : mul; + return n < 0 ? round(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::powi`. */ +export const f64Powi = (x: f64, n: number): f64 => powi(x, n, double); + +/** libm's `pow`: 1 for a base of 1, and for -1 to an infinite power, where `Math.pow` is NaN. */ +const pow = (x: number, y: number): number => (x === 1 || (x === -1 && Math.abs(y) === Infinity) ? 1 : Math.pow(x, y)); + +/** `f64::powf`. */ +export const f64Powf = (x: f64, y: f64): f64 => 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)); +/** `atanh`, as std computes it rather than libm, each step rounded by `round`. */ +const atanh = (x: number, round: (x: number) => number): number => round(0.5 * round(Math.log1p(round(round(2 * x) / round(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; +/** `f64::atanh`. */ +export const f64Atanh = (x: f64): f64 => atanh(x, double); + +function divEuclid(x: number, rhs: number, round: (x: number) => number): number { + const q = Math.trunc(round(x / rhs)); + if (x % rhs < 0) return round(rhs > 0 ? q - 1 : q + 1); return q; } -/** `f64::rem_euclid`. */ -export function f64RemEuclid(x: f64, rhs: f64): f64 { +/** `f64::div_euclid`. */ +export const f64DivEuclid = (x: f64, rhs: f64): f64 => divEuclid(x, rhs, double); + +function remEuclid(x: number, rhs: number, round: (x: number) => number): number { const r = x % rhs; - return r < 0 ? r + Math.abs(rhs) : r; + return r < 0 ? round(r + Math.abs(rhs)) : r; } +/** `f64::rem_euclid`. */ +export const f64RemEuclid = (x: f64, rhs: f64): f64 => remEuclid(x, rhs, double); + /** `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; -/** `f64::clamp`, which panics with both bounds' `Debug` unless `min <= max`. */ -export function f64Clamp(x: f64, min: f64, max: f64): f64 { +/** `clamp`, which panics with both bounds' `Debug` unless `min <= max`. */ +function clamp(x: number, min: number, max: number, debug: (x: number, f: Formatter) => FmtResult): number { if (!(min <= max)) { - const debug = (v: f64) => fmtToString((f) => f64Debug(v, f)); - panic(`min > max, or either was NaN. min = ${debug(min)}, max = ${debug(max)}`); + const text = (v: number) => fmtToString((f) => debug(v, f)); + panic(`min > max, or either was NaN. min = ${text(min)}, max = ${text(max)}`); } return x < min ? min : x > max ? max : x; } +/** `f64::clamp`. */ +export const f64Clamp = (x: f64, min: f64, max: f64): f64 => clamp(x, min, max, f64Debug); + +// `f32`'s methods: an `f64`'s where they give the same single, else rounded as std's. + +/** `f32::MIN_POSITIVE`, the least normal single. */ +const MIN_POSITIVE_F32 = 1.1754943508222875e-38; + +/** `f32::round`. */ +export const f32Round: (x: f32) => f32 = f64Round as never; +/** `f32::round_ties_even`. */ +export const f32RoundTiesEven: (x: f32) => f32 = f64RoundTiesEven as never; +/** `f32::fract`. */ +export const f32Fract: (x: f32) => f32 = f64Fract as never; +/** `f32::is_sign_negative`. */ +export const f32IsSignNegative: (x: f32) => boolean = f64IsSignNegative as never; +/** `f32::is_sign_positive`. */ +export const f32IsSignPositive: (x: f32) => boolean = f64IsSignPositive as never; +/** `f32::signum`. */ +export const f32Signum: (x: f32) => f32 = f64Signum as never; +/** `f32::copysign`. */ +export const f32Copysign: (x: f32, y: f32) => f32 = f64Copysign as never; +/** `f32::is_infinite`. */ +export const f32IsInfinite: (x: f32) => boolean = f64IsInfinite as never; +/** `f32::max`. */ +export const f32Max: (a: f32, b: f32) => f32 = f64Max as never; +/** `f32::min`. */ +export const f32Min: (a: f32, b: f32) => f32 = f64Min as never; +/** `f32::total_cmp`, which orders singles' bits as their doubles' order. */ +export const f32TotalCmp: (a: f32, b: f32) => Ordering = f64TotalCmp as never; +/** `f32`'s `PartialOrd::partial_cmp`. */ +export const f32PartialCmp: (a: f32, b: f32) => Ordering | undefined = f64PartialCmp as never; + +/** `f32::is_normal`. */ +export function f32IsNormal(x: f32): boolean { + const a = Math.abs(x); + return a >= MIN_POSITIVE_F32 && a < Infinity; +} + +/** `f32::is_subnormal`. */ +export function f32IsSubnormal(x: f32): boolean { + const a = Math.abs(x); + return a > 0 && a < MIN_POSITIVE_F32; +} + +/** `x`'s bits as a single moved by one toward +infinity if `up`, from a finite or infinite non-NaN. */ +function step32(x: number, up: boolean): number { + if (x === 0) return up ? 1.401298464324817e-45 : -1.401298464324817e-45; + const view = bytes(); + view.setFloat32(0, x); + // The magnitude grows if it moves away from zero. + view.setUint32(0, view.getUint32(0) + (up === x > 0 ? 1 : -1)); + return view.getFloat32(0); +} + +/** `f32::next_up`: the least single greater than `x`. */ +export const f32NextUp = (x: f32): f32 => (x !== x || x === Infinity ? x : step32(x, true)); + +/** `f32::next_down`: the greatest single less than `x`. */ +export const f32NextDown = (x: f32): f32 => (x !== x || x === -Infinity ? x : step32(x, false)); + +/** `f32::midpoint`, which std computes in `f64` on wasm32. */ +export const f32Midpoint = (a: f32, b: f32): f32 => Math.fround((a + b) * 0.5); + +/** `f32::to_bits`: a NaN's sign and payload are the top of its double's. */ +export function f32ToBits(x: f32): u32 { + const view = bytes(); + if (x === x) { + view.setFloat32(0, x); + return view.getUint32(0); + } + view.setFloat64(0, x); + const hi = view.getUint32(0); + return ((hi & 0x80000000) | 0x7f800000 | ((hi & 0xfffff) << 3) | (view.getUint32(4) >>> 29)) >>> 0; +} + +/** `f32::from_bits`: a NaN's sign and payload go to the top of its double's, which `getFloat32` would quiet. */ +export function f32FromBits(v: u32): f32 { + const view = bytes(); + if ((v & 0x7f800000) !== 0x7f800000 || (v & 0x7fffff) === 0) { + view.setUint32(0, v); + return view.getFloat32(0); + } + view.setUint32(0, (v & 0x80000000) | 0x7ff00000 | ((v & 0x7fffff) >>> 3)); + view.setUint32(4, v << 29); + return view.getFloat64(0); +} + +function toBytes32(x: number, little: boolean): Uint8Array { + const out = new Uint8Array(4); + new DataView(out.buffer).setUint32(0, f32ToBits(x), little); + return out; +} + +function fromBytes32(bytes: Uint8Array, little: boolean): number { + return f32FromBits(new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, little)); +} + +/** `f32::to_be_bytes`. */ +export const f32ToBeBytes = (x: f32): Uint8Array => toBytes32(x, false); + +/** `f32::to_le_bytes`, and `to_ne_bytes`, as wasm32 is little-endian. */ +export const f32ToLeBytes = (x: f32): Uint8Array => toBytes32(x, true); + +/** `f32::from_be_bytes`. */ +export const f32FromBeBytes = (bytes: Uint8Array): f32 => fromBytes32(bytes, false); + +/** `f32::from_le_bytes`, and `from_ne_bytes`, as wasm32 is little-endian. */ +export const f32FromLeBytes = (bytes: Uint8Array): f32 => fromBytes32(bytes, true); + +/** `f32::powi`, multiplied by squaring in singles as compiler-builtins' `__powisf2` is. */ +export const f32Powi = (x: f32, n: number): f32 => powi(x, n, Math.fround); + +/** `f32::powf`: libm's `powf`, within its ULPs. */ +export const f32Powf = (x: f32, y: f32): f32 => Math.fround(pow(x, y)); + +/** `f32::sin_cos`. */ +export const f32SinCos = (x: f32): [f32, f32] => [Math.fround(Math.sin(x)), Math.fround(Math.cos(x))]; + +/** `f32::atanh`, as std computes it, each step in singles. */ +export const f32Atanh = (x: f32): f32 => atanh(x, Math.fround); + +/** `f32::div_euclid`. */ +export const f32DivEuclid = (x: f32, rhs: f32): f32 => divEuclid(x, rhs, Math.fround); + +/** `f32::rem_euclid`. */ +export const f32RemEuclid = (x: f32, rhs: f32): f32 => remEuclid(x, rhs, Math.fround); + +/** `f32`'s `+`, which `Iterator::sum` folds with. */ +export const f32Add = (a: f32, b: f32): f32 => Math.fround(a + b); + +/** `f32`'s `*`, which `Iterator::product` folds with. */ +export const f32Mul = (a: f32, b: f32): f32 => Math.fround(a * b); + +/** `f32::clamp`. */ +export const f32Clamp = (x: f32, min: f32, max: f32): f32 => clamp(x, min, max, f32Debug); + // Formatting, as `core::fmt::float` does it. A finite nonzero magnitude is digits and the // power of ten that `0.digits` scales by, as `flt2dec` makes them. +/** A float's shortest digits that read back as it: the digits, and the power of ten that `0.digits` scales by. */ +type Shortest = (x: number) => [string, number]; + /** `x`'s shortest digits that read back as it, the closest to it of those, and the greater of two as close. */ function shortest(x: number): [string, number] { // JS picks the shortest and then the closest too, but the even one of two as close. @@ -244,7 +405,26 @@ function shortest(x: number): [string, number] { // The greater ending in 0 would be shorter, so it can't read back as `x`. if (all !== digits + "5" || at !== exp || digits.endsWith("9")) return [digits, exp]; const up = digits.slice(0, -1) + String.fromCharCode(digits.charCodeAt(digits.length - 1) + 1); - return nearest(up, exp - up.length) === x ? [up, exp] : [digits, exp]; + return nearest(up, exp - up.length, F64) === x ? [up, exp] : [digits, exp]; +} + +/** + * `x`'s shortest digits that read back as the single it is, as Dragon finds them: at the + * first length where the digits cut short of `x` or the next ones up read back, the one + * that does, or the closer of two, the greater if `x` is half a digit past the lesser. + */ +function shortest32(x: number): [string, number] { + const [all, at] = exact(x); + for (let n = 1; n < all.length; n++) { + const down = all.slice(0, n); + const nines = end(down, "9"); + const [up, upAt] = nines === 0 ? ["1", at + 1] : [down.slice(0, nines - 1) + String.fromCharCode(down.charCodeAt(nines - 1) + 1), at]; + const fitsDown = nearest(down, at - n, F32) === x; + const fitsUp = nearest(up, upAt - up.length, F32) === x; + if (fitsUp && (!fitsDown || all[n] >= "5")) return [up, upAt]; + if (fitsDown) return [down, at]; + } + return [all, at]; } /** How many binary digits `x`, finite and nonzero, has past the point. */ @@ -260,7 +440,7 @@ function fractionBits(x: number): number { return Math.max(0, 1075 - Math.max(biased, 1) - zeros); } -/** `x`'s exact digits, without trailing zeros, which a double always has: its significand times a power of two. */ +/** `x`'s exact digits, without trailing zeros, which a double, and so a single, always has: its significand times a power of two. */ function exact(x: number): [string, number] { const view = bytes(); view.setFloat64(0, x); @@ -313,7 +493,7 @@ function exponential(digits: string, exp: number, frac: number, e: "e" | "E"): s const zero = (frac: number): string => (frac > 0 ? `0.${"0".repeat(frac)}` : "0"); /** `to_shortest_str`: the shortest digits, with at least `frac` past the point. */ -function shortestFixed(x: number, frac: number): string { +function shortestFixed(x: number, frac: number, shortest: Shortest): string { if (x === 0) return zero(frac); const [digits, exp] = shortest(x); return decimal(digits, exp, frac); @@ -328,7 +508,7 @@ function exactFixed(x: number, frac: number): string { } /** `to_shortest_exp_str` without a precision, else `to_exact_exp_str`: `precision` digits past the point. */ -function scientific(x: number, precision: number | undefined, e: "e" | "E"): string { +function scientific(x: number, precision: number | undefined, e: "e" | "E", shortest: Shortest): string { if (x === 0) return `${zero(precision ?? 0)}${e}0`; if (precision === undefined) { const [digits, exp] = shortest(x); @@ -340,47 +520,81 @@ function scientific(x: number, precision: number | undefined, e: "e" | "E"): str } /** Write `x` padded, its magnitude by `finite`: a NaN has no sign, and -0.0 has one. */ -function write(x: f64, f: Formatter, finite: (magnitude: number) => string): FmtResult { +function write(x: number, f: Formatter, finite: (magnitude: number) => string): FmtResult { if (x !== x) return fmtPadFormatted(f, "", "NaN"); const sign = f64IsSignNegative(x) ? "-" : f.spec.plus ? "+" : ""; const magnitude = Math.abs(x); return fmtPadFormatted(f, sign, magnitude === Infinity ? "inf" : finite(magnitude)); } -/** `f64`'s `Display`: the shortest digits, or rounded to the precision. */ -export function f64Display(x: f64, f: Formatter): FmtResult { +/** `Display`: the shortest digits, or rounded to the precision. */ +function display(x: number, f: Formatter, shortest: Shortest): FmtResult { const precision = f.spec.precision; - return write(x, f, (m) => (precision === undefined ? shortestFixed(m, 0) : exactFixed(m, precision))); + return write(x, f, (m) => (precision === undefined ? shortestFixed(m, 0, shortest) : exactFixed(m, precision))); } -/** `f64`'s `Debug`: as `Display`, but with a point, and an exponent below 1e-4 and from 1e16. */ -export function f64Debug(x: f64, f: Formatter): FmtResult { +/** `Debug`: as `Display`, but with a point, and an exponent below 1e-4 and from 1e16, each as the type has it. */ +function debug(x: number, f: Formatter, shortest: Shortest, tiny: number, huge: number): FmtResult { const precision = f.spec.precision; return write(x, f, (m) => { if (precision !== undefined) return exactFixed(m, precision); - return (m !== 0 && m < 1e-4) || m >= 1e16 ? scientific(m, undefined, "e") : shortestFixed(m, 1); + return (m !== 0 && m < tiny) || m >= huge ? scientific(m, undefined, "e", shortest) : shortestFixed(m, 1, shortest); }); } +/** `f64`'s `Display`. */ +export const f64Display = (x: f64, f: Formatter): FmtResult => display(x, f, shortest); + +/** `f64`'s `Debug`. */ +export const f64Debug = (x: f64, f: Formatter): FmtResult => debug(x, f, shortest, 1e-4, 1e16); + /** `f64`'s `LowerExp`. */ -export const f64LowerExp = (x: f64, f: Formatter): FmtResult => write(x, f, (m) => scientific(m, f.spec.precision, "e")); +export const f64LowerExp = (x: f64, f: Formatter): FmtResult => write(x, f, (m) => scientific(m, f.spec.precision, "e", shortest)); /** `f64`'s `UpperExp`. */ -export const f64UpperExp = (x: f64, f: Formatter): FmtResult => write(x, f, (m) => scientific(m, f.spec.precision, "E")); +export const f64UpperExp = (x: f64, f: Formatter): FmtResult => write(x, f, (m) => scientific(m, f.spec.precision, "E", shortest)); + +/** `f32`'s `Display`. */ +export const f32Display = (x: f32, f: Formatter): FmtResult => display(x, f, shortest32); + +/** `f32`'s `Debug`, whose bounds are 1e-4 and 1e16 rounded to singles. */ +export const f32Debug = (x: f32, f: Formatter): FmtResult => debug(x, f, shortest32, 0.00009999999747378752, 10000000272564224); + +/** `f32`'s `LowerExp`. */ +export const f32LowerExp = (x: f32, f: Formatter): FmtResult => write(x, f, (m) => scientific(m, f.spec.precision, "e", shortest32)); + +/** `f32`'s `UpperExp`. */ +export const f32UpperExp = (x: f32, f: Formatter): FmtResult => write(x, f, (m) => scientific(m, f.spec.precision, "E", shortest32)); // Parsing, as `core::num::dec2flt` does it. /** `core::num::ParseFloatError`: whether the string was empty or not a float. */ export type ParseFloatError = { readonly kind: FloatErrorKind }; -/** `core::num::FloatErrorKind`: why an `f64` didn't parse, by variant name. */ +/** `core::num::FloatErrorKind`: why a float didn't parse, by variant name. */ export type FloatErrorKind = "Empty" | "Invalid"; /** Every power of ten a double holds exactly. */ const POWERS = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22]; +/** + * A binary float type: its significand's bits, the implicit one's included, and its + * exponent's bias; the most digits and the greatest power of ten Clinger's fast path + * takes exactly; and the digits and exponent past which a decimal is at least 10^`over` + * or less than 10^`under`, both of which round past the type's range. + */ +type Binary = { readonly bits: number; readonly bias: number; readonly digits: number; readonly powers: number; readonly over: number; readonly under: number }; + +const F64: Binary = { bits: 53, bias: 1023, digits: 15, powers: 22, over: 310, under: -324 }; +const F32: Binary = { bits: 24, bias: 127, digits: 7, powers: 10, over: 40, under: -45 }; + /** `str::parse::`: std's grammar, without JS's whitespace, hex, or `Infinity`, rounded to the nearest double, half to even. */ -export function f64FromStr(s: string): Result { +export const f64FromStr = (s: string): Result => fromStr(s, F64); + +/** `str::parse::`: rounded once to the nearest single, never through a double. */ +export const f32FromStr = (s: string): Result => fromStr(s, F32); + +function fromStr(s: string, binary: Binary): Result { if (s === "") return Err({ kind: "Empty" }); const signed = s[0] === "-" || s[0] === "+"; const rest = signed ? s.slice(1) : s; @@ -391,7 +605,7 @@ export function f64FromStr(s: string): Result { // std stops adding exponent digits once past 0x10000. let exp = 0; for (let i = 0; i < digits.length; i++) if (exp < 0x10000) exp = exp * 10 + digits.charCodeAt(i) - 48; - x = nearest(int + frac, (sign === "-" ? -exp : exp) - frac.length); + x = nearest(int + frac, (sign === "-" ? -exp : exp) - frac.length, binary); } else if (/^(?:inf|infinity)$/i.test(rest)) { x = Infinity; } else if (/^nan$/i.test(rest)) { @@ -402,43 +616,54 @@ export function f64FromStr(s: string): Result { return Ok(s[0] === "-" ? -x : x); } -/** The double nearest `digits * 10^exp`, half to even. */ -function nearest(digits: string, exp: number): number { +/** The `binary` float nearest `digits * 10^exp`, half to even. */ +function nearest(digits: string, exp: number, binary: Binary): number { const last = end(digits, "0"); const trimmed = digits.slice(0, last).replace(/^0+/, ""); if (trimmed === "") return 0; exp += digits.length - last; const n = trimmed.length; - // Both operands exact, so the one operation rounds once, as Clinger's fast path does. - if (n <= 15 && Math.abs(exp) <= 22) return exp < 0 ? Number(trimmed) / POWERS[-exp] : Number(trimmed) * POWERS[exp]; - // At least 10^309, or less than half the least subnormal. - if (n + exp > 310) return Infinity; - if (n + exp < -324) return 0; + // Both operands exact in the type, so the one operation rounds once, as Clinger's fast + // path does. A double's quotient or product of two singles, rounded to one, is the + // single nearest the exact value. + if (n <= binary.digits && Math.abs(exp) <= binary.powers) { + const x = exp < 0 ? Number(trimmed) / POWERS[-exp] : Number(trimmed) * POWERS[exp]; + return binary === F32 ? Math.fround(x) : x; + } + if (n + exp > binary.over) return Infinity; + if (n + exp < binary.under) return 0; const big = BigInt(trimmed); - return exp < 0 ? quotient(big, 10n ** BigInt(-exp)) : quotient(big * 10n ** BigInt(exp), 1n); + return exp < 0 ? quotient(big, 10n ** BigInt(-exp), binary) : quotient(big * 10n ** BigInt(exp), 1n, binary); } -/** The double nearest `num / den`, half to even, built from its bits so nothing rounds twice. */ -function quotient(num: bigint, den: bigint): number { - const TWO52 = 1n << 52n; - // The power of two that leaves a 53-bit significand, or the subnormals' one. - let e = Math.max(num.toString(2).length - den.toString(2).length - 53, -1074); +/** The `binary` float nearest `num / den`, half to even, built from its bits so nothing rounds twice. */ +function quotient(num: bigint, den: bigint, binary: Binary): number { + const { bits, bias } = binary; + const implicit = 1n << BigInt(bits - 1); + // The power of two that leaves a full significand, or the subnormals' one. + let e = Math.max(num.toString(2).length - den.toString(2).length - bits, 2 - bias - bits); const divide = (): [bigint, bigint, bigint] => { const [n, d] = e < 0 ? [num << BigInt(-e), den] : [num, den << BigInt(e)]; return [n / d, n % d, d]; }; let [q, r, d] = divide(); - if (q >= TWO52 << 1n) { + if (q >= implicit << 1n) { e++; [q, r, d] = divide(); } if (2n * r > d || (2n * r === d && (q & 1n) === 1n)) q++; - if (q === TWO52 << 1n) { - q = TWO52; + if (q === implicit << 1n) { + q = implicit; e++; } - if (e + 1075 >= 2047) return Infinity; + const biased = e + bias + bits - 1; + if (biased >= 2 * bias + 1) return Infinity; + const field = q < implicit ? q : (BigInt(biased) << BigInt(bits - 1)) | (q - implicit); const view = bytes(); - view.setBigUint64(0, q < TWO52 ? q : (BigInt(e + 1075) << 52n) | (q - TWO52)); + if (binary === F32) { + view.setUint32(0, Number(field)); + return view.getFloat32(0); + } + view.setBigUint64(0, field); return view.getFloat64(0); } diff --git a/ts/runtime/rt/int.ts b/ts/runtime/rt/int.ts index 79e48a1c8..1ff6252c8 100644 --- a/ts/runtime/rt/int.ts +++ b/ts/runtime/rt/int.ts @@ -46,6 +46,8 @@ export type i32 = Num<"i32">; export type isize = Num<"isize">; /** Rust's `f64`: a number, which is the same IEEE 754 double. */ export type f64 = Num<"f64">; +/** Rust's `f32`: a number that is exactly an IEEE 754 single, as `Math.fround` makes one. */ +export type f32 = Num<"f32">; /** Rust's `char`: a Unicode scalar value, which is at most 0x10ffff and no surrogate. */ export type char = Num<"char">; /** @@ -1835,9 +1837,10 @@ export const i128Clamp = (a: i128, min: i128, max: i128): i128 => clampWide(a, m /** `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. +// Casts between integers and floats. An integer of at most 32 bits is already its `f64`; +// a wider one rounds to the nearest, ties to even, once, and so does one to an `f32`. A +// float to an integer truncates toward zero, saturates at the type's bounds, and sends +// NaN to 0; an `f32` does so as the `f64` it is exactly. /** `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 { @@ -1894,3 +1897,25 @@ export const i64ToF64 = (a: i64): f64 => (typeof a === "number" ? (a as number) 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))); + +/** + * `v as f32` past 2^53: its top 53 bits, the last set if any bit below them is, which + * `Math.fround` then rounds once as if from the exact value, where the nearest double + * would round twice. + */ +function wideToF32(v: bigint): f32 { + const m = v < 0n ? -v : v; + const drop = BigInt(Math.max(0, m.toString(2).length - 53)); + const odd = (m >> drop) | (m & ((1n << drop) - 1n) ? 1n : 0n); + const x = Math.fround(Number(odd << drop)); + return v < 0n ? -x : x; +} + +/** `a as f32`: a safe integer is exact as a double, so rounds once. */ +export const u64ToF32 = (a: u64): f32 => (typeof a === "number" ? Math.fround(a) : wideToF32(exact(a))); +/** `a as f32`. */ +export const i64ToF32 = (a: i64): f32 => (typeof a === "number" ? Math.fround(a) : wideToF32(exact(a))); +/** `a as f32`. */ +export const u128ToF32 = (a: u128): f32 => (typeof a === "number" ? Math.fround(a) : wideToF32(exact(a))); +/** `a as f32`. */ +export const i128ToF32 = (a: i128): f32 => (typeof a === "number" ? Math.fround(a) : wideToF32(exact(a))); diff --git a/ts/runtime/rt/time.ts b/ts/runtime/rt/time.ts index 61a1e2b5d..0c429759f 100644 --- a/ts/runtime/rt/time.ts +++ b/ts/runtime/rt/time.ts @@ -9,7 +9,7 @@ import { Err, type FmtResult, type Formatter, fmtDebugStructFields, fmtPad, Ok, import { cmp, type Ordering } from "./collections.ts"; import { type Hasher, hash32, hash64 } from "./hash.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 f32, type f64, type u32, type u64, type u128, u64CheckedAdd, u64CheckedMul, u64CheckedSub, u64Div, u64Eq, u64Of, u64Rem, u64ToF32, 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. */ @@ -268,6 +268,32 @@ export const durationMulF64 = (d: Duration, rhs: f64): Duration => durationFromS /** `Duration::div_f64`: through seconds as an `f64`, as std computes it. */ export const durationDivF64 = (d: Duration, rhs: f64): Duration => durationFromSecsF64(durationAsSecsF64(d) / rhs); +/** `Duration::as_secs_f32`, each step rounded to a single as std's. */ +export function durationAsSecsF32(d: Duration): f32 { + return Math.fround(u64ToF32(d.secs) + Math.fround(Math.fround(d.nanos) / NANOS_PER_SEC)); +} + +/** `Duration::div_duration_f32`: their nanoseconds, each made in singles as std's, divided. */ +export function durationDivDurationF32(a: Duration, b: Duration): f32 { + const nanos = (d: Duration) => Math.fround(Math.fround(u64ToF32(d.secs) * NANOS_PER_SEC) + Math.fround(d.nanos)); + return Math.fround(nanos(a) / nanos(b)); +} + +/** + * `Duration::try_from_secs_f32`, which rounds the exact value to the nearest nanosecond, + * half to even, and errs where `try_from_secs_f64` of the same value does. + */ +export const durationTryFromSecsF32: (secs: f32) => Result = durationTryFromSecsF64 as never; + +/** `Duration::from_secs_f32`, with `from_secs_f64`'s panic. */ +export const durationFromSecsF32: (secs: f32) => Duration = durationFromSecsF64 as never; + +/** `Duration::mul_f32`, which std computes as `mul_f64`. */ +export const durationMulF32: (d: Duration, rhs: f32) => Duration = durationMulF64 as never; + +/** `Duration::div_f32`, which std computes as `div_f64`. */ +export const durationDivF32: (d: Duration, rhs: f32) => Duration = durationDivF64 as never; + 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}`; diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index ef5ffdede..612460351 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -1381,7 +1381,7 @@ pub fn cmp_fn<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, partial: bool, spa 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()), + _ if partial && let Some(float) = super::float::Float::of(ty) => Expr::rt_of(float.name(), "partialCmp"), 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()), diff --git a/ts/src/lower/consts.rs b/ts/src/lower/consts.rs index d480bd9ae..2217659a5 100644 --- a/ts/src/lower/consts.rs +++ b/ts/src/lower/consts.rs @@ -45,8 +45,8 @@ 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())), + _ if let Some(float) = super::float::Float::of(ty) => match leaf(valtree) { + Some(scalar) => float.constant(scalar), None => self.err(span, "a float constant without a value"), }, ty::Char => match leaf(valtree) { diff --git a/ts/src/lower/float.rs b/ts/src/lower/float.rs index 23769fef5..5a5b8f62b 100644 --- a/ts/src/lower/float.rs +++ b/ts/src/lower/float.rs @@ -1,24 +1,83 @@ -//! `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. +//! `f64` and `f32`. An `f64` is a JS number, which is the same IEEE 754 double, so its +//! arithmetic and comparisons are JS's operators, rounded as Rust's are. An `f32` is a +//! number a single holds exactly: each arithmetic result is rounded to one by +//! `Math.fround`, which gives the single nearest the exact value, since a double holds +//! the sum, product, quotient, or square root of singles closely enough. 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_middle::ty::{self, ScalarInt, 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()) +/// A float type JS has a value of. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Float { + F32, + F64, } -/// Whether `ty` is `f64`. The other float types have no JS value yet. +impl Float { + /// `ty`'s, if it is `f32` or `f64`. The other float types have no JS value yet. + pub fn of(ty: Ty<'_>) -> Option { + match ty.kind() { + ty::Float(ty::FloatTy::F32) => Some(Self::F32), + ty::Float(ty::FloatTy::F64) => Some(Self::F64), + _ => None, + } + } + + /// The Rust name, which also prefixes the type's runtime functions: `rt.f32Round`. + pub fn name(self) -> &'static str { + match self { + Self::F32 => "f32", + Self::F64 => "f64", + } + } + + /// The TypeScript type of a value. + pub fn ty(self) -> Type { + Type::Rt(self.name(), Vec::new()) + } + + /// `e`, a number JS computed exactly or as a double, rounded to this type. + pub fn round(self, e: Expr) -> Expr { + match self { + Self::F32 => Expr::ident("Math").member("fround").call(vec![e]), + Self::F64 => e, + } + } + + /// A constant from its bits. + pub fn constant(self, int: ScalarInt) -> Expr { + match self { + Self::F32 => lit32(f32::from_bits(int.to_u32())), + Self::F64 => lit(f64::from_bits(int.to_u64())), + } + } + + /// `v`, which this type holds exactly, as a literal. + pub fn lit(self, v: f64) -> Expr { + match self { + Self::F32 => lit32(v as f32), + Self::F64 => lit(v), + } + } + + /// The type's runtime function `name`, uncalled. + fn func(self, name: &str) -> Expr { + Expr::rt_of(self.name(), name) + } +} + +/// Whether `ty` is a float type JS has a value of. pub fn is(ty: Ty<'_>) -> bool { - matches!(ty.kind(), ty::Float(ty::FloatTy::F64)) + Float::of(ty).is_some() } /// `v` as a literal: the shortest digits that read back as it, as Rust's `Debug` prints them. @@ -40,60 +99,93 @@ pub fn lit(v: f64) -> Expr { } } -/// `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) +/// An `f32` as a literal: the double it is exactly, or a NaN other than `f32::NAN` from its bits. +fn lit32(v: f32) -> Expr { + match v { + _ if v.is_nan() && v.to_bits() != f32::NAN.to_bits() => { + let u32 = Int { + bits: 32, + signed: false, + pointer: false, + }; + Expr::rt("f32FromBits", vec![u32.lit(i128::from(v.to_bits()))]) + } + _ => lit(f64::from(v)), + } +} + +/// `l op r` of two floats, which JS's operators compute as Rust's do, an `f32`'s rounded. +pub fn binary<'tcx>(b: &mut Body<'_, '_, 'tcx>, float: Float, op: BinOp, l: Expr, r: Expr, span: Span) -> Expr { + let rounds = |js| float.round(l.clone().binary(js, r.clone())); + match op { + BinOp::Add => rounds("+"), + BinOp::Sub => rounds("-"), + BinOp::Mul => rounds("*"), + BinOp::Div => rounds("/"), + // Both are C's `fmod`, which is exact, with the dividend's sign. + BinOp::Rem => l.binary("%", r), + BinOp::Eq => l.binary("===", r), + BinOp::Ne => l.binary("!==", r), + BinOp::Lt => l.binary("<", r), + BinOp::Le => l.binary("<=", r), + BinOp::Gt => l.binary(">", r), + BinOp::Ge => l.binary(">=", r), + _ => b.err(span, format!("`{op:?}` on `{}`", float.name())), + } } -pub fn unary<'tcx>(b: &mut Body<'_, '_, 'tcx>, op: UnOp, value: Expr, span: Span) -> Expr { +pub fn unary<'tcx>(b: &mut Body<'_, '_, 'tcx>, float: Float, op: UnOp, value: Expr, span: Span) -> Expr { match op { UnOp::Neg => Expr::unary("-", value), - _ => b.err(span, format!("`{op:?}` on `f64`")), + _ => b.err(span, format!("`{op:?}` on `{}`", float.name())), } } /// `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, + match (Float::of(from), Float::of(to), Int::of(from), Int::of(to)) { + (Some(f), Some(t), ..) if f == t => value.expr, + // Every single is exactly a double. + (Some(Float::F32), Some(Float::F64), ..) => Expr::As(Box::new(value.expr), Float::F64.ty()), + (Some(Float::F64), Some(Float::F32), ..) => Float::F32.round(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()), + (None, Some(t), ..) if from.is_bool() => { + Expr::Cond(Box::new(value.expr), Box::new(t.lit(1.0)), Box::new(t.lit(0.0))) + } + // An integer to the nearest float, rounded once: an integer of at most 32 bits is + // exactly a double, which then rounds to a single once. + (None, Some(t), Some(int), _) => match (value.value, t) { + (Some(c), Float::F64) if int.signed => lit(c as f64), + (Some(c), Float::F64) => lit(c as u128 as f64), + (Some(c), Float::F32) if int.signed => lit32(c as f32), + (Some(c), Float::F32) => lit32(c as u128 as f32), + (None, Float::F64) if int.big() => int.method("toF64", vec![value.expr]), + (None, Float::F32) if int.big() => int.method("toF32", vec![value.expr]), + (None, Float::F64) => Expr::As(Box::new(value.expr), t.ty()), + (None, Float::F32) => Float::F32.round(value.expr), }, - // Truncated, saturated at the type's bounds, and 0 for NaN. - (None, Some(int)) if is(from) => int.method("fromF64", vec![value.expr]), + // Truncated, saturated at the type's bounds, and 0 for NaN, of an `f32` as the double it is. + (Some(f), None, _, Some(int)) => { + let value = match f { + Float::F32 => Expr::As(Box::new(value.expr), Float::F64.ty()), + Float::F64 => value.expr, + }; + int.method("fromF64", vec![value]) + } _ => 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); +/// A call of the float type's own `name`, or `None` if it is not mapped. +pub fn method<'tcx>(b: &mut Body<'_, '_, 'tcx>, float: Float, name: &str, args: Vec, span: Span) -> Option { + let math = |f: &str, args: Vec| float.round(Expr::ident("Math").member(f).call(args)); + let rt = |f: &str, args: Vec| float.func(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), + "floor" | "ceil" | "trunc" | "abs" => Expr::ident("Math").member(name).call(args), + // Correctly rounded, so a single's is its double's rounded once. + "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), @@ -101,16 +193,29 @@ pub fn method<'tcx>(b: &mut Body<'_, '_, 'tcx>, name: &str, args: Vec, spa "exp_m1" => math("expm1", args), "ln_1p" => math("log1p", args), "exp2" => math("pow", vec![Expr::lit("2"), one(args)?]), + // std divides the two `ln`s, each of the type. "log" => { let [x, base] = <[Expr; 2]>::try_from(args).ok()?; - math("log", vec![x]).binary("/", math("log", vec![base])) + float.round(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)), + "recip" => float.round(Expr::lit("1").binary("/", one(args)?)), + // std multiplies by these constants, of the type, rounded once. + "to_degrees" => { + let c = match float { + Float::F32 => lit32(57.295_78_f32), + Float::F64 => lit(180.0 / std::f64::consts::PI), + }; + float.round(one(args)?.binary("*", c)) + } + "to_radians" => { + let c = match float { + Float::F32 => lit32(std::f32::consts::PI / 180.0), + Float::F64 => lit(std::f64::consts::PI / 180.0), + }; + float.round(one(args)?.binary("*", c)) + } "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" @@ -120,7 +225,10 @@ pub fn method<'tcx>(b: &mut Body<'_, '_, 'tcx>, name: &str, args: Vec, spa // 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"), + "mul_add" => b.err( + span, + format!("`{}::mul_add`, which rounds once, where JS would round twice", float.name()), + ), _ => return None, }) } diff --git a/ts/src/lower/fmt.rs b/ts/src/lower/fmt.rs index de431ed6c..cfbec2174 100644 --- a/ts/src/lower/fmt.rs +++ b/ts/src/lower/fmt.rs @@ -238,7 +238,7 @@ pub fn std_impl<'tcx>( }; Int::of(ty)?.method(method, vec![value, f]) } - _ if super::float::is(ty) => { + _ if let Some(float) = super::float::Float::of(ty) => { let method = match tr { Trait::Display => "display", Trait::Debug => "debug", @@ -246,7 +246,7 @@ pub fn std_impl<'tcx>( Trait::UpperExp => "upperExp", _ => return None, }; - Expr::rt_of("f64", method).call(vec![value, f]) + Expr::rt_of(float.name(), method).call(vec![value, f]) } ty::Bool if text => rt("bool", vec![value, f]), ty::Char if tr == Trait::Display => rt("charDisplay", vec![value, f]), diff --git a/ts/src/lower/iter.rs b/ts/src/lower/iter.rs index 865f59926..b10349560 100644 --- a/ts/src/lower/iter.rs +++ b/ts/src/lower/iter.rs @@ -449,7 +449,7 @@ pub fn call<'tcx>( Some(Expr::rt("durationSum", tail(vec![recv.clone()], vec![drops]))) } // 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)?) => { + "sum" | "product" if let Some(float) = super::float::Float::of(targ(1)?) => { let (op, unit) = match name { "sum" => ("add", -0.0), _ => ("mul", 1.0), @@ -457,7 +457,7 @@ pub fn call<'tcx>( let drops = iter_drops(b, self_ty, span); method( name, - tail(vec![Expr::rt_of("f64", op), super::float::lit(unit)], vec![drops]), + tail(vec![Expr::rt_of(float.name(), op), float.lit(unit)], vec![drops]), ) } "sum" | "product" => { diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index d2dff29a8..25391b132 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -1099,8 +1099,13 @@ fn mapping<'tcx>( _ => e, }); } - if owner == "f64" { - return super::float::method(b, &name, args, span); + let float = match owner.as_str() { + "f32" => Some(super::float::Float::F32), + "f64" => Some(super::float::Float::F64), + _ => None, + }; + if let Some(float) = float { + return super::float::method(b, float, &name, args, span); } let self_ty = || gargs.types().next(); @@ -1136,8 +1141,7 @@ fn mapping<'tcx>( { return Some(super::collections::heap_vec(value.member("d"), is_u8(args.type_at(0)))); } - // Each `From` between integers, `char`, and `f64`, or from `bool`, keeps the - // value, as `as` then does. + // Each `From` between integers, `char`, and floats, or from `bool`, is as `as`. 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; @@ -1811,9 +1815,9 @@ fn mapping<'tcx>( } ("Drain", "as_str") => Expr::rt("stringDrainAsStr", args), ("ParseIntError", "kind") => args.pop()?.member("kind"), - // `str::parse::`, by std's grammar rather than `Number`'s. - ("str", "parse") | ("FromStr", "from_str") if super::float::is(targ(gargs, 0)) => { - Expr::rt_of("f64", "fromStr").call(args) + // `str::parse` of a float, by std's grammar rather than `Number`'s. + ("str", "parse") | ("FromStr", "from_str") if let Some(float) = super::float::Float::of(targ(gargs, 0)) => { + Expr::rt_of(float.name(), "fromStr").call(args) } // A `char`'s steps through its chars by `&mut`, which no body of std's may. ("str", "parse") | ("FromStr", "from_str") if targ(gargs, 0).is_char() => Expr::rt("charFromStr", args), @@ -2234,7 +2238,7 @@ pub fn default_value<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span, } ty::Bool => Expr::lit("false"), ty::Int(_) | ty::Uint(_) => Int::of(ty)?.lit(0), - _ if super::float::is(ty) => super::float::lit(0.0), + _ if let Some(float) = super::float::Float::of(ty) => 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/place.rs b/ts/src/lower/mir/place.rs index 120c9f6f7..8dc781421 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -1408,7 +1408,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { ty::RawPtr(..) => self.err(span, format!("a constant of type `{ty}` made from an integer")), 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())), + _ if let Some(float) = crate::lower::float::Float::of(ty) => float.constant(int), _ => match Int::of(ty) { Some(i) => { let size = rustc_abi::Size::from_bits(i.bits); diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index bcd8cbb1a..d77223e28 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -1263,7 +1263,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(), + _ if let Some(float) = float::Float::of(ty) => float.ty(), ty::Char => Type::Rt("char", Vec::new()), ty::Str => Type::Builtin("string"), ty::Never => Type::Builtin("never"), diff --git a/ts/src/lower/num.rs b/ts/src/lower/num.rs index f4458d260..beb8405b1 100644 --- a/ts/src/lower/num.rs +++ b/ts/src/lower/num.rs @@ -218,8 +218,8 @@ 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); + if let Some(float) = float::Float::of(lty) { + return float::binary(b, float, op, l.expr, r.expr, span); } let compare = match op { BinOp::Eq => Some("==="), @@ -344,8 +344,8 @@ 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); + if let Some(float) = float::Float::of(ty) { + return float::unary(b, float, op, value, span); } match (op, ty.kind()) { (UnOp::Not, ty::Bool) => value.not(), diff --git a/ts/src/lower/stock.rs b/ts/src/lower/stock.rs index f56c25cd1..3fb9441e0 100644 --- a/ts/src/lower/stock.rs +++ b/ts/src/lower/stock.rs @@ -113,6 +113,9 @@ const BUF: &[&str] = &[ "get_i128", "get_i128_le", "get_i128_ne", + "get_f32", + "get_f32_le", + "get_f32_ne", "get_f64", "get_f64_le", "get_f64_ne", @@ -157,6 +160,9 @@ const BUF_MUT: &[&str] = &[ "put_i128", "put_i128_le", "put_i128_ne", + "put_f32", + "put_f32_le", + "put_f32_ne", "put_f64", "put_f64_le", "put_f64_ne", @@ -477,6 +483,7 @@ fn buf<'tcx>( let read = rt("ReadInt", vec![this, size, args.next()?, signed, le])?; int.method("fromBeBytes", vec![read]) } + "f32" => Expr::rt("f32FromBeBytes", vec![rt("Read", vec![this, Expr::lit("4"), le])?]), "f64" => Expr::rt("f64FromBeBytes", vec![rt("Read", vec![this, Expr::lit("8"), le])?]), _ => { let int = Int::of(out)?; @@ -532,6 +539,7 @@ fn buf_mut<'tcx>( let be = Int::of(input)?.method("toBeBytes", vec![value]); rt("PutInt", vec![this, be, args.next()?, le]) } + "f32" => rt("PutBytes", vec![this, Expr::rt("f32ToBeBytes", vec![value]), le]), "f64" => rt("PutBytes", vec![this, Expr::rt("f64ToBeBytes", vec![value]), le]), _ => { let int = Int::of(input)?; diff --git a/ts/src/lower/time.rs b/ts/src/lower/time.rs index 2f96fa6ea..5c76fa3a9 100644 --- a/ts/src/lower/time.rs +++ b/ts/src/lower/time.rs @@ -60,6 +60,12 @@ pub fn call<'tcx>( "mul_f64" => "durationMulF64", "div_f64" => "durationDivF64", "div_duration_f64" => "durationDivDurationF64", + "as_secs_f32" => "durationAsSecsF32", + "from_secs_f32" => "durationFromSecsF32", + "try_from_secs_f32" => "durationTryFromSecsF32", + "mul_f32" => "durationMulF32", + "div_f32" => "durationDivF32", + "div_duration_f32" => "durationDivDurationF32", _ => return Ok(None), }, ("Add", "add") if is(0) => "durationAdd", diff --git a/ts/tests/nan.ts b/ts/tests/nan.ts index b25d9927b..d945f54f5 100644 --- a/ts/tests/nan.ts +++ b/ts/tests/nan.ts @@ -26,3 +26,16 @@ for (const bits of [0x7ff8000000000000n, 0xfff8000000000000n, 0x7ff8000000000123 const held = [x, 1.5]; assert.equal(big(rt.f64ToBits(held[0])), bits | QUIET, `${bits.toString(16)} held in an array`); } + +// An `f32` NaN is a double NaN with the single's sign and payload at the top, which a +// signaling one keeps through `from_bits`, `to_bits`, its bytes, and `copysign`. +for (const bits of [0x7fc00000, 0xffc00000, 0x7fc00123, 0xffe0beef, 0x7f800001, 0xffa00abc]) { + const x = rt.f32FromBits(bits); + const negative = bits >>> 31 === 1; + assert.equal(rt.f32ToBits(x), bits, `${bits.toString(16)} round trips`); + assert.equal(rt.f32IsSignNegative(x), negative); + assert.equal(rt.f32ToBits(rt.f32Copysign(x, -1)), (bits | 0x80000000) >>> 0); + assert.equal(rt.f32ToBits(rt.f32Copysign(x, 1)), bits & 0x7fffffff); + assert.equal(rt.f32ToBits(rt.f32FromBeBytes(rt.f32ToBeBytes(x))), bits); + assert.equal(rt.f32ToBits(rt.f32FromLeBytes(rt.f32ToLeBytes(x))), bits); +} diff --git a/ts/tests/runtime.rs b/ts/tests/runtime.rs index 1394c21c5..27993c910 100644 --- a/ts/tests/runtime.rs +++ b/ts/tests/runtime.rs @@ -14,7 +14,7 @@ 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. +/// A NaN keeps its payload and sign through `f64`'s and `f32`'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"); From 0d135d2c1e0da3edcdbc7aa72a25adcf46f12af4 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:08:37 -0700 Subject: [PATCH 3/5] style(ts): format f32 changes; note array quieting of an f32 NaN Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 3 ++- ts/fixtures/lang/src/singles.rs | 44 +++++++++++++++++++++++++++++---- ts/src/lower/float.rs | 5 +++- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/ts/README.md b/ts/README.md index 6762c3c76..438da175b 100644 --- a/ts/README.md +++ b/ts/README.md @@ -176,7 +176,8 @@ does, as `div_euclid`, `powi`, and `atanh` do, and in doubles where std does, as `midpoint` does. A cast to `f64` is the same number, and `f64 as f32` is `Math.fround`; an integer rounds to a single once, never through the nearest double, which could round twice. A NaN keeps its sign and payload at the top of a double NaN's, so `from_bits` and -`to_bits` keep a signaling one, which JS's conversion from a single would quiet. +`to_bits` keep a signaling one, which JS's conversion from a single would quiet, and V8 +quiets one stored in an array as it does an `f64`'s. `Duration`'s `f32` methods are std's: `as_secs_f32` and `div_duration_f32` round each step to a single, and the rest go through `f64` as std's do. diff --git a/ts/fixtures/lang/src/singles.rs b/ts/fixtures/lang/src/singles.rs index 358f9a2a7..e9ecbbee2 100644 --- a/ts/fixtures/lang/src/singles.rs +++ b/ts/fixtures/lang/src/singles.rs @@ -96,10 +96,31 @@ pub fn singles(kind: u8, data: &[u8], out: &mut Vec) { match kind % 10 { 0 => { // Arithmetic, each result a single, 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, a * b + a, (a - b) / (a + b)] { + for x in [ + a + b, + a - b, + a * b, + a / b, + a % b, + -a, + a + 1.5, + 0.1 * a, + a * b + a, + (a - b) / (a + b), + ] { put_f32(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)] { + 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; @@ -114,7 +135,12 @@ pub fn singles(kind: u8, data: &[u8], out: &mut Vec) { for x in [wa, wa * wb, wa / wb, wa + 1e-30] { put_f64(out, x); } - for x in [(wa * wb) as f32, (wa / wb) as f32, (wa + 1e-30) as f32, (wa + wb) as f32] { + for x in [ + (wa * wb) as f32, + (wa / wb) as f32, + (wa + 1e-30) as f32, + (wa + wb) as f32, + ] { put_f32(out, x); } } @@ -216,7 +242,12 @@ pub fn singles(kind: u8, data: &[u8], out: &mut Vec) { for x in [a.max(b), a.min(b)] { put_f32(out, if zeros { 0.0 } else { x }); } - for x in [a.midpoint(b), a.div_euclid(b), a.rem_euclid(b), a.powi((b.to_bits() as i32) % 300)] { + for x in [ + a.midpoint(b), + a.div_euclid(b), + a.rem_euclid(b), + a.powi((b.to_bits() as i32) % 300), + ] { put_f32(out, x); } out.push(a.total_cmp(&b) as i8 as u8); @@ -276,7 +307,10 @@ pub fn singles(kind: u8, data: &[u8], out: &mut Vec) { // `Duration`'s `f32` methods, with their errors and panics. let d = Duration::new(rest.first().map_or(0, |&s| u64::from(s)) << 20, 123_456_789); put_f32(out, d.as_secs_f32()); - put_f32(out, Duration::new(u64::from(b.to_bits()), a.to_bits() % 1_000_000_000).as_secs_f32()); + put_f32( + out, + Duration::new(u64::from(b.to_bits()), a.to_bits() % 1_000_000_000).as_secs_f32(), + ); put_f32(out, d.div_duration_f32(Duration::from_millis(1500))); put_f32(out, Duration::ZERO.div_duration_f32(Duration::ZERO)); put_secs(out, Duration::try_from_secs_f32(a)); diff --git a/ts/src/lower/float.rs b/ts/src/lower/float.rs index 5a5b8f62b..66bf4b5e7 100644 --- a/ts/src/lower/float.rs +++ b/ts/src/lower/float.rs @@ -227,7 +227,10 @@ pub fn method<'tcx>(b: &mut Body<'_, '_, 'tcx>, float: Float, name: &str, args: "from_ne_bytes" => rt("fromLeBytes", args), "mul_add" => b.err( span, - format!("`{}::mul_add`, which rounds once, where JS would round twice", float.name()), + format!( + "`{}::mul_add`, which rounds once, where JS would round twice", + float.name() + ), ), _ => return None, }) From 691c2dc1e665dd19e2739b0ebeb6543ee01f803f Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:14:37 -0700 Subject: [PATCH 4/5] test(ts): snapshot bytes' get_f32 and put_f32 Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/stock/expected/buf.ts | 35 ++++++++++---- .../stock/expected/bytes/buf/buf_impl.ts | 46 +++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/ts/fixtures/stock/expected/buf.ts b/ts/fixtures/stock/expected/buf.ts index 58df91ca6..a285f5530 100644 --- a/ts/fixtures/stock/expected/buf.ts +++ b/ts/fixtures/stock/expected/buf.ts @@ -4,7 +4,7 @@ import * as rt from "./rt.ts"; import * as Ops from "./buf.Ops.ts"; import * as Pieces from "./buf.Pieces.ts"; import * as Provided from "./buf.Provided.ts"; -import { advance$Buf_Bytes, advance$Buf_u8s, copyToBytes$Buf_Bytes, copyToBytes$Buf_u8s, copyToSlice$Buf_Bytes, copyToSlice$Buf_u8s, getF64$Buf_Bytes, getF64$Buf_u8s, getI128$Buf_Bytes, getI128$Buf_u8s, getI16Le$Buf_Bytes, getI16Le$Buf_u8s, getI32$Buf_Bytes, getI32$Buf_u8s, getI64Le$Buf_Bytes, getI64Le$Buf_u8s, getI8$Buf_Bytes, getI8$Buf_u8s, getIntLe$Buf_Bytes, getIntLe$Buf_u8s, getU128Le$Buf_Bytes, getU128Le$Buf_u8s, getU16$Buf_Bytes, getU16$Buf_u8s, getU32Le$Buf_Bytes, getU32Le$Buf_u8s, getU64$Buf_Bytes, getU64$Buf_u8s, getU8$Buf_Bytes, getU8$Buf_u8s, getUint$Buf_Bytes, getUint$Buf_u8s, hasRemaining$Buf_Bytes, hasRemaining$Buf_Bytes$2, hasRemaining$Buf_BytesMut, hasRemaining$Buf_u8s, hasRemaining$Buf_u8s$2, remaining$Buf_Bytes, remaining$Buf_Bytes$2, remaining$Buf_BytesMut, remaining$Buf_u8s, remaining$Buf_u8s$2 } from "./bytes/buf/buf_impl.ts"; +import { advance$Buf_Bytes, advance$Buf_u8s, copyToBytes$Buf_Bytes, copyToBytes$Buf_u8s, copyToSlice$Buf_Bytes, copyToSlice$Buf_u8s, getF32Le$Buf_Bytes, getF32Le$Buf_u8s, getF64$Buf_Bytes, getF64$Buf_u8s, getI128$Buf_Bytes, getI128$Buf_u8s, getI16Le$Buf_Bytes, getI16Le$Buf_u8s, getI32$Buf_Bytes, getI32$Buf_u8s, getI64Le$Buf_Bytes, getI64Le$Buf_u8s, getI8$Buf_Bytes, getI8$Buf_u8s, getIntLe$Buf_Bytes, getIntLe$Buf_u8s, getU128Le$Buf_Bytes, getU128Le$Buf_u8s, getU16$Buf_Bytes, getU16$Buf_u8s, getU32Le$Buf_Bytes, getU32Le$Buf_u8s, getU64$Buf_Bytes, getU64$Buf_u8s, getU8$Buf_Bytes, getU8$Buf_u8s, getUint$Buf_Bytes, getUint$Buf_u8s, hasRemaining$Buf_Bytes, hasRemaining$Buf_Bytes$2, hasRemaining$Buf_BytesMut, hasRemaining$Buf_u8s, hasRemaining$Buf_u8s$2, remaining$Buf_Bytes, remaining$Buf_Bytes$2, remaining$Buf_BytesMut, remaining$Buf_u8s, remaining$Buf_u8s$2 } from "./bytes/buf/buf_impl.ts"; import { hasRemainingMut$BufMut_BytesMut, hasRemainingMut$BufMut_Vec_u8, putI128Le$BufMut_BytesMut, putI128Le$BufMut_Vec_u8, putF64$BufMut_BytesMut, putF64$BufMut_Vec_u8, putI32$BufMut_BytesMut, putI32$BufMut_Vec_u8, putI8$BufMut_BytesMut, putI8$BufMut_Vec_u8, putSlice$BufMut_BytesMut, putSlice$BufMut_Vec_u8, putU16Le$BufMut_BytesMut, putU16Le$BufMut_Vec_u8, putU64$BufMut_BytesMut, putU64$BufMut_Vec_u8, putU8$BufMut_BytesMut, putU8$BufMut_Vec_u8, remainingMut$BufMut_BytesMut, remainingMut$BufMut_Vec_u8 } from "./bytes/buf/buf_mut.ts"; import { then$1y5f3ge } from "./core/bool.ts"; @@ -135,7 +135,7 @@ export function getU8Checked$Checked_BytesMut(self: rt.BytesMut): rt.u8 { /** A scalar read off the front of a buffer by `n`'s pick, as a `u128`'s bytes. */ export function read$Bytes(buf: rt.Place>, n: rt.usize): rt.u128 { - const _3: rt.usize = rt.usizeRem(n, 14); + const _3: rt.usize = rt.usizeRem(n, 15); if (_3 === 0) { return getU8$Buf_Bytes(buf) as rt.u128; } @@ -175,6 +175,9 @@ export function read$Bytes(buf: rt.Place>, n: rt.usize): if (_3 === 12) { return rt.u128Cast(rt.f64ToBits(getF64$Buf_Bytes(buf))); } + if (_3 === 13) { + return rt.f32ToBits(getF32Le$Buf_Bytes(buf)) as rt.u128; + } const dst: Uint8Array = rt.bytes(3); const part = rt.usizeRem(n, 4); copyToSlice$Buf_Bytes(buf, rt.byteSliceRange(rt.byteSliceFrom(dst), 0, part)); @@ -183,7 +186,7 @@ export function read$Bytes(buf: rt.Place>, n: rt.usize): /** A scalar read off the front of a buffer by `n`'s pick, as a `u128`'s bytes. */ export function read$Bytes$2(buf: rt.Place, n: rt.usize): rt.u128 { - const _3: rt.usize = rt.usizeRem(n, 14); + const _3: rt.usize = rt.usizeRem(n, 15); if (_3 === 0) { return ((rt.bytesReadWord(buf, 1, false) as rt.u8)) as rt.u128; } @@ -223,6 +226,9 @@ export function read$Bytes$2(buf: rt.Place, n: rt.usize): rt.u128 if (_3 === 12) { return rt.u128Cast(rt.f64ToBits(rt.f64FromBeBytes(rt.bytesRead(buf, 8, false)))); } + if (_3 === 13) { + return rt.f32ToBits(rt.f32FromBeBytes(rt.bytesRead(buf, 4, true))) as rt.u128; + } const dst: Uint8Array = rt.bytes(3); const part = rt.usizeRem(n, 4); rt.bytesCopyToSlice(buf, rt.byteSliceRange(rt.byteSliceFrom(dst), 0, part)); @@ -231,7 +237,7 @@ export function read$Bytes$2(buf: rt.Place, n: rt.usize): rt.u128 /** A scalar read off the front of a buffer by `n`'s pick, as a `u128`'s bytes. */ export function read$BytesMut(buf: rt.BytesMut, n: rt.usize): rt.u128 { - const _3: rt.usize = rt.usizeRem(n, 14); + const _3: rt.usize = rt.usizeRem(n, 15); if (_3 === 0) { return ((rt.bytesMutReadWord(buf, 1, false) as rt.u8)) as rt.u128; } @@ -271,6 +277,9 @@ export function read$BytesMut(buf: rt.BytesMut, n: rt.usize): rt.u128 { if (_3 === 12) { return rt.u128Cast(rt.f64ToBits(rt.f64FromBeBytes(rt.bytesMutRead(buf, 8, false)))); } + if (_3 === 13) { + return rt.f32ToBits(rt.f32FromBeBytes(rt.bytesMutRead(buf, 4, true))) as rt.u128; + } const dst: Uint8Array = rt.bytes(3); const part = rt.usizeRem(n, 4); rt.bytesMutCopyToSlice(buf, rt.byteSliceRange(rt.byteSliceFrom(dst), 0, part)); @@ -279,7 +288,7 @@ export function read$BytesMut(buf: rt.BytesMut, n: rt.usize): rt.u128 { /** A scalar read off the front of a buffer by `n`'s pick, as a `u128`'s bytes. */ export function read$u8s(buf: rt.Place, n: rt.usize): rt.u128 { - const _3: rt.usize = rt.usizeRem(n, 14); + const _3: rt.usize = rt.usizeRem(n, 15); if (_3 === 0) { return ((rt.bytesReadWord(buf, 1, false) as rt.u8)) as rt.u128; } @@ -319,6 +328,9 @@ export function read$u8s(buf: rt.Place, n: rt.usize): rt.u128 { if (_3 === 12) { return rt.u128Cast(rt.f64ToBits(rt.f64FromBeBytes(rt.bytesRead(buf, 8, false)))); } + if (_3 === 13) { + return rt.f32ToBits(rt.f32FromBeBytes(rt.bytesRead(buf, 4, true))) as rt.u128; + } const dst: Uint8Array = rt.bytes(3); const part = rt.usizeRem(n, 4); rt.bytesCopyToSlice(buf, rt.byteSliceRange(rt.byteSliceFrom(dst), 0, part)); @@ -327,7 +339,7 @@ export function read$u8s(buf: rt.Place, n: rt.usize): rt.u128 { /** A scalar read off the front of a buffer by `n`'s pick, as a `u128`'s bytes. */ export function read$u8s$2(buf: rt.Place>, n: rt.usize): rt.u128 { - const _3: rt.usize = rt.usizeRem(n, 14); + const _3: rt.usize = rt.usizeRem(n, 15); if (_3 === 0) { return getU8$Buf_u8s(buf) as rt.u128; } @@ -367,6 +379,9 @@ export function read$u8s$2(buf: rt.Place>, n: rt.usize): if (_3 === 12) { return rt.u128Cast(rt.f64ToBits(getF64$Buf_u8s(buf))); } + if (_3 === 13) { + return rt.f32ToBits(getF32Le$Buf_u8s(buf)) as rt.u128; + } const dst: Uint8Array = rt.bytes(3); const part = rt.usizeRem(n, 4); copyToSlice$Buf_u8s(buf, rt.byteSliceRange(rt.byteSliceFrom(dst), 0, part)); @@ -650,7 +665,7 @@ export function writeAll$BytesMut(buf: rt.BytesMut, data: rt.ByteSlice): void { const op2: rt.u8 = op; const n2: rt.u8 = n; const v: rt.u64 = rt.u64Mul(n2 as rt.u64, { hi: 0x1010101, lo: 0x1010101 } as rt.u64); - const _18: rt.u8 = rt.u8Rem(op2, 15); + const _18: rt.u8 = rt.u8Rem(op2, 16); if (_18 === 0) { rt.bytesMutPutWord(buf, n2, 1, false); } else if (_18 === 1) { @@ -682,6 +697,8 @@ export function writeAll$BytesMut(buf: rt.BytesMut, data: rt.ByteSlice): void { rt.bytesMutPutFill(buf, n2, rt.usizeRem(n2 as rt.usize, 7)); } else if (_18 === 13) { rt.bytesMutPutStorage(buf, rt.byteSliceFrom(Uint8Array.of(112, 117, 116))); + } else if (_18 === 14) { + rt.bytesMutPutBytes(buf, rt.f32ToBeBytes(Math.fround(Math.fround(n2) / 3.0)), false); } else { rt.bytesMutPutBytes(buf, rt.f64ToBeBytes(((n2 as rt.f64)) / 3.0), false); } @@ -705,7 +722,7 @@ export function writeAll$Vec_u8(buf: rt.ByteVec, data: rt.ByteSlice): void { const op2: rt.u8 = op; const n2: rt.u8 = n; const v: rt.u64 = rt.u64Mul(n2 as rt.u64, { hi: 0x1010101, lo: 0x1010101 } as rt.u64); - const _18: rt.u8 = rt.u8Rem(op2, 15); + const _18: rt.u8 = rt.u8Rem(op2, 16); if (_18 === 0) { rt.byteVecPutWord(buf, n2, 1, false); } else if (_18 === 1) { @@ -737,6 +754,8 @@ export function writeAll$Vec_u8(buf: rt.ByteVec, data: rt.ByteSlice): void { rt.byteVecPutFill(buf, n2, rt.usizeRem(n2 as rt.usize, 7)); } else if (_18 === 13) { rt.byteVecExtendFromSlice(buf, rt.byteSliceFrom(Uint8Array.of(112, 117, 116))); + } else if (_18 === 14) { + rt.byteVecPutBytes(buf, rt.f32ToBeBytes(Math.fround(Math.fround(n2) / 3.0)), false); } else { rt.byteVecPutBytes(buf, rt.f64ToBeBytes(((n2 as rt.f64)) / 3.0), false); } diff --git a/ts/fixtures/stock/expected/bytes/buf/buf_impl.ts b/ts/fixtures/stock/expected/bytes/buf/buf_impl.ts index 474f0c252..b22a0bb0d 100644 --- a/ts/fixtures/stock/expected/bytes/buf/buf_impl.ts +++ b/ts/fixtures/stock/expected/bytes/buf/buf_impl.ts @@ -178,6 +178,52 @@ export function copyToSlice$Buf_u8s(self: rt.Place>, dst: rt.bytesCopyToSlice(self.v, dst); } +/** + * Gets an IEEE754 single-precision (4 bytes) floating point number from + * `self` in little-endian byte order. + * + * The current position is advanced by 4. + * + * # Examples + * + * ``` + * use bytes::Buf; + * + * let mut buf = &b"\x9A\x99\x99\x3F hello"[..]; + * assert_eq!(1.2f32, buf.get_f32_le()); + * ``` + * + * # Panics + * + * This function panics if there is not enough remaining data in `self`. + */ +export function getF32Le$Buf_Bytes(self: rt.Place>): rt.f32 { + return rt.f32FromBeBytes(rt.bytesRead(self.v, 4, true)); +} + +/** + * Gets an IEEE754 single-precision (4 bytes) floating point number from + * `self` in little-endian byte order. + * + * The current position is advanced by 4. + * + * # Examples + * + * ``` + * use bytes::Buf; + * + * let mut buf = &b"\x9A\x99\x99\x3F hello"[..]; + * assert_eq!(1.2f32, buf.get_f32_le()); + * ``` + * + * # Panics + * + * This function panics if there is not enough remaining data in `self`. + */ +export function getF32Le$Buf_u8s(self: rt.Place>): rt.f32 { + return rt.f32FromBeBytes(rt.bytesRead(self.v, 4, true)); +} + /** * Gets an IEEE754 double-precision (8 bytes) floating point number from * `self` in big-endian byte order. From 54af929c701af13904c2c8e8b586bc4be149091b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 30 Oct 2026 11:35:29 -0700 Subject: [PATCH 5/5] test(ts): regenerate the lang harness after merging main Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/expected/harness.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/fixtures/lang/expected/harness.ts b/ts/fixtures/lang/expected/harness.ts index 6efd28499..0801da818 100644 --- a/ts/fixtures/lang/expected/harness.ts +++ b/ts/fixtures/lang/expected/harness.ts @@ -34,6 +34,7 @@ import { orders, bytes as orders_bytes } from "./orders.ts"; import { lists, shared as pointers_shared, places, unchecked, nested as pointers_nested, copies as pointers_copies, sentinels, cells as pointers_cells, rebound, writers, made } from "./pointers.ts"; import { refs, toRefs } from "./refs.ts"; import { copies as shapes_copies, order, discriminants, loops, patterns, uninhabited, closures, library, flows } from "./shapes.ts"; +import { singles, formats as singles_formats, parses as singles_parses } from "./singles.ts"; import { parts, chunked, subslices, drains, ranges as slices_ranges, edits as slices_edits } from "./slices.ts"; import { splits } from "./splits.ts"; import { statics, cells as statics_cells, onces } from "./statics.ts"; @@ -182,7 +183,7 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) asSlices(data66, out67); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["ints::widenings", widenings] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["shapes::uninhabited", (_2167: rt.u8, data67: rt.ByteSlice, out68: rt.ByteVec): void => { uninhabited(data67, out68); - }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["keys::lent_keys", lentKeys] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["keys::lent_values", lentValues] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void]]; + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["keys::lent_keys", lentKeys] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["keys::lent_values", lentValues] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["singles::singles", singles] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["singles::formats", singles_formats] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void], ["singles::parses", singles_parses] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => void]]; } /** Run one case. */