diff options
Diffstat (limited to 'tests/custom')
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 |