diff options
author | Jo-Philipp Wich <jo@mein.io> | 2022-08-23 15:43:25 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2022-08-24 15:33:18 +0200 |
commit | 131d99c45e586e06d2fa3adba32e92c0370ad022 (patch) | |
tree | 0a089858e2f2fbf2c19440be8dda7e945667d83b /tests/custom/03_stdlib | |
parent | 8e8dae0eb0f90dea3cb4b244c79f7fa855219f92 (diff) |
lib: introduce three new functions call(), loadstring() and loadfile()
Introduce new functions dealing with on-the-fly compilation of code and
execution of functions with different global scope.
The `loadstring()` and `loadfile()` functions will compile the given
ucode source string or ucode file path respectively and return the entry
function of the resulting program.
An optional dictionary specifying parse options may be given as second
argument.
Both functions return `null` on invalid arguments and throw an exception
in case of compilation errors.
The `call()` function allows invoking a given function value with a
different `this` context and/or a different global environment.
Finally refactor the existing `uc_require_ucode()` implementation to
reuse the new `uc_loadfile()` and `uc_call()` implementations and adjust
as well as simplify affected testcases.
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'tests/custom/03_stdlib')
-rw-r--r-- | tests/custom/03_stdlib/29_require | 34 | ||||
-rw-r--r-- | tests/custom/03_stdlib/35_include | 32 | ||||
-rw-r--r-- | tests/custom/03_stdlib/36_render | 32 | ||||
-rw-r--r-- | tests/custom/03_stdlib/61_loadstring | 153 | ||||
-rw-r--r-- | tests/custom/03_stdlib/62_loadfile | 166 | ||||
-rw-r--r-- | tests/custom/03_stdlib/63_call | 102 |
6 files changed, 452 insertions, 67 deletions
diff --git a/tests/custom/03_stdlib/29_require b/tests/custom/03_stdlib/29_require index 4fb4216..a81edb4 100644 --- a/tests/custom/03_stdlib/29_require +++ b/tests/custom/03_stdlib/29_require @@ -119,20 +119,9 @@ A compilation error in the module triggers an exception. -- Testcase -- {% - try { - push(REQUIRE_SEARCH_PATH, TESTFILES_PATH + '/*.uc'); + push(REQUIRE_SEARCH_PATH, TESTFILES_PATH + '/*.uc'); - require("require.test.broken"); - } - catch (e) { - // Catch and rethrow exception with modified message to - // ensure stable test output. - e.message = replace(e.message, - /(compile module '.+require\/test\/broken\.uc')/, - "compile module '.../require/test/broken.uc'"); - - die(e); - } + require("require.test.broken"); %} -- End -- @@ -142,19 +131,18 @@ return { -- End -- -- Expect stderr -- -Unable to compile module '.../require/test/broken.uc': -Syntax error: Expecting label -In line 2, byte 10: - - `return {` - ^-- Near here - +Runtime error: Unable to compile source file './files/require/test/broken.uc': + | Syntax error: Expecting label + | In line 2, byte 10: + | + | `return {` + | ^-- Near here -In line 14, byte 8: +In line 4, byte 31: - ` die(e);` - Near here ---^ + ` require("require.test.broken");` + Near here -----------------------^ -- End -- diff --git a/tests/custom/03_stdlib/35_include b/tests/custom/03_stdlib/35_include index 1d428f1..83c34bb 100644 --- a/tests/custom/03_stdlib/35_include +++ b/tests/custom/03_stdlib/35_include @@ -132,18 +132,7 @@ A compilation error in the file triggers an exception. -- Testcase -- {% - try { - include("files/broken.uc"); - } - catch (e) { - // Catch and rethrow exception with modified message to - // ensure stable test output. - e.message = replace(e.message, - /(compile module '.+broken\.uc')/, - "compile module '.../broken.uc'"); - - die(e); - } + include("files/broken.uc"); %} -- End -- @@ -155,19 +144,18 @@ A compilation error in the file triggers an exception. -- End -- -- Expect stderr -- -Unable to compile module '.../broken.uc': -Syntax error: Expecting label -In line 3, byte 11: +Runtime error: Unable to compile source file './files/broken.uc': - ` return {` - Near here --^ + | Syntax error: Expecting label + | In line 3, byte 11: + | + | ` return {` + | Near here --^ +In line 2, byte 27: - -In line 12, byte 8: - - ` die(e);` - Near here ---^ + ` include("files/broken.uc");` + Near here -------------------^ -- End -- diff --git a/tests/custom/03_stdlib/36_render b/tests/custom/03_stdlib/36_render index 64ef08a..55a1105 100644 --- a/tests/custom/03_stdlib/36_render +++ b/tests/custom/03_stdlib/36_render @@ -126,18 +126,7 @@ A compilation error in the file triggers an exception. -- Testcase -- {% - try { - include("files/broken.uc"); - } - catch (e) { - // Catch and rethrow exception with modified message to - // ensure stable test output. - e.message = replace(e.message, - /(compile module '.+broken\.uc')/, - "compile module '.../broken.uc'"); - - die(e); - } + include("files/broken.uc"); %} -- End -- @@ -149,19 +138,18 @@ A compilation error in the file triggers an exception. -- End -- -- Expect stderr -- -Unable to compile module '.../broken.uc': -Syntax error: Expecting label -In line 3, byte 11: +Runtime error: Unable to compile source file './files/broken.uc': - ` return {` - Near here --^ + | Syntax error: Expecting label + | In line 3, byte 11: + | + | ` return {` + | Near here --^ +In line 2, byte 27: - -In line 12, byte 8: - - ` die(e);` - Near here ---^ + ` include("files/broken.uc");` + Near here -------------------^ -- End -- diff --git a/tests/custom/03_stdlib/61_loadstring b/tests/custom/03_stdlib/61_loadstring new file mode 100644 index 0000000..2bb1f50 --- /dev/null +++ b/tests/custom/03_stdlib/61_loadstring @@ -0,0 +1,153 @@ +The `loadstring()` function compiles the given string argument into a +ucode program and returns the resulting entry function. + +Throws an exception on compilation failure. + +Returns the compiled program entry function. + + +Compile a simple program with default options + +-- Testcase -- +{% + let fn = loadstring('return 1 + 1;\n'); + fn(); +%} +-- End -- + +-- Expect stdout -- +return 1 + 1; +-- End -- + + +Compile a program in raw mode + +-- Testcase -- +{% + let fn = loadstring('printf("%d\\n", 1 + 1);\n', { raw_mode: true }); + fn(); +%} +-- End -- + +-- Expect stdout -- +2 +-- End -- + + +Compile a program in template mode + +-- Testcase -- +{% + let fn = loadstring('{{ 1 + 1 }}\n', { raw_mode: false }); + fn(); +%} +-- End -- + +-- Expect stdout -- +2 +-- End -- + + +Override module search path during compilation (import should fail due to empty path) + +-- Testcase -- +{% + loadstring('import { readfile } from "fs";\n', { + raw_mode: true, + module_search_path: [] + }); +%} +-- End -- + +-- Expect stderr -- +Runtime error: Unable to compile source string: + + | Syntax error: Unable to resolve path for module 'fs' + | In line 1, byte 30: + | + | `import { readfile } from "fs";` + | Near here -------------------^ + +In line 5, byte 3: + + ` });` + ^-- Near here + + +-- End -- + + +Force dynamic loading of unknown extensions at compile time (should succeed) + +-- Testcase -- +{% + loadstring('import foo from "doesnotexist";\n', { + raw_mode: true, + force_dynlink_list: [ "doesnotexist" ] + }); + + print("OK\n"); +%} +-- End -- + +-- Expect stdout -- +OK +-- End -- + + +Compiling a syntax error (should fail with syntax error exception) + +-- Testcase -- +{% + loadstring('1 +', { raw_mode: true }); +%} +-- End -- + +-- Expect stderr -- +Runtime error: Unable to compile source string: + + | Syntax error: Expecting expression + | In line 1, byte 4: + | + | `1 +` + | ^-- Near here + +In line 2, byte 38: + + ` loadstring('1 +', { raw_mode: true });` + Near here ------------------------------^ + + +-- End -- + + +Test loading precompiled bytecode + +-- Testcase -- +{% + // utpl -c -o - -e $'Hello world\n' | hexdump -v -e '"" 16/1 "%02x " "\n"' + const program = hexdec(` + 23 21 2f 75 73 72 2f 62 69 6e 2f 65 6e 76 20 75 + 63 6f 64 65 0a 1b 75 63 62 00 00 00 03 00 00 00 + 01 00 00 00 0e 5b 2d 65 20 61 72 67 75 6d 65 6e + 74 5d 00 00 00 00 00 00 0d 48 65 6c 6c 6f 20 77 + 6f 72 6c 64 0a 00 00 00 00 00 00 00 03 8b 80 80 + 00 00 00 00 01 00 00 00 00 00 00 00 05 00 00 00 + 10 00 00 00 0c 48 65 6c 6c 6f 20 77 6f 72 6c 64 + 0a 00 00 00 01 00 00 00 70 00 00 00 05 6d 61 69 + 6e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 08 01 00 00 00 00 41 07 3c 00 00 00 + 01 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 + 00 00 00 00 01 00 00 00 00 00 00 00 05 00 00 00 + 10 00 00 00 08 28 63 61 6c 6c 65 65 29 00 00 00 + 00 00 00 00 01 40 00 00 00 + `); + + let fn = loadstring(program); + fn(); +%} +-- End -- + +-- Expect stdout -- +Hello world +-- End -- diff --git a/tests/custom/03_stdlib/62_loadfile b/tests/custom/03_stdlib/62_loadfile new file mode 100644 index 0000000..4926696 --- /dev/null +++ b/tests/custom/03_stdlib/62_loadfile @@ -0,0 +1,166 @@ +The `loadfile()` function operates similar to `loadstring()` but reads the +input to compile from the specified file path instead. + +It compiles the given file name into a ucode program and returns the resulting +entry function. + +Throws an exception on compilation or file i/o failure. + +Returns the compiled program entry function. + + +Compile a simple program with default options + +-- Testcase -- +{% + let fn = loadfile('./files/test1.uc'); + fn(); +%} +-- End -- + +-- File test1.uc -- +return 1 + 1; +-- End -- + +-- Expect stdout -- +return 1 + 1; +-- End -- + + +Compile a program in raw mode + +-- Testcase -- +{% + let fn = loadfile('./files/test2.uc', { raw_mode: true }); + fn(); +%} +-- End -- + +-- File test2.uc -- +printf("%d\n", 1 + 1); +-- End -- + +-- Expect stdout -- +2 +-- End -- + + +Compile a program in template mode + +-- Testcase -- +{% + let fn = loadfile('./files/test3.uc', { raw_mode: false }); + fn(); +%} +-- End -- + +-- File test3.uc -- +{{ 1 + 1 }} +-- End -- + +-- Expect stdout -- +2 +-- End -- + + +Override module search path during compilation (import should fail due to empty path) + +-- Testcase -- +{% + loadfile('./files/test4.uc', { + raw_mode: true, + module_search_path: [] + }); +%} +-- End -- + +-- File test4.uc -- +import { readfile } from "fs"; +-- End -- + +-- Expect stderr -- +Runtime error: Unable to compile source file './files/test4.uc': + + | Syntax error: Unable to resolve path for module 'fs' + | In line 1, byte 30: + | + | `import { readfile } from "fs";` + | Near here -------------------^ + +In line 5, byte 3: + + ` });` + ^-- Near here + + +-- End -- + + +Force dynamic loading of unknown extensions at compile time (should succeed) + +-- Testcase -- +{% + loadfile('./files/test5.uc', { + raw_mode: true, + force_dynlink_list: [ "doesnotexist" ] + }); + + print("OK\n"); +%} +-- End -- + +-- File test5.uc -- +import foo from "doesnotexist"; +-- End -- + +-- Expect stdout -- +OK +-- End -- + + +Compiling a syntax error (should fail with syntax error exception) + +-- Testcase -- +{% + loadfile('./files/test6.uc', { raw_mode: true }); +%} +-- End -- + +-- File test6.uc -- +1 + +-- End -- + +-- Expect stderr -- +Runtime error: Unable to compile source file './files/test6.uc': + + | Syntax error: Expecting expression + | In line 1, byte 5: + | + | `1 +` + | ^-- Near here + +In line 2, byte 49: + + ` loadfile('./files/test6.uc', { raw_mode: true });` + Near here -----------------------------------------^ + + +-- End -- + + +Test loading precompiled bytecode + +-- Testcase -- +{% + import { readlink } from 'fs'; + + system(`${readlink('/proc/self/exe')} -T, -c -o ./files/test7.uc -e 'Hello world\n'`); + + let fn = loadfile('./files/test7.uc'); + fn(); +%} +-- End -- + +-- Expect stdout -- +Hello world +-- End -- diff --git a/tests/custom/03_stdlib/63_call b/tests/custom/03_stdlib/63_call new file mode 100644 index 0000000..41064eb --- /dev/null +++ b/tests/custom/03_stdlib/63_call @@ -0,0 +1,102 @@ +The `call()` function allows invoking functions with a modified `this` context +and global environment. It's main use case is binding global variables for +dynamiclly loaded code at runtime. + +Returns `null` if the given function value is not callable. +Returns the value returned by the invoked function in all other cases. + + +Test modifying `this` context + +-- Testcase -- +{% + let o1 = { + name: "Object #1", + func: function() { + print(`This is ${this.name}\n`); + } + }; + + let o2 = { + name: "Object #2" + }; + + o1.func(); + call(o1.func, o2); +%} +-- End -- + +-- Expect stdout -- +This is Object #1 +This is Object #2 +-- End -- + + +Test modifying environment + +-- Testcase -- +{% + function fn() { + print("Hello world\n"); + } + + fn(); + call(fn, null, { print: (s) => printf("Overridden print(): %s", s) }); +%} +-- End -- + +-- Expect stdout -- +Hello world +Overridden print(): Hello world +-- End -- + + +Test isolating environment + +-- Testcase -- +{% + function fn() { + print("Hello world\n"); + } + + fn(); + call(fn, null, proto({}, {})); // should fail due to unavailable print +%} +-- End -- + +-- Expect stdout -- +Hello world +-- End -- + +-- Expect stderr -- +Type error: left-hand side is not a function +In fn(), line 3, byte 24: + called from function call ([C]) + called from anonymous function ([stdin]:7:30) + + ` print("Hello world\n");` + Near here -------------------^ + + +-- End -- + + +Test passing through arguments + +-- Testcase -- +{% + function fn(a, b) { + printf("The product of %d * %d is %d\n", a, b, a * b); + } + + fn(3, 4); + call(fn, null, null, 5, 6); + call((...args) => printf("Args: %J\n", args), null, null, 1, 2, 3, 4, 5, 6); +%} +-- End -- + +-- Expect stdout -- +The product of 3 * 4 is 12 +The product of 5 * 6 is 30 +Args: [ 1, 2, 3, 4, 5, 6 ] +-- End -- |