Repository navigation
A float literal carries its bits, so it cannot decode an ulp away from CRuby (fixes #141) - #143
Open
jiro-by-nori[bot] wants to merge 1 commit into
Open
jiro-by-nori[bot] wants to merge 1 commit into
jiro-by-nori[bot] wants to merge 1 commit into
Conversation
Lean's `OfScientific.ofScientific` truncates the mantissa to 64 bits before its final rounding, so a JSON number that names a decimal double can decode one ulp away from CRuby's `strtod`: `403355592039198e57` came out as `4.0335559203919798e+71` where CRuby answers `4.03355592039198e+71`, and `5.724162270483689e-257` and `5.7241622704836886e-257`, which CRuby distinguishes, were one double in the model. A silent wrong answer, not a gate. The export now writes a float literal as `["flt_bits", <UInt64>]`, the head the imaginary-component path already used, and `Float.ofBits` finishes the job, so no decimal-to-binary64 conversion happens in the model at all. `["flt", number]` stays readable: the model decoder and the derivation emitter both still accept it. The wire version does not move, because the decoder already knew the head. `ruby-lean/RubyCore/Generated/PreludeJson.lean` is regenerated: the prelude's one float literal (`def to_f = 0.0`) carries its bits now, and `make gen-check` requires the committed copy to match. `Prelude.lean` decodes to the same terms, so nothing the proofs see moves. A regression case pins both witnesses in `difftest/corpus/regressions/`. Fixes #141.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #141.
Authored by Jiro, an autonomous agent (Nori sessions) on behalf of @sam-xif.
What was wrong
A Ruby float literal exported as a JSON number, and the model decoded it with
JsonNumber.toFloat, i.e.OfScientific.ofScientific mantissa true exponent.That keeps 64 bits of mantissa and truncates before the final rounding, so it
double-rounds: for some literals the model's double is one ulp away from the
one CRuby's
strtodproduces. A silent wrong answer, not a gate.p 403355592039198e574.03355592039198e+714.0335559203919798e+71a = 5.724162270483689e-257; b = 5.7241622704836886e-257; p [a == b, a > b][false, true][true, false]The second row is the sharper witness: the two literals are different doubles
(
0abb80e67590c875and0abb80e67590c874) and the model collapsed them.The fix
The export writes a float literal as
["flt_bits", <UInt64>]— the head theimaginary-component path already used — so no decimal-to-binary64 conversion
happens inside the model;
Float.ofBitsfinishes the job on exact bits. Thenumber never travels through
OfScientific.desugar/lib/export.rb: a[:flt, f]node exports as its bits (the finiteand payload checks stay, as
ArgumentErrors).imag's component goes throughthe same path, so the two cannot drift.
ruby-lean/RubyCore/Syntax.lean:["flt", number]stays readable, for adocument written before this. No new head, so
Export::VERSIONdoes not move.books/Books/TypeSoundness/Checker/Lang/Expr.lean: the checker's own copy ofthe decoder gains
flt_bitsthe same way (its.fltalready carried bits).books/scripts/emit_deriv.rb:fltLitreads the bits;["flt", number]isstill accepted.
ruby-lean/RubyCore/Generated/PreludeJson.leanis regenerated (make genequivalent). The prelude has one float literal —
def to_f = 0.0— and theregenerated
Prelude.leanis byte-identical, so nothing the proofs see moves.Most of that file's diff is the fixed-width line re-wrapping the chunked JSON
string, shift-and-all from the changed node.
-0.0and the imaginary path are unchanged in behaviour:p -0.0andp 1.5istill agree with CRuby. The-0.0literal keeps its-@normal form(the desugared shape is pinned by
desugar/test/test_variants.rb); only thecomment explaining why changed, because the reason it gave (JSON cannot carry
a negative zero) no longer holds.
Test plan
New regression case
difftest/corpus/regressions/float-literal-double-round.rb(
status: fixed), replayed by everymake conformance: both witnesses above,and
p [a, b]to pin that the two literals stay distinct.How I validated, on this branch:
bin/ruby-lean --compareon bothwitnesses is
DIFFERonmain(model4.0335559203919798e+71,[true, false]) andAGREEon this branch.make conformance—ran 1287, agree 1087, sut_unsupported 194, not_compared 6, disagree 0:CONFORMANCE OK, the baseline holds exactly. Thenew regression case is
held; theregressionstier is 222 held / 1 gated /0 failures, and the
tier3replay is 57 agree / 0 disagree.make soundness—SOUNDNESS CHECK PASSED. The corpus pipeline rebuildsevery AST (now with
flt_bits) andvalidateDaccepts 140/268, exactly theset in
corpus/accepted.txt;007-flt-litis among them, so the checker'snew decode path is exercised.
make books(1403 jobs),make metatheory(axiom audit included),make comparator(Your solution is okay!),make book-checks(FastPower 72/72, LoopSum AGREE),
make feature-loading(AGREE).make desugar-test— 1257 agree, 0 disagree, 76 out-of-fragment, rulecoverage 76/76.
make desugar-coverage— in-fragment 1211/1282, the baseline.make desugar-unit— 921 runs, 0 failures; branch coverage moved up(88.91 → 88.92) and the baseline is recorded in this commit, as the check
asks.
make emitter-unit— 368 runs, 0 failures, coverage holds (a new testdrives both the
flt_bitsand the legacyfltarm, so neither is uncovered).make difftest-test— 189 passed, coverage holds.make docs(
mkdocs build --strict),make gen-check(all four generated files fresh),make check-not-weakened(CHECKS NOT WEAKENED),make ci-sync.make lint-equivalent:rubocopover the tree (44 files, no offenses),ruff format --checkandruff check(115 files, clean). shellcheck and actionlintcover files this change does not touch.
Found while testing, not fixed here
-0.0as a sub-expression still takes the-@route, which is nowunnecessary but harmless; changing it would move the pinned normal form for no
behavioural gain, so it is left alone.