From fc0fec0b51d8832d360ad2a91796a10736928469 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 18 Sep 2023 12:07:38 -0400 Subject: [PATCH 1/3] feat(runtime): floating point modulo support --- compiler/test/TestFramework.re | 1 + compiler/test/runner.re | 17 +++++ compiler/test/runtime/numbers.test.gr | 95 +++++++++++++++++++++++++++ compiler/test/suites/runtime.re | 10 +++ stdlib/runtime/numbers.gr | 45 +++++++++---- 5 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 compiler/test/runtime/numbers.test.gr create mode 100644 compiler/test/suites/runtime.re diff --git a/compiler/test/TestFramework.re b/compiler/test/TestFramework.re index 8df384b9a5..e1c612cad2 100644 --- a/compiler/test/TestFramework.re +++ b/compiler/test/TestFramework.re @@ -27,6 +27,7 @@ let test_data_dir = Fp.At.(test_dir / "test-data"); let test_input_dir = Fp.At.(test_dir / "input"); let test_output_dir = Fp.At.(test_dir / "output"); let test_stdlib_dir = Fp.At.(test_dir / "stdlib"); +let test_runtime_dir = Fp.At.(test_dir / "runtime"); let test_snapshots_dir = Fp.At.(test_dir / "__snapshots__"); let test_grainfmt_dir = Fp.At.(test_dir / "grainfmt"); diff --git a/compiler/test/runner.re b/compiler/test/runner.re index bec81fe4f8..07c1831a93 100644 --- a/compiler/test/runner.re +++ b/compiler/test/runner.re @@ -20,6 +20,8 @@ let grainfile = name => Filepath.to_string(Fp.At.(test_input_dir / (name ++ ".gr"))); let stdlibfile = name => Filepath.to_string(Fp.At.(test_stdlib_dir / (name ++ ".gr"))); +let runtimefile = name => + Filepath.to_string(Fp.At.(test_runtime_dir / (name ++ ".gr"))); let wasmfile = name => Filepath.to_string(Fp.At.(test_output_dir / (name ++ ".gr.wasm"))); let watfile = name => @@ -392,6 +394,21 @@ let makeStdlibRunner = (test, ~code=0, name) => { }); }; +let makeRuntimeRunner = (test, ~code=0, name) => { + test(name, ({expect}) => { + Config.preserve_all_configs(() => { + // Run stdlib suites in release mode + Config.profile := Some(Release); + let infile = runtimefile(name); + let outfile = wasmfile(name); + ignore @@ compile_file(infile, outfile); + let (result, exit_code) = run(outfile); + expect.int(exit_code).toBe(code); + expect.string(result).toEqual(""); + }) + }); +}; + let parse = (name, lexbuf, source) => { let ret = Grain_parsing.Driver.parse(~name, lexbuf, source); open Grain_parsing; diff --git a/compiler/test/runtime/numbers.test.gr b/compiler/test/runtime/numbers.test.gr new file mode 100644 index 0000000000..308bd81b04 --- /dev/null +++ b/compiler/test/runtime/numbers.test.gr @@ -0,0 +1,95 @@ +module NumberTest + +include "runtime/numbers" + +// Simple isNaN +let isNaN = x => x != x +let isInfinite = x => x == Infinity || x == -Infinity +from Numbers use { (%) } + +// (%) +assert 20 % 4 == 0 +assert 20 % 3 == 2 +assert 15 % 6 == 3 +assert -10 % 3 == 2 +assert 5 % 2.75 == 2.25 +assert 3.0 % 2.0 == 1.0 +assert 3.0 % -2.0 == 1.0 +assert -3.0 % 2.0 == -1.0 +assert -3.0 % -2.0 == -1.0 +assert 3.5 % 2.0 == 1.5 +assert 3.5 % -2.0 == 1.5 +assert -3.5 % 2.0 == -1.5 +assert -3.5 % -2.0 == -1.5 +assert 3.0 % 2.5 == 0.5 +assert 3.0 % -2.5 == 0.5 +assert -3.0 % 2.5 == -0.5 +assert -3.0 % -2.5 == -0.5 +assert 0.5 % 1.0 == 0.5 +assert 0.5 % -1.0 == 0.5 +assert -0.5 % 1.0 == -0.5 +assert -0.5 % -1.0 == -0.5 +assert 1.5 % 1.0 == 0.5 +assert 1.5 % -1.0 == 0.5 +assert -1.5 % 1.0 == -0.5 +assert -1.5 % -1.0 == -0.5 +assert 1.25 % 1.0 == 0.25 +assert 1.25 % -1.0 == 0.25 +assert -1.25 % 1.0 == -0.25 +assert -1.25 % -1.0 == -0.25 +assert 1.0 % 1.25 == 1.0 +assert 1.0 % -1.25 == 1.0 +assert -1.0 % 1.25 == -1.0 +assert -1.0 % -1.25 == -1.0 +assert -13 % 64 == 51 +assert isNaN(0.0 % 0.0) +assert isNaN(-0.0 % 0.0) +assert isNaN(0.0 % -0.0) +assert isNaN(-0.0 % -0.0) +assert 0.0 % 1.0 == 0.0 +assert -0.0 % 1.0 == -0.0 +assert 0.0 % -1.0 == 0.0 +assert -0.0 % -1.0 == -0.0 +assert isNaN(1.0 % 0.0) +assert isNaN(-1.0 % 0.0) +assert isNaN(1.0 % -0.0) +assert isNaN(-1.0 % -0.0) +assert isNaN(NaN % 0.0) +assert isNaN(NaN % -0.0) +assert isNaN(NaN % 1.0) +assert isNaN(NaN % -1.0) +assert isNaN(NaN % 0.0) +assert isNaN(NaN % -0.0) +assert isNaN(NaN % 1.0) +assert isNaN(NaN % -1.0) +assert isNaN(NaN % NaN) +assert 0.0 % Infinity == 0.0 +assert -0.0 % Infinity == -0.0 +assert 0.0 % -Infinity == 0.0 +assert -0.0 % -Infinity == -0.0 +assert 1.0 % Infinity == 1.0 +assert -1.0 % Infinity == -1.0 +assert 1.0 % -Infinity == 1.0 +assert -1.0 % -Infinity == -1.0 +assert isNaN(Infinity % 0.0) +assert isNaN(Infinity % -0.0) +assert isNaN(-Infinity % 0.0) +assert isNaN(-Infinity % -0.0) +assert isNaN(Infinity % 1.0) +assert isNaN(Infinity % -1.0) +assert isNaN(-Infinity % 1.0) +assert isNaN(-Infinity % -1.0) +assert isNaN(Infinity % Infinity) +assert isNaN(-Infinity % Infinity) +assert isNaN(Infinity % -Infinity) +assert isNaN(-Infinity % -Infinity) +assert isNaN(Infinity % NaN) +assert isNaN(-Infinity % NaN) +assert isNaN(NaN % Infinity) +assert isNaN(NaN % -Infinity) +assert -17 % 4 == 3 +assert -17 % -4 == -1 +assert 17 % -4 == 5 +assert -17 % 17 == 0 +assert 17 % -17 == 0 +assert 17 % 17 == 0 diff --git a/compiler/test/suites/runtime.re b/compiler/test/suites/runtime.re new file mode 100644 index 0000000000..f049f94730 --- /dev/null +++ b/compiler/test/suites/runtime.re @@ -0,0 +1,10 @@ +open Grain_tests.TestFramework; +open Grain_tests.Runner; + +describe("runtime", ({test, testSkip}) => { + let test_or_skip = + Sys.backend_type == Other("js_of_ocaml") ? testSkip : test; + + let assertRuntime = makeRuntimeRunner(test_or_skip); + assertRuntime("numbers.test"); +}); diff --git a/stdlib/runtime/numbers.gr b/stdlib/runtime/numbers.gr index ba9ffa7366..6a29da8bad 100644 --- a/stdlib/runtime/numbers.gr +++ b/stdlib/runtime/numbers.gr @@ -1585,29 +1585,46 @@ let i64abs = x => { from WasmI64 use { (-), (>=) } if (x >= 0N) x else 0N - x } - @unsafe let numberMod = (x, y) => { from WasmI64 use { (!=), (-), (*), (<), (>) } // incRef x and y to reuse them via WasmI32.toGrain Memory.incRef(x) Memory.incRef(y) - let xval = coerceNumberToWasmI64(WasmI32.toGrain(x): Number) - let yval = coerceNumberToWasmI64(WasmI32.toGrain(y): Number) - if (WasmI64.eqz(yval)) { - throw Exception.ModuloByZero - } - // We implement true modulo - if (xval < 0N && yval > 0N || xval > 0N && yval < 0N) { - let modval = WasmI64.remS(i64abs(xval), i64abs(yval)) - let result = if (modval != 0N) { - i64abs(yval) - modval * (if (yval < 0N) -1N else 1N) + if (isFloat(x) || isFloat(y) || isRational(x) || isRational(y)) { + from WasmF64 use { (==), (/), (*), (-) } + let xval = coerceNumberToWasmF64(WasmI32.toGrain(x): Number) + let yval = coerceNumberToWasmF64(WasmI32.toGrain(y): Number) + let yInfinite = yval == InfinityW || yval == -InfinityW + let out = if ( + yval == 0.0W || yInfinite && (xval == InfinityW || xval == -InfinityW) + ) { + newFloat64(NaNW) + } else if (yInfinite) { + newFloat64(xval) } else { - modval + newFloat64(xval - WasmF64.trunc(xval / yval) * yval) } - reducedInteger(result) + Memory.incRef(out) + out } else { - reducedInteger(WasmI64.remS(xval, yval)) + let xval = coerceNumberToWasmI64(WasmI32.toGrain(x): Number) + let yval = coerceNumberToWasmI64(WasmI32.toGrain(y): Number) + if (WasmI64.eqz(yval)) { + throw Exception.ModuloByZero + } + // We implement true modulo + if (xval < 0N && yval > 0N || xval > 0N && yval < 0N) { + let modval = WasmI64.remS(i64abs(xval), i64abs(yval)) + let result = if (modval != 0N) { + i64abs(yval) - modval * (if (yval < 0N) -1N else 1N) + } else { + modval + } + reducedInteger(result) + } else { + reducedInteger(WasmI64.remS(xval, yval)) + } } } From ed5a05dca641cbae65c65d334603971eb8f947b4 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 29 Jan 2024 12:34:05 -0500 Subject: [PATCH 2/3] chore: Cleanup extra `incRef` --- stdlib/runtime/numbers.gr | 1 - 1 file changed, 1 deletion(-) diff --git a/stdlib/runtime/numbers.gr b/stdlib/runtime/numbers.gr index 6a29da8bad..fb5c82ac0d 100644 --- a/stdlib/runtime/numbers.gr +++ b/stdlib/runtime/numbers.gr @@ -1605,7 +1605,6 @@ let numberMod = (x, y) => { } else { newFloat64(xval - WasmF64.trunc(xval / yval) * yval) } - Memory.incRef(out) out } else { let xval = coerceNumberToWasmI64(WasmI32.toGrain(x): Number) From 3e8a06dd03af9129999d70bdab6052ecc3d82b15 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 29 Jan 2024 16:01:06 -0500 Subject: [PATCH 3/3] chore: Apply suggestions from code review --- stdlib/runtime/numbers.gr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/runtime/numbers.gr b/stdlib/runtime/numbers.gr index fb5c82ac0d..7cf980feb1 100644 --- a/stdlib/runtime/numbers.gr +++ b/stdlib/runtime/numbers.gr @@ -1585,6 +1585,7 @@ let i64abs = x => { from WasmI64 use { (-), (>=) } if (x >= 0N) x else 0N - x } + @unsafe let numberMod = (x, y) => { from WasmI64 use { (!=), (-), (*), (<), (>) } @@ -1596,7 +1597,7 @@ let numberMod = (x, y) => { let xval = coerceNumberToWasmF64(WasmI32.toGrain(x): Number) let yval = coerceNumberToWasmF64(WasmI32.toGrain(y): Number) let yInfinite = yval == InfinityW || yval == -InfinityW - let out = if ( + if ( yval == 0.0W || yInfinite && (xval == InfinityW || xval == -InfinityW) ) { newFloat64(NaNW) @@ -1605,7 +1606,6 @@ let numberMod = (x, y) => { } else { newFloat64(xval - WasmF64.trunc(xval / yval) * yval) } - out } else { let xval = coerceNumberToWasmI64(WasmI32.toGrain(x): Number) let yval = coerceNumberToWasmI64(WasmI32.toGrain(y): Number)