- blocking stdlib calls that PARK (net.accept, socket read/write, time.sleep, a child wait) no longer restart the syscall when the stop flag is set on an interruption: a server sitting in accept ignored SIGTERM and only `kill -9` ended it - a stop is NOT a trap -- builtin.h's WO_SYS_STOPPED carries no error record and no catch handler sees it (`try` must not swallow SIGTERM); the VM unwinds the whole stack through the same drop machinery an uncaught trap uses, so nothing leaks on the way out - wo_vm_call gained a third outcome (1 = stopped); the CLI maps it to the status the program's own `return 0` would have given, and a regular-file read keeps its plain EINTR retry -- it does not park - an ASSIGNMENT was not an ownership boundary: `api_key = j.mcp.apiKey` moved the field pointer into the local, so the local aliased the record and the first unwind freed the same string twice (SIGSEGV in class_free). `let` copied a Text place, assignment now does too -- the same double free was latent on the normal exit path, hidden by the order the compiler happens to emit drops in - log-watcher-accept is 7 checks: the seventh is the stop itself, with the hard kill demoted to a fallback whose use is the failure - measured under ASan: mcp parked, mcp after traffic, watch and run all exit rc 0 with zero leaks; SIGINT behaves as SIGTERM - gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 565/0, wovm-test green, log-watcher 7/0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
602 lines
22 KiB
C
602 lines
22 KiB
C
#include "vm.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "borrow.h"
|
|
#include "builtin.h"
|
|
#include "cont.h"
|
|
#include "gc.h"
|
|
|
|
uint32_t wo_vm_depth(const wo_vm *vm) { return vm->depth; }
|
|
|
|
int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) {
|
|
memset(vm, 0, sizeof(*vm));
|
|
vm->mod = mod;
|
|
return wo_rt_init(&vm->rt, heap_cap, mod->classes, mod->class_cnt);
|
|
}
|
|
|
|
void wo_vm_destroy(wo_vm *vm) { wo_rt_destroy(&vm->rt); }
|
|
|
|
/* The drop-table entry governing instruction [pc]: the last one recorded
|
|
* at or before it. NULL = nothing live there. */
|
|
static const wo_dropent *vm_dropent(const wo_methodrec *me, uint32_t pc) {
|
|
const wo_dropent *ent = NULL;
|
|
for (uint32_t i = 0; i < me->drop_cnt && me->drops[i].pc <= pc; i++) ent = &me->drops[i];
|
|
return ent;
|
|
}
|
|
|
|
/* Release what frame [d-1] owns at [pc] but no longer owns at [keep_pc] —
|
|
* the values the abandoned region of that frame created. [keep_pc] =
|
|
* UINT32_MAX means "keep nothing", which is the dying-frame case every
|
|
* uncaught trap uses. A borrow held by a dying register does not block
|
|
* its drop — the borrower IS the dying region. */
|
|
static void vm_release_frame(wo_vm *vm, uint32_t d, uint32_t pc, uint32_t keep_pc) {
|
|
const wo_frame *f = &vm->frames[d - 1];
|
|
const wo_methodrec *me = &vm->mod->methods[f->method];
|
|
const wo_dropent *ent = vm_dropent(me, pc);
|
|
if (!ent) return; /* no entry: nothing live in this frame */
|
|
uint64_t keep_owned = 0, keep_gc = 0;
|
|
if (keep_pc != UINT32_MAX) {
|
|
const wo_dropent *k = vm_dropent(me, keep_pc);
|
|
if (k) {
|
|
keep_owned = k->owned;
|
|
keep_gc = k->gc;
|
|
}
|
|
}
|
|
uint64_t *R = vm->regs + f->base;
|
|
for (uint32_t r = 0; r < me->reg_cnt; r++) {
|
|
uint64_t bit = 1ull << r;
|
|
if ((ent->owned & bit) && !(keep_owned & bit) && R[r]) {
|
|
wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
|
R[r] = 0;
|
|
}
|
|
if ((ent->gc & bit) && !(keep_gc & bit) && R[r]) {
|
|
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
|
R[r] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Trap unwinding — the spec's "traps never leak" promise (spec §6). Walk
|
|
* frames innermost to outermost down to (not including) [stop_depth];
|
|
* in each, the governing instruction is the trap pc for the innermost
|
|
* frame and the instruction before the saved resume pc — i.e. the CALL —
|
|
* for every outer frame. Window overlap is safe: a slot dropped by the
|
|
* callee frame is nulled, so an outer mask covering the same physical
|
|
* slot sees 0 and skips. stop_depth is 0 for an uncaught trap (the whole
|
|
* stack dies) and the catching frame's depth for a caught one. */
|
|
static void vm_unwind(wo_vm *vm, uint32_t stop_depth) {
|
|
for (uint32_t d = vm->depth; d > stop_depth; d--) {
|
|
const wo_frame *f = &vm->frames[d - 1];
|
|
vm_release_frame(vm, d, (d == vm->depth) ? f->pc : f->pc - 1, UINT32_MAX);
|
|
}
|
|
vm->depth = stop_depth;
|
|
}
|
|
|
|
/* Residual runtime checks the loader cannot do statically (registers are
|
|
* untyped): non-null receiver, an actual class object (not a native
|
|
* sentinel), field index inside the class. NULL return = trap BOUNDS with
|
|
* *why naming the reason. */
|
|
static wo_hdr *recv_check(wo_vm *vm, uint64_t v, uint32_t fidx,
|
|
const char **why) {
|
|
if (!v) {
|
|
*why = "null receiver";
|
|
return NULL;
|
|
}
|
|
wo_hdr *o = (wo_hdr *)(uintptr_t)v;
|
|
if (o->class_id >= vm->mod->class_cnt) {
|
|
*why = "native object has no fields";
|
|
return NULL;
|
|
}
|
|
if (fidx >= vm->mod->classes[o->class_id].field_cnt) {
|
|
*why = "field index out of range";
|
|
return NULL;
|
|
}
|
|
return o;
|
|
}
|
|
|
|
static wo_str *str_check(uint64_t v, const char **why) {
|
|
if (!v) {
|
|
*why = "null text";
|
|
return NULL;
|
|
}
|
|
wo_str *s = (wo_str *)(uintptr_t)v;
|
|
if (s->h.class_id != WO_CLS_STR) {
|
|
*why = "not a text value";
|
|
return NULL;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/* Fills [out] with the trap's structured error (spec §6): the code, the
|
|
* source line of the trapping pc, the trapping method's name, and the
|
|
* message. One shape forever — the CLI prints it, and the catch arm of a
|
|
* `try` binds exactly the same four fields. */
|
|
static void vm_fill_err(wo_vm *vm, wo_err *out, uint32_t tcode, const char *fmt, va_list ap) {
|
|
const wo_module *mod = vm->mod;
|
|
const wo_frame *f = &vm->frames[vm->depth - 1];
|
|
const wo_methodrec *me = &mod->methods[f->method];
|
|
out->code = tcode;
|
|
out->line = 0; /* last line-table entry with pc <= trapping pc */
|
|
for (uint32_t i = 0; i < me->line_cnt && me->lines[i].pc <= f->pc; i++)
|
|
out->line = me->lines[i].line;
|
|
const wo_str *nm = mod->consts[me->name].s;
|
|
int nlen = nm->len < 63 ? (int)nm->len : 63;
|
|
snprintf(out->method, sizeof(out->method), "%.*s", nlen, nm->data);
|
|
vsnprintf(out->msg, sizeof(out->msg), fmt, ap);
|
|
}
|
|
|
|
/* 0 = the trap was caught: the stack is unwound down to the catching
|
|
* frame, that frame's pc now points at the handler, and the caller must
|
|
* reload and keep interpreting. -1 = uncaught: *err is filled and the
|
|
* stack is fully unwound (depth 0), exactly as before Task 5. */
|
|
static int vm_trap(wo_vm *vm, wo_err *err, uint32_t tcode, const char *fmt,
|
|
...) {
|
|
/* The record the catch arm reads is always filled, even when the
|
|
* caller passed no err: it is the value `catch (e)` binds. */
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vm_fill_err(vm, &vm->caught, tcode, fmt, ap);
|
|
va_end(ap);
|
|
if (vm->ncatch) {
|
|
const wo_catch *c = &vm->catches[vm->ncatch - 1];
|
|
uint32_t cdepth = c->depth;
|
|
uint32_t hpc = c->pc;
|
|
/* Which instruction governs the catching frame's own live set has
|
|
* to be decided before unwinding moves the depth: the trapping
|
|
* instruction when the trap was raised in this very frame, the
|
|
* CALL (pc - 1, the saved pc is the resume point) when it came
|
|
* from deeper. */
|
|
int trapped_here = (cdepth == vm->depth);
|
|
vm->ncatch--;
|
|
/* frames above the catching one die whole */
|
|
vm_unwind(vm, cdepth);
|
|
/* in the catching frame only the try region's own values die: the
|
|
* handler's drop entry names what survives into the catch arm */
|
|
wo_frame *cf = &vm->frames[cdepth - 1];
|
|
vm_release_frame(vm, cdepth, trapped_here ? cf->pc : cf->pc - 1, hpc);
|
|
cf->pc = hpc;
|
|
return 0;
|
|
}
|
|
if (err) *err = vm->caught;
|
|
vm_unwind(vm, 0);
|
|
return -1;
|
|
}
|
|
|
|
static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
|
const wo_module *mod = vm->mod;
|
|
const wo_methodrec *me;
|
|
const uint32_t *code;
|
|
uint32_t pc;
|
|
uint64_t *R;
|
|
uint32_t ins = 0;
|
|
|
|
#define RELOAD() \
|
|
do { \
|
|
me = &mod->methods[vm->frames[vm->depth - 1].method]; \
|
|
code = me->code; \
|
|
pc = vm->frames[vm->depth - 1].pc; \
|
|
R = vm->regs + vm->frames[vm->depth - 1].base; \
|
|
} while (0)
|
|
|
|
/* pc is post-incremented at dispatch: the trapping instruction is pc-1.
|
|
* A caught trap (vm_trap == 0) has already unwound to the handler's frame
|
|
* and pointed it at the handler, so the interpreter just reloads and
|
|
* keeps going — the same macro serves both surfaces. */
|
|
#define TRAPF(tcode, ...) \
|
|
do { \
|
|
vm->frames[vm->depth - 1].pc = pc - 1; \
|
|
if (vm_trap(vm, err, tcode, __VA_ARGS__) == 0) { \
|
|
RELOAD(); \
|
|
NEXT(); \
|
|
} \
|
|
return -1; \
|
|
} while (0)
|
|
|
|
RELOAD();
|
|
|
|
/* dual-flavor dispatch, one shared case-body text (spec §5): computed
|
|
* goto under GNU C, plain switch under -DWO_ISO_C — the ISO flavor has
|
|
* its own make target so the fallback can never rot */
|
|
#ifndef WO_ISO_C
|
|
static const void *JT[WOP_MAX + 1] = {
|
|
[WOP_NOP] = &&L_NOP, [WOP_LOADK] = &&L_LOADK,
|
|
[WOP_MOVE] = &&L_MOVE, [WOP_ADD] = &&L_ADD,
|
|
[WOP_SUB] = &&L_SUB, [WOP_MUL] = &&L_MUL,
|
|
[WOP_DIV] = &&L_DIV, [WOP_NEG] = &&L_NEG,
|
|
[WOP_CONCAT] = &&L_CONCAT, [WOP_EQ] = &&L_EQ,
|
|
[WOP_LT] = &&L_LT, [WOP_LE] = &&L_LE,
|
|
[WOP_EQS] = &&L_EQS, [WOP_JMP] = &&L_JMP,
|
|
[WOP_JZ] = &&L_JZ, [WOP_CALL] = &&L_CALL,
|
|
[WOP_ICALL] = &&L_ICALL, [WOP_RET] = &&L_RET,
|
|
[WOP_RET0] = &&L_RET0, [WOP_NEW] = &&L_NEW,
|
|
[WOP_GETF] = &&L_GETF, [WOP_SETF] = &&L_SETF,
|
|
[WOP_DROP] = &&L_DROP, [WOP_BORROW_S] = &&L_BORROW_S,
|
|
[WOP_BORROW_X] = &&L_BORROW_X, [WOP_RELEASE_S] = &&L_RELEASE_S,
|
|
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_RC_INC] = &&L_RC_INC,
|
|
[WOP_RC_DEC] = &&L_RC_DEC, [WOP_BUILTIN] = &&L_BUILTIN,
|
|
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
|
[WOP_TRY] = &&L_TRY, [WOP_ENDTRY] = &&L_ENDTRY,
|
|
};
|
|
#define CASE(name) L_##name
|
|
#define NEXT() \
|
|
do { \
|
|
ins = code[pc++]; \
|
|
goto *JT[wo_ins_op(ins)]; \
|
|
} while (0)
|
|
NEXT();
|
|
#else
|
|
#define CASE(name) case WOP_##name
|
|
#define NEXT() goto dispatch
|
|
dispatch:
|
|
ins = code[pc++];
|
|
switch (wo_ins_op(ins)) {
|
|
#endif
|
|
|
|
CASE(NOP) : NEXT();
|
|
|
|
CASE(LOADK) : {
|
|
const wo_const *k = &mod->consts[wo_ins_bx(ins)];
|
|
R[wo_ins_a(ins)] = k->tag == WOB_K_INT ? (uint64_t)k->i
|
|
: (uint64_t)(uintptr_t)k->s;
|
|
NEXT();
|
|
}
|
|
|
|
CASE(MOVE) : {
|
|
/* for owned values this IS the move: the compiler guarantees the
|
|
* source register is dead afterwards */
|
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)];
|
|
NEXT();
|
|
}
|
|
|
|
/* i64 arithmetic: two's-complement wrapping via unsigned math */
|
|
CASE(ADD) : {
|
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] + R[wo_ins_c(ins)];
|
|
NEXT();
|
|
}
|
|
CASE(SUB) : {
|
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] - R[wo_ins_c(ins)];
|
|
NEXT();
|
|
}
|
|
CASE(MUL) : {
|
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] * R[wo_ins_c(ins)];
|
|
NEXT();
|
|
}
|
|
CASE(DIV) : {
|
|
int64_t x = (int64_t)R[wo_ins_b(ins)], y = (int64_t)R[wo_ins_c(ins)];
|
|
if (y == 0) TRAPF(WO_T_DIV0, "division by zero");
|
|
if (x == INT64_MIN && y == -1)
|
|
TRAPF(WO_T_DIV0, "INT64_MIN / -1 overflows");
|
|
R[wo_ins_a(ins)] = (uint64_t)(x / y);
|
|
NEXT();
|
|
}
|
|
CASE(NEG) : {
|
|
R[wo_ins_a(ins)] = 0u - R[wo_ins_b(ins)];
|
|
NEXT();
|
|
}
|
|
|
|
CASE(EQ) : {
|
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] == R[wo_ins_c(ins)] ? 1 : 0;
|
|
NEXT();
|
|
}
|
|
CASE(LT) : {
|
|
R[wo_ins_a(ins)] =
|
|
(int64_t)R[wo_ins_b(ins)] < (int64_t)R[wo_ins_c(ins)] ? 1 : 0;
|
|
NEXT();
|
|
}
|
|
CASE(LE) : {
|
|
R[wo_ins_a(ins)] =
|
|
(int64_t)R[wo_ins_b(ins)] <= (int64_t)R[wo_ins_c(ins)] ? 1 : 0;
|
|
NEXT();
|
|
}
|
|
|
|
CASE(JMP) : {
|
|
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
|
NEXT();
|
|
}
|
|
CASE(JZ) : {
|
|
if (R[wo_ins_a(ins)] == 0)
|
|
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
|
NEXT();
|
|
}
|
|
|
|
CASE(CALL) : {
|
|
/* Lua-style window overlap: callee r0 = caller slot A; args sit at
|
|
* A..A+argc-1; the return value lands back in slot A */
|
|
const wo_methodrec *callee = &mod->methods[wo_ins_bx(ins)];
|
|
uint32_t nbase = vm->frames[vm->depth - 1].base + wo_ins_a(ins);
|
|
if (vm->depth >= WO_MAX_FRAMES)
|
|
TRAPF(WO_T_STACK, "frame stack overflow (%u frames)",
|
|
(unsigned)WO_MAX_FRAMES);
|
|
if (nbase + callee->reg_cnt > WO_STACK_SLOTS)
|
|
TRAPF(WO_T_STACK, "value stack overflow");
|
|
vm->frames[vm->depth - 1].pc = pc;
|
|
vm->frames[vm->depth].method = wo_ins_bx(ins);
|
|
vm->frames[vm->depth].pc = 0;
|
|
vm->frames[vm->depth].base = nbase;
|
|
vm->depth++;
|
|
/* zero non-argument registers: drop masks must never see stale bits */
|
|
memset(vm->regs + nbase + callee->arg_cnt, 0,
|
|
(size_t)(callee->reg_cnt - callee->arg_cnt) * 8u);
|
|
RELOAD();
|
|
NEXT();
|
|
}
|
|
|
|
/* A frame leaving takes its still-open try regions with it: a `return`
|
|
* out of a try region never runs its ENDTRY, and a handler pc in a frame
|
|
* that no longer exists would land the next trap on a dead window. */
|
|
#define DROP_CATCHES() \
|
|
while (vm->ncatch && vm->catches[vm->ncatch - 1].depth > vm->depth) \
|
|
vm->ncatch--
|
|
|
|
CASE(RET) : {
|
|
uint64_t rv = R[wo_ins_a(ins)];
|
|
vm->regs[vm->frames[vm->depth - 1].base] = rv;
|
|
vm->depth--;
|
|
DROP_CATCHES();
|
|
if (vm->depth == 0) {
|
|
*ret = rv;
|
|
return 0;
|
|
}
|
|
RELOAD();
|
|
NEXT();
|
|
}
|
|
CASE(RET0) : {
|
|
vm->regs[vm->frames[vm->depth - 1].base] = 0;
|
|
vm->depth--;
|
|
DROP_CATCHES();
|
|
if (vm->depth == 0) {
|
|
*ret = 0;
|
|
return 0;
|
|
}
|
|
RELOAD();
|
|
NEXT();
|
|
}
|
|
|
|
CASE(NEW) : {
|
|
wo_hdr *o = wo_obj_new(&vm->rt, wo_ins_bx(ins));
|
|
if (!o) TRAPF(WO_T_OOM, "out of memory");
|
|
R[wo_ins_a(ins)] = (uint64_t)(uintptr_t)o;
|
|
NEXT();
|
|
}
|
|
|
|
CASE(GETF) : {
|
|
const char *why;
|
|
wo_hdr *o = recv_check(vm, R[wo_ins_b(ins)], wo_ins_c(ins), &why);
|
|
if (!o) TRAPF(WO_T_BOUNDS, "%s", why);
|
|
R[wo_ins_a(ins)] = wo_fields(o)[wo_ins_c(ins)];
|
|
NEXT();
|
|
}
|
|
|
|
CASE(SETF) : {
|
|
/* overwriting a non-scalar field does NOT auto-drop the old value:
|
|
* the compiler emits the drop (format doc).
|
|
*
|
|
* A TEXT field is COPIED into (2026-08-14), the same rule push/set
|
|
* follow: the field's kind makes the object the owner of that string,
|
|
* so storing a pointer the caller still owns would give it two owners.
|
|
* It is also what lets `self.name = name` — a borrowed Text parameter
|
|
* stored in a field, the most ordinary line there is — stay legal
|
|
* without demanding `take`. A freshly built Text handed to a field is
|
|
* still the caller's, and the compiler drops it at the store site. */
|
|
const char *why;
|
|
wo_hdr *o = recv_check(vm, R[wo_ins_a(ins)], wo_ins_b(ins), &why);
|
|
if (!o) TRAPF(WO_T_BOUNDS, "%s", why);
|
|
uint64_t v = R[wo_ins_c(ins)];
|
|
if (v && vm->mod->classes[o->class_id].kinds[wo_ins_b(ins)] == WO_K_TEXT) {
|
|
const wo_str *src = (const wo_str *)(uintptr_t)v;
|
|
if (src->h.class_id != WO_CLS_STR) TRAPF(WO_T_BOUNDS, "not a text value");
|
|
wo_str *cp = wo_str_new(&vm->rt, src->data, src->len);
|
|
if (!cp) TRAPF(WO_T_OOM, "out of memory");
|
|
v = (uint64_t)(uintptr_t)cp;
|
|
}
|
|
wo_fields(o)[wo_ins_b(ins)] = v;
|
|
NEXT();
|
|
}
|
|
|
|
CASE(DROP) : {
|
|
uint64_t v = R[wo_ins_a(ins)];
|
|
if (v) wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)v);
|
|
R[wo_ins_a(ins)] = 0; /* unwinding must never double-free */
|
|
NEXT();
|
|
}
|
|
|
|
CASE(BORROW_S) : {
|
|
uint64_t v = R[wo_ins_a(ins)];
|
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
|
if (wo_borrow_shared((wo_hdr *)(uintptr_t)v) != 0)
|
|
TRAPF(WO_T_BORROW, "shared borrow of exclusively borrowed value");
|
|
NEXT();
|
|
}
|
|
CASE(BORROW_X) : {
|
|
uint64_t v = R[wo_ins_a(ins)];
|
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
|
if (wo_borrow_excl((wo_hdr *)(uintptr_t)v) != 0)
|
|
TRAPF(WO_T_BORROW, "exclusive borrow of already borrowed value");
|
|
NEXT();
|
|
}
|
|
CASE(RELEASE_S) : {
|
|
/* releases are unconditional: the compiler emits them balanced */
|
|
wo_release_shared((wo_hdr *)(uintptr_t)R[wo_ins_a(ins)]);
|
|
NEXT();
|
|
}
|
|
CASE(RELEASE_X) : {
|
|
wo_release_excl((wo_hdr *)(uintptr_t)R[wo_ins_a(ins)]);
|
|
NEXT();
|
|
}
|
|
|
|
CASE(RC_INC) : {
|
|
uint64_t v = R[wo_ins_a(ins)];
|
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
|
wo_rc_inc((wo_hdr *)(uintptr_t)v);
|
|
NEXT();
|
|
}
|
|
CASE(RC_DEC) : {
|
|
uint64_t v = R[wo_ins_a(ins)];
|
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
|
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)v);
|
|
NEXT();
|
|
}
|
|
|
|
CASE(CONCAT) : {
|
|
const char *why;
|
|
wo_str *x = str_check(R[wo_ins_b(ins)], &why);
|
|
if (!x) TRAPF(WO_T_BOUNDS, "%s", why);
|
|
wo_str *y = str_check(R[wo_ins_c(ins)], &why);
|
|
if (!y) TRAPF(WO_T_BOUNDS, "%s", why);
|
|
wo_str *z = wo_str_concat(&vm->rt, x, y);
|
|
if (!z) TRAPF(WO_T_OOM, "out of memory");
|
|
R[wo_ins_a(ins)] = (uint64_t)(uintptr_t)z; /* new owned text */
|
|
NEXT();
|
|
}
|
|
CASE(EQS) : {
|
|
/* Text content equality — and the one comparison that must accept a
|
|
* nil operand: two `?Text` values compare with this opcode, and the
|
|
* language's answer is "both absent is equal, one absent is not"
|
|
* (trapping instead would make `a != b` on optionals unusable). Only a
|
|
* NON-nil value still has to actually be a Text. */
|
|
uint64_t bv = R[wo_ins_b(ins)], cv = R[wo_ins_c(ins)];
|
|
if (!bv || !cv) {
|
|
R[wo_ins_a(ins)] = bv == cv ? 1 : 0;
|
|
NEXT();
|
|
}
|
|
const char *why;
|
|
wo_str *x = str_check(bv, &why);
|
|
if (!x) TRAPF(WO_T_BOUNDS, "%s", why);
|
|
wo_str *y = str_check(cv, &why);
|
|
if (!y) TRAPF(WO_T_BOUNDS, "%s", why);
|
|
R[wo_ins_a(ins)] = wo_str_eq(x, y) ? 1 : 0;
|
|
NEXT();
|
|
}
|
|
|
|
CASE(BUILTIN) : {
|
|
const char *bmsg = "builtin failed";
|
|
int brc = wo_builtin(vm, R, ins, &bmsg);
|
|
/* A stop is not a trap: no error record, no catch handler gets a
|
|
* look (`try` must not be able to swallow SIGTERM), and no message.
|
|
* The stack is unwound exactly as an uncaught trap unwinds it, so
|
|
* every live value is still released on the way out; the CLI turns
|
|
* this into the same exit status a clean `return 0` gives. */
|
|
if (brc == WO_SYS_STOPPED) {
|
|
vm->frames[vm->depth - 1].pc = pc - 1;
|
|
vm->ncatch = 0;
|
|
vm_unwind(vm, 0);
|
|
return 1;
|
|
}
|
|
if (brc) TRAPF((uint32_t)brc, "%s", bmsg);
|
|
NEXT();
|
|
}
|
|
|
|
CASE(ICALL) : {
|
|
/* structural-interface dispatch (spec §2): binary search the sorted
|
|
* (class, slot, method) triples by the RECEIVER's class. The
|
|
* compiler's type checker makes a miss unreachable in compiled
|
|
* code; the VM keeps the trap as defense (spec §6). */
|
|
uint64_t v = R[wo_ins_a(ins)];
|
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
|
wo_hdr *o = (wo_hdr *)(uintptr_t)v;
|
|
if (o->class_id >= mod->class_cnt)
|
|
TRAPF(WO_T_BOUNDS, "interface call on a native value");
|
|
uint32_t slot = wo_ins_bx(ins);
|
|
const wo_vtabent *hit = NULL;
|
|
for (uint32_t lo = 0, hi = mod->vtab_cnt; lo < hi;) {
|
|
uint32_t mid = lo + (hi - lo) / 2;
|
|
const wo_vtabent *e = &mod->vtabs[mid];
|
|
if (e->class_id < o->class_id ||
|
|
(e->class_id == o->class_id && e->slot < slot)) {
|
|
lo = mid + 1;
|
|
} else if (e->class_id == o->class_id && e->slot == slot) {
|
|
hit = e;
|
|
break;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
if (!hit) TRAPF(WO_T_BOUNDS, "no vtable entry for receiver class");
|
|
/* exactly the CALL sequence at the same window base: the receiver
|
|
* already sits in slot A = callee's self */
|
|
const wo_methodrec *callee = &mod->methods[hit->method];
|
|
if ((uint32_t)wo_ins_a(ins) + callee->arg_cnt > me->reg_cnt)
|
|
TRAPF(WO_T_STACK, "call window exceeds frame"); /* runtime: callee
|
|
unknown to the loader here */
|
|
uint32_t nbase = vm->frames[vm->depth - 1].base + wo_ins_a(ins);
|
|
if (vm->depth >= WO_MAX_FRAMES)
|
|
TRAPF(WO_T_STACK, "frame stack overflow (%u frames)",
|
|
(unsigned)WO_MAX_FRAMES);
|
|
if (nbase + callee->reg_cnt > WO_STACK_SLOTS)
|
|
TRAPF(WO_T_STACK, "value stack overflow");
|
|
vm->frames[vm->depth - 1].pc = pc;
|
|
vm->frames[vm->depth].method = hit->method;
|
|
vm->frames[vm->depth].pc = 0;
|
|
vm->frames[vm->depth].base = nbase;
|
|
vm->depth++;
|
|
memset(vm->regs + nbase + callee->arg_cnt, 0,
|
|
(size_t)(callee->reg_cnt - callee->arg_cnt) * 8u);
|
|
RELOAD();
|
|
NEXT();
|
|
}
|
|
|
|
CASE(DB_STUB) : { TRAPF(WO_T_DB, "engine not linked"); }
|
|
|
|
CASE(TRAP) : { TRAPF(wo_ins_bx(ins), "explicit trap"); }
|
|
|
|
CASE(TRY) : {
|
|
if (vm->ncatch >= WO_MAX_CATCH)
|
|
TRAPF(WO_T_STACK, "catch stack overflow (%u regions)",
|
|
(unsigned)WO_MAX_CATCH);
|
|
vm->catches[vm->ncatch].depth = vm->depth;
|
|
vm->catches[vm->ncatch].pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
|
vm->catches[vm->ncatch].reg = wo_ins_a(ins);
|
|
vm->ncatch++;
|
|
NEXT();
|
|
}
|
|
CASE(ENDTRY) : {
|
|
/* the try region completed without trapping. Defensive on an
|
|
* unpaired ENDTRY (a miscompile the loader cannot see): pop
|
|
* nothing rather than corrupt the stack. */
|
|
if (vm->ncatch) vm->ncatch--;
|
|
NEXT();
|
|
}
|
|
|
|
#ifdef WO_ISO_C
|
|
default:
|
|
TRAPF(WO_T_EXPLICIT, "unknown opcode"); /* unreachable: loader */
|
|
}
|
|
#endif
|
|
|
|
#undef CASE
|
|
#undef NEXT
|
|
#undef RELOAD
|
|
#undef TRAPF
|
|
#undef DROP_CATCHES
|
|
}
|
|
|
|
int wo_vm_call(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
|
|
uint32_t argc, uint64_t *ret, wo_err *err) {
|
|
if (err) memset(err, 0, sizeof(*err));
|
|
if (method_idx >= vm->mod->method_cnt) {
|
|
if (err) {
|
|
err->code = WO_T_EXPLICIT;
|
|
snprintf(err->msg, sizeof(err->msg), "no such method");
|
|
}
|
|
return -1;
|
|
}
|
|
const wo_methodrec *me = &vm->mod->methods[method_idx];
|
|
if (argc != me->arg_cnt) {
|
|
if (err) {
|
|
err->code = WO_T_EXPLICIT;
|
|
snprintf(err->msg, sizeof(err->msg), "bad call arity");
|
|
}
|
|
return -1;
|
|
}
|
|
vm->depth = 1;
|
|
vm->ncatch = 0; /* catch regions never survive a call boundary */
|
|
vm->frames[0].method = method_idx;
|
|
vm->frames[0].pc = 0;
|
|
vm->frames[0].base = 0;
|
|
if (argc) memcpy(vm->regs, args, (size_t)argc * 8u);
|
|
memset(vm->regs + argc, 0, (size_t)(me->reg_cnt - argc) * 8u);
|
|
return vm_run(vm, ret, err);
|
|
}
|