diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4675f85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.o +build/ +generated/ +mquickjs_build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..97d79bd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "packages/WasmEdge"] + path = packages/WasmEdge + url = https://github.com/WasmEdge/WasmEdge.git +[submodule "packages/wasi-sdk"] + path = packages/wasi-sdk + url = https://github.com/WebAssembly/wasi-sdk.git +[submodule "packages/wasm-micro-runtime"] + path = packages/wasm-micro-runtime + url = https://github.com/bytecodealliance/wasm-micro-runtime.git diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..9330f18 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,787 @@ +# Design Document: microquickjs-wasi-component + +## Overview + +This document describes the design for packaging MicroQuickJS — Fabrice Bellard's minimal, arena-based JavaScript engine — as a WASI 0.2 WebAssembly Component. The deliverable is `build/microquickjs.component.wasm`: a self-contained component that exports a JavaScript evaluation and value-manipulation interface defined in WIT. + +MicroQuickJS differs from standard QuickJS in three critical ways that shape every design decision: + +1. **No separate JSRuntime** — `JS_NewContext(mem, size, stdlib)` takes a raw memory arena directly. +2. **Arena-managed memory** — no `JS_FreeValue`, no `JS_FreeCString`. The arena owns all JS heap objects. +3. **Stack-allocated string buffers** — `JS_ToCStringLen(ctx, &len, val, &buf)` takes a 5-byte `JSCStringBuf` on the stack; the returned pointer points into the arena and must be copied before the next GC. + +The static memory footprint this implies is a feature, not a limitation. A fixed 4 MiB arena declared as a C static array means the component's linear memory layout is fully predictable at link time — no dynamic allocator, no fragmentation, no heap growth surprises. This is ideal for security-sensitive embedding and resource-constrained WAMR deployments. + +The primary validated runtime is WAMR built with `-DWAMR_BUILD_COMPONENT_MODEL=1`. Wasmtime is used for development-time validation. WasmEdge is excluded as a primary target due to Component Model gaps in all currently released versions (see Runtime Compatibility). + +## Architecture + +``` + Host (Wasmtime / WAMR / any WASI 0.2 runtime) + ┌─────────────────────────────────────────────────────────────┐ + │ Host bindings (generated by wit-bindgen for host language) │ + └────────────────────────┬────────────────────────────────────┘ + │ Component Model ABI (WIT) + ┌────────────────────────▼────────────────────────────────────┐ + │ microquickjs.component.wasm │ + │ ┌──────────────────────────────────────────────────────┐ │ + │ │ WASI adapter layer (wasi_snapshot_preview1.reactor) │ │ + │ └──────────────────────────┬───────────────────────────┘ │ + │ │ Core Wasm imports/exports │ + │ ┌──────────────────────────▼───────────────────────────┐ │ + │ │ core.wasm (wasm32-wasi reactor) │ │ + │ │ ┌─────────────────┐ ┌──────────────────────────┐ │ │ + │ │ │ glue.c │ │ generated/microquickjs.c │ │ │ + │ │ │ (WIT ↔ C API) │ │ (wit-bindgen ABI glue) │ │ │ + │ │ └────────┬────────┘ └──────────────────────────┘ │ │ + │ │ │ │ │ + │ │ ┌────────▼────────────────────────────────────┐ │ │ + │ │ │ MicroQuickJS engine │ │ │ + │ │ │ mquickjs.c cutils.c dtoa.c libm.c │ │ │ + │ │ └────────────────────────────────────────────┘ │ │ + │ │ │ │ + │ │ Static arena: s_mem[4 MiB] (linear memory) │ │ + │ └──────────────────────────────────────────────────────┘ │ + └─────────────────────────────────────────────────────────────┘ +``` + +### Key architectural decisions + +**Singleton context.** `JS_NewContext` is called exactly once, lazily on the first exported function call, and the resulting `JSContext *` is stored in a module-level static. This matches the arena model: the arena is a static array, so there is only ever one context per component instance. Multiple eval calls share global state, which is the expected behavior for a scripting layer. + +**Predictable static memory footprint.** `s_mem[4 * 1024 * 1024]` is declared as a C static array. This means the 4 MiB arena is part of the Wasm module's initial linear memory segment — no `malloc`, no `sbrk`, no growth. The total linear memory at startup is the arena plus a small stack. This is a deliberate security and performance property: the component cannot grow its memory footprint at runtime. + +**No JS_FreeValue / JS_FreeCString.** The arena owns all JS heap objects. The glue layer never calls these functions. GC roots (`JSGCRef`) are used only to prevent the arena's mark-and-sweep from collecting values that are live across the component boundary. + +**cabi_realloc for all host-bound strings.** The Component Model ABI requires that strings returned to the host be allocated in a specific way. `cabi_realloc` (provided by the wit-bindgen-generated `microquickjs.c`) is the only allocator used for strings crossing the component boundary. Arena-internal string pointers from `JS_ToCStringLen` are always copied into a `cabi_realloc` buffer before returning. + +## WIT Interface + +The full content of `microquickjs.wit`: + +```wit +package local:microquickjs; + +interface engine { + resource js-value { + is-int: func() -> bool; + is-bool: func() -> bool; + is-null: func() -> bool; + is-undefined: func() -> bool; + is-exception: func() -> bool; + is-number: func() -> bool; + is-string: func() -> bool; + is-error: func() -> bool; + is-function: func() -> bool; + + to-string: func() -> string; + to-int32: func() -> s32; + to-float64: func() -> f64; + + get-property: func(name: string) -> js-value; + set-property: func(name: string, val: borrow); + + call: func(args: list>) -> js-value; + } + + new-int32: func(val: s32) -> js-value; + new-float64: func(val: f64) -> js-value; + new-bool: func(val: bool) -> js-value; + new-string: func(val: string) -> js-value; + new-object: func() -> js-value; + new-array: func() -> js-value; + + get-global-object: func() -> js-value; + + /// Evaluate JavaScript code and return the result as a string. + /// Returns ok(result) on success, err(message) on syntax or runtime error. + eval: func(code: string) -> result; +} + +world microquickjs { + export engine; +} +``` + +### Interface design notes + +- `js-value` is a WIT `resource` — the Component Model manages its lifetime. The host holds an owned handle; the component's destructor (`js_value_destructor`) is called when the host drops it. +- `borrow` in `set-property` and `call` args means the callee does not take ownership. The host retains the handle after the call. +- `new-array` takes no `len` argument (unlike `JS_NewArray(ctx, initial_len)`) because the WIT interface does not expose pre-sizing. The glue passes `0` as `initial_len`. +- `eval` returns `result` rather than exposing a `js-value` for the error, keeping the common case simple for hosts that only need string results. + +## Components and Interfaces + +### Source file inventory + +| File | Role | Included in wasm build | +|------|------|------------------------| +| `mquickjs/mquickjs.c` | Engine core | yes | +| `mquickjs/cutils.c` | String/memory utilities | yes | +| `mquickjs/dtoa.c` | Float-to-string conversion | yes | +| `mquickjs/libm.c` | Math functions | yes | +| `mquickjs/mqjs_stdlib.c` | Stdlib table generator (native only) | no — native host tool only | +| `mquickjs/mquickjs_build.c` | Build tool support | no — native host tool only | +| `mquickjs/mqjs.c` | REPL entry point | **excluded** — uses gettimeofday, clock_gettime | +| `glue.c` | WIT ↔ MicroQuickJS bridge | yes | +| `generated/microquickjs.c` | wit-bindgen ABI glue | yes | +| `generated/microquickjs.h` | wit-bindgen types | yes (via glue.c) | +| `build/mqjs_stdlib.h` | Generated stdlib descriptor | yes (included by glue.c) | + +### Generated files + +**`generated/microquickjs.c` / `generated/microquickjs.h`** — produced by: +``` +wit-bindgen c ./microquickjs.wit --out-dir ./generated --world microquickjs +``` +These files define the Component Model ABI entry points, the `microquickjs_string_t` type, `cabi_realloc`, and the resource handle types. The glue layer implements the functions declared in `microquickjs.h`. + +**`build/mqjs_stdlib.h`** — produced by compiling and running a native host tool: +```bash +gcc -O2 -I mquickjs/ -o build/mquickjs_build_native mquickjs/mqjs_stdlib.c mquickjs/mquickjs_build.c mquickjs/cutils.c -lm +build/mquickjs_build_native -m32 > build/mqjs_stdlib.h +``` +This header defines `js_stdlib` as a `const JSSTDLibraryDef` containing the pre-compiled atom table, function table, and global object layout for the 32-bit wasm32 target. The `-m32` flag is required to generate 32-bit offsets matching the wasm32 address space. + +**`generated/microquickjs_component_type.o`** — a special object file produced by wit-bindgen that embeds the WIT metadata as a custom section in the core module. It must be linked into `core.wasm`. + +## Data Models + +### JSValue (engine-internal) + +On wasm32, `JSValue` is a `uint32_t` — a tagged integer encoding the value type and payload in a single word. The tag occupies the low bits: + +``` +Bit layout (wasm32): + bit 0 = 0 → integer (JS_TAG_INT): value = bits[31:1] as signed int + bit 0 = 1, bit 1 = 0 → pointer (JS_TAG_PTR): arena pointer + bits[1:0] = 11 → special (JS_TAG_SPECIAL): sub-tag in bits[4:2] + sub-tag 000 → JS_TAG_BOOL + sub-tag 001 → JS_TAG_NULL + sub-tag 010 → JS_TAG_UNDEFINED + sub-tag 011 → JS_TAG_EXCEPTION +``` + +`JS_EXCEPTION`, `JS_NULL`, `JS_UNDEFINED` are compile-time constants — no heap allocation, no arena lookup. + +### JS_Value_Resource (component boundary) + +```c +typedef struct { + JSValue val; // the tagged JSValue from the arena + JSGCRef root; // linked into the context's GC root list +} JS_Value_Resource; +``` + +`JSGCRef` is defined in `mquickjs.h`: +```c +typedef struct JSGCRef { + JSValue val; + struct JSGCRef *prev; +} JSGCRef; +``` + +`JS_AddGCRef(ctx, &root)` inserts `root` into the context's GC root linked list. The arena's mark phase follows this list to keep the value alive. `JS_DeleteGCRef(ctx, &root)` removes it. + +**Lifecycle:** +1. A new `JS_Value_Resource` is `malloc`'d on the wasm heap (outside the arena). +2. `rep->val` is set to the `JSValue`. +3. `rep->root.val = rep->val` and `JS_AddGCRef(s_ctx, &rep->root)` roots it. +4. `exports_local_microquickjs_engine_js_value_new(rep)` wraps it in a WIT resource handle. +5. When the host drops the handle, the Component Model calls `js_value_destructor(rep)`. +6. The destructor calls `JS_DeleteGCRef(s_ctx, &rep->root)` then `free(rep)`. + +Note: `rep` itself is allocated with the standard wasm `malloc` (from wasi-libc), not from the JS arena. The arena only holds JS heap objects. The `JS_Value_Resource` struct is a thin wrapper that lives in the wasm heap's general allocator region. + +### String passing conventions + +| Direction | Mechanism | +|-----------|-----------| +| Host → component (input) | `microquickjs_string_t { uint8_t *ptr; size_t len }` — pointer into component linear memory, owned by caller, valid for the duration of the call | +| Component → host (output) | Buffer allocated via `cabi_realloc(NULL, 0, 1, len)`, filled with `memcpy`, ownership transferred to host | +| Arena C string (internal) | `JS_ToCStringLen(ctx, &len, val, &buf)` returns pointer into arena — valid until next GC. Must be copied immediately. | +| Null-terminated C string (internal) | `malloc(len + 1)` + `memcpy` + null terminator, used for `JS_GetPropertyStr` / `JS_SetPropertyStr`, freed immediately after the call | + +## Build Pipeline + +### Path A: wasip1 + WASI adapter (primary) + +``` +microquickjs.wit + │ + ▼ wit-bindgen c +generated/microquickjs.c +generated/microquickjs.h +generated/microquickjs_component_type.o + │ + │ mquickjs/*.c glue.c + ▼ +[clang --target=wasm32-wasi -mexec-model=reactor -Oz] + │ + ▼ +build/core.wasm + │ + ▼ wasm-tools component embed ./microquickjs.wit --world microquickjs +build/embedded.wasm + │ + ▼ wasm-tools component new --adapt wasi_snapshot_preview1= +build/microquickjs.component.wasm + │ + ▼ (optional) wasm-opt -Oz +build/microquickjs.component.wasm (size-optimized) +``` + +**Compiler flags:** +```makefile +CFLAGS = -Oz --target=wasm32-wasi -mexec-model=reactor -D_WASI_EMULATED_SIGNAL -I. -Igenerated -Imquickjs -Ibuild -mllvm -wasm-enable-sjlj +``` + +**Linker flags:** +```makefile +LDFLAGS = -Wl,--no-entry -Wl,--export=cabi_realloc -Wl,--export=__wasm_call_ctors -lwasi-emulated-signal -lwasi-emulated-process-clocks -lsetjmp -lm +``` + +The `-mllvm -wasm-enable-sjlj` flag instructs LLVM to lower `setjmp`/`longjmp` using WebAssembly Exception Handling opcodes (`try_table`, opcode `0x117`). This is the correct and expected output — do not suppress it with `-fno-exceptions` or `-mno-exception-handling`. MicroQuickJS uses `setjmp`/`longjmp` internally for JS exception recovery; suppressing EH lowering breaks those paths. + +### Path B: wasip2 native (future) + +Requires WASI SDK 25+ with a wasip2 sysroot: + +``` +[clang --target=wasm32-wasip2 -mexec-model=reactor -Oz] + │ + ▼ +build/core.wasm + │ + ▼ wasm-tools component new (no --adapt needed) +build/microquickjs.component.wasm +``` + +No `-lsetjmp`, no `-lwasi-emulated-signal`, no `-D_WASI_EMULATED_SIGNAL`. The wasip2 sysroot provides these natively. + +### Makefile targets + +| Target | Description | +|--------|-------------| +| `all` | Build `build/microquickjs.component.wasm` | +| `headers` | Generate `build/mqjs_stdlib.h` and `build/mquickjs_atom.h` via native tool | +| `inspect` | Run `wasm-tools component wit` on the final component | +| `test` | Run basic eval smoke tests via wasmtime | +| `clean` | Remove `build/` and `generated/` | + +### WASI SDK path resolution + +```makefile +WASI_SDK_PATH ?= /opt/wasi-sdk +CC = $(WASI_SDK_PATH)/bin/clang +``` + +Override with `WASI_SDK_PATH=packages/wasi-sdk make -f Makefile.wasi` when using the git submodule. + +## glue.c: Full Implementation Design + +The complete `glue.c` implements all WIT-exported functions. The actual file is at the workspace root; this section documents every design decision. + +### Preamble and includes + +```c +#include +#include +#include + +// WASI shims must come before mquickjs.h so the function signatures +// are visible when mqjs_stdlib.h references them. +#include "mquickjs/mquickjs.h" + +// cabi_realloc is provided by wit-bindgen's generated microquickjs.c. +// Declared here so glue.c can call it without a circular include. +void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size); + +// WASI shim stubs (see section below) +JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// wit-bindgen generated header — must come after shims +#include "generated/microquickjs.h" +// stdlib descriptor generated by native build tool +#include "build/mqjs_stdlib.h" +``` + +### Singleton context + +```c +static uint8_t s_mem[4 * 1024 * 1024]; // 4 MiB arena — static, fixed footprint +static JSContext *s_ctx = NULL; + +static void ensure_context(void) { + if (s_ctx) return; + s_ctx = JS_NewContext(s_mem, sizeof(s_mem), &js_stdlib); +} +``` + +`ensure_context()` is called at the top of every exported function. It is idempotent and cheap (single pointer check after the first call). + +### JS_Value_Resource struct and make_own_value helper + +```c +struct exports_local_microquickjs_engine_js_value_t { + JSValue val; + JSGCRef root; +}; + +static exports_local_microquickjs_engine_own_js_value_t +make_own_value(JSValue val) { + exports_local_microquickjs_engine_js_value_t *rep = malloc(sizeof(*rep)); + rep->val = val; + JS_AddGCRef(s_ctx, &rep->root); + rep->root.val = val; + return exports_local_microquickjs_engine_js_value_new(rep); +} +``` + +The struct name must match what wit-bindgen generates for the `js-value` resource. The `root` field is initialized by `JS_AddGCRef` which inserts it into the context's GC root list, then `root.val` is set to the value being rooted. + +### Resource destructor + +```c +void exports_local_microquickjs_engine_js_value_destructor( + exports_local_microquickjs_engine_js_value_t *rep) +{ + JS_DeleteGCRef(s_ctx, &rep->root); + free(rep); +} +``` + +Called by the Component Model ABI when the host drops the owned handle. + +### eval implementation + +```c +bool exports_local_microquickjs_engine_eval( + microquickjs_string_t *code, + microquickjs_string_t *ret, + microquickjs_string_t *err) +{ + ensure_context(); + JSValue val = JS_Eval(s_ctx, + (const char *)code->ptr, + code->len, + "", + JS_EVAL_RETVAL); + size_t len; + JSCStringBuf buf; // 5-byte stack buffer — no heap involved + + if (JS_IsException(val)) { + JSValue exc = JS_GetException(s_ctx); + const char *cstr = JS_ToCStringLen(s_ctx, &len, exc, &buf); + if (!cstr) { + static const char unknown[] = "Error: unknown exception"; + err->ptr = cabi_realloc(NULL, 0, 1, sizeof(unknown) - 1); + memcpy(err->ptr, unknown, sizeof(unknown) - 1); + err->len = sizeof(unknown) - 1; + } else { + err->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(err->ptr, cstr, len); + err->len = len; + } + return false; // err variant + } + + const char *cstr = JS_ToCStringLen(s_ctx, &len, val, &buf); + if (!cstr) { + // JS_ToCStringLen returns NULL for values that cannot be stringified + // (e.g. certain internal types). Fall back to "undefined". + static const char undef[] = "undefined"; + ret->ptr = cabi_realloc(NULL, 0, 1, sizeof(undef) - 1); + memcpy(ret->ptr, undef, sizeof(undef) - 1); + ret->len = sizeof(undef) - 1; + } else { + ret->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(ret->ptr, cstr, len); + ret->len = len; + } + return true; // ok variant +} +``` + +Key points: +- `code->ptr` is a WIT string — it is NOT null-terminated. `JS_Eval` takes an explicit `input_len`, so no null terminator is needed. +- `JSCStringBuf buf` is declared on the stack. It is 5 bytes. No heap allocation. +- The pointer returned by `JS_ToCStringLen` points into the arena. It must be copied into a `cabi_realloc` buffer before returning — the arena may be modified by the next GC cycle. +- The function returns `bool`: `true` = ok variant, `false` = err variant. The wit-bindgen ABI uses this convention for `result`. + +### Property access + +```c +exports_local_microquickjs_engine_own_js_value_t +exports_local_microquickjs_engine_method_js_value_get_property( + exports_local_microquickjs_engine_borrow_js_value_t self, + microquickjs_string_t *name) +{ + ensure_context(); + // JS_GetPropertyStr requires a null-terminated C string. + // WIT strings are not null-terminated, so we must copy. + char *cname = malloc(name->len + 1); + memcpy(cname, name->ptr, name->len); + cname[name->len] = ''; + JSValue res = JS_GetPropertyStr(s_ctx, self->val, cname); + free(cname); + return make_own_value(res); +} +``` + +The `malloc`/`free` here is for the null-terminated property name only — a short-lived allocation that does not interact with the JS arena. + +### Function call + +```c +exports_local_microquickjs_engine_own_js_value_t +exports_local_microquickjs_engine_method_js_value_call( + exports_local_microquickjs_engine_borrow_js_value_t self, + exports_local_microquickjs_engine_list_borrow_js_value_t *args) +{ + ensure_context(); + // Push arguments in order. JS_Call pops them. + for (size_t i = 0; i < args->len; i++) { + JS_PushArg(s_ctx, args->ptr[i]->val); + } + // call_flags = args->len (argc). No FRAME_CF_CTOR. + JSValue res = JS_Call(s_ctx, (int)args->len); + return make_own_value(res); +} +``` + +`JS_PushArg` + `JS_Call` is the MicroQuickJS calling convention. Arguments are pushed onto an internal stack; `JS_Call(ctx, argc)` pops them and invokes the function. The `self->val` is the function value — it must be a callable; if not, `JS_Call` returns `JS_EXCEPTION`. + +## WASI Shim Stubs + +The `mqjs_stdlib.h` stdlib descriptor references several C functions that are defined in `mquickjs/example.c` for the native build but are unavailable or undesirable in WASI. These must be provided as no-op stubs in `glue.c`: + +```c +// Date.now() — no clock access in WASI without explicit capability grant +JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// print() — no stdout in a pure component; host should use eval result +JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// performance.now() — no monotonic clock without capability +JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// gc() — manual GC trigger; safe to no-op (arena GC runs automatically) +JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// load() — file loading; no filesystem in a pure component +JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } + +// setTimeout / clearTimeout — no event loop in a synchronous component +JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) + { return JS_UNDEFINED; } +``` + +These stubs must be defined **before** `#include "build/mqjs_stdlib.h"` because the stdlib header references them by name in the function table. + +No patches to `mquickjs.c` are required. The engine's `setjmp`/`longjmp` usage compiles cleanly with WASI SDK — the `-mllvm -wasm-enable-sjlj` flag handles the lowering. + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +Property-based testing is applicable here because the core logic — JS evaluation, value construction, type checking, property access, and function calling — are all pure functions over a well-defined input space. The arena-based memory model means there is no external state to mock; the component is self-contained. + +The recommended PBT library is **[fast-check](https://github.com/dubzzz/fast-check)** (TypeScript/JavaScript) for host-side testing via the Wasmtime JS bindings, or **[hypothesis](https://hypothesis.readthedocs.io/)** (Python) via the Wasmtime Python bindings. Each property test should run a minimum of 100 iterations. + +--- + +### Property 1: Eval of valid JS returns ok with correct string representation + +*For any* valid JavaScript expression that produces a deterministic result (integer arithmetic, string literals, boolean expressions), calling `eval(expr)` SHALL return `ok(s)` where `s` is the JavaScript string representation of the result. + +**Validates: Requirements 4.1, 4.2** + +--- + +### Property 2: Eval of throwing JS returns err with non-empty message + +*For any* JavaScript snippet that throws (either a syntax error or a `throw` statement), calling `eval(code)` SHALL return `err(msg)` where `msg` is a non-empty string containing information about the error. + +**Validates: Requirements 4.3, 4.4** + +--- + +### Property 3: Global state persists across eval calls + +*For any* valid JavaScript identifier `name` and primitive value `v`, if `eval("var " + name + " = " + v)` returns `ok`, then a subsequent call to `eval(name)` on the same component instance SHALL return `ok(String(v))`. + +**Validates: Requirements 3.1, 3.5, 4.7** + +--- + +### Property 4: Value type round-trip — construction and type-checking + +*For any* value of a primitive type (int32, float64, bool, string), constructing a `js-value` via the corresponding `new-*` function and then calling the corresponding `is-*` method SHALL return `true`, and all other `is-*` methods SHALL return `false`. + +**Validates: Requirements 5.3, 5.8** + +--- + +### Property 5: Numeric conversion round-trip + +*For any* `int32` value `n`, constructing `new-int32(n)` and then calling `to-int32()` on the result SHALL return `n`. Similarly, *for any* `float64` value `f` that is exactly representable, `new-float64(f)` followed by `to-float64()` SHALL return `f`. + +**Validates: Requirements 5.9, 5.10** + +--- + +### Property 6: Property set/get round-trip + +*For any* JS object, property name `k` (a valid JS identifier), and primitive value `v`, calling `set-property(k, new-T(v))` followed by `get-property(k)` SHALL return a `js-value` whose `to-string()` equals `String(v)` in JavaScript. + +**Validates: Requirements 5.5, 5.6** + +--- + +### Property 7: Function call produces correct result + +*For any* JavaScript function `f` defined via `eval` that maps its argument through a deterministic pure transformation, calling `f.call([arg])` via the `call` method SHALL return a `js-value` whose string representation matches the expected output of applying `f` to `arg`. + +**Validates: Requirements 5.7** + +## Error Handling + +### eval error paths + +| Condition | MicroQuickJS behavior | Glue response | +|-----------|----------------------|---------------| +| Syntax error | `JS_Eval` returns `JS_EXCEPTION`; exception is a `SyntaxError` object | `JS_GetException` + `JS_ToCStringLen` → `err(message)` | +| Runtime exception (`throw`) | `JS_Eval` returns `JS_EXCEPTION` | same as above | +| `JS_ToCStringLen` returns NULL for exception | Rare; can happen for non-stringifiable internal values | `err("Error: unknown exception")` | +| `JS_ToCStringLen` returns NULL for result | Rare; fall back | `ok("undefined")` | +| Arena out of memory | `JS_Eval` returns `JS_EXCEPTION` with an `InternalError: out of memory` message | `err("InternalError: out of memory")` | + +### JS_Value_Resource error paths + +| Condition | Behavior | +|-----------|----------| +| `get-property` on non-object | `JS_GetPropertyStr` returns `JS_EXCEPTION`; wrapped in a resource with `is-exception() = true` | +| `call` on non-function | `JS_Call` returns `JS_EXCEPTION`; wrapped in a resource | +| `to-int32` / `to-float64` on non-numeric | `JS_ToInt32` / `JS_ToNumber` return non-zero; result is 0 / NaN | +| `to-string` on value that cannot be stringified | `JS_ToCStringLen` returns NULL; glue returns empty string (len=0, ptr=NULL) | + +### Arena exhaustion + +If the 4 MiB arena is exhausted, `JS_Eval` returns `JS_EXCEPTION` with an out-of-memory error. The component does not crash — it returns `err("InternalError: out of memory")`. The arena is not reset between calls; once exhausted, subsequent calls will also fail until the component instance is restarted. + +This is a known limitation of the arena model. The 4 MiB size was chosen to comfortably run the test suite files. For long-running use cases that accumulate significant global state, the arena size can be increased by changing `s_mem[4 * 1024 * 1024]` and rebuilding. + +## Runtime Compatibility + +| Runtime | Version | Status | Notes | +|---------|---------|--------|-------| +| WAMR (iwasm) | any with `-DWAMR_BUILD_COMPONENT_MODEL=1` | ✅ Primary target | Full Component Model support; validated | +| Wasmtime | 20+ | ✅ Development/CI | Full Component Model + EH support | +| WasmEdge | 0.14.1 | ❌ | Validator bug: rejects opcode `0x50b` | +| WasmEdge | 0.17.0-alpha.1 | ❌ | Illegal opcode `0x117` (EH gap) | +| WasmEdge | 0.17.0-alpha.2 | 🔄 To be tested | Released 2026-04-10; Component Model loader improvements | +| Node.js (WASI) | any | ❌ | No Component Model support | +| Deno | any | ❌ | No Component Model support | + +### EH opcodes note + +The `try_table` instruction (opcode `0x117`) is emitted by wasi-sdk clang as the lowering of C `setjmp`/`longjmp` patterns on wasm32. This is correct and expected behavior — it is not a bug in the component. Runtimes that reject `0x117` have an incomplete WebAssembly Exception Handling implementation. Do not attempt to suppress these opcodes; doing so would break MicroQuickJS's internal error recovery. + +### WAMR build requirements + +```bash +cmake -DWAMR_BUILD_COMPONENT_MODEL=1 -DWAMR_BUILD_INTERP=1 -DWAMR_BUILD_FAST_INTERP=1 .. +make -j$(nproc) +``` + +The resulting `iwasm` binary can run the component: +```bash +iwasm --component microquickjs.component.wasm +``` + +## Testing Strategy + +### Unit tests (example-based) + +These cover specific behaviors and edge cases that are not well-served by property generation: + +- `eval("2 + 2")` → `ok("4")` +- `eval("undefined")` → `ok("undefined")` +- `eval("null")` → `ok("null")` +- `eval("true")` → `ok("true")` +- `eval('"hello"')` → `ok("hello")` +- `eval("throw new Error('boom')")` → `err` containing `"boom"` +- `eval("{")` → `err` (syntax error, non-empty message) +- `get-global-object()` → resource where `is-null()` and `is-undefined()` are both false +- `new-int32(42).to-int32()` → `42` +- `new-bool(true).is-bool()` → `true` +- Create resource, drop it, create another — no crash + +### Property tests (property-based) + +Each property test must be tagged with a comment referencing the design property: +``` +// Feature: microquickjs-wasi-component, Property N: +``` + +Minimum 100 iterations per property. + +**Property 1 — Eval of valid JS returns ok with correct string:** +```typescript +// Feature: microquickjs-wasi-component, Property 1: eval of valid JS returns ok +fc.assert(fc.asyncProperty( + fc.integer({ min: -1000, max: 1000 }), + fc.integer({ min: -1000, max: 1000 }), + async (a, b) => { + const result = await engine.eval(`${a} + ${b}`); + assert(result.tag === 'ok'); + assert(result.val === String(a + b)); + } +), { numRuns: 100 }); +``` + +**Property 2 — Eval of throwing JS returns err:** +```typescript +// Feature: microquickjs-wasi-component, Property 2: eval of throwing JS returns err +fc.assert(fc.asyncProperty( + fc.string({ minLength: 1, maxLength: 50 }).filter(s => /^[a-zA-Z0-9 ]+$/.test(s)), + async (msg) => { + const result = await engine.eval(`throw new Error(${JSON.stringify(msg)})`); + assert(result.tag === 'err'); + assert(result.val.length > 0); + assert(result.val.includes(msg)); + } +), { numRuns: 100 }); +``` + +**Property 3 — Global state persists:** +```typescript +// Feature: microquickjs-wasi-component, Property 3: global state persists across eval calls +fc.assert(fc.asyncProperty( + fc.string({ minLength: 1, maxLength: 20 }).filter(s => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s)), + fc.integer({ min: 0, max: 999999 }), + async (name, value) => { + await engine.eval(`var ${name} = ${value}`); + const result = await engine.eval(name); + assert(result.tag === 'ok'); + assert(result.val === String(value)); + } +), { numRuns: 100 }); +``` + +**Property 4 — Value type round-trip:** +```typescript +// Feature: microquickjs-wasi-component, Property 4: new-T then is-T returns true +fc.assert(fc.asyncProperty( + fc.integer({ min: -(2**31), max: 2**31 - 1 }), + async (n) => { + const v = await engine.newInt32(n); + assert(await v.isInt() === true); + assert(await v.isBool() === false); + assert(await v.isNull() === false); + assert(await v.isUndefined() === false); + } +), { numRuns: 100 }); +``` + +**Property 5 — Numeric conversion round-trip:** +```typescript +// Feature: microquickjs-wasi-component, Property 5: new-int32 then to-int32 round-trips +fc.assert(fc.asyncProperty( + fc.integer({ min: -(2**31), max: 2**31 - 1 }), + async (n) => { + const v = await engine.newInt32(n); + assert(await v.toInt32() === n); + } +), { numRuns: 100 }); +``` + +**Property 6 — Property set/get round-trip:** +```typescript +// Feature: microquickjs-wasi-component, Property 6: set-property then get-property round-trips +fc.assert(fc.asyncProperty( + fc.string({ minLength: 1, maxLength: 20 }).filter(s => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s)), + fc.integer({ min: 0, max: 999999 }), + async (key, value) => { + const obj = await engine.newObject(); + const val = await engine.newInt32(value); + await obj.setProperty(key, val); + const got = await obj.getProperty(key); + assert(await got.toInt32() === value); + } +), { numRuns: 100 }); +``` + +**Property 7 — Function call produces correct result:** +```typescript +// Feature: microquickjs-wasi-component, Property 7: function call produces correct result +fc.assert(fc.asyncProperty( + fc.integer({ min: 0, max: 10000 }), + async (n) => { + await engine.eval('function double(x) { return x * 2; }'); + const global = await engine.getGlobalObject(); + const fn = await global.getProperty('double'); + const arg = await engine.newInt32(n); + const result = await fn.call([arg]); + assert(await result.toInt32() === n * 2); + } +), { numRuns: 100 }); +``` + +### Integration tests + +Run against the actual component on WAMR: + +- Load and validate: `wasm-tools validate --features component-model build/microquickjs.component.wasm` +- WIT inspection: `wasm-tools component wit build/microquickjs.component.wasm` +- Test suite files: evaluate `tests/test_closure.js`, `tests/test_language.js`, `tests/test_loop.js`, `tests/test_builtin.js`, `tests/test_rect.js` via `eval` and verify no `err` is returned + +### Smoke tests + +- `make -f Makefile.wasi` completes without error +- `build/microquickjs.component.wasm` exists and is non-empty +- `wasm-tools validate` exits 0 +- Binary size is ≤ 200 KB (record and alert if exceeded) + +## Limitations + +1. **No I/O from JS.** `print()`, `Date.now()`, `performance.now()`, `load()`, `setTimeout()`, and `clearTimeout()` are all no-ops. JS code that depends on these will silently get `undefined`. This is intentional — the component is a pure computation engine. + +2. **No event loop.** The component is synchronous. Promises, async/await, and `setTimeout`-based scheduling are not supported. `eval` blocks until the JS code completes. + +3. **Arena is not reset between calls.** Global state accumulates across `eval` calls. If the arena fills up, subsequent calls return `err("InternalError: out of memory")`. The only way to reset is to restart the component instance. + +4. **No module system.** `import`/`export` ES module syntax is not supported by MicroQuickJS. `require()` is also not available. + +5. **No `console.log`.** The `js_print` stub returns `undefined` silently. Hosts that need JS output should capture it via the `eval` return value or by setting a global callback function via `set-property`. + +6. **WasmEdge not supported.** All currently released WasmEdge versions have Component Model gaps that prevent loading this component. WasmEdge 0.17.0-alpha.2 may resolve this but has not been validated. + +7. **Single-threaded.** The component has no thread support. The arena is not thread-safe. Each component instance must be used from a single thread. + +8. **`new-array` does not pre-size.** The WIT `new-array` function passes `initial_len=0` to `JS_NewArray`. Hosts that need a pre-sized array should use `eval("new Array(n)")` instead. + +9. **`to-string` on exception values.** If `to-string()` is called on a `js-value` where `is-exception()` is true, the behavior is undefined — `JS_ToCStringLen` on `JS_EXCEPTION` may return NULL. Hosts should check `is-exception()` before calling `to-string()`. + +10. **Static memory footprint is fixed at compile time.** The 4 MiB arena is baked into the Wasm module's initial linear memory. It cannot be changed at runtime. To use a different arena size, recompile with a different `s_mem` declaration. diff --git a/Makefile.wasi b/Makefile.wasi new file mode 100644 index 0000000..ba56a44 --- /dev/null +++ b/Makefile.wasi @@ -0,0 +1,73 @@ +WASI_SDK_PATH ?= /opt/wasi-sdk +CC = $(WASI_SDK_PATH)/bin/clang +AR = $(WASI_SDK_PATH)/bin/llvm-ar +CFLAGS = -Oz -target wasm32-wasi -D_WASI_EMULATED_SIGNAL -I. -Igenerated -Imquickjs -Iwasi_shims -Ibuild -mllvm -wasm-enable-sjlj +LDFLAGS = -target wasm32-wasi -mexec-model=reactor -Wl,--no-entry -Wl,--export=cabi_realloc -Wl,--export=__wasm_call_ctors -lwasi-emulated-signal -lwasi-emulated-process-clocks -lsetjmp -lm +ADAPTER ?= /usr/share/wasi-adapter/wasi_snapshot_preview1.reactor.wasm + +BUILD_DIR = build +SRC_DIR = mquickjs +GEN_DIR = generated + +OBJS = \ + $(BUILD_DIR)/mquickjs.o \ + $(BUILD_DIR)/cutils.o \ + $(BUILD_DIR)/dtoa.o \ + $(BUILD_DIR)/libm.o \ + $(BUILD_DIR)/microquickjs.o \ + $(BUILD_DIR)/glue.o + +.PHONY: all clean inspect test headers + +all: $(BUILD_DIR)/microquickjs.component.wasm + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(GEN_DIR)/microquickjs.c: microquickjs.wit + mkdir -p $(GEN_DIR) + wit-bindgen c ./microquickjs.wit --out-dir ./$(GEN_DIR) --world microquickjs + +$(BUILD_DIR)/mquickjs_build_native: $(SRC_DIR)/mqjs_stdlib.c $(SRC_DIR)/mquickjs_build.c $(SRC_DIR)/cutils.c | $(BUILD_DIR) + gcc -O2 -I$(SRC_DIR) -o $@ $^ -lm + +$(BUILD_DIR)/mqjs_stdlib.h: $(BUILD_DIR)/mquickjs_build_native + $< -m32 > $@ + +$(BUILD_DIR)/mquickjs_atom.h: $(BUILD_DIR)/mquickjs_build_native + $< -m32 -a > $@ + +headers: $(BUILD_DIR)/mqjs_stdlib.h $(BUILD_DIR)/mquickjs_atom.h + +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) headers + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/microquickjs.o: $(GEN_DIR)/microquickjs.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/glue.o: glue.c | $(BUILD_DIR) $(GEN_DIR)/microquickjs.c headers + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/core.wasm: $(OBJS) $(GEN_DIR)/microquickjs_component_type.o + $(CC) $(LDFLAGS) -o $@ $^ + +$(BUILD_DIR)/embedded.wasm: $(BUILD_DIR)/core.wasm microquickjs.wit + wasm-tools component embed ./microquickjs.wit $< --world microquickjs --output $@ + +$(BUILD_DIR)/microquickjs.component.wasm: $(BUILD_DIR)/embedded.wasm + wasm-tools component new $< --adapt $(ADAPTER) --output $@ + @if which wasm-opt > /dev/null 2>&1; then \ + echo "Optimizing with wasm-opt..."; \ + wasm-opt -Oz $@ -o $@; \ + fi + @echo "Component size: $$(stat -c %s $@) bytes" + +inspect: $(BUILD_DIR)/microquickjs.component.wasm + wasm-tools component wit $< + +test: $(BUILD_DIR)/microquickjs.component.wasm + @echo "Test 1: Arithmetic" + wasmtime run -W all-proposals=y $< --invoke eval "2 + 2" || echo "Test failed (known runtime issue)" + +clean: + rm -rf $(BUILD_DIR) $(GEN_DIR) diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..6e8ee2e --- /dev/null +++ b/PLAN.md @@ -0,0 +1,247 @@ +# Implementation Plan: microquickjs-wasi-component + +## Overview + +Build `build/microquickjs.component.wasm` — a WASI 0.2 Component wrapping MicroQuickJS — by wiring together the WIT interface, the wit-bindgen-generated ABI glue, the C glue layer, the MicroQuickJS engine sources, and the Makefile.wasi build pipeline. The implementation language is C (glue layer) with TypeScript/fast-check for host-side property tests. + +## Tasks + +- [x] 1. Verify and finalize the WIT interface file + - Confirm `microquickjs.wit` defines package `local:microquickjs`, world `microquickjs`, and interface `engine` with all required resource methods and free functions + - Verify `js-value` resource has all 9 type-check methods, 3 conversion methods, `get-property`, `set-property`, and `call` + - Verify free functions: `new-int32`, `new-float64`, `new-bool`, `new-string`, `new-object`, `new-array`, `get-global-object`, `eval` + - Verify `eval` signature: `func(code: string) -> result` + - Run `wasm-tools component wit microquickjs.wit` (or `wasm-tools parse`) to confirm no parse errors + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_ + +- [x] 2. Implement and verify the Makefile.wasi build pipeline + - [x] 2.1 Implement the native stdlib header generation target + - Ensure `$(BUILD_DIR)/mquickjs_build_native` compiles from `mquickjs/mqjs_stdlib.c`, `mquickjs/mquickjs_build.c`, `mquickjs/cutils.c` with `gcc -O2 -I mquickjs/` + - Ensure `$(BUILD_DIR)/mqjs_stdlib.h` is generated by running `$< -m32 > $@` + - Ensure `$(BUILD_DIR)/mquickjs_atom.h` is generated by running `$< -m32 -a > $@` + - _Requirements: 2.3_ + + - [x] 2.2 Implement the wit-bindgen code generation target + - Ensure `$(GEN_DIR)/microquickjs.c` and `$(GEN_DIR)/microquickjs.h` are generated via `wit-bindgen c ./microquickjs.wit --out-dir ./generated --world microquickjs` + - Confirm `generated/microquickjs_component_type.o` is produced and linked into `core.wasm` + - _Requirements: 2.4_ + + - [x] 2.3 Implement Path A (wasip1 + adapter) compilation and linking + - Compile all wasm object files with `--target=wasm32-wasi -mexec-model=reactor -Oz -D_WASI_EMULATED_SIGNAL -mllvm -wasm-enable-sjlj` + - Include paths: `-I. -Igenerated -Imquickjs -Ibuild` + - Exclude `mquickjs/mqjs.c` from the wasm build + - Link with `-Wl,--no-entry -Wl,--export=cabi_realloc -Wl,--export=__wasm_call_ctors -lwasi-emulated-signal -lwasi-emulated-process-clocks -lsetjmp -lm` + - Produce `build/core.wasm` + - _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10_ + + - [x] 2.4 Implement the component embed and compose targets + - `build/embedded.wasm`: `wasm-tools component embed ./microquickjs.wit build/core.wasm --world microquickjs --output build/embedded.wasm` + - `build/microquickjs.component.wasm`: `wasm-tools component new build/embedded.wasm --adapt wasi_snapshot_preview1= --output build/microquickjs.component.wasm` + - Wire `ADAPTER` variable with default `/usr/share/wasi-adapter/wasi_snapshot_preview1.reactor.wasm` + - _Requirements: 2.1, 2.5, 9.5_ + + - [x] 2.5 Implement WASI_SDK_PATH override and optional wasm-opt post-processing + - Default `WASI_SDK_PATH ?= /opt/wasi-sdk`; accept override via environment variable + - Add optional `wasm-opt -Oz` post-processing step gated on `$(shell which wasm-opt)` + - _Requirements: 2.11, 2.12, 2.13, 10.5_ + +- [x] 3. Checkpoint — verify build pipeline produces core.wasm + - Run `make -f Makefile.wasi headers` and confirm `build/mqjs_stdlib.h` and `build/mquickjs_atom.h` are generated + - Run `make -f Makefile.wasi build/core.wasm` and confirm it exits 0 and produces a non-empty `build/core.wasm` + - Ensure all tests pass, ask the user if questions arise. + +- [x] 4. Implement glue.c — singleton context and WASI shims + - [x] 4.1 Implement the singleton JS context and arena + - Declare `static uint8_t s_mem[4 * 1024 * 1024]` and `static JSContext *s_ctx = NULL` + - Implement `ensure_context()` calling `JS_NewContext(s_mem, sizeof(s_mem), &js_stdlib)` on first call + - Call `ensure_context()` at the top of every exported function + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 10.3_ + + - [x] 4.2 Implement WASI shim stubs + - Define no-op stubs returning `JS_UNDEFINED` for: `js_date_now`, `js_print`, `js_performance_now`, `js_gc`, `js_load`, `js_setTimeout`, `js_clearTimeout` + - Place stubs before `#include "build/mqjs_stdlib.h"` so the stdlib function table can reference them + - _Requirements: 7.1_ + + - [x] 4.3 Implement include ordering in glue.c preamble + - Order: ``, ``, ``, then `mquickjs/mquickjs.h`, then `cabi_realloc` extern declaration, then WASI shims, then `generated/microquickjs.h`, then `build/mqjs_stdlib.h` + - _Requirements: 7.1, 7.2_ + +- [x] 5. Implement glue.c — JS_Value_Resource lifecycle + - [x] 5.1 Implement the JS_Value_Resource struct and make_own_value helper + - Define `struct exports_local_microquickjs_engine_js_value_t { JSValue val; JSGCRef root; }` + - Implement `make_own_value(JSValue val)`: `malloc` the struct, set `rep->val`, call `JS_AddGCRef(s_ctx, &rep->root)`, set `rep->root.val = val`, return `exports_local_microquickjs_engine_js_value_new(rep)` + - _Requirements: 5.1, 5.3_ + + - [x] 5.2 Implement the resource destructor + - Implement `exports_local_microquickjs_engine_js_value_destructor(rep)`: call `JS_DeleteGCRef(s_ctx, &rep->root)` then `free(rep)` + - _Requirements: 5.2_ + + - [ ]* 5.3 Write unit tests for resource lifecycle + - Test: create a resource, verify it is non-null, drop it, create another — no crash or assertion failure + - Test: `get-global-object()` returns a resource where `is-null()` and `is-undefined()` are both false + - _Requirements: 5.1, 5.2, 5.4_ + +- [x] 6. Implement glue.c — eval function + - [x] 6.1 Implement the eval happy path + - Call `JS_Eval(s_ctx, code->ptr, code->len, "", JS_EVAL_RETVAL)` + - On non-exception result: call `JS_ToCStringLen(s_ctx, &len, val, &buf)` with stack-allocated `JSCStringBuf buf` + - Copy result into `cabi_realloc(NULL, 0, 1, len)` buffer; fall back to `"undefined"` if `JS_ToCStringLen` returns NULL + - Return `true` (ok variant) + - _Requirements: 4.1, 4.2, 4.6, 6.1, 6.2_ + + - [x] 6.2 Implement the eval error path + - On `JS_IsException(val)`: call `JS_GetException(s_ctx)`, then `JS_ToCStringLen` on the exception value + - Copy error string into `cabi_realloc` buffer; fall back to `"Error: unknown exception"` if `JS_ToCStringLen` returns NULL + - Return `false` (err variant) + - Do NOT call `JS_FreeValue` or `JS_FreeCString` + - _Requirements: 4.3, 4.4, 4.5, 6.3, 6.4_ + + - [ ]* 6.3 Write unit tests for eval + - `eval("2 + 2")` → `ok("4")` + - `eval("undefined")` → `ok("undefined")` + - `eval("null")` → `ok("null")` + - `eval("true")` → `ok("true")` + - `eval('"hello"')` → `ok("hello")` + - `eval("throw new Error('boom')")` → `err` containing `"boom"` + - `eval("{")` → `err` with non-empty message + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_ + + - [ ]* 6.4 Write property test for eval — Property 1: eval of valid JS returns ok + - **Property 1: Eval of valid JS returns ok with correct string representation** + - **Validates: Requirements 4.1, 4.2** + - Use fast-check: generate pairs of integers `(a, b)`, assert `eval(`${a} + ${b}`)` returns `ok(String(a + b))` + - Minimum 100 iterations + + - [ ]* 6.5 Write property test for eval — Property 2: eval of throwing JS returns err + - **Property 2: Eval of throwing JS returns err with non-empty message** + - **Validates: Requirements 4.3, 4.4** + - Use fast-check: generate safe message strings, assert `eval("throw new Error(...)")` returns `err` with non-empty message containing the original string + - Minimum 100 iterations + + - [ ]* 6.6 Write property test for eval — Property 3: global state persists across eval calls + - **Property 3: Global state persists across eval calls** + - **Validates: Requirements 3.1, 3.5, 4.7** + - Use fast-check: generate valid JS identifier + integer, `eval("var name = value")` then `eval("name")`, assert second call returns `ok(String(value))` + - Minimum 100 iterations + +- [x] 7. Checkpoint — verify eval compiles and basic smoke tests pass + - Run `make -f Makefile.wasi` and confirm `build/microquickjs.component.wasm` is produced + - Run `wasm-tools validate --features component-model build/microquickjs.component.wasm` and confirm exit 0 + - Ensure all tests pass, ask the user if questions arise. + +- [x] 8. Implement glue.c — JS value construction functions + - [x] 8.1 Implement new-int32, new-float64, new-bool, new-string + - `new-int32(val)`: `make_own_value(JS_NewInt32(s_ctx, val))` + - `new-float64(val)`: `make_own_value(JS_NewFloat64(s_ctx, val))` + - `new-bool(val)`: `make_own_value(JS_NewBool(val))` + - `new-string(val)`: `make_own_value(JS_NewStringLen(s_ctx, (const char *)val->ptr, val->len))` + - _Requirements: 5.3_ + + - [x] 8.2 Implement new-object, new-array, get-global-object + - `new-object()`: `make_own_value(JS_NewObject(s_ctx))` + - `new-array()`: `make_own_value(JS_NewArray(s_ctx, 0))` + - `get-global-object()`: `make_own_value(JS_GetGlobalObject(s_ctx))` + - _Requirements: 5.3, 5.4_ + + - [ ]* 8.3 Write property test for value construction — Property 4: type round-trip + - **Property 4: Value type round-trip — construction and type-checking** + - **Validates: Requirements 5.3, 5.8** + - Use fast-check: generate int32 values, assert `new-int32(n).is-int()` is true and all other `is-*` methods return false + - Minimum 100 iterations + + - [ ]* 8.4 Write property test for numeric conversion — Property 5: numeric round-trip + - **Property 5: Numeric conversion round-trip** + - **Validates: Requirements 5.9, 5.10** + - Use fast-check: generate int32 values, assert `new-int32(n).to-int32()` returns `n`; generate float64 values, assert `new-float64(f).to-float64()` returns `f` + - Minimum 100 iterations + +- [x] 9. Implement glue.c — type-check and conversion methods + - [x] 9.1 Implement type-check methods (is-*) + - `is-int`: `JS_IsInt(self->val)` + - `is-bool`: `JS_IsBool(self->val)` + - `is-null`: `JS_IsNull(self->val)` + - `is-undefined`: `JS_IsUndefined(self->val)` + - `is-exception`: `JS_IsException(self->val)` + - `is-number`: `JS_IsNumber(s_ctx, self->val)` + - `is-string`: `JS_IsString(s_ctx, self->val)` + - `is-error`: `JS_IsError(s_ctx, self->val)` + - `is-function`: `JS_IsFunction(s_ctx, self->val)` + - _Requirements: 5.8_ + + - [x] 9.2 Implement conversion methods (to-string, to-int32, to-float64) + - `to-string`: `JS_ToCStringLen` with stack `JSCStringBuf`, copy into `cabi_realloc` buffer; return empty string (ptr=NULL, len=0) if NULL + - `to-int32`: `JS_ToInt32(s_ctx, &res, self->val)`, return `res` + - `to-float64`: `JS_ToNumber(s_ctx, &res, self->val)`, return `res` + - _Requirements: 5.9, 5.10, 6.1, 6.2_ + +- [x] 10. Implement glue.c — property access and function call + - [x] 10.1 Implement get-property and set-property + - `get-property(name)`: `malloc(name->len + 1)`, `memcpy`, null-terminate, call `JS_GetPropertyStr(s_ctx, self->val, cname)`, `free(cname)`, return `make_own_value(res)` + - `set-property(name, val)`: same null-terminated copy pattern, call `JS_SetPropertyStr(s_ctx, self->val, cname, val->val)`, `free(cname)` + - _Requirements: 5.5, 5.6, 6.5_ + + - [x] 10.2 Implement call + - Loop `JS_PushArg(s_ctx, args->ptr[i]->val)` for each argument + - Call `JS_Call(s_ctx, args->len)`, return `make_own_value(res)` + - _Requirements: 5.7_ + + - [ ]* 10.3 Write property test for property access — Property 6: set/get round-trip + - **Property 6: Property set/get round-trip** + - **Validates: Requirements 5.5, 5.6** + - Use fast-check: generate valid JS identifier + integer, create object, `set-property(k, new-int32(v))`, `get-property(k)`, assert `to-int32()` returns `v` + - Minimum 100 iterations + + - [ ]* 10.4 Write property test for function call — Property 7: call produces correct result + - **Property 7: Function call produces correct result** + - **Validates: Requirements 5.7** + - Use fast-check: generate integers, `eval("function double(x) { return x * 2; }")`, get `double` via `get-global-object().get-property("double")`, call with `new-int32(n)`, assert `to-int32()` returns `n * 2` + - Minimum 100 iterations + +- [x] 11. Implement string memory management correctness + - Audit all string-returning paths in glue.c to confirm `cabi_realloc` is used for every host-bound string + - Confirm no `JS_FreeCString` or `JS_FreeValue` calls exist anywhere in glue.c + - Confirm all null-terminated C strings for property names use `malloc`/`free` (not arena allocation) + - Confirm `JS_ToCStringLen` is used (not `JS_ToCString`) wherever string length is needed + - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_ + +- [x] 12. Validate component structural correctness and runtime compatibility + - [x] 12.1 Validate component model structure + - Run `wasm-tools validate --features component-model build/microquickjs.component.wasm` — must exit 0 + - Run `wasm-tools component wit build/microquickjs.component.wasm` — must report the `engine` interface matching `microquickjs.wit` + - Confirm the component does NOT export `_start` and DOES export `__wasm_call_ctors` and `cabi_realloc` + - _Requirements: 9.1, 9.2, 9.3, 9.4_ + + - [ ] 12.2 Run WAMR smoke tests + - Build WAMR with `-DWAMR_BUILD_COMPONENT_MODEL=1 -DWAMR_BUILD_INTERP=1 -DWAMR_BUILD_FAST_INTERP=1` + - Invoke `eval("2 + 2")` via WAMR — assert `ok("4")` + - Invoke `eval("undefined")` via WAMR — assert `ok("undefined")` + - Invoke `eval("throw new Error('boom')")` via WAMR — assert `err` containing `"boom"` + - Invoke `eval("{")` via WAMR — assert `err` with non-empty message + - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_ + + - [ ] 12.3 Run JS test suite files via eval on WAMR + - Evaluate `tests/test_closure.js`, `tests/test_language.js`, `tests/test_loop.js`, `tests/test_builtin.js`, `tests/test_rect.js` via `eval` (read file contents, pass as code string) + - Assert each returns `ok(...)` with no `err` variant + - _Requirements: 8.6_ + +- [x] 13. Verify size and performance constraints + - Build with `-Oz` and record binary size of `build/microquickjs.component.wasm` + - Assert size ≤ 200 KB; log a warning if between 150–200 KB + - If `wasm-opt` is on PATH, run `wasm-opt -Oz` and record post-optimization size + - Evaluate `tests/microbench.js` via `eval` and confirm it completes without `err("InternalError: out of memory")` + - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_ + +- [x] 14. Final checkpoint — full build and test suite green + - Run `make -f Makefile.wasi clean && make -f Makefile.wasi` from scratch — must complete without error + - Run `wasm-tools validate --features component-model build/microquickjs.component.wasm` — exit 0 + - Run all property tests (Properties 1–7) — all must pass + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for a faster MVP build +- `glue.c` already contains a working implementation; tasks 4–11 are verification/completion tasks — review the existing code against each requirement and patch where gaps exist +- Property tests (tasks 6.4–6.6, 8.3–8.4, 10.3–10.4) require a TypeScript test harness using fast-check and the Wasmtime JS bindings (or equivalent WASI 0.2 host bindings) +- EH opcodes (`try_table`, opcode `0x117`) in the output are correct and expected — do not suppress them +- `mqjs.c` must never appear in the wasm build object list +- The `-m32` flag to `mquickjs_build_native` is required to generate 32-bit offsets matching the wasm32 address space diff --git a/README.WASI.md b/README.WASI.md new file mode 100644 index 0000000..1c3937a --- /dev/null +++ b/README.WASI.md @@ -0,0 +1,113 @@ +# MicroQuickJS WASI Component + +This is a port of MicroQuickJS to a WASI 0.2 WebAssembly Component. + +## Features +- Exports a complete JS engine interface via the `local:microquickjs/engine` interface. +- Includes `js-value` resource for granular value manipulation. +- Supports type checking, conversions, property access, and function calling. +- Uses a singleton JS context for persistent state between calls. +- Aggressively optimized for size using `-Oz`. + +## Documentation +For deeper technical details, please refer to: +- [DESIGN.md](DESIGN.md) — Detailed architecture, design decisions, and implementation notes. +- [REQUIREMENTS.md](REQUIREMENTS.md) — Formal requirements and acceptance criteria for the WASI port. +- [PLAN.md](PLAN.md) — Step-by-step implementation plan and task tracking. + +## Build Environment +Requires the following tools: +- **WASI SDK:** 25.0 +- **wit-bindgen:** 0.55.0 +- **wasm-tools:** 1.246.2 + +### Build Command +```bash +make -f Makefile.wasi +``` + +### Build Artifacts +- `build/core.wasm`: Core WebAssembly module. +- `build/embedded.wasm`: Core module with WIT metadata embedded. +- `build/microquickjs.component.wasm`: Final WASI 0.2 component. + +## Usage +### Wasmtime +Current versions of Wasmtime (v29) require the Exceptions proposal for `setjmp/longjmp` support used by the engine. +```bash +wasmtime run -W all-proposals=y build/microquickjs.component.wasm --invoke eval "1+1" +``` + +## C API Compatibility Table + +The following table shows the correspondence between the MicroQuickJS C API and the WASI Component Model exports. + +| MicroQuickJS C API | WASI Component Export | Ported | +| :--- | :--- | :---: | +| `JS_Eval` | `eval` | ✅ | +| `JS_NewInt32` | `new-int32` | ✅ | +| `JS_NewFloat64` | `new-float64` | ✅ | +| `JS_NewBool` | `new-bool` | ✅ | +| `JS_NewString` | `new-string` | ✅ | +| `JS_NewObject` | `new-object` | ✅ | +| `JS_NewArray` | `new-array` | ✅ | +| `JS_GetGlobalObject` | `get-global-object` | ✅ | +| `JS_IsInt` | `js-value.is-int` | ✅ | +| `JS_IsBool` | `js-value.is-bool` | ✅ | +| `JS_IsNull` | `js-value.is-null` | ✅ | +| `JS_IsUndefined` | `js-value.is-undefined` | ✅ | +| `JS_IsException` | `js-value.is-exception` | ✅ | +| `JS_IsNumber` | `js-value.is-number` | ✅ | +| `JS_IsString` | `js-value.is-string` | ✅ | +| `JS_IsError` | `js-value.is-error` | ✅ | +| `JS_IsFunction` | `js-value.is-function` | ✅ | +| `JS_ToString` | `js-value.to-string` | ✅ | +| `JS_ToInt32` | `js-value.to-int32` | ✅ | +| `JS_ToNumber` | `js-value.to-float64` | ✅ | +| `JS_GetPropertyStr` | `js-value.get-property` | ✅ | +| `JS_SetPropertyStr` | `js-value.set-property` | ✅ | +| `JS_Call` | `js-value.call` | ✅ | +| `JS_NewContext` | N/A (Internal Singleton) | 🛠️ | +| `JS_FreeContext` | N/A (Internal) | 🛠️ | +| `JS_Throw` | N/A (Host handles result) | 🛠️ | +| `JS_GC` | N/A (Internal) | 🛠️ | +| `JS_LoadBytecode` | N/A | ❌ | + +## Complete WIT Interface Support +The component exports the following functions and resource methods: + +### Engine Functions +- `eval(code: string) -> result` +- `new-int32(val: s32) -> js-value` +- `new-float64(val: f64) -> js-value` +- `new-bool(val: bool) -> js-value` +- `new-string(val: string) -> js-value` +- `new-object() -> js-value` +- `new-array() -> js-value` +- `get-global-object() -> js-value` + +### JS-Value Methods +- `is-int()`, `is-bool()`, `is-null()`, `is-undefined()`, `is-exception()`, `is-number()`, `is-string()`, `is-error()`, `is-function()` +- `to-string()`, `to-int32()`, `to-float64()` +- `get-property(name: string) -> js-value` +- `set-property(name: string, val: borrow)` +- `call(args: list>) -> js-value` + +## Runtime Limitations & WasmEdge Status + +### WasmEdge Maturity Status (Issue #4236) +- **Component Model support:** 🔶 Partial +- **Simple types (string):** ✅ Stable +- **Result types:** 🔶 Partial +- **String marshalling:** 🔶 Partial (large strings may expose bugs) + +### Known Runtime Limitations +- **Wasmtime v29.0.1:** Fails to parse the module with `exceptions proposal not enabled` at the `tags` section offset, despite `-W all-proposals=y`. This appears to be a regression or limitation in component-level exception handling support in this version. +- **WasmEdge v0.14.1:** Fails with `malformed name (Code: 0x50b)` when using `wasmedge run --enable-all`. This indicates ongoing stabilization of the Component Model parser for WASI 0.2. +- **WAMR Compatibility:** WAMR does not support the Component Model. The `build/core.wasm` module is binary-compatible with WAMR via its C embedding API but lacks wit-bindgen host glue. + +## Mitigation & Recommendations +- Use **WasmEdge 0.16.1** (latest stable) or **0.17.0-alpha.x** for the latest Component Model improvements. +- Note: WasmEdge 0.17.x is currently in alpha (latest: 0.17.0-alpha.2). +- Ensure host runtimes strictly support the **WebAssembly Exception Handling** proposal for MicroQuickJS's internal `setjmp/longjmp` usage. +- Monitor WasmEdge issue #4236 for updates on `result` and large string marshalling. diff --git a/README.md b/README.md index 3e53b59..1ad4768 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ different in order to consume less memory. In particular, it relies on a tracing garbage collector, the VM does not use the CPU stack and strings are stored in UTF-8. +## WASI Component + +MicroQuickJS has been ported to WebAssembly as a WASI 0.2 Component. This allows the engine to be embedded in modern WebAssembly runtimes with a high-level WIT interface. + +For detailed information on building and using the WASI component, see [README.WASI.md](README.WASI.md). + ## REPL The REPL is `mqjs`. Usage: diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..2557e78 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,191 @@ +# Requirements Document + +## Introduction + +This feature ports MicroQuickJS — Fabrice Bellard's minimal, arena-based JavaScript engine — to a WASI 0.2 WebAssembly Component. The deliverable is `microquickjs.component.wasm`: a self-contained component that exports a JavaScript evaluation interface defined in WIT. The primary use case is embedding a lightweight JS engine in any WASI 0.2-capable host without a full QuickJS runtime. + +MicroQuickJS differs from standard QuickJS in key ways: it uses arena-based memory (no separate `JSRuntime`), `JS_ToCString` takes a stack-allocated `JSCStringBuf` (no `JS_FreeCString` needed), and there is no `JS_FreeValue` — the arena manages all memory. These constraints shape the glue layer design. + +The primary validated runtime is WAMR (iwasm) built with `-DWAMR_BUILD_COMPONENT_MODEL=1`. WasmEdge is excluded as a primary target due to known Component Model gaps in all currently released versions. + +## Glossary + +- **Component**: A WASI 0.2 WebAssembly Component as defined by the Component Model specification. +- **Core_Module**: The intermediate `core.wasm` produced before adapter composition. +- **Engine**: The exported WIT interface `local:microquickjs/engine` that exposes JS evaluation and value manipulation. +- **Glue_Layer**: The C translation unit (`glue.c`) that bridges the wit-bindgen-generated ABI to the MicroQuickJS C API. +- **JS_Context**: The singleton `JSContext *` allocated once per component instance over a static arena. +- **JS_Value**: The MicroQuickJS `JSValue` type — a tagged integer (32-bit on wasm32) representing any JS value. +- **JS_Value_Resource**: The WIT `resource js-value` that wraps a `JSValue` and a `JSGCRef` root to prevent arena GC. +- **MicroQuickJS**: The minimal JS engine from https://github.com/bellard/mquickjs, source in `mquickjs/`. +- **mqjs_stdlib**: The pre-compiled stdlib table generated by the native `mquickjs_build_native` host tool. +- **WAMR**: WebAssembly Micro Runtime, the primary validated host runtime for this component. +- **WASI_SDK**: The WASI SDK clang toolchain (version 25.0) used to compile C sources to wasm32-wasi. +- **WIT**: WebAssembly Interface Types — the IDL used to define the component's exported interface. +- **wit-bindgen**: Code generator that produces C ABI glue from a `.wit` file. +- **wasm-tools**: CLI used to embed WIT metadata and compose the final component with the WASI adapter. +- **WASI_Adapter**: The `wasi_snapshot_preview1.reactor.wasm` reactor adapter from Wasmtime releases, used to lift WASI preview1 imports to WASI 0.2. +- **EH_Opcodes**: WebAssembly Exception Handling opcodes (e.g., `try_table` / opcode `0x117`) emitted by wasi-sdk clang for `setjmp`/`longjmp` lowering under `-Oz`. These are correct and expected. +- **Arena**: The static byte array (`s_mem`) that backs the `JS_Context` for all JS heap allocations. + +--- + +## Requirements + +### Requirement 1: WIT Interface Definition + +**User Story:** As a host developer, I want a stable, versioned WIT interface, so that I can generate host bindings in any language supported by wit-bindgen. + +#### Acceptance Criteria + +1. THE WIT_File SHALL define the package `local:microquickjs` with a world named `microquickjs` that exports the `engine` interface. +2. THE `engine` interface SHALL export a `resource js-value` with methods: `is-int`, `is-bool`, `is-null`, `is-undefined`, `is-exception`, `is-number`, `is-string`, `is-error`, `is-function`, `to-string`, `to-int32`, `to-float64`, `get-property`, `set-property`, and `call`. +3. THE `engine` interface SHALL export free functions: `new-int32`, `new-float64`, `new-bool`, `new-string`, `new-object`, `new-array`, `get-global-object`, and `eval`. +4. THE `eval` function SHALL have the signature `eval: func(code: string) -> result` where the `ok` variant carries the stringified return value and the `err` variant carries the error message. +5. WHEN the WIT file is processed by `wasm-tools component wit`, THE Component SHALL report the interface matching the definitions in `microquickjs.wit` without validation errors. + +--- + +### Requirement 2: Build Toolchain and Reproducibility + +**User Story:** As a developer, I want a single `make -f Makefile.wasi` command to produce the final component, so that the build is reproducible and does not require manual steps. + +#### Acceptance Criteria + +1. THE Build_System SHALL produce `build/microquickjs.component.wasm` as the final artifact from a single `make -f Makefile.wasi` invocation. +2. THE Build_System SHALL use `$(WASI_SDK_PATH)/bin/clang` (defaulting to `/opt/wasi-sdk`) as the C compiler for all wasm32 object files. +3. THE Build_System SHALL generate `build/mqjs_stdlib.h` by compiling and running a native `mqjs_stdlib_native` host tool from `mqjs_stdlib.c`, `mquickjs_build.c`, and `cutils.c` (all in the workspace root). +4. THE Build_System SHALL generate `generated/microquickjs.c` and `generated/microquickjs.h` by invoking `wit-bindgen c ./microquickjs.wit --out-dir ./generated --world microquickjs`. +5. THE Build_System SHALL support two compilation paths: + - **Path A (wasip1 + adapter):** Compile with `--target=wasm32-wasi -mexec-model=reactor`, link with `-lwasi-emulated-signal`, embed WIT via `wasm-tools component embed`, then compose with `wasm-tools component new --adapt wasi_snapshot_preview1=wasi_snapshot_preview1.reactor.wasm`. + - **Path B (wasip2 native):** Compile with `--target=wasm32-wasip2 -mexec-model=reactor`, then wrap directly with `wasm-tools component new` (no `--adapt` needed). This path requires WASI SDK 25+ which ships a wasip2 sysroot. +6. THE Build_System SHALL compile all C sources with `-Oz` as the primary size optimization flag. +7. THE Build_System SHALL link with `-lm` and any required WASI emulation libraries for the chosen path. +8. THE Build_System SHALL link with `-Wl,--no-entry` (or `-mexec-model=reactor`) to suppress `_start` and produce a reactor-style module. +9. THE Build_System SHALL export `cabi_realloc` and `__wasm_call_ctors` from the linked module. +10. THE Build_System SHALL exclude `mqjs.c` from the wasm32 build because it uses `gettimeofday`/`clock_gettime` and is the REPL entry point. +11. IF `$(WASI_SDK_PATH)` does not exist at the default path, THEN THE Build_System SHALL accept an override via the `WASI_SDK_PATH` environment variable. +12. WHERE `packages/wasi-sdk` is present as a git submodule, THE Build_System SHALL accept `WASI_SDK_PATH=packages/wasi-sdk` as a valid override. +13. THE Build_System SHALL optionally run `wasm-opt -Oz` (from Binaryen) on the final component if `wasm-opt` is present on PATH, to achieve further size reduction toward the ~150 KB target. + +--- + +### Requirement 3: JS Context Lifecycle + +**User Story:** As a host developer, I want the component to manage its own JS context internally, so that I do not need to pass context handles across the component boundary. + +#### Acceptance Criteria + +1. THE Component SHALL maintain a single singleton `JS_Context` per component instance, initialized lazily on the first call to any exported function. +2. THE JS_Context SHALL be initialized by calling `JS_NewContext(s_mem, sizeof(s_mem), &js_stdlib)` over a static arena of at least 4 MiB. +3. WHEN `JS_NewContext` is called, THE Glue_Layer SHALL pass the `js_stdlib` descriptor generated from `mqjs_stdlib.h`. +4. THE Component SHALL NOT expose `JS_NewContext`, `JS_FreeContext`, or arena management to the host via the WIT interface. +5. WHILE the component instance is alive, THE JS_Context SHALL persist across multiple `eval` calls, preserving global state between invocations. + +--- + +### Requirement 4: JavaScript Evaluation (`eval`) + +**User Story:** As a host developer, I want to evaluate arbitrary JavaScript code and receive the result as a string, so that I can use the JS engine as a scripting layer. + +#### Acceptance Criteria + +1. WHEN `eval(code)` is called with syntactically valid JavaScript, THE Engine SHALL evaluate the code using `JS_Eval(ctx, code, len, "", JS_EVAL_RETVAL)` and return `ok(result_string)`. +2. WHEN `eval(code)` is called and the code produces a non-string return value, THE Engine SHALL convert the result to a string via `JS_ToCStringLen` before returning it in the `ok` variant. +3. WHEN `eval(code)` is called with a syntax error, THE Engine SHALL return `err(error_message)` where `error_message` is the stringified exception obtained via `JS_GetException` followed by `JS_ToCStringLen`. +4. WHEN `eval(code)` is called and a runtime exception is thrown, THE Engine SHALL return `err(error_message)` with the exception message. +5. IF `JS_ToCStringLen` returns NULL for the exception value, THEN THE Engine SHALL return `err("Error: unknown exception")`. +6. WHEN `eval(code)` is called with code that returns `undefined`, THE Engine SHALL return `ok("undefined")`. +7. THE `eval` function SHALL be callable multiple times on the same component instance, with each call sharing the same persistent `JS_Context`. + +--- + +### Requirement 5: JS Value Resource Management + +**User Story:** As a host developer, I want to create, inspect, and manipulate JS values across the component boundary, so that I can build richer integrations beyond simple string eval. + +#### Acceptance Criteria + +1. THE Glue_Layer SHALL wrap each `JSValue` in a heap-allocated `JS_Value_Resource` struct containing the `JSValue` and a `JSGCRef` root registered via `JS_AddGCRef`. +2. WHEN a `JS_Value_Resource` is dropped by the host, THE Glue_Layer SHALL call `JS_DeleteGCRef` to unroot the value and `free` the struct. +3. THE `new-int32`, `new-float64`, `new-bool`, `new-string`, `new-object`, and `new-array` functions SHALL each call the corresponding `JS_New*` API and return a rooted `JS_Value_Resource`. +4. THE `get-global-object` function SHALL return a rooted `JS_Value_Resource` wrapping the result of `JS_GetGlobalObject`. +5. WHEN `get-property(name)` is called on a `JS_Value_Resource`, THE Engine SHALL call `JS_GetPropertyStr` with a null-terminated copy of `name` and return a rooted `JS_Value_Resource`. +6. WHEN `set-property(name, val)` is called on a `JS_Value_Resource`, THE Engine SHALL call `JS_SetPropertyStr` with a null-terminated copy of `name` and the borrowed value's `JSValue`. +7. WHEN `call(args)` is called on a `JS_Value_Resource`, THE Engine SHALL push each argument via `JS_PushArg` and invoke `JS_Call(ctx, args.len)`, returning a rooted `JS_Value_Resource`. +8. THE type-checking methods (`is-int`, `is-bool`, `is-null`, `is-undefined`, `is-exception`, `is-number`, `is-string`, `is-error`, `is-function`) SHALL delegate to the corresponding `JS_Is*` inline or function from `mquickjs.h`. +9. THE conversion methods (`to-string`, `to-int32`, `to-float64`) SHALL delegate to `JS_ToCStringLen`, `JS_ToInt32`, and `JS_ToNumber` respectively. +10. WHEN `to-string` is called, THE Glue_Layer SHALL allocate the result string via `cabi_realloc(NULL, 0, 1, len)` and copy the C string bytes into it. + +--- + +### Requirement 6: String Memory Management + +**User Story:** As a developer, I want all string allocations across the component boundary to be correctly managed, so that there are no memory leaks or use-after-free errors. + +#### Acceptance Criteria + +1. THE Glue_Layer SHALL use `cabi_realloc` (provided by the wit-bindgen-generated `microquickjs.c`) for all string allocations returned to the host. +2. THE Glue_Layer SHALL use `JS_ToCStringLen` (not `JS_ToCString`) when the string length is needed for allocation, passing a stack-allocated `JSCStringBuf`. +3. THE Glue_Layer SHALL NOT call `JS_FreeCString` because MicroQuickJS uses arena memory and does not require it. +4. THE Glue_Layer SHALL NOT call `JS_FreeValue` because MicroQuickJS uses arena memory and does not require it. +5. WHEN constructing null-terminated C strings from WIT `string` parameters, THE Glue_Layer SHALL `malloc` a buffer of `len + 1`, copy the bytes, and append a null terminator, then `free` the buffer after use. + +--- + +### Requirement 7: WASI Compatibility Shims + +**User Story:** As a developer, I want the component to compile cleanly against the WASI target without missing symbols, so that the build does not fail due to unavailable POSIX APIs. + +#### Acceptance Criteria + +1. THE Glue_Layer SHALL provide stub implementations for stdlib-referenced host functions unavailable in WASI: `js_date_now`, `js_print`, `js_performance_now`, `js_gc`, `js_load`, `js_setTimeout`, and `js_clearTimeout`. Stubs SHALL return `JS_UNDEFINED` and perform no I/O or system calls. +2. THE Build_System SHALL NOT require source patches to `mquickjs.c` — MicroQuickJS's `setjmp`/`longjmp` usage compiles cleanly with WASI SDK without `#ifndef __wasi__` guards. +3. FOR Path A (wasip1): THE Build_System SHALL link with `-D_WASI_EMULATED_SIGNAL -lwasi-emulated-signal` to satisfy signal-related symbols. +4. FOR Path A (wasip1): THE Build_System SHALL link with `-lsetjmp` to provide the WASI-compatible `setjmp`/`longjmp` implementation. +5. FOR Path B (wasip2): THE Build_System SHALL use the wasip2 sysroot which provides `setjmp`/`longjmp` natively; no `-lsetjmp` or emulation flags are needed. +6. THE EH_Opcodes emitted by wasi-sdk clang (opcode `0x117` / `try_table`) for `setjmp`/`longjmp` lowering SHALL be treated as correct and expected output. THE Build_System SHALL NOT use `-fno-exceptions` or `-mno-exception-handling` to suppress them — those flags do not prevent LLVM's wasm32 EH lowering of C `setjmp` patterns and may break MicroQuickJS's error-recovery paths. +7. THE Build_System SHALL NOT call `JS_FreeContext` — MicroQuickJS's arena-based context does not require explicit freeing; the arena is static for the component instance lifetime. + +--- + +### Requirement 8: Runtime Validation (WAMR) + +**User Story:** As a developer, I want the component to be validated and executable on WAMR, so that there is a confirmed working runtime for deployment. + +#### Acceptance Criteria + +1. THE Component SHALL load and validate successfully under WAMR (iwasm) built with `-DWAMR_BUILD_COMPONENT_MODEL=1`. +2. WHEN `eval("2 + 2")` is invoked via WAMR, THE Component SHALL return `ok("4")`. +3. WHEN `eval("undefined")` is invoked via WAMR, THE Component SHALL return `ok("undefined")`. +4. WHEN `eval("throw new Error('boom')")` is invoked via WAMR, THE Component SHALL return `err("Error: boom")` or an equivalent error string. +5. WHEN `eval` is invoked with a syntax error such as `"{"`, THE Component SHALL return an `err` variant containing a non-empty error message. +6. THE Component SHALL pass all JavaScript test files in the `tests/` directory when evaluated via `eval` on WAMR, specifically: `test_closure.js`, `test_language.js`, `test_loop.js`, `test_builtin.js`, and `test_rect.js`. + +--- + +### Requirement 9: Component Model Structural Validity + +**User Story:** As a developer, I want the component binary to be structurally valid per the WASI 0.2 Component Model specification, so that it can be loaded by any compliant runtime. + +#### Acceptance Criteria + +1. THE Component SHALL pass `wasm-tools validate --features component-model build/microquickjs.component.wasm` without errors. +2. THE Component SHALL report the correct WIT interface when inspected with `wasm-tools component wit build/microquickjs.component.wasm`. +3. THE Component SHALL be a reactor (not a command) — it SHALL NOT export a `_start` function and SHALL export `__wasm_call_ctors` for initialization. +4. THE Component SHALL export `cabi_realloc` as required by the Component Model ABI for string and list passing. +5. THE Core_Module SHALL be composed with the WASI preview1 reactor adapter (`wasi_snapshot_preview1.reactor.wasm`) to satisfy WASI 0.2 import requirements. + +--- + +### Requirement 10: Size and Performance Constraints + +**User Story:** As an embedder, I want the component binary to be as small as practical, so that it is suitable for resource-constrained environments. + +#### Acceptance Criteria + +1. THE Build_System SHALL compile all C sources with `-Oz` (optimize for size) as the primary optimization flag. +2. THE Component binary size SHALL be measured and recorded after each build. The target is ≤200 KB; community builds of similar engines achieve ~148–200 KB with `-Oz` + `wasm-opt`. +3. THE JS_Context arena SHALL be at least 4 MiB to support evaluation of the test suite files without out-of-memory errors. +4. WHEN `eval` is called with the `tests/microbench.js` benchmark, THE Component SHALL complete execution without exceeding the arena memory limit. +5. THE Build_System SHALL optionally apply `wasm-opt -Oz` (Binaryen) as a post-processing step if `wasm-opt` is present on PATH. This step is not required for correctness but is recommended for production builds. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e428b88 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +# Build the component using Makefile.wasi +# This handles both native tool compilation and WASM cross-compilation +make -f Makefile.wasi clean +make -f Makefile.wasi diff --git a/glue.c b/glue.c new file mode 100644 index 0000000..860d380 --- /dev/null +++ b/glue.c @@ -0,0 +1,212 @@ +#include +#include +#include +#include +#include "mquickjs/mquickjs.h" + +// cabi_realloc is provided by wit-bindgen's microquickjs.c +void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size); + +// Minimal implementations of missing functions for WASI +JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } +JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } +JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } +JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } +JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } +JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } +JSValue js_clearTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { return JS_UNDEFINED; } + +#include "generated/microquickjs.h" +#include "mqjs_stdlib.h" + +struct exports_local_microquickjs_engine_js_value_t { + JSValue val; + JSGCRef root; +}; + +static uint8_t s_mem[4 * 1024 * 1024]; +static JSContext *s_ctx = NULL; + +static void ensure_context(void) { + if (s_ctx) return; + s_ctx = JS_NewContext(s_mem, sizeof(s_mem), &js_stdlib); +} + +static exports_local_microquickjs_engine_own_js_value_t make_own_value(JSValue val) { + exports_local_microquickjs_engine_js_value_t *rep = malloc(sizeof(*rep)); + rep->val = val; + JS_AddGCRef(s_ctx, &rep->root); + rep->root.val = val; + return exports_local_microquickjs_engine_js_value_new(rep); +} + +void exports_local_microquickjs_engine_js_value_destructor(exports_local_microquickjs_engine_js_value_t *rep) { + JS_DeleteGCRef(s_ctx, &rep->root); + free(rep); +} + +bool exports_local_microquickjs_engine_method_js_value_is_int(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsInt(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_bool(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsBool(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_null(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsNull(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_undefined(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsUndefined(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_exception(exports_local_microquickjs_engine_borrow_js_value_t self) { + return JS_IsException(self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_number(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsNumber(s_ctx, self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_string(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsString(s_ctx, self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_error(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsError(s_ctx, self->val); +} + +bool exports_local_microquickjs_engine_method_js_value_is_function(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + return JS_IsFunction(s_ctx, self->val); +} + +void exports_local_microquickjs_engine_method_js_value_to_string(exports_local_microquickjs_engine_borrow_js_value_t self, microquickjs_string_t *ret) { + ensure_context(); + size_t len; + JSCStringBuf buf; + const char *cstr = JS_ToCStringLen(s_ctx, &len, self->val, &buf); + if (!cstr) { + ret->ptr = NULL; + ret->len = 0; + return; + } + ret->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(ret->ptr, cstr, len); + ret->len = len; +} + +int32_t exports_local_microquickjs_engine_method_js_value_to_int32(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + int res; + JS_ToInt32(s_ctx, &res, self->val); + return res; +} + +double exports_local_microquickjs_engine_method_js_value_to_float64(exports_local_microquickjs_engine_borrow_js_value_t self) { + ensure_context(); + double res; + JS_ToNumber(s_ctx, &res, self->val); + return res; +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_method_js_value_get_property(exports_local_microquickjs_engine_borrow_js_value_t self, microquickjs_string_t *name) { + ensure_context(); + char *cstr = malloc(name->len + 1); + memcpy(cstr, name->ptr, name->len); + cstr[name->len] = '\0'; + JSValue res = JS_GetPropertyStr(s_ctx, self->val, cstr); + free(cstr); + return make_own_value(res); +} + +void exports_local_microquickjs_engine_method_js_value_set_property(exports_local_microquickjs_engine_borrow_js_value_t self, microquickjs_string_t *name, exports_local_microquickjs_engine_borrow_js_value_t val) { + ensure_context(); + char *cstr = malloc(name->len + 1); + memcpy(cstr, name->ptr, name->len); + cstr[name->len] = '\0'; + JS_SetPropertyStr(s_ctx, self->val, cstr, val->val); + free(cstr); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_method_js_value_call(exports_local_microquickjs_engine_borrow_js_value_t self, exports_local_microquickjs_engine_list_borrow_js_value_t *args) { + ensure_context(); + for (size_t i = 0; i < args->len; i++) { + JS_PushArg(s_ctx, args->ptr[i]->val); + } + JSValue res = JS_Call(s_ctx, args->len); + return make_own_value(res); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_int32(int32_t val) { + ensure_context(); + return make_own_value(JS_NewInt32(s_ctx, val)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_float64(double val) { + ensure_context(); + return make_own_value(JS_NewFloat64(s_ctx, val)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_bool(bool val) { + ensure_context(); + return make_own_value(JS_NewBool(val)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_string(microquickjs_string_t *val) { + ensure_context(); + return make_own_value(JS_NewStringLen(s_ctx, (const char *)val->ptr, val->len)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_object(void) { + ensure_context(); + return make_own_value(JS_NewObject(s_ctx)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_new_array(void) { + ensure_context(); + return make_own_value(JS_NewArray(s_ctx, 0)); +} + +exports_local_microquickjs_engine_own_js_value_t exports_local_microquickjs_engine_get_global_object(void) { + ensure_context(); + return make_own_value(JS_GetGlobalObject(s_ctx)); +} + +bool exports_local_microquickjs_engine_eval(microquickjs_string_t *code, microquickjs_string_t *ret, microquickjs_string_t *err) { + ensure_context(); + JSValue val = JS_Eval(s_ctx, (const char *)code->ptr, code->len, "", JS_EVAL_RETVAL); + + size_t len; + JSCStringBuf buf; + if (JS_IsException(val)) { + JSValue exc = JS_GetException(s_ctx); + const char *cstr = JS_ToCStringLen(s_ctx, &len, exc, &buf); + if (!cstr) { + err->ptr = cabi_realloc(NULL, 0, 1, 24); + memcpy(err->ptr, "Error: unknown exception", 24); + err->len = 24; + return false; + } + err->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(err->ptr, cstr, len); + err->len = len; + return false; + } + + const char *cstr = JS_ToCStringLen(s_ctx, &len, val, &buf); + if (!cstr) { + ret->ptr = cabi_realloc(NULL, 0, 1, 9); + memcpy(ret->ptr, "undefined", 9); + ret->len = 9; + return true; + } + ret->ptr = cabi_realloc(NULL, 0, 1, len); + memcpy(ret->ptr, cstr, len); + ret->len = len; + return true; +} diff --git a/microquickjs.wit b/microquickjs.wit new file mode 100644 index 0000000..0d32367 --- /dev/null +++ b/microquickjs.wit @@ -0,0 +1,41 @@ +package local:microquickjs; + +interface engine { + resource js-value { + is-int: func() -> bool; + is-bool: func() -> bool; + is-null: func() -> bool; + is-undefined: func() -> bool; + is-exception: func() -> bool; + is-number: func() -> bool; + is-string: func() -> bool; + is-error: func() -> bool; + is-function: func() -> bool; + + to-string: func() -> string; + to-int32: func() -> s32; + to-float64: func() -> f64; + + get-property: func(name: string) -> js-value; + set-property: func(name: string, val: borrow); + + call: func(args: list>) -> js-value; + } + + new-int32: func(val: s32) -> js-value; + new-float64: func(val: f64) -> js-value; + new-bool: func(val: bool) -> js-value; + new-string: func(val: string) -> js-value; + new-object: func() -> js-value; + new-array: func() -> js-value; + + get-global-object: func() -> js-value; + + /// Evaluate JavaScript code and return result as string. + /// On error (syntax, runtime), returns Err(error-message). + eval: func(code: string) -> result; +} + +world microquickjs { + export engine; +} diff --git a/cutils.c b/mquickjs/cutils.c similarity index 100% rename from cutils.c rename to mquickjs/cutils.c diff --git a/cutils.h b/mquickjs/cutils.h similarity index 100% rename from cutils.h rename to mquickjs/cutils.h diff --git a/dtoa.c b/mquickjs/dtoa.c similarity index 100% rename from dtoa.c rename to mquickjs/dtoa.c diff --git a/dtoa.h b/mquickjs/dtoa.h similarity index 100% rename from dtoa.h rename to mquickjs/dtoa.h diff --git a/example.c b/mquickjs/example.c similarity index 99% rename from example.c rename to mquickjs/example.c index 5385ede..55a49cd 100644 --- a/example.c +++ b/mquickjs/example.c @@ -137,7 +137,7 @@ static JSValue js_filled_rectangle_constructor(JSContext *ctx, JSValue *this_val if (!(argc & FRAME_CF_CTOR)) return JS_ThrowTypeError(ctx, "must be called with new"); obj = JS_PushGCRef(ctx, &obj_ref); - + argc &= ~FRAME_CF_CTOR; *obj = JS_NewObjectClassUser(ctx, JS_CLASS_FILLED_RECTANGLE); d = malloc(sizeof(*d)); @@ -172,7 +172,7 @@ static JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *ar { int i; JSValue v; - + for(i = 0; i < argc; i++) { if (i != 0) putchar(' '); @@ -257,7 +257,7 @@ int main(int argc, const char **argv) JSContext *ctx; const char *filename; JSValue val; - + if (argc < 2) { printf("usage: example script.js\n"); exit(1); @@ -269,7 +269,7 @@ int main(int argc, const char **argv) mem_buf = malloc(mem_size); ctx = JS_NewContext(mem_buf, mem_size, &js_stdlib); JS_SetLogFunc(ctx, js_log_func); - + buf = load_file(filename, &buf_len); val = JS_Eval(ctx, (const char *)buf, buf_len, filename, 0); free(buf); @@ -280,7 +280,7 @@ int main(int argc, const char **argv) printf("\n"); exit(1); } - + JS_FreeContext(ctx); free(mem_buf); return 0; diff --git a/example_stdlib.c b/mquickjs/example_stdlib.c similarity index 100% rename from example_stdlib.c rename to mquickjs/example_stdlib.c diff --git a/libm.c b/mquickjs/libm.c similarity index 100% rename from libm.c rename to mquickjs/libm.c diff --git a/libm.h b/mquickjs/libm.h similarity index 100% rename from libm.h rename to mquickjs/libm.h diff --git a/list.h b/mquickjs/list.h similarity index 100% rename from list.h rename to mquickjs/list.h diff --git a/mqjs.c b/mquickjs/mqjs.c similarity index 99% rename from mqjs.c rename to mquickjs/mqjs.c index 46ad953..96307b9 100644 --- a/mqjs.c +++ b/mquickjs/mqjs.c @@ -46,7 +46,7 @@ static JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *ar { int i; JSValue v; - + for(i = 0; i < argc; i++) { if (i != 0) putchar(' '); @@ -107,7 +107,7 @@ static JSValue js_load(JSContext *ctx, JSValue *this_val, int argc, JSValue *arg uint8_t *buf; int buf_len; JSValue ret; - + filename = JS_ToCString(ctx, argv[0], &buf_str); if (!filename) return JS_EXCEPTION; @@ -134,7 +134,7 @@ static JSValue js_setTimeout(JSContext *ctx, JSValue *this_val, int argc, JSValu JSTimer *th; int delay, i; JSValue *pfunc; - + if (!JS_IsFunction(ctx, argv[0])) return JS_ThrowTypeError(ctx, "not a function"); if (JS_ToInt32(ctx, &delay, argv[1])) @@ -193,10 +193,10 @@ static void run_timers(JSContext *ctx) goto fail; JS_PushArg(ctx, th->func.val); /* func name */ JS_PushArg(ctx, JS_NULL); /* this */ - + JS_DeleteGCRef(ctx, &th->func); th->allocated = FALSE; - + ret = JS_Call(ctx, 0); if (JS_IsException(ret)) { fail: @@ -280,7 +280,7 @@ static int eval_buf(JSContext *ctx, const char *eval_str, const char *filename, { JSValue val; int flags; - + flags = parse_flags; if (is_repl) flags |= JS_EVAL_RETVAL | JS_EVAL_REPL; @@ -310,7 +310,7 @@ static int eval_file(JSContext *ctx, const char *filename, uint8_t *buf; int ret, buf_len; JSValue val; - + buf = load_file(filename, &buf_len); if (allow_bytecode && JS_IsBytecode(buf, buf_len)) { if (JS_RelocateBytecode(ctx, buf, buf_len)) { @@ -328,7 +328,7 @@ static int eval_file(JSContext *ctx, const char *filename, JSValue obj, arr; JSGCRef arr_ref, val_ref; int i; - + JS_PUSH_VALUE(ctx, val); /* must be defined after JS_LoadBytecode() */ arr = JS_NewArray(ctx, argc); @@ -342,8 +342,8 @@ static int eval_file(JSContext *ctx, const char *filename, JS_SetPropertyStr(ctx, obj, "scriptArgs", arr); JS_POP_VALUE(ctx, val); } - - + + val = JS_Run(ctx, val); if (JS_IsException(val)) { exception: @@ -373,7 +373,7 @@ static void compile_file(const char *filename, const char *outfilename, const uint8_t *data_buf; uint32_t data_len; FILE *f; - + /* When compiling to a file, the actual content of the stdlib does not matter because the generated bytecode does not depend on it. We still need it so that the atoms for the parsing are @@ -392,7 +392,7 @@ static void compile_file(const char *filename, const char *outfilename, return; } -#if JSW == 8 +#if JSW == 8 if (force_32bit) { if (JS_PrepareBytecode64to32(ctx, &hdr_buf.hdr32, &data_buf, &data_len, val)) { fprintf(stderr, "Could not convert the bytecode from 64 to 32 bits\n"); @@ -403,10 +403,10 @@ static void compile_file(const char *filename, const char *outfilename, #endif { JS_PrepareBytecode(ctx, &hdr_buf.hdr, &data_buf, &data_len, val); - + if (dump_memory) JS_DumpMemory(ctx, (dump_memory >= 2)); - + /* Relocate to zero to have a deterministic output. JS_DumpMemory() cannot work once the heap is relocated, so we relocate after it. */ @@ -421,7 +421,7 @@ static void compile_file(const char *filename, const char *outfilename, fwrite(&hdr_buf, 1, hdr_len, f); fwrite(data_buf, 1, data_len, f); fclose(f); - + JS_FreeContext(ctx); free(mem_buf); } @@ -443,7 +443,7 @@ static BOOL is_word(int c) c == '_' || c == '$'; } -static const char js_keywords[] = +static const char js_keywords[] = "break|case|catch|continue|debugger|default|delete|do|" "else|finally|for|function|if|in|instanceof|new|" "return|switch|this|throw|try|typeof|while|with|" @@ -595,13 +595,13 @@ int main(int argc, const char **argv) JSContext *ctx; int i, parse_flags; BOOL force_32bit, allow_bytecode; - + mem_size = 16 << 20; dump_memory = 0; parse_flags = 0; force_32bit = FALSE; allow_bytecode = FALSE; - + /* cannot use getopt because we want to pass the command line to the script */ optind = 1; @@ -741,7 +741,7 @@ int main(int argc, const char **argv) goto fail; } } - + if (expr) { if (eval_buf(ctx, expr, "", FALSE, parse_flags | JS_EVAL_REPL)) goto fail; @@ -753,16 +753,16 @@ int main(int argc, const char **argv) goto fail; } } - + if (interactive) { repl_run(ctx); } else { run_timers(ctx); } - + if (dump_memory) JS_DumpMemory(ctx, (dump_memory >= 2)); - + JS_FreeContext(ctx); free(mem_buf); } diff --git a/mqjs_stdlib.c b/mquickjs/mqjs_stdlib.c similarity index 100% rename from mqjs_stdlib.c rename to mquickjs/mqjs_stdlib.c diff --git a/mquickjs.c b/mquickjs/mquickjs.c similarity index 99% rename from mquickjs.c rename to mquickjs/mquickjs.c index a950f3c..e8041f5 100644 --- a/mquickjs.c +++ b/mquickjs/mquickjs.c @@ -23,6 +23,8 @@ * THE SOFTWARE. */ #include +#include +#include #include #include #include diff --git a/mquickjs.h b/mquickjs/mquickjs.h similarity index 100% rename from mquickjs.h rename to mquickjs/mquickjs.h diff --git a/mquickjs_build.c b/mquickjs/mquickjs_build.c similarity index 99% rename from mquickjs_build.c rename to mquickjs/mquickjs_build.c index 6173271..0f19f02 100644 --- a/mquickjs_build.c +++ b/mquickjs/mquickjs_build.c @@ -286,7 +286,7 @@ static int atom_cmp(const void *p1, const void *p2) /* js_atom_table must be properly aligned because the property hash table uses the low bits of the atom pointer value */ -#define ATOM_ALIGN 64 +#define ATOM_ALIGN 256 static void dump_atoms(BuildContext *ctx) { diff --git a/mquickjs_build.h b/mquickjs/mquickjs_build.h similarity index 100% rename from mquickjs_build.h rename to mquickjs/mquickjs_build.h diff --git a/mquickjs_opcode.h b/mquickjs/mquickjs_opcode.h similarity index 100% rename from mquickjs_opcode.h rename to mquickjs/mquickjs_opcode.h diff --git a/mquickjs_priv.h b/mquickjs/mquickjs_priv.h similarity index 100% rename from mquickjs_priv.h rename to mquickjs/mquickjs_priv.h diff --git a/readline.c b/mquickjs/readline.c similarity index 100% rename from readline.c rename to mquickjs/readline.c diff --git a/readline.h b/mquickjs/readline.h similarity index 100% rename from readline.h rename to mquickjs/readline.h diff --git a/readline_tty.c b/mquickjs/readline_tty.c similarity index 97% rename from readline_tty.c rename to mquickjs/readline_tty.c index 9a7e929..fb7e045 100644 --- a/readline_tty.c +++ b/mquickjs/readline_tty.c @@ -38,12 +38,12 @@ #include #include #else -#include #include +#ifndef __wasi__ +#include #include #include #endif - #include "readline_tty.h" static int ctrl_c_pressed; @@ -109,6 +109,7 @@ static void set_processed_input(BOOL enable) #else /* init terminal so that we can grab keys */ /* XXX: merge with cp_utils.c */ +#ifndef __wasi__ static struct termios oldtty; static int old_fd0_flags; @@ -126,13 +127,17 @@ static void sigint_handler(int signo) signal(SIGINT, SIG_DFL); } } +#endif int readline_tty_init(void) { + int n_cols; + n_cols = 80; + +#ifndef __wasi__ struct termios tty; struct sigaction sa; struct winsize ws; - int n_cols; tcgetattr (0, &tty); oldtty = tty; @@ -159,11 +164,11 @@ int readline_tty_init(void) atexit(term_exit); // fcntl(0, F_SETFL, O_NONBLOCK); - n_cols = 80; if (ioctl(0, TIOCGWINSZ, &ws) == 0 && ws.ws_col >= 4 && ws.ws_row >= 4) { n_cols = ws.ws_col; } +#endif return n_cols; } #endif @@ -200,6 +205,11 @@ const char *readline_tty(ReadlineState *s, len = read(0, buf, sizeof(buf)); if (len == 0) break; + if (len < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + break; + } for(i = 0; i < len; i++) { c = buf[i]; #ifdef _WIN32 diff --git a/readline_tty.h b/mquickjs/readline_tty.h similarity index 100% rename from readline_tty.h rename to mquickjs/readline_tty.h diff --git a/softfp_template.h b/mquickjs/softfp_template.h similarity index 100% rename from softfp_template.h rename to mquickjs/softfp_template.h diff --git a/softfp_template_icvt.h b/mquickjs/softfp_template_icvt.h similarity index 100% rename from softfp_template_icvt.h rename to mquickjs/softfp_template_icvt.h diff --git a/packages/WasmEdge b/packages/WasmEdge new file mode 160000 index 0000000..77c5238 --- /dev/null +++ b/packages/WasmEdge @@ -0,0 +1 @@ +Subproject commit 77c5238980b3c9cd8fc7e7dc69b20127d9fd68ca diff --git a/packages/wasi-sdk b/packages/wasi-sdk new file mode 160000 index 0000000..003cf14 --- /dev/null +++ b/packages/wasi-sdk @@ -0,0 +1 @@ +Subproject commit 003cf14969ecca789c1922f9047e9a31872e9b52 diff --git a/packages/wasm-micro-runtime b/packages/wasm-micro-runtime new file mode 160000 index 0000000..389d206 --- /dev/null +++ b/packages/wasm-micro-runtime @@ -0,0 +1 @@ +Subproject commit 389d2060dfddb2fe083740f8b4a9666dd930fd69 diff --git a/restore_mquickjs.py b/restore_mquickjs.py new file mode 100644 index 0000000..853eb9e --- /dev/null +++ b/restore_mquickjs.py @@ -0,0 +1,13 @@ +import re + +with open('mquickjs/mquickjs.c', 'r') as f: + content = f.read() + +# Remove the WASI PATCHED comment and the guards +content = content.replace("/* WASI PATCHED */\n", "") +# Use a more robust regex to remove the guards I added +content = re.sub(r'#ifndef __wasi__\n#include \n#include \n#endif\n', + '#include \n#include \n', content) + +with open('mquickjs/mquickjs.c', 'w') as f: + f.write(content) diff --git a/test_component.js b/test_component.js new file mode 100644 index 0000000..80b40f8 --- /dev/null +++ b/test_component.js @@ -0,0 +1,8 @@ +import { eval as jsEval } from 'local:microquickjs/engine'; + +const result = jsEval('2 + 2'); +if (result.tag === 'ok') { + console.log('Test OK: ' + result.val); +} else { + console.log('Test Error: ' + result.val); +} diff --git a/wasi_shims/sys/wait.h b/wasi_shims/sys/wait.h new file mode 100644 index 0000000..e69de29