Repository navigation
feat(ts): translate f64 and Duration's f64 methods - #165
Conversation
Quest-Claim: 2ce0a055a10071e6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
| /// `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"), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
| pub fn lit(v: f64) -> Expr { | ||
| match v { | ||
| _ if v.is_nan() => Expr::lit("NaN"), | ||
| f64::INFINITY => Expr::lit("Infinity"), |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
|
Merging. Since the last review I merged main three times, for #124, #146, #159, and #166:
Codex reviewed head 998d48d with no findings. CI passed. Locally, (written by Claude Opus 5.5) |
Problem
rust2-ts refused every
f64, so moq-net'sBandwidth::scaled, its probe thresholds, andDuration::mul_f64jitter 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, andclamp.f64is 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 (Infinityand-0.0included); a NaN other thanf64::NANis made from its bits, keeping its sign and payload.ascasts 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 f64works too.rt/float.ts:roundhalves away from zero,round_ties_even,max/minignore a NaN,signum/copysign/is_sign_*read the sign bit,next_up/next_down,midpoint,div_euclid/rem_euclid,total_cmp,partial_cmp(also insideOptions and tuples),to_bits/from_bits, and the byte conversions.powiis ported from compiler-builtins'__powidf2, and the tests find it bit-exact.Math.*.powfkeeps libm'spow(1, NaN) = 1andpow(-1, ±inf) = 1.atanhuses std's own formula.mul_addis refused with its span.Duration'sas_secs_f64,from_secs_f64,try_from_secs_f64,mul_f64,div_f64, anddiv_duration_f64port std's integertry_from_secs!algorithm (via bigint), with its panics and itsTryFromFloatSecsError(Display/Debug).Iterator::sum/productoverf64start from std's-0.0/1.0.Default,&mut f64places, andOption<f64>equality work.Math,Number,NaN,Infinity, andStringjoin the globals that names escape, since the output now calls them.Testing:
langfixture gainsfloats.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::MAXseconds, and integers whosef64rounds).differential.tstakes an optionalwithinhook from the caller, andcaller.tsaccepts ≤ 2 ULPs. Over 100k inputs in Node, every function measured within 2 ULPs of native wasm (hypotandtanhat 2; most 0 or 1;powiexact).tests/nan.ts(Node, injust test) checks NaN payloads and sign throughfrom_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:
moq-net's only float refusal left is
rand::RngExt::random::<f64>(ietf/publisher.rs:599), which #147'srandmapping covers. The model's roots don't reachBandwidth::scaled, so its census is unchanged.Impact
f64values arert.f64(a brandednumber). New runtime exports:rt.f64*(rt/float.ts),rt.<int>FromF64/rt.<wide>ToF64,rt.duration*F64,rt.TryFromFloatSecsError, and itsfmt*functions. Nothing breaks, so there is no MIGRATE entry.Math,Number,NaN,Infinity, orStringis now emitted with a trailing_.Choices made without asking
Display/Debug/precision/{:e}),str::parse::<f64>, andf64::clampremain in the rewritten quest.clamp's panic prints its bounds withDebug, so it waits for formatting. Printing anf64is refused with its span, asrejectshows.max/minon ±0.0: std leaves the result unspecified, so the test normalizes it. The runtime returnsMath.max/Math.min's choice.classify(FpCategory): not mapped yet, for lack of a consumer. It refuses as an unmapped call.Alternatives
Math.*: rejected by the plan decided on #135.try_from_secs_f64without bigint (double-double splitting): bigint mirrors std's algorithm line for line, and this isn't a hot path.Follow-ups
f32; worth a/quest-auditlook.tests/nan.tsand the libm ULP bound, if browser coverage in CI is wanted.🤖 Generated with Claude Code
(written by Claude Opus 5.5)