summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2022-08-23 15:43:25 +0200
committerJo-Philipp Wich <jo@mein.io>2022-08-24 15:33:18 +0200
commit131d99c45e586e06d2fa3adba32e92c0370ad022 (patch)
tree0a089858e2f2fbf2c19440be8dda7e945667d83b
parent8e8dae0eb0f90dea3cb4b244c79f7fa855219f92 (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>
-rw-r--r--README.md122
-rw-r--r--lib.c262
-rw-r--r--tests/custom/03_stdlib/29_require34
-rw-r--r--tests/custom/03_stdlib/35_include32
-rw-r--r--tests/custom/03_stdlib/36_render32
-rw-r--r--tests/custom/03_stdlib/61_loadstring153
-rw-r--r--tests/custom/03_stdlib/62_loadfile166
-rw-r--r--tests/custom/03_stdlib/63_call102
8 files changed, 784 insertions, 119 deletions
diff --git a/README.md b/README.md
index 53d10ae..bd7d0c4 100644
--- a/README.md
+++ b/README.md
@@ -1514,10 +1514,126 @@ Depending on the given `operation` string argument, the meaning of
The following operations are defined:
- `collect` - Perform a complete garbage collection cycle, returns `true`.
- - `start` - (Re-)start periodic garbage collection, `argument` is an optional integer in the range 1..65535 specifying the interval. Defaults to `1000` if omitted. Returns `true` if the periodic GC was previously stopped and is now started or if the interval changed. Returns `false` otherwise.
- - `stop` - Stop periodic garbage collection. Returns `true` if the periodic GC was previously started and is now stopped, `false` otherwise.
- - `count` - Count the amount of active complex object references in the VM context, returns the counted amount.
+ - `start` - (Re-)start periodic garbage collection, `argument` is an optional
+ integer in the range 1..65535 specifying the interval. Defaults
+ to `1000` if omitted. Returns `true` if the periodic GC was
+ previously stopped and is now started or if the interval changed.
+ Returns `false` otherwise.
+ - `stop` - Stop periodic garbage collection. Returns `true` if the periodic GC
+ was previously started and is now stopped, `false` otherwise.
+ - `count` - Count the amount of active complex object references in the VM
+ context, returns the counted amount.
If the `operation` argument is omitted, the default is `collect`.
Returns `null` if a non-string `operation` value is given.
+
+#### 6.73. `loadstring(code[, options])`
+
+Compiles the given code string into a ucode program and returns the resulting
+program entry function. The optinal `options` dictionary allows overriding
+parse and compile options.
+
+If a non-string `code` argument is given, it is implicitly converted to a
+string value first.
+
+If `options` is omitted or a non-object value, the compile options of the
+running ucode program are reused.
+
+The following keys in the `options` dictionary are recognized:
+
+| Key | Type | Description |
+|-----------------------|-------|----------------------------------------------------------|
+| `lstrip_blocks` | bool | Strip leading whitespace before statement template blocks|
+| `trim_blocks` | bool | Strip newline after statement template blocks |
+| `strict_declarations` | bool | Treat access to undefined variables as fatal error |
+| `raw_mode` | bool | Compile source in script mode, don't treat it as template|
+| `module_search_path` | array | Override compile time module search path |
+| `force_dynlink_list` | array | List of module names to treat as dynamic extensions |
+
+Unrecognized keys are ignored, unspecified options default to those of the
+running program.
+
+Returns the compiled program entry function.
+
+Throws an exception on compilation errors.
+
+```javascript
+let fn1 = loadstring("Hello, {{ name }}", { raw_mode: false });
+
+global.name = "Alice";
+fn1(); // prints `Hello, Alice`
+
+
+let fn2 = loadstring("return 1 + 2;", { raw_mode: true });
+fn2(); // 3
+```
+
+#### 6.74. `loadfile(path[, options])`
+
+Compiles the given file into a ucode program and returns the resulting program
+entry function.
+
+See `loadfile()` for details.
+
+Returns the compiled program entry function.
+
+Throws an exception on compilation or file i/o errors.
+
+```javascript
+loadfile("./templates/example.uc"); // function main() { ... }
+```
+
+#### 6.75. `call(fn[, ctx[, scope[, arg1[, ...]]]])`
+
+Calls the given function value with a modified environment. The given `ctx`
+argument is used as `this` context for the invoked function and the given
+`scope` value as global environment. Any further arguments are passed to the
+invoked function as-is.
+
+When `ctx` is omitted or `null`, the function will get invoked with `this`
+being `null`.
+
+When `scope` is omitted or `null`, the function will get executed with the
+current global environment of the running program. When `scope` is set to a
+dictionary, the dictionary is used as global function environment.
+
+When the `scope` dictionary has no prototype, the current global environment
+will be set as prototype, means the scope will inherit from it. When a scope
+prototype is set, it is kept. This allows passing an isolated (sandboxed)
+function scope without access to the global environment.
+
+Any further argument is forwarded as-is to the invoked function as function
+call argument.
+
+Returns `null` if the given function value `fn` is not callable.
+
+Returns the return value of the invoked function in all other cases.
+
+Forwards exceptions thrown by the invoked function.
+
+```javascript
+// Override this context
+call(function() { printf("%J\n", this) }); // null
+call(function() { printf("%J\n", this) }, null); // null
+call(function() { printf("%J\n", this) }, { x: 1 }); // { "x": 1 }
+call(function() { printf("%J\n", this) }, { x: 2 }); // { "x": 2 }
+
+// Run with default scope
+global.a = 1;
+call(function() { printf("%J\n", a) }); // 1
+
+// Override scope, inherit from current global scope (implicit)
+call(function() { printf("%J\n", a) }, null, { a: 2 }); // 2
+
+// Override scope, inherit from current global scope (explicit)
+call(function() { printf("%J\n", a) }, null,
+ proto({ a: 2 }, global)); // 2
+
+// Override scope, don't inherit (pass `printf()` but not `a`)
+call(function() { printf("%J\n", a) }, null,
+ proto({}, { printf })); // null
+
+// Forward arguments
+x = call((x, y, z) => x * y * z, null, null, 2, 3, 4); // x = 24
+```
diff --git a/lib.c b/lib.c
index fcdd17b..37563c2 100644
--- a/lib.c
+++ b/lib.c
@@ -1638,71 +1638,44 @@ uc_require_so(uc_vm_t *vm, const char *path, uc_value_t **res)
return true;
}
+static uc_value_t *
+uc_loadfile(uc_vm_t *vm, size_t nargs);
+
+static uc_value_t *
+uc_callfunc(uc_vm_t *vm, size_t nargs);
+
static bool
uc_require_ucode(uc_vm_t *vm, const char *path, uc_value_t *scope, uc_value_t **res, bool raw_mode)
{
- uc_parse_config_t config = { 0 };
- uc_exception_type_t extype;
- uc_program_t *program;
- uc_value_t *prev_scope;
+ uc_parse_config_t config = *vm->config, *prev_config = vm->config;
uc_value_t *closure;
- uc_source_t *source;
struct stat st;
- bool prev_mode;
- char *err;
if (stat(path, &st))
return false;
- source = uc_source_new_file(path);
+ config.raw_mode = raw_mode;
+ vm->config = &config;
- if (!source) {
- uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
- "Unable to open file '%s': %s", path, strerror(errno));
+ uc_vm_stack_push(vm, ucv_string_new(path));
- return true;
- }
-
- if (!vm->config)
- vm->config = &config;
-
- prev_mode = vm->config->raw_mode;
- vm->config->raw_mode = raw_mode;
-
- program = uc_compile(vm->config, source, &err);
+ closure = uc_loadfile(vm, 1);
- uc_source_put(source);
-
- if (!program) {
- uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
- "Unable to compile module '%s':\n%s", path, err);
+ ucv_put(uc_vm_stack_pop(vm));
- free(err);
+ if (closure) {
+ uc_vm_stack_push(vm, closure);
+ uc_vm_stack_push(vm, NULL);
+ uc_vm_stack_push(vm, scope);
- vm->config->raw_mode = prev_mode;
+ *res = uc_callfunc(vm, 3);
- return true;
- }
-
- closure = ucv_closure_new(vm, uc_program_entry(program), false);
-
- uc_vm_stack_push(vm, closure);
- uc_program_put(program);
-
- if (scope) {
- prev_scope = ucv_get(uc_vm_scope_get(vm));
- uc_vm_scope_set(vm, ucv_get(scope));
+ uc_vm_stack_pop(vm);
+ uc_vm_stack_pop(vm);
+ uc_vm_stack_pop(vm);
}
- extype = uc_vm_call(vm, false, 0);
-
- if (scope)
- uc_vm_scope_set(vm, prev_scope);
-
- if (extype == EXCEPTION_NONE)
- *res = uc_vm_stack_pop(vm);
-
- vm->config->raw_mode = prev_mode;
+ vm->config = prev_config;
return true;
}
@@ -3589,6 +3562,194 @@ uc_gc(uc_vm_t *vm, size_t nargs)
return NULL;
}
+static void
+uc_compile_parse_config(uc_parse_config_t *config, uc_value_t *spec)
+{
+ uc_value_t *v, *p;
+ size_t i, j;
+ bool found;
+
+ struct {
+ const char *key;
+ bool *flag;
+ uc_search_path_t *path;
+ } fields[] = {
+ { "lstrip_blocks", &config->lstrip_blocks, NULL },
+ { "trim_blocks", &config->trim_blocks, NULL },
+ { "strict_declarations", &config->strict_declarations, NULL },
+ { "raw_mode", &config->raw_mode, NULL },
+ { "module_search_path", NULL, &config->module_search_path },
+ { "force_dynlink_list", NULL, &config->force_dynlink_list }
+ };
+
+ for (i = 0; i < ARRAY_SIZE(fields); i++) {
+ v = ucv_object_get(spec, fields[i].key, &found);
+
+ if (!found)
+ continue;
+
+ if (fields[i].flag) {
+ *fields[i].flag = ucv_is_truish(v);
+ }
+ else if (fields[i].path) {
+ fields[i].path->count = 0;
+ fields[i].path->entries = NULL;
+
+ for (j = 0; j < ucv_array_length(v); j++) {
+ p = ucv_array_get(v, j);
+
+ if (ucv_type(p) != UC_STRING)
+ continue;
+
+ uc_vector_push(fields[i].path, ucv_string_get(p));
+ }
+ }
+ }
+}
+
+static uc_value_t *
+uc_load_common(uc_vm_t *vm, size_t nargs, uc_source_t *source)
+{
+ uc_parse_config_t conf = *vm->config;
+ uc_program_t *program;
+ uc_value_t *closure;
+ char *err = NULL;
+
+ uc_compile_parse_config(&conf, uc_fn_arg(1));
+
+ program = uc_compile(&conf, source, &err);
+ closure = program ? ucv_closure_new(vm, uc_program_entry(program), false) : NULL;
+
+ uc_program_put(program);
+
+ if (!vm->config || conf.module_search_path.entries != vm->config->module_search_path.entries)
+ uc_vector_clear(&conf.module_search_path);
+
+ if (!vm->config || conf.force_dynlink_list.entries != vm->config->force_dynlink_list.entries)
+ uc_vector_clear(&conf.force_dynlink_list);
+
+ if (!closure) {
+ uc_error_message_indent(&err);
+
+ if (source->buffer)
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
+ "Unable to compile source string:\n\n%s", err);
+ else
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
+ "Unable to compile source file '%s':\n\n%s", source->filename, err);
+ }
+
+ uc_source_put(source);
+ free(err);
+
+ return closure;
+}
+
+static uc_value_t *
+uc_loadstring(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *code = uc_fn_arg(0);
+ uc_source_t *source;
+ size_t len;
+ char *s;
+
+ if (ucv_type(code) == UC_STRING) {
+ len = ucv_string_length(code);
+ s = xalloc(len);
+ memcpy(s, ucv_string_get(code), len);
+ }
+ else {
+ s = ucv_to_string(vm, code);
+ len = strlen(s);
+ }
+
+ source = uc_source_new_buffer("[loadstring argument]", s, len);
+
+ if (!source) {
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
+ "Unable to allocate source buffer: %s",
+ strerror(errno));
+
+ return NULL;
+ }
+
+ return uc_load_common(vm, nargs, source);
+}
+
+static uc_value_t *
+uc_loadfile(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *path = uc_fn_arg(0);
+ uc_source_t *source;
+
+ if (ucv_type(path) != UC_STRING)
+ return NULL;
+
+ source = uc_source_new_file(ucv_string_get(path));
+
+ if (!source) {
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
+ "Unable to open source file %s: %s",
+ ucv_string_get(path), strerror(errno));
+
+ return NULL;
+ }
+
+ return uc_load_common(vm, nargs, source);
+}
+
+static uc_value_t *
+uc_callfunc(uc_vm_t *vm, size_t nargs)
+{
+ size_t argoff = vm->stack.count - nargs, i;
+ uc_value_t *fn_scope, *prev_scope, *res;
+ uc_value_t *fn = uc_fn_arg(0);
+ uc_value_t *this = uc_fn_arg(1);
+ uc_value_t *scope = uc_fn_arg(2);
+
+ if (!ucv_is_callable(fn))
+ return NULL;
+
+ if (scope && ucv_type(scope) != UC_OBJECT)
+ return NULL;
+
+ if (ucv_prototype_get(scope)) {
+ fn_scope = ucv_get(scope);
+ }
+ else if (scope) {
+ fn_scope = ucv_object_new(vm);
+
+ ucv_object_foreach(scope, k, v)
+ ucv_object_add(fn_scope, k, ucv_get(v));
+
+ ucv_prototype_set(fn_scope, ucv_get(uc_vm_scope_get(vm)));
+ }
+ else {
+ fn_scope = NULL;
+ }
+
+ uc_vm_stack_push(vm, ucv_get(this));
+ uc_vm_stack_push(vm, ucv_get(fn));
+
+ for (i = 3; i < nargs; i++)
+ uc_vm_stack_push(vm, ucv_get(vm->stack.entries[3 + argoff++]));
+
+ if (fn_scope) {
+ prev_scope = ucv_get(uc_vm_scope_get(vm));
+ uc_vm_scope_set(vm, fn_scope);
+ }
+
+ if (uc_vm_call(vm, true, i - 3) == EXCEPTION_NONE)
+ res = uc_vm_stack_pop(vm);
+ else
+ res = NULL;
+
+ if (fn_scope)
+ uc_vm_scope_set(vm, prev_scope);
+
+ return res;
+}
+
const uc_function_list_t uc_stdlib_functions[] = {
{ "chr", uc_chr },
@@ -3656,7 +3817,10 @@ const uc_function_list_t uc_stdlib_functions[] = {
{ "clock", uc_clock },
{ "hexdec", uc_hexdec },
{ "hexenc", uc_hexenc },
- { "gc", uc_gc }
+ { "gc", uc_gc },
+ { "loadstring", uc_loadstring },
+ { "loadfile", uc_loadfile },
+ { "call", uc_callfunc },
};
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 --