writeonce/runtime/test/test_loader.c
shoney.arickathil bab88a53b9 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

116 lines
3.5 KiB
C

/* test_loader — happy path from a builder image, then a rejection battery.
* Every reject must produce a nonempty error and leak nothing (ASan). */
#include <stdlib.h>
#include "loader.h"
#include "t.h"
#include "wob_build.h"
/* a valid two-const, one-class, one-method image */
static uint8_t *valid_image(size_t *len) {
wb_t *b = wb_new();
uint32_t k42 = wb_const_int(b, 42);
uint32_t kname = wb_const_text(b, "main");
uint8_t kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
wb_class(b, kname, 0, kinds, 2);
uint32_t code[] = {
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k42),
wo_ins_abc(WOP_RET, 0, 0, 0),
};
uint32_t lines[] = {0, 10, 1, 11};
uint32_t m = wb_method(b, kname, WOB_NONE, 0, 2, code, 2, lines, 2, NULL, 0);
wb_entry(b, m);
return wb_finish(b, len);
}
static void test_happy_path(void) {
size_t len;
uint8_t *img = valid_image(&len);
wo_module m;
char err[256] = "";
T_EQ(wo_load_buf(&m, img, len, err, sizeof err), 0);
T_EQ(m.const_cnt, 2);
T_EQ(m.consts[0].tag, WOB_K_INT);
T_EQ(m.consts[0].i, 42);
T_EQ(m.consts[1].tag, WOB_K_TEXT);
T_CHECK(m.consts[1].s != NULL);
T_CHECK(m.consts[1].s->h.flags & WO_F_CONST); /* interned */
T_EQ(m.consts[1].s->len, 4);
T_CHECK(memcmp(m.consts[1].s->data, "main", 4) == 0);
T_EQ(m.class_cnt, 1);
T_EQ(m.classes[0].field_cnt, 2);
T_EQ(m.method_cnt, 1);
T_EQ(m.methods[0].reg_cnt, 2);
T_EQ(m.methods[0].ninstr, 2);
T_EQ(m.methods[0].line_cnt, 2);
T_EQ(m.methods[0].lines[1].pc, 1);
T_EQ(m.methods[0].lines[1].line, 11);
T_EQ(m.entry, 0);
wo_module_free(&m);
free(img);
}
/* build an image with one patched method-code instruction */
static uint8_t *image_with_code(uint32_t i0, uint32_t i1, size_t *len) {
wb_t *b = wb_new();
wb_const_int(b, 42);
uint32_t kname = wb_const_text(b, "main");
uint32_t code[] = {i0, i1};
wb_method(b, kname, WOB_NONE, 0, 2, code, 2, NULL, 0, NULL, 0);
return wb_finish(b, len);
}
static void expect_reject(uint8_t *img, size_t len, const char *what) {
wo_module m;
char err[256] = "";
int rc = wo_load_buf(&m, img, len, err, sizeof err);
T_EQ(rc, -1);
if (rc != 0 && err[0] == '\0')
fprintf(stderr, "empty error for reject case: %s\n", what);
T_CHECK(err[0] != '\0');
}
static void test_rejects(void) {
size_t len;
/* corrupted magic */
uint8_t *img = valid_image(&len);
img[0] ^= 0xFF;
expect_reject(img, len, "magic");
free(img);
/* unknown opcode */
img = image_with_code(wo_ins_abc(200, 0, 0, 0),
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
expect_reject(img, len, "opcode");
free(img);
/* constant index out of range */
img = image_with_code(wo_ins_abx(WOP_LOADK, 0, 99),
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
expect_reject(img, len, "const oob");
free(img);
/* register out of range (regc = 2) */
img = image_with_code(wo_ins_abc(WOP_MOVE, 5, 0, 0),
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
expect_reject(img, len, "reg oob");
free(img);
/* last instruction is not a terminator */
img = image_with_code(wo_ins_abc(WOP_RET, 0, 0, 0),
wo_ins_abx(WOP_LOADK, 0, 0), &len);
expect_reject(img, len, "non-terminator tail");
free(img);
/* truncated buffer */
img = valid_image(&len);
expect_reject(img, len / 2, "truncated");
free(img);
}
int main(void) {
test_happy_path();
test_rejects();
return t_report("test_loader");
}