diff --git a/clojure-runtime/Clojure/Lib/EdnReader.cs b/clojure-runtime/Clojure/Lib/EdnReader.cs index 8a9fe961..8ed1c830 100644 --- a/clojure-runtime/Clojure/Lib/EdnReader.cs +++ b/clojure-runtime/Clojure/Lib/EdnReader.cs @@ -604,7 +604,16 @@ public static object MatchNumber(string s) // val = val.Substring(1); return BigDecimal.Parse(val); } - return (object)Double.Parse(s, System.Globalization.CultureInfo.InvariantCulture); + try + { + return (object)Double.Parse(s, System.Globalization.CultureInfo.InvariantCulture); + } + catch (OverflowException) + { + // Mono and .NET Framework throw on out-of-range input where Double.parseDouble + // and .NET Core saturate. Underflow returns zero, so only overflow reaches this. + return s[0] == '-' ? (object)Double.NegativeInfinity : (object)Double.PositiveInfinity; + } } m = ratioRE.Match(s); if (m.Success) diff --git a/clojure-runtime/Clojure/Lib/LispReader.cs b/clojure-runtime/Clojure/Lib/LispReader.cs index 135905ff..0ecc6faf 100644 --- a/clojure-runtime/Clojure/Lib/LispReader.cs +++ b/clojure-runtime/Clojure/Lib/LispReader.cs @@ -883,7 +883,16 @@ public static object MatchNumber(string s) // val = val.Substring(1); return BigDecimal.Parse(val); } - return (object)Double.Parse(s, System.Globalization.CultureInfo.InvariantCulture); + try + { + return (object)Double.Parse(s, System.Globalization.CultureInfo.InvariantCulture); + } + catch (OverflowException) + { + // Mono and .NET Framework throw on out-of-range input where Double.parseDouble + // and .NET Core saturate. Underflow returns zero, so only overflow reaches this. + return s[0] == '-' ? (object)Double.NegativeInfinity : (object)Double.PositiveInfinity; + } } m = ratioRE.Match(s); if (m.Success) diff --git a/magic-compiler/dll-sources.edn b/magic-compiler/dll-sources.edn index 05737e70..6249e699 100644 --- a/magic-compiler/dll-sources.edn +++ b/magic-compiler/dll-sources.edn @@ -13,7 +13,7 @@ clojure.core.specs.alpha {:source "magic-compiler/src/stdlib/clojure/core/specs/alpha.clj", :sha256 "41d82e6e56c9165e806f931b0598a05def8388d9b7248d7d431d7e822527928b"} clojure.core_clr {:source "magic-compiler/src/stdlib/clojure/core_clr.clj", :sha256 "371b1553b349138b17e9741e37a134dc4193c621366b1f4b154abcdfa5c69f2e"} clojure.core_deftype {:source "magic-compiler/src/stdlib/clojure/core_deftype.clj", :sha256 "b643bf26b11cc6628116aedddf4569687bb8addff4cf6d0525cd11c4d815b8ad"} - clojure.core_print {:source "magic-compiler/src/stdlib/clojure/core_print.clj", :sha256 "458d036fe8d8616c6f585164524fc25fc90cf1fb9178cbbeb79230f94c3273cd"} + clojure.core_print {:source "magic-compiler/src/stdlib/clojure/core_print.clj", :sha256 "43371f550ebb965398ded47161946b348326c827fa8fde90334b3769618796b8"} clojure.core_proxy {:source "magic-compiler/src/stdlib/clojure/core_proxy.clj", :sha256 "946aeb183fc90cbb1a52dceb325ed81816ed28cea5177ee6dd8b4e801c6cd9c2"} clojure.data {:source "magic-compiler/src/stdlib/clojure/data.clj", :sha256 "e60889339006b57f672c681892710164399ae281d0f37371886b1d9bc73ee8cd"} clojure.datafy {:source "magic-compiler/src/stdlib/clojure/datafy.clj", :sha256 "454d0d43ec60601d031bb62a48ece29f41f8c75648d0da34fe972abce72c56ea"} diff --git a/magic-compiler/src/stdlib/clojure/core_print.clj b/magic-compiler/src/stdlib/clojure/core_print.clj index b649bb30..da3f36ed 100644 --- a/magic-compiler/src/stdlib/clojure/core_print.clj +++ b/magic-compiler/src/stdlib/clojure/core_print.clj @@ -131,7 +131,9 @@ ;;; DM ADDED (defn fp-str [x] - (let [s (str x)] + ;; "R" round-trips: the default ToString emits 15 significant digits, so most doubles + ;; do not read back equal, and the default culture would use a comma separator. + (let [s (.ToString x "R" System.Globalization.CultureInfo/InvariantCulture)] (if (or (.Contains s ".") (.Contains s "E")) s (str s ".0")))) diff --git a/magic-unity-dual/Runtime/Infrastructure/Export/Clojure.dll b/magic-unity-dual/Runtime/Infrastructure/Export/Clojure.dll index aeb841a4..8c691006 100644 Binary files a/magic-unity-dual/Runtime/Infrastructure/Export/Clojure.dll and b/magic-unity-dual/Runtime/Infrastructure/Export/Clojure.dll differ diff --git a/magic-unity-dual/Runtime/Infrastructure/Export/clojure.core_print.clj.dll b/magic-unity-dual/Runtime/Infrastructure/Export/clojure.core_print.clj.dll index 67374616..a77f0716 100755 Binary files a/magic-unity-dual/Runtime/Infrastructure/Export/clojure.core_print.clj.dll and b/magic-unity-dual/Runtime/Infrastructure/Export/clojure.core_print.clj.dll differ diff --git a/magic-unity/Runtime/Infrastructure/Export/Clojure.dll b/magic-unity/Runtime/Infrastructure/Export/Clojure.dll index aeb841a4..8c691006 100644 Binary files a/magic-unity/Runtime/Infrastructure/Export/Clojure.dll and b/magic-unity/Runtime/Infrastructure/Export/Clojure.dll differ diff --git a/magic-unity/Runtime/Infrastructure/Export/clojure.core_print.clj.dll b/magic-unity/Runtime/Infrastructure/Export/clojure.core_print.clj.dll index 67374616..a77f0716 100755 Binary files a/magic-unity/Runtime/Infrastructure/Export/clojure.core_print.clj.dll and b/magic-unity/Runtime/Infrastructure/Export/clojure.core_print.clj.dll differ diff --git a/nostrand/ArgumentReader.cs b/nostrand/ArgumentReader.cs index 95632d0b..3da5ed4c 100644 --- a/nostrand/ArgumentReader.cs +++ b/nostrand/ArgumentReader.cs @@ -605,7 +605,16 @@ public static object MatchNumber(string s) // val = val.Substring(1); return BigDecimal.Parse(val); } - return (object)Double.Parse(s, System.Globalization.CultureInfo.InvariantCulture); + try + { + return (object)Double.Parse(s, System.Globalization.CultureInfo.InvariantCulture); + } + catch (OverflowException) + { + // Mono and .NET Framework throw on out-of-range input where Double.parseDouble + // and .NET Core saturate. Underflow returns zero, so only overflow reaches this. + return s[0] == '-' ? (object)Double.NegativeInfinity : (object)Double.PositiveInfinity; + } } m = ratioRE.Match(s); if (m.Success) diff --git a/nostrand/references/clojure.core_print.clj.dll b/nostrand/references/clojure.core_print.clj.dll index 67374616..a77f0716 100755 Binary files a/nostrand/references/clojure.core_print.clj.dll and b/nostrand/references/clojure.core_print.clj.dll differ diff --git a/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/read_print.clj b/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/read_print.clj new file mode 100644 index 00000000..e3edab0f --- /dev/null +++ b/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/read_print.clj @@ -0,0 +1,83 @@ +(ns smoke.read-print + "Reading and printing floating point. + + Regression for two Mono divergences from the JVM and .NET Core, fixed in the + reader and the printer rather than worked around. + + Reading: Double.Parse throws OverflowException on an out-of-range literal + where Double.parseDouble saturates, so MatchNumber now catches it and returns + an infinity. Under AOT this covers exception handling inside the reader. + + Printing: the default ToString emits 15 significant digits for a double and 7 + for a float, so neither read back equal. fp-str now uses the \"R\" round-trip + specifier, and IL2CPP supplies its own + Double.ToString(string, IFormatProvider), so the Mono result does not cover + this. + + One check per distinct branch: each sign of the infinity ternary, each of the + two reader copies, the two ways Parse can overflow, the two cases that must + still NOT saturate, both numeric widths through fp-str, and the symbolic + values that bypass fp-str entirely." + (:require [clojure.edn :as edn])) + +(def ^:private ic System.Globalization.CultureInfo/InvariantCulture) + +(defn- pass [n] {:name n :pass? true}) +(defn- fail [n detail] {:name n :pass? false :detail detail}) + +(defn- check [name thunk expected] + (try + (let [actual (thunk)] + (if (= expected actual) + (pass name) + (fail name (str "expected " (pr-str expected) " got " (pr-str actual))))) + (catch System.Exception e + (fail name (str (.. e GetType FullName) ": " (.Message e)))))) + +(defn suite [] + [;; the literal is read at compile time, so this pins the emitted constant + (check "out-of-range literal compiles to Inf" + #(identity 1E1000) + Double/PositiveInfinity) + ;; both signs of the saturation ternary, through the runtime reader + (check "read-string of out-of-range literal" + #(read-string "1E1000") + Double/PositiveInfinity) + (check "read-string of negative out-of-range literal" + #(read-string "-1E1000") + Double/NegativeInfinity) + ;; EdnReader carries its own copy of MatchNumber + (check "edn/read-string of out-of-range literal" + #(edn/read-string "1E1000") + Double/PositiveInfinity) + ;; overflow via an exponent that does not fit Int32, a different internal path + (check "exponent too large for Int32 still reads as Inf" + #(read-string "1E99999999999999999999") + Double/PositiveInfinity) + ;; must NOT saturate: underflow returns zero, malformed input still errors + (check "underflow reads as zero, not Inf" + #(read-string "1E-1000") + 0.0) + (check "malformed literal still fails to read" + #(try (pr-str (read-string "1.2.3")) + (catch System.FormatException _ :threw)) + :threw) + ;; fp-str at both widths; the default format loses 1125899906842624 and + ;; 0.333333343, so these fail without the "R" specifier + (check "double survives pr-str then read-string" + #(let [d (Math/Pow 2 50)] (= d (read-string (pr-str d)))) + true) + (check "float survives pr-str then read-string" + #(let [f (float (/ 1.0 3.0))] (= f (System.Single/Parse (pr-str f) ic))) + true) + ;; fp-str's other branch: append ".0" when "R" yields no "." or "E" + (check "ordinary doubles print unchanged" + #(mapv pr-str [0.1 1.0 0.5 100.25]) + ["0.1" "1.0" "0.5" "100.25"]) + ;; symbolic values short-circuit before fp-str, and must survive a round-trip + (check "infinities and NaN print as symbolic values" + #(mapv pr-str [Double/PositiveInfinity Double/NegativeInfinity Double/NaN]) + ["##Inf" "##-Inf" "##NaN"]) + (check "symbolic infinity survives pr-str then read-string" + #(read-string (pr-str Double/PositiveInfinity)) + Double/PositiveInfinity)]) diff --git a/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/read_print.clj.meta b/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/read_print.clj.meta new file mode 100644 index 00000000..2004ba69 --- /dev/null +++ b/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/read_print.clj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b3daae5232b904f578e119c687a82881 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/runner.clj b/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/runner.clj index 5ba864d1..121593ed 100644 --- a/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/runner.clj +++ b/unity-examples/magic-unity-smoke/Assets/Clojure/smoke/runner.clj @@ -10,6 +10,7 @@ [smoke.control-flow :as control-flow] [smoke.stdlib-1-10 :as stdlib-1-10] [smoke.interop :as interop] + [smoke.read-print :as read-print] [clojure.string :as str])) (defn- run [] @@ -18,7 +19,8 @@ ["polymorphism" (polymorphism/suite)] ["control-flow" (control-flow/suite)] ["stdlib-1.10" (stdlib-1-10/suite)] - ["interop" (interop/suite)]] + ["interop" (interop/suite)] + ["read-print" (read-print/suite)]] flat (for [[group results] groups r results] (assoc r :group group))]