diff --git a/compiler/src/disasm.ml b/compiler/src/disasm.ml index af5bd55..fcf74a7 100644 --- a/compiler/src/disasm.ml +++ b/compiler/src/disasm.ml @@ -184,7 +184,7 @@ let dump (img : string) : string = 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. *) (* 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 koff = u32 img 16 and kcnt = u32 img 20 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 2 <> 0 then [ "volatile" ] else []) @ (if flags land 4 <> 0 then [ "resident=keys" ] else []) + @ (if flags land 8 <> 0 then [ "table" ] else []) in if parts = [] then "-" else String.concat "+" parts) (String.concat ", " fields)) diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 45dae28..f5d974c 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -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 *) (* 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. - v7 (databasev2 2): two class flag bits, no layout change. *) -let wob_version = 7 + v7 (databasev2 2): two class flag bits, no layout change. + v8 (databasev2 2 task 6a): the table bit, no layout change. *) +let wob_version = 8 let wob_hdr_size = 44 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 *) let classf_volatile = 0x02 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_loadk = 1 @@ -5093,7 +5097,8 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols) Buf.u32 cls ((if c.cr_gc then classf_gc else 0) 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); 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 diff --git a/compiler/test/runner.ml b/compiler/test/runner.ml index 305a510..63e1817 100644 --- a/compiler/test/runner.ml +++ b/compiler/test/runner.ml @@ -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 none = 0xFFFFFFFF in 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 koff = u32 16 and kcnt = u32 20 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 o := !o + 12; 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 - deliberately independent reimplementation of runtime/src/loader.c's - validation, so it tracks the same contract — including refusing the pair - that would leave rows neither logged nor resident. *) - if flags land lnot 0x07 <> 0 then fail (Printf.sprintf "class %d: unknown flags" i); + (* v7 (databasev2 2): bit1 VOLATILE, bit2 RESIDENT_KEYS; v8 (task 6a): bit3 + TABLE. This battery is a deliberately independent reimplementation of + runtime/src/loader.c's validation, so it tracks the same contract — + including refusing the pair that would leave rows neither logged nor + 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 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); class_fields.(i) <- fcnt; let kco = !o in (* the kind bytes' offset: the v3 index walk re-reads them *) diff --git a/docs/plan/oop-vm/00-wob-format.md b/docs/plan/oop-vm/00-wob-format.md index ff1fcaf..e9a593c 100644 --- a/docs/plan/oop-vm/00-wob-format.md +++ b/docs/plan/oop-vm/00-wob-format.md @@ -11,11 +11,11 @@ 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. -**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). 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 paths are gate-verified — the loader's by forging the flags word in an 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`). diff --git a/runtime/src/loader.c b/runtime/src/loader.c index 4792a2a..5cf6348 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -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)) BAIL("class %u: durable:false with resident:keys — rows would have " "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 > k.len - k.off) BAIL("class %u: truncated kinds", (unsigned)i); for (uint32_t j = 0; j < fcnt; j++) diff --git a/runtime/src/main.c b/runtime/src/main.c index aaeafa4..fc58687 100644 --- a/runtime/src/main.c +++ b/runtime/src/main.c @@ -243,10 +243,13 @@ int main(int argc, char **argv) { * serving program writes to sockets whose peers vanish. */ signal(SIGPIPE, SIG_IGN); /* The database engine boots with the VM: every class IS a table. - * Durability is opt-in — WO_DATA=