From 58e37cc19edd6740c1c063a43d05530bbb9b7ce0 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Thu, 20 Aug 2026 06:40:58 +0200 Subject: [PATCH] refactor(runtime): extract the fiber context from wo_vm (arc T1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wo_fiber = the interpreter state wo_vm held inline: register window, frame stack, catch stack, caught-error slot; wo_vm keeps module, runtime, the embedded fiber 0 (main) and the cur pointer every interpreter access now reads through - vm_gc_roots split into a per-fiber walker + the all-fibers caller (one fiber today; the loop is where stage 1 T2 adds the rest) - PURE refactor, no functional change to hide behind: full battery byte-identical — wovm-test (test, test-iso, cli_smoke) green, oop-e2e 89/0, woc-test green, log-watcher 7/0, employee 8/0, web-app 21/0, deps-accept 8/0 - plan: docs/superpowers/plans/2026-08-20-shard-fiber-arc.md task 1 (plan/spec docs live on branch language-surface-strictness) Co-Authored-By: Claude Opus 5 (1M context) --- runtime/src/builtin.c | 12 ++-- runtime/src/vm.c | 134 ++++++++++++++++++++++-------------------- runtime/src/vm.h | 17 +++++- 3 files changed, 90 insertions(+), 73 deletions(-) diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index c062bca..b3b35f1 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -328,22 +328,22 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { *msg = "error record is not a 4-field class"; return WO_T_BOUNDS; } - wo_str *meth = wo_str_new(rt, vm->caught.method, - (uint32_t)strlen(vm->caught.method)); + wo_str *meth = wo_str_new(rt, vm->cur->caught.method, + (uint32_t)strlen(vm->cur->caught.method)); if (!meth) { *msg = "out of memory"; return WO_T_OOM; } - wo_str *text = wo_str_new(rt, vm->caught.msg, - (uint32_t)strlen(vm->caught.msg)); + wo_str *text = wo_str_new(rt, vm->cur->caught.msg, + (uint32_t)strlen(vm->cur->caught.msg)); if (!text) { wo_str_free(rt, meth); *msg = "out of memory"; return WO_T_OOM; } uint64_t *fs = wo_fields(o); - fs[0] = vm->caught.code; - fs[1] = vm->caught.line; + fs[0] = vm->cur->caught.code; + fs[1] = vm->cur->caught.line; fs[2] = (uint64_t)(uintptr_t)meth; fs[3] = (uint64_t)(uintptr_t)text; R[A] = R[B]; diff --git a/runtime/src/vm.c b/runtime/src/vm.c index f254a03..6aef2ec 100644 --- a/runtime/src/vm.c +++ b/runtime/src/vm.c @@ -9,11 +9,12 @@ #include "cont.h" #include "gc.h" -uint32_t wo_vm_depth(const wo_vm *vm) { return vm->depth; } +uint32_t wo_vm_depth(const wo_vm *vm) { return vm->cur->depth; } int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) { memset(vm, 0, sizeof(*vm)); vm->mod = mod; + vm->cur = &vm->f0; /* fiber 0: main — the one-fiber degenerate case */ return wo_rt_init(&vm->rt, heap_cap, mod->classes, mod->class_cnt); } @@ -33,7 +34,7 @@ static const wo_dropent *vm_dropent(const wo_methodrec *me, uint32_t pc) { * 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_frame *f = &vm->cur->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 */ @@ -45,7 +46,7 @@ static void vm_release_frame(wo_vm *vm, uint32_t d, uint32_t pc, uint32_t keep_p keep_gc = k->gc; } } - uint64_t *R = vm->regs + f->base; + uint64_t *R = vm->cur->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]) { @@ -69,14 +70,17 @@ static void vm_release_frame(wo_vm *vm, uint32_t d, uint32_t pc, uint32_t keep_p * frame (the caller synced f->pc first), the CALL for outer ones. Runs * once, atomically, when a cycle begins: bounded by the stack, not the * heap. */ -static void vm_gc_roots(wo_vm *vm) { - for (uint32_t d = vm->depth; d > 0; d--) { - const wo_frame *f = &vm->frames[d - 1]; +/* One fiber's frames as GC roots. EVERY fiber — running, queued, parked — + * pins its values (the arc's rule); vm_gc_roots walks them all. Stage 1 + * Task 1: exactly one fiber exists. */ +static void vm_gc_roots_fiber(wo_vm *vm, const wo_fiber *fb) { + for (uint32_t d = fb->depth; d > 0; d--) { + const wo_frame *f = &fb->frames[d - 1]; const wo_methodrec *me = &vm->mod->methods[f->method]; - uint32_t gpc = (d == vm->depth) ? f->pc : f->pc - 1; + uint32_t gpc = (d == fb->depth) ? f->pc : f->pc - 1; const wo_dropent *ent = vm_dropent(me, gpc); if (!ent) continue; - const uint64_t *R = vm->regs + f->base; + const uint64_t *R = fb->regs + f->base; for (uint32_t r = 0; r < me->reg_cnt; r++) { uint64_t bit = 1ull << r; if (((ent->gc | ent->owned) & bit) && R[r]) @@ -85,6 +89,8 @@ static void vm_gc_roots(wo_vm *vm) { } } +static void vm_gc_roots(wo_vm *vm) { vm_gc_roots_fiber(vm, &vm->f0); } + /* One safepoint: start a cycle when the trigger says so (snapshot the * roots before the mutator resumes), then run one budgeted slice while a * cycle is live. The caller synced the innermost frame's pc first. */ @@ -105,11 +111,11 @@ static void vm_gc_safepoint(wo_vm *vm) { * 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); + for (uint32_t d = vm->cur->depth; d > stop_depth; d--) { + const wo_frame *f = &vm->cur->frames[d - 1]; + vm_release_frame(vm, d, (d == vm->cur->depth) ? f->pc : f->pc - 1, UINT32_MAX); } - vm->depth = stop_depth; + vm->cur->depth = stop_depth; } /* Residual runtime checks the loader cannot do statically (registers are @@ -153,7 +159,7 @@ static wo_str *str_check(uint64_t v, const char **why) { * `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_frame *f = &vm->cur->frames[vm->cur->depth - 1]; const wo_methodrec *me = &mod->methods[f->method]; out->code = tcode; out->line = 0; /* last line-table entry with pc <= trapping pc */ @@ -175,10 +181,10 @@ static int vm_trap(wo_vm *vm, wo_err *err, uint32_t tcode, const char *fmt, * 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); + vm_fill_err(vm, &vm->cur->caught, tcode, fmt, ap); va_end(ap); - if (vm->ncatch) { - const wo_catch *c = &vm->catches[vm->ncatch - 1]; + if (vm->cur->ncatch) { + const wo_catch *c = &vm->cur->catches[vm->cur->ncatch - 1]; uint32_t cdepth = c->depth; uint32_t hpc = c->pc; /* Which instruction governs the catching frame's own live set has @@ -186,18 +192,18 @@ static int vm_trap(wo_vm *vm, wo_err *err, uint32_t tcode, const char *fmt, * 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--; + int trapped_here = (cdepth == vm->cur->depth); + vm->cur->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]; + wo_frame *cf = &vm->cur->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; + if (err) *err = vm->cur->caught; vm_unwind(vm, 0); return -1; } @@ -212,10 +218,10 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) { #define RELOAD() \ do { \ - me = &mod->methods[vm->frames[vm->depth - 1].method]; \ + me = &mod->methods[vm->cur->frames[vm->cur->depth - 1].method]; \ code = me->code; \ - pc = vm->frames[vm->depth - 1].pc; \ - R = vm->regs + vm->frames[vm->depth - 1].base; \ + pc = vm->cur->frames[vm->cur->depth - 1].pc; \ + R = vm->cur->regs + vm->cur->frames[vm->cur->depth - 1].base; \ } while (0) /* pc is post-incremented at dispatch: the trapping instruction is pc-1. @@ -224,7 +230,7 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) { * keeps going — the same macro serves both surfaces. */ #define TRAPF(tcode, ...) \ do { \ - vm->frames[vm->depth - 1].pc = pc - 1; \ + vm->cur->frames[vm->cur->depth - 1].pc = pc - 1; \ if (vm_trap(vm, err, tcode, __VA_ARGS__) == 0) { \ RELOAD(); \ NEXT(); \ @@ -239,7 +245,7 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) { #define GC_SAFEPOINT() \ do { \ if (vm->rt.gc_phase != WO_GC_IDLE || wo_gc_want_start(&vm->rt)) { \ - vm->frames[vm->depth - 1].pc = pc - 1; \ + vm->cur->frames[vm->cur->depth - 1].pc = pc - 1; \ vm_gc_safepoint(vm); \ } \ } while (0) @@ -356,19 +362,19 @@ dispatch: /* 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) + uint32_t nbase = vm->cur->frames[vm->cur->depth - 1].base + wo_ins_a(ins); + if (vm->cur->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++; + vm->cur->frames[vm->cur->depth - 1].pc = pc; + vm->cur->frames[vm->cur->depth].method = wo_ins_bx(ins); + vm->cur->frames[vm->cur->depth].pc = 0; + vm->cur->frames[vm->cur->depth].base = nbase; + vm->cur->depth++; /* zero non-argument registers: drop masks must never see stale bits */ - memset(vm->regs + nbase + callee->arg_cnt, 0, + memset(vm->cur->regs + nbase + callee->arg_cnt, 0, (size_t)(callee->reg_cnt - callee->arg_cnt) * 8u); RELOAD(); NEXT(); @@ -378,15 +384,15 @@ dispatch: * 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-- + while (vm->cur->ncatch && vm->cur->catches[vm->cur->ncatch - 1].depth > vm->cur->depth) \ + vm->cur->ncatch-- CASE(RET) : { uint64_t rv = R[wo_ins_a(ins)]; - vm->regs[vm->frames[vm->depth - 1].base] = rv; - vm->depth--; + vm->cur->regs[vm->cur->frames[vm->cur->depth - 1].base] = rv; + vm->cur->depth--; DROP_CATCHES(); - if (vm->depth == 0) { + if (vm->cur->depth == 0) { *ret = rv; return 0; } @@ -394,10 +400,10 @@ dispatch: NEXT(); } CASE(RET0) : { - vm->regs[vm->frames[vm->depth - 1].base] = 0; - vm->depth--; + vm->cur->regs[vm->cur->frames[vm->cur->depth - 1].base] = 0; + vm->cur->depth--; DROP_CATCHES(); - if (vm->depth == 0) { + if (vm->cur->depth == 0) { *ret = 0; return 0; } @@ -527,8 +533,8 @@ dispatch: * 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->cur->frames[vm->cur->depth - 1].pc = pc - 1; + vm->cur->ncatch = 0; vm_unwind(vm, 0); return 1; } @@ -568,18 +574,18 @@ dispatch: 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) + uint32_t nbase = vm->cur->frames[vm->cur->depth - 1].base + wo_ins_a(ins); + if (vm->cur->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, + vm->cur->frames[vm->cur->depth - 1].pc = pc; + vm->cur->frames[vm->cur->depth].method = hit->method; + vm->cur->frames[vm->cur->depth].pc = 0; + vm->cur->frames[vm->cur->depth].base = nbase; + vm->cur->depth++; + memset(vm->cur->regs + nbase + callee->arg_cnt, 0, (size_t)(callee->reg_cnt - callee->arg_cnt) * 8u); RELOAD(); NEXT(); @@ -590,20 +596,20 @@ dispatch: CASE(TRAP) : { TRAPF(wo_ins_bx(ins), "explicit trap"); } CASE(TRY) : { - if (vm->ncatch >= WO_MAX_CATCH) + if (vm->cur->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++; + vm->cur->catches[vm->cur->ncatch].depth = vm->cur->depth; + vm->cur->catches[vm->cur->ncatch].pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins)); + vm->cur->catches[vm->cur->ncatch].reg = wo_ins_a(ins); + vm->cur->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--; + if (vm->cur->ncatch) vm->cur->ncatch--; NEXT(); } @@ -639,12 +645,12 @@ int wo_vm_call(wo_vm *vm, uint32_t method_idx, const uint64_t *args, } 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); + vm->cur->depth = 1; + vm->cur->ncatch = 0; /* catch regions never survive a call boundary */ + vm->cur->frames[0].method = method_idx; + vm->cur->frames[0].pc = 0; + vm->cur->frames[0].base = 0; + if (argc) memcpy(vm->cur->regs, args, (size_t)argc * 8u); + memset(vm->cur->regs + argc, 0, (size_t)(me->reg_cnt - argc) * 8u); return vm_run(vm, ret, err); } diff --git a/runtime/src/vm.h b/runtime/src/vm.h index 28a9487..3a89f8b 100644 --- a/runtime/src/vm.h +++ b/runtime/src/vm.h @@ -34,9 +34,13 @@ typedef struct wo_catch { #define WO_MAX_CATCH 64u -typedef struct wo_vm { - const wo_module *mod; - wo_rt rt; +/* The concurrency arc (iterations 8+11, stage 1): a FIBER is exactly the + * interpreter state the vm used to hold inline — the register window, the + * frame stack, the catch stack, and the caught-error slot. The vm owns + * the shard-wide pieces (module, runtime, and which fiber is live). + * Stage 1 Task 1 is a pure extraction: one embedded fiber, `cur` always + * points at it, behavior byte-identical. */ +typedef struct wo_fiber { uint64_t regs[WO_STACK_SLOTS]; wo_frame frames[WO_MAX_FRAMES]; uint32_t depth; @@ -47,6 +51,13 @@ typedef struct wo_vm { /* the error a caught trap landed with, read by WO_B_ERR_FILL while * the catch arm builds its record */ wo_err caught; +} wo_fiber; + +typedef struct wo_vm { + const wo_module *mod; + wo_rt rt; + wo_fiber f0; /* fiber 0: main. Stage 1 Task 2 grows the run queue. */ + wo_fiber *cur; /* the live fiber — every interpreter access goes here */ } wo_vm; /* heap_cap = arena byte capacity (the CLI's WO_HEAP_MB feeds this) */