feat(db2-ephemeral): refuse a durable table without WO_DATA; WO_EPHEMERAL=1 opts out; .wob v8 table bit

- 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)
This commit is contained in:
shoney.arickathil 2026-09-15 01:03:49 +02:00
parent 0435bd96f5
commit d38b4f864b
9 changed files with 180 additions and 18 deletions

View file

@ -184,7 +184,7 @@ let dump (img : string) : string =
opcodes 34-41). The disassembler tracks the emitter, not a range: an old opcodes 34-41). The disassembler tracks the emitter, not a range: an old
image is a different format and reading it as this one would misrender. *) image is a different format and reading it as this one would misrender. *)
(* tracks emit.ml's wob_version and wob.h's WOB_VERSION *) (* tracks emit.ml's wob_version and wob.h's WOB_VERSION *)
if ver <> 7 then raise (Bad (Printf.sprintf "unsupported version %d" ver)); if ver <> 8 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
let coff = u32 img 8 and ccnt = u32 img 12 in let coff = u32 img 8 and ccnt = u32 img 12 in
let koff = u32 img 16 and kcnt = u32 img 20 in let koff = u32 img 16 and kcnt = u32 img 20 in
let ioff = u32 img 24 and icnt = u32 img 28 in let ioff = u32 img 24 and icnt = u32 img 28 in
@ -264,6 +264,7 @@ let dump (img : string) : string =
(if flags land 1 <> 0 then [ "gc" ] else []) (if flags land 1 <> 0 then [ "gc" ] else [])
@ (if flags land 2 <> 0 then [ "volatile" ] else []) @ (if flags land 2 <> 0 then [ "volatile" ] else [])
@ (if flags land 4 <> 0 then [ "resident=keys" ] else []) @ (if flags land 4 <> 0 then [ "resident=keys" ] else [])
@ (if flags land 8 <> 0 then [ "table" ] else [])
in in
if parts = [] then "-" else String.concat "+" parts) if parts = [] then "-" else String.concat "+" parts)
(String.concat ", " fields)) (String.concat ", " fields))

View file

@ -156,8 +156,9 @@ let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
builtins 70-83. v4 (iteration 7b): RC opcodes retired; gc mask = GC roots *) builtins 70-83. v4 (iteration 7b): RC opcodes retired; gc mask = GC roots *)
(* MUST track runtime/src/wob.h's WOB_VERSION — the loader is an exact-match (* MUST track runtime/src/wob.h's WOB_VERSION — the loader is an exact-match
check, so a drift here is not a warning, it is every image refused. check, so a drift here is not a warning, it is every image refused.
v7 (databasev2 2): two class flag bits, no layout change. *) v7 (databasev2 2): two class flag bits, no layout change.
let wob_version = 7 v8 (databasev2 2 task 6a): the table bit, no layout change. *)
let wob_version = 8
let wob_hdr_size = 44 let wob_hdr_size = 44
let wob_none = 0xFFFFFFFF let wob_none = 0xFFFFFFFF
@ -173,6 +174,9 @@ let classf_gc = 0x01
(* databasev2 2: spare bits of the same flags word — see runtime/src/wob.h *) (* databasev2 2: spare bits of the same flags word — see runtime/src/wob.h *)
let classf_volatile = 0x02 let classf_volatile = 0x02
let classf_resident_keys = 0x04 let classf_resident_keys = 0x04
(* v8: has @table. The runtime's durability rules apply to these classes only;
the two bits above are meaningful — and loader-accepted — only with it. *)
let classf_table = 0x08
let op_nop = 0 let op_nop = 0
let op_loadk = 1 let op_loadk = 1
@ -5093,7 +5097,8 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
Buf.u32 cls Buf.u32 cls
((if c.cr_gc then classf_gc else 0) ((if c.cr_gc then classf_gc else 0)
lor (if c.cr_durable then 0 else classf_volatile) lor (if c.cr_durable then 0 else classf_volatile)
lor (if c.cr_resident_keys then classf_resident_keys else 0)); lor (if c.cr_resident_keys then classf_resident_keys else 0)
lor (if c.cr_is_table then classf_table else 0));
Buf.u32 cls (Array.length c.cr_fields); Buf.u32 cls (Array.length c.cr_fields);
Array.iter (fun (_, ty) -> Buf.u8 cls (field_kind p ty)) c.cr_fields; Array.iter (fun (_, ty) -> Buf.u8 cls (field_kind p ty)) c.cr_fields;
let pad = (4 - (Array.length c.cr_fields mod 4)) mod 4 in let pad = (4 - (Array.length c.cr_fields mod 4)) mod 4 in

View file

@ -2402,7 +2402,7 @@ let validate_image (img : string) : string list =
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
let none = 0xFFFFFFFF in let none = 0xFFFFFFFF in
if u32 0 <> 0x31424F57 then fail "bad magic"; if u32 0 <> 0x31424F57 then fail "bad magic";
if u32 4 <> 7 then fail "unsupported version"; (* v7: databasev2 2 *) if u32 4 <> 8 then fail "unsupported version"; (* v8: databasev2 2 task 6a *)
let coff = u32 8 and ccnt = u32 12 in let coff = u32 8 and ccnt = u32 12 in
let koff = u32 16 and kcnt = u32 20 in let koff = u32 16 and kcnt = u32 20 in
let ioff = u32 24 and icnt = u32 28 in let ioff = u32 24 and icnt = u32 28 in
@ -2439,13 +2439,16 @@ let validate_image (img : string) : string list =
let nm = u32 !o and flags = u32 (!o + 4) and fcnt = u32 (!o + 8) in let nm = u32 !o and flags = u32 (!o + 4) and fcnt = u32 (!o + 8) in
o := !o + 12; o := !o + 12;
if not (text_const nm) then fail (Printf.sprintf "class %d: bad name constant" i); if not (text_const nm) then fail (Printf.sprintf "class %d: bad name constant" i);
(* v7 (databasev2 2): bit1 VOLATILE, bit2 RESIDENT_KEYS. This battery is a (* v7 (databasev2 2): bit1 VOLATILE, bit2 RESIDENT_KEYS; v8 (task 6a): bit3
deliberately independent reimplementation of runtime/src/loader.c's TABLE. This battery is a deliberately independent reimplementation of
validation, so it tracks the same contract — including refusing the pair runtime/src/loader.c's validation, so it tracks the same contract —
that would leave rows neither logged nor resident. *) including refusing the pair that would leave rows neither logged nor
if flags land lnot 0x07 <> 0 then fail (Printf.sprintf "class %d: unknown flags" i); resident, and storage bits on a class that is not a @table. *)
if flags land lnot 0x0f <> 0 then fail (Printf.sprintf "class %d: unknown flags" i);
if flags land 0x02 <> 0 && flags land 0x04 <> 0 then if flags land 0x02 <> 0 && flags land 0x04 <> 0 then
fail (Printf.sprintf "class %d: durable:false with resident:keys" i); fail (Printf.sprintf "class %d: durable:false with resident:keys" i);
if flags land 0x06 <> 0 && flags land 0x08 = 0 then
fail (Printf.sprintf "class %d: storage flags on a class that is not a @table" i);
if fcnt > 65535 then fail (Printf.sprintf "class %d: too many fields" i); if fcnt > 65535 then fail (Printf.sprintf "class %d: too many fields" i);
class_fields.(i) <- fcnt; class_fields.(i) <- fcnt;
let kco = !o in (* the kind bytes' offset: the v3 index walk re-reads them *) let kco = !o in (* the kind bytes' offset: the v3 index walk re-reads them *)

View file

@ -11,11 +11,11 @@
All integers little-endian; offsets are absolute file offsets. All integers little-endian; offsets are absolute file offsets.
**Header (44 bytes):** magic `"WOB1"`, version 7 (databasev2 2; see "v7: table storage flags" below — v6 was iteration 36's "v6: the Int bitwise set", v5 iteration 19's "v5: Float and Bytes"), then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none). **Header (44 bytes):** magic `"WOB1"`, version 8 (databasev2 2 task 6a; see "v8: the table bit" below — v7 was databasev2 2's "v7: table storage flags", v6 was iteration 36's "v6: the Int bitwise set", v5 iteration 19's "v5: Float and Bytes"), then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none).
**Constant pool** — sequential entries: one tag byte; tag 0 = i64 follows; tag 1 = text (u32 length + bytes, no NUL); tag 2 = f64 as its IEEE 754 bit pattern in an LE u64 (v5). There is no Bytes tag: Bytes has no literal form. **Constant pool** — sequential entries: one tag byte; tag 0 = i64 follows; tag 1 = text (u32 length + bytes, no NUL); tag 2 = f64 as its IEEE 754 bit pattern in an LE u64 (v5). There is no Bytes tag: Bytes has no literal form.
**Class table** — per class: name constant index, flags u32 (bit0 = instances are `@gc`; **bit1 = `@table(durable: false)`, bit2 = `@table(resident: keys)`** — v7, and 0 in both means the pre-v7 behaviour of durable-and-fully-resident), field count, then one kind byte per field padded to a 4-byte boundary, then **three u32 arrays of per-field metadata** (v2), one entry per field each, in declaration order: **Class table** — per class: name constant index, flags u32 (bit0 = instances are `@gc`; **bit1 = `@table(durable: false)`, bit2 = `@table(resident: keys)`** — v7, and 0 in both means the pre-v7 behaviour of durable-and-fully-resident; **bit3 = the class has `@table`** — v8, required by bit1/bit2, clear on every class that is not a table), field count, then one kind byte per field padded to a 4-byte boundary, then **three u32 arrays of per-field metadata** (v2), one entry per field each, in declaration order:
1. `field_names[i]` — constant index of the field's name, or all-ones for "not recorded" (what a hand-built test image writes). 1. `field_names[i]` — constant index of the field's name, or all-ones for "not recorded" (what a hand-built test image writes).
2. `field_class[i]` — the class id the field refers to: its own class for an OWNED/GCREF field, its *element's* class for a container of records; `0xFFFFFFFE` marks a `json.Value` field, whose Text holds a raw JSON slice; `0xFFFFFFFD` a nullable scalar (`WO_NIL_SCALAR` nil); `0xFFFFFFFC` a plain `Bool` (json encodes `true`/`false`); `0xFFFFFFFB` a `?Bool` (both); `0xFFFFFFFA` a `?Float` (v5 — nil is `WO_NIL_FLOAT`, not `WO_NIL_SCALAR`); all-ones for none. 2. `field_class[i]` — the class id the field refers to: its own class for an OWNED/GCREF field, its *element's* class for a container of records; `0xFFFFFFFE` marks a `json.Value` field, whose Text holds a raw JSON slice; `0xFFFFFFFD` a nullable scalar (`WO_NIL_SCALAR` nil); `0xFFFFFFFC` a plain `Bool` (json encodes `true`/`false`); `0xFFFFFFFB` a `?Bool` (both); `0xFFFFFFFA` a `?Float` (v5 — nil is `WO_NIL_FLOAT`, not `WO_NIL_SCALAR`); all-ones for none.
@ -338,3 +338,29 @@ refuses this at compile time (WO-E102), and the loader refuses it again on the
standing principle that what the loader accepts, the interpreter trusts. Both standing principle that what the loader accepts, the interpreter trusts. Both
paths are gate-verified — the loader's by forging the flags word in an paths are gate-verified — the loader's by forging the flags word in an
otherwise valid image, since `woc` will not emit one. otherwise valid image, since `woc` will not emit one.
## v8: the table bit (databasev2 2 task 6a)
Again no layout change: one more spare bit of the class descriptor's `flags`
u32.
**What v8 adds**
- `flags` bit3 — `WO_CLASSF_TABLE`: the class has `@table`; its instances are
row ids and the runtime's durability rules apply to it. `woc` sets it from
the class record's `cr_is_table`.
- bit1 and bit2 now **require** bit3. A plain class, a variant class and a
predeclared record (`Error`, `Stat`, …) have all three clear.
**Why.** `durable: true` is the default for a `@table`, and v7 spelled it as
the *absence* of bit1 — which every non-table class also has. When the runtime
started refusing a durable table without `WO_DATA` (task 6a) it had no way to
tell `@table class Notes` from `class Tick`, and every class-bearing program
refused. The bit is the missing fact, recorded where the other two are.
**Refused by the loader, independently of the compiler:** bit1 or bit2 set
with bit3 clear ("storage flags on a class that is not a @table"). A v7 image
is refused by the exact-match version check rather than read with bit3 clear —
read that way it would contain no tables at all and silently skip every
durability rule, the mirror image of the failure the v7 bump guarded against.
Gate-verified by forging the flags word (`runtime/test/test_loader.c`).

View file

@ -205,6 +205,16 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
if ((flags & WO_CLASSF_VOLATILE) && (flags & WO_CLASSF_RESIDENT_KEYS)) if ((flags & WO_CLASSF_VOLATILE) && (flags & WO_CLASSF_RESIDENT_KEYS))
BAIL("class %u: durable:false with resident:keys — rows would have " BAIL("class %u: durable:false with resident:keys — rows would have "
"nowhere to be read from", (unsigned)i); "nowhere to be read from", (unsigned)i);
/* v8 (databasev2 2 task 6a): the storage bits describe a @table's
* rows, so on a class without WO_CLASSF_TABLE they describe nothing —
* refuse rather than let main.c's refusal loops see a table that is
* not one. A v7 image (no table bit) is refused by the version check
* above, the same way task 3's v7 refused v6: read with the bit clear
* it would have no tables and silently skip every durability rule. */
if ((flags & (WO_CLASSF_VOLATILE | WO_CLASSF_RESIDENT_KEYS)) &&
!(flags & WO_CLASSF_TABLE))
BAIL("class %u: storage flags on a class that is not a @table",
(unsigned)i);
if (fcnt > 65535) BAIL("class %u: too many fields", (unsigned)i); if (fcnt > 65535) BAIL("class %u: too many fields", (unsigned)i);
if (fcnt > k.len - k.off) BAIL("class %u: truncated kinds", (unsigned)i); if (fcnt > k.len - k.off) BAIL("class %u: truncated kinds", (unsigned)i);
for (uint32_t j = 0; j < fcnt; j++) for (uint32_t j = 0; j < fcnt; j++)

View file

@ -243,10 +243,13 @@ int main(int argc, char **argv) {
* serving program writes to sockets whose peers vanish. */ * serving program writes to sockets whose peers vanish. */
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
/* The database engine boots with the VM: every class IS a table. /* The database engine boots with the VM: every class IS a table.
* Durability is opt-in — WO_DATA=<dir> opens <dir>/shard-0.wal, * Durability is the default — WO_DATA=<dir> opens <dir>/shard-0.wal,
* replays it before the entry runs (boot-before-listeners doctrine), * replays it before the entry runs (boot-before-listeners doctrine),
* and every insert commits before it acknowledges. Without WO_DATA * and every insert commits before it acknowledges. A program with any
* the engine runs RAM-only, which is what the corpus expects. * durable @table (the default) refuses to start without WO_DATA;
* WO_EPHEMERAL=1 opts into a RAM-only run (the corpus's mode), and
* @table(durable: false) opts a table out. A class without @table is
* storage for the engine but never a durable table (v8 WO_CLASSF_TABLE).
* Arc stage 3 obligation: this whole block runs BEFORE the worker * Arc stage 3 obligation: this whole block runs BEFORE the worker
* shards spawn — replay completes before anything can serve, and the * shards spawn — replay completes before anything can serve, and the
* engine's immutable class-table pointer is published to the worker * engine's immutable class-table pointer is published to the worker
@ -262,6 +265,16 @@ int main(int argc, char **argv) {
VM.rt.db = &DB; VM.rt.db = &DB;
DB.rt = &VM.rt; /* databasev2 2 (5c): the loop a borrow reads the WAL through */ DB.rt = &VM.rt; /* databasev2 2 (5c): the loop a borrow reads the WAL through */
const char *data_dir = getenv("WO_DATA"); const char *data_dir = getenv("WO_DATA");
const char *ephemeral = getenv("WO_EPHEMERAL");
if (data_dir && data_dir[0] && ephemeral) {
/* databasev2 2 (6a): the two knobs answer the same question with
* opposite answers — refuse rather than pick one silently. */
fprintf(stderr, "wovm: WO_EPHEMERAL=1 is incompatible with WO_DATA\n");
wo_db_destroy(&DB);
wo_vm_destroy(&VM);
wo_module_free(&mod);
return 2;
}
if (!data_dir || !data_dir[0]) { if (!data_dir || !data_dir[0]) {
/* databasev2 3: the loader used to refuse `resident: keys` outright; /* databasev2 3: the loader used to refuse `resident: keys` outright;
* now it is accepted because UPDATE landed, but every row still * now it is accepted because UPDATE landed, but every row still
@ -269,6 +282,9 @@ int main(int argc, char **argv) {
* durable:false+resident:keys refusal does, rather than let reads * durable:false+resident:keys refusal does, rather than let reads
* silently misbehave with no WAL to fold from. */ * silently misbehave with no WAL to fold from. */
for (uint32_t i = 0; i < mod.class_cnt; i++) { for (uint32_t i = 0; i < mod.class_cnt; i++) {
/* v8: the storage bits are a @table's; the loader refuses them
* on anything else, so the table check here is belt and braces */
if (!(mod.classes[i].flags & WO_CLASSF_TABLE)) continue;
if (!(mod.classes[i].flags & WO_CLASSF_RESIDENT_KEYS)) continue; if (!(mod.classes[i].flags & WO_CLASSF_RESIDENT_KEYS)) continue;
const char *cname = "?"; const char *cname = "?";
int cnlen = 1; int cnlen = 1;
@ -286,6 +302,58 @@ int main(int argc, char **argv) {
wo_module_free(&mod); wo_module_free(&mod);
return 2; return 2;
} }
/* databasev2 2 (6a): a durable table (the default) without WO_DATA
* used to run RAM-only and drop every write at exit — the one
* outcome `durable: true` promises against. Refuse by name unless
* the developer opted into RAM-only explicitly. Startup-only:
* db.c's `w && table_is_durable` guards are untouched, so the RAM
* path under WO_EPHEMERAL=1 is byte-for-byte the old one. After the
* keys loop on purpose: a keys-resident table has nowhere to read
* from at all, which is the more specific refusal and is not
* rescued by WO_EPHEMERAL. `durable: true` is a @table property (v8
* WO_CLASSF_TABLE): a plain class, variant or predeclared record is
* not a table, so a program with no durable table starts as it
* always did and WO_EPHEMERAL is not consulted at all. */
uint32_t first_durable = mod.class_cnt;
for (uint32_t i = 0; i < mod.class_cnt; i++) {
if (!(mod.classes[i].flags & WO_CLASSF_TABLE)) continue;
if (mod.classes[i].flags & WO_CLASSF_VOLATILE) continue;
first_durable = i;
break;
}
if (first_durable < mod.class_cnt && ephemeral && strcmp(ephemeral, "1") != 0) {
fprintf(stderr,
"wovm: WO_EPHEMERAL=%s is not accepted — the only accepted "
"value is WO_EPHEMERAL=1.\n",
ephemeral);
wo_db_destroy(&DB);
wo_vm_destroy(&VM);
wo_module_free(&mod);
return 2;
}
if (first_durable < mod.class_cnt && ephemeral) {
fprintf(stderr,
"wovm: WO_EPHEMERAL=1 — durable tables served from RAM, "
"nothing is written.\n");
} else if (first_durable < mod.class_cnt) {
const char *cname = "?";
int cnlen = 1;
uint32_t k = mod.classes[first_durable].name;
if (k < mod.const_cnt && mod.consts[k].s) {
cname = mod.consts[k].s->data;
cnlen = (int)mod.consts[k].s->len;
}
fprintf(stderr,
"wovm: `%.*s` is a durable table (the default) and WO_DATA "
"is not set — set WO_DATA=<dir or file> to keep its rows, "
"WO_EPHEMERAL=1 to run RAM-only, or declare it "
"@table(durable: false).\n",
cnlen, cname);
wo_db_destroy(&DB);
wo_vm_destroy(&VM);
wo_module_free(&mod);
return 2;
}
} }
wo_schema compiled_schema; wo_schema compiled_schema;
int have_schema = 0; int have_schema = 0;

View file

@ -63,7 +63,9 @@ typedef struct wo_rt {
void *out; /* FILE*; kept void* so obj.h needn't pull in stdio */ void *out; /* FILE*; kept void* so obj.h needn't pull in stdio */
/* the database engine's handles (database/src), opaque here so the VM /* the database engine's handles (database/src), opaque here so the VM
core needn't include engine headers: db = wo_db*, wal = wo_wal*. core needn't include engine headers: db = wo_db*, wal = wo_wal*.
NULL = engine absent (test binaries) / durability off (no WO_DATA). NULL = engine absent (test binaries) / RAM-only under WO_EPHEMERAL=1
(a program with any durable table — the default — refuses to start
without WO_DATA; @table(durable: false) opts a table out).
Set by main.c at boot; db.c casts. */ Set by main.c at boot; db.c casts. */
void *db; void *db;
void *wal; void *wal;

View file

@ -13,7 +13,14 @@
/* ---- file header (44 bytes, absolute offsets) ---- */ /* ---- file header (44 bytes, absolute offsets) ---- */
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */ #define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
#define WOB_VERSION 7u /* v7 (databasev2 2): two class flag bits — #define WOB_VERSION 8u /* v8 (databasev2 2 task 6a): one class flag bit —
* WO_CLASSF_TABLE (the class has @table: a row-bearing class). No layout
* change. Needed because `durable: true` is a property of @table classes
* only, and v7 gave the runtime no way to tell a table from a plain class:
* the no-WO_DATA refusal fired on every class-bearing program. A v7 image is
* refused by the exact-match check, as v6 was by v7 (task 3): read with the
* bit clear it would have no tables at all and silently skip every refusal.
* v7 (databasev2 2): two class flag bits —
* WO_CLASSF_VOLATILE (@table durable: false) and WO_CLASSF_RESIDENT_KEYS * WO_CLASSF_VOLATILE (@table durable: false) and WO_CLASSF_RESIDENT_KEYS
* (@table resident: keys). No layout change: both ride spare bits of the * (@table resident: keys). No layout change: both ride spare bits of the
* class descriptor's existing `flags` u32, so the serialized shape is * class descriptor's existing `flags` u32, so the serialized shape is
@ -655,7 +662,11 @@ typedef struct wo_classdesc {
flags word means today's behaviour: durable, every row resident. */ flags word means today's behaviour: durable, every row resident. */
#define WO_CLASSF_VOLATILE 0x02u /* @table(durable: false) — never logged */ #define WO_CLASSF_VOLATILE 0x02u /* @table(durable: false) — never logged */
#define WO_CLASSF_RESIDENT_KEYS 0x04u /* @table(resident: keys) — rows read from the log */ #define WO_CLASSF_RESIDENT_KEYS 0x04u /* @table(resident: keys) — rows read from the log */
#define WO_CLASSF_ALL 0x07u /* v8 (databasev2 2 task 6a): has @table — a row-bearing class. The two storage
bits above require it; a plain class, variant or predeclared record has all
three clear and is never a durable table. */
#define WO_CLASSF_TABLE 0x08u
#define WO_CLASSF_ALL 0x0fu
/* runtime object layout: 16-byte header then one 8-byte slot per field */ /* runtime object layout: 16-byte header then one 8-byte slot per field */
static inline size_t wo_obj_size(const wo_classdesc *c) { static inline size_t wo_obj_size(const wo_classdesc *c) {

View file

@ -109,8 +109,44 @@ static void test_rejects(void) {
free(img); 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) { int main(void) {
test_happy_path(); test_happy_path();
test_rejects(); test_rejects();
test_storage_flags_need_table();
return t_report("test_loader"); return t_report("test_loader");
} }