summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md135
-rw-r--r--compiler.c36
-rw-r--r--include/ucode/lib.h1
-rw-r--r--lib.c311
-rw-r--r--source.c2
-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
-rw-r--r--types.c3
12 files changed, 849 insertions, 158 deletions
diff --git a/README.md b/README.md
index dbbd463..bd7d0c4 100644
--- a/README.md
+++ b/README.md
@@ -1502,3 +1502,138 @@ Returns the encoded hexadecimal digit string.
```javascript
hexenc("Hello world!\n"); // "48656c6c6f20776f726c64210a"
```
+
+#### 6.72. `gc([operation[, argument]])`
+
+The `gc()` function allows interaction with the mark and sweep garbage
+collector of the running ucode virtual machine.
+
+Depending on the given `operation` string argument, the meaning of
+`argument` and the function return value differs.
+
+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.
+
+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/compiler.c b/compiler.c
index 1199d82..2cc9244 100644
--- a/compiler.c
+++ b/compiler.c
@@ -172,8 +172,8 @@ uc_compiler_syntax_error(uc_compiler_t *compiler, size_t off, const char *fmt, .
uc_source_t *source = uc_compiler_current_source(compiler);
uc_stringbuf_t *buf = compiler->parser->error;
size_t line = 0, byte = 0, len = 0;
- char *s, *p, *nl;
va_list ap;
+ char *s;
if (compiler->parser->synchronizing)
return;
@@ -195,32 +195,8 @@ uc_compiler_syntax_error(uc_compiler_t *compiler, size_t off, const char *fmt, .
va_end(ap);
ucv_stringbuf_append(buf, "Syntax error: ");
-
- p = strstr(s, "\nSyntax error: ");
-
- if (!p) {
- ucv_stringbuf_addstr(buf, s, len);
- ucv_stringbuf_append(buf, "\n");
- }
- else {
- ucv_stringbuf_printf(buf, "%.*s\n\n", (int)(p - s), s);
-
- while (len > 0 && s[len-1] == '\n')
- s[--len] = 0;
-
- for (p++, nl = strchr(p, '\n'); p != NULL;
- p = nl ? nl + 1 : NULL, nl = p ? strchr(p, '\n') : NULL)
- {
- if (!nl)
- ucv_stringbuf_printf(buf, " | %s", p);
- else if (nl != p)
- ucv_stringbuf_printf(buf, " | %.*s\n", (int)(nl - p), p);
- else
- ucv_stringbuf_append(buf, " |\n");
- }
-
- ucv_stringbuf_append(buf, "\n\n");
- }
+ ucv_stringbuf_addstr(buf, s, len);
+ ucv_stringbuf_append(buf, "\n");
free(s);
@@ -3413,9 +3389,11 @@ uc_compiler_compile_module(uc_compiler_t *compiler, const char *name, uc_value_t
err = NULL;
res = uc_compiler_compile_module_source(compiler, source, imports, &err);
- if (!res)
+ if (!res) {
+ uc_error_message_indent(&err);
uc_compiler_syntax_error(compiler, compiler->parser->curr.pos,
- "Unable to compile module '%s':\n%s", source->filename, err);
+ "Unable to compile module '%s':\n\n%s", source->filename, err);
+ }
free(err);
}
diff --git a/include/ucode/lib.h b/include/ucode/lib.h
index 416fea5..0709702 100644
--- a/include/ucode/lib.h
+++ b/include/ucode/lib.h
@@ -33,6 +33,7 @@ uc_cfn_ptr_t uc_stdlib_function(const char *name);
__hidden bool uc_source_context_format(uc_stringbuf_t *buf, uc_source_t *src, size_t off, bool compact);
__hidden bool uc_error_context_format(uc_stringbuf_t *buf, uc_source_t *src, uc_value_t *stacktrace, size_t off);
+__hidden void uc_error_message_indent(char **msg);
__hidden uc_value_t *uc_require_library(uc_vm_t *vm, uc_value_t *nameval, bool so_only);
diff --git a/lib.c b/lib.c
index 1cbde6d..37563c2 100644
--- a/lib.c
+++ b/lib.c
@@ -208,6 +208,40 @@ uc_error_context_format(uc_stringbuf_t *buf, uc_source_t *src, uc_value_t *stack
return uc_source_context_format(buf, src, off, false);
}
+void
+uc_error_message_indent(char **msg) {
+ uc_stringbuf_t *buf = xprintbuf_new();
+ char *s, *p, *nl;
+ size_t len;
+
+ if (!msg || !*msg)
+ return;
+
+ s = *msg;
+ len = strlen(s);
+
+ while (len > 0 && s[len-1] == '\n')
+ s[--len] = 0;
+
+ for (p = s, nl = strchr(p, '\n'); p != NULL;
+ p = nl ? nl + 1 : NULL, nl = p ? strchr(p, '\n') : NULL)
+ {
+ if (!nl)
+ ucv_stringbuf_printf(buf, " | %s", p);
+ else if (nl != p)
+ ucv_stringbuf_printf(buf, " | %.*s\n", (int)(nl - p), p);
+ else
+ ucv_stringbuf_append(buf, " |\n");
+ }
+
+ ucv_stringbuf_append(buf, "\n");
+
+ *msg = buf->buf;
+
+ free(buf);
+ free(s);
+}
+
static char *uc_cast_string(uc_vm_t *vm, uc_value_t **v, bool *freeable) {
if (ucv_type(*v) == UC_STRING) {
*freeable = false;
@@ -1604,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);
-
- if (!source) {
- uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
- "Unable to open file '%s': %s", path, strerror(errno));
-
- return true;
- }
+ config.raw_mode = raw_mode;
+ vm->config = &config;
- if (!vm->config)
- vm->config = &config;
+ uc_vm_stack_push(vm, ucv_string_new(path));
- prev_mode = vm->config->raw_mode;
- vm->config->raw_mode = raw_mode;
+ closure = uc_loadfile(vm, 1);
- program = uc_compile(vm->config, source, &err);
+ ucv_put(uc_vm_stack_pop(vm));
- uc_source_put(source);
+ if (closure) {
+ uc_vm_stack_push(vm, closure);
+ uc_vm_stack_push(vm, NULL);
+ uc_vm_stack_push(vm, scope);
- if (!program) {
- uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
- "Unable to compile module '%s':\n%s", path, err);
-
- free(err);
-
- vm->config->raw_mode = prev_mode;
-
- return true;
- }
-
- closure = ucv_closure_new(vm, uc_program_entry(program), false);
-
- uc_vm_stack_push(vm, closure);
- uc_program_put(program);
+ *res = uc_callfunc(vm, 3);
- 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;
}
@@ -2324,19 +2331,12 @@ include_path(const char *curpath, const char *incpath)
if (*incpath == '/')
return realpath(incpath, NULL);
- if (curpath) {
- dup = strdup(curpath);
+ dup = curpath ? strrchr(curpath, '/') : NULL;
- if (!dup)
- return NULL;
-
- len = asprintf(&res, "%s/%s", dirname(dup), incpath);
-
- free(dup);
- }
- else {
+ if (dup)
+ len = asprintf(&res, "%.*s/%s", (int)(dup - curpath), curpath, incpath);
+ else
len = asprintf(&res, "./%s", incpath);
- }
if (len == -1)
return NULL;
@@ -3562,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 },
@@ -3629,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/source.c b/source.c
index 902ad4c..3bdc210 100644
--- a/source.c
+++ b/source.c
@@ -194,7 +194,7 @@ uc_source_runpath_set(uc_source_t *source, const char *runpath)
if (source->runpath != source->filename)
free(source->runpath);
- source->runpath = xstrdup(runpath);
+ source->runpath = runpath ? xstrdup(runpath) : NULL;
}
bool
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 --
diff --git a/types.c b/types.c
index e05d685..5274d23 100644
--- a/types.c
+++ b/types.c
@@ -266,7 +266,8 @@ ucv_free(uc_value_t *uv, bool retain)
ref = &closure->ref;
for (i = 0; i < function->nupvals; i++)
- ucv_put_value(&closure->upvals[i]->header, retain);
+ if (closure->upvals[i])
+ ucv_put_value(&closure->upvals[i]->header, retain);
ucv_put_value(&function->program->header, retain);
break;