fix: render out-of-int64 values positionally instead of saturating - #52
Merged
Conversation
Both rendering paths -- runtime.lua to_str (the printer) and prims.lua
numToStr (the str primitive) -- render a finite integral double with
string.format("%d", n). Under LuaJIT/5.1 there is no math.tointeger to
guard with and %d SATURATES rather than erroring, so every value outside
int64 range prints as the same sentinel:
(* 1000000000.0 10000000000.0) => 9223372036854775807
(str 1e19) => 9223372036854775807
1e300 => 9223372036854775807
c83c40e fixed the compile-time literal path (cnum) so the VALUE is now
right; only the rendering is destroyed. shen-cl and shen-rust both print
the full positional integer.
The spec asserts both paths, asserts they agree with each other, and
pins the collateral (ordinary integers, negatives, zero, 2^53, the max
double below 2^63, non-integral floats, 1e-300, inf/-inf/nan). Every
big-value expectation was taken byte-for-byte from a fresh shen-rust.
Currently: 153 pass, 42 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runtime.lua to_str (the printer) and prims.lua numToStr (the `str`
primitive) each rendered a finite integral double with
string.format("%d", n). LuaJIT/5.1 have no math.tointeger to guard with
and there %d SATURATES instead of erroring, so every value at or beyond
int64 range printed as one sentinel:
(* 1000000000.0 10000000000.0) => 9223372036854775807
(str 1e19) => 9223372036854775807
1e300 => 9223372036854775807
c83c40e fixed the same class at compile time (cnum) so the VALUE was
already right; only the rendering was destroyed.
Rule now, in ONE place (runtime.num_to_str):
* finite integral, |x| < 2^63 -- exact %d, completely unchanged;
* finite integral, |x| >= 2^63 -- shortest round-trippable decimal,
expanded positionally: no exponent at any magnitude. 1e19 renders as
10000000000000000000, 1e300 as 301 digits;
* everything else (non-integral, +-inf, NaN) -- shortest_float,
unchanged. Overflow policy is deliberately untouched.
Shortest-round-trip rather than the double's exact value (%.0f) because
it round-trips, it is what shen-rust prints for the same double (checked
byte-for-byte on 20 expressions), and it is the rule this printer already
applies to non-integral values (issue #24). The boundary is
sign-symmetric so -2^63 is the negation of 2^63 rather than exposing the
two's-complement asymmetry of %d.
prims.lua no longer carries its own copy of the rule -- it now calls
R.num_to_str. The duplicate is how the two paths diverged before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
c83c40efixed the int64-saturation class at compile time (cnum), soshen-lua now computes large values correctly. It deliberately left the
rendering paths out of scope, and they still saturated:
Two sites, same root cause:
string.format("%d", n)on a finite integraldouble. Under LuaJIT/5.1 there is no
math.tointegerto guard with and%dsaturates rather than erroring, so everything at or beyond int64 range
collapsed onto one sentinel.
runtime.luato_str— the printer (REPL echo,shen.tostring, specs)prims.luanumToStr— the ShenstrprimitiveThe rule, now in exactly one place
runtime.num_to_str, called by both surfaces. They are two surfaces on onerule and have diverged in this repo before, so
prims.luano longer carriesits own copy.
shortest_float— unchanged (issue #24)+inf/-inf/ NaNinf/-inf/nan— unchanged; overflow policy deliberately untouched1e19->10000000000000000000.1e300-> 301 digits.Why shortest-round-trip and not the double's exact value (
%.0f)? Itround-trips; it is what shen-rust prints for the same double (checked
byte-for-byte, below); and it is the rule this printer already applies to every
non-integral value.
%.0fwould match nobody. The boundary is sign-symmetric(
|x| >= 2^63, matchingcnum's existing> -TWO63) so-2^63renders as thenegation of
2^63instead of exposing the two's-complement asymmetry of%d.Cross-port verification
Given the same double, the new renderer is byte-for-byte identical to a
fresh shen-rust on all 20 anchored expressions tested (
1e19,-1e19,1e20,1e21,1e100,1e300,-1e300,1e308,2^63,-2^63,2^64,12345678901234567890, and reader-independent products such as(* 4611686018427387904.0 2.0)).1e1992233720368547758071000000000000000000010000000000000000000100000000000000000001e+19(str 1e19)92233720368547758071000000000000000000010000000000000000000100000000000000000001e+19(* 1000000000.0 10000000000.0)9223372036854775807100000000000000000001e+19-1e19-9223372036854775808-10000000000000000000-1e+191e30092233720368547758071000...0(301 digits)1.0000000000000002e+3001844674407370955161692233720368547758071844674407370955200018446744073709551616184467440737095520001.8446744073709552e+19+inf/-inf/ NaNinf/-inf/naninf/-inf/NaNinf/-inf/nan42,0,-42,2^53,0.1,1/3,1e-300Where shen-lua and shen-cl still differ on a big literal (
2^64,12345678901234567890) it is because shen-cl has bignums and keeps theliteral exact; shen-lua only has doubles and cannot. Where shen-lua and
shen-rust differ on
1e300's digits it is a reader difference, not arenderer one — shen-lua, shen-go and shen-rust all read
1e300as1.0000000000000002e300and print identical digits from it.The
.0question (investigated, not "fixed")shen-rust prints
(/ 1e300 1e290)as10000000000.0but1e19as10000000000000000000. This is not a magnitude rule and shen-cl does notagree with it:
(/ 1e300 1e290)1000000000010000000000.01000000000010000000000(* 1.0 5)55.055(* 2.5 2)5.05.055(/ 10.0 2.0)55.055The
.0is a type tag, not a formatting choice. shen-cl and shen-rust bothhave a distinct integer type alongside floats and suffix
.0on the float one;they disagree with each other because their readers disagree about which
literals are integers (shen-cl's reader makes
1.0and1e300exact integers;shen-rust's does not). shen-lua and shen-go have a single number type — a
double — and render every integral value bare. Reproducing
.0would requireintroducing an int/float distinction across the whole port and would turn
42into
42.0. Out of scope, orthogonal to this defect, and reported rather thanguessed at.
Tests
test/numeric_render_spec.lua(new, registered inscripts/run-tests.lua),committed failing first (
caaedd6: 153 pass / 42 fail) then fixed(
009e51f: 195 pass / 0 fail). It asserts both rendering paths on everycase and asserts that they agree with each other, plus the collateral:
ordinary integers, negatives, zero,
2^53, the largest double below2^63,non-integral floats,
1e-300, andinf/-inf/nan.Not changed: overflow policy,
compiler.luacnum,boot.lua's%.17gimageserialisation, and shen-go / shen-rust (owned elsewhere).