feat(runtime): fiber run queue + reduction budget (arc T2)

- fiber states (RUNNABLE/PARKED/DONE), intrusive FIFO run queue,
  wo_vm_spawn_fiber (calloc'd context, frame 0 set up like wo_vm_call)
- reduction budget: WO_REDUCTIONS (default 4000), checked at loop
  BACK-EDGES AFTER the jump lands so the saved pc is the loop head —
  a pre-instruction save at budget 1 re-executes the jump into the
  same decrement and livelocks (found by reasoning, pinned by the
  budget-1 test; deviation from the spec's three-site wording,
  recorded in the yield macro's comment)
- FIBER_DONE: main returning ends the program and reaps every
  remaining fiber through vm_unwind (drop maps run); a spawned fiber
  ending frees silently; its return value is discarded by contract
- TRAPF: an uncaught trap in a spawned fiber kills that fiber ALONE
  (stderr report, program lives); in main it stays the program's death
- WO_SYS_STOPPED reaps all fibers wherever it lands (main unlinked
  from the queue and unwound if a spawned fiber caught the stop)
- vm_gc_roots walks the live fiber plus every queued one
- test_fiber (45 checks, ASan): EXACT round-robin interleave at budget
  1 across three fibers pushing tags into one shared multi;
  main-return reaps a spinning fiber holding an owned Big (ASan proves
  the free); a DIV0 fiber dies alone, main answers 0
- full battery green: wovm-test, oop-e2e 89/0, woc-test, log-watcher
  7/0, employee 8/0, web-app 21/0, deps-accept 8/0 (scheduler dormant
  = one branch per back-edge)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-20 07:03:40 +02:00
parent 58e37cc19e
commit f88aa11cef
3 changed files with 380 additions and 11 deletions

View file

@ -2,6 +2,7 @@
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "borrow.h" #include "borrow.h"
@ -15,11 +16,80 @@ int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) {
memset(vm, 0, sizeof(*vm)); memset(vm, 0, sizeof(*vm));
vm->mod = mod; vm->mod = mod;
vm->cur = &vm->f0; /* fiber 0: main — the one-fiber degenerate case */ vm->cur = &vm->f0; /* fiber 0: main — the one-fiber degenerate case */
vm->budget0 = 4000; /* reductions per slice, the BEAM-ish default */
{
const char *e = getenv("WO_REDUCTIONS");
if (e && *e) {
long v = atol(e);
if (v > 0) vm->budget0 = v;
}
}
vm->budget = vm->budget0;
return wo_rt_init(&vm->rt, heap_cap, mod->classes, mod->class_cnt); 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); } void wo_vm_destroy(wo_vm *vm) { wo_rt_destroy(&vm->rt); }
/* ---- the run queue (stage 1 Task 2) ---------------------------------- */
static void vm_unwind(wo_vm *vm, uint32_t stop_depth);
static void fib_enqueue(wo_vm *vm, wo_fiber *fb) {
fb->state = WO_FIB_RUNNABLE;
fb->next = NULL;
if (vm->qtail) vm->qtail->next = fb;
else vm->qhead = fb;
vm->qtail = fb;
}
static wo_fiber *fib_dequeue(wo_vm *vm) {
wo_fiber *fb = vm->qhead;
if (fb) {
vm->qhead = fb->next;
if (!vm->qhead) vm->qtail = NULL;
fb->next = NULL;
}
return fb;
}
wo_fiber *wo_vm_spawn_fiber(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
uint32_t argc) {
if (method_idx >= vm->mod->method_cnt) return NULL;
const wo_methodrec *sme = &vm->mod->methods[method_idx];
if (argc != sme->arg_cnt) return NULL;
wo_fiber *fb = calloc(1, sizeof(*fb));
if (!fb) return NULL;
fb->depth = 1;
fb->frames[0].method = method_idx;
fb->frames[0].pc = 0;
fb->frames[0].base = 0;
if (argc) memcpy(fb->regs, args, (size_t)argc * 8u);
memset(fb->regs + argc, 0, (size_t)(sme->reg_cnt - argc) * 8u);
vm->nfibers++;
fib_enqueue(vm, fb);
return fb;
}
/* Unwind and release one fiber's live frames (drop maps run — parked and
* queued fibers die as cleanly as trapped ones), then free it if it is a
* spawned one. `vm->cur` is borrowed to do it, restored after. */
static void fib_reap(wo_vm *vm, wo_fiber *fb) {
wo_fiber *save = vm->cur;
vm->cur = fb;
vm_unwind(vm, 0);
vm->cur = save;
if (fb != &vm->f0) {
vm->nfibers--;
free(fb);
}
}
/* Main finished (return or stop): every remaining fiber unwinds clean. */
static void fib_reap_all(wo_vm *vm) {
wo_fiber *fb;
while ((fb = fib_dequeue(vm)) != NULL) fib_reap(vm, fb);
}
/* The drop-table entry governing instruction [pc]: the last one recorded /* The drop-table entry governing instruction [pc]: the last one recorded
* at or before it. NULL = nothing live there. */ * at or before it. NULL = nothing live there. */
static const wo_dropent *vm_dropent(const wo_methodrec *me, uint32_t pc) { static const wo_dropent *vm_dropent(const wo_methodrec *me, uint32_t pc) {
@ -89,7 +159,13 @@ static void vm_gc_roots_fiber(wo_vm *vm, const wo_fiber *fb) {
} }
} }
static void vm_gc_roots(wo_vm *vm) { vm_gc_roots_fiber(vm, &vm->f0); } static void vm_gc_roots(wo_vm *vm) {
/* the live fiber plus every queued one; fiber 0 is always one of
* those two (parked fibers join here in stage 1 Task 4) */
vm_gc_roots_fiber(vm, vm->cur);
for (const wo_fiber *fb = vm->qhead; fb; fb = fb->next)
vm_gc_roots_fiber(vm, fb);
}
/* One safepoint: start a cycle when the trigger says so (snapshot the /* One safepoint: start a cycle when the trigger says so (snapshot the
* roots before the mutator resumes), then run one budgeted slice while a * roots before the mutator resumes), then run one budgeted slice while a
@ -235,6 +311,24 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
RELOAD(); \ RELOAD(); \
NEXT(); \ NEXT(); \
} \ } \
/* uncaught: the fiber's stack is already unwound. Main dying is \
* the program dying (unchanged); a spawned fiber dies ALONE — \
* the report goes to stderr the uncaught-trap way and the \
* program lives (the arc's isolation rule). */ \
if (vm->cur != &vm->f0) { \
if (err) \
fprintf(stderr, \
"wovm: fiber trap %d at %s:%d: %s\n", \
err->code, err->method, err->line, err->msg); \
wo_fiber *dead = vm->cur; \
vm->cur = fib_dequeue(vm); \
vm->nfibers--; \
free(dead); \
vm->budget = vm->budget0; \
RELOAD(); \
NEXT(); \
} \
fib_reap_all(vm); \
return -1; \ return -1; \
} while (0) } while (0)
@ -250,6 +344,27 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
} \ } \
} while (0) } while (0)
/* Reduction budget (stage 1 Task 2). Checked ONLY at loop back-edges,
* AFTER the jump has landed, so the saved pc is the loop head and resume
* makes progress — a pre-instruction save at budget 1 would re-execute
* the jump, hit the same decrement, and livelock. (Deviation from the
* spec's "same three sites as the GC": NEW/CALL re-execution has the
* identical livelock shape; back-edges alone bound every loop, which is
* what preemption is for. Recorded in the arc plan.) */
#define FIBER_BUDGET() \
do { \
if (--vm->budget <= 0) { \
vm->budget = vm->budget0; \
if (vm->qhead) { \
vm->cur->frames[vm->cur->depth - 1].pc = pc; \
fib_enqueue(vm, vm->cur); \
vm->cur = fib_dequeue(vm); \
RELOAD(); \
NEXT(); \
} \
} \
} while (0)
RELOAD(); RELOAD();
/* dual-flavor dispatch, one shared case-body text (spec §5): computed /* dual-flavor dispatch, one shared case-body text (spec §5): computed
@ -347,7 +462,12 @@ dispatch:
} }
CASE(JMP) : { CASE(JMP) : {
if (wo_ins_sbx(ins) < 0) GC_SAFEPOINT(); /* loop back-edge */ if (wo_ins_sbx(ins) < 0) {
GC_SAFEPOINT(); /* loop back-edge */
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
FIBER_BUDGET(); /* after the jump lands: resume = the loop head */
NEXT();
}
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins)); pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
NEXT(); NEXT();
} }
@ -387,15 +507,35 @@ dispatch:
while (vm->cur->ncatch && vm->cur->catches[vm->cur->ncatch - 1].depth > vm->cur->depth) \ while (vm->cur->ncatch && vm->cur->catches[vm->cur->ncatch - 1].depth > vm->cur->depth) \
vm->cur->ncatch-- vm->cur->ncatch--
/* A fiber's last frame returned. Main ending IS the program ending: every
* other fiber unwinds through its drop maps (clean, ASan-proven) and the
* program's value is main's. A spawned fiber ending just leaves the
* scheduler; its return value is discarded (the spawn surface's entry
* wrapper returns nothing owned — Task 3's contract). The queue cannot be
* empty when a spawned fiber ends: main never parks in stage 1, so it is
* either live or queued. */
#define FIBER_DONE(rv) \
do { \
if (vm->cur == &vm->f0) { \
fib_reap_all(vm); \
*ret = (rv); \
return 0; \
} \
wo_fiber *dead = vm->cur; \
vm->cur = fib_dequeue(vm); \
vm->nfibers--; \
free(dead); \
vm->budget = vm->budget0; \
RELOAD(); \
NEXT(); \
} while (0)
CASE(RET) : { CASE(RET) : {
uint64_t rv = R[wo_ins_a(ins)]; uint64_t rv = R[wo_ins_a(ins)];
vm->cur->regs[vm->cur->frames[vm->cur->depth - 1].base] = rv; vm->cur->regs[vm->cur->frames[vm->cur->depth - 1].base] = rv;
vm->cur->depth--; vm->cur->depth--;
DROP_CATCHES(); DROP_CATCHES();
if (vm->cur->depth == 0) { if (vm->cur->depth == 0) FIBER_DONE(rv);
*ret = rv;
return 0;
}
RELOAD(); RELOAD();
NEXT(); NEXT();
} }
@ -403,10 +543,7 @@ dispatch:
vm->cur->regs[vm->cur->frames[vm->cur->depth - 1].base] = 0; vm->cur->regs[vm->cur->frames[vm->cur->depth - 1].base] = 0;
vm->cur->depth--; vm->cur->depth--;
DROP_CATCHES(); DROP_CATCHES();
if (vm->cur->depth == 0) { if (vm->cur->depth == 0) FIBER_DONE(0);
*ret = 0;
return 0;
}
RELOAD(); RELOAD();
NEXT(); NEXT();
} }
@ -536,6 +673,25 @@ dispatch:
vm->cur->frames[vm->cur->depth - 1].pc = pc - 1; vm->cur->frames[vm->cur->depth - 1].pc = pc - 1;
vm->cur->ncatch = 0; vm->cur->ncatch = 0;
vm_unwind(vm, 0); vm_unwind(vm, 0);
/* a stop ends the PROGRAM: every fiber — the stopped one,
* queued ones, main wherever it is — unwinds clean */
if (vm->cur != &vm->f0) {
wo_fiber *dead = vm->cur;
vm->cur = &vm->f0;
vm->nfibers--;
free(dead);
if (vm->f0.depth) {
/* main was queued mid-run: release its frames too */
wo_fiber *q = vm->qhead, *prev = NULL;
while (q && q != &vm->f0) { prev = q; q = q->next; }
if (q) { /* unlink f0 from the queue */
if (prev) prev->next = q->next; else vm->qhead = q->next;
if (vm->qtail == q) vm->qtail = prev;
vm_unwind(vm, 0);
}
}
}
fib_reap_all(vm);
return 1; return 1;
} }
if (brc) TRAPF((uint32_t)brc, "%s", bmsg); if (brc) TRAPF((uint32_t)brc, "%s", bmsg);
@ -624,6 +780,7 @@ dispatch:
#undef RELOAD #undef RELOAD
#undef TRAPF #undef TRAPF
#undef GC_SAFEPOINT #undef GC_SAFEPOINT
#undef FIBER_BUDGET
#undef DROP_CATCHES #undef DROP_CATCHES
} }

View file

@ -40,6 +40,12 @@ typedef struct wo_catch {
* the shard-wide pieces (module, runtime, and which fiber is live). * the shard-wide pieces (module, runtime, and which fiber is live).
* Stage 1 Task 1 is a pure extraction: one embedded fiber, `cur` always * Stage 1 Task 1 is a pure extraction: one embedded fiber, `cur` always
* points at it, behavior byte-identical. */ * points at it, behavior byte-identical. */
typedef enum {
WO_FIB_RUNNABLE = 0,
WO_FIB_PARKED = 1, /* stage 1 Task 4: waiting on an fd/deadline */
WO_FIB_DONE = 2,
} wo_fib_state;
typedef struct wo_fiber { typedef struct wo_fiber {
uint64_t regs[WO_STACK_SLOTS]; uint64_t regs[WO_STACK_SLOTS];
wo_frame frames[WO_MAX_FRAMES]; wo_frame frames[WO_MAX_FRAMES];
@ -51,15 +57,28 @@ typedef struct wo_fiber {
/* the error a caught trap landed with, read by WO_B_ERR_FILL while /* the error a caught trap landed with, read by WO_B_ERR_FILL while
* the catch arm builds its record */ * the catch arm builds its record */
wo_err caught; wo_err caught;
wo_fib_state state;
struct wo_fiber *next; /* intrusive FIFO link (run queue) */
} wo_fiber; } wo_fiber;
typedef struct wo_vm { typedef struct wo_vm {
const wo_module *mod; const wo_module *mod;
wo_rt rt; wo_rt rt;
wo_fiber f0; /* fiber 0: main. Stage 1 Task 2 grows the run queue. */ wo_fiber f0; /* fiber 0: main — embedded; spawned fibers are calloc'd */
wo_fiber *cur; /* the live fiber — every interpreter access goes here */ wo_fiber *cur; /* the live fiber — every interpreter access goes here */
wo_fiber *qhead, *qtail; /* RUNNABLE fibers awaiting the interpreter */
uint32_t nfibers; /* live fibers besides main */
int64_t budget0; /* reductions per slice (WO_REDUCTIONS, default 4000) */
int64_t budget; /* countdown for the live fiber */
} wo_vm; } wo_vm;
/* Spawn a fiber that will run method_idx(args) — the runtime half the
* `spawn` expression lowers onto (stage 1 Task 3); Task 2's tests drive it
* directly. The fiber is RUNNABLE and queued; it runs when the scheduler
* reaches it. Returns NULL on allocation failure or bad method/arity. */
wo_fiber *wo_vm_spawn_fiber(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
uint32_t argc);
/* heap_cap = arena byte capacity (the CLI's WO_HEAP_MB feeds this) */ /* heap_cap = arena byte capacity (the CLI's WO_HEAP_MB feeds this) */
int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap); int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap);
void wo_vm_destroy(wo_vm *vm); void wo_vm_destroy(wo_vm *vm);

193
runtime/test/test_fiber.c Normal file
View file

@ -0,0 +1,193 @@
/* test_fiber — the arc's stage 1 Task 2: run queue + reduction budget.
*
* 1. Under WO_REDUCTIONS=1, three fibers pushing tags into one shared
* multi interleave in EXACT round-robin — the deterministic-
* scheduling criterion.
* 2. Main returning reaps a still-looping fiber holding an owned
* object: the drop map runs (ASan proves the free).
* 3. A spawned fiber's uncaught trap (DIV0) kills that fiber alone;
* main finishes with rc 0 — the isolation rule.
*/
#define _POSIX_C_SOURCE 200112L /* setenv/unsetenv under -std=c11 */
#include <stdlib.h>
#include "cont.h"
#include "gc.h"
#include "loader.h"
#include "t.h"
#include "vm.h"
#include "wob_build.h"
#define BIG 130 /* malloc-path class so ASan sees the free (test_unwind's trick) */
static wo_vm VM;
static uint8_t big_kinds[BIG];
/* Worker: argc=2 (r0 = shared multi as a raw word, r1 = tag), pushes the
* tag K times. The backward JMP is the budget's yield site.
* pc0 LOADK r2,#0 pc1 LOADK r3,#K
* pc2 LT r4,r2,r3 pc3 JZ r4,+4 -> pc8
* pc4 BUILTIN r4, base=0, MULTI_PUSH (container r0, element r1)
* pc5 LOADK r4,#1 pc6 ADD r2,r2,r4
* pc7 JMP -6 -> pc2 pc8 RET0 */
static void worker_code(uint32_t *code, uint32_t k0, uint32_t kK, uint32_t k1) {
code[0] = wo_ins_abx(WOP_LOADK, 2, (uint16_t)k0);
code[1] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kK);
code[2] = wo_ins_abc(WOP_LT, 4, 2, 3);
code[3] = wo_ins_asbx(WOP_JZ, 4, 4);
code[4] = wo_ins_abc(WOP_BUILTIN, 4, 0, WO_B_MULTI_PUSH);
code[5] = wo_ins_abx(WOP_LOADK, 4, (uint16_t)k1);
code[6] = wo_ins_abc(WOP_ADD, 2, 2, 4);
code[7] = wo_ins_asbx(WOP_JMP, 0, -6);
code[8] = wo_ins_abc(WOP_RET0, 0, 0, 0);
}
static void test_round_robin(void) {
wb_t *b = wb_new();
uint32_t kname = wb_const_text(b, "worker");
uint32_t k0 = wb_const_int(b, 0);
uint32_t kK = wb_const_int(b, 5);
uint32_t k1 = wb_const_int(b, 1);
uint32_t code[9];
worker_code(code, k0, kK, k1);
uint32_t lines[] = {0, 1};
wb_method(b, kname, WOB_NONE, 2, 8, code, 9, lines, 1, NULL, 0);
size_t len;
uint8_t *img = wb_finish(b, &len);
wo_module mod;
char lerr[256];
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
setenv("WO_REDUCTIONS", "1", 1);
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
unsetenv("WO_REDUCTIONS");
wo_multi *m = wo_multi_new(&VM.rt, WO_K_SCALAR);
T_CHECK(m != NULL);
uint64_t a2[2] = {(uint64_t)(uintptr_t)m, 2};
uint64_t a3[2] = {(uint64_t)(uintptr_t)m, 3};
T_CHECK(wo_vm_spawn_fiber(&VM, 0, a2, 2) != NULL);
T_CHECK(wo_vm_spawn_fiber(&VM, 0, a3, 2) != NULL);
uint64_t ret = 0;
wo_err err = {0};
uint64_t a1[2] = {(uint64_t)(uintptr_t)m, 1};
T_EQ(wo_vm_call(&VM, 0, a1, 2, &ret, &err), 0);
/* budget 1: every backward JMP yields, so one push per turn — the
* order is main(1), fiber(2), fiber(3), repeated exactly */
T_EQ(m->len, 15u);
for (uint32_t i = 0; i < 15; i++) {
uint64_t v = 0;
T_EQ(wo_multi_get(m, i, &v), 0);
T_EQ(v, (uint64_t)(i % 3) + 1);
}
wo_drop_obj(&VM.rt, (wo_hdr *)m); /* the test owns m; drop frees items */
wo_vm_destroy(&VM);
wo_module_free(&mod);
free(img);
}
/* Main loops 3 times (yielding), then returns; the worker allocated a Big
* (owned, in its drop mask) and loops forever. Main's return must reap it
* drop-clean — ASan fails this test if the Big leaks. */
static void test_main_return_reaps(void) {
wb_t *b = wb_new();
uint32_t kbig = wb_const_text(b, "Big");
uint32_t kw = wb_const_text(b, "spin");
uint32_t km = wb_const_text(b, "main");
wb_class(b, kbig, 0, big_kinds, BIG);
/* spin: NEW r0 Big, then loop forever (backward JMP = yields) */
uint32_t wcode[] = {
wo_ins_abx(WOP_NEW, 0, 0),
wo_ins_asbx(WOP_JMP, 0, -1), /* pc1 -> pc1: jump to itself */
};
wb_drop wdrops[] = {{.pc = 1, .owned = 1u << 0, .gc = 0}};
uint32_t wl[] = {0, 1};
wb_method(b, kw, WOB_NONE, 0, 2, wcode, 2, wl, 1, wdrops, 1);
/* main: count 0..3 with backward jumps, then RET0 */
uint32_t k0 = wb_const_int(b, 0);
uint32_t kK = wb_const_int(b, 3);
uint32_t k1 = wb_const_int(b, 1);
uint32_t mcode[] = {
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k0),
wo_ins_abx(WOP_LOADK, 1, (uint16_t)kK),
wo_ins_abc(WOP_LT, 2, 0, 1),
wo_ins_asbx(WOP_JZ, 2, 3),
wo_ins_abx(WOP_LOADK, 2, (uint16_t)k1),
wo_ins_abc(WOP_ADD, 0, 0, 2),
wo_ins_asbx(WOP_JMP, 0, -5),
wo_ins_abc(WOP_RET0, 0, 0, 0),
};
uint32_t ml[] = {0, 1};
wb_method(b, km, WOB_NONE, 0, 3, mcode, 8, ml, 1, NULL, 0);
size_t len;
uint8_t *img = wb_finish(b, &len);
wo_module mod;
char lerr[256];
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
setenv("WO_REDUCTIONS", "1", 1);
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
unsetenv("WO_REDUCTIONS");
T_CHECK(wo_vm_spawn_fiber(&VM, 0, NULL, 0) != NULL); /* spin */
uint64_t ret = 0;
wo_err err = {0};
T_EQ(wo_vm_call(&VM, 1, NULL, 0, &ret, &err), 0); /* main */
wo_vm_destroy(&VM);
wo_module_free(&mod);
free(img); /* ASan: spin's Big must have been freed by the reap */
}
/* A spawned fiber divides by zero; the program (main) still answers 0. */
static void test_fiber_trap_isolated(void) {
wb_t *b = wb_new();
uint32_t kw = wb_const_text(b, "boom");
uint32_t km = wb_const_text(b, "main");
uint32_t kone = wb_const_int(b, 1);
uint32_t kzero = wb_const_int(b, 0);
uint32_t wcode[] = {
wo_ins_abx(WOP_LOADK, 0, (uint16_t)kone),
wo_ins_abx(WOP_LOADK, 1, (uint16_t)kzero),
wo_ins_abc(WOP_DIV, 2, 0, 1), /* DIV0: uncaught, fiber dies alone */
wo_ins_abc(WOP_RET0, 0, 0, 0),
};
uint32_t wl[] = {0, 1};
wb_method(b, kw, WOB_NONE, 0, 3, wcode, 4, wl, 1, NULL, 0);
uint32_t kK = wb_const_int(b, 3);
uint32_t mcode[] = {
wo_ins_abx(WOP_LOADK, 0, (uint16_t)kzero),
wo_ins_abx(WOP_LOADK, 1, (uint16_t)kK),
wo_ins_abc(WOP_LT, 2, 0, 1),
wo_ins_asbx(WOP_JZ, 2, 3),
wo_ins_abx(WOP_LOADK, 2, (uint16_t)kone),
wo_ins_abc(WOP_ADD, 0, 0, 2),
wo_ins_asbx(WOP_JMP, 0, -5),
wo_ins_abc(WOP_RET0, 0, 0, 0),
};
uint32_t ml[] = {0, 1};
wb_method(b, km, WOB_NONE, 0, 3, mcode, 8, ml, 1, NULL, 0);
size_t len;
uint8_t *img = wb_finish(b, &len);
wo_module mod;
char lerr[256];
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
setenv("WO_REDUCTIONS", "1", 1);
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
unsetenv("WO_REDUCTIONS");
T_CHECK(wo_vm_spawn_fiber(&VM, 0, NULL, 0) != NULL); /* boom */
uint64_t ret = 0;
wo_err err = {0};
T_EQ(wo_vm_call(&VM, 1, NULL, 0, &ret, &err), 0);
wo_vm_destroy(&VM);
wo_module_free(&mod);
free(img);
}
int main(void) {
test_round_robin();
test_main_return_reaps();
test_fiber_trap_isolated();
return t_report("test_fiber");
}