summaryrefslogtreecommitdiffhomepage
path: root/tests/custom
diff options
context:
space:
mode:
Diffstat (limited to 'tests/custom')
-rw-r--r--tests/custom/00_syntax/00_single_line_comments15
-rw-r--r--tests/custom/00_syntax/01_unterminated_comment15
-rw-r--r--tests/custom/00_syntax/02_multi_line_comments12
-rw-r--r--tests/custom/00_syntax/03_expression_blocks11
-rw-r--r--tests/custom/00_syntax/04_statement_blocks20
-rw-r--r--tests/custom/00_syntax/05_block_nesting24
-rw-r--r--tests/custom/00_syntax/06_open_statement_block13
-rw-r--r--tests/custom/00_syntax/07_embedded_single_line_comments21
-rw-r--r--tests/custom/00_syntax/08_embedded_multi_line_comments24
-rw-r--r--tests/custom/00_syntax/09_string_literals50
-rw-r--r--tests/custom/00_syntax/10_numeric_literals26
-rw-r--r--tests/custom/00_syntax/11_misc_literals17
-rw-r--r--tests/custom/00_syntax/12_block_whitespace_control47
-rw-r--r--tests/custom/00_syntax/13_object_literals174
-rw-r--r--tests/custom/00_syntax/14_array_literals82
-rw-r--r--tests/custom/00_syntax/15_function_declarations164
-rw-r--r--tests/custom/00_syntax/16_for_loop299
-rw-r--r--tests/custom/00_syntax/17_while_loop71
-rw-r--r--tests/custom/00_syntax/18_if_condition121
-rw-r--r--tests/custom/00_syntax/19_arrow_functions124
-rw-r--r--tests/custom/00_syntax/20_list_expressions45
-rw-r--r--tests/custom/00_syntax/21_regex_literals89
-rw-r--r--tests/custom/01_arithmetic/00_value_conversion125
-rw-r--r--tests/custom/01_arithmetic/01_division53
-rw-r--r--tests/custom/01_arithmetic/02_modulo32
-rw-r--r--tests/custom/01_arithmetic/03_bitwise54
-rw-r--r--tests/custom/01_arithmetic/04_inc_dec49
-rw-r--r--tests/custom/02_runtime/00_scoping161
-rw-r--r--tests/custom/02_runtime/01_break_continue50
-rw-r--r--tests/custom/02_runtime/02_this50
-rw-r--r--tests/custom/02_runtime/03_try_catch138
-rw-r--r--tests/custom/02_runtime/04_switch_case325
-rw-r--r--tests/custom/02_runtime/05_closure_scope35
-rw-r--r--tests/custom/02_runtime/06_recursion59
-rw-r--r--tests/custom/03_bugs/01_try_catch_stack_mismatch52
-rw-r--r--tests/custom/03_bugs/02_array_pop_use_after_free14
-rw-r--r--tests/custom/03_bugs/03_switch_fallthrough_miscompilation16
-rw-r--r--tests/custom/03_bugs/04_property_set_abort76
-rw-r--r--tests/custom/03_bugs/05_duplicate_ressource_type31
-rw-r--r--tests/custom/03_bugs/06_lexer_escape_at_boundary12
-rw-r--r--tests/custom/03_bugs/07_lexer_overlong_lines13
-rw-r--r--tests/custom/03_bugs/08_compiler_arrow_fn_expressions15
-rw-r--r--tests/custom/03_bugs/09_reject_invalid_array_indexes25
-rw-r--r--tests/custom/03_bugs/10_break_stack_mismatch38
-rw-r--r--tests/custom/03_bugs/11_switch_stack_mismatch39
-rw-r--r--tests/custom/03_bugs/12_altblock_stack_mismatch83
-rw-r--r--tests/custom/CMakeLists.txt7
-rwxr-xr-xtests/custom/run_tests.sh179
48 files changed, 3195 insertions, 0 deletions
diff --git a/tests/custom/00_syntax/00_single_line_comments b/tests/custom/00_syntax/00_single_line_comments
new file mode 100644
index 0000000..24a32a2
--- /dev/null
+++ b/tests/custom/00_syntax/00_single_line_comments
@@ -0,0 +1,15 @@
+Single line comments.
+
+-- Expect stdout --
+This is a test.
+ a test.
+A test .
+
+-- End --
+
+-- Testcase --
+This is {# a comment within #} a test.
+{# Comment before #} a test.
+A test {# and a comment after #}.
+{# This is a comment with embedded "{#" tag. #}
+-- End --
diff --git a/tests/custom/00_syntax/01_unterminated_comment b/tests/custom/00_syntax/01_unterminated_comment
new file mode 100644
index 0000000..1d3669c
--- /dev/null
+++ b/tests/custom/00_syntax/01_unterminated_comment
@@ -0,0 +1,15 @@
+Unterminated comment
+
+-- Expect stderr --
+Syntax error: Unterminated template block
+In line 1, byte 9:
+
+ `This is {# an unclosed comment`
+ ^-- Near here
+
+
+-- End --
+
+-- Testcase --
+This is {# an unclosed comment
+-- End --
diff --git a/tests/custom/00_syntax/02_multi_line_comments b/tests/custom/00_syntax/02_multi_line_comments
new file mode 100644
index 0000000..99fc37e
--- /dev/null
+++ b/tests/custom/00_syntax/02_multi_line_comments
@@ -0,0 +1,12 @@
+Multiline comments.
+
+-- Expect stdout --
+This is an example text for testing comment blocks.
+-- End --
+
+-- Testcase --
+This is an example text {# containing
+a comment spanning multiple lines and
+ different indentation
+ #} for testing comment blocks.
+-- End --
diff --git a/tests/custom/00_syntax/03_expression_blocks b/tests/custom/00_syntax/03_expression_blocks
new file mode 100644
index 0000000..3568016
--- /dev/null
+++ b/tests/custom/00_syntax/03_expression_blocks
@@ -0,0 +1,11 @@
+Testing expression blocks.
+
+-- Expect stdout --
+The result of 3 * 7 is 21.
+To escape the start tag, output it as string expression: {{
+-- End --
+
+-- Testcase --
+The result of 3 * 7 is {{ 3 * 7 }}.
+To escape the start tag, output it as string expression: {{ "{{" }}
+-- End --
diff --git a/tests/custom/00_syntax/04_statement_blocks b/tests/custom/00_syntax/04_statement_blocks
new file mode 100644
index 0000000..920ed71
--- /dev/null
+++ b/tests/custom/00_syntax/04_statement_blocks
@@ -0,0 +1,20 @@
+Testing statement blocks.
+
+-- Expect stdout --
+The result of 3 * 7 is 21.
+A statement block may contain multiple statements: Hello World
+To escape the start tag, output it as string expression: {%
+Alternatively print it: {%
+-- End --
+
+-- Testcase --
+The result of 3 * 7 is {%+ print(3 * 7) %}.
+A statement block may contain multiple statements: {%+
+ print("Hello ");
+ print("World");
+%}
+
+To escape the start tag, output it as string expression: {{ "{%" }}
+Alternatively print it: {%+ print("{%") %}
+
+-- End --
diff --git a/tests/custom/00_syntax/05_block_nesting b/tests/custom/00_syntax/05_block_nesting
new file mode 100644
index 0000000..fcfd7da
--- /dev/null
+++ b/tests/custom/00_syntax/05_block_nesting
@@ -0,0 +1,24 @@
+Nesting blocks into non-comment blocks should fail.
+
+-- Expect stderr --
+Syntax error: Template blocks may not be nested
+In line 2, byte 61:
+
+ `We may not nest statement blocks into expression blocks: {{ {% print(1 + 2) %} }}.`
+ Near here --------------------------------------------------^
+
+
+Syntax error: Template blocks may not be nested
+In line 3, byte 61:
+
+ `We may not nest expression blocks into statement blocks: {% {{ 1 + 2 }} %}.`
+ Near here --------------------------------------------------^
+
+
+-- End --
+
+-- Testcase --
+We can nest other block types into comments: {# {% {{ 1 + 2 }} %} #}
+We may not nest statement blocks into expression blocks: {{ {% print(1 + 2) %} }}.
+We may not nest expression blocks into statement blocks: {% {{ 1 + 2 }} %}.
+-- End --
diff --git a/tests/custom/00_syntax/06_open_statement_block b/tests/custom/00_syntax/06_open_statement_block
new file mode 100644
index 0000000..9c2d142
--- /dev/null
+++ b/tests/custom/00_syntax/06_open_statement_block
@@ -0,0 +1,13 @@
+The last statement block of a template may remain open, this is useful for templates
+that contain only code.
+
+-- Expect stdout --
+This template consists entirely of script code!
+-- End --
+
+-- Testcase --
+{%
+ print("This template ");
+ print("consists entirely ");
+ print("of script code!\n");
+-- End --
diff --git a/tests/custom/00_syntax/07_embedded_single_line_comments b/tests/custom/00_syntax/07_embedded_single_line_comments
new file mode 100644
index 0000000..43f188c
--- /dev/null
+++ b/tests/custom/00_syntax/07_embedded_single_line_comments
@@ -0,0 +1,21 @@
+Statement and expression blocks may contain C++-style comments.
+A C++-style comment is started by two subsequent slashes and spans
+until the next newline.
+
+-- Expect stdout --
+The result of 5 + 9 is 14.
+Statement blocks may use C++ comments too: Test Another test.
+-- End --
+
+-- Testcase --
+The result of 5 + 9 is {{ // The block end tag is ignored: }}
+// And the expression block continues here!
+5 + 9 }}.
+Statement blocks may use C++ comments too: {%+
+ print("Test"); // A comment.
+
+ // Another comment.
+ print(" Another test.");
+%}
+
+-- End --
diff --git a/tests/custom/00_syntax/08_embedded_multi_line_comments b/tests/custom/00_syntax/08_embedded_multi_line_comments
new file mode 100644
index 0000000..75aba5f
--- /dev/null
+++ b/tests/custom/00_syntax/08_embedded_multi_line_comments
@@ -0,0 +1,24 @@
+Statement and expression blocks may contain C-style comments.
+A C-style comment is started by a slash followed by an asterisk
+and ended by an asterisk followed by a slash.
+
+Such comments may appear everywhere within statement or expression
+blocks, even in the middle of statements or expressions.
+
+-- Expect stdout --
+The result of 12 - 4 is 8.
+Statement blocks may use C comments too: Test Another test. The final test.
+-- End --
+
+-- Testcase --
+The result of 12 - 4 is {{ /* A comment before */ 12 - /* or even within */ 4 /* or after an expression */ }}.
+Statement blocks may use C comments too: {%+
+ print("Test"); /* A comment. */
+
+ /* Another comment. */
+ print(" Another test.");
+
+ print(/* A comment within */ " The final test.");
+%}
+
+-- End --
diff --git a/tests/custom/00_syntax/09_string_literals b/tests/custom/00_syntax/09_string_literals
new file mode 100644
index 0000000..0967850
--- /dev/null
+++ b/tests/custom/00_syntax/09_string_literals
@@ -0,0 +1,50 @@
+String literals may be enclosed in single or double quotes.
+Embedded escape sequences are started with a backslash, followed
+by either a hexadecimal, an octal or a single character escape sequence.
+
+-- Expect stdout --
+Single quoted string
+Double quoted string
+Unicode escape sequence: ☀💩
+Escaped double quote (") character
+Escaped single quote (') character
+Hexadecimal escape: XYZ xyz
+Octal escape: ABC xyz
+{ "Single char escape": "\u0007\b\u001b\f\r\t\u000b\\\n" }
+-- End --
+
+-- Testcase --
+{{ 'Single quoted string' }}
+{{ "Double quoted string" }}
+{{ "Unicode escape sequence: \u2600\uD83D\uDCA9" }}
+{{ "Escaped double quote (\") character" }}
+{{ 'Escaped single quote (\') character' }}
+{{ "Hexadecimal escape: \x58\x59\x5A \x78\x79\x7a" }}
+{{ "Octal escape: \101\102\103 \170\171\172" }}
+{{ { "Single char escape": "\a\b\e\f\r\t\v\\\n" } }}
+-- End --
+
+
+Testing various parsing corner cases.
+
+-- Expect stdout --
+[ "\t", "\n", "y", "\u0001", "\n", "\u0001\u0002", "\u0001\u0002", "\u0001\u0002", "\u0001a", "\na" ]
+-- End --
+
+-- Testcase --
+{%
+ print([
+ "\ ", // properly handle escaped tab
+ "\
+", // properly handle escaped newline
+ "\y", // substitute unrecognized escape with escaped char
+ "\1", // handle short octal sequence at end of string
+ "\12", // handle short octal sequence at end of string
+ "\1\2", // handle subsequent short octal sequences
+ "\001\2", // handle short sequence after long one
+ "\1\002", // handle long sequence after short one
+ "\1a", // handle short octal sequence terminated by non-octal char
+ "\12a" // handle short octal sequence terminated by non-octal char
+ ], "\n");
+%}
+-- End --
diff --git a/tests/custom/00_syntax/10_numeric_literals b/tests/custom/00_syntax/10_numeric_literals
new file mode 100644
index 0000000..3e367d0
--- /dev/null
+++ b/tests/custom/00_syntax/10_numeric_literals
@@ -0,0 +1,26 @@
+C-style numeric integer and float literals are understood, as well
+as the special keywords "Infinity" and "NaN" to denote the IEEE 754
+floating point values.
+
+Numeric values are either stored as signed 64 bit integers or signed
+doubles internally.
+
+-- Expect stdout --
+Integers literals: 123, 127, 2748, 57082
+Float literals: 10, 10.3, 1.23456e-65, 16.0625
+Special values: Infinity, Infinity, NaN, NaN
+Minimum values: -9223372036854775808, -1.79769e+308
+Maximum values: 9223372036854775807, 1.79769e+308
+Minimum truncation: -9223372036854775808, -Infinity
+Maximum truncation: 9223372036854775807, Infinity
+-- End --
+
+-- Testcase --
+Integers literals: {{ 123 }}, {{ 0177 }}, {{ 0xabc }}, {{ 0xDEFA }}
+Float literals: {{ 10. }}, {{ 10.3 }}, {{ 123.456e-67 }}, {{ 0x10.1 }}
+Special values: {{ Infinity }}, {{ 1 / 0 }}, {{ NaN }}, {{ "x" / 1 }}
+Minimum values: {{ -9223372036854775808 }}, {{ -1.7976931348623158e+308 }}
+Maximum values: {{ 9223372036854775807 }}, {{ 1.7976931348623158e+308 }}
+Minimum truncation: {{ -10000000000000000000 }}, {{ -1.0e309 }}
+Maximum truncation: {{ 10000000000000000000 }}, {{ 1.0e309 }}
+-- End --
diff --git a/tests/custom/00_syntax/11_misc_literals b/tests/custom/00_syntax/11_misc_literals
new file mode 100644
index 0000000..372741c
--- /dev/null
+++ b/tests/custom/00_syntax/11_misc_literals
@@ -0,0 +1,17 @@
+The utpl script language features a number of keywords which represent
+certain special values.
+
+-- Expect stdout --
+The "this" keyword refers to the current function context: object
+The "null" keyword represents the null value: null
+The "true" keyword represents a true boolean value: true
+The "false" keyword represents a false boolean value: false
+-- End --
+
+-- Testcase --
+{% let t = { f: function() { return this; } } %}
+The "this" keyword refers to the current function context: {{ type(t.f()) }}
+The "null" keyword represents the null value: {{ "" + null }}
+The "true" keyword represents a true boolean value: {{ true }}
+The "false" keyword represents a false boolean value: {{ false }}
+-- End --
diff --git a/tests/custom/00_syntax/12_block_whitespace_control b/tests/custom/00_syntax/12_block_whitespace_control
new file mode 100644
index 0000000..911171c
--- /dev/null
+++ b/tests/custom/00_syntax/12_block_whitespace_control
@@ -0,0 +1,47 @@
+By default, whitespace before a block start tag or after a block end tag
+is retained. By suffixing the start tag or prefixing the end tag with a
+dash, the leading or trailing whitespace is trimmed respectively.
+
+-- Expect stdout --
+Whitespace control applies to all block types:
+Comment before: | |, after: | |, both: ||
+Statement before: |test |, after: | test|, both: |test|
+Expression before: |test |, after: | test|, both: |test|
+
+By default whitespace around a block is retained.
+Leading whitespace can be trimmed like this.
+The same applies to trailing whitespace.
+It is also possible to trim bothleading and trailingwhitespace.
+
+Stripping works across multiple lines as well:test
+
+Likewise, stripping over multiple lines of trailing whitespace works as
+expected too.This is after the block.
+-- End --
+
+-- Testcase --
+Whitespace control applies to all block types:
+Comment before: | {#- test #} |, after: | {#- test #} |, both: | {#- test -#} |
+Statement before: | {%- print("test") %} |, after: | {%+ print("test") -%} |, both: | {%- print("test") -%} |
+Expression before: | {{- "test" }} |, after: | {{ "test" -}} |, both: | {{- "test" -}} |
+
+By default whitespace {{ "around a block" }} is retained.
+Leading whitespace can be trimmed {#- note the leading dash #} like this.
+The same applies to {# note the trailing dash -#} trailing whitespace.
+It is also possible to trim both {{- "leading and trailing" -}} whitespace.
+
+Stripping works across multiple lines as well:
+
+{%-
+ /* The word "test" will be printed after "well:" above */
+ print("test")
+%}
+
+
+Likewise, stripping over multiple lines of trailing whitespace works as
+expected too.
+
+{#- Any whitespace after "expected too." and before "This is after the block" will be trimmed. -#}
+
+This is after the block.
+-- End --
diff --git a/tests/custom/00_syntax/13_object_literals b/tests/custom/00_syntax/13_object_literals
new file mode 100644
index 0000000..18fbbed
--- /dev/null
+++ b/tests/custom/00_syntax/13_object_literals
@@ -0,0 +1,174 @@
+The utpl script language supports declaring objects (dictionaries) using
+either JSON or JavaScript notation.
+
+-- Expect stdout --
+{ }
+{ "name": "Bob", "age": 31, "email": { "work": "bob@example.com", "private": "bob@example.org" } }
+{ "banana": "yellow", "tomato": "red", "broccoli": "green" }
+{ "foo": "bar", "complex key": "qrx" }
+{ "foo": { "bar": true } }
+-- End --
+
+-- Testcase --
+{%
+ // An empty object can be declared using a pair of curly brackets
+ empty_obj = { };
+
+ // It is also possible to use JSON notation to declare an object
+ json_obj = {
+ "name": "Bob",
+ "age": 31,
+ "email": {
+ "work": "bob@example.com",
+ "private": "bob@example.org"
+ }
+ };
+
+ // Declaring an object in JavaScript notation is supported as well
+ another_obj = {
+ banana: "yellow",
+ tomato: "red",
+ broccoli: "green"
+ };
+
+ // Mixing styles is allowed too
+ third_obj = {
+ foo: "bar",
+ "complex key": "qrx"
+ };
+
+ // Important caveat: when nesting objects, ensure that curly brackets
+ // are separated by space or newline to avoid interpretation as
+ // expression block tag!
+ nested_obj = { foo: { bar: true } }; // <-- mind the space in "} }"
+
+ // Printing (or stringifying) objects will return their JSON representation
+ print(empty_obj, "\n");
+ print(json_obj, "\n");
+ print(another_obj, "\n");
+ print(third_obj, "\n");
+ print(nested_obj, "\n");
+%}
+-- End --
+
+
+Additionally, utpl implements ES6-like spread operators to allow shallow copying
+of object properties into other objects.
+
+-- Expect stdout --
+{ "foo": true, "bar": false }
+{ "foo": true, "bar": false, "baz": 123, "qrx": 456 }
+{ "foo": false, "bar": true, "baz": 123, "qrx": 456 }
+{ "foo": true, "bar": false }
+{ "foo": true, "bar": false, "level2": { "baz": 123, "qrx": 456 } }
+{ "foo": true, "bar": false, "0": 7, "1": 8, "2": 9 }
+-- End --
+
+-- Testcase --
+{%
+ o1 = { foo: true, bar: false };
+ o2 = { baz: 123, qrx: 456 };
+ arr = [7, 8, 9];
+
+ print(join("\n", [
+ // copying one object into another
+ { ...o1 },
+
+ // combining two objects
+ { ...o1, ...o2 },
+
+ // copying object and override properties
+ { ...o1, ...o2, foo: false, bar: true },
+
+ // default properties overwritten by spread operator
+ { foo: 123, bar: 456, ...o1 },
+
+ // nested spread operators
+ { ...o1, level2: { ...o2 } },
+
+ // merging array into objects
+ { ...o1, ...arr }
+ ]), "\n");
+%}
+-- End --
+
+
+ES2015 short hand property notation is supported as well.
+
+-- Expect stdout --
+{ "a": 123, "b": true, "c": "test" }
+-- End --
+
+-- Testcase --
+{%
+ a = 123;
+ b = true;
+ c = "test";
+
+ o = { a, b, c };
+
+ print(o, "\n");
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ':'
+In line 2, byte 14:
+
+ ` o = { "foo" };`
+ Near here ------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ o = { "foo" };
+%}
+-- End --
+
+
+ES2015 computed property names are supported.
+
+-- Expect stdout --
+{ "test": true, "hello": false, "ABC": 123 }
+-- End --
+
+-- Testcase --
+{%
+ s = "test";
+ o = {
+ [s]: true,
+ ["he" + "llo"]: false,
+ [uc("abc")]: 123
+ };
+
+ print(o, "\n");
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Expecting expression
+In line 2, byte 10:
+
+ ` o1 = { []: true };`
+ Near here --^
+
+
+Syntax error: Unexpected token
+Expecting ']'
+In line 3, byte 14:
+
+ ` o2 = { [true, false]: 123 };`
+ Near here ------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ o1 = { []: true };
+ o2 = { [true, false]: 123 };
+%}
+-- End --
diff --git a/tests/custom/00_syntax/14_array_literals b/tests/custom/00_syntax/14_array_literals
new file mode 100644
index 0000000..941ee4a
--- /dev/null
+++ b/tests/custom/00_syntax/14_array_literals
@@ -0,0 +1,82 @@
+The utpl script language supports declaring arrays using JSON notation.
+
+-- Expect stdout --
+[ ]
+[ "first", "second", 123, [ "a", "nested", "array" ], { "a": "nested object" } ]
+-- End --
+
+-- Testcase --
+{%
+ // An empty array can be declared using a pair of square brackets
+ empty_array = [ ];
+
+ // JSON notation is used to declare an array with contents
+ json_array = [
+ "first",
+ "second",
+ 123,
+ [ "a", "nested", "array" ],
+ { a: "nested object" }
+ ];
+
+ // Printing (or stringifying) arrays will return their JSON representation
+ print(empty_array, "\n");
+ print(json_array, "\n");
+-- End --
+
+
+Additionally, utpl implements ES6-like spread operators to allow shallow copying
+of array values into other arrays.
+
+-- Expect stdout --
+[ 1, 2, 3 ]
+[ 1, 2, 3, 4, 5, 6 ]
+[ 1, 2, 3, 4, 5, 6, false, true ]
+[ 1, 2, 3, false, true, 4, 5, 6 ]
+[ 1, 2, 3, [ 4, 5, 6 ] ]
+-- End --
+
+-- Testcase --
+{%
+ a1 = [ 1, 2, 3 ];
+ a2 = [ 4, 5, 6 ];
+
+ print(join("\n", [
+ // copying one array into another
+ [ ...a1 ],
+
+ // combining two arrays
+ [ ...a1, ...a2 ],
+
+ // copying array and append values
+ [ ...a1, ...a2, false, true ],
+
+ // copy array and interleave values
+ [ ...a1, false, true, ...a2 ],
+
+ // nested spread operators
+ [ ...a1, [ ...a2 ] ]
+ ]), "\n");
+%}
+-- End --
+
+Contrary to merging arrays into objects, objects cannot be merged into arrays.
+
+-- Expect stderr --
+Type error: ({ "foo": true, "bar": false }) is not iterable
+In line 5, byte 21:
+
+ ` print([ ...arr, ...obj ], "\n");`
+ Near here -------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ arr = [ 1, 2, 3 ];
+ obj = { foo: true, bar: false };
+
+ print([ ...arr, ...obj ], "\n");
+%}
+-- End --
diff --git a/tests/custom/00_syntax/15_function_declarations b/tests/custom/00_syntax/15_function_declarations
new file mode 100644
index 0000000..4257dd6
--- /dev/null
+++ b/tests/custom/00_syntax/15_function_declarations
@@ -0,0 +1,164 @@
+Function declarations follow the ECMAScript 5 syntax. Functions can be
+declared anonymously, which is useful for "throw-away" functions such
+as sort or filter callbacks or for building objects or arrays of function
+values.
+
+If functions are declared with a name, the resulting function value is
+automatically assigned under the given name to the current scope.
+
+When function values are stringifed, the resulting string will describe
+the declaration of the function.
+
+Nesting function declarations is possible as well.
+
+
+-- Expect stdout --
+function() { ... }
+function test_fn(a, b) { ... }
+function test2_fn(a, b) { ... }
+
+A function declaration using the alternative syntax:
+The function was called with arguments 123 and 456.
+
+-- End --
+
+-- Testcase --
+{%
+ // declare an anonymous function and
+ // assign resulting value
+ anon_fn = function() {
+ return "test";
+ };
+
+ // declare a named function
+ function test_fn(a, b) {
+ return a + b;
+ }
+
+ // nesting functions is legal
+ function test2_fn(a, b) {
+ function test3_fn(a, b) {
+ return a * b;
+ }
+
+ return a + test3_fn(a, b);
+ }
+
+ print(anon_fn, "\n");
+ print(test_fn, "\n");
+ print(test2_fn, "\n");
+%}
+
+A function declaration using the alternative syntax:
+{% function test3_fn(a, b): %}
+The function was called with arguments {{ a }} and {{ b }}.
+{% endfunction %}
+{{ test3_fn(123, 456) }}
+-- End --
+
+
+Additionally, utpl implements ES6-like "rest" argument syntax to declare
+variadic functions.
+
+-- Expect stdout --
+function non_variadic(a, b, c, d, e) { ... }
+[ 1, 2, 3, 4, 5 ]
+function variadic_1(a, b, ...args) { ... }
+[ 1, 2, [ 3, 4, 5 ] ]
+function variadic_2(...args) { ... }
+[ [ 1, 2, 3, 4, 5 ] ]
+-- End --
+
+-- Testcase --
+{%
+ // ordinary, non-variadic function
+ function non_variadic(a, b, c, d, e) {
+ return [ a, b, c, d, e ];
+ }
+
+ // fixed amount of arguments with variable remainder
+ function variadic_1(a, b, ...args) {
+ return [ a, b, args ];
+ }
+
+ // only variable arguments
+ function variadic_2(...args) {
+ return [ args ];
+ }
+
+ print(join("\n", [
+ non_variadic,
+ non_variadic(1, 2, 3, 4, 5),
+ variadic_1,
+ variadic_1(1, 2, 3, 4, 5),
+ variadic_2,
+ variadic_2(1, 2, 3, 4, 5)
+ ]), "\n");
+%}
+-- End --
+
+
+Complementary to the "rest" argument syntax, the spread operator may be
+used in function call arguments to pass arrays of values as argument list.
+
+-- Expect stdout --
+[ 1, 2, 3, 4, 5, 6 ]
+[ 4, 5, 6, 1, 2, 3 ]
+[ 1, 2, 3, 1, 2, 3 ]
+[ 1, 2, 3 ]
+-- End --
+
+-- Testcase --
+{%
+ arr = [ 1, 2, 3 ];
+
+ function test(...args) {
+ return args;
+ }
+
+ print(join("\n", [
+ test(...arr, 4, 5, 6),
+ test(4, 5, 6, ...arr),
+ test(...arr, ...arr),
+ test(...arr)
+ ]), "\n");
+%}
+-- End --
+
+
+Rest arguments may be only used once in a declaration and they must always
+be the last item in the argument list.
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ')'
+In line 2, byte 26:
+
+ ` function illegal(...args, ...args2) {}`
+ Near here ------------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ function illegal(...args, ...args2) {}
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ')'
+In line 2, byte 26:
+
+ ` function illegal(...args, a, b) {}`
+ Near here ------------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ function illegal(...args, a, b) {}
+%}
+-- End --
diff --git a/tests/custom/00_syntax/16_for_loop b/tests/custom/00_syntax/16_for_loop
new file mode 100644
index 0000000..67edc21
--- /dev/null
+++ b/tests/custom/00_syntax/16_for_loop
@@ -0,0 +1,299 @@
+Two for-loop variants are supported: a C-style counting for loop
+consisting of an initialization expression, a test condition
+and a step expression and a for-in-loop variant which allows
+enumerating properties of objects or items of arrays.
+
+Additionally, utpl supports an alternative syntax suitable for
+template block tags.
+
+
+-- Expect stdout --
+A simple counting for-loop:
+Iteration 0
+Iteration 1
+Iteration 2
+Iteration 3
+Iteration 4
+Iteration 5
+Iteration 6
+Iteration 7
+Iteration 8
+Iteration 9
+
+If the loop body consists of only one statement, the curly braces
+may be omitted:
+Iteration 0
+Iteration 1
+Iteration 2
+Iteration 3
+Iteration 4
+Iteration 5
+Iteration 6
+Iteration 7
+Iteration 8
+Iteration 9
+
+Any of the init-, test- and increment expressions may be omitted.
+
+Loop without initialization statement:
+Iteration null
+Iteration 1
+Iteration 2
+
+Loop without test statement:
+Iteration 0
+Iteration 1
+Iteration 2
+
+Loop without init-, test- or increment statement:
+Iteration 1
+Iteration 2
+Iteration 3
+
+For-in loop enumerating object properties:
+Key: foo
+Key: bar
+
+For-in loop enumerating array items:
+Item: one
+Item: two
+Item: three
+
+A counting for-loop using the alternative syntax:
+Iteration 0
+Iteration 1
+Iteration 2
+Iteration 3
+Iteration 4
+Iteration 5
+Iteration 6
+Iteration 7
+Iteration 8
+Iteration 9
+
+A for-in loop using the alternative syntax:
+Item 123
+Item 456
+Item 789
+
+For-in and counting for loops may declare variables:
+Iteration 0
+Iteration 1
+Iteration 2
+
+Item 123
+Item 456
+Item 789
+-- End --
+
+-- Testcase --
+A simple counting for-loop:
+{%
+ for (i = 0; i < 10; i++) {
+ print("Iteration ");
+ print(i);
+ print("\n");
+ }
+%}
+
+If the loop body consists of only one statement, the curly braces
+may be omitted:
+{%
+ for (i = 0; i < 10; i++)
+ print("Iteration ", i, "\n");
+%}
+
+Any of the init-, test- and increment expressions may be omitted.
+
+Loop without initialization statement:
+{%
+ for (; j < 3; j++)
+ print("Iteration " + j + "\n");
+%}
+
+Loop without test statement:
+{%
+ for (j = 0;; j++) {
+ if (j == 3)
+ break;
+
+ print("Iteration ", j, "\n");
+ }
+%}
+
+Loop without init-, test- or increment statement:
+{%
+ for (;;) {
+ if (k++ == 3)
+ break;
+
+ print("Iteration ", k, "\n");
+ }
+%}
+
+For-in loop enumerating object properties:
+{%
+ obj = { foo: true, bar: false };
+ for (key in obj)
+ print("Key: ", key, "\n");
+%}
+
+For-in loop enumerating array items:
+{%
+ arr = [ "one", "two", "three" ];
+ for (item in arr)
+ print("Item: ", item, "\n");
+%}
+
+A counting for-loop using the alternative syntax:
+{% for (x = 0; x < 10; x++): -%}
+Iteration {{ x }}
+{% endfor %}
+
+A for-in loop using the alternative syntax:
+{% for (n in [123, 456, 789]): -%}
+Item {{ n }}
+{% endfor %}
+
+For-in and counting for loops may declare variables:
+{% for (let i = 0; i < 3; i++): %}
+Iteration {{ i }}
+{% endfor %}
+
+{% for (let n in [123, 456, 789]): %}
+Item {{ n }}
+{% endfor %}
+-- End --
+
+
+By specifying two loop variables in for-in loop expressions, keys
+and values can be iterated simultaneously.
+
+-- Expect stdout --
+true
+false
+123
+456
+[ 0, true ]
+[ 1, false ]
+[ 2, 123 ]
+[ 3, 456 ]
+foo
+bar
+baz
+qrx
+[ "foo", true ]
+[ "bar", false ]
+[ "baz", 123 ]
+[ "qrx", 456 ]
+-- End --
+
+-- Testcase --
+{%
+ let arr = [ true, false, 123, 456 ];
+ let obj = { foo: true, bar: false, baz: 123, qrx: 456 };
+
+ // iterating arrays with one loop variable yields the array values
+ for (let x in arr)
+ print(x, "\n");
+
+ // iterating arrays with two loop variables yields the array indexes
+ // and their corresponding values
+ for (let x, y in arr)
+ print([x, y], "\n");
+
+ // iterating objects with one loop variable yields the object keys
+ for (let x in obj)
+ print(x, "\n");
+
+ // iterating objects with two loop variables yields the object keys
+ // and their corresponding values
+ for (let x, y in obj)
+ print([x, y], "\n");
+%}
+-- End --
+
+
+Ensure that for-in loop expressions with more than two variables are
+rejected.
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ';'
+In line 2, byte 24:
+
+ ` for (let x, y, z in {})`
+ Near here ----------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ for (let x, y, z in {})
+ ;
+%}
+-- End --
+
+
+Ensure that assignments in for-in loop expressions are rejected.
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ';'
+In line 2, byte 25:
+
+ ` for (let x = 1, y in {})`
+ Near here -----------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ for (let x = 1, y in {})
+ ;
+%}
+-- End --
+
+
+Ensure that too short for-in loop expressions are rejected (1/2).
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ';'
+In line 2, byte 12:
+
+ ` for (let x)`
+ Near here ----^
+
+
+-- End --
+
+-- Testcase --
+{%
+ for (let x)
+ ;
+%}
+-- End --
+
+
+Ensure that too short for-in loop expressions are rejected (2/2).
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ';'
+In line 2, byte 15:
+
+ ` for (let x, y)`
+ Near here -------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ for (let x, y)
+ ;
+%}
+-- End --
diff --git a/tests/custom/00_syntax/17_while_loop b/tests/custom/00_syntax/17_while_loop
new file mode 100644
index 0000000..1e68d6b
--- /dev/null
+++ b/tests/custom/00_syntax/17_while_loop
@@ -0,0 +1,71 @@
+Utpl implements C-style while loops which run as long as the condition
+is fulfilled.
+
+Like with for-loops, an alternative syntax form suitable for template
+blocks is supported.
+
+
+-- Expect stdout --
+A simple counting while-loop:
+Iteration 0
+Iteration 1
+Iteration 2
+Iteration 3
+Iteration 4
+Iteration 5
+Iteration 6
+Iteration 7
+Iteration 8
+Iteration 9
+
+If the loop body consists of only one statement, the curly braces
+may be omitted:
+Iteration 0
+Iteration 1
+Iteration 2
+Iteration 3
+Iteration 4
+Iteration 5
+Iteration 6
+Iteration 7
+Iteration 8
+Iteration 9
+
+A counting while-loop using the alternative syntax:
+Iteration 0
+Iteration 1
+Iteration 2
+Iteration 3
+Iteration 4
+Iteration 5
+Iteration 6
+Iteration 7
+Iteration 8
+Iteration 9
+-- End --
+
+-- Testcase --
+A simple counting while-loop:
+{%
+ i = 0;
+ while (i < 10) {
+ print("Iteration ");
+ print(i);
+ print("\n");
+ i++;
+ }
+%}
+
+If the loop body consists of only one statement, the curly braces
+may be omitted:
+{%
+ i = 0;
+ while (i < 10)
+ print("Iteration ", i++, "\n");
+%}
+
+A counting while-loop using the alternative syntax:
+{% while (x < 10): -%}
+Iteration {{ "" + x++ }}
+{% endwhile %}
+-- End --
diff --git a/tests/custom/00_syntax/18_if_condition b/tests/custom/00_syntax/18_if_condition
new file mode 100644
index 0000000..9e02767
--- /dev/null
+++ b/tests/custom/00_syntax/18_if_condition
@@ -0,0 +1,121 @@
+Utpl implements C-style if/else conditions and ?: ternary statements.
+
+Like with for- and while-loops, an alternative syntax form suitable
+for template blocks is supported.
+
+
+-- Expect stdout --
+This should print "one":
+one
+
+This should print "two":
+two
+
+Multiple conditions can be used by chaining if/else statements:
+three
+
+If the conditional block consists of only one statement, the curly
+braces may be omitted:
+two
+
+An if-condition using the alternative syntax:
+Variable x has another value.
+
+
+An if-condition using the special "elif" keyword in alternative syntax mode:
+Variable x was set to five.
+
+
+Ternary expressions function similar to if/else statements but
+only allow for a single expression in the true and false branches:
+Variable x is one
+-- End --
+
+-- Testcase --
+This should print "one":
+{%
+ x = 0;
+
+ if (x == 0) {
+ print("one");
+ }
+ else {
+ print("two");
+ }
+%}
+
+
+This should print "two":
+{%
+ x = 1;
+
+ if (x == 0) {
+ print("one");
+ }
+ else {
+ print("two");
+ }
+%}
+
+
+Multiple conditions can be used by chaining if/else statements:
+{%
+ x = 2;
+
+ if (x == 0) {
+ print("one");
+ }
+ else if (x == 1) {
+ print("two");
+ }
+ else if (x == 2) {
+ print("three");
+ }
+ else {
+ print("four");
+ }
+%}
+
+
+If the conditional block consists of only one statement, the curly
+braces may be omitted:
+{%
+ x = 5;
+
+ if (x == 0)
+ print("one");
+ else
+ print("two");
+%}
+
+
+An if-condition using the alternative syntax:
+{% if (x == 1): -%}
+Variable x was set to one.
+{% else -%}
+Variable x has another value.
+{% endif %}
+
+
+An if-condition using the special "elif" keyword in alternative syntax mode:
+{% if (x == 0): -%}
+Variable x was set to zero.
+{% elif (x == 1): -%}
+Variable x was set to one.
+{% elif (x == 5): -%}
+Variable x was set to five.
+{% else -%}
+Variable x has another value.
+{% endif %}
+
+
+Ternary expressions function similar to if/else statements but
+only allow for a single expression in the true and false branches:
+{%
+ x = 1;
+ s = (x == 1) ? "Variable x is one" : "Variable x has another value";
+
+ print(s);
+%}
+
+-- End --
diff --git a/tests/custom/00_syntax/19_arrow_functions b/tests/custom/00_syntax/19_arrow_functions
new file mode 100644
index 0000000..102c527
--- /dev/null
+++ b/tests/custom/00_syntax/19_arrow_functions
@@ -0,0 +1,124 @@
+Besides the ordinary ES5-like function declarations, utpl supports ES6 inspired
+arrow function syntax as well. Such arrow functions are useful for callbacks to functions such as replace(), map() or filter().
+
+-- Expect stdout --
+() => { ... }
+test
+(a, b) => { ... }
+3
+(...args) => { ... }
+15
+(a) => { ... }
+10
+(a) => { ... }
+36
+-- End --
+
+-- Testcase --
+{%
+
+ // assign arrow function to variable
+ test1_fn = () => {
+ return "test";
+ };
+
+ // assign arrow function with parameters
+ test2_fn = (a, b) => {
+ return a + b;
+ };
+
+ // nesting functions is legal
+ test3_fn = (...args) => {
+ nested_fn = (a, b) => {
+ return a * b;
+ };
+
+ return args[0] + nested_fn(args[0], args[1]);
+ };
+
+ // parentheses may be omitted if arrow function takes only one argument
+ test4_fn = a => {
+ a * 2;
+ };
+
+ // curly braces may be omitted if function body is a single expression
+ test5_fn = a => a * a;
+
+ print(join("\n", [
+ test1_fn,
+ test1_fn(),
+ test2_fn,
+ test2_fn(1, 2),
+ test3_fn,
+ test3_fn(3, 4),
+ test4_fn,
+ test4_fn(5),
+ test5_fn,
+ test5_fn(6)
+ ]), "\n");
+%}
+-- End --
+
+
+While the main advantage of arrow functions is the compact syntax, another
+important difference to normal functions is the "this" context behaviour -
+arrow functions do not have an own "this" context and simply inherit it from
+the outer calling scope.
+
+-- Expect stdout --
+this is set to obj: true
+arrow function uses outher this: true
+normal function has own this: true
+arrow function as method has no this: true
+-- End --
+
+-- Testcase --
+{%
+ obj = {
+ method: function() {
+ let that = this;
+ let arr = () => {
+ print("arrow function uses outher this: ", that == this, "\n");
+ };
+ let fn = function() {
+ print("normal function has own this: ", that != this, "\n");
+ };
+
+ print("this is set to obj: ", this == obj, "\n");
+
+ arr();
+ fn();
+ },
+
+ arrowfn: () => {
+ print("arrow function as method has no this: ", this == null, "\n");
+ }
+ };
+
+ obj.method();
+ obj.arrowfn();
+%}
+-- End --
+
+
+Due to the difficulty of recognizing arrow function expressions with an LR(1)
+grammar the parser has to use a generic expression rule on the lhs argument list
+and verify that it does not contain non-label nodes while building the ast. The
+subsequent testcase asserts that case.
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ';'
+In line 2, byte 10:
+
+ ` (a + 1) => { print("test\n") }`
+ Near here --^
+
+
+-- End --
+
+-- Testcase --
+{%
+ (a + 1) => { print("test\n") }
+%}
+-- End --
diff --git a/tests/custom/00_syntax/20_list_expressions b/tests/custom/00_syntax/20_list_expressions
new file mode 100644
index 0000000..d5ba459
--- /dev/null
+++ b/tests/custom/00_syntax/20_list_expressions
@@ -0,0 +1,45 @@
+Similar to ES5, utpl's language grammar allows comma separated list expressions
+in various contexts. Unless such lists happen to be part of a function call
+or array construction expression, only the last result of such an expression
+list should be used while still evaluating all sub-expressions, triggering
+side effects such as function calls or variable assignments.
+
+-- Expect stdout --
+4
+[ 1, 3 ]
+{ "a": true, "b": 1 }
+function call
+[ "test", "assigment" ]
+true
+true
+true
+[ 2, 3 ]
+-- End --
+
+-- Testcase --
+{%
+ // only the last value is considered
+ print(1 + (2, 3), "\n");
+
+ // in array constructors, parenthesized lists are reduced to the last value
+ print([ (0, 1), (2, 3) ], "\n");
+
+ // in object constructors, parenthesized lists are reduced to the last value
+ print({ a: (false, true), b: (0, 1) }, "\n");
+
+ // all list expressions are evaluated and may have side effects, even if
+ // results are discareded
+ x = (print("function call\n"), y = "assigment", "test");
+ print([x, y], "\n");
+
+ // property access operates on the last value of a parenthesized list expression
+ print(({foo: false}, {foo: true}).foo, "\n");
+ print(({foo: false}, {foo: true})["foo"], "\n");
+
+ // computed property access uses the last list expression value
+ print(({foo: true})["bar", "baz", "foo"], "\n");
+
+ // same list semantics apply to function call parameters
+ ((...args) => print(args, "\n"))((1, 2), 3);
+%}
+-- End --
diff --git a/tests/custom/00_syntax/21_regex_literals b/tests/custom/00_syntax/21_regex_literals
new file mode 100644
index 0000000..3af53bb
--- /dev/null
+++ b/tests/custom/00_syntax/21_regex_literals
@@ -0,0 +1,89 @@
+Regex literals are enclosed in forward slashes and may contain zero
+or more trailing flag characters. Interpretation of escape sequences
+within regular expression literals is subject of the underlying
+regular expression engine.
+
+-- Expect stdout --
+[ "/Hello world/", "/test/gis", "/test/g", "/test1 \\\/ test2/", "/\\x31\n\\.\u0007\b\\c\\u2600\\\\/" ]
+-- End --
+
+-- Testcase --
+{%
+ print([
+ /Hello world/, // A very simple expression
+ /test/gsi, // Regular expression flags
+ /test/gg, // Repeated flags
+ /test1 \/ test2/, // Escaped forward slash
+ /\x31\n\.\a\b\c\u2600\\/ // Ensure that escape sequences are passed as-is
+ ], "\n");
+%}
+-- End --
+
+
+Testing regular expression type.
+
+-- Expect stdout --
+object
+-- End --
+
+-- Testcase --
+{{ type(/foo/) }}
+-- End --
+
+
+Testing invalid flag characters.
+
+-- Expect stderr --
+Syntax error: Unexpected token
+Expecting ';'
+In line 2, byte 8:
+
+ ` /test/x`
+ ^-- Near here
+
+
+-- End --
+
+-- Testcase --
+{%
+ /test/x
+%}
+-- End --
+
+
+Testing unclosed regular expression.
+
+-- Expect stderr --
+Syntax error: Unterminated string
+In line 2, byte 2:
+
+ ` /foo \/`
+ ^-- Near here
+
+
+-- End --
+
+-- Testcase --
+{%
+ /foo \/
+%}
+-- End --
+
+
+Testing regex compilation errors.
+
+-- Expect stderr --
+Syntax error: Unmatched \{
+In line 2, byte 3:
+
+ ` /foo {/`
+ ^-- Near here
+
+
+-- End --
+
+-- Testcase --
+{%
+ /foo {/
+%}
+-- End --
diff --git a/tests/custom/01_arithmetic/00_value_conversion b/tests/custom/01_arithmetic/00_value_conversion
new file mode 100644
index 0000000..c44ad00
--- /dev/null
+++ b/tests/custom/01_arithmetic/00_value_conversion
@@ -0,0 +1,125 @@
+Performing unary plus or minus, or performing arithmetic operations may
+implicitly convert non-numeric values to numeric ones.
+
+If an addition is performed and either operand is a string, the other
+operand is converted into a string as well and the addition result is a
+concatenated string consisting of the two operand values.
+
+-- Expect stdout --
+HelloWorld
+12
+34
+true
+false
+null
+{ "some": "object" }
+[ "some", "array" ]
+function() { ... }
+1.2
+Infinity
+124
+123
+123
+NaN
+NaN
+NaN
+124.2
+Infinity
+1
+0
+0
+NaN
+NaN
+NaN
+1.2
+Infinity
+123
+12.3
+NaN
+-1
+0
+0
+NaN
+NaN
+NaN
+-1.2
+-Infinity
+-123
+-12.3
+NaN
+4.2
+9.6
+-- End --
+
+-- Testcase --
+{%
+ print(join("\n", [
+
+ // Adding two strings concatenates them:
+ "Hello" + "World",
+
+ // Adding a number to a string results in a string:
+ "1" + 2,
+
+ // Adding a string to a number results in a string:
+ 3 + "4",
+
+ // Adding any non-string value to a string or vice versa will
+ // force stringification of the non-string value
+ "" + true,
+ "" + false,
+ "" + null,
+ "" + { some: "object" },
+ "" + [ "some", "array" ],
+ "" + function() {},
+ "" + 1.2,
+ "" + (1 / 0),
+
+ // Adding a numeric value to a non-string, non-numeric value
+ // or vice versa will convert the non-numeric argument to a
+ // number
+ 123 + true,
+ 123 + false,
+ 123 + null,
+ 123 + { some: "object" },
+ 123 + [ "some", "array" ],
+ 123 + function() {},
+ 123 + 1.2,
+ 123 + (1 / 0),
+
+ // The unary "+" operator follows the same logic as adding a
+ // non-numeric, non-string value to a numeric one. Additionally
+ // the unary plus forces conversion of string values into numbers
+ +true,
+ +false,
+ +null,
+ +{ some: "object" },
+ +[ "some", "array" ],
+ +function() {},
+ +1.2,
+ +(1 / 0),
+ +"123",
+ +"12.3",
+ +"this is not a number",
+
+ // The unary "-" operator functions like the unary "+" one and
+ // it additionally returns the negation of the numeric value
+ -true,
+ -false,
+ -null,
+ -{ some: "object" },
+ -[ "some", "array" ],
+ -function() {},
+ -1.2,
+ -(1 / 0),
+ -"123",
+ -"12.3",
+ -"this is not a number",
+
+ // Adding a double to an integer or vice versa will force the
+ // result to a double as well
+ 1.2 + 3,
+ 4 + 5.6,
+
+ "" ]));
+-- End --
diff --git a/tests/custom/01_arithmetic/01_division b/tests/custom/01_arithmetic/01_division
new file mode 100644
index 0000000..d4a2adb
--- /dev/null
+++ b/tests/custom/01_arithmetic/01_division
@@ -0,0 +1,53 @@
+While arithmetic divisions generally follow the value conversion rules
+outlined in the "00_value_conversion" test case, a number of additional
+constraints apply.
+
+-- Expect stdout --
+Division by zero yields Infinity:
+1 / 0 = Infinity
+
+Division by Infinity yields zero:
+1 / Infinity = 0
+
+Dividing Infinity yields Infinity:
+Infinity / 1 = Infinity
+
+Dividing Infinity by Infinity yields NaN:
+Infinity / Infinity = NaN
+
+If either operand is NaN, the result is NaN:
+1 / NaN = NaN
+NaN / 1 = NaN
+
+If both operands are integers, integer division is performed:
+10 / 3 = 3
+
+If either operand is a double, double division is performed:
+10.0 / 3 = 3.33333
+10 / 3.0 = 3.33333
+-- End --
+
+-- Testcase --
+Division by zero yields Infinity:
+1 / 0 = {{ 1 / 0 }}
+
+Division by Infinity yields zero:
+1 / Infinity = {{ 1 / Infinity }}
+
+Dividing Infinity yields Infinity:
+Infinity / 1 = {{ Infinity / 1 }}
+
+Dividing Infinity by Infinity yields NaN:
+Infinity / Infinity = {{ Infinity / Infinity }}
+
+If either operand is NaN, the result is NaN:
+1 / NaN = {{ 1 / NaN }}
+NaN / 1 = {{ NaN / 1 }}
+
+If both operands are integers, integer division is performed:
+10 / 3 = {{ 10 / 3 }}
+
+If either operand is a double, double division is performed:
+10.0 / 3 = {{ 10.0 / 3 }}
+10 / 3.0 = {{ 10 / 3.0 }}
+-- End --
diff --git a/tests/custom/01_arithmetic/02_modulo b/tests/custom/01_arithmetic/02_modulo
new file mode 100644
index 0000000..244d624
--- /dev/null
+++ b/tests/custom/01_arithmetic/02_modulo
@@ -0,0 +1,32 @@
+The utpl language supports modulo divisions, however they're only defined
+for integer values.
+
+-- Expect stdout --
+If both operands are integers or convertible to integers,
+the modulo division yields the remainder:
+10 % 4 = 2
+"10" % 4 = 2
+10 % "4" = 2
+"10" % "4" = 2
+
+If either operand is a double value, the modulo operation
+yields NaN:
+10.0 % 4 = NaN
+10 % 4.0 = NaN
+"10.0" % 4 = NaN
+-- End --
+
+-- Testcase --
+If both operands are integers or convertible to integers,
+the modulo division yields the remainder:
+10 % 4 = {{ 10 % 4 }}
+"10" % 4 = {{ "10" % 4 }}
+10 % "4" = {{ 10 % 4 }}
+"10" % "4" = {{ "10" % "4" }}
+
+If either operand is a double value, the modulo operation
+yields NaN:
+10.0 % 4 = {{ 10.0 % 4 }}
+10 % 4.0 = {{ 10 % 4.0 }}
+"10.0" % 4 = {{ "10.0" % 4 }}
+-- End --
diff --git a/tests/custom/01_arithmetic/03_bitwise b/tests/custom/01_arithmetic/03_bitwise
new file mode 100644
index 0000000..faf4ffd
--- /dev/null
+++ b/tests/custom/01_arithmetic/03_bitwise
@@ -0,0 +1,54 @@
+Utpl implements C-style bitwise operations. One detail is that these operations
+coerce their operands to signed 64bit integer values internally.
+
+-- Expect stdout --
+Left shift:
+10 << 2 = 40
+3.3 << 4.1 = 48
+
+Right shift:
+10 >> 2 = 2
+3.3 >> 4.1 = 0
+
+Bitwise and:
+1234 & 200 = 192
+120.3 & 54.3 = 48
+
+Bitwise xor:
+1234 ^ 200 = 1050
+120.3 ^ 54.3 = 78
+
+Bitwise or:
+1234 | 200 = 1242
+120.3 | 54.3 = 126
+
+Complement:
+~0 = -1
+~10.4 = -11
+-- End --
+
+-- Testcase --
+Left shift:
+10 << 2 = {{ 10 << 2 }}
+3.3 << 4.1 = {{ 3.3 << 4.1 }}
+
+Right shift:
+10 >> 2 = {{ 10 >> 2 }}
+3.3 >> 4.1 = {{ 3.3 >> 4.1 }}
+
+Bitwise and:
+1234 & 200 = {{ 1234 & 200 }}
+120.3 & 54.3 = {{ 120.3 & 54.3 }}
+
+Bitwise xor:
+1234 ^ 200 = {{ 1234 ^ 200 }}
+120.3 ^ 54.3 = {{ 120.3 ^ 54.3 }}
+
+Bitwise or:
+1234 | 200 = {{ 1234 | 200 }}
+120.3 | 54.3 = {{ 120.3 | 54.3 }}
+
+Complement:
+~0 = {{ ~0 }}
+~10.4 = {{ ~10.4 }}
+-- End --
diff --git a/tests/custom/01_arithmetic/04_inc_dec b/tests/custom/01_arithmetic/04_inc_dec
new file mode 100644
index 0000000..ae50ceb
--- /dev/null
+++ b/tests/custom/01_arithmetic/04_inc_dec
@@ -0,0 +1,49 @@
+Utpl implements C-style pre- and postfix increment and decrement operators.
+
+Pre-increment or -decrement operations first mutate the value and then return
+the resulting value while post-increment or -decrement operations first return
+the initial value and then mutate the operand.
+
+Since the decrement and increment operators mutate their operand, they
+may only be applied to variables, not constant literal expressions.
+
+If an undefined variable is incremented or decremented, its initial value
+is assumed to be "0".
+
+If a non-numeric value is incremented or decremented, it is converted to a
+number first. If the value is not convertible, the result of the increment
+or decrement operation is NaN.
+
+-- Expect stdout --
+Incrementing a not existing variable assumes "0" as initial value:
+
+ - Postfix increment result: 0, value after: 1
+ - Prefix increment result: 1, value after: 1
+ - Postfix decrement result: 0, value after: -1
+ - Prefix decrement result: -1, value after: -1
+
+Incrementing a non-numeric value will convert it to a number:
+
+124
+3.5
+2
+NaN
+-- End --
+
+-- Testcase --
+Incrementing a not existing variable assumes "0" as initial value:
+
+ - Postfix increment result: {{ "" + a++ }}, value after: {{ a }}
+ - Prefix increment result: {{ "" + ++b }}, value after: {{ b }}
+ - Postfix decrement result: {{ "" + c-- }}, value after: {{ c }}
+ - Prefix decrement result: {{ "" + --d }}, value after: {{ d }}
+
+Incrementing a non-numeric value will convert it to a number:
+
+{%
+ n = "123"; n++; print(n, "\n");
+ n = "4.5"; n--; print(n, "\n");
+ n = true; n++; print(n, "\n");
+ n = { some: "object" }; n--; print(n, "\n");
+%}
+-- End --
diff --git a/tests/custom/02_runtime/00_scoping b/tests/custom/02_runtime/00_scoping
new file mode 100644
index 0000000..5fadf43
--- /dev/null
+++ b/tests/custom/02_runtime/00_scoping
@@ -0,0 +1,161 @@
+Utpl implements function scoping, make sure that let variables are
+invisible outside of the function scope.
+
+-- Expect stdout --
+a_global=true
+a_local=true
+
+b_global=true
+b_local=false
+
+c_global=true
+c_local=false
+
+
+When seting a nonlocal variable, it is set in the nearest parent
+scope containing the variable or in the root scope if the variable
+was not found.
+
+x=2
+y=
+z=1
+
+
+Variables implicitly declared by for-in or counting for loops follow the same
+scoping rules.
+
+inner2 f_a=3
+inner2 f_b=
+inner2 f_c=3
+inner2 f_d=
+inner2 f_e=3
+
+inner f_a=3
+inner f_b=
+inner f_c=3
+inner f_d=
+inner f_e=3
+
+outer f_a=3
+outer f_b=
+outer f_c=
+outer f_d=
+outer f_e=3
+-- End --
+
+-- Testcase --
+{%
+ a_global = true;
+ let a_local = true;
+
+ function test() {
+ b_global = true;
+ let b_local = true;
+
+ function test2() {
+ c_global = true;
+ let c_local = true;
+ }
+
+ test2();
+ }
+
+ test();
+-%}
+
+a_global={{ !!a_global }}
+a_local={{ !!a_local }}
+
+b_global={{ !!b_global }}
+b_local={{ !!b_local }}
+
+c_global={{ !!c_global }}
+c_local={{ !!c_local }}
+
+
+When seting a nonlocal variable, it is set in the nearest parent
+scope containing the variable or in the root scope if the variable
+was not found.
+
+{%
+ x = 1;
+
+ function scope1() {
+ x = 2;
+ let y;
+
+ function scope2() {
+ // this does not set "y" in the root scope but overwrites the
+ // variable declared in the "scope1" function scope.
+ y = 2;
+
+ // this sets "z" in the root scope because it was not declared
+ // anywhere yet
+ z = 1;
+ }
+
+ scope2();
+ }
+
+ scope1();
+-%}
+
+x={{ x }}
+y={{ y }}
+z={{ z }}
+
+
+Variables implicitly declared by for-in or counting for loops follow the same
+scoping rules.
+
+{%
+ function scope3() {
+ // f_a is not declared local and be set in the root scope
+ for (f_a = 1; f_a < 3; f_a++)
+ ;
+
+ for (let f_b = 1; f_b < 3; f_b++)
+ ;
+
+ let f_c;
+
+ function scope4() {
+ // f_c is not declared local but declared in the parent scope, it
+ // will be set there
+ for (f_c in [1, 2, 3])
+ ;
+
+ for (let f_d in [1, 2, 3])
+ ;
+
+ // f_e is not declared, it will be set in the root scope
+ for (f_e in [1, 2, 3])
+ ;
+
+ print("inner2 f_a=", f_a, "\n");
+ print("inner2 f_b=", f_b, "\n");
+ print("inner2 f_c=", f_c, "\n");
+ print("inner2 f_d=", f_d, "\n");
+ print("inner2 f_e=", f_e, "\n");
+ print("\n");
+ }
+
+ scope4();
+
+ print("inner f_a=", f_a, "\n");
+ print("inner f_b=", f_b, "\n");
+ print("inner f_c=", f_c, "\n");
+ print("inner f_d=", f_d, "\n");
+ print("inner f_e=", f_e, "\n");
+ print("\n");
+ }
+
+ scope3();
+
+ print("outer f_a=", f_a, "\n");
+ print("outer f_b=", f_b, "\n");
+ print("outer f_c=", f_c, "\n");
+ print("outer f_d=", f_d, "\n");
+ print("outer f_e=", f_e, "\n");
+%}
+-- End --
diff --git a/tests/custom/02_runtime/01_break_continue b/tests/custom/02_runtime/01_break_continue
new file mode 100644
index 0000000..a27d072
--- /dev/null
+++ b/tests/custom/02_runtime/01_break_continue
@@ -0,0 +1,50 @@
+The "break" and "continue" statements allow to abort a running loop or to
+prematurely advance to the next cycle.
+
+-- Expect stdout --
+Testing break:
+ - Iteration 0
+ - Iteration 1
+ - Iteration 2
+ - Iteration 3
+ - Iteration 4
+ - Iteration 5
+ - Iteration 6
+ - Iteration 7
+ - Iteration 8
+ - Iteration 9
+ - Iteration 10
+
+Testing continue:
+ - Iteration 0
+ - Iteration 2
+ - Iteration 4
+ - Iteration 6
+ - Iteration 8
+-- End --
+
+-- Testcase --
+Testing break:
+{%
+ let i = 0;
+
+ while (true) {
+ print(" - Iteration ", i, "\n");
+
+ if (i == 10)
+ break;
+
+ i++;
+ }
+%}
+
+Testing continue:
+{%
+ for (i = 0; i < 10; i++) {
+ if (i % 2)
+ continue;
+
+ print(" - Iteration ", i, "\n");
+ }
+%}
+-- End --
diff --git a/tests/custom/02_runtime/02_this b/tests/custom/02_runtime/02_this
new file mode 100644
index 0000000..d8e85d2
--- /dev/null
+++ b/tests/custom/02_runtime/02_this
@@ -0,0 +1,50 @@
+The "this" object accesses the current function context.
+
+-- Expect stdout --
+true
+true
+-- End --
+
+-- Testcase --
+{%
+ // Functions not invoked on objects have no this context
+ function test() {
+ return (this === null);
+ }
+
+ // When invoked, "this" will point to the object containing the function
+ let o;
+ o = {
+ test: function() {
+ return (this === o);
+ }
+ };
+
+ print(test(), "\n");
+ print(o.test(), "\n");
+%}
+-- End --
+
+Test that the context is properly restored if function call arguments are
+dot or bracket expressions as well.
+
+-- Expect stdout --
+true
+true
+-- End --
+
+-- Testcase --
+{%
+ let o;
+ o = {
+ test: function() {
+ return (this === o);
+ }
+ };
+
+ let dummy = { foo: true, bar: false };
+
+ print(o.test(dummy.foo, dummy.bar), "\n");
+ print(o.test(dummy.foo, o.test(dummy.foo, dummy.bar)), "\n");
+%}
+-- End --
diff --git a/tests/custom/02_runtime/03_try_catch b/tests/custom/02_runtime/03_try_catch
new file mode 100644
index 0000000..751ca1d
--- /dev/null
+++ b/tests/custom/02_runtime/03_try_catch
@@ -0,0 +1,138 @@
+Wrapping an exeptional operation in try {} catch {} allows handling the
+resulting exception and to continue the execution flow.
+
+-- Expect stdout --
+Catched first exception.
+Catched second exception: exception 2.
+After exceptions.
+-- End --
+
+-- Testcase --
+{%
+ // A try-catch block that discards the exception information.
+ try {
+ die("exception 1");
+ }
+ catch {
+ print("Catched first exception.\n");
+ }
+
+ // A try-catch block that captures the resulting exception in
+ // the given variable.
+ try {
+ die("exception 2");
+ }
+ catch (e) {
+ print("Catched second exception: ", e, ".\n");
+ }
+
+ print("After exceptions.\n");
+%}
+-- End --
+
+
+Ensure that exceptions are propagated through C function calls.
+
+-- Expect stderr --
+exception
+In [anonymous function](), line 3, byte 18:
+ called from function replace ([C])
+ called from anonymous function ([stdin]:4:3)
+
+ ` die("exception");`
+ Near here -------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ replace("test", "t", function(m) {
+ die("exception");
+ });
+%}
+-- End --
+
+
+Ensure that exception can be catched through C function calls.
+
+-- Expect stdout --
+Caught exception: exception
+-- End --
+
+-- Testcase --
+{%
+ try {
+ replace("test", "t", function(m) {
+ die("exception");
+ });
+ }
+ catch (e) {
+ print("Caught exception: ", e, "\n");
+ }
+%}
+-- End --
+
+
+Ensure that exceptions are propagated through user function calls.
+
+-- Expect stderr --
+exception
+In a(), line 3, byte 18:
+ called from function b ([stdin]:7:5)
+ called from function c ([stdin]:11:5)
+ called from anonymous function ([stdin]:14:4)
+
+ ` die("exception");`
+ Near here -------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ function a() {
+ die("exception");
+ }
+
+ function b() {
+ a();
+ }
+
+ function c() {
+ b();
+ }
+
+ c();
+%}
+-- End --
+
+
+Ensure that exceptions can be caught in parent functions.
+
+-- Expect stdout --
+Caught exception: exception
+-- End --
+
+-- Testcase --
+{%
+ function a() {
+ die("exception");
+ }
+
+ function b() {
+ a();
+ }
+
+ function c() {
+ try {
+ b();
+ }
+ catch (e) {
+ print("Caught exception: ", e, "\n");
+ }
+ }
+
+ c();
+%}
+-- End --
diff --git a/tests/custom/02_runtime/04_switch_case b/tests/custom/02_runtime/04_switch_case
new file mode 100644
index 0000000..4c1fc57
--- /dev/null
+++ b/tests/custom/02_runtime/04_switch_case
@@ -0,0 +1,325 @@
+Testing utpl switch statements.
+
+
+1. Ensure that execution starts at the first matching case.
+
+-- Expect stdout --
+1a
+-- End --
+
+-- Testcase --
+{%
+ switch (1) {
+ case 1:
+ print("1a\n");
+ break;
+
+ case 1:
+ print("1b\n");
+ break;
+
+ case 2:
+ print("2\n");
+ break;
+ }
+%}
+-- End --
+
+
+2. Ensure that default case is only used if no case matches,
+ even if declared first.
+
+-- Expect stdout --
+1
+default
+-- End --
+
+-- Testcase --
+{%
+ for (n in [1, 3]) {
+ switch (n) {
+ default:
+ print("default\n");
+ break;
+
+ case 1:
+ print("1\n");
+ break;
+
+ case 2:
+ print("2\n");
+ break;
+ }
+ }
+%}
+-- End --
+
+
+3. Ensure that cases without break fall through into
+ subsequent cases.
+
+-- Expect stdout --
+1
+2
+default
+1
+2
+-- End --
+
+-- Testcase --
+{%
+ for (n in [1, 3]) {
+ switch (n) {
+ default:
+ print("default\n");
+
+ case 1:
+ print("1\n");
+
+ case 2:
+ print("2\n");
+ }
+ }
+%}
+-- End --
+
+
+4. Ensure that a single default case matches.
+
+-- Expect stdout --
+default
+default
+-- End --
+
+-- Testcase --
+{%
+ for (n in [1, 3]) {
+ switch (n) {
+ default:
+ print("default\n");
+ }
+ }
+%}
+-- End --
+
+
+5. Ensure that duplicate default cases emit a syntax
+ error during parsing.
+
+-- Expect stderr --
+Syntax error: more than one switch default case
+In line 6, byte 3:
+
+ ` default:`
+ ^-- Near here
+
+
+Syntax error: Expecting expression
+In line 8, byte 2:
+
+ ` }`
+ ^-- Near here
+
+
+-- End --
+
+-- Testcase --
+{%
+ switch (1) {
+ default:
+ print("default1\n");
+
+ default:
+ print("default2\n");
+ }
+%}
+-- End --
+
+
+6. Ensure that case values use strict comparison.
+
+-- Expect stdout --
+b
+b
+-- End --
+
+-- Testcase --
+{%
+ switch (1.0) {
+ case 1:
+ print("a\n");
+ break;
+
+ case 1.0:
+ print("b\n");
+ break;
+ }
+
+ switch ("123") {
+ case 123:
+ print("a\n");
+ break;
+
+ case "123":
+ print("b\n");
+ break;
+ }
+%}
+-- End --
+
+
+7. Ensure that case values may be complex expressions.
+
+-- Expect stdout --
+2, 3, 1
+-- End --
+
+-- Testcase --
+{%
+ switch (1) {
+ case a = 2, b = 3, c = 1:
+ print(join(", ", [ a, b, c ]), "\n");
+ break;
+ }
+%}
+-- End --
+
+
+8. Ensure that empty switch statements are accepted by the
+ parser and that the test expression is evaluated.
+
+-- Expect stdout --
+true
+-- End --
+
+-- Testcase --
+{%
+ x = false;
+
+ switch (x = true) {
+
+ }
+
+ print(x, "\n");
+%}
+-- End --
+
+
+9. Ensure that `return` breaks out of switch statements.
+
+-- Expect stdout --
+one
+two
+-- End --
+
+-- Testcase --
+{%
+ function test(n) {
+ switch (n) {
+ case 1:
+ return "one";
+
+ case 2:
+ return "two";
+
+ default:
+ return "three";
+ }
+ }
+
+ print(test(1), "\n");
+ print(test(2), "\n");
+%}
+-- End --
+
+
+10. Ensure that `continue` breaks out of switch statements.
+
+-- Expect stdout --
+one
+two
+-- End --
+
+-- Testcase --
+{%
+ for (n in [1,2]) {
+ switch (n) {
+ case 1:
+ print("one\n");
+ continue;
+
+ case 2:
+ print("two\n");
+ continue;
+
+ default:
+ print("three\n");
+ }
+ }
+%}
+-- End --
+
+
+11. Ensure that exceptions break out of switch statements.
+
+-- Expect stdout --
+one
+-- End --
+
+-- Expect stderr --
+Died
+In test(), line 6, byte 8:
+ called from anonymous function ([stdin]:17:14)
+
+ ` die();`
+ Near here ------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ function test(n) {
+ switch (n) {
+ case 1:
+ print("one\n");
+ die();
+
+ case 2:
+ print("two\n");
+ die();
+
+ default:
+ print("three\n");
+ }
+ }
+
+ print(test(1), "\n");
+%}
+-- End --
+
+
+12. Ensure that consecutive cases values are properly handled.
+
+-- Expect stdout --
+three and four
+-- End --
+
+-- Testcase --
+{%
+ switch (3) {
+ case 1:
+ case 2:
+ print("one and two\n");
+ break;
+
+ case 3:
+ case 4:
+ print("three and four\n");
+ break;
+
+ default:
+ print("five\n");
+ }
+%}
+-- End --
diff --git a/tests/custom/02_runtime/05_closure_scope b/tests/custom/02_runtime/05_closure_scope
new file mode 100644
index 0000000..c59a433
--- /dev/null
+++ b/tests/custom/02_runtime/05_closure_scope
@@ -0,0 +1,35 @@
+Testing closure scopes.
+
+
+1. Ensure that the declaring scope is retained in functions.
+
+-- Expect stdout --
+Make function with x=1
+Make function with x=2
+Make function with x=3
+x is 1
+x is 2
+x is 3
+-- End --
+
+-- Testcase --
+{%
+ let count=0;
+
+ function a() {
+ let x = ++count;
+ print("Make function with x=", x, "\n");
+ return function() {
+ print("x is ", x, "\n");
+ };
+ }
+
+ let fn1 = a();
+ let fn2 = a();
+ let fn3 = a();
+
+ fn1();
+ fn2();
+ fn3();
+%}
+-- End --
diff --git a/tests/custom/02_runtime/06_recursion b/tests/custom/02_runtime/06_recursion
new file mode 100644
index 0000000..470fc3a
--- /dev/null
+++ b/tests/custom/02_runtime/06_recursion
@@ -0,0 +1,59 @@
+Testing recursive invocations.
+
+
+1. Testing recursive invocation.
+
+-- Expect stdout --
+1
+2
+4
+8
+16
+32
+64
+128
+256
+512
+1024
+2048
+4096
+8192
+16384
+-- End --
+
+-- Testcase --
+{%
+ function test(x) {
+ print(x, "\n");
+
+ if (x < 10000)
+ test(x * 2);
+ }
+
+ test(1);
+%}
+-- End --
+
+
+2. Testing infinite recursion.
+
+-- Expect stderr --
+Runtime error: Too much recursion
+In test(), line 3, byte 8:
+ called from anonymous function ([stdin]:6:7)
+
+ ` test();`
+ Near here ---^
+
+
+-- End --
+
+-- Testcase --
+{%
+ function test() {
+ test();
+ }
+
+ test();
+%}
+-- End --
diff --git a/tests/custom/03_bugs/01_try_catch_stack_mismatch b/tests/custom/03_bugs/01_try_catch_stack_mismatch
new file mode 100644
index 0000000..f6e5a0a
--- /dev/null
+++ b/tests/custom/03_bugs/01_try_catch_stack_mismatch
@@ -0,0 +1,52 @@
+When compiling a try/catch statement with an exception variable, the catch
+skip jump incorrectly pointed to the POP instruction popping the exception
+variable off the stack, leading to a stack position mismatch between
+compiler and vm, causing local variables to yield wrong values at runtime.
+
+-- Expect stdout --
+1
+-- End --
+
+-- Testcase --
+{%
+ function f() {
+ let x;
+
+ try {
+ x = 1;
+ }
+ catch(e) {
+
+ }
+
+ // Before the fix, `x` incorrectly yielded the print function value
+ print(x, "\n");
+ }
+
+ f()
+%}
+-- End --
+
+
+When compiling a try/catch statement with local variable declearations
+within the try block, the catch skip jump incorrectly happened before the
+local try block variables were popped off the stack, leading to a stack
+position mismatch between compiler and vm, causing local variables to
+yield wrong values at runtime.
+
+-- Expect stdout --
+1
+-- End --
+
+-- Testcase --
+{%
+ try {
+ let a;
+ }
+ catch {}
+
+ let b = 1;
+
+ print(b, "\n");
+%}
+-- End --
diff --git a/tests/custom/03_bugs/02_array_pop_use_after_free b/tests/custom/03_bugs/02_array_pop_use_after_free
new file mode 100644
index 0000000..22f63ff
--- /dev/null
+++ b/tests/custom/03_bugs/02_array_pop_use_after_free
@@ -0,0 +1,14 @@
+When popping an element off an array, especially the last one, the popped
+value might have been freed before the refcount was increased later on
+function return.
+
+-- Expect stdout --
+1
+-- End --
+
+-- Testcase --
+{%
+ x = [1];
+ print(pop(x), "\n"); // This caused a SIGABRT before the bugfix
+%}
+-- End --
diff --git a/tests/custom/03_bugs/03_switch_fallthrough_miscompilation b/tests/custom/03_bugs/03_switch_fallthrough_miscompilation
new file mode 100644
index 0000000..3e6410e
--- /dev/null
+++ b/tests/custom/03_bugs/03_switch_fallthrough_miscompilation
@@ -0,0 +1,16 @@
+When falling through from a matched switch case into the default case,
+the compiler incorrectly emitted bytecode that led to an endless loop.
+
+-- Expect stdout --
+1
+-- End --
+
+-- Testcase --
+{%
+ switch (1) {
+ case 1:
+ default:
+ print("1\n");
+ }
+%}
+-- End --
diff --git a/tests/custom/03_bugs/04_property_set_abort b/tests/custom/03_bugs/04_property_set_abort
new file mode 100644
index 0000000..8af477f
--- /dev/null
+++ b/tests/custom/03_bugs/04_property_set_abort
@@ -0,0 +1,76 @@
+When attempting to set a property on a non-array, non-object value the
+VM aborted due to an assert triggered by libjson-c.
+
+-- Testcase --
+{% (null).x = 1 %}
+-- End --
+
+-- Expect stderr --
+Type error: attempt to set property on null value
+In line 1, byte 15:
+
+ `{% (null).x = 1 %}`
+ Near here ----^
+
+
+-- End --
+
+
+-- Testcase --
+{% (1).x = 1 %}
+-- End --
+
+-- Expect stderr --
+Type error: attempt to set property on integer value
+In line 1, byte 12:
+
+ `{% (1).x = 1 %}`
+ Near here -^
+
+
+-- End --
+
+
+-- Testcase --
+{% (1.2).x = 1 %}
+-- End --
+
+-- Expect stderr --
+Type error: attempt to set property on double value
+In line 1, byte 14:
+
+ `{% (1.2).x = 1 %}`
+ Near here ---^
+
+
+-- End --
+
+
+-- Testcase --
+{% (true).x = 1 %}
+-- End --
+
+-- Expect stderr --
+Type error: attempt to set property on boolean value
+In line 1, byte 15:
+
+ `{% (true).x = 1 %}`
+ Near here ----^
+
+
+-- End --
+
+
+-- Testcase --
+{% ("test").x = 1 %}
+-- End --
+
+-- Expect stderr --
+Type error: attempt to set property on string value
+In line 1, byte 17:
+
+ `{% ("test").x = 1 %}`
+ Near here ------^
+
+
+-- End --
diff --git a/tests/custom/03_bugs/05_duplicate_ressource_type b/tests/custom/03_bugs/05_duplicate_ressource_type
new file mode 100644
index 0000000..21166b2
--- /dev/null
+++ b/tests/custom/03_bugs/05_duplicate_ressource_type
@@ -0,0 +1,31 @@
+When requiring a C module that registers custom ressource types multiple
+times, ressource 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.
+
+-- Testcase --
+{%
+ fs = require("fs");
+ fd = fs.open("README.md");
+
+ printf("fd.read() #1: %s\n",
+ fd.read("line") ? "working" : "not working (" + fd.error() + ")");
+
+ fd.close();
+
+
+ fs = require("fs");
+ fd = fs.open("README.md");
+
+ printf("fd.read() #2: %s\n",
+ fd.read("line") ? "working" : "not working (" + fd.error() + ")");
+
+ fd.close();
+%}
+-- End --
+
+-- Expect stdout --
+fd.read() #1: working
+fd.read() #2: working
+-- End --
diff --git a/tests/custom/03_bugs/06_lexer_escape_at_boundary b/tests/custom/03_bugs/06_lexer_escape_at_boundary
new file mode 100644
index 0000000..e80b0a1
--- /dev/null
+++ b/tests/custom/03_bugs/06_lexer_escape_at_boundary
@@ -0,0 +1,12 @@
+When the lexer processed a backslash introducing a string escape directly
+at the buffer boundary, the backslash was incorrectly retained.
+
+-- Testcase --
+{%
+ print("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl\n");
+%}
+-- End --
+
+-- Expect stdout --
+abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl
+-- End --
diff --git a/tests/custom/03_bugs/07_lexer_overlong_lines b/tests/custom/03_bugs/07_lexer_overlong_lines
new file mode 100644
index 0000000..d2dd3be
--- /dev/null
+++ b/tests/custom/03_bugs/07_lexer_overlong_lines
@@ -0,0 +1,13 @@
+A logic flaw in the lineinfo encoding function led to an endless tight
+loop when a buffer chunk with 128 byte got consumed, which may happen
+when parsing very long literals.
+
+-- Testcase --
+{%
+ print("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg\n");
+%}
+-- End --
+
+-- Expect stdout --
+abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg
+-- End --
diff --git a/tests/custom/03_bugs/08_compiler_arrow_fn_expressions b/tests/custom/03_bugs/08_compiler_arrow_fn_expressions
new file mode 100644
index 0000000..5cd8960
--- /dev/null
+++ b/tests/custom/03_bugs/08_compiler_arrow_fn_expressions
@@ -0,0 +1,15 @@
+Arrow functions with single expression bodies were parsed with a wrong
+precedence level, causing comma expressions to be greedily consumed.
+
+-- Testcase --
+{%
+ print({
+ a: () => 1,
+ b: () => 2
+ }, "\n");
+%}
+-- End --
+
+-- Expect stdout --
+{ "a": "() => { ... }", "b": "() => { ... }" }
+-- End --
diff --git a/tests/custom/03_bugs/09_reject_invalid_array_indexes b/tests/custom/03_bugs/09_reject_invalid_array_indexes
new file mode 100644
index 0000000..a7e5272
--- /dev/null
+++ b/tests/custom/03_bugs/09_reject_invalid_array_indexes
@@ -0,0 +1,25 @@
+Since libjson-c's json_object_get_int64() returns 0 for any input value
+that has no integer representation, any kind of invalid array index
+incorrectly yielded the first array element.
+
+-- Testcase --
+{%
+ x = [1, 2, 3];
+
+ print([
+ x[1],
+ x["1"],
+ x[1.0],
+ x[1.1],
+ x.foo,
+ x["foo"],
+ x["0abc"],
+ x[x],
+ x[{ foo: true }]
+ ], "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 2, 2, 2, null, null, null, null, null, null ]
+-- End --
diff --git a/tests/custom/03_bugs/10_break_stack_mismatch b/tests/custom/03_bugs/10_break_stack_mismatch
new file mode 100644
index 0000000..ae16dac
--- /dev/null
+++ b/tests/custom/03_bugs/10_break_stack_mismatch
@@ -0,0 +1,38 @@
+When emitting jump instructions for breaking out of for-loops, the compiler
+incorrectly set the jump target before the pop instruction clearing the
+intermediate loop variables. Since the break instruction itself already
+compiles to a series of pop instructions reverting the stack to it's the
+pre-loop state, intermediate values got popped twice, leading to a stack
+layout mismatch between compiler and VM, resulting in wrong local variable
+values or segmentation faults at runtime.
+
+-- Testcase --
+{%
+ let x = 1;
+
+ for (let y in [2])
+ break;
+
+ print(x, "\n");
+%}
+-- End --
+
+-- Expect stdout --
+1
+-- End --
+
+
+-- Testcase --
+{%
+ let x = 1;
+
+ for (let y = 0; y < 1; y++)
+ break;
+
+ print(x, "\n");
+%}
+-- End --
+
+-- Expect stdout --
+1
+-- End --
diff --git a/tests/custom/03_bugs/11_switch_stack_mismatch b/tests/custom/03_bugs/11_switch_stack_mismatch
new file mode 100644
index 0000000..cc3b41a
--- /dev/null
+++ b/tests/custom/03_bugs/11_switch_stack_mismatch
@@ -0,0 +1,39 @@
+When jumping into a case following prior cases declaring local variables,
+the preceding local variable declarations were skipped, leading to an
+unexpected stack layout which caused local variables to carry wrong
+values at run time and eventual segmentation faults when attempting to
+unwind the stack on leaving the lexical switch scope.
+
+-- Expect stdout --
+Matching 1:
+ - 1: [ null, null, 3, 4 ]
+ - 2: [ null, null, 3, 4, 5, 6 ]
+Matching 2:
+ - 2: [ null, null, null, null, 5, 6 ]
+Matching 3:
+ - default: [ 1, 2 ]
+ - 1: [ 1, 2, 3, 4 ]
+ - 2: [ 1, 2, 3, 4, 5, 6 ]
+-- End --
+
+-- Testcase --
+{%
+ for (let n in [1, 2, 3]) {
+ printf("Matching %d:\n", n);
+
+ switch (n) {
+ default:
+ let x = 1, y = 2;
+ print(" - default: ", [x, y], "\n");
+
+ case 1:
+ let a = 3, b = 4;
+ print(" - 1: ", [x, y, a, b], "\n");
+
+ case 2:
+ let c = 5, d = 6;
+ print(" - 2: ", [x, y, a, b, c, d], "\n");
+ }
+ }
+%}
+-- End --
diff --git a/tests/custom/03_bugs/12_altblock_stack_mismatch b/tests/custom/03_bugs/12_altblock_stack_mismatch
new file mode 100644
index 0000000..e350660
--- /dev/null
+++ b/tests/custom/03_bugs/12_altblock_stack_mismatch
@@ -0,0 +1,83 @@
+When compiling alternative syntax blocks, such as `for ...: endfor`,
+`if ...: endif` etc., the compiler didn't assign the contained statements
+to a dedicated lexical scope, which caused a stack mismatch between
+compiler and vm when such blocks declaring local variables weren't
+actually executed.
+
+-- Expect stdout --
+2
+-- End --
+
+-- Testcase --
+{%
+ if (false):
+ let a = 1;
+ endif;
+
+ /* Due to lack of own lexical scope above, the compiler assumed
+ * that `a` is still on stack but the code to initialize it was
+ * never executed, so stack offsets were shifted by one from here
+ * on throughout the rest of the program. */
+
+ let b = 2;
+
+ print(b, "\n");
+%}
+-- End --
+
+
+Test a variation of the bug using `for in..endfor` loop syntax.
+
+-- Expect stdout --
+2
+-- End --
+
+-- Testcase --
+{%
+ for (let x in []):
+ let a = 1;
+ endfor;
+
+ let b = 2;
+
+ print(b, "\n");
+%}
+-- End --
+
+
+Test a variation of the bug using `for..endfor` count loop syntax.
+
+-- Expect stdout --
+2
+-- End --
+
+-- Testcase --
+{%
+ for (let i = 0; i < 0; i++):
+ let a = 1;
+ endfor;
+
+ let b = 2;
+
+ print(b, "\n");
+%}
+-- End --
+
+
+Test a variation of the bug using `while..endwhile` loop syntax.
+
+-- Expect stdout --
+2
+-- End --
+
+-- Testcase --
+{%
+ while (false):
+ let a = 1;
+ endwhile;
+
+ let b = 2;
+
+ print(b, "\n");
+%}
+-- End --
diff --git a/tests/custom/CMakeLists.txt b/tests/custom/CMakeLists.txt
new file mode 100644
index 0000000..9672fce
--- /dev/null
+++ b/tests/custom/CMakeLists.txt
@@ -0,0 +1,7 @@
+ADD_TEST(
+ NAME custom
+ COMMAND run_tests.sh
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+SET_PROPERTY(TEST custom APPEND PROPERTY ENVIRONMENT "UCODE_BIN=$<TARGET_FILE:ucode>")
diff --git a/tests/custom/run_tests.sh b/tests/custom/run_tests.sh
new file mode 100755
index 0000000..962cd9a
--- /dev/null
+++ b/tests/custom/run_tests.sh
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+
+line='........................................'
+ucode_bin=${UCODE_BIN:-./ucode}
+
+extract_sections() {
+ local file=$1
+ local dir=$2
+ local count=0
+ local tag line outfile
+
+ while IFS= read -r line; do
+ case "$line" in
+ "-- Testcase --")
+ tag="test"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.in" "$dir" $count)
+ printf "" > "$outfile"
+ ;;
+ "-- Expect stdout --"|"-- Expect stderr --"|"-- Expect exitcode --")
+ tag="${line#-- Expect }"
+ tag="${tag% --}"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag")
+ printf "" > "$outfile"
+ ;;
+ "-- End --")
+ tag=""
+ outfile=""
+ ;;
+ *)
+ if [ -n "$tag" ]; then
+ printf "%s\\n" "$line" >> "$outfile"
+ fi
+ ;;
+ esac
+ done < "$file"
+
+ return $(ls -l "$dir/"*.in 2>/dev/null | wc -l)
+}
+
+run_testcase() {
+ local num=$1
+ local dir=$2
+ local in=$3
+ local out=$4
+ local err=$5
+ local code=$6
+ local fail=0
+
+ $ucode_bin -e '{ "REQUIRE_SEARCH_PATH": [ "./lib/*.so" ] }' -i - <"$in" >"$dir/res.out" 2>"$dir/res.err"
+
+ touch "$dir/empty"
+ printf "%d\n" $? > "$dir/res.code"
+
+ 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"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ 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"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ 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"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ return $fail
+}
+
+run_test() {
+ local file=$1
+ local name=${file##*/}
+ local res ecode eout eerr ein tests
+ local testcase_first=0 failed=0 count=0
+
+ printf "%s %s " "$name" "${line:${#name}}"
+
+ mkdir "/tmp/test.$$"
+
+ extract_sections "$file" "/tmp/test.$$"
+ tests=$?
+
+ [ -f "/tmp/test.$$/001.in" ] && testcase_first=1
+
+ for res in "/tmp/test.$$/"[0-9]*; do
+ case "$res" in
+ *.in)
+ count=$((count + 1))
+
+ if [ $testcase_first = 1 ]; then
+ # Flush previous test
+ if [ -n "$ein" ]; then
+ run_testcase $count "/tmp/test.$$" "$ein" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+ eout=""
+ eerr=""
+ ecode=""
+ fi
+
+ ein=$res
+ else
+ run_testcase $count "/tmp/test.$$" "$res" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+ eout=""
+ eerr=""
+ ecode=""
+ fi
+
+ ;;
+ *.stdout) eout=$res ;;
+ *.stderr) eerr=$res ;;
+ *.exitcode) ecode=$res ;;
+ esac
+ done
+
+ # Flush last test
+ if [ $testcase_first = 1 ] && [ -n "$eout$eerr$ecode" ]; then
+ run_testcase $count "/tmp/test.$$" "$ein" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+ fi
+
+ rm -r "/tmp/test.$$"
+
+ if [ $failed = 0 ]; then
+ printf "OK\n"
+ else
+ printf "%s %s FAILED (%d/%d)\n" "$name" "${line:${#name}}" $failed $tests
+ fi
+
+ return $failed
+}
+
+
+n_tests=0
+n_fails=0
+
+select_tests="$@"
+
+use_test() {
+ local input="$(readlink -f "$1")"
+ local test
+
+ [ -f "$input" ] || return 1
+ [ -n "$select_tests" ] || return 0
+
+ for test in "$select_tests"; do
+ test="$(readlink -f "$test")"
+
+ [ "$test" != "$input" ] || return 0
+ done
+
+ return 1
+}
+
+for catdir in [0-9][0-9]_*; do
+ [ -d "$catdir" ] || continue
+
+ printf "\n##\n## Running %s tests\n##\n\n" "${catdir##*/[0-9][0-9]_}"
+
+ for testfile in "$catdir/"[0-9][0-9]_*; do
+ use_test "$testfile" || continue
+
+ n_tests=$((n_tests + 1))
+ run_test "$testfile" || n_fails=$((n_fails + 1))
+ done
+done
+
+printf "\nRan %d tests, %d okay, %d failures\n" $n_tests $((n_tests - n_fails)) $n_fails