summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--lib.c63
-rw-r--r--source.c2
-rw-r--r--tests/custom/03_stdlib/01_chr27
-rw-r--r--tests/custom/03_stdlib/02_die49
-rw-r--r--tests/custom/03_stdlib/03_exists38
-rw-r--r--tests/custom/03_stdlib/04_exit36
-rw-r--r--tests/custom/03_stdlib/05_getenv30
-rw-r--r--tests/custom/03_stdlib/06_filter113
-rw-r--r--tests/custom/03_stdlib/07_hex33
-rw-r--r--tests/custom/03_stdlib/08_int42
-rw-r--r--tests/custom/03_stdlib/09_join31
-rw-r--r--tests/custom/03_stdlib/10_keys21
-rw-r--r--tests/custom/03_stdlib/11_lc27
-rw-r--r--tests/custom/03_stdlib/12_map111
-rw-r--r--tests/custom/03_stdlib/13_ord46
-rw-r--r--tests/custom/03_stdlib/14_type43
-rw-r--r--tests/custom/03_stdlib/15_reverse31
-rw-r--r--tests/custom/03_stdlib/16_sort108
-rw-r--r--tests/custom/03_stdlib/17_splice98
-rw-r--r--tests/custom/03_stdlib/18_split87
-rw-r--r--tests/custom/03_stdlib/19_substr78
-rw-r--r--tests/custom/03_stdlib/20_time18
-rw-r--r--tests/custom/03_stdlib/21_uc27
-rw-r--r--tests/custom/03_stdlib/22_uchr19
-rw-r--r--tests/custom/03_stdlib/23_values35
-rw-r--r--tests/custom/03_stdlib/24_trim46
-rw-r--r--tests/custom/03_stdlib/25_ltrim46
-rw-r--r--tests/custom/03_stdlib/26_rtrim46
-rw-r--r--tests/custom/03_stdlib/27_sprintf550
-rw-r--r--tests/custom/03_stdlib/28_printf526
-rw-r--r--tests/custom/03_stdlib/29_require164
-rw-r--r--tests/custom/03_stdlib/30_iptoarr46
-rw-r--r--tests/custom/03_stdlib/31_arrtoip57
-rw-r--r--tests/custom/03_stdlib/32_match55
-rw-r--r--tests/custom/03_stdlib/33_replace207
-rw-r--r--tests/custom/03_stdlib/34_json110
-rw-r--r--tests/custom/03_stdlib/35_include173
-rw-r--r--tests/custom/03_stdlib/36_render167
-rw-r--r--tests/custom/03_stdlib/37_warn40
-rw-r--r--tests/custom/03_stdlib/38_system148
-rw-r--r--tests/custom/03_stdlib/39_trace79
-rw-r--r--tests/custom/03_stdlib/40_proto89
-rw-r--r--tests/custom/03_stdlib/41_sleep46
-rw-r--r--tests/custom/03_stdlib/42_assert30
-rw-r--r--tests/custom/03_stdlib/43_regexp117
-rw-r--r--tests/custom/03_stdlib/44_wildcard43
-rw-r--r--tests/custom/03_stdlib/45_sourcepath69
-rw-r--r--tests/custom/03_stdlib/46_min28
-rw-r--r--tests/custom/03_stdlib/47_max28
-rw-r--r--tests/custom/03_stdlib/48_b64dec31
-rw-r--r--tests/custom/03_stdlib/49_b64enc25
-rw-r--r--tests/custom/03_stdlib/50_uniq66
-rw-r--r--tests/custom/04_bugs/05_duplicate_resource_type (renamed from tests/custom/04_bugs/05_duplicate_ressource_type)12
-rwxr-xr-xtests/custom/run_tests.sh21
-rw-r--r--vm.c16
55 files changed, 4263 insertions, 31 deletions
diff --git a/lib.c b/lib.c
index e204ded..5ad08e7 100644
--- a/lib.c
+++ b/lib.c
@@ -927,9 +927,18 @@ uc_split(uc_vm_t *vm, size_t nargs)
if (res == REG_NOMATCH)
break;
- ucv_array_push(arr, ucv_string_new_length(splitstr, pmatch.rm_so));
+ if (pmatch.rm_so != pmatch.rm_eo) {
+ ucv_array_push(arr, ucv_string_new_length(splitstr, pmatch.rm_so));
+ splitstr += pmatch.rm_eo;
+ }
+ else if (*splitstr) {
+ ucv_array_push(arr, ucv_string_new_length(splitstr, 1));
+ splitstr++;
+ }
+ else {
+ goto out;
+ }
- splitstr += pmatch.rm_eo;
eflags |= REG_NOTBOL;
}
@@ -956,6 +965,7 @@ uc_split(uc_vm_t *vm, size_t nargs)
return NULL;
}
+out:
return arr;
}
@@ -1794,7 +1804,13 @@ uc_match(uc_vm_t *vm, size_t nargs)
ucv_array_push(rv, m);
- p += pmatch[0].rm_eo;
+ if (pmatch[0].rm_so != pmatch[0].rm_eo)
+ p += pmatch[0].rm_eo;
+ else if (*p)
+ p++;
+ else
+ break;
+
eflags |= REG_NOTBOL;
}
else {
@@ -1959,7 +1975,12 @@ uc_replace(uc_vm_t *vm, size_t nargs)
uc_replace_str(vm, replace, p, pmatch, ARRAY_SIZE(pmatch), resbuf);
}
- p += pmatch[0].rm_eo;
+ if (pmatch[0].rm_so != pmatch[0].rm_eo)
+ p += pmatch[0].rm_eo;
+ else if (*p)
+ ucv_stringbuf_addstr(resbuf, p++, 1);
+ else
+ break;
if (re->global)
eflags |= REG_NOTBOL;
@@ -1973,8 +1994,10 @@ uc_replace(uc_vm_t *vm, size_t nargs)
pt = uc_cast_string(vm, &pattern, &pt_freeable);
pl = strlen(pt);
- for (l = p = sb; *p; p++) {
- if (!strncmp(p, pt, pl)) {
+ l = p = sb;
+
+ while (true) {
+ if (pl == 0 || !strncmp(p, pt, pl)) {
ucv_stringbuf_addstr(resbuf, l, p - l);
pmatch[0].rm_so = p - l;
@@ -1997,9 +2020,17 @@ uc_replace(uc_vm_t *vm, size_t nargs)
uc_replace_str(vm, replace, l, pmatch, 1, resbuf);
}
- l = p + pl;
- p += pl - 1;
+ if (pl) {
+ l = p + pl;
+ p += pl - 1;
+ }
+ else {
+ l = p;
+ }
}
+
+ if (!*p++)
+ break;
}
ucv_stringbuf_addstr(resbuf, l, strlen(l));
@@ -2017,10 +2048,10 @@ uc_replace(uc_vm_t *vm, size_t nargs)
static uc_value_t *
uc_json(uc_vm_t *vm, size_t nargs)
{
- uc_value_t *rv, *src = uc_fn_arg(0);
+ uc_value_t *rv = NULL, *src = uc_fn_arg(0);
struct json_tokener *tok = NULL;
enum json_tokener_error err;
- json_object *jso;
+ json_object *jso = NULL;
const char *str;
size_t len;
@@ -2042,32 +2073,30 @@ uc_json(uc_vm_t *vm, size_t nargs)
err = json_tokener_get_error(tok);
if (err == json_tokener_continue) {
- json_object_put(jso);
uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
"Unexpected end of string in JSON data");
- return NULL;
+ goto out;
}
else if (err != json_tokener_success) {
- json_object_put(jso);
uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
"Failed to parse JSON string: %s",
json_tokener_error_desc(err));
- return NULL;
+ goto out;
}
else if (json_tokener_get_parse_end(tok) < len) {
- json_object_put(jso);
uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
"Trailing garbage after JSON data");
- return NULL;
+ goto out;
}
- json_tokener_free(tok);
rv = ucv_from_json(vm, jso);
+out:
+ json_tokener_free(tok);
json_object_put(jso);
return rv;
diff --git a/source.c b/source.c
index f2bd8bf..c4060eb 100644
--- a/source.c
+++ b/source.c
@@ -126,7 +126,7 @@ uc_source_put(uc_source_t *source)
uc_source_type_t
uc_source_type_test(uc_source_t *source)
{
- union { char s[sizeof(uint32_t)]; uint32_t n; } buf;
+ union { char s[sizeof(uint32_t)]; uint32_t n; } buf = { 0 };
uc_source_type_t type = UC_SOURCE_TYPE_PLAIN;
FILE *fp = source->fp;
size_t rlen;
diff --git a/tests/custom/03_stdlib/01_chr b/tests/custom/03_stdlib/01_chr
new file mode 100644
index 0000000..17163e3
--- /dev/null
+++ b/tests/custom/03_stdlib/01_chr
@@ -0,0 +1,27 @@
+The `chr()` function converts each given numeric value into a character
+and returns the resulting string, e.g. passing 97, 98 and 99 will yield
+the string `abc`.
+
+Negative numeric values and values which cannot be converted to integers
+are treated as `0`, values larger than `255` are capped to `255`.
+
+The resulting string will have the same length as the amount of arguments
+passed to the `chr()` function.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ chr(),
+ chr(97, 98, 99),
+ chr(-1, false, null, [], {}, "0x41", 66.5, 1000)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "",
+ "abc",
+ "\u0000\u0000\u0000\u0000\u0000AB\u00ff"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/02_die b/tests/custom/03_stdlib/02_die
new file mode 100644
index 0000000..344069a
--- /dev/null
+++ b/tests/custom/03_stdlib/02_die
@@ -0,0 +1,49 @@
+The `die()` function triggers a user defined runtime exception when invoked,
+using the given value as exception message.
+
+The given message value is converted to a string internally if it is not a
+string already. If no message argument is given or if the message argument
+is `null`, the default message is `Died`.
+
+The function does not return.
+
+-- Testcase --
+{%
+ print("Before invoking die()\n");
+
+ die("An exception!");
+
+ print("After invoking die()\n");
+%}
+-- End --
+
+-- Expect stdout --
+Before invoking die()
+-- End --
+
+-- Expect stderr --
+An exception!
+In line 4, byte 21:
+
+ ` die("An exception!");`
+ Near here -------------^
+
+
+-- End --
+
+
+-- Testcase --
+{%
+ die();
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In line 2, byte 6:
+
+ ` die();`
+ ^-- Near here
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/03_exists b/tests/custom/03_stdlib/03_exists
new file mode 100644
index 0000000..3d71aa7
--- /dev/null
+++ b/tests/custom/03_stdlib/03_exists
@@ -0,0 +1,38 @@
+The `exists()` function checks the existence of the given key within the
+given object. If the object contains the given key, `true` is returned,
+otherwise `false`.
+
+If the object argument is not an object, `false` is returned as well.
+
+The key argument is converted to a string in case it is not one already.
+
+-- Testcase --
+{%
+ let obj = {
+ "foo": true,
+ "bar": false,
+ "false": null,
+ "123": "a number"
+ };
+
+ printf("%.J\n", [
+ exists(true, "test"),
+ exists(obj, "doesnotexists"),
+ exists(obj, "foo"),
+ exists(obj, "bar"),
+ exists(obj, !true),
+ exists(obj, 123)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ false,
+ false,
+ true,
+ true,
+ true,
+ true
+]
+-- End --
diff --git a/tests/custom/03_stdlib/04_exit b/tests/custom/03_stdlib/04_exit
new file mode 100644
index 0000000..1f941d1
--- /dev/null
+++ b/tests/custom/03_stdlib/04_exit
@@ -0,0 +1,36 @@
+The `exit()` function terminates the running program with the given exit
+code or 0 in case no argument is given or if the argument cannot be
+converted to an integer.
+
+The function does not return.
+
+-- Testcase --
+{%
+ print("Before invoking exit()\n");
+
+ exit();
+
+ print("After invoking exit()\n");
+%}
+-- End --
+
+-- Expect stdout --
+Before invoking exit()
+-- End --
+
+-- Expect exitcode --
+0
+-- End --
+
+
+Passing a code argument overrides the default "0" value.
+
+-- Testcase --
+{%
+ exit(123)
+%}
+-- End --
+
+-- Expect exitcode --
+123
+-- End --
diff --git a/tests/custom/03_stdlib/05_getenv b/tests/custom/03_stdlib/05_getenv
new file mode 100644
index 0000000..350e952
--- /dev/null
+++ b/tests/custom/03_stdlib/05_getenv
@@ -0,0 +1,30 @@
+The `getenv()` function returns the value of the given environment variable
+or `null` if either the given variable does not exist or if the given name
+argument is not a string.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ getenv("TEST_VARIABLE"),
+ getenv("EMPTY_VARIABLE"),
+ getenv("THIS_LIKELY_DOES_NOT_EXIST"),
+ getenv(123),
+ getenv(null)
+ ]);
+%}
+-- End --
+
+-- Vars --
+TEST_VARIABLE=Test Value
+EMPTY_VARIABLE=
+-- End --
+
+-- Expect stdout --
+[
+ "Test Value",
+ "",
+ null,
+ null,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/06_filter b/tests/custom/03_stdlib/06_filter
new file mode 100644
index 0000000..400db34
--- /dev/null
+++ b/tests/custom/03_stdlib/06_filter
@@ -0,0 +1,113 @@
+The `filter()` function filters the given array by invoking the specified
+callback for each item of the input array and only keeping items for which
+the callback returned a truish value.
+
+Returns the filtered copy of the input array, maintaining the original order
+of items. The input array is not modified.
+
+Returns `null` if the first argument is not an array.
+
+-- Testcase --
+{%
+ let numbers = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
+
+ printf("%.J\n",
+ filter(numbers, function(n) {
+ return (n % 2) == 0;
+ })
+ );
+%}
+-- End --
+
+-- Expect stdout --
+[
+ 0,
+ 2,
+ 4,
+ 6,
+ 8
+]
+-- End --
+
+
+Supplying an invalid callback will trigger an exception.
+
+-- Testcase --
+{%
+ filter([1, 2, 3], "not_a_function")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: left-hand side is not a function
+In line 2, byte 36:
+
+ ` filter([1, 2, 3], "not_a_function")`
+ Near here ----------------------------^
+
+
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", filter("not_an_array", function(i) { return i > 3 }));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+The callback is invoked with three argument for each item, the current item
+value, the index position of the item and the input array being mapped.
+
+-- Testcase --
+{%
+ let words = [ "foo", "bar", "baz", "qrx" ];
+
+ print(join("\n",
+ filter(words, function(word, idx, src) {
+ printf("word=%s, idx=%d, src=%J\n", word, idx, src);
+
+ return true;
+ })
+ ), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+word=foo, idx=0, src=[ "foo", "bar", "baz", "qrx" ]
+word=bar, idx=1, src=[ "foo", "bar", "baz", "qrx" ]
+word=baz, idx=2, src=[ "foo", "bar", "baz", "qrx" ]
+word=qrx, idx=3, src=[ "foo", "bar", "baz", "qrx" ]
+foo
+bar
+baz
+qrx
+-- End --
+
+
+Exceptions in the callback terminate the filter process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ filter([ 1, 2, 3 ], function() { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 39:
+ called from function filter ([C])
+ called from anonymous function ([stdin]:2:42)
+
+ ` filter([ 1, 2, 3 ], function() { die() });`
+ Near here -------------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/07_hex b/tests/custom/03_stdlib/07_hex
new file mode 100644
index 0000000..419970c
--- /dev/null
+++ b/tests/custom/03_stdlib/07_hex
@@ -0,0 +1,33 @@
+The `hex()` function converts the given hexadecimal string into a signed
+integer value and returns the resulting number.
+
+Returns `NaN` if the given argument is not a string, an empty string or
+a string containing non-hexadecimal digits.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ hex(),
+ hex(false),
+ hex(123),
+ hex(""),
+ hex("invalid"),
+ hex("deaf"),
+ hex("0x1000"),
+ hex("ffffffffffffffff")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "NaN",
+ "NaN",
+ "NaN",
+ "NaN",
+ "NaN",
+ 57007,
+ 4096,
+ 9223372036854775807
+]
+-- End --
diff --git a/tests/custom/03_stdlib/08_int b/tests/custom/03_stdlib/08_int
new file mode 100644
index 0000000..a6b5923
--- /dev/null
+++ b/tests/custom/03_stdlib/08_int
@@ -0,0 +1,42 @@
+The `int()` function converts the given value into a signed integer
+value and returns the resulting number.
+
+Returns `NaN` if the given argument is not convertible into a number.
+
+Returns `NaN` if the conversion result is out of range.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ int(),
+ int(false),
+ int(123),
+ int(456.789),
+ int(""),
+ int("invalid"),
+ int("deaf"),
+ int("0x1000"),
+ int("0xffffffffffffffff"),
+ int("0177"),
+ int("+145"),
+ int("-96")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ 0,
+ 0,
+ 123,
+ 456,
+ 0,
+ "NaN",
+ "NaN",
+ 4096,
+ "NaN",
+ 127,
+ "NaN",
+ -96
+]
+-- End --
diff --git a/tests/custom/03_stdlib/09_join b/tests/custom/03_stdlib/09_join
new file mode 100644
index 0000000..dac49c3
--- /dev/null
+++ b/tests/custom/03_stdlib/09_join
@@ -0,0 +1,31 @@
+The `join()` function constructs a string out of the given array by
+converting each array item into a string and then joining these substrings
+putting the given separator value in between. An empty array will result in
+an empty string.
+
+The separator argument is converted into a string in case it is not already
+a string value.
+
+Returns `null` if the given array argument is not an array value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ join("|", []),
+ join("|", [ 1, 2, 3 ]),
+ join("|", [ null, false, "" ]),
+ join(123, [ "a", "b", "c" ]),
+ join(123, { "not": "an", "array": "value" })
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "",
+ "1|2|3",
+ "null|false|",
+ "a123b123c",
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/10_keys b/tests/custom/03_stdlib/10_keys
new file mode 100644
index 0000000..46462a0
--- /dev/null
+++ b/tests/custom/03_stdlib/10_keys
@@ -0,0 +1,21 @@
+The `keys()` function returns an array containing all keys of the given
+dictionary value. The keys are sorted in declaration order.
+
+-- Testcase --
+{{ keys({ "foo": true, "bar": false, "qrx": 123 }) }}
+-- End --
+
+-- Expect stdout --
+[ "foo", "bar", "qrx" ]
+-- End --
+
+
+If the given argument is not a dictionary, the function returns `null`.
+
+-- Testcase --
+{{ keys(true) === null }}
+-- End --
+
+-- Expect stdout --
+true
+-- End --
diff --git a/tests/custom/03_stdlib/11_lc b/tests/custom/03_stdlib/11_lc
new file mode 100644
index 0000000..1ae3cb1
--- /dev/null
+++ b/tests/custom/03_stdlib/11_lc
@@ -0,0 +1,27 @@
+The `lc()` function turns each upper case character in the source string
+into lower case and returns the resulting copy.
+
+The input argument is converted to a string in case it is not already a
+string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ lc("This Will Be All Lowercased."),
+ lc([ "An", "array", "ABC" ]),
+ lc(123),
+ lc(false),
+ lc()
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "this will be all lowercased.",
+ "[ \"an\", \"array\", \"abc\" ]",
+ "123",
+ "false",
+ "null"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/12_map b/tests/custom/03_stdlib/12_map
new file mode 100644
index 0000000..508838d
--- /dev/null
+++ b/tests/custom/03_stdlib/12_map
@@ -0,0 +1,111 @@
+The `map()` function creates a new array from the given input array by
+invoking the specified callback for each item of the input array and
+putting the resulting return value into the new array.
+
+Returns the newly created array. The input array is not modified.
+
+Returns `null` if the first argument is not an array.
+
+-- Testcase --
+{%
+ let numbers = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
+
+ printf("%.J\n",
+ map(numbers, function(n) {
+ return (n * n);
+ })
+ );
+%}
+-- End --
+
+-- Expect stdout --
+[
+ 0,
+ 1,
+ 4,
+ 9,
+ 16,
+ 25,
+ 36,
+ 49,
+ 64,
+ 81
+]
+-- End --
+
+
+Supplying an invalid callback will trigger an exception.
+
+-- Testcase --
+{%
+ map([1, 2, 3], "not_a_function")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: left-hand side is not a function
+In line 2, byte 33:
+
+ ` map([1, 2, 3], "not_a_function")`
+ Near here -------------------------^
+
+
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", map("not_an_array", function(i) { return i > 3 }));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+The callback is invoked with three argument for each item, the current item
+value, the index position of the item and the input array being mapped.
+
+-- Testcase --
+{%
+ let words = [ "foo", "bar", "baz", "qrx" ];
+
+ print(join("\n",
+ map(words, function(word, idx, src) {
+ return sprintf("word=%s, idx=%d, src=%J", word, idx, src);
+ })
+ ), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+word=foo, idx=0, src=[ "foo", "bar", "baz", "qrx" ]
+word=bar, idx=1, src=[ "foo", "bar", "baz", "qrx" ]
+word=baz, idx=2, src=[ "foo", "bar", "baz", "qrx" ]
+word=qrx, idx=3, src=[ "foo", "bar", "baz", "qrx" ]
+-- End --
+
+
+Exceptions in the callback terminate the map process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ map([ 1, 2, 3 ], function() { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 36:
+ called from function map ([C])
+ called from anonymous function ([stdin]:2:39)
+
+ ` map([ 1, 2, 3 ], function() { die() });`
+ Near here ----------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/13_ord b/tests/custom/03_stdlib/13_ord
new file mode 100644
index 0000000..3a69228
--- /dev/null
+++ b/tests/custom/03_stdlib/13_ord
@@ -0,0 +1,46 @@
+The `ord()` function extracts the byte values of characters within the
+given input string at different offsets, depending on the arguments.
+
+Without further arguments, the function will return the byte value of
+the first character within the given string.
+
+If one or more offset arguments are given, the function returns an array
+containing the byte values of each character at the corresponding offset.
+
+Returns `null` if the given input string argument is not a string.
+
+Returns `null` if the given input string is empty and no offset arguments
+are provided.
+
+If invalid offsets are given, the corresponding values within the result
+array will be set to `null`.
+
+Invalid offsets are non-integer values or integers equal to or larger than
+the length of the input string. Negative offsets are converted to positive
+ones by adding the length of the input string. If the negative value is
+too large, the offset is considered invalid.
+
+
+-- Testcase --
+{%
+ print(join("\n", [
+ ord(123),
+ ord(""),
+ ord("abcd"),
+ ord("abcd", 0),
+ ord("abcd", 1, 3, 2),
+ ord("abcd", -1, -2),
+ ord("abcd", -10, 10)
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+null
+97
+[ 97 ]
+[ 98, 100, 99 ]
+[ 100, 99 ]
+[ null, null ]
+-- End --
diff --git a/tests/custom/03_stdlib/14_type b/tests/custom/03_stdlib/14_type
new file mode 100644
index 0000000..243618e
--- /dev/null
+++ b/tests/custom/03_stdlib/14_type
@@ -0,0 +1,43 @@
+The `type()` function returns the type name of the given argument as
+string.
+
+Returns `null` if the given argument is `null` or omitted.
+
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ type(),
+ type(null),
+ type(false),
+ type(true),
+ type(123),
+ type(-0xaf),
+ type(456.789),
+ type(-456.789),
+ type([ "foo", "bar", "baz" ]),
+ type({ example: "object" }),
+ type(function() {}),
+ type((n) => n * n),
+ type(print)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ null,
+ "bool",
+ "bool",
+ "int",
+ "int",
+ "double",
+ "double",
+ "array",
+ "object",
+ "function",
+ "function",
+ "function"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/15_reverse b/tests/custom/03_stdlib/15_reverse
new file mode 100644
index 0000000..176cd4c
--- /dev/null
+++ b/tests/custom/03_stdlib/15_reverse
@@ -0,0 +1,31 @@
+The `reverse()` function returns the input argument in reverse order.
+
+Returns a reversed copy of the input string if the given input value
+argument is a string.
+
+Returns a reversed copy of the input array if the given input value
+argument is an array.
+
+Returns `null` if the input argument is neither a string nor an array.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ reverse("abc"),
+ reverse([1, 2, 3]),
+ reverse(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "cba",
+ [
+ 3,
+ 2,
+ 1
+ ],
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/16_sort b/tests/custom/03_stdlib/16_sort
new file mode 100644
index 0000000..ccc235f
--- /dev/null
+++ b/tests/custom/03_stdlib/16_sort
@@ -0,0 +1,108 @@
+The `sort()` function performs an in-place sorting on the given array,
+invoking the specified callback (if any) to compare items during the
+sort process.
+
+If no callback is given or if the callback argument is `null`, a default
+comparator function is used which will sort number values numerically
+and all other value types lexically.
+
+Returns the sorted input array.
+
+Returns `null` if the given input array value is not an array.
+
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // default numeric sort
+ sort([ 6, 4.3, 1, 45, 3.01, 2 ]),
+
+ // default lexical sort
+ sort([ "qrx", "bar", "foo", "abc" ]),
+
+ // default lexical sort due to implicit stringification
+ sort([ true, false, null, 1, "2b" ]),
+
+ // sort with custom callback (by word length)
+ sort([ "apple", "pear", "banana", "grapefruit" ], (a, b) => length(a) - length(b)),
+
+ // sort with custom callback (by type, then value)
+ sort([ 4, 1, 9, 2, "x", "a", "q", "b" ], (a, b) => {
+ let t1 = type(a), t2 = type(b);
+ if (t1 < t2)
+ return -1;
+ else if (t2 > t2)
+ return 1;
+
+ if (a < b)
+ return -1;
+ else if (a > b)
+ return 1;
+
+ return 0;
+ })
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 1, 2, 3.01, 4.3, 6, 45 ]
+[ "abc", "bar", "foo", "qrx" ]
+[ 1, "2b", false, null, true ]
+[ "pear", "apple", "banana", "grapefruit" ]
+[ 1, 2, 4, 9, "a", "b", "q", "x" ]
+-- End --
+
+
+Supplying an invalid callback will trigger an exception.
+
+-- Testcase --
+{%
+ sort([3, 1, 2], "not_a_function")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: left-hand side is not a function
+In line 2, byte 34:
+
+ ` sort([3, 1, 2], "not_a_function")`
+ Near here --------------------------^
+
+
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", sort("not_an_array", function(a, b) { return a - b }));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+Exceptions in the callback terminate the sort process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ sort([ 1, 2, 3 ], function() { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 37:
+ called from function sort ([C])
+ called from anonymous function ([stdin]:2:40)
+
+ ` sort([ 1, 2, 3 ], function() { die() });`
+ Near here -----------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/17_splice b/tests/custom/03_stdlib/17_splice
new file mode 100644
index 0000000..f8fb9a2
--- /dev/null
+++ b/tests/custom/03_stdlib/17_splice
@@ -0,0 +1,98 @@
+The `splice()` function performs in-place addition and removal of elements
+on the given array.
+
+If no offset, remove count and additional items are supplied, all elements
+are removed from the array.
+
+If just an offset, but no remove count and not additional items are given,
+all elements beginning with the given offset until the end of the array
+are removed.
+
+If at least an offset and a remove count are given, then that amount of
+items are removed from the array, beginning at the specified offset. Any
+further supplied additional item (if any) is inserted in the same order
+beginning at the given offset.
+
+If either the offset or the remove count are negative, they're treated
+as counting towards the end of the array. If either value exceeds the
+array length, it is capped to the length of the array.
+
+Returns the modified input array.
+
+Returns `null` if the given input array value is not an array.
+
+
+-- Testcase --
+{%
+ let arr = [ 6, 4.3, 1, 45, 3.01, 2 ];
+
+ print(join("\n", [
+ // remove all items
+ splice([ ...arr ]),
+
+ // remove all items from index 4 till end
+ splice([ ...arr ], 4),
+
+ // remove item 2 and 3
+ splice([ ...arr ], 1, 2),
+
+ // remove last two items
+ splice([ ...arr ], -2),
+
+ // remove items 4 and 5
+ splice([ ...arr ], -3, -1),
+
+ // replace item 2
+ splice([ ...arr ], 1, 1, 7.9),
+
+ // add item between 3 and 4
+ splice([ ...arr ], 3, 0, 34),
+
+ // append three items
+ splice([ ...arr ], length(arr), 0, 123, 456, 789)
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ ]
+[ 6, 4.3, 1, 45 ]
+[ 6, 45, 3.01, 2 ]
+[ 6, 4.3, 1, 45 ]
+[ 6, 4.3, 1, 2 ]
+[ 6, 7.9, 1, 45, 3.01, 2 ]
+[ 6, 4.3, 1, 34, 45, 3.01, 2 ]
+[ 6, 4.3, 1, 45, 3.01, 2, 123, 456, 789 ]
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", splice("not_an_array", 0, 1));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+Invalid, non-numeric offset or index values are treated as 0.
+
+-- Testcase --
+{%
+ let arr = [ 6, 4.3, 1, 45, 3.01, 2 ];
+
+ print(join("\n", [
+ splice([ ...arr ], "foo", "bar"),
+ splice([ ...arr ], "foo", "bar", "baz")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 6, 4.3, 1, 45, 3.01, 2 ]
+[ "baz", 6, 4.3, 1, 45, 3.01, 2 ]
+-- End --
diff --git a/tests/custom/03_stdlib/18_split b/tests/custom/03_stdlib/18_split
new file mode 100644
index 0000000..e31ce78
--- /dev/null
+++ b/tests/custom/03_stdlib/18_split
@@ -0,0 +1,87 @@
+The `split()` function breaks the given string into multiple substrings,
+using the given separator value.
+
+The separator may be either a string or a regular expression value.
+
+Returns an array containing the resulting parts.
+
+Returns `null` if the given input value is not a string or if the separator
+argument is neither a string nor a regular expression.
+
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // split by string
+ split("foo|bar|baz", "|"),
+
+ // split by regexp
+ split("apples, bananas and strawberries are fruits", /, | and | are | /),
+
+ // splitting an empty string yields an array containing one empty string
+ split("", "|"),
+ split("", ""),
+ split("", /\s+/),
+
+ // splitting with an empty string as separator yields an array containing
+ // all characters individually
+ split("foo|bar|baz", ""),
+ split("foo|bar|baz", /()/),
+
+ // splitting on a separator not found within the string will yield an
+ // array containing the entire string as sole element
+ split("foo|bar|baz", "xxx"),
+ split("foo|bar|baz", /\d+/),
+
+ // subsequent separators are not coalesced
+ split("abc|||def", "|"),
+ split("foo1bar23baz", /[[:digit:]]/),
+
+ // leading and trailing empty substrings are retained
+ split("|abc|def|", "|"),
+ split(",foo;bar:", /[,;:]/),
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ "foo", "bar", "baz" ]
+[ "apples", "bananas", "strawberries", "fruits" ]
+[ "" ]
+[ "" ]
+[ "" ]
+[ "f", "o", "o", "|", "b", "a", "r", "|", "b", "a", "z" ]
+[ "f", "o", "o", "|", "b", "a", "r", "|", "b", "a", "z" ]
+[ "foo|bar|baz" ]
+[ "foo|bar|baz" ]
+[ "abc", "", "", "def" ]
+[ "foo", "bar", "", "baz" ]
+[ "", "abc", "def", "" ]
+[ "", "foo", "bar", "" ]
+-- End --
+
+
+Supplying an invalid input string value will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", split(true, "u"));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+Supplying a non-string, non-regexp separator will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", split("null true false", true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/19_substr b/tests/custom/03_stdlib/19_substr
new file mode 100644
index 0000000..0c7eed3
--- /dev/null
+++ b/tests/custom/03_stdlib/19_substr
@@ -0,0 +1,78 @@
+The `substr()` function extracts a portion of the given input string,
+specified by offset and length. and returns the resulting substring.
+
+If neither an offset, nor a length argument are provided, a copy of
+the entire input string is returned.
+
+If just an offset is specified, the entire remainder of the input string
+after the specified offset is returned.
+
+If both an offset and a length are specified, then that much characters
+of the string are extracted, beginning at the offset.
+
+If either offset or length are negative, they're counted towards the end
+of the string. If either value exceeds the input string length, it is
+capped to the length.
+
+Returns the resulting substring.
+
+Returns `null` if the given input value is not a string.
+
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // extract entire string
+ substr("Hello world!"),
+
+ // extract anything after the 3rd character
+ substr("Hello world!", 3),
+
+ // extract the last 6 characters
+ substr("Hello world!", -6),
+
+ // extract chars 5-8
+ substr("Hello world!", 4, 3),
+
+ // extract characters 8-10
+ substr("Hello world!", -5, -2),
+
+ // overlong values are capped
+ substr("Hello world!", 100),
+ substr("Hello world!", 0, 100),
+ substr("Hello world!", 100, 100),
+
+ // invalid offset or length values are treated as 0
+ substr("Hello world!", "inval"),
+ substr("Hello world!", "inval", "inval")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello world!",
+ "lo world!",
+ "world!",
+ "o w",
+ "orl",
+ "",
+ "Hello world!",
+ "",
+ "Hello world!",
+ ""
+]
+-- End --
+
+
+Supplying an invalid input string value will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", substr(true, 0, 1));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/20_time b/tests/custom/03_stdlib/20_time
new file mode 100644
index 0000000..173ae09
--- /dev/null
+++ b/tests/custom/03_stdlib/20_time
@@ -0,0 +1,18 @@
+The `time()` function returns the current UNIX epoch time.
+
+
+-- Testcase --
+{%
+ let timestamp = time();
+ let testcmd = sprintf('t=$(date +%%s); [ $t -gt %d -a $t -lt %d ]', timestamp - 3, timestamp + 3);
+
+ if (system(testcmd) == 0)
+ print("time() works\n");
+ else
+ print("time() and `date +%s` yield different results!\n");
+%}
+-- End --
+
+-- Expect stdout --
+time() works
+-- End --
diff --git a/tests/custom/03_stdlib/21_uc b/tests/custom/03_stdlib/21_uc
new file mode 100644
index 0000000..a5aeed3
--- /dev/null
+++ b/tests/custom/03_stdlib/21_uc
@@ -0,0 +1,27 @@
+The `uc()` function turns each lower case character in the source string
+into upper case and returns the resulting copy.
+
+The input argument is converted to a string in case it is not already a
+string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ uc("This Will Be All Uppercased."),
+ uc([ "An", "array", "ABC" ]),
+ uc(123),
+ uc(false),
+ uc()
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "THIS WILL BE ALL UPPERCASED.",
+ "[ \"AN\", \"ARRAY\", \"ABC\" ]",
+ "123",
+ "FALSE",
+ "NULL"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/22_uchr b/tests/custom/03_stdlib/22_uchr
new file mode 100644
index 0000000..8f24f3a
--- /dev/null
+++ b/tests/custom/03_stdlib/22_uchr
@@ -0,0 +1,19 @@
+The `uchr()` function takes a series of code point values and turns them
+into an UTF-8 encoded string. The resulting string will have as many
+characters as there were arguments to the function. The number of bytes
+per character varies between 1 to 4, depending on the code point value.
+
+Invalid numeric arguments or arguments being out of range 0-0x10FFFF will
+be encoded as the Unicode replacement character 0xFFFD.
+
+Returns the resulting UTF-8 string.
+
+-- Testcase --
+{{ uchr(0x2600, 0x2601, 0x2602) }}
+{{ uchr("inval", -1, 0xffffffff) }}
+-- End --
+
+-- Expect stdout --
+☀☁☂
+���
+-- End --
diff --git a/tests/custom/03_stdlib/23_values b/tests/custom/03_stdlib/23_values
new file mode 100644
index 0000000..34b600e
--- /dev/null
+++ b/tests/custom/03_stdlib/23_values
@@ -0,0 +1,35 @@
+The `values()` extracts all values of a given dictionary. The values in the
+resulting array are ordered according to the keys which in turn follow
+declaration or assignment order.
+
+Returns an array containg the value of each key within the given dictionary
+value.
+
+Returns `null` if the given dictionary argment is not a valid dictionary.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ values({ foo: true, bar: false, baz: null, qrx: 123, xyz: "test" }),
+ values({}),
+ values(true),
+ values()
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ [
+ true,
+ false,
+ null,
+ 123,
+ "test"
+ ],
+ [
+ ],
+ null,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/24_trim b/tests/custom/03_stdlib/24_trim
new file mode 100644
index 0000000..263fd18
--- /dev/null
+++ b/tests/custom/03_stdlib/24_trim
@@ -0,0 +1,46 @@
+The `trim()` function removes specific leading and trailing characters from
+a given input string. If the characters to trim are unspecified, the space, tab,
+carriage return and newline characters will be used by default.
+
+Returns a copy of the input string with the specified leading and trailing
+characters removed.
+
+Returns `null` if the given input argment is not a valid string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // not specifying trim characters will trim whitespace
+ trim(" Hello World! \r\n"),
+
+ // if trim characters are specified, only those are removed
+ trim("|* Foo Bar +|", "+*|"),
+
+ // trim does not affect characters in the middle of the string
+ trim(" Foo Bar "),
+ trim("|Foo|Bar|", "|")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ " Foo Bar ",
+ "Foo Bar",
+ "Foo|Bar"
+]
+-- End --
+
+
+Supplying an invalid string will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", trim(true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/25_ltrim b/tests/custom/03_stdlib/25_ltrim
new file mode 100644
index 0000000..2001322
--- /dev/null
+++ b/tests/custom/03_stdlib/25_ltrim
@@ -0,0 +1,46 @@
+The `ltrim()` function removes specific leading characters from the given
+input string. If the characters to trim are unspecified, the space, tab,
+carriage return and newline characters will be used by default.
+
+Returns a copy of the input string with the specified leading characters
+removed.
+
+Returns `null` if the given input argment is not a valid string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // not specifying trim characters will trim whitespace
+ ltrim(" Hello World!"),
+
+ // if trim characters are specified, only those are removed
+ ltrim("|* Foo Bar +|", "+*|"),
+
+ // ltrim does not affect characters in the middle or the end
+ ltrim(" Foo Bar "),
+ ltrim("|Foo|Bar|", "|")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ " Foo Bar +|",
+ "Foo Bar ",
+ "Foo|Bar|"
+]
+-- End --
+
+
+Supplying an invalid string will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", ltrim(true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/26_rtrim b/tests/custom/03_stdlib/26_rtrim
new file mode 100644
index 0000000..17b54f7
--- /dev/null
+++ b/tests/custom/03_stdlib/26_rtrim
@@ -0,0 +1,46 @@
+The `rtrim()` function removes specific trailing characters from the given
+input string. If the characters to trim are unspecified, the space, tab,
+carriage return and newline characters will be used by default.
+
+Returns a copy of the input string with the specified trailing characters
+removed.
+
+Returns `null` if the given input argment is not a valid string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // not specifying trim characters will trim whitespace
+ rtrim("Hello World! \r \n"),
+
+ // if trim characters are specified, only those are removed
+ rtrim("|* Foo Bar +|", "+*|"),
+
+ // rtrim does not affect characters in the middle or the beginning
+ rtrim(" Foo Bar "),
+ rtrim("|Foo|Bar|", "|")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ "|* Foo Bar ",
+ " Foo Bar",
+ "|Foo|Bar"
+]
+-- End --
+
+
+Supplying an invalid string will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", ltrim(true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/27_sprintf b/tests/custom/03_stdlib/27_sprintf
new file mode 100644
index 0000000..3edcd48
--- /dev/null
+++ b/tests/custom/03_stdlib/27_sprintf
@@ -0,0 +1,550 @@
+The `sprintf()` function formats given input value according to a format
+string specified as first argument. The format string mimicks the syntax
+and directives used by C printf().
+
+Each directive (with the exception of %%) in the format string expects
+a corresponding argument. If fewer arguments are passed to sprintf() than
+required by the format string, missing values will be assumed to be `null`
+and be interpreted accordingly. Excess arguments are ignored.
+
+Returns an output string formatted according to the format string with all
+format directives interpolated by their respective values.
+
+Returns an empty string in case the format string argument is not a valid
+string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // String interpolation, corresponding value will be converted
+ // into string if needed.
+ sprintf("Hello %s!", "World"),
+ sprintf("Hello %s!", false),
+ sprintf("Hello %s!", 123),
+ sprintf("Hello %s!", null),
+ sprintf("Hello %s!"),
+
+ // Signed integer interpolation, corresponding value will be
+ // converted into integer if needed. Also `d` and `i` are aliases.
+ sprintf("%d", 123),
+ sprintf("%i", 456.789),
+ sprintf("%d", true),
+ sprintf("%d", "0x42"),
+ sprintf("%d", "invalid"),
+ sprintf("%d", null),
+ sprintf("%d", 0xffffffffffffffff),
+ sprintf("%d"),
+
+ // Unsigned integer interpolation in decimal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ sprintf("%u", 123),
+ sprintf("%u", -123),
+ sprintf("%u", 0xffffffffffffffff),
+ sprintf("%u", 456.789),
+ sprintf("%u", "invalid"),
+ sprintf("%u", null),
+ sprintf("%u"),
+
+ // Unsigned integer interpolation in octal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ sprintf("%o", 123),
+ sprintf("%o", -123),
+ sprintf("%o", 0xffffffffffffffff),
+ sprintf("%o", 456.789),
+ sprintf("%o", "invalid"),
+ sprintf("%o", null),
+ sprintf("%o"),
+
+ // Unsigned integer interpolation in lower case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ sprintf("%x", 123),
+ sprintf("%x", -123),
+ sprintf("%x", 0xffffffffffffffff),
+ sprintf("%x", 456.789),
+ sprintf("%x", "invalid"),
+ sprintf("%x", null),
+ sprintf("%x"),
+
+ // Unsigned integer interpolation in upper case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ sprintf("%X", 123),
+ sprintf("%X", -123),
+ sprintf("%X", 0xffffffffffffffff),
+ sprintf("%X", 456.789),
+ sprintf("%X", "invalid"),
+ sprintf("%X", null),
+ sprintf("%X"),
+
+ // Floating point value interpolation in exponential notation,
+ // corresponding value will be converted to double if needed.
+ sprintf("%e", 123),
+ sprintf("%e", -123),
+ sprintf("%e", 456.789),
+ sprintf("%e", -456.789),
+ sprintf("%e", "invalid"),
+ sprintf("%e", null),
+ sprintf("%e"),
+
+ // Floating point value interpolation in exponential notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ sprintf("%E", 123),
+ sprintf("%E", -123),
+ sprintf("%E", 456.789),
+ sprintf("%E", -456.789),
+ sprintf("%E", "invalid"),
+ sprintf("%E", null),
+ sprintf("%E"),
+
+ // Floating point value interpolation in decimal point notation,
+ // corresponding value will be converted to double if needed.
+ sprintf("%f", 123),
+ sprintf("%f", -123),
+ sprintf("%f", 456.789),
+ sprintf("%f", -456.789),
+ sprintf("%f", "invalid"),
+ sprintf("%f", null),
+ sprintf("%f"),
+
+ // Floating point value interpolation in decimal point notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ sprintf("%F", 123),
+ sprintf("%F", -123),
+ sprintf("%F", 456.789),
+ sprintf("%F", -456.789),
+ sprintf("%F", "invalid"),
+ sprintf("%F", null),
+ sprintf("%F"),
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent. Corresponding
+ // value will be converted to double if needed.
+ sprintf("%g", 123.456),
+ sprintf("%g", 0.0000001),
+ sprintf("%g", "invalid"),
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent and using
+ // uppercase characters. Corresponding value will be converted to
+ // double if needed.
+ sprintf("%G", 123.456),
+ sprintf("%G", 0.0000001),
+ sprintf("%G", "invalid"),
+
+ // Character interpolation. The corresponding value is casted as `char`
+ // and the resulting character is interpolated.
+ sprintf("%c", 65),
+ sprintf("%c", -1),
+ sprintf("%c", 456.789),
+ sprintf("%c", "invalid"),
+
+ // JSON interpolation. The corresponding value is JSON encoded and
+ // interpolated as string.
+ sprintf("%J", "Hello\n"),
+ sprintf("%J", 123),
+ sprintf("%J", [ 1, 2, 3 ]),
+ sprintf("%J", { some: "dictionary", an: [ "array", true, false ] }),
+ sprintf("%J", null),
+ sprintf("%J"),
+
+ // Escaping `%`. The `%%` format string will produce a literal `%`.
+ // No corresponding argument is expected.
+ sprintf("%%")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ "Hello false!",
+ "Hello 123!",
+ "Hello (null)!",
+ "Hello (null)!",
+ "123",
+ "456",
+ "1",
+ "66",
+ "0",
+ "0",
+ "-1",
+ "0",
+ "123",
+ "18446744073709551493",
+ "18446744073709551615",
+ "456",
+ "0",
+ "0",
+ "0",
+ "173",
+ "1777777777777777777605",
+ "1777777777777777777777",
+ "710",
+ "0",
+ "0",
+ "0",
+ "7b",
+ "ffffffffffffff85",
+ "ffffffffffffffff",
+ "1c8",
+ "0",
+ "0",
+ "0",
+ "7B",
+ "FFFFFFFFFFFFFF85",
+ "FFFFFFFFFFFFFFFF",
+ "1C8",
+ "0",
+ "0",
+ "0",
+ "1.230000e+02",
+ "-1.230000e+02",
+ "4.567890e+02",
+ "-4.567890e+02",
+ "nan",
+ "0.000000e+00",
+ "0.000000e+00",
+ "1.230000E+02",
+ "-1.230000E+02",
+ "4.567890E+02",
+ "-4.567890E+02",
+ "NAN",
+ "0.000000E+00",
+ "0.000000E+00",
+ "123.000000",
+ "-123.000000",
+ "456.789000",
+ "-456.789000",
+ "nan",
+ "0.000000",
+ "0.000000",
+ "123.000000",
+ "-123.000000",
+ "456.789000",
+ "-456.789000",
+ "NAN",
+ "0.000000",
+ "0.000000",
+ "123.456",
+ "1e-07",
+ "nan",
+ "123.456",
+ "1E-07",
+ "NAN",
+ "A",
+ "\u00ff",
+ "\u00c8",
+ "\u0000",
+ "\"Hello\\n\"",
+ "123",
+ "[ 1, 2, 3 ]",
+ "{ \"some\": \"dictionary\", \"an\": [ \"array\", true, false ] }",
+ "null",
+ "null",
+ "%"
+]
+-- End --
+
+
+Field widths may be specified for format directives.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // by default the output of a format directive is as long as the
+ // string representation of the corresponding value
+ sprintf("[%s]", "test"),
+
+ // by specifying a field width, the output will be padded to the
+ // given length
+ sprintf("[%10s]", "test"),
+
+ // the same applies to numbers
+ sprintf("[%10d]", 123),
+ sprintf("[%10f]", 1.0),
+
+ // and to char formats
+ sprintf("[%10c]", 65),
+
+ // field width is not applicable to `%` formats
+ sprintf("[%10%]")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "[test]",
+ "[ test]",
+ "[ 123]",
+ "[ 1.000000]",
+ "[ A]",
+ "[%]"
+]
+-- End --
+
+
+Precisions may be specified for format directives.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // For `f`, `F`, `e` and `E`, the precision specifies the amount of
+ // digits after the comma
+ sprintf("[%.3f]", 1/3),
+ sprintf("[%.3F]", 1/3),
+ sprintf("[%.3e]", 1/3),
+ sprintf("[%.3E]", 1/3),
+
+ // For `g` and `G` the precision specifies the number of significant
+ // digits to print before switching to exponential notation
+ sprintf("[%.3g]", 1000.1),
+ sprintf("[%.3G]", 1000.1),
+
+ // For strings, the precision specifies the amount of characters to
+ // print at most
+ sprintf("[%.5s]", "test"),
+ sprintf("[%.3s]", "test"),
+
+ // For JSON format, the precision specifies the amount of indentation
+ // to use. Omitting precision will not indent, specifying a precision
+ // of `0` uses tabs for indentation, any other precision uses this
+ // many spaces
+ sprintf("<%J>", [ 1, 2, 3, { true: false } ]), // no indent
+ sprintf("<%.J>", [ 1, 2, 3, { true: false } ]), // tab indent
+ sprintf("<%.0J>", [ 1, 2, 3, { true: false } ]), // tab indent
+ sprintf("<%.1J>", [ 1, 2, 3, { true: false } ]), // indent using one space
+ sprintf("<%.4J>", [ 1, 2, 3, { true: false } ]), // indent using four spaces
+
+ // precision does not apply to char, integer or `%` formats
+ sprintf("[%.3d]", 1000),
+ sprintf("[%.3c]", 65),
+ sprintf("[%.3%]"),
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[0.000]
+[0.000]
+[0.000e+00]
+[0.000E+00]
+[1e+03]
+[1E+03]
+[test]
+[tes]
+<[ 1, 2, 3, { "true": false } ]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+[1000]
+[A]
+[%]
+-- End --
+
+
+A number of flag characters are supported for format directives.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // The recognized flag characters are `#`, `0`, `-`, `+` and ` ` (space)
+ sprintf("%#0+- s", "test"),
+
+ // Repetitions of flag characters are accepted
+ sprintf("%###s", "test"),
+ sprintf("%000s", "test"),
+ sprintf("%++-s", "test"),
+ sprintf("%-- s", "test"),
+ sprintf("% s", "test"),
+
+ // The `#` flag produces alternative forms of various conversions
+ sprintf("%o / %#o", 15, 15),
+ sprintf("%x / %#x", 16, 16),
+ sprintf("%X / %#X", 17, 17),
+ sprintf("%g / %#g", 1.0, 1.0),
+
+ // The `0` flag indicates zero- instead of space-padding for various
+ // numeric conversions.
+ sprintf("%5d / %05d", -10, -10),
+ sprintf("%5d / %05d", 11, 11),
+ sprintf("%5g / %05g", -12.0, -12.0),
+ sprintf("%5g / %05g", 13.0, 13.0),
+ sprintf("%5s / %05s", "a", "a"),
+
+ // The `-` flag indicates left, instead of right padding. It will
+ // override `0` and always pad with spaces
+ sprintf("%-5d / %-05d", -10, -10),
+ sprintf("%-5d / %-05d", 11, 11),
+ sprintf("%-5g / %-05g", -12.0, -12.0),
+ sprintf("%-5g / %-05g", 13.0, 13.0),
+ sprintf("%-5s / %-05s", "a", "a"),
+
+ // The `+` flag indicates that a sign (`+` or `-`) should be placed
+ // before signed numeric values. It overrides ` ` (space).
+ sprintf("%+5d / %+05d", -10, -10),
+ sprintf("%+5d / %+05d", 11, 11),
+ sprintf("%+5g / %+05g", -12.0, -12.0),
+ sprintf("%+5g / %+05g", 13.0, 13.0),
+ sprintf("%+5s / %+05s", "a", "a"),
+
+ // The ` ` (space) flag indicates that a blank should be placed
+ // before positive numbers (useful to ensure that negative and
+ // positive values in output are aligned)
+ sprintf("%-5d / %- 5d", -10, -10),
+ sprintf("%-5d / %- 5d", 11, 11),
+ sprintf("%-5g / %- 5g", -12.0, -12.0),
+ sprintf("%-5g / %- 5g", 13.0, 13.0),
+ sprintf("%-5s / %- 5s", "a", "a"),
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "test",
+ "test",
+ "test",
+ "test",
+ "test",
+ "test",
+ "17 / 017",
+ "10 / 0x10",
+ "11 / 0X11",
+ "1 / 1.00000",
+ " -10 / -0010",
+ " 11 / 00011",
+ " -12 / -0012",
+ " 13 / 00013",
+ " a / a",
+ "-10 / -10 ",
+ "11 / 11 ",
+ "-12 / -12 ",
+ "13 / 13 ",
+ "a / a ",
+ " -10 / -0010",
+ " +11 / +0011",
+ " -12 / -0012",
+ " +13 / +0013",
+ " a / a",
+ "-10 / -10 ",
+ "11 / 11 ",
+ "-12 / -12 ",
+ "13 / 13 ",
+ "a / a "
+]
+-- End --
+
+
+Unrecognized format directives are copied to the output string as-is.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // A truncated format directive is preserved
+ sprintf("test %", "test"),
+ sprintf("test %-010.3", "test"),
+
+ // An unrecognized format directive is preserved
+ sprintf("test %y test", 123),
+ sprintf("test %~123s test", 123)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "test %",
+ "test %-010.3",
+ "test %y test",
+ "test %~123s test"
+]
+-- End --
+
+
+Missing values for format directives are treated as `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ sprintf("%s"),
+ sprintf("%d"),
+ sprintf("%u"),
+ sprintf("%o"),
+ sprintf("%x"),
+ sprintf("%X"),
+ sprintf("%f"),
+ sprintf("%F"),
+ sprintf("%e"),
+ sprintf("%E"),
+ sprintf("%g"),
+ sprintf("%G"),
+ sprintf("%c"),
+ sprintf("%J")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "(null)",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0.000000",
+ "0.000000",
+ "0.000000e+00",
+ "0.000000E+00",
+ "0",
+ "0",
+ "\u0000",
+ "null"
+]
+-- End --
+
+
+Supplying a non-string format value will yield an empty string result.
+
+-- Testcase --
+{%
+ printf("%.J\n", sprintf(true, 1, 2, 3));
+%}
+-- End --
+
+-- Expect stdout --
+""
+-- End --
diff --git a/tests/custom/03_stdlib/28_printf b/tests/custom/03_stdlib/28_printf
new file mode 100644
index 0000000..a2a6d27
--- /dev/null
+++ b/tests/custom/03_stdlib/28_printf
@@ -0,0 +1,526 @@
+The `printf()` function formats given input value according to a format
+string specified as first argument. The format string mimicks the syntax
+and directives used by C printf().
+
+Each directive (with the exception of %%) in the format string expects
+a corresponding argument. If fewer arguments are passed to printf() than
+required by the format string, missing values will be assumed to be `null`
+and be interpreted accordingly. Excess arguments are ignored.
+
+Writes the output string formatted according to the format string with all
+format directives interpolated by their respective values to the standard
+output stream of the VM.
+
+Returns the number of bytes written to the output stream.
+
+Writes an empty string in case the format string argument is not a valid
+string value.
+
+-- Testcase --
+{%
+ // String interpolation, corresponding value will be converted
+ // into string if needed.
+ printf("Hello %s!\n", "World");
+ printf("Hello %s!\n", false);
+ printf("Hello %s!\n", 123);
+ printf("Hello %s!\n", null);
+ printf("Hello %s!\n");
+
+ // Signed integer interpolation, corresponding value will be
+ // converted into integer if needed. Also `d` and `i` are aliases.
+ printf("%d\n", 123);
+ printf("%i\n", 456.789);
+ printf("%d\n", true);
+ printf("%d\n", "0x42");
+ printf("%d\n", "invalid");
+ printf("%d\n", null);
+ printf("%d\n", 0xffffffffffffffff);
+ printf("%d\n");
+
+ // Unsigned integer interpolation in decimal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ printf("%u\n", 123);
+ printf("%u\n", -123);
+ printf("%u\n", 0xffffffffffffffff);
+ printf("%u\n", 456.789);
+ printf("%u\n", "invalid");
+ printf("%u\n", null);
+ printf("%u\n");
+
+ // Unsigned integer interpolation in octal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ printf("%o\n", 123);
+ printf("%o\n", -123);
+ printf("%o\n", 0xffffffffffffffff);
+ printf("%o\n", 456.789);
+ printf("%o\n", "invalid");
+ printf("%o\n", null);
+ printf("%o\n");
+
+ // Unsigned integer interpolation in lower case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ printf("%x\n", 123);
+ printf("%x\n", -123);
+ printf("%x\n", 0xffffffffffffffff);
+ printf("%x\n", 456.789);
+ printf("%x\n", "invalid");
+ printf("%x\n", null);
+ printf("%x\n");
+
+ // Unsigned integer interpolation in upper case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ printf("%X\n", 123);
+ printf("%X\n", -123);
+ printf("%X\n", 0xffffffffffffffff);
+ printf("%X\n", 456.789);
+ printf("%X\n", "invalid");
+ printf("%X\n", null);
+ printf("%X\n");
+
+ // Floating point value interpolation in exponential notation,
+ // corresponding value will be converted to double if needed.
+ printf("%e\n", 123);
+ printf("%e\n", -123);
+ printf("%e\n", 456.789);
+ printf("%e\n", -456.789);
+ printf("%e\n", "invalid");
+ printf("%e\n", null);
+ printf("%e\n");
+
+ // Floating point value interpolation in exponential notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ printf("%E\n", 123);
+ printf("%E\n", -123);
+ printf("%E\n", 456.789);
+ printf("%E\n", -456.789);
+ printf("%E\n", "invalid");
+ printf("%E\n", null);
+ printf("%E\n");
+
+ // Floating point value interpolation in decimal point notation,
+ // corresponding value will be converted to double if needed.
+ printf("%f\n", 123);
+ printf("%f\n", -123);
+ printf("%f\n", 456.789);
+ printf("%f\n", -456.789);
+ printf("%f\n", "invalid");
+ printf("%f\n", null);
+ printf("%f\n");
+
+ // Floating point value interpolation in decimal point notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ printf("%F\n", 123);
+ printf("%F\n", -123);
+ printf("%F\n", 456.789);
+ printf("%F\n", -456.789);
+ printf("%F\n", "invalid");
+ printf("%F\n", null);
+ printf("%F\n");
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent. Corresponding
+ // value will be converted to double if needed.
+ printf("%g\n", 123.456);
+ printf("%g\n", 0.0000001);
+ printf("%g\n", "invalid");
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent and using
+ // uppercase characters. Corresponding value will be converted to
+ // double if needed.
+ printf("%G\n", 123.456);
+ printf("%G\n", 0.0000001);
+ printf("%G\n", "invalid");
+
+ // Character interpolation. The corresponding value is casted as `char`
+ // and the resulting character is interpolated.
+ printf("%c\n", 65);
+ //printf("%c\n", -1);
+ //printf("%c\n", 456.789);
+ //printf("%c\n", "invalid");
+
+ // JSON interpolation. The corresponding value is JSON encoded and
+ // interpolated as string.
+ printf("%J\n", "Hello\n");
+ printf("%J\n", 123);
+ printf("%J\n", [ 1, 2, 3 ]);
+ printf("%J\n", { some: "dictionary", an: [ "array", true, false ] });
+ printf("%J\n", null);
+ printf("%J\n");
+
+ // Escaping `%`. The `%%` format string will produce a literal `%`.
+ // No corresponding argument is expected.
+ printf("%%\n");
+%}
+-- End --
+
+-- Expect stdout --
+Hello World!
+Hello false!
+Hello 123!
+Hello (null)!
+Hello (null)!
+123
+456
+1
+66
+0
+0
+-1
+0
+123
+18446744073709551493
+18446744073709551615
+456
+0
+0
+0
+173
+1777777777777777777605
+1777777777777777777777
+710
+0
+0
+0
+7b
+ffffffffffffff85
+ffffffffffffffff
+1c8
+0
+0
+0
+7B
+FFFFFFFFFFFFFF85
+FFFFFFFFFFFFFFFF
+1C8
+0
+0
+0
+1.230000e+02
+-1.230000e+02
+4.567890e+02
+-4.567890e+02
+nan
+0.000000e+00
+0.000000e+00
+1.230000E+02
+-1.230000E+02
+4.567890E+02
+-4.567890E+02
+NAN
+0.000000E+00
+0.000000E+00
+123.000000
+-123.000000
+456.789000
+-456.789000
+nan
+0.000000
+0.000000
+123.000000
+-123.000000
+456.789000
+-456.789000
+NAN
+0.000000
+0.000000
+123.456
+1e-07
+nan
+123.456
+1E-07
+NAN
+A
+"Hello\n"
+123
+[ 1, 2, 3 ]
+{ "some": "dictionary", "an": [ "array", true, false ] }
+null
+null
+%
+-- End --
+
+
+Field widths may be specified for format directives.
+
+-- Testcase --
+{%
+ // by default the output of a format directive is as long as the
+ // string representation of the corresponding value
+ printf("[%s]\n", "test");
+
+ // by specifying a field width, the output will be padded to the
+ // given length
+ printf("[%10s]\n", "test");
+
+ // the same applies to numbers
+ printf("[%10d]\n", 123);
+ printf("[%10f]\n", 1.0);
+
+ // and to char formats
+ printf("[%10c]\n", 65);
+
+ // field width is not applicable to `%` formats
+ printf("[%10%]\n");
+%}
+-- End --
+
+-- Expect stdout --
+[test]
+[ test]
+[ 123]
+[ 1.000000]
+[ A]
+[%]
+-- End --
+
+
+Precisions may be specified for format directives.
+
+-- Testcase --
+{%
+ // For `f`, `F`, `e` and `E`, the precision specifies the amount of
+ // digits after the comma
+ printf("[%.3f]\n", 1/3);
+ printf("[%.3F]\n", 1/3);
+ printf("[%.3e]\n", 1/3);
+ printf("[%.3E]\n", 1/3);
+
+ // For `g` and `G` the precision specifies the number of significant
+ // digits to print before switching to exponential notation
+ printf("[%.3g]\n", 1000.1);
+ printf("[%.3G]\n", 1000.1);
+
+ // For strings, the precision specifies the amount of characters to
+ // print at most
+ printf("[%.5s]\n", "test");
+ printf("[%.3s]\n", "test");
+
+ // For JSON format, the precision specifies the amount of indentation
+ // to use. Omitting precision will not indent, specifying a precision
+ // of `0` uses tabs for indentation, any other precision uses this
+ // many spaces
+ printf("<%J>\n", [ 1, 2, 3, { true: false } ]), // no indent
+ printf("<%.J>\n", [ 1, 2, 3, { true: false } ]), // tab indent
+ printf("<%.0J>\n", [ 1, 2, 3, { true: false } ]), // tab indent
+ printf("<%.1J>\n", [ 1, 2, 3, { true: false } ]), // indent using one space
+ printf("<%.4J>\n", [ 1, 2, 3, { true: false } ]), // indent using four spaces
+
+ // precision does not apply to char, integer or `%` formats
+ printf("[%.3d]\n", 1000);
+ printf("[%.3c]\n", 65);
+ printf("[%.3%]\n");
+%}
+-- End --
+
+-- Expect stdout --
+[0.000]
+[0.000]
+[0.000e+00]
+[0.000E+00]
+[1e+03]
+[1E+03]
+[test]
+[tes]
+<[ 1, 2, 3, { "true": false } ]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+[1000]
+[A]
+[%]
+-- End --
+
+
+A number of flag characters are supported for format directives.
+
+-- Testcase --
+{%
+ // The recognized flag characters are `#`, `0`, `-`, `+` and ` ` (space)
+ printf("%#0+- s\n", "test");
+
+ // Repetitions of flag characters are accepted
+ printf("%###s\n", "test");
+ printf("%000s\n", "test");
+ printf("%++-s\n", "test");
+ printf("%-- s\n", "test");
+ printf("% s\n", "test");
+
+ // The `#` flag produces alternative forms of various conversions
+ printf("%o / %#o\n", 15, 15);
+ printf("%x / %#x\n", 16, 16);
+ printf("%X / %#X\n", 17, 17);
+ printf("%g / %#g\n", 1.0, 1.0);
+
+ // The `0` flag indicates zero- instead of space-padding for various
+ // numeric conversions.
+ printf("[%5d / %05d]\n", -10, -10);
+ printf("[%5d / %05d]\n", 11, 11);
+ printf("[%5g / %05g]\n", -12.0, -12.0);
+ printf("[%5g / %05g]\n", 13.0, 13.0);
+ printf("[%5s / %05s]\n", "a", "a");
+
+ // The `-` flag indicates left, instead of right padding. It will
+ // override `0` and always pad with spaces
+ printf("[%-5d / %-05d]\n", -10, -10);
+ printf("[%-5d / %-05d]\n", 11, 11);
+ printf("[%-5g / %-05g]\n", -12.0, -12.0);
+ printf("[%-5g / %-05g]\n", 13.0, 13.0);
+ printf("[%-5s / %-05s]\n", "a", "a");
+
+ // The `+` flag indicates that a sign (`+` or `-`) should be placed
+ // before signed numeric values. It overrides ` ` (space).
+ printf("[%+5d / %+05d]\n", -10, -10);
+ printf("[%+5d / %+05d]\n", 11, 11);
+ printf("[%+5g / %+05g]\n", -12.0, -12.0);
+ printf("[%+5g / %+05g]\n", 13.0, 13.0);
+ printf("[%+5s / %+05s]\n", "a", "a");
+
+ // The ` ` (space) flag indicates that a blank should be placed
+ // before positive numbers (useful to ensure that negative and
+ // positive values in output are aligned)
+ printf("[%-5d / %- 5d]\n", -10, -10);
+ printf("[%-5d / %- 5d]\n", 11, 11);
+ printf("[%-5g / %- 5g]\n", -12.0, -12.0);
+ printf("[%-5g / %- 5g]\n", 13.0, 13.0);
+ printf("[%-5s / %- 5s]\n", "a", "a");
+%}
+-- End --
+
+-- Expect stdout --
+test
+test
+test
+test
+test
+test
+17 / 017
+10 / 0x10
+11 / 0X11
+1 / 1.00000
+[ -10 / -0010]
+[ 11 / 00011]
+[ -12 / -0012]
+[ 13 / 00013]
+[ a / a]
+[-10 / -10 ]
+[11 / 11 ]
+[-12 / -12 ]
+[13 / 13 ]
+[a / a ]
+[ -10 / -0010]
+[ +11 / +0011]
+[ -12 / -0012]
+[ +13 / +0013]
+[ a / a]
+[-10 / -10 ]
+[11 / 11 ]
+[-12 / -12 ]
+[13 / 13 ]
+[a / a ]
+-- End --
+
+
+Unrecognized format directives are copied to the output string as-is.
+
+-- Testcase --
+{%
+ // A truncated format directive is preserved
+ printf("test %\n", "test");
+ printf("test %-010.3\n", "test");
+
+ // An unrecognized format directive is preserved
+ printf("test %y test\n", 123);
+ printf("test %~123s test\n", 123);
+%}
+-- End --
+
+-- Expect stdout --
+test %
+test %-010.3
+test %y test
+test %~123s test
+-- End --
+
+
+Missing values for format directives are treated as `null`.
+
+-- Testcase --
+{%
+ printf("%s\n");
+ printf("%d\n");
+ printf("%u\n");
+ printf("%o\n");
+ printf("%x\n");
+ printf("%X\n");
+ printf("%f\n");
+ printf("%F\n");
+ printf("%e\n");
+ printf("%E\n");
+ printf("%g\n");
+ printf("%G\n");
+ //printf("%c\n");
+ printf("%J\n");
+%}
+-- End --
+
+-- Expect stdout --
+(null)
+0
+0
+0
+0
+0
+0.000000
+0.000000
+0.000000e+00
+0.000000E+00
+0
+0
+null
+-- End --
+
+
+Supplying a non-string format value will yield an empty string result.
+
+-- Testcase --
+{%
+ printf(true, 1, 2, 3);
+%}
+-- End --
+
+-- Expect stdout --
+-- End --
diff --git a/tests/custom/03_stdlib/29_require b/tests/custom/03_stdlib/29_require
new file mode 100644
index 0000000..681f3f7
--- /dev/null
+++ b/tests/custom/03_stdlib/29_require
@@ -0,0 +1,164 @@
+The `require()` function loads the specified module, executes it and returns
+the returned value to the caller.
+
+The global array `REQUIRE_SEARCH_PATH` specifies the list of locations to
+check for a matching module file.
+
+The return value of a successfully loaded module is cached in a global
+registry, subsequent require calls with the same name will return the
+cached value.
+
+Throws an exception if the global `REQUIRE_SEARCH_PATH` variable is unset or
+not pointing to an array.
+
+Throws an exception if the requested module name cannot be found.
+
+Throws an exception if a module file could be found but not opened.
+
+Throws an exception if a module file could not be compiled.
+
+Returns the value returned by the invoked module code (typically an object).
+
+-- Testcase --
+{%
+ push(REQUIRE_SEARCH_PATH, TESTFILES_PATH + '/*.uc');
+
+ let mod1 = require("require.test.module");
+ printf("require() #1 returned %.J\n\n", mod1);
+
+ let mod2 = require("require.test.module");
+ printf("require() #2 returned %.J\n\n", mod2);
+
+ printf("Instances are identical: %s\n\n", mod1 === mod2);
+
+ // deleting the entry from the global module registry forces reload
+ delete global.modules["require.test.module"];
+
+ let mod3 = require("require.test.module");
+ printf("require() #3 returned %.J\n\n", mod3);
+
+ printf("Instances are identical: %s\n\n", mod1 === mod3);
+%}
+-- End --
+
+-- File require/test/module.uc --
+{%
+ print("This is require.test.module running!\n\n");
+
+ return {
+ greeting: function(name) {
+ printf("Hello, %s!\n", name);
+ }
+ };
+%}
+-- End --
+
+-- Expect stdout --
+This is require.test.module running!
+
+require() #1 returned {
+ "greeting": "function(name) { ... }"
+}
+
+require() #2 returned {
+ "greeting": "function(name) { ... }"
+}
+
+Instances are identical: true
+
+This is require.test.module running!
+
+require() #3 returned {
+ "greeting": "function(name) { ... }"
+}
+
+Instances are identical: false
+
+-- End --
+
+
+A clobbered `REQUIRE_SEARCH_PATH` triggers an exception.
+
+-- Testcase --
+{%
+ REQUIRE_SEARCH_PATH = null;
+
+ require("test");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: Global require search path not set
+In line 4, byte 16:
+
+ ` require("test");`
+ Near here --------^
+
+
+-- End --
+
+
+A not found module triggers an exception.
+
+-- Testcase --
+{%
+ require("test");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: No module named 'test' could be found
+In line 2, byte 16:
+
+ ` require("test");`
+ Near here --------^
+
+
+-- End --
+
+
+A compilation error in the module triggers an exception.
+
+-- Testcase --
+{%
+ try {
+ push(REQUIRE_SEARCH_PATH, TESTFILES_PATH + '/*.uc');
+
+ require("require.test.broken");
+ }
+ catch (e) {
+ // Catch and rethrow exception with modified message to
+ // ensure stable test output.
+ e.message = replace(e.message,
+ /(compile module '.+require\/test\/broken\.uc')/,
+ "compile module '.../require/test/broken.uc'");
+
+ die(e);
+ }
+%}
+-- End --
+
+-- File require/test/broken.uc --
+{%
+ // Unclosed object to force syntax error
+ return {
+%}
+-- End --
+
+-- Expect stderr --
+Unable to compile module '.../require/test/broken.uc':
+Syntax error: Expecting label
+In line 3, byte 11:
+
+ ` return {`
+ Near here --^
+
+
+
+In line 14, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/30_iptoarr b/tests/custom/03_stdlib/30_iptoarr
new file mode 100644
index 0000000..5f1ae24
--- /dev/null
+++ b/tests/custom/03_stdlib/30_iptoarr
@@ -0,0 +1,46 @@
+The `iptoarr()` function parses the given IP address string into an array
+of byte values.
+
+Returns an array of byte values for the parsed IP address.
+
+Returns `null` if the given IP argument is not a string value or if the
+IP address could not be parsed.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ iptoarr("0.0.0.0"),
+ iptoarr("10.11.12.13"),
+ iptoarr("::"),
+ iptoarr("::ffff:192.168.1.1"),
+ iptoarr("2001:db8:1234:4567:789a:bcde:f012:3456"),
+ iptoarr("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 0, 0, 0, 0 ]
+[ 10, 11, 12, 13 ]
+[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 192, 168, 1, 1 ]
+[ 32, 1, 13, 184, 18, 52, 69, 103, 120, 154, 188, 222, 240, 18, 52, 86 ]
+[ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ]
+-- End --
+
+
+Supplying a non-string value or an unparsable address yields `null`.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ iptoarr(true),
+ iptoarr("invalid")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+null
+-- End --
diff --git a/tests/custom/03_stdlib/31_arrtoip b/tests/custom/03_stdlib/31_arrtoip
new file mode 100644
index 0000000..61d82e1
--- /dev/null
+++ b/tests/custom/03_stdlib/31_arrtoip
@@ -0,0 +1,57 @@
+The `arrtoip()` function converts the given byte array into an IP address
+string. Array of length 4 are converted to IPv4 addresses, arrays of
+length 16 to IPv6 addresses.
+
+Returns the resulting IPv4 or IPv6 address string.
+
+Returns `null` if the given value is not an array, if the array has an
+unsuitable length or if any item within the array is not an integer within
+the range 0-255.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ arrtoip([ 0, 0, 0, 0 ]),
+ arrtoip([ 192, 168, 1, 1 ]),
+ arrtoip([ 255, 255, 255, 255 ]),
+ arrtoip([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]),
+ arrtoip([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 192, 168, 1, 1 ]),
+ arrtoip([ 32, 1, 13, 184, 18, 52, 69, 103, 120, 154, 188, 222, 240, 18, 52, 86 ]),
+ arrtoip([ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ])
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+0.0.0.0
+192.168.1.1
+255.255.255.255
+::
+::ffff:192.168.1.1
+2001:db8:1234:4567:789a:bcde:f012:3456
+ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
+-- End --
+
+
+Supplying a non-array value, an array of unsuitable length or an array
+containing invalid byte values yields `null`.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ arrtoip(true),
+ arrtoip([ ]),
+ arrtoip([ 1, 2, 3 ]),
+ arrtoip([ 192, 168, 1, -1 ]),
+ arrtoip([ true, false, -5, 500 ])
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+null
+null
+null
+null
+-- End --
diff --git a/tests/custom/03_stdlib/32_match b/tests/custom/03_stdlib/32_match
new file mode 100644
index 0000000..b053044
--- /dev/null
+++ b/tests/custom/03_stdlib/32_match
@@ -0,0 +1,55 @@
+The `match()` function applies the given regular expression pattern on
+the given subject value.
+
+Depending on whether the given regular expression sets the global (`g`)
+modifier, either an array of match groups or the first match group is
+returned.
+
+Returns `null` if the given pattern argument is not a regular expression
+value, or if the subject is `null` or unspecified.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // match all key=value pairs
+ match("kind=fruit name=strawberry color=red", /([[:alpha:]]+)=([^= ]+)/g),
+
+ // match any word
+ match("The quick brown fox jumps over the lazy dog", /[[:alpha:]]+/g),
+
+ // match the first three lowercase words
+ match("The quick brown fox jumps over the lazy dog", / ([[:lower:]]+) ([[:lower:]]+) ([[:lower:]]+)/),
+
+ // special case: match any empty string sequence
+ match("foo", /()/g),
+
+ // special case: match first empty string sequence
+ match("foo", /()/),
+
+ // subject is implictly converted to string
+ match(true, /u/)
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ [ "kind=fruit", "kind", "fruit" ], [ "name=strawberry", "name", "strawberry" ], [ "color=red", "color", "red" ] ]
+[ [ "The" ], [ "quick" ], [ "brown" ], [ "fox" ], [ "jumps" ], [ "over" ], [ "the" ], [ "lazy" ], [ "dog" ] ]
+[ " quick brown fox", "quick", "brown", "fox" ]
+[ [ "", "" ], [ "", "" ], [ "", "" ], [ "", "" ] ]
+[ "", "" ]
+[ "u" ]
+-- End --
+
+
+Omitting the subject yields `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", match(null, /u/));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/33_replace b/tests/custom/03_stdlib/33_replace
new file mode 100644
index 0000000..b662ae8
--- /dev/null
+++ b/tests/custom/03_stdlib/33_replace
@@ -0,0 +1,207 @@
+The `replace()` function replaces the given regular expression or plain
+string pattern on the given subject value with the specified replacement.
+
+In case a regular expression with the global (`g`) modifier set or a
+string is passed as pattern, all found occurrences are replaced. In case
+a regular expression without global modifier is given, only the first
+match will be replaced.
+
+The replacement value may be either a string, which is inserted in place
+of the matched result after certain interpolation steps or a function
+which is invoked for each match and whose return value is used as
+replacement.
+
+The subject is implicitly converted to a string if it is not a string.
+
+The pattern is implicitly converted to a string if it is neither a string
+nor a regular expression value.
+
+The replacement value is implicitly converted to a string if it is neither
+a string nor a function value.
+
+Returns a copy of the input string with the match(es) replaced by their
+corresponding replacement values.
+
+Returns `null` either the subject, the pattern or the replacement value
+is `null`.
+
+-- Testcase --
+{%
+ print(join("\n###\n", [
+ // Capitalize and reformat all key=value pairs using a callback
+ replace("kind=fruit name=strawberry color=red",
+ /([[:alpha:]])([[:alpha:]]*)=(.)([^= ]*) */g,
+ function(m, letter1, rest1, letter2, rest2) {
+ return sprintf('%s%s: %s%s\n',
+ uc(letter1), rest1,
+ uc(letter2), rest2
+ );
+ }),
+
+ // strike any three letter word
+ replace("The quick brown fox jumps over the lazy dog",
+ /(^| )([[:alpha:]]{3})( |$)/g,
+ "$1<s>$2</s>$3"),
+
+ // highlight any vowel
+ replace("The quick brown fox jumps over the lazy dog",
+ /[aeiou]/g,
+ "[$&]"),
+
+ // replace with fixed pattern
+ replace("foo bar foo baz foo qrx", "foo", "xxx"),
+
+ // testing all possible replacement interpolations
+ replace("before abc def ghi jkl mno pqr stu vwx yz! after",
+ / ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z!]{3}) /,
+ '|\n---\n' +
+ 'Entire match ($$&): [$&]\n' +
+ 'Before match ($$`): [$`]\n' +
+ "After match ($$'): [$']\n" +
+ 'Group 1 match ($$1): [$1]\n' +
+ 'Group 2 match ($$2): [$2]\n' +
+ 'Group 3 match ($$3): [$3]\n' +
+ 'Group 4 match ($$4): [$4]\n' +
+ 'Group 5 match ($$5): [$5]\n' +
+ 'Group 6 match ($$6): [$6]\n' +
+ 'Group 7 match ($$7): [$7]\n' +
+ 'Group 8 match ($$8): [$8]\n' +
+ 'Group 9 match ($$9): [$9]\n' +
+ 'Literal $$: [$$]\n' +
+ '---\n|'),
+
+ // testing that all captures are passed to the callback
+ replace("before abc def ghi jkl mno pqr stu vwx yz! after",
+ / ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z!]{3}) /,
+ function(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9) {
+ return sprintf(
+ '|\n---\n' +
+ 'Entire match (arg 0): [%s]\n' +
+ 'Group 1 match (arg 1): [%s]\n' +
+ 'Group 2 match (arg 2): [%s]\n' +
+ 'Group 3 match (arg 3): [%s]\n' +
+ 'Group 4 match (arg 4): [%s]\n' +
+ 'Group 5 match (arg 5): [%s]\n' +
+ 'Group 6 match (arg 6): [%s]\n' +
+ 'Group 7 match (arg 7): [%s]\n' +
+ 'Group 8 match (arg 8): [%s]\n' +
+ 'Group 9 match (arg 9): [%s]\n' +
+ '---\n|',
+ m0, m1, m2, m3, m4, m5, m6, m7, m8, m9
+ );
+ }),
+
+ // the subject is implictly stringified
+ replace({ foo: true }, "foo", "xxx"),
+
+ // the pattern is implictly stringified
+ replace({ foo: true }, true, "false"),
+
+ // the replacement is implictly stringified
+ replace({ foo: true }, "foo", 0x7b),
+
+ // special case: replace all empty matches
+ replace("foo", "", "."),
+ replace("foo", /()/g, ".")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+Kind: Fruit
+Name: Strawberry
+Color: Red
+
+###
+<s>The</s> quick brown <s>fox</s> jumps over <s>the</s> lazy <s>dog</s>
+###
+Th[e] q[u][i]ck br[o]wn f[o]x j[u]mps [o]v[e]r th[e] l[a]zy d[o]g
+###
+xxx bar xxx baz xxx qrx
+###
+before |
+---
+Entire match ($&): [ abc def ghi jkl mno pqr stu vwx yz! ]
+Before match ($`): [before ]
+After match ($'): [ after]
+Group 1 match ($1): [abc]
+Group 2 match ($2): [def]
+Group 3 match ($3): [ghi]
+Group 4 match ($4): [jkl]
+Group 5 match ($5): [mno]
+Group 6 match ($6): [pqr]
+Group 7 match ($7): [stu]
+Group 8 match ($8): [vwx]
+Group 9 match ($9): [yz!]
+Literal $: [$]
+---
+| after
+###
+before |
+---
+Entire match (arg 0): [ abc def ghi jkl mno pqr stu vwx yz! ]
+Group 1 match (arg 1): [abc]
+Group 2 match (arg 2): [def]
+Group 3 match (arg 3): [ghi]
+Group 4 match (arg 4): [jkl]
+Group 5 match (arg 5): [mno]
+Group 6 match (arg 6): [pqr]
+Group 7 match (arg 7): [stu]
+Group 8 match (arg 8): [vwx]
+Group 9 match (arg 9): [yz!]
+---
+| after
+###
+{ "xxx": true }
+###
+{ "foo": false }
+###
+{ "123": true }
+###
+.f.o.o.
+###
+.f.o.o.
+-- End --
+
+
+Omitting subject, pattern or replacement yields `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ replace(null, "u", "x"),
+ replace("nullnull", null, "x"),
+ replace("foo", "o", null)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ null,
+ null
+]
+-- End --
+
+
+Exceptions in the callback terminate the replacement process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ replace("foo", "o", function(m) { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 40:
+ called from function replace ([C])
+ called from anonymous function ([stdin]:2:43)
+
+ ` replace("foo", "o", function(m) { die() });`
+ Near here --------------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/34_json b/tests/custom/03_stdlib/34_json
new file mode 100644
index 0000000..ba8ad9f
--- /dev/null
+++ b/tests/custom/03_stdlib/34_json
@@ -0,0 +1,110 @@
+The `json()` function parses the given string value as JSON.
+
+Throws an exception if the given input value is not a string.
+Throws an exception if the given input string cannot be parsed as JSON.
+
+Returns the resulting value.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ json("null"),
+ json("true"),
+ json("false"),
+ json("123"),
+ json("456.7890000"),
+ json("-1.4E10"),
+ json("1e309"),
+ json('"A string \u2600"'),
+ json("[ 1, 2, 3 ]"),
+ json('{ "test": [ 1, 2, 3 ] }'),
+
+ // surrounding white space is ignored
+ json(' [ 1, 2, 3 ] ')
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+true
+false
+123
+456.789
+-1.4e+10
+Infinity
+A string ☀
+[ 1, 2, 3 ]
+{ "test": [ 1, 2, 3 ] }
+[ 1, 2, 3 ]
+-- End --
+
+
+Passing a non-string value throws an exception.
+
+-- Testcase --
+{%
+ json(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed value is not a string
+In line 2, byte 11:
+
+ ` json(true);`
+ Near here ---^
+
+
+-- End --
+
+
+Unparseable JSON throws exceptions.
+
+-- Testcase --
+{%
+ json('[ "incomplete", "array" ');
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Failed to parse JSON string: unexpected end of data
+In line 2, byte 33:
+
+ ` json('[ "incomplete", "array" ');`
+ Near here -------------------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ json('invalid syntax');
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Failed to parse JSON string: unexpected character
+In line 2, byte 23:
+
+ ` json('invalid syntax');`
+ Near here ---------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ json('[] trailing garbage');
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Trailing garbage after JSON data
+In line 2, byte 28:
+
+ ` json('[] trailing garbage');`
+ Near here --------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/35_include b/tests/custom/03_stdlib/35_include
new file mode 100644
index 0000000..6d808f2
--- /dev/null
+++ b/tests/custom/03_stdlib/35_include
@@ -0,0 +1,173 @@
+The `include()` function executes the specified path as ucode script,
+optionally setting a different execution scope for the invoked file.
+
+If the specified path is relative, it is treated as being relative to the
+source file currently being executed or the current working directory in
+case the interpreter executes code from stdin or a command line argument.
+
+Throws an exception if the given path value is not a string.
+
+Throws an exception if a scope argument is specified and not a valid object.
+
+Throws an exception if the given path could not be found or opened.
+
+Throws an exception if the given file could not be compiled.
+
+Returns no value.
+
+-- Testcase --
+{%
+ let real_printf = printf;
+
+ // include by relative path
+ include("files/include.uc");
+
+ printf("---\n");
+
+ // include by absolute path
+ include(TESTFILES_PATH + "/include.uc");
+
+ printf("---\n");
+
+ // include with overridden scope
+ include("files/include.uc", {
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ });
+
+ printf("---\n");
+
+ // include with isolated scope
+ include("files/include.uc", proto({
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ }, {}));
+%}
+-- End --
+
+-- File include.uc --
+{%
+ printf("This is the include file running! Can I access the global env? %s\n",
+ REQUIRE_SEARCH_PATH ? "Yes!" : "No.");
+%}
+-- End --
+
+-- Expect stdout --
+This is the include file running! Can I access the global env? Yes!
+---
+This is the include file running! Can I access the global env? Yes!
+---
+This is the wrapped printf() getting called!
+This is the include file running! Can I access the global env? Yes!
+---
+This is the wrapped printf() getting called!
+This is the include file running! Can I access the global env? No.
+-- End --
+
+
+An invalid path value triggers an exception.
+
+-- Testcase --
+{%
+ include(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed filename is not a string
+In line 2, byte 14:
+
+ ` include(true);`
+ Near here ------^
+
+
+-- End --
+
+
+An invalid scope value triggers an exception.
+
+-- Testcase --
+{%
+ include("test", true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed scope value is not an object
+In line 2, byte 22:
+
+ ` include("test", true);`
+ Near here --------------^
+
+
+-- End --
+
+
+A not found file triggers an exception.
+
+-- Testcase --
+{%
+ include("files/doesnotexist.uc");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: Include file not found
+In line 2, byte 33:
+
+ ` include("files/doesnotexist.uc");`
+ Near here -------------------------^
+
+
+-- End --
+
+
+A compilation error in the file triggers an exception.
+
+-- Testcase --
+{%
+ try {
+ include("files/broken.uc");
+ }
+ catch (e) {
+ // Catch and rethrow exception with modified message to
+ // ensure stable test output.
+ e.message = replace(e.message,
+ /(compile module '.+broken\.uc')/,
+ "compile module '.../broken.uc'");
+
+ die(e);
+ }
+%}
+-- End --
+
+-- File broken.uc --
+{%
+ // Unclosed object to force syntax error
+ return {
+%}
+-- End --
+
+-- Expect stderr --
+Unable to compile module '.../broken.uc':
+Syntax error: Expecting label
+In line 3, byte 11:
+
+ ` return {`
+ Near here --^
+
+
+
+In line 12, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/36_render b/tests/custom/03_stdlib/36_render
new file mode 100644
index 0000000..64ef08a
--- /dev/null
+++ b/tests/custom/03_stdlib/36_render
@@ -0,0 +1,167 @@
+The `render()` function executes the specified path as ucode script,
+optionally setting a different execution scope for the invoked file,
+and captures the produced output in a string.
+
+If the specified path is relative, it is treated as being relative to the
+source file currently being executed or the current working directory in
+case the interpreter executes code from stdin or a command line argument.
+
+Throws an exception if the given path value is not a string.
+
+Throws an exception if a scope argument is specified and not a valid object.
+
+Throws an exception if the given path could not be found or opened.
+
+Throws an exception if the given file could not be compiled.
+
+Returns a string containing the captured output of the executed file.
+
+-- Testcase --
+{%
+ let real_printf = printf;
+
+ printf("%.J\n", [
+ // include by relative path
+ render("files/include.uc"),
+
+ // include by absolute path
+ render(TESTFILES_PATH + "/include.uc"),
+
+ // include with overridden scope
+ render("files/include.uc", {
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ }),
+
+ // include with isolated scope
+ render("files/include.uc", proto({
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ }, {}))
+ ]);
+%}
+-- End --
+
+-- File include.uc --
+{%
+ printf("This is the include file running! Can I access the global env? %s\n",
+ REQUIRE_SEARCH_PATH ? "Yes!" : "No.");
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "This is the include file running! Can I access the global env? Yes!\n",
+ "This is the include file running! Can I access the global env? Yes!\n",
+ "This is the wrapped printf() getting called!\nThis is the include file running! Can I access the global env? Yes!\n",
+ "This is the wrapped printf() getting called!\nThis is the include file running! Can I access the global env? No.\n"
+]
+-- End --
+
+
+An invalid path value triggers an exception.
+
+-- Testcase --
+{%
+ include(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed filename is not a string
+In line 2, byte 14:
+
+ ` include(true);`
+ Near here ------^
+
+
+-- End --
+
+
+An invalid scope value triggers an exception.
+
+-- Testcase --
+{%
+ include("test", true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed scope value is not an object
+In line 2, byte 22:
+
+ ` include("test", true);`
+ Near here --------------^
+
+
+-- End --
+
+
+A not found file triggers an exception.
+
+-- Testcase --
+{%
+ include("files/doesnotexist.uc");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: Include file not found
+In line 2, byte 33:
+
+ ` include("files/doesnotexist.uc");`
+ Near here -------------------------^
+
+
+-- End --
+
+
+A compilation error in the file triggers an exception.
+
+-- Testcase --
+{%
+ try {
+ include("files/broken.uc");
+ }
+ catch (e) {
+ // Catch and rethrow exception with modified message to
+ // ensure stable test output.
+ e.message = replace(e.message,
+ /(compile module '.+broken\.uc')/,
+ "compile module '.../broken.uc'");
+
+ die(e);
+ }
+%}
+-- End --
+
+-- File broken.uc --
+{%
+ // Unclosed object to force syntax error
+ return {
+%}
+-- End --
+
+-- Expect stderr --
+Unable to compile module '.../broken.uc':
+Syntax error: Expecting label
+In line 3, byte 11:
+
+ ` return {`
+ Near here --^
+
+
+
+In line 12, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/37_warn b/tests/custom/03_stdlib/37_warn
new file mode 100644
index 0000000..2c0ff24
--- /dev/null
+++ b/tests/custom/03_stdlib/37_warn
@@ -0,0 +1,40 @@
+The `warn()` function outputs the given values to stderr.
+
+Returns the amount of bytes written.
+
+-- Testcase --
+{%
+ let n = 0;
+
+ n += warn(null, "\n");
+ n += warn(true, "\n");
+ n += warn(false, "\n");
+ n += warn(123, "\n");
+ n += warn(456.789, "\n");
+ n += warn(NaN, "\n");
+ n += warn(Infinity, "\n");
+ n += warn("Hello world", "\n");
+ n += warn([ 1, 2, 3 ], "\n");
+ n += warn({ some: "dict" }, "\n");
+ n += warn(warn, "\n");
+
+ warn(n, " bytes written\n");
+%}
+-- End --
+
+-- Expect stderr --
+
+true
+false
+123
+456.789
+NaN
+Infinity
+Hello world
+[ 1, 2, 3 ]
+{ "some": "dict" }
+function warn(...) { [native code] }
+117 bytes written
+-- End --
+
+
diff --git a/tests/custom/03_stdlib/38_system b/tests/custom/03_stdlib/38_system
new file mode 100644
index 0000000..faf8aa5
--- /dev/null
+++ b/tests/custom/03_stdlib/38_system
@@ -0,0 +1,148 @@
+The `system()` function executes the given shell command or raw command
+vector, optionally terminating the spawned process after the specified
+timeout.
+
+Throws an exception if a timeout is specified but not a valid positive
+integer value.
+
+Throws an exception if the command argument is neither an array nor a
+string value.
+
+Throws an exception if an empty command vector is given.
+
+Returns the exit code of the invoked process.
+
+-- Testcase --
+{%
+ // When passing the command as string, `/bin/sh -c` is invoked with
+ // the given command string as second argument
+ system('x=1; echo $((x + x))');
+
+ // When passing the command as array, the first value is taken as
+ // executable to invoke and any further item as argument to the
+ // invoked program. Internally `execvp()` is used, which means that
+ // the executable path may be relative in which case it is looked
+ // up in the directories specified by `$PATH`. Any array items are
+ // implicitly stringified.
+ system([ '/bin/sh', TESTFILES_PATH + '/testscripts/hello.sh', true, 0x42, 123.456000, { some: "dict" } ]);
+
+ // By specifying a timeout, maximum execution time is limited to
+ // that many milliseconds. If the program does not finish before the
+ // timeout occurs, it is forcibly terminated with SIGKILL.
+ system([ '/bin/sh', TESTFILES_PATH + '/testscripts/sleep.sh' ], 100);
+
+ // The return value of system() is the exit code of the invoked program.
+ let rc = system([ '/bin/sh', TESTFILES_PATH + '/testscripts/exit.sh' ]);
+
+ printf("Return value is %d\n", rc);
+%}
+-- End --
+
+-- File testscripts/hello.sh --
+#!/bin/sh
+
+echo "This is our test program running!"
+echo "My arguments are:"
+
+for arg in "$@"; do
+ echo "<$arg>"
+done
+-- End --
+
+-- File testscripts/sleep.sh --
+#!/bin/sh
+
+echo "I'll sleep for 10s now..."
+sleep 10
+echo "I am done sleeping."
+-- End --
+
+-- File testscripts/exit.sh --
+#!/bin/sh
+
+echo "I'll exit with code 5 now."
+exit 5
+-- End --
+
+-- Expect stdout --
+2
+This is our test program running!
+My arguments are:
+<true>
+<66>
+<123.456>
+<{ "some": "dict" }>
+I'll sleep for 10s now...
+I'll exit with code 5 now.
+Return value is 5
+-- End --
+
+
+Passing an invalid command value throws an exception.
+
+-- Testcase --
+{%
+ system(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed command is neither string nor array
+In line 2, byte 13:
+
+ ` system(true);`
+ Near here -----^
+
+
+-- End --
+
+-- Testcase --
+{%
+ system([]);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed command array is empty
+In line 2, byte 11:
+
+ ` system([]);`
+ Near here ---^
+
+
+-- End --
+
+
+Passing an invalid timeout throws an exception.
+
+-- Testcase --
+{%
+ system("exit 0", "invalid")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Invalid timeout specified
+In line 2, byte 28:
+
+ ` system("exit 0", "invalid")`
+ Near here --------------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ system("exit 0", -100)
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Invalid timeout specified
+In line 2, byte 23:
+
+ ` system("exit 0", -100)`
+ Near here ---------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/39_trace b/tests/custom/03_stdlib/39_trace
new file mode 100644
index 0000000..2b2295a
--- /dev/null
+++ b/tests/custom/03_stdlib/39_trace
@@ -0,0 +1,79 @@
+The `trace()` function sets the execution trace level of the VM.
+
+Throws an exception if trace level argument is not a valid integer.
+
+Returns the previously used execution trace level.
+
+-- Testcase --
+{%
+ printf("Code before enabling tracing.\n");
+
+ trace(1);
+
+ printf("Code after enabling tracing.\n");
+
+ trace(0);
+
+ printf("Code after disabling tracing.\n");
+%}
+-- End --
+
+-- Expect stdout --
+Code before enabling tracing.
+Code after enabling tracing.
+Code after disabling tracing.
+-- End --
+
+-- Expect stderr --
+ [-2] 1
+ [-1] "function trace(...) { [native code] }"
+ [+1] 0
+ [stdin]:4 trace(1);
+0000001c POP
+ [-1] 0
+0000001d LVAR {0x0} ; "printf"
+ [+1] "function printf(...) { [native code] }"
+ [stdin]:6 printf("Code after enabling tracing.\n");
+00000022 LOAD {0x3} ; "Code after enabling tracing.\n"
+ [+2] "Code after enabling tracing.\n"
+ [stdin]:6 printf("Code after enabling tracing.\n");
+00000027 CALL {0x1}
+ [*] CALLFRAME[1]
+ |- stackframe 1/3
+ |- ctx null
+ [-2] "Code after enabling tracing.\n"
+ [-1] "function printf(...) { [native code] }"
+ [+1] 29
+ [stdin]:6 printf("Code after enabling tracing.\n");
+0000002c POP
+ [-1] 29
+0000002d LVAR {0x2} ; "trace"
+ [+1] "function trace(...) { [native code] }"
+ [stdin]:8 trace(0);
+00000032 LOAD8 {0}
+ [+2] 0
+ [stdin]:8 trace(0);
+00000034 CALL {0x1}
+ [*] CALLFRAME[1]
+ |- stackframe 1/3
+ |- ctx null
+-- End --
+
+
+Passing an invalid trace value throws an exception.
+
+-- Testcase --
+{%
+ trace("inval");
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Invalid level specified
+In line 2, byte 15:
+
+ ` trace("inval");`
+ Near here -------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/40_proto b/tests/custom/03_stdlib/40_proto
new file mode 100644
index 0000000..017a821
--- /dev/null
+++ b/tests/custom/03_stdlib/40_proto
@@ -0,0 +1,89 @@
+The `proto()` function retrievs or sets the prototype of the given object
+or resource value.
+
+Throws an exception if given value does not support setting prototypes.
+
+When invoked with one argument, returns the prototype of the given value
+(if any).
+
+When invoked with two arguments, returns the given value.
+
+-- Testcase --
+{%
+ let fs = require("fs");
+
+ // create a "class instance" by attaching a function dictionary to
+ // a plain object.
+ let obj = proto({}, {
+ greeting: function(name) {
+ printf("Hello, %s!\n", name);
+ }
+ });
+
+ // accessing a property on `obj` will look up the prototype chain
+ // if the object itself does not have it
+ obj.greeting("World");
+
+ printf("%.J\n", [
+ // retrieve prototype of `fs.file` resource
+ proto(fs.stdout),
+
+ // retrieve prototype of `obj`
+ proto(obj)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+Hello, World!
+[
+ {
+ "error": "function error(...) { [native code] }",
+ "fileno": "function fileno(...) { [native code] }",
+ "close": "function close(...) { [native code] }",
+ "tell": "function tell(...) { [native code] }",
+ "seek": "function seek(...) { [native code] }",
+ "write": "function write(...) { [native code] }",
+ "read": "function read(...) { [native code] }"
+ },
+ {
+ "greeting": "function(name) { ... }"
+ }
+]
+-- End --
+
+
+
+Passing an invalid value throws an exception.
+
+-- Testcase --
+{%
+ proto("inval", {});
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed value is neither a prototype, resource or object
+In line 2, byte 19:
+
+ ` proto("inval", {});`
+ Near here -----------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ proto({}, "inval");
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed value is neither a prototype, resource or object
+In line 2, byte 19:
+
+ ` proto({}, "inval");`
+ Near here -----------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/41_sleep b/tests/custom/03_stdlib/41_sleep
new file mode 100644
index 0000000..bca6f0d
--- /dev/null
+++ b/tests/custom/03_stdlib/41_sleep
@@ -0,0 +1,46 @@
+The `sleep()` function pauses program execution for the given amount of
+milliseconds.
+
+Returns `true` if the program slept.
+
+Returns `false` when the given time value was not convertible to an integer,
+negative or zero.
+
+-- Testcase --
+{%
+ let t1 = time();
+
+ sleep(1000);
+
+ let t2 = time();
+
+ printf("Slept for %d second(s).\n", t2 - t1);
+%}
+-- End --
+
+-- Expect stdout --
+Slept for 1 second(s).
+-- End --
+
+
+Passing an invalid value yields `false`.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ sleep("inval"),
+ sleep([]),
+ sleep(-1),
+ sleep(0)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ false,
+ false,
+ false,
+ false
+]
+-- End --
diff --git a/tests/custom/03_stdlib/42_assert b/tests/custom/03_stdlib/42_assert
new file mode 100644
index 0000000..e6200e9
--- /dev/null
+++ b/tests/custom/03_stdlib/42_assert
@@ -0,0 +1,30 @@
+The `assert()` function raises an exception using the second argument as
+message when the first argument value is not truish.
+
+Throws an exception if the first argument value is not truish.
+
+Returns the value of the first argument.
+
+-- Testcase --
+{%
+ let x = assert(123, "This should not trigger");
+ printf("x = %d\n", x);
+
+ let y = assert(false, "This should trigger");
+ printf("y = %d\n", y);
+%}
+-- End --
+
+-- Expect stdout --
+x = 123
+-- End --
+
+-- Expect stderr --
+This should trigger
+In line 5, byte 45:
+
+ ` let y = assert(false, "This should trigger");`
+ Near here -------------------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/43_regexp b/tests/custom/03_stdlib/43_regexp
new file mode 100644
index 0000000..9809c87
--- /dev/null
+++ b/tests/custom/03_stdlib/43_regexp
@@ -0,0 +1,117 @@
+The `regexp()` function compiles the given pattern string into a regular
+expression, optionally applying the flags specified in the second argument.
+
+Throws an exception if unrecognized flag characters are specified or if the
+flags argument is not a string value.
+
+Throws an exception if the given pattern string cannot be compiled into a
+regular expression.
+
+Returns the compiled regexp object.
+
+-- Testcase --
+{%
+ let re1 = regexp("begin (.+) end", "i");
+ let re2 = regexp("[a-z]+", "g");
+ let re3 = regexp("Dots (.+) newlines", "s");
+
+ printf("%.J\n", [
+ match("BEGIN this is some text END", re1),
+ match("This is a group of words", re2),
+ match("Dots now\ndon't\nmatch\ntext\nwith newlines", re3)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ [
+ "BEGIN this is some text END",
+ "this is some text"
+ ],
+ [
+ [
+ "his"
+ ],
+ [
+ "is"
+ ],
+ [
+ "a"
+ ],
+ [
+ "group"
+ ],
+ [
+ "of"
+ ],
+ [
+ "words"
+ ]
+ ],
+ null
+]
+-- End --
+
+
+Passing an uncompilable regexp throws an exception.
+
+-- Testcase --
+{%
+ try {
+ // unterminated capture group to trigger syntax error
+ regexp("foo(");
+ }
+ catch (e) {
+ // Massage compile error message for stable output since it is
+ // dependant on the underyling C library.
+ e.message = "Compile error";
+ die(e);
+ }
+%}
+-- End --
+
+-- Expect stderr --
+Compile error
+In line 10, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
+
+
+Passing an invalid flags argument throws an exception.
+
+-- Testcase --
+{%
+ regexp(".*", true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Given flags argument is not a string
+In line 2, byte 19:
+
+ ` regexp(".*", true);`
+ Near here -----------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ regexp(".*", "igz");
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Unrecognized flag character 'z'
+In line 2, byte 20:
+
+ ` regexp(".*", "igz");`
+ Near here ------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/44_wildcard b/tests/custom/03_stdlib/44_wildcard
new file mode 100644
index 0000000..d838e47
--- /dev/null
+++ b/tests/custom/03_stdlib/44_wildcard
@@ -0,0 +1,43 @@
+The `wildcard()` function tests whether the given wildcard pattern matches
+the given subject, optionally ignoring letter case.
+
+Returns `true` if the pattern matches the subject.
+
+Returns `false` if the pattern does not match the subject.
+
+Returns `null` if the pattern argument is not a string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // A simple glob pattern match
+ wildcard("file.txt", "*.txt"),
+
+ // Using `?` as single character placeholder and case folding
+ wildcard("2022-02-02_BACKUP.LIST", "????-??-??_backup.*", true),
+
+ // Using bracket expressions
+ wildcard("aaa_123_zzz", "[a-z][a-z][a-z]_???_*"),
+
+ // Using no meta characters at all
+ wildcard("test", "test"),
+
+ // No match yields `false`
+ wildcard("abc", "d*"),
+
+ // Invalid pattern value yields `null`
+ wildcard("true", true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ true,
+ true,
+ true,
+ true,
+ false,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/45_sourcepath b/tests/custom/03_stdlib/45_sourcepath
new file mode 100644
index 0000000..92c63bf
--- /dev/null
+++ b/tests/custom/03_stdlib/45_sourcepath
@@ -0,0 +1,69 @@
+The `sourcepath()` function determines the path of the currently executed
+ucode script file, optionally only the directory portion.
+
+By specifying the a depth parameter, the owning files of functions further
+up the call stack can be determined.
+
+Returns a string containing the path (or directory) of the running ucode
+source file.
+
+Returns `null` if the path is indeterminate.
+
+-- Testcase --
+{%
+ let output = render("files/include/level1.uc");
+
+ // replace dynamic testfiles path with placeholder for stable output
+ output = replace(output, TESTFILES_PATH, "...");
+
+ print(output);
+%}
+-- End --
+
+-- File include/level1.uc --
+This is the 1st level include.
+
+{% include("level2.uc") %}
+-- End --
+
+-- File include/level2.uc --
+This is the 2nd level include.
+
+{% include("level3.uc") %}
+-- End --
+
+-- File include/level3.uc --
+This is the 3rd level include.
+
+{% for (let depth in [0, 1, 2, 3]): %}
+Depth {{ depth }}:
+ Path: {{ sourcepath(depth, false) || "indeterminate" }}
+ Directory: {{ sourcepath(depth, true) || "indeterminate" }}
+
+{% endfor %}
+-- End --
+
+-- Expect stdout --
+This is the 1st level include.
+
+This is the 2nd level include.
+
+This is the 3rd level include.
+
+Depth 0:
+ Path: .../include/level3.uc
+ Directory: .../include
+
+Depth 1:
+ Path: .../include/level2.uc
+ Directory: .../include
+
+Depth 2:
+ Path: .../include/level1.uc
+ Directory: .../include
+
+Depth 3:
+ Path: indeterminate
+ Directory: indeterminate
+
+-- End --
diff --git a/tests/custom/03_stdlib/46_min b/tests/custom/03_stdlib/46_min
new file mode 100644
index 0000000..c07fbff
--- /dev/null
+++ b/tests/custom/03_stdlib/46_min
@@ -0,0 +1,28 @@
+The `min()` function returns the minimum of all given arguments.
+
+If multiple equivalent minimum values are given (e.g. `null` and `false`
+both are treated as `0` when comparing numerically), the first minimal
+value is returned.
+
+Returns the minimum value among all given arguments or `null` if no
+arguments were passed.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ min(),
+ min(5, 1, 3, -10),
+ min("foo", "bar", "xxx", "abc"),
+ min(false, null, 0, NaN)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ -10,
+ "abc",
+ false
+]
+-- End --
diff --git a/tests/custom/03_stdlib/47_max b/tests/custom/03_stdlib/47_max
new file mode 100644
index 0000000..69e6fb8
--- /dev/null
+++ b/tests/custom/03_stdlib/47_max
@@ -0,0 +1,28 @@
+The `max()` function returns the maximum of all given arguments.
+
+If multiple equivalent maximum values are given (e.g. `null` and `false`
+both are treated as `0` when comparing numerically), the first maximal
+value is returned.
+
+Returns the maximum value among all given arguments or `null` if no
+arguments were passed.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ max(),
+ max(5, 1, 3, -10),
+ max("foo", "bar", "xxx", "abc"),
+ max(false, null, 0, NaN)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ 5,
+ "xxx",
+ false
+]
+-- End --
diff --git a/tests/custom/03_stdlib/48_b64dec b/tests/custom/03_stdlib/48_b64dec
new file mode 100644
index 0000000..898aa7a
--- /dev/null
+++ b/tests/custom/03_stdlib/48_b64dec
@@ -0,0 +1,31 @@
+The `b64dec()` function decodes the given base64 input string.
+
+Returns a string containing the decoded data.
+
+Returns `null` if the input is not a string or if the input string was
+invalid base64 data (e.g. missing padding or non-whitespace characters
+outside the expected alphabet).
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ b64dec("SGVsbG8sIHdvcmxkIQ=="),
+ b64dec("SGVsbG8sIHdvcmxkIQ"),
+ b64dec("AAECAw=="),
+ b64dec("xxx"),
+ b64dec("==="),
+ b64dec(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello, world!",
+ null,
+ "\u0000\u0001\u0002\u0003",
+ null,
+ null,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/49_b64enc b/tests/custom/03_stdlib/49_b64enc
new file mode 100644
index 0000000..3ef0381
--- /dev/null
+++ b/tests/custom/03_stdlib/49_b64enc
@@ -0,0 +1,25 @@
+The `b64enc()` function encodes the given input string as base64.
+
+Returns a string containing the encoded data.
+
+Returns `null` if the input is not a string.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ b64enc("Hello, world!"),
+ b64enc("\u0000\u0001\u0002\u0003"),
+ b64enc(""),
+ b64enc(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "SGVsbG8sIHdvcmxkIQ==",
+ "AAECAw==",
+ "",
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/50_uniq b/tests/custom/03_stdlib/50_uniq
new file mode 100644
index 0000000..6e13077
--- /dev/null
+++ b/tests/custom/03_stdlib/50_uniq
@@ -0,0 +1,66 @@
+The `uniq()` function extracts the unique set of all values within the
+given input array, maintaining the original order.
+
+Returns an array containing all unique items of the input array.
+
+Returns `null` if the input is not an array value.
+
+-- Testcase --
+{%
+ let o1 = { an: "object" };
+ let o2 = { an: "object" }; // same but not identical
+
+ let a1 = [ 1, 2, 3 ];
+ let a2 = [ 1, 2, 3 ]; // same but not identical
+
+ printf("%.J\n", [
+ // strict comparison is used, 0 and "0" are not unique
+ uniq([ 0, 1, 2, 0, "0", 2, 3, "4", 4 ]),
+
+ // despite NaN != NaN, two NaN values are not unique
+ uniq([ NaN, NaN ]),
+
+ // only identical objects are filtered, not equivalent ones
+ uniq([ o1, o1, o2, a1, a1, a2 ]),
+
+ // invalid input yields `null`
+ uniq(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ [
+ 0,
+ 1,
+ 2,
+ "0",
+ 3,
+ "4",
+ 4
+ ],
+ [
+ "NaN"
+ ],
+ [
+ {
+ "an": "object"
+ },
+ {
+ "an": "object"
+ },
+ [
+ 1,
+ 2,
+ 3
+ ],
+ [
+ 1,
+ 2,
+ 3
+ ]
+ ],
+ null
+]
+-- End --
diff --git a/tests/custom/04_bugs/05_duplicate_ressource_type b/tests/custom/04_bugs/05_duplicate_resource_type
index 21166b2..6d8d8f5 100644
--- a/tests/custom/04_bugs/05_duplicate_ressource_type
+++ b/tests/custom/04_bugs/05_duplicate_resource_type
@@ -1,5 +1,5 @@
-When requiring a C module that registers custom ressource types multiple
-times, ressource values instantiated after subsequent requires of the
+When requiring a C module that registers custom resource types multiple
+times, resource values instantiated after subsequent requires of the
same extensions didn't properly function since the internal type prototype
was resolved to the initial copy and subsequently discarded due to an index
mismatch.
@@ -7,7 +7,7 @@ mismatch.
-- Testcase --
{%
fs = require("fs");
- fd = fs.open("README.md");
+ fd = fs.open("files/test.txt");
printf("fd.read() #1: %s\n",
fd.read("line") ? "working" : "not working (" + fd.error() + ")");
@@ -16,7 +16,7 @@ mismatch.
fs = require("fs");
- fd = fs.open("README.md");
+ fd = fs.open("files/test.txt");
printf("fd.read() #2: %s\n",
fd.read("line") ? "working" : "not working (" + fd.error() + ")");
@@ -25,6 +25,10 @@ mismatch.
%}
-- End --
+-- File test.txt --
+A random line.
+-- End --
+
-- Expect stdout --
fd.read() #1: working
fd.read() #2: working
diff --git a/tests/custom/run_tests.sh b/tests/custom/run_tests.sh
index d09859c..2f13c3b 100755
--- a/tests/custom/run_tests.sh
+++ b/tests/custom/run_tests.sh
@@ -40,6 +40,14 @@ extract_sections() {
outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag")
printf "" > "$outfile"
;;
+ "-- File "*" --")
+ tag="file"
+ outfile="${line#-- File }"
+ outfile="$(echo "${outfile% --}" | xargs)"
+ outfile="$dir/files$(readlink -m "/${outfile:-file}")"
+ mkdir -p "$(dirname "$outfile")"
+ printf "" > "$outfile"
+ ;;
"-- End (no-eol) --")
truncate -s -1 "$outfile"
tag=""
@@ -72,7 +80,7 @@ run_testcase() {
local fail=0
(
- cd "$topdir"
+ cd "$dir"
IFS=$'\n'
@@ -85,7 +93,10 @@ run_testcase() {
IFS=$' \t\n'
- $ucode_bin $args -e '{ "REQUIRE_SEARCH_PATH": [ "'"$ucode_lib"'/*.so" ] }' -i - <"$in" >"$dir/res.out" 2>"$dir/res.err"
+ $ucode_bin $args -e '{
+ "REQUIRE_SEARCH_PATH": [ "'"$ucode_lib"'/*.so" ],
+ "TESTFILES_PATH": "'"$dir"'/files"
+ }' -i - <"$in" >"$dir/res.out" 2>"$dir/res.err"
)
printf "%d\n" $? > "$dir/res.code"
@@ -94,7 +105,7 @@ run_testcase() {
if ! cmp -s "$dir/res.err" "${err:-$dir/empty}"; then
[ $fail = 0 ] && printf "!\n"
printf "Testcase #%d: Expected stderr did not match:\n" $num
- diff -u --color=always --label="Expected stderr" --label="Resulting stderr" "${err:-$dir/empty}" "$dir/res.err"
+ diff -au --color=always --label="Expected stderr" --label="Resulting stderr" "${err:-$dir/empty}" "$dir/res.err"
printf -- "---\n"
fail=1
fi
@@ -102,7 +113,7 @@ run_testcase() {
if ! cmp -s "$dir/res.out" "${out:-$dir/empty}"; then
[ $fail = 0 ] && printf "!\n"
printf "Testcase #%d: Expected stdout did not match:\n" $num
- diff -u --color=always --label="Expected stdout" --label="Resulting stdout" "${out:-$dir/empty}" "$dir/res.out"
+ diff -au --color=always --label="Expected stdout" --label="Resulting stdout" "${out:-$dir/empty}" "$dir/res.out"
printf -- "---\n"
fail=1
fi
@@ -110,7 +121,7 @@ run_testcase() {
if [ -n "$code" ] && ! cmp -s "$dir/res.code" "$code"; then
[ $fail = 0 ] && printf "!\n"
printf "Testcase #%d: Expected exit code did not match:\n" $num
- diff -u --color=always --label="Expected code" --label="Resulting code" "$code" "$dir/res.code"
+ diff -au --color=always --label="Expected code" --label="Resulting code" "$code" "$dir/res.code"
printf -- "---\n"
fail=1
fi
diff --git a/vm.c b/vm.c
index f6886ba..3e027bf 100644
--- a/vm.c
+++ b/vm.c
@@ -459,6 +459,7 @@ uc_vm_call_function(uc_vm_t *vm, uc_value_t *ctx, uc_value_t *fno, bool mcall, s
/* XXX: make dependent on stack size */
if (vm->callframes.count >= 1000) {
uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Too much recursion");
+ ucv_put(fno);
return false;
}
@@ -495,6 +496,7 @@ uc_vm_call_function(uc_vm_t *vm, uc_value_t *ctx, uc_value_t *fno, bool mcall, s
s = ucv_to_string(vm, arg);
uc_vm_raise_exception(vm, EXCEPTION_TYPE, "(%s) is not iterable", s);
free(s);
+ ucv_put(fno);
return false;
}
@@ -526,6 +528,7 @@ uc_vm_call_function(uc_vm_t *vm, uc_value_t *ctx, uc_value_t *fno, bool mcall, s
if (ucv_type(fno) != UC_CLOSURE) {
uc_vm_raise_exception(vm, EXCEPTION_TYPE, "left-hand side is not a function");
+ ucv_put(fno);
return false;
}
@@ -1562,7 +1565,7 @@ uc_vm_value_arith(uc_vm_t *vm, uc_vm_insn_t operation, uc_value_t *value, uc_val
static void
uc_vm_insn_update_var(uc_vm_t *vm, uc_vm_insn_t insn)
{
- uc_value_t *name, *val, *inc = uc_vm_stack_pop(vm);
+ uc_value_t *name, *val = NULL, *inc = uc_vm_stack_pop(vm);
uc_value_t *scope, *next;
bool found;
@@ -2291,12 +2294,17 @@ uc_vm_execute_chunk(uc_vm_t *vm)
uc_chunk_t *chunk = uc_vm_frame_chunk(frame);
uc_value_t *retval;
uc_vm_insn_t insn;
+ uint8_t *ip;
while (chunk) {
- if (vm->trace)
- uc_dump_insn(vm, frame->ip, (insn = uc_vm_decode_insn(vm, frame, chunk)));
- else
+ if (vm->trace) {
+ ip = frame->ip;
+ insn = uc_vm_decode_insn(vm, frame, chunk);
+ uc_dump_insn(vm, ip, insn);
+ }
+ else {
insn = uc_vm_decode_insn(vm, frame, chunk);
+ }
switch (insn) {
case I_LOAD: