diff options
-rw-r--r-- | README.md | 135 | ||||
-rw-r--r-- | compiler.c | 36 | ||||
-rw-r--r-- | include/ucode/lib.h | 1 | ||||
-rw-r--r-- | lib.c | 311 | ||||
-rw-r--r-- | source.c | 2 | ||||
-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 | ||||
-rw-r--r-- | types.c | 3 |
12 files changed, 849 insertions, 158 deletions
@@ -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 +``` @@ -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); @@ -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 }, }; @@ -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 -- @@ -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; |