- main.c, startup only: WO_DATA unset and a class carrying WO_CLASSF_TABLE
without WO_CLASSF_VOLATILE (`durable: true`, the default) refuses — exit 2,
one stderr line naming the class and the three ways forward (WO_DATA=<dir
or file>, WO_EPHEMERAL=1, @table(durable: false)); before, every write
was silently dropped at exit — the one outcome `durable: true` forbids
- WO_EPHEMERAL=1 (exact value) is the whole-program escape: one boot notice,
rc 0, the RAM path byte-for-byte the old one (db.c untouched); any other
value refuses; set alongside WO_DATA refuses regardless of tables; the
`resident: keys` loop still wins and is not rescued
- .wob v8: WO_CLASSF_TABLE 0x08 (WO_CLASSF_ALL 0x0f), set from emit.ml's
cr_is_table — the first cut keyed on !VOLATILE and refused every
class-bearing program (fibers' Tick, subprocess's ConnMsg), because v7
spelled `durable: true` as the mere absence of a bit
- loader refuses VOLATILE/RESIDENT_KEYS without the table bit ("storage
flags on a class that is not a @table"); a v7 image is refused by the
exact-match version check, as v7 refused v6; disasm prints `table`;
runner.ml's independent validator carries both rules; obj.h comment
- test_loader: test_storage_flags_need_table (forged flags word: both
refusals, and the same bits WITH the table bit load); no golden moved
- contract: docs/plan/oop-vm/00-wob-format.md "v8: the table bit"
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 863692a590d426da0047831ae315142ef5b24416)
152 lines
4.9 KiB
C
152 lines
4.9 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);
|
|
}
|
|
|
|
/* a valid one-class image whose class descriptor carries `flags` */
|
|
static uint8_t *image_with_class_flags(uint32_t flags, size_t *len) {
|
|
wb_t *b = wb_new();
|
|
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, flags, kinds, 2);
|
|
uint32_t code[] = {wo_ins_abc(WOP_RET, 0, 0, 0)};
|
|
wb_method(b, kname, WOB_NONE, 0, 1, code, 1, NULL, 0, NULL, 0);
|
|
return wb_finish(b, len);
|
|
}
|
|
|
|
/* databasev2 2 (6a, .wob v8): the storage bits describe a @table's rows, so
|
|
* they are meaningless — and refused — on a class without WO_CLASSF_TABLE.
|
|
* woc never emits the combination; the loader refuses it independently. */
|
|
static void test_storage_flags_need_table(void) {
|
|
size_t len;
|
|
uint8_t *img = image_with_class_flags(WO_CLASSF_VOLATILE, &len);
|
|
expect_reject(img, len, "volatile without table");
|
|
free(img);
|
|
|
|
img = image_with_class_flags(WO_CLASSF_RESIDENT_KEYS, &len);
|
|
expect_reject(img, len, "resident:keys without table");
|
|
free(img);
|
|
|
|
/* the same bits WITH the table bit load */
|
|
img = image_with_class_flags(WO_CLASSF_TABLE | WO_CLASSF_VOLATILE, &len);
|
|
wo_module m;
|
|
char err[256] = "";
|
|
T_EQ(wo_load_buf(&m, img, len, err, sizeof err), 0);
|
|
T_EQ(m.classes[0].flags, WO_CLASSF_TABLE | WO_CLASSF_VOLATILE);
|
|
wo_module_free(&m);
|
|
free(img);
|
|
}
|
|
|
|
int main(void) {
|
|
test_happy_path();
|
|
test_rejects();
|
|
test_storage_flags_need_table();
|
|
return t_report("test_loader");
|
|
}
|