Skip to content

feat(ts): translate f64 and Duration's f64 methods - #165

Merged
kixelated merged 9 commits into
mainfrom
quest/m2/ts-model/floats
Oct 28, 2026
Merged

kixelated merged 9 commits into
mainfrom
quest/m2/ts-model/floats

Conversation

@kixelated

@kixelated kixelated commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

rust2-ts refused every f64, so moq-net's Bandwidth::scaled, its probe thresholds, and Duration::mul_f64 jitter couldn't translate: 26 of moq-net's census refusals were floats.

Approach

This lands the first independently landable piece of floats. The quest is rewritten to what remains: formatting, str::parse, and clamp.

  • An f64 is a JS number (rt.f64, branded like the integers), so + - * / % and comparisons are JS's operators, which round as Rust's do. Constants print as their shortest round-trip digits (Infinity and -0.0 included); a NaN other than f64::NAN is made from its bits, keeping its sign and payload.
  • as casts run both ways for every integer width. To an integer, a cast truncates, saturates, and sends NaN to 0 (rt.u8FromF64...). From a 64- or 128-bit integer it rounds once (rt.u64ToF64...). From<u32/i16/bool/...> for f64 works too.
  • Where JS differs from std, the methods are in the new rt/float.ts: round halves away from zero, round_ties_even, max/min ignore a NaN, signum/copysign/is_sign_* read the sign bit, next_up/next_down, midpoint, div_euclid/rem_euclid, total_cmp, partial_cmp (also inside Options and tuples), to_bits/from_bits, and the byte conversions. powi is ported from compiler-builtins' __powidf2, and the tests find it bit-exact.
  • The functions std leaves to libm map to Math.*. powf keeps libm's pow(1, NaN) = 1 and pow(-1, ±inf) = 1. atanh uses std's own formula. mul_add is refused with its span.
  • Duration's as_secs_f64, from_secs_f64, try_from_secs_f64, mul_f64, div_f64, and div_duration_f64 port std's integer try_from_secs! algorithm (via bigint), with its panics and its TryFromFloatSecsError (Display/Debug).
  • Iterator::sum/product over f64 start from std's -0.0/1.0. Default, &mut f64 places, and Option<f64> equality work.
  • Math, Number, NaN, Infinity, and String join the globals that names escape, since the output now calls them.

Testing:

  • The lang fixture gains floats.rs: 10 kinds over random bits plus about 22k named edge cases (each side of halves, 2^52/2^53, every type's saturation bounds, subnormals, infinities, NaNs, nanosecond ties, u64::MAX seconds, and integers whose f64 rounds).
  • For libm results (kind 7), differential.ts takes an optional within hook from the caller, and caller.ts accepts ≤ 2 ULPs. Over 100k inputs in Node, every function measured within 2 ULPs of native wasm (hypot and tanh at 2; most 0 or 1; powi exact).
  • tests/nan.ts (Node, in just test) checks NaN payloads and sign through from_bits/to_bits, copysign, and the sign tests, and pins that V8 quiets a signaling NaN stored in a double array. I ran the same check once by hand in headless Chrome 155, with the same result. CI has no Chrome job for it.

Census, moq at the pinned revision:

Census Before After
moq-net 9,052 9,021
model 3,143 3,143
kio 327 327

moq-net's only float refusal left is rand::RngExt::random::<f64> (ietf/publisher.rs:599), which #147's rand mapping covers. The model's roots don't reach Bandwidth::scaled, so its census is unchanged.

Impact

  • Output: f64 values are rt.f64 (a branded number). New runtime exports: rt.f64* (rt/float.ts), rt.<int>FromF64/rt.<wide>ToF64, rt.duration*F64, rt.TryFromFloatSecsError, and its fmt* functions. Nothing breaks, so there is no MIGRATE entry.
  • A crate item named Math, Number, NaN, Infinity, or String is now emitted with a trailing _.

Choices made without asking

  • Split: formatting (Display/Debug/precision/{:e}), str::parse::<f64>, and f64::clamp remain in the rewritten quest. clamp's panic prints its bounds with Debug, so it waits for formatting. Printing an f64 is refused with its span, as reject shows.
  • ULP bound: 2, from measurement in Node, stated in the README.
  • max/min on ±0.0: std leaves the result unspecified, so the test normalizes it. The runtime returns Math.max/Math.min's choice.
  • classify (FpCategory): not mapped yet, for lack of a consumer. It refuses as an unmapped call.

Alternatives

  • An exact libm port instead of Math.*: rejected by the plan decided on #135.
  • try_from_secs_f64 without bigint (double-double splitting): bigint mirrors std's algorithm line for line, and this isn't a hot path.

Follow-ups

  • Float formatting (rewritten here). It has no moq-net consumer, so it may belong in m3 beside f32; worth a /quest-audit look.
  • A Chrome job for tests/nan.ts and the libm ULP bound, if browser coverage in CI is wanted.

🤖 Generated with Claude Code

(written by Claude Opus 5.5)

kixelated and others added 4 commits October 28, 2026 00:57
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Floats feat(ts): translate f64 and Duration's f64 methods Oct 28, 2026
@kixelated
kixelated marked this pull request as ready for review October 28, 2026 08:32
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T11:49:48.709882Z 998d48d New commits
🔒 Security Review ✅ Completed 2026-10-28T11:49:32.181560Z 998d48d New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c1fb65b4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ts/src/lower/float.rs
/// `v` as a literal: the shortest digits that read back as it, as Rust's `Debug` prints them.
pub fn lit(v: f64) -> Expr {
match v {
_ if v.is_nan() => Expr::lit("NaN"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve explicit NaN bits when lowering float constants

For a constant such as const X: f64 = f64::from_bits(0xfff8000000000123), this branch discards the specified sign and payload and emits plain NaN. Both constant-lowering paths call this helper, so returning X and subsequently calling to_bits() produces 0x7ff8000000000000 in Node instead of Rust's 0xfff8000000000123; is_sign_negative() also changes from true to false. This is not an engine choosing a NaN after arithmetic: the runtime's existing f64FromBits preserves this exact value in Node. Emit a bit-preserving construction for NaN constants and cover constant lowering in the differential tests.

AGENTS.md reference: AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

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.

Fixed in 734a454: a NaN constant other than f64::NAN lowers to rt.f64FromBits(<its bits>), and floats.rs writes f64::from_bits(0xfff8_0000_0000_0123) as a const's raw bits and sign, with no NaN normalization, against native.

(written by Claude Opus 5.5)

@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: 9c1fb65

One actionable correctness issue: NaN constants lose their explicit bits during lowering (inline). No earlier review findings to reconcile.

Overall direction: the scoped f64 support is sensible; separate runtime helpers, explicit refusal of fused operations/formatting, and boundary-oriented differential fixtures keep the complexity justified. No additional actionable issue found in the numeric casts or Duration algorithms inspected.

Verification: static inspection of lowering, runtime, generated fixtures, and tests; no build or tests executed. The documented engine-level NaN limitations remain separate from the compiler-side loss identified here.

Comment thread ts/src/lower/float.rs
pub fn lit(v: f64) -> Expr {
match v {
_ if v.is_nan() => Expr::lit("NaN"),
f64::INFINITY => Expr::lit("Infinity"),

@kixelated kixelated Oct 28, 2026 •

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.

[P2] Explicit NaN constants lose their sign/payload

Independently confirmed the same issue as #165 (comment), which appeared between my final review-state check and submission. Please treat that thread as the primary finding rather than this as a separate issue. The fix should preserve v.to_bits() when lowering a NaN constant, with a translated constant regression that checks bits/sign without NaN normalization.

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.

Fixed in 734a454: a NaN constant other than f64::NAN lowers to rt.f64FromBits(<its bits>), and floats.rs writes f64::from_bits(0xfff8_0000_0000_0123) as a const's raw bits and sign, with no NaN normalization, against native.

(written by Claude Opus 5.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: 734a454

Reviewed the one-commit update since 9c1fb65; the base is unchanged. The prior P2 NaN-constant sign/payload finding (also independently reported in #165 (comment)) is fixed: noncanonical NaNs now lower through their exact bits, and the generated regression exercises raw bits/sign without NaN normalization.

No new actionable bugs found in this delta. Direction: the narrow fix reuses the existing runtime primitive and adds a focused regression, without unnecessary complexity.

Verification: static inspection of the changed lowering and Rust/generated-TypeScript fixture; no build or tests executed.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging. Since the last review I merged main three times, for #124, #146, #159, and #166:

  • The lang harness's op numbers collided. floats is now op 58 (% 59), after skips 54, wrappers 55, edits 56, and utf8 57. caller.ts's cases and the within hook follow it.
  • Snapshots were regenerated. floats.ts now reads partial_cmp through rt.f64PartialCmp, from feat(ts): translate Bound, Reverse, NonZero, and Vec's editing and sorting methods #124's comparison lowering. The census matches the PR body.
  • In ts/README.md, the runtime prose keeps both edits.

Codex reviewed head 998d48d with no findings. CI passed. Locally, just check, the ts tests (18/18), and quest check --base origin/main pass.

(written by Claude Opus 5.5)

@kixelated
kixelated merged commit 5e6e7db into main Oct 28, 2026
1 check passed
@kixelated
kixelated deleted the quest/m2/ts-model/floats branch October 28, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant