Skip to content

feat(ts): translate f32 as f64 - #296

Merged
kixelated merged 7 commits into
mainfrom
quest/m3/ts-f32
Oct 30, 2026
Merged

kixelated merged 7 commits into
mainfrom
quest/m3/ts-f32

Conversation

@kixelated

@kixelated kixelated commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

rust2-ts refused every f32: the type, its constants, arithmetic, casts, methods, formatting, parsing, and Duration's f32 methods. Completes quest/m3/ts-f32.md.

Approach

Reuses #165 and #260's design: a JS number, with the runtime holding std's algorithms where JS differs.

  • Value: an f32 is a number a single holds exactly, typed rt.f32. float.rs now has a Float kind (F32/F64) every call site dispatches on.
  • Arithmetic: + - * / become Math.fround(l op r). A double holds 53 bits, at least 2 * 24 + 2, so rounding the double result again gives the single nearest the exact value (the same holds for sqrt). % (exact fmod), comparisons, and negation need no rounding.
  • Casts: f32 as f64 keeps the number; f64 as f32 is Math.fround. An integer of at most 32 bits is exactly a double, then Math.fround rounds it once. 64- and 128-bit integers go through new rt.u64ToF32 etc.: round to odd at 53 bits, then Math.fround, so the rounding happens once (2^60 + 2^36 + 1 gives 2^60 + 2^37, where the nearest double would give 2^60). Float to integer reuses fromF64 on the exact double.
  • Methods: rounded or exact as std computes them on wasm32: div_euclid, rem_euclid, powi (__powisf2, rounding each product to a single), and atanh work in singles; midpoint works in doubles, as std does on wasm32; to_degrees and to_radians multiply by std's f32 constants; libm's functions are Math.fround(Math.f(x)). Where an f64 method gives the same single (round, signum, max, total_cmp, ...), f32X aliases it. mul_add is still refused with a span, as for f64.
  • Bits and NaN: an f32 NaN is the double NaN with the single's sign and payload in its top bits. f32::from_bits and to_bits build and read that layout by hand, because getFloat32 would quiet a signaling NaN. As with f64, V8 still quiets a signaling NaN stored in an array.
  • Formatting: shortest32 follows Dragon: at the first length where the truncated digits or the next digits up read back as the single, take the one that does; if both do, take the closer, or the greater on a tie. Precision formatting reuses the exact expansion, which is the same for the single and its double. Debug switches to an exponent at 1e-4f32 and 1e16f32, as core compares in f32.
  • Parsing: nearest and quotient take a Binary format. f32FromStr uses Clinger's fast path for up to 7 digits and |exp| <= 10 (a double result of two singles, rounded once). Otherwise it divides exactly with BigInt and builds the single's bits directly, so it never rounds through a double.
  • Duration: as_secs_f32 and div_duration_f32 round each step to a single, as std does. try_from_secs_f32 and from_secs_f32 share f64's runtime: std's integer algorithm rounds the exact value to the nearest nanosecond either way, so the results are identical. mul_f32 and div_f32 are mul_f64 and div_f64 in std too.
  • bytes' get_f32*/put_f32* map as their f64 counterparts do.
  • The reject fixture's unsupported-float roots move to f16. The feature goes on the existing #![feature] line, so no other span shifts.

Tests:

  • New lang differential case singles (native wasm32 vs translated), with singles::singles kinds 0 to 9, singles::formats, and singles::parses. Named operands: each side of 2^23, 2^24, and each integer bound; f32::MAX; normal/subnormal edges; the Debug switch points; a signaling NaN; a payload NaN.
  • Integers just past a single's halfway point after 2^53.
  • Parse texts within a hair of halfway between two singles, plus seeded halfway points written short of exact.
  • Random bits through the harness's random cases, and 64 random singles plus 64 tie-prone singles per seeded format case.
  • tests/nan.ts checks the f32 NaN layout.
  • Mutations are caught: dropping Math.fround from arithmetic fails the snapshot. Parsing f32 through f64 gives 106 differential differences, and taking the lesser on a shortest-digits tie gives 195.
  • One-off sweep, not checked in: every 997th bit pattern (4,307,891 singles), with Display, Debug, {:e}, and parsing the {:e} of the double halfway above each, against native Rust. 0 differences.

Census (just ts census at the pin, origin/main vs this branch): moq-net 1294 → 1279 refusals, with nothing added. The 15 removed are the 12 in bytes' get_f32* and 3 f32 types in rs/moq-net/src/transport/boxed.rs. Model 387 and kio 132 are unchanged. No benchmark: moq's source does no f32 arithmetic, and neither the codec fixture nor the task fixture touches f32.

Impact

  • Translator output: f32 translates as rt.f32, where every use was refused before, so nothing breaks.
  • Runtime: new f32 type, f32* methods, f32Display/Debug/LowerExp/UpperExp, f32FromStr, f32Clamp, f32Add/f32Mul, f32PartialCmp, u64ToF32/i64ToF32/u128ToF32/i128ToF32, and durationAsSecsF32, durationDivDurationF32, durationTryFromSecsF32, durationFromSecsF32, durationMulF32, durationDivF32. Existing f64 exports are unchanged.

Alternatives

  • Translating core's flt2dec/dec2flt MIR: blocked for the same reasons as feat(ts): format and parse f64 as std does #260 (unsafe code, &mut writes, a bignum the engine already has as BigInt).
  • Shortest digits via toPrecision plus a readback: still needs an exact single parse, and at powers of two the closest candidate can fail while the farther one reads back. The exact expansion is simpler and always correct.
  • Parsing through f64FromStr then Math.fround: rounds twice (the mutation above).
  • A Float32Array or DataView per value: no benefit over a number, and getFloat32 quiets signaling NaNs.

Choices made without asking

  • mul_add stays refused for f32, matching f64. A single's FMA could be done exactly (TwoSum plus round to odd), but that is new scope for both types.
  • try_from_secs_f32, from_secs_f32, mul_f32, and div_f32 map to the f64 runtime functions, not separate ports. The reasoning is above, and the differential covers them.
  • The reject fixture's float roots now use f16, which stays refused, so the fixture keeps its "unsupported type" and census-over-generics cases.

Follow-ups

  • Suggested quest: an integer's to_ne_bytes (and from_ne_bytes) has no mapping, though to_le_bytes does. I found this while writing the fixture, which uses to_le_bytes instead. No quest covers it yet; it's left for /quest-plan.
  • Suggested quest: an exact mul_add for f32 and f64 (see the choices above).

Review

Codex (quest review), gpt-6.1-sol, medium effort. Round 2 reviewed head 2187f1045c546ec0e49ebd26dcc5574769adc7fa and reported no findings.

just ci passed locally on 2187f104 inside nix develop. The merge of origin/main (9169909e) conflicted only in the lang harness case lists and generated files. I kept main's cases first, then this PR's, and regenerated the snapshots.

Merge of main at cecbf0d7 (#295) into 2187f104, as 54af929c: ordered main's keys::lent_* cases before singles::* in the case list and regenerated harness.ts (the new cases only). Recorded skip, too trivial to review; just ci passed on 54af929c.

🤖 Generated with Claude Code

(written by Claude Opus 5.5)

kixelated and others added 5 commits October 30, 2026 10:32
Quest-Claim: 82ac1b971f0533f4
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ts/fixtures/lang/caller.ts
#	ts/fixtures/lang/expected/harness.ts
#	ts/fixtures/lang/src/harness.rs
#	ts/fixtures/reject/census.md
@kixelated kixelated changed the title f32 feat(ts): translate f32 as f64 Oct 30, 2026
@kixelated
kixelated marked this pull request as ready for review October 30, 2026 18:31
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 44 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5bd28c55-2ce4-4336-b5dd-45057c179c51
Commits

Reviewing files that changed from the base of the PR and between cecbf0d and 54af929.

Files selected for processing: 35
  • quest/m3/README.md
  • quest/m3/ts-f32.md
  • quest/m3/ts-unsafe.md
  • ts/README.md
  • ts/fixtures/lang/caller.ts
  • ts/fixtures/lang/expected/core/option.ts
  • ts/fixtures/lang/expected/harness.ts
  • ts/fixtures/lang/expected/singles.Gauge.ts
  • ts/fixtures/lang/expected/singles.Reading.ts
  • ts/fixtures/lang/expected/singles.ts
  • ts/fixtures/lang/src/harness.rs
  • ts/fixtures/lang/src/lib.rs
  • ts/fixtures/lang/src/singles.rs
  • ts/fixtures/reject/census.md
  • ts/fixtures/reject/expected.txt
  • ts/fixtures/reject/src/lib.rs
  • ts/fixtures/stock/expected/buf.ts
  • ts/fixtures/stock/expected/bytes/buf/buf_impl.ts
  • ts/fixtures/stock/src/buf.rs
  • ts/runtime/rt/float.ts
  • ts/runtime/rt/int.ts
  • ts/runtime/rt/time.ts
  • ts/src/lower/collections.rs
  • ts/src/lower/consts.rs
  • ts/src/lower/float.rs
  • ts/src/lower/fmt.rs
  • ts/src/lower/iter.rs
  • ts/src/lower/library.rs
  • ts/src/lower/mir/place.rs
  • ts/src/lower/mod.rs
  • ts/src/lower/num.rs
  • ts/src/lower/stock.rs
  • ts/src/lower/time.rs
  • ts/tests/nan.ts
  • ts/tests/runtime.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

# Conflicts:
#	ts/fixtures/lang/expected/harness.ts
#	ts/fixtures/lang/src/harness.rs

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 2187f10

No actionable bugs found in the f32 lowering/runtime changes. The shared Float dispatch and format-parameterized parsing keep the implementation coherent; explicit single rounding, round-to-odd wide-integer conversion, and separate shortest-single formatting address the important differences from f64 without duplicating the entire runtime. The checked-in differential cases cover arithmetic, casts, formatting/parsing boundaries, NaN layout, and Duration integration.

Verification: static review of the changes and relevant runtime code only. I did not run tests, builds, the reported exhaustive sweep, or performance measurements; the PR's reported results were not independently verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant