writeonce/runtime/test/test_unwind.c
shoney.arickathil 631d277e0c feat(runtime): wovm VM core (Iteration 2)
- 16 tasks complete: arena, object model, borrow word, containers,
  RC + budgeted cycle collector, wob_build, validating loader,
  interpreter core (dual dispatch), object opcodes, drop-map unwinding,
  builtins + DB_STUB + TRAP, ICALL, wovm CLI + just recipes
- 13 test suites × 2 dispatch flavors (ASan+UBSan) + CLI smoke, all green
- .wob v1 format pinned in src/wob.h + docs/plan/oop-vm/00-wob-format.md
- wo-rt.c reference event-loop preserved for sub-project 2

This is Iteration 2 of the OOP milestone; compiler front (Iteration 3)
is in progress on this branch. They meet at Iteration 4 (emitter+e2e).
2026-08-10 09:35:55 +02:00

133 lines
4.6 KiB
C

/* test_unwind — borrow/rc opcodes and drop-map trap unwinding: the spec's
* "traps never leak" promise. Classes use the malloc-path trick (~130
* fields) so ASan proves every free on the trap paths. */
#include <stdlib.h>
#include "loader.h"
#include "t.h"
#include "vm.h"
#include "wob_build.h"
#define BIG 130
static wo_vm VM;
static int run_img(uint8_t *img, size_t len, uint64_t *ret, wo_err *err) {
wo_module mod;
char lerr[256];
if (wo_load_buf(&mod, img, len, lerr, sizeof lerr) != 0) {
fprintf(stderr, "loader rejected test image: %s\n", lerr);
return -2;
}
if (wo_vm_init(&VM, &mod, 1 << 20) != 0) {
wo_module_free(&mod);
return -2;
}
int rc = wo_vm_call(&VM, 0, NULL, 0, ret, err);
wo_vm_destroy(&VM);
wo_module_free(&mod);
return rc;
}
static uint8_t big_kinds[BIG]; /* all scalars */
/* Double-exclusive borrow traps BORROW at the right line; the owned object
* named in the frame's drop mask is freed during unwinding. */
static void test_borrow_violation_frees_owned(void) {
wb_t *b = wb_new();
uint32_t kc = wb_const_text(b, "Big");
uint32_t kf = wb_const_text(b, "main");
wb_class(b, kc, 0, big_kinds, BIG);
uint32_t code[] = {
wo_ins_abx(WOP_NEW, 0, 0),
wo_ins_abc(WOP_BORROW_X, 0, 0, 0), /* ok */
wo_ins_abc(WOP_BORROW_X, 0, 0, 0), /* violation: line 99 */
wo_ins_abc(WOP_RET0, 0, 0, 0),
};
uint32_t lines[] = {2, 99};
wb_drop drops[] = {{.pc = 1, .owned = 1u << 0, .gc = 0}};
wb_method(b, kf, WOB_NONE, 0, 2, code, 4, lines, 1, drops, 1);
size_t len;
uint8_t *img = wb_finish(b, &len);
uint64_t ret = 0;
wo_err err = {0};
T_EQ(run_img(img, len, &ret, &err), -1);
T_EQ(err.code, WO_T_BORROW);
T_EQ(err.line, 99);
T_EQ(wo_vm_depth(&VM), 0);
free(img); /* ASan: the Big instance must have been freed by unwinding */
}
/* A child method traps mid-body: owned objects in BOTH frames are freed —
* the child's argument (moved in), the child's own allocation, and the
* caller's local. */
static void test_two_frame_unwind_frees_both(void) {
wb_t *b = wb_new();
uint32_t kc = wb_const_text(b, "Big");
uint32_t kchild = wb_const_text(b, "child");
uint32_t kmain = wb_const_text(b, "main");
wb_class(b, kc, 0, big_kinds, BIG);
/* method 0 = child(o): allocates its own Big, then traps */
uint32_t ccode[] = {
wo_ins_abx(WOP_NEW, 1, 0),
wo_ins_abx(WOP_TRAP, 0, WO_T_EXPLICIT),
};
wb_drop cdrops[] = {{.pc = 1, .owned = (1u << 0) | (1u << 1), .gc = 0}};
wb_method(b, kchild, WOB_NONE, 1, 3, ccode, 2, NULL, 0, cdrops, 1);
/* method 1 = main: local Big in r0, arg Big in the call window r2 */
uint32_t mcode[] = {
wo_ins_abx(WOP_NEW, 0, 0),
wo_ins_abx(WOP_NEW, 2, 0),
wo_ins_abx(WOP_CALL, 2, 0), /* pc 2: ownership of r2 moves in */
wo_ins_abc(WOP_DROP, 0, 0, 0),
wo_ins_abc(WOP_RET0, 0, 0, 0),
};
wb_drop mdrops[] = {{.pc = 2, .owned = 1u << 0, .gc = 0}};
wb_method(b, kmain, WOB_NONE, 0, 4, mcode, 5, NULL, 0, mdrops, 1);
wb_entry(b, 1);
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);
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
uint64_t ret = 0;
wo_err err = {0};
T_EQ(wo_vm_call(&VM, 1, NULL, 0, &ret, &err), -1);
T_EQ(err.code, WO_T_EXPLICIT);
T_STREQ(err.method, "child");
T_EQ(wo_vm_depth(&VM), 0);
wo_vm_destroy(&VM);
wo_module_free(&mod);
free(img); /* ASan: all three Big instances freed across both frames */
}
/* rc inc/dec through opcodes frees exactly at zero (@gc malloc-path class) */
static void test_rc_opcodes_free_at_zero(void) {
wb_t *b = wb_new();
uint32_t kc = wb_const_text(b, "GBig");
uint32_t kf = wb_const_text(b, "main");
wb_class(b, kc, WO_CLASSF_GC, big_kinds, BIG);
uint32_t code[] = {
wo_ins_abx(WOP_NEW, 0, 0), /* rc 1 */
wo_ins_abc(WOP_RC_INC, 0, 0, 0), /* rc 2 */
wo_ins_abc(WOP_RC_DEC, 0, 0, 0), /* rc 1 */
wo_ins_abc(WOP_RC_DEC, 0, 0, 0), /* rc 0: freed */
wo_ins_abc(WOP_RET0, 0, 0, 0),
};
wb_method(b, kf, WOB_NONE, 0, 1, code, 5, NULL, 0, NULL, 0);
size_t len;
uint8_t *img = wb_finish(b, &len);
uint64_t ret = 0;
wo_err err;
T_EQ(run_img(img, len, &ret, &err), 0); /* no trap; ASan proves the free */
free(img);
}
int main(void) {
test_borrow_violation_frees_owned();
test_two_frame_unwind_frees_both();
test_rc_opcodes_free_at_zero();
return t_report("test_unwind");
}