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..0530306c 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); @@ -1343,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) { @@ -1363,9 +1360,58 @@ 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); + } + } + + 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 && t != Value::OBJECT) { + std::stringstream ss; + ss << "extVarWithDefault argument " << (i + 1) + << " must be string, number, boolean, null, or object, 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; + const AST *expr = getExtVarOrValue(var8, &result); + if (expr) { + return expr; + } else if (result.t == Value::STRING) { + scratch = result; return nullptr; } + scratch = args[1]; + return nullptr; } const AST *builtinPrimitiveEquals(const LocationRange &loc, const std::vector &args) 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' 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']) &&