summaryrefslogtreecommitdiffhomepage
path: root/tests/custom/00_syntax
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2021-04-27 12:43:38 +0200
committerGitHub <noreply@github.com>2021-04-27 12:43:38 +0200
commit8469c4b1be228f42c46f08852f028f7801b93cc9 (patch)
treef61121e8f89e39787a960e621fc8492e57fc4bc0 /tests/custom/00_syntax
parentf360350bd874aeec0806c8df02c7a20a54c44406 (diff)
parent64eec7f90e945696572ee076b75d1f35e8f2248a (diff)
Merge pull request #5 from jow-/new-type-system
New type system
Diffstat (limited to 'tests/custom/00_syntax')
-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
22 files changed, 1464 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..6d85e97
--- /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 --
+regexp
+-- 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 --