From 2ae5f99d44a2d13a2ba30573ce48296c0add5b8d Mon Sep 17 00:00:00 2001 From: mmsqe Date: Wed, 3 Sep 2025 09:01:18 +0800 Subject: [PATCH 1/4] feat: add extVarWithDefault as builtin to support extVar with a default value --- core/desugarer.cpp | 3 ++- core/vm.cpp | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/core/desugarer.cpp b/core/desugarer.cpp index 3ed2b453..c5fa0850 100644 --- a/core/desugarer.cpp +++ b/core/desugarer.cpp @@ -36,7 +36,7 @@ struct BuiltinDecl { std::vector params; }; -static unsigned long max_builtin = 40; +static unsigned long max_builtin = 41; BuiltinDecl jsonnet_builtin_decl(unsigned long builtin) { switch (builtin) { @@ -81,6 +81,7 @@ BuiltinDecl jsonnet_builtin_decl(unsigned long builtin) 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"}}; + case 41: return {U"extVarWithDefault", {U"x", U"default"}}; default: std::cerr << "INTERNAL ERROR: Unrecognized builtin function: " << builtin << std::endl; std::abort(); diff --git a/core/vm.cpp b/core/vm.cpp index 0d702de4..87ab93c0 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -973,6 +973,7 @@ class Interpreter { builtins["decodeUTF8"] = &Interpreter::builtinDecodeUTF8; builtins["atan2"] = &Interpreter::builtinAtan2; builtins["hypot"] = &Interpreter::builtinHypot; + builtins["extVarWithDefault"] = &Interpreter::builtinExtVarWithDefault; DesugaredObject *stdlib = makeStdlibAST(alloc, "__internal__"); jsonnet_static_analysis(stdlib); @@ -1368,6 +1369,50 @@ class Interpreter { } } + const AST *builtinExtVarWithDefault(const LocationRange &loc, const std::vector &args) + { + if (args.size() != 2) { + std::stringstream ss; + ss << "extVarWithDefault expects 2 arguments, got " << args.size(); + throw makeError(loc, ss.str()); + } + for (int i = 0; i < 2; ++i) { + Value::Type t = args[i].t; + if (t != Value::STRING && t != Value::NUMBER && t != Value::BOOLEAN && + t != Value::NULL_TYPE) { + std::stringstream ss; + ss << "extVarWithDefault argument " << (i + 1) + << " must be string, number, boolean, or null, got " << type_str(args[i]); + throw makeError(loc, ss.str()); + } + } + const UString &var = static_cast(args[0].v.h)->value; + std::string var8 = encode_utf8(var); + Value result; + auto it = externalVars.find(var8); + if (it == externalVars.end()) { + return nullptr; + } + const VmExt &ext = it->second; + if (ext.isCode) { + std::string filename = ""; + Tokens tokens = jsonnet_lex(filename, ext.data.c_str()); + AST *expr = jsonnet_parse(alloc, tokens); + jsonnet_desugar(alloc, expr, nullptr); + jsonnet_static_analysis(expr); + stack.pop(); + return expr; + } else { + result = makeString(decode_utf8(ext.data)); + } + if (result.t == Value::STRING) { + scratch = result; + return nullptr; + } + scratch = args[1]; + return nullptr; + } + const AST *builtinPrimitiveEquals(const LocationRange &loc, const std::vector &args) { if (args.size() != 2) { From 3b1fc2b6aac92828f643622a346b37aefd9c4591 Mon Sep 17 00:00:00 2001 From: mmsqe Date: Wed, 3 Sep 2025 09:02:16 +0800 Subject: [PATCH 2/4] test --- test_cmd/extWithDefault1.golden.stdout | 1 + test_cmd/extWithDefault2.golden.stdout | 1 + test_cmd/extWithDefault3.golden.stdout | 1 + test_cmd/extWithDefault4.golden.stdout | 1 + test_cmd/extWithDefault5.golden.stdout | 1 + test_cmd/extWithDefault6.golden.stdout | 5 +++++ test_cmd/extWithDefault7.golden.stdout | 1 + test_cmd/run_cmd_tests.sh | 21 +++++++++++++++++++++ 8 files changed, 32 insertions(+) create mode 100644 test_cmd/extWithDefault1.golden.stdout create mode 100644 test_cmd/extWithDefault2.golden.stdout create mode 100644 test_cmd/extWithDefault3.golden.stdout create mode 100644 test_cmd/extWithDefault4.golden.stdout create mode 100644 test_cmd/extWithDefault5.golden.stdout create mode 100644 test_cmd/extWithDefault6.golden.stdout create mode 100644 test_cmd/extWithDefault7.golden.stdout diff --git a/test_cmd/extWithDefault1.golden.stdout b/test_cmd/extWithDefault1.golden.stdout new file mode 100644 index 00000000..f27b76c5 --- /dev/null +++ b/test_cmd/extWithDefault1.golden.stdout @@ -0,0 +1 @@ +"1" diff --git a/test_cmd/extWithDefault2.golden.stdout b/test_cmd/extWithDefault2.golden.stdout new file mode 100644 index 00000000..f27b76c5 --- /dev/null +++ b/test_cmd/extWithDefault2.golden.stdout @@ -0,0 +1 @@ +"1" diff --git a/test_cmd/extWithDefault3.golden.stdout b/test_cmd/extWithDefault3.golden.stdout new file mode 100644 index 00000000..bfa9269a --- /dev/null +++ b/test_cmd/extWithDefault3.golden.stdout @@ -0,0 +1 @@ +"default" diff --git a/test_cmd/extWithDefault4.golden.stdout b/test_cmd/extWithDefault4.golden.stdout new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/test_cmd/extWithDefault4.golden.stdout @@ -0,0 +1 @@ +2 diff --git a/test_cmd/extWithDefault5.golden.stdout b/test_cmd/extWithDefault5.golden.stdout new file mode 100644 index 00000000..d224909d --- /dev/null +++ b/test_cmd/extWithDefault5.golden.stdout @@ -0,0 +1 @@ +"hi\n" diff --git a/test_cmd/extWithDefault6.golden.stdout b/test_cmd/extWithDefault6.golden.stdout new file mode 100644 index 00000000..f6ef3c11 --- /dev/null +++ b/test_cmd/extWithDefault6.golden.stdout @@ -0,0 +1,5 @@ +{ + "a": 1, + "b": 2, + "c": 3 +} diff --git a/test_cmd/extWithDefault7.golden.stdout b/test_cmd/extWithDefault7.golden.stdout new file mode 100644 index 00000000..1efb7c8c --- /dev/null +++ b/test_cmd/extWithDefault7.golden.stdout @@ -0,0 +1 @@ +"lib3_test.jsonnet" diff --git a/test_cmd/run_cmd_tests.sh b/test_cmd/run_cmd_tests.sh index 4aa7df53..dae67f8a 100755 --- a/test_cmd/run_cmd_tests.sh +++ b/test_cmd/run_cmd_tests.sh @@ -69,6 +69,27 @@ do_test "ext4" 0 --ext-code x=1+1 -e 'std.extVar("x")' do_test "ext5" 0 --ext-str-file "x=test.txt" -e 'std.extVar("x")' do_test "ext6" 0 --ext-code-file "x=test.jsonnet" -e 'std.extVar("x")' do_test "ext7" 0 --ext-code-file "x=lib1/lib3_test.jsonnet" -e 'std.extVar("x")' +if do_test "extWithDefault1" 0 --ext-str x=1 -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault1" "out/extWithDefault1/stdout" "extWithDefault1.golden.stdout" +fi +if do_test "extWithDefault2" 0 -V x=1 -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault2" "out/extWithDefault2/stdout" "extWithDefault2.golden.stdout" +fi +if do_test "extWithDefault3" 0 -V y=1 -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault3" "out/extWithDefault3/stdout" "extWithDefault3.golden.stdout" +fi +if do_test "extWithDefault4" 0 --ext-code x=1+1 -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault4" "out/extWithDefault4/stdout" "extWithDefault4.golden.stdout" +fi +if do_test "extWithDefault5" 0 --ext-str-file "x=test.txt" -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault5" "out/extWithDefault5/stdout" "extWithDefault5.golden.stdout" +fi +if do_test "extWithDefault6" 0 --ext-code-file "x=test.jsonnet" -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault6" "out/extWithDefault6/stdout" "extWithDefault6.golden.stdout" +fi +if do_test "extWithDefault7" 0 --ext-code-file "x=lib1/lib3_test.jsonnet" -e 'std.extVarWithDefault("x", "default")'; then + check_file "extWithDefault7" "out/extWithDefault7/stdout" "extWithDefault7.golden.stdout" +fi do_test "tla1" 0 --tla-str x=1 -e 'function(x) x' do_test "tla2" 0 -A x=1 -e 'function(x) x' do_test "tla3" 1 -A y=1 -e 'function(x) x' From 0020f5eb00a3a636668e1e8ea2d57068cabcaf33 Mon Sep 17 00:00:00 2001 From: mmsqe Date: Wed, 3 Sep 2025 09:02:32 +0800 Subject: [PATCH 3/4] reuse --- core/vm.cpp | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/core/vm.cpp b/core/vm.cpp index 87ab93c0..84f89d10 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -1344,15 +1344,11 @@ class Interpreter { return nullptr; } - const AST *builtinExtVar(const LocationRange &loc, const std::vector &args) + const AST *getExtVarOrValue(const std::string &var8, Value *outValue = nullptr) { - validateBuiltinArgs(loc, "extVar", args, {Value::STRING}); - const UString &var = static_cast(args[0].v.h)->value; - std::string var8 = encode_utf8(var); auto it = externalVars.find(var8); if (it == externalVars.end()) { - std::string msg = "undefined external variable: " + var8; - throw makeError(loc, msg); + return nullptr; } const VmExt &ext = it->second; if (ext.isCode) { @@ -1364,8 +1360,26 @@ class Interpreter { stack.pop(); return expr; } else { - scratch = makeString(decode_utf8(ext.data)); + if (outValue) *outValue = makeString(decode_utf8(ext.data)); + return nullptr; + } + } + + const AST *builtinExtVar(const LocationRange &loc, const std::vector &args) + { + validateBuiltinArgs(loc, "extVar", args, {Value::STRING}); + const UString &var = static_cast(args[0].v.h)->value; + std::string var8 = encode_utf8(var); + Value result; + const AST *expr = getExtVarOrValue(var8, &result); + if (expr) { + return expr; + } else if (result.t == Value::STRING) { + scratch = result; return nullptr; + } else { + std::string msg = "undefined external variable: " + var8; + throw makeError(loc, msg); } } @@ -1389,23 +1403,10 @@ class Interpreter { const UString &var = static_cast(args[0].v.h)->value; std::string var8 = encode_utf8(var); Value result; - auto it = externalVars.find(var8); - if (it == externalVars.end()) { - return nullptr; - } - const VmExt &ext = it->second; - if (ext.isCode) { - std::string filename = ""; - Tokens tokens = jsonnet_lex(filename, ext.data.c_str()); - AST *expr = jsonnet_parse(alloc, tokens); - jsonnet_desugar(alloc, expr, nullptr); - jsonnet_static_analysis(expr); - stack.pop(); + const AST *expr = getExtVarOrValue(var8, &result); + if (expr) { return expr; - } else { - result = makeString(decode_utf8(ext.data)); - } - if (result.t == Value::STRING) { + } else if (result.t == Value::STRING) { scratch = result; return nullptr; } From 4708615b98bdebcccf7791eb1dc5ecd1a43a31e4 Mon Sep 17 00:00:00 2001 From: mmsqe Date: Wed, 3 Sep 2025 09:07:10 +0800 Subject: [PATCH 4/4] support obj --- core/vm.cpp | 4 ++-- test_suite/stdlib.jsonnet | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/vm.cpp b/core/vm.cpp index 84f89d10..0530306c 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -1393,10 +1393,10 @@ class Interpreter { for (int i = 0; i < 2; ++i) { Value::Type t = args[i].t; if (t != Value::STRING && t != Value::NUMBER && t != Value::BOOLEAN && - t != Value::NULL_TYPE) { + t != Value::NULL_TYPE && t != Value::OBJECT) { std::stringstream ss; ss << "extVarWithDefault argument " << (i + 1) - << " must be string, number, boolean, or null, got " << type_str(args[i]); + << " must be string, number, boolean, null, or object, got " << type_str(args[i]); throw makeError(loc, ss.str()); } } diff --git a/test_suite/stdlib.jsonnet b/test_suite/stdlib.jsonnet index 9d11766a..9fa2d7d8 100644 --- a/test_suite/stdlib.jsonnet +++ b/test_suite/stdlib.jsonnet @@ -516,6 +516,11 @@ std.assertEqual(std.toString(std.extVar('var2')), '{"x": 1, "y": 2}') && std.assertEqual(std.extVar('var2'), { x: 1, y: 2 }) && std.assertEqual(std.extVar('var2') { x+: 2 }.x, 3) && +std.assertEqual(std.extVarWithDefault('var1', 'default'), 'test') && +std.assertEqual(std.extVarWithDefault('missing', 'default'), 'default') && +std.assertEqual(std.extVarWithDefault('var2', { x: 0, y: 0 }), { x: 1, y: 2 }) && +std.assertEqual(std.extVarWithDefault('missingObj', { x: 0, y: 0 }), { x: 0, y: 0 }) && + std.assertEqual(std.split('foo/bar', '/'), ['foo', 'bar']) && std.assertEqual(std.split('/foo/', '/'), ['', 'foo', '']) && std.assertEqual(std.split('foo/_bar', '/_'), ['foo', 'bar']) &&