From 450c4f866e36168f913c65de8d7ce55e26e1f3ef Mon Sep 17 00:00:00 2001 From: Robert Bennett Date: Tue, 3 Dec 2024 00:06:22 -0500 Subject: [PATCH 1/5] Added constant pi and several math functions Added: * pi * atan2 * deg2rad, rad2deg * hypot * log2, log10 --- core/desugarer.cpp | 3 +- core/vm.cpp | 8 +++++ doc/_stdlib_gen/stdlib-content.jsonnet | 9 +++++ doc/ref/spec.html | 3 ++ doc/ref/stdlib.html | 5 +++ stdlib/std.jsonnet | 11 ++++++ test_suite/stdlib.jsonnet | 48 +++++++++++++++++--------- 7 files changed, 70 insertions(+), 17 deletions(-) diff --git a/core/desugarer.cpp b/core/desugarer.cpp index 6c38eb913..635dd6abd 100644 --- a/core/desugarer.cpp +++ b/core/desugarer.cpp @@ -36,7 +36,7 @@ struct BuiltinDecl { std::vector params; }; -static unsigned long max_builtin = 38; +static unsigned long max_builtin = 39; BuiltinDecl jsonnet_builtin_decl(unsigned long builtin) { switch (builtin) { @@ -79,6 +79,7 @@ BuiltinDecl jsonnet_builtin_decl(unsigned long builtin) case 36: return {U"parseYaml", {U"str"}}; case 37: return {U"encodeUTF8", {U"str"}}; case 38: return {U"decodeUTF8", {U"arr"}}; + case 39: return {U"atan2", {U"y", U"x"}}; default: std::cerr << "INTERNAL ERROR: Unrecognized builtin function: " << builtin << std::endl; std::abort(); diff --git a/core/vm.cpp b/core/vm.cpp index 0e21c82aa..8ff6039af 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -937,6 +937,7 @@ class Interpreter { builtins["parseYaml"] = &Interpreter::builtinParseYaml; builtins["encodeUTF8"] = &Interpreter::builtinEncodeUTF8; builtins["decodeUTF8"] = &Interpreter::builtinDecodeUTF8; + builtins["atan2"] = &Interpreter::builtinAtan2; DesugaredObject *stdlib = makeStdlibAST(alloc, "__internal__"); jsonnet_static_analysis(stdlib); @@ -1099,6 +1100,13 @@ class Interpreter { return nullptr; } + const AST *builtinAtan2(const LocationRange &loc, const std::vector &args) + { + validateBuiltinArgs(loc, "atan2", args, {Value::NUMBER, Value::NUMBER}); + scratch = makeNumberCheck(loc, std::atan2(args[0].v.d, args[1].v.d)); + return nullptr; + } + const AST *builtinType(const LocationRange &, const std::vector &args) { switch (args[0].t) { diff --git a/doc/_stdlib_gen/stdlib-content.jsonnet b/doc/_stdlib_gen/stdlib-content.jsonnet index c36e1c5a8..27b1d63e2 100644 --- a/doc/_stdlib_gen/stdlib-content.jsonnet +++ b/doc/_stdlib_gen/stdlib-content.jsonnet @@ -98,6 +98,8 @@ local html = import 'html.libsonnet';
    std.pow(x, n)
    std.exp(x)
    std.log(x)
+
    std.log2(x)
+
    std.log10(x)
    std.exponent(x)
    std.mantissa(x)
    std.floor(x)
@@ -109,12 +111,19 @@ local html = import 'html.libsonnet';
    std.asin(x)
    std.acos(x)
    std.atan(x)
+
    std.atan2(y, x)
+
    std.deg2rad(x)
+
    std.rad2deg(x)
+
    std.hypot(a, b)
    std.round(x)
    std.isEven(x)
    std.isOdd(x)
    std.isInteger(x)
    std.isDecimal(x)
+

+ The constant std.pi is also available. +

The function std.mod(a, b) is what the % operator is desugared to. It performs modulo arithmetic if the left hand side is a number, or if the left hand side is a string, diff --git a/doc/ref/spec.html b/doc/ref/spec.html index 9cfd6c01b..a9d7b852e 100644 --- a/doc/ref/spec.html +++ b/doc/ref/spec.html @@ -2825,7 +2825,10 @@

Execution

std.asin(x), std.acos(x), std.atan(x), + std.atan2(y, x), std.log(x), + std.log2(x), + std.log10(x), std.exp(x), std.mantissa(x), std.exponent(x) and diff --git a/doc/ref/stdlib.html b/doc/ref/stdlib.html index dc18e227d..79703bbbb 100644 --- a/doc/ref/stdlib.html +++ b/doc/ref/stdlib.html @@ -234,6 +234,8 @@

    std.pow(x, n)
    std.exp(x)
    std.log(x)
+
    std.log2(x)
+
    std.log10(x)
    std.exponent(x)
    std.mantissa(x)
    std.floor(x)
@@ -245,6 +247,9 @@

    std.asin(x)
    std.acos(x)
    std.atan(x)
+
    std.atan2(y, x)
+
    std.deg2rad(x)
+
    std.rad2deg(x)
    std.round(x)
    std.isEven(x)
    std.isOdd(x)
diff --git a/stdlib/std.jsonnet b/stdlib/std.jsonnet index fccbe26d5..22a5abc91 100644 --- a/stdlib/std.jsonnet +++ b/stdlib/std.jsonnet @@ -261,6 +261,17 @@ limitations under the License. else error 'Operator % cannot be used on types ' + std.type(a) + ' and ' + std.type(b) + '.', + // this is the most precision that will fit in a f64 + pi:: 3.14159265358979311600, + + deg2rad(x):: x * std.pi / 180, + rad2deg(x):: x * 180 / std.pi, + + hypot(a, b):: std.sqrt(a * a + b * b), + + log2(x):: std.log(x) / std.log(2), + log10(x):: std.log(x) / std.log(10), + map(func, arr):: if !std.isFunction(func) then error ('std.map first param must be function, got ' + std.type(func)) diff --git a/test_suite/stdlib.jsonnet b/test_suite/stdlib.jsonnet index fc1161ed5..1ea325c68 100644 --- a/test_suite/stdlib.jsonnet +++ b/test_suite/stdlib.jsonnet @@ -56,31 +56,47 @@ std.assertEqual(std.abs(33), 33) && std.assertEqual(std.abs(-33), 33) && std.assertEqual(std.abs(0), 0) && -// Ordinary (non-test) code can define pi as 2*std.acos(0) -local pi = 3.14159265359; - -assertClose(std.sin(0.0 * pi), 0) && -assertClose(std.sin(0.5 * pi), 1) && -assertClose(std.sin(1.0 * pi), 0) && -assertClose(std.sin(1.5 * pi), -1) && -assertClose(std.sin(2.0 * pi), 0) && -assertClose(std.cos(0.0 * pi), 1) && -assertClose(std.cos(0.5 * pi), 0) && -assertClose(std.cos(1.0 * pi), -1) && -assertClose(std.cos(1.5 * pi), 0) && -assertClose(std.cos(2.0 * pi), 1) && +assertClose(std.sin(0.0 * std.pi), 0) && +assertClose(std.sin(0.5 * std.pi), 1) && +assertClose(std.sin(1.0 * std.pi), 0) && +assertClose(std.sin(1.5 * std.pi), -1) && +assertClose(std.sin(2.0 * std.pi), 0) && +assertClose(std.cos(0.0 * std.pi), 1) && +assertClose(std.cos(0.5 * std.pi), 0) && +assertClose(std.cos(1.0 * std.pi), -1) && +assertClose(std.cos(1.5 * std.pi), 0) && +assertClose(std.cos(2.0 * std.pi), 1) && assertClose(std.tan(0), 0) && -assertClose(std.tan(0.25 * pi), 1) && +assertClose(std.tan(0.25 * std.pi), 1) && assertClose(std.asin(0), 0) && assertClose(std.acos(1), 0) && -assertClose(std.asin(1), 0.5 * pi) && -assertClose(std.acos(0), 0.5 * pi) && +assertClose(std.asin(1), 0.5 * std.pi) && +assertClose(std.acos(0), 0.5 * std.pi) && assertClose(std.atan(0), 0) && +assertClose(std.atan2(1, 1), std.pi / 4) && +assertClose(std.atan2(-1, 1), -std.pi / 4) && +assertClose(std.atan2(1.2, -3.8), 2.835713782184941) && // arbitrary, done on a calculator +assertClose(std.deg2rad(0), 0) && +assertClose(std.deg2rad(45), std.pi / 4) && +assertClose(std.deg2rad(90), std.pi / 2) && +assertClose(std.deg2rad(172), 3.0019663134302466) && // arbitrary, done on a calculator +assertClose(std.rad2deg(std.pi / 4), 45) && +assertClose(std.rad2deg(std.pi / 2), 90) && +assertClose(std.rad2deg(3.0019663134302466), 172) && // arbitrary, done on a calculator +assertClose(std.hypot(3, 4), 5) && +assertClose(std.hypot(5, 12), 13) && +assertClose(std.hypot(1, 1), std.sqrt(2)) && assertClose(std.log(std.exp(5)), 5) && assertClose(std.mantissa(1), 0.5) && assertClose(std.exponent(1), 1) && assertClose(std.mantissa(128), 0.5) && assertClose(std.exponent(128), 8) && +assertClose(std.log2(std.pow(2, -5)), -5) && +assertClose(std.log2(std.pow(2, 0)), 0) && +assertClose(std.log2(std.pow(2, std.pi)), std.pi) && +assertClose(std.log10(std.pow(10, -5)), -5) && +assertClose(std.log10(std.pow(10, 0)), 0) && +assertClose(std.log10(std.pow(10, std.pi)), std.pi) && std.assertEqual(std.clamp(-3, 0, 5), 0) && std.assertEqual(std.clamp(4, 0, 5), 4) && From d7cf0b65183a3ae2b6de0161b362df9913d4fa85 Mon Sep 17 00:00:00 2001 From: Robert Bennett Date: Tue, 3 Dec 2024 00:13:54 -0500 Subject: [PATCH 2/5] updated email address From eb722e7b7709c004b7a673b6c9123dad4bc8f105 Mon Sep 17 00:00:00 2001 From: Robert Bennett Date: Tue, 3 Dec 2024 00:19:10 -0500 Subject: [PATCH 3/5] Fixed potential overflow in hypot --- stdlib/std.jsonnet | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/stdlib/std.jsonnet b/stdlib/std.jsonnet index 22a5abc91..c69dfc415 100644 --- a/stdlib/std.jsonnet +++ b/stdlib/std.jsonnet @@ -267,7 +267,15 @@ limitations under the License. deg2rad(x):: x * std.pi / 180, rad2deg(x):: x * 180 / std.pi, - hypot(a, b):: std.sqrt(a * a + b * b), + hypot(a, b):: + // copied from + // https://www.johndcook.com/blog/2010/06/02/whats-so-hard-about-finding-a-hypotenuse/ + local a_abs = std.abs(a); + local b_abs = std.abs(b); + local max = std.max(a_abs, b_abs); + local min = std.min(a_abs, b_abs); + local r = min / max; + max * std.sqrt(1 + r * r), log2(x):: std.log(x) / std.log(2), log10(x):: std.log(x) / std.log(10), From 05859b9098335275a7333f99e388f4be7ecaf99f Mon Sep 17 00:00:00 2001 From: Robert Bennett Date: Tue, 3 Dec 2024 00:21:58 -0500 Subject: [PATCH 4/5] Moved hypot to native function to guarantee correct implementation --- core/desugarer.cpp | 3 ++- core/vm.cpp | 8 ++++++++ stdlib/std.jsonnet | 10 ---------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/desugarer.cpp b/core/desugarer.cpp index 635dd6abd..494960032 100644 --- a/core/desugarer.cpp +++ b/core/desugarer.cpp @@ -36,7 +36,7 @@ struct BuiltinDecl { std::vector params; }; -static unsigned long max_builtin = 39; +static unsigned long max_builtin = 40; BuiltinDecl jsonnet_builtin_decl(unsigned long builtin) { switch (builtin) { @@ -80,6 +80,7 @@ BuiltinDecl jsonnet_builtin_decl(unsigned long builtin) case 37: return {U"encodeUTF8", {U"str"}}; case 38: return {U"decodeUTF8", {U"arr"}}; case 39: return {U"atan2", {U"y", U"x"}}; + case 40: return {U"hypot", {U"a", U"b"}}; default: std::cerr << "INTERNAL ERROR: Unrecognized builtin function: " << builtin << std::endl; std::abort(); diff --git a/core/vm.cpp b/core/vm.cpp index 8ff6039af..41e5b758d 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -938,6 +938,7 @@ class Interpreter { builtins["encodeUTF8"] = &Interpreter::builtinEncodeUTF8; builtins["decodeUTF8"] = &Interpreter::builtinDecodeUTF8; builtins["atan2"] = &Interpreter::builtinAtan2; + builtins["hypot"] = &Interpreter::builtinHypot; DesugaredObject *stdlib = makeStdlibAST(alloc, "__internal__"); jsonnet_static_analysis(stdlib); @@ -1107,6 +1108,13 @@ class Interpreter { return nullptr; } + const AST *builtinHypot(const LocationRange &loc, const std::vector &args) + { + validateBuiltinArgs(loc, "atan2", args, {Value::NUMBER, Value::NUMBER}); + scratch = makeNumberCheck(loc, std::hypot(args[0].v.d, args[1].v.d)); + return nullptr; + } + const AST *builtinType(const LocationRange &, const std::vector &args) { switch (args[0].t) { diff --git a/stdlib/std.jsonnet b/stdlib/std.jsonnet index c69dfc415..ec7ab8f4d 100644 --- a/stdlib/std.jsonnet +++ b/stdlib/std.jsonnet @@ -267,16 +267,6 @@ limitations under the License. deg2rad(x):: x * std.pi / 180, rad2deg(x):: x * 180 / std.pi, - hypot(a, b):: - // copied from - // https://www.johndcook.com/blog/2010/06/02/whats-so-hard-about-finding-a-hypotenuse/ - local a_abs = std.abs(a); - local b_abs = std.abs(b); - local max = std.max(a_abs, b_abs); - local min = std.min(a_abs, b_abs); - local r = min / max; - max * std.sqrt(1 + r * r), - log2(x):: std.log(x) / std.log(2), log10(x):: std.log(x) / std.log(10), From 53a359cd97076ebca22d9af5468b405e7cc33570 Mon Sep 17 00:00:00 2001 From: Robert Bennett Date: Fri, 6 Dec 2024 23:19:07 -0500 Subject: [PATCH 5/5] Fixed wrong validated name in `builtinHypot` --- core/vm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm.cpp b/core/vm.cpp index 41e5b758d..47a4bb8d3 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -1110,7 +1110,7 @@ class Interpreter { const AST *builtinHypot(const LocationRange &loc, const std::vector &args) { - validateBuiltinArgs(loc, "atan2", args, {Value::NUMBER, Value::NUMBER}); + validateBuiltinArgs(loc, "hypot", args, {Value::NUMBER, Value::NUMBER}); scratch = makeNumberCheck(loc, std::hypot(args[0].v.d, args[1].v.d)); return nullptr; }