From 2d4135ff07470802b39e94017ebb7e46e671c59b Mon Sep 17 00:00:00 2001 From: Luc Blaeser <112870813+luc-blaeser@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:51:04 +0100 Subject: [PATCH 1/2] Base Library Adjustments for Latest Motoko RTS (#589) Adjusting the base library for latest Motoko RTS changes: * Also test enhanced orthogonal persistence with Wasm Memory 64. * Be prepared for the RTS dependency upgrade https://github.com/dfinity/motoko/pull/4677 Changes: The original Musl float formatter has been replaced by the Rust implementation with some subtle format changes: - Numbers are displayed with the actually defined precision, no longer truncating trailing zeros and potentially revealing existing numeric errors. - The exponent is printed with an explicit sign and multiple digits. - NaN is formatted differently, now `NaN`, while the NaN sign bit is omitted. - The hexadecimal float formatter is no longer supported (`Float.format(#hex)`). This is probably acceptable as it is rarely used. Once we have https://github.com/dfinity/motoko/pull/4677 merged in Motoko `master`, then we can remove the backwards compatibility of float formatting in these tests. Backwards compatibility is here needed to make `motoko-base` tests pass with the old Motoko compiler version. --------- Co-authored-by: Claudio Russo --- CHANGELOG.md | 10 ++++ src/Float.mo | 13 +++-- test/Float.test.mo | 134 ++++++++++++++++++++++----------------------- test/Makefile | 28 +++++++--- 4 files changed, 104 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bcf596a..777ebd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.13.4 + +* Breaking change (minor): `Float.format(#hex)` is no longer supported. + This is because newer versions of Motoko (such as with enhanced orthogonal persistence) + rely on the Rust-native formatter that does not offer this functionality. + It is expected that this formatter is very rarely used in practice. + +* Formatter change (minor): The text formatting of `NaN`, positive or negative, + will be `NaN` in newer Motoko versions, while it was `nan` or `-nan` in older versions. + ## 0.13.3 * Add modules `OrderedMap` and `OrderedSet` to replace `RBTree` (thanks to Serokell) (#662). diff --git a/src/Float.mo b/src/Float.mo index 8519b076..ce8e3009 100644 --- a/src/Float.mo +++ b/src/Float.mo @@ -402,13 +402,15 @@ module { /// * `#fix prec` as fixed-point format with `prec` digits /// * `#exp prec` as exponential format with `prec` digits /// * `#gen prec` as generic format with `prec` digits - /// * `#hex prec` as hexadecimal format with `prec` digits /// * `#exact` as exact format that can be decoded without loss. /// /// `-0.0` is formatted with negative sign bit. - /// Positive infinity is formatted as `inf`. - /// Negative infinity is formatted as `-inf`. - /// `NaN` is formatted as `NaN` or `-NaN` depending on its sign bit. + /// Positive infinity is formatted as "inf". + /// Negative infinity is formatted as "-inf". + /// + /// Note: The numerical precision and the text format can vary between + /// Motoko versions and runtime configuration. Moreover, `NaN` can be printed + /// differently, i.e. "NaN" or "nan", potentially omitting the `NaN` sign. /// /// Example: /// ```motoko @@ -416,11 +418,10 @@ module { /// /// Float.format(#exp 3, 123.0) // => "1.230e+02" /// ``` - public func format(fmt : { #fix : Nat8; #exp : Nat8; #gen : Nat8; #hex : Nat8; #exact }, x : Float) : Text = switch fmt { + public func format(fmt : { #fix : Nat8; #exp : Nat8; #gen : Nat8; #exact }, x : Float) : Text = switch fmt { case (#fix(prec)) { Prim.floatToFormattedText(x, prec, 0) }; case (#exp(prec)) { Prim.floatToFormattedText(x, prec, 1) }; case (#gen(prec)) { Prim.floatToFormattedText(x, prec, 2) }; - case (#hex(prec)) { Prim.floatToFormattedText(x, prec, 3) }; case (#exact) { Prim.floatToFormattedText(x, 17, 2) } }; diff --git a/test/Float.test.mo b/test/Float.test.mo index ca56738f..10cdfb32 100644 --- a/test/Float.test.mo +++ b/test/Float.test.mo @@ -2,6 +2,7 @@ import Debug "../src/Debug"; import Float "../src/Float"; +import Text "../src/Text"; import Suite "mo:matchers/Suite"; import T "mo:matchers/Testable"; @@ -44,11 +45,11 @@ let positiveNaN = Float.copySign(0.0 / 0.0, 1.0); let negativeNaN = Float.copySign(0.0 / 0.0, -1.0); func isPositiveNaN(number : Float) : Bool { - debug_show (number) == "nan" + Float.isNaN(number) and Float.copySign(1.0, number) == 1.0 }; func isNegativeNaN(number : Float) : Bool { - debug_show (number) == "-nan" + Float.isNaN(number) and Float.copySign(1.0, number) == -1.0 }; let positiveZero = 0.0; @@ -87,7 +88,7 @@ let smallEpsilon = 1e-6; class NaNMatcher() : M.Matcher { public func describeMismatch(number : Float, _description : M.Description) { - Debug.print(debug_show (number) # " should be 'nan' or '-nan'") + Debug.print(debug_show (number) # " should be 'NaN' or '-NaN'") }; public func matches(number : Float) : Bool { @@ -97,7 +98,7 @@ class NaNMatcher() : M.Matcher { class PositiveNaNMatcher() : M.Matcher { public func describeMismatch(number : Float, _description : M.Description) { - Debug.print(debug_show (number) # " should be 'nan' (positive)") + Debug.print(debug_show (number) # " should be 'NaN' (positive)") }; public func matches(number : Float) : Bool { @@ -107,7 +108,7 @@ class PositiveNaNMatcher() : M.Matcher { class NegativeNaNMatcher() : M.Matcher { public func describeMismatch(number : Float, _description : M.Description) { - Debug.print(debug_show (number) # " should be '-nan' (negative)") + Debug.print(debug_show (number) # " should be '-NaN' (negative)") }; public func matches(number : Float) : Bool { @@ -115,6 +116,41 @@ class NegativeNaNMatcher() : M.Matcher { } }; +// The Rust float formatter prints `NaN`. +// The Musl float formatter prints `nan`. +class PositiveNaNTextMatcher() : M.Matcher { + public func describeMismatch(text : Text, _description : M.Description) { + Debug.print("'" # text # "' should be 'NaN' or 'nan', depending on the Motoko version and runtime configuration") + }; + + public func matches(text : Text) : Bool { + text == "NaN" or text == "nan" + } +}; + +// The Rust float formatter ignores the sign of NaN and emits `NaN`. +// The Musl float formatter prints `-nan`. +class NegativeNaNTextMatcher() : M.Matcher { + public func describeMismatch(text : Text, _description : M.Description) { + Debug.print("'" # text # "' should be 'NaN' or '-nan', depending on the Motoko version and runtime configuration") + }; + + public func matches(text : Text) : Bool { + text == "NaN" or text == "-nan" + } +}; + +// Account for different numerical errors becoming visible in float formatting. +class TextPrefixMatcher(prefix : Text) : M.Matcher { + public func describeMismatch(text : Text, _description : M.Description) { + Debug.print("'" # text # "' does not start with '" # prefix # "'") + }; + + public func matches(text : Text) : Bool { + Text.startsWith(text, #text prefix) + } +}; + // Some tests are adopted from Motoko compiler test `float-ops.mo`. /* --------------------------------------- */ @@ -1341,7 +1377,7 @@ run( test( "one", Float.exp(1.0), - M.equals(FloatTestable(Float.e, noEpsilon)) + M.equals(FloatTestable(Float.e, smallEpsilon)) ), test( "positive infinity", @@ -1431,22 +1467,22 @@ run( test( "exact positive", Float.format(#exact, 20.12345678901), - M.equals(T.text("20.12345678901")) + TextPrefixMatcher("20.1234567890") ), test( "exact negative", Float.format(#exact, -20.12345678901), - M.equals(T.text("-20.12345678901")) + TextPrefixMatcher("-20.1234567890") ), test( "exact positive zero", Float.format(#exact, positiveZero), - M.equals(T.text("0")) + M.anyOf([M.equals(T.text("0")), M.equals(T.text("0.00000000000000000"))]) ), test( "exact negative zero", Float.format(#exact, negativeZero), - M.equals(T.text("-0")) + M.anyOf([M.equals(T.text("-0")), M.equals(T.text("-0.00000000000000000"))]) ), test( "exact positive infinity", @@ -1461,12 +1497,12 @@ run( test( "exact positive NaN", Float.format(#exact, positiveNaN), - M.equals(T.text("nan")) + PositiveNaNTextMatcher() ), test( "exact negative NaN", Float.format(#exact, negativeNaN), - M.equals(T.text("-nan")) + NegativeNaNTextMatcher() ), test( "fix positive", @@ -1501,32 +1537,32 @@ run( test( "fix positive NaN", Float.format(#fix 6, positiveNaN), - M.equals(T.text("nan")) + PositiveNaNTextMatcher() ), test( "fix negative NaN", Float.format(#fix 6, negativeNaN), - M.equals(T.text("-nan")) + NegativeNaNTextMatcher() ), test( "exp positive", Float.format(#exp 9, 20.12345678901), - M.equals(T.text("2.012345679e+01")) + M.anyOf([M.equals(T.text("2.012345679e1")), M.equals(T.text("2.012345679e+01"))]) ), test( "exp negative", Float.format(#exp 9, -20.12345678901), - M.equals(T.text("-2.012345679e+01")) + M.anyOf([M.equals(T.text("-2.012345679e1")), M.equals(T.text("-2.012345679e+01"))]) ), test( "exp positive zero", Float.format(#exp 9, positiveZero), - M.equals(T.text("0.000000000e+00")) + M.anyOf([M.equals(T.text("0.000000000e0")), M.equals(T.text("0.000000000e+00"))]) ), test( "exp negative zero", Float.format(#exp 9, negativeZero), - M.equals(T.text("-0.000000000e+00")) + M.anyOf([M.equals(T.text("-0.000000000e0")), M.equals(T.text("-0.000000000e+00"))]) ), test( "exp positive infinity", @@ -1541,32 +1577,32 @@ run( test( "exp positive NaN", Float.format(#exp 9, positiveNaN), - M.equals(T.text("nan")) + PositiveNaNTextMatcher() ), test( "exp negative NaN", Float.format(#exp 9, negativeNaN), - M.equals(T.text("-nan")) + NegativeNaNTextMatcher() ), test( "gen positive", Float.format(#gen 12, 20.12345678901), - M.equals(T.text("20.123456789")) + TextPrefixMatcher("20.123456789") ), test( "gen negative", Float.format(#gen 12, -20.12345678901), - M.equals(T.text("-20.123456789")) + TextPrefixMatcher("-20.123456789") ), test( "gen positive zero", Float.format(#gen 12, positiveZero), - M.equals(T.text("0")) + M.anyOf([M.equals(T.text("0")), M.equals(T.text("0.000000000000"))]) ), test( "gen negative zero", Float.format(#gen 12, negativeZero), - M.equals(T.text("-0")) + M.anyOf([M.equals(T.text("-0")), M.equals(T.text("-0.000000000000"))]) ), test( "gen positive infinity", @@ -1581,53 +1617,15 @@ run( test( "gen positive NaN", Float.format(#gen 12, positiveNaN), - M.equals(T.text("nan")) + PositiveNaNTextMatcher() ), test( "gen negative NaN", Float.format(#gen 12, negativeNaN), - M.equals(T.text("-nan")) - ), - test( - "hex positive", - Float.format(#hex 10, 20.12345678901), - M.equals(T.text("0x1.41f9add374p+4")) + NegativeNaNTextMatcher() ), - test( - "hex negative", - Float.format(#hex 10, -20.12345678901), - M.equals(T.text("-0x1.41f9add374p+4")) - ), - test( - "hex positive zero", - Float.format(#hex 10, positiveZero), - M.equals(T.text("0x0.0000000000p+0")) - ), - test( - "hex negative zero", - Float.format(#hex 10, negativeZero), - M.equals(T.text("-0x0.0000000000p+0")) - ), - test( - "hex positive infinity", - Float.format(#hex 10, positiveInfinity), - M.equals(T.text("inf")) - ), - test( - "hex negative infinity", - Float.format(#hex 10, negativeInfinity), - M.equals(T.text("-inf")) - ), - test( - "hex positive NaN", - Float.format(#hex 10, positiveNaN), - M.equals(T.text("nan")) - ), - test( - "hex negative NaN", - Float.format(#hex 10, negativeNaN), - M.equals(T.text("-nan")) - ) + // hex float formatting was only supported with Musl + // and is no longer supported with Rust-implemented formatter. ] ) ); @@ -1671,12 +1669,12 @@ run( test( "positive NaN", Float.toText(positiveNaN), - M.equals(T.text("nan")) + PositiveNaNTextMatcher() ), test( "negative NaN", Float.toText(negativeNaN), - M.equals(T.text("-nan")) + NegativeNaNTextMatcher() ) ] ) diff --git a/test/Makefile b/test/Makefile index 8ce4c80d..df3cbc53 100644 --- a/test/Makefile +++ b/test/Makefile @@ -2,20 +2,28 @@ STDLIB ?= ../src MOC ?= moc -WASMTIME_OPTIONS = -C cache=n -W nan-canonicalization=y -W multi-memory -W bulk-memory +WASMTIME_OPTIONS = -C cache=n -W nan-canonicalization=y -W memory64 -W multi-memory -W bulk-memory OUTDIR=_out TESTS = $(wildcard *.mo) -TEST_TARGETS = $(patsubst %.mo,_out/%.checked,$(TESTS)) +TEST_CLASSICAL_TARGETS = $(patsubst %.mo,_out/%.classical.checked,$(TESTS)) +TEST_ENHANCED_TARGETS = $(patsubst %.mo,_out/%.enhanced.checked,$(TESTS)) -all: $(OUTDIR)/import_all.checked $(TEST_TARGETS) +all: $(OUTDIR)/import_all.classical.checked \ + $(OUTDIR)/import_all.enhanced.checked \ + $(TEST_CLASSICAL_TARGETS) \ + $(TEST_ENHANCED_TARGETS) STDLIB_FILES= $(wildcard $(STDLIB)/*.mo) VESSEL_PKGS= $(shell vessel sources) +MOC_COMMON_FLAGS=-c --package base $(STDLIB) $(VESSEL_PKGS) -wasi-system-api +MOC_CLASSICAL=$(MOC) $(MOC_COMMON_FLAGS) +MOC_ENHANCED=$(MOC) $(MOC_COMMON_FLAGS) --enhanced-orthogonal-persistence + $(OUTDIR): @mkdir $@ @@ -25,11 +33,17 @@ $(OUTDIR)/import_all.mo: $(STDLIB_FILES) | $(OUTDIR) echo "import _Import_$$f \"mo:base/$$f\";" >> $@; \ done -$(OUTDIR)/%.wasm: %.mo | $(OUTDIR) - $(MOC) -c --package base $(STDLIB) $(VESSEL_PKGS) -wasi-system-api -o $@ $< +$(OUTDIR)/%.classical.wasm: %.mo | $(OUTDIR) + $(MOC_CLASSICAL) -o $@ $< + +$(OUTDIR)/%.enhanced.wasm: %.mo | $(OUTDIR) + $(MOC_ENHANCED) -o $@ $< + +$(OUTDIR)/import_all.classical.wasm: $(OUTDIR)/import_all.mo | $(OUTDIR) + $(MOC_CLASSICAL) -o $@ $< -$(OUTDIR)/%.wasm: $(OUTDIR)/%.mo | $(OUTDIR) - $(MOC) -c --package base $(STDLIB) $(VESSEL_PKGS) -wasi-system-api -o $@ $< +$(OUTDIR)/import_all.enhanced.wasm: $(OUTDIR)/import_all.mo | $(OUTDIR) + $(MOC_ENHANCED) -o $@ $< $(OUTDIR)/%.checked: $(OUTDIR)/%.wasm wasmtime run $(WASMTIME_OPTIONS) $< From b3e58c746f1010806e9aec3adc6092548704bccd Mon Sep 17 00:00:00 2001 From: Claudio Russo Date: Fri, 29 Nov 2024 19:41:08 +0000 Subject: [PATCH 2/2] Motoko 0.13.4 --- .github/workflows/ci.yml | 2 +- .github/workflows/package-set.yml | 2 +- mops.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7adc3665..4b111aa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: # Remember to update me in package-set.yml as well env: vessel_version: "v0.7.0" - moc_version: "0.13.3" + moc_version: "0.13.4" jobs: tests: diff --git a/.github/workflows/package-set.yml b/.github/workflows/package-set.yml index badb0b0d..5c550722 100644 --- a/.github/workflows/package-set.yml +++ b/.github/workflows/package-set.yml @@ -8,7 +8,7 @@ on: env: vessel_version: "v0.7.0" - moc_version: "0.13.3" + moc_version: "0.13.4" jobs: verify: diff --git a/mops.toml b/mops.toml index 42e90c67..70df5e7d 100644 --- a/mops.toml +++ b/mops.toml @@ -1,6 +1,6 @@ [package] name = "base" -version = "0.13.3" +version = "0.13.4" description = "The Motoko base library" repository = "https://github.com/dfinity/motoko-base" keywords = [ "base" ] @@ -10,5 +10,5 @@ license = "Apache-2.0" matchers = "https://github.com/kritzcreek/motoko-matchers#v1.3.0@3dac8a071b69e4e651b25a7d9683fe831eb7cffd" [toolchain] -moc = "0.13.3" +moc = "0.13.4" wasmtime = "17.0.0"