feat(wob): v7 — class descriptor carries durability and residency

Task 3 of docs/superpowers/plans/2026-08-26-table-residency.md.

- NO LAYOUT CHANGE. The plan said to add descriptor fields; the descriptor
  already had a `flags` u32 with only bit0 used, so both properties ride
  spare bits (WO_CLASSF_VOLATILE 0x02, WO_CLASSF_RESIDENT_KEYS 0x04). A v7
  class record is byte-identical in shape to a v6 one, which is a much
  smaller and safer change than the plan assumed
- both spelled as the NON-default, so a zero flags word means exactly what
  every pre-v7 image meant: durable, every row resident. A non-@table class
  has both clear by construction
- the loader refuses the meaningless pair (bit1+bit2) independently of woc,
  on the standing principle that what the loader accepts the interpreter
  trusts. Verified by FORGING the flags word in an otherwise valid image,
  since woc will not emit one: flags=6 gives "durable:false with
  resident:keys", flags=8 still gives "unknown flags"
- WOB_VERSION 6 -> 7. Kept because an OLDER runtime reading a v7 image would
  otherwise treat a volatile table as durable and quietly disagree with its
  own source. loader.c's check is exact-match, so a v6 image is refused
  rather than read with the bits clear — verified by patching a v7 header
  back down to 6

GAP FOUND AND CLOSED: woc ACCEPTED `durable: false, resident: keys`. Task 1's
steps covered duplicates and bad values but never the combination, and the
plan had only put that refusal in the loader. The spec wants both, so the
compiler now refuses it too (WO-E102, checked after the argument list is
complete since it is a property of the pair). A compile error is the one a
developer can act on.

VERSION DRIFT: the constant lives in FOUR places, not one. wob.h,
emit.ml:157, disasm.ml:186, and compiler/test/runner.ml:2405 — the last is a
deliberately independent reimplementation of the loader battery, and it
caught the drift as 14 failures rather than silently passing. Its flags mask
and the combination refusal are now in sync too, which is the point of it
being independent rather than shared.

Gates: woc-test 557/0, 18 runtime suites 0 fail, 18 ISO-flavour suites 0
fail, cli_smoke OK, oop-e2e 118/0, employee 8/0, db-actor 8/0, site 21/0.
Zero goldens moved (git diff over golden/ empty).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-26 23:22:26 +02:00
parent e120129f2c
commit 477f8d1b2b
8 changed files with 113 additions and 13 deletions

View file

@ -183,7 +183,8 @@ let dump (img : string) : string =
set; iteration 19's v5 added the Float constant tag, kinds 6/7 and set; iteration 19's v5 added the Float constant tag, kinds 6/7 and
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. *)
if ver <> 6 then raise (Bad (Printf.sprintf "unsupported version %d" ver)); (* tracks emit.ml's wob_version and wob.h's WOB_VERSION *)
if ver <> 7 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
@ -259,7 +260,12 @@ let dump (img : string) : string =
in in
line line
(Printf.sprintf "c%-3d %s flags=%s fields=[%s]" i (kname nm) (Printf.sprintf "c%-3d %s flags=%s fields=[%s]" i (kname nm)
(if flags land 1 <> 0 then "gc" else "-") (let parts =
(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 [])
in
if parts = [] then "-" else String.concat "+" parts)
(String.concat ", " fields)) (String.concat ", " fields))
done; done;
(* interfaces + vtable rows *) (* interfaces + vtable rows *)

View file

@ -154,7 +154,10 @@ let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
(* v5 (iteration 19): the Float constant tag, field kinds 6/7, opcodes 34-41, (* v5 (iteration 19): the Float constant tag, field kinds 6/7, opcodes 34-41,
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 *)
let wob_version = 6 (* 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
let wob_hdr_size = 44 let wob_hdr_size = 44
let wob_none = 0xFFFFFFFF let wob_none = 0xFFFFFFFF
@ -167,6 +170,9 @@ let k_text = 1
let k_float = 2 let k_float = 2
let max_regs = 64 let max_regs = 64
let classf_gc = 0x01 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
let op_nop = 0 let op_nop = 0
let op_loadk = 1 let op_loadk = 1
@ -393,6 +399,10 @@ let code_push (c : code) (v : int) : unit =
type clsrec = { type clsrec = {
cr_name : string; cr_name : string;
cr_gc : bool; cr_gc : bool;
(* databasev2 2: storage properties, spelled as the DEFAULT here so a
non-table class (union payload records below) trivially gets flags 0 *)
cr_durable : bool;
cr_resident_keys : bool;
cr_fields : (string * Ast.field_ty) array; cr_fields : (string * Ast.field_ty) array;
cr_methods : string list; (* method names, declaration order *) cr_methods : string list; (* method names, declaration order *)
(* iteration 9 Task 4: (unique, column indices) per secondary index — (* iteration 9 Task 4: (unique, column indices) per secondary index —
@ -4765,6 +4775,14 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
cfg.Ast.indexes cfg.Ast.indexes
| None -> ()); | None -> ());
{ cr_name = c.name; cr_gc = Types.is_gc_class syms c.name; { cr_name = c.name; cr_gc = Types.is_gc_class syms c.name;
cr_durable =
(match c.Ast.table with
| Some cfg -> cfg.Ast.durable
| None -> true);
cr_resident_keys =
(match c.Ast.table with
| Some cfg -> cfg.Ast.resident = Ast.ResKeys
| None -> false);
cr_fields = cr_fields =
Array.of_list Array.of_list
(List.filter_map (List.filter_map
@ -4802,7 +4820,8 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
class_id := SM.add key cid !class_id; class_id := SM.add key cid !class_id;
incr nclasses; incr nclasses;
classes := classes :=
{ cr_name = key; cr_gc = false; cr_indexes = []; cr_is_table = false; { cr_name = key; cr_gc = false; cr_durable = true;
cr_resident_keys = false; cr_indexes = []; cr_is_table = false;
cr_backlinks = []; cr_backlinks = [];
cr_fields = Array.of_list vd.Ast.v_fields; cr_fields = Array.of_list vd.Ast.v_fields;
cr_methods = [] } cr_methods = [] }
@ -4846,7 +4865,9 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
class_id := SM.add name cid !class_id; class_id := SM.add name cid !class_id;
incr nclasses; incr nclasses;
classes := classes :=
{ cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = []; (* not a @table (a predeclared record), so storage flags stay 0 *)
{ cr_name = name; cr_gc = false; cr_durable = true; cr_resident_keys = false;
cr_fields = Array.of_list fields; cr_methods = [];
cr_indexes = []; cr_is_table = false; cr_backlinks = [] } cr_indexes = []; cr_is_table = false; cr_backlinks = [] }
:: !classes :: !classes
end) end)
@ -5023,7 +5044,10 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
Array.iteri Array.iteri
(fun cid (c : clsrec) -> (fun cid (c : clsrec) ->
Buf.u32 cls class_name_k.(cid); Buf.u32 cls class_name_k.(cid);
Buf.u32 cls (if c.cr_gc then classf_gc else 0); 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));
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

@ -394,6 +394,16 @@ let parse_table_cfg (st : state) : Ast.table_cfg =
end end
done done
end; end;
(* databasev2 2: rows that are neither logged nor resident have nowhere to
live. Checked here, after the whole argument list is known, because it is
a property of the COMBINATION rather than of either argument. The loader
refuses it again (runtime/src/wob.h, loader.c) on the principle that what
the loader accepts the interpreter trusts — but a compile error is the one
a developer can act on. *)
if (not !cfg.Ast.durable) && !cfg.Ast.resident = Ast.ResKeys then
fail st (peek_pos st) table_code
"@table(durable: false, resident: keys): rows would be neither logged \
nor resident, so there is nowhere to read them from — pick one";
!cfg !cfg
type type_annotations = { type type_annotations = {

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 <> 6 then fail "unsupported version"; (* v6: iteration 36 *) if u32 4 <> 7 then fail "unsupported version"; (* v7: databasev2 2 *)
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,7 +2439,13 @@ 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);
if flags land lnot 0x01 <> 0 then fail (Printf.sprintf "class %d: unknown flags" 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);
if flags land 0x02 <> 0 && flags land 0x04 <> 0 then
fail (Printf.sprintf "class %d: durable:false with resident:keys" 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 6 (iteration 36; see "v6: the Int bitwise set" below — v5 was 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 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).
**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`), 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), 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.
@ -307,3 +307,34 @@ left to fall out accidentally):
magic, or offsets that don't exactly account for every trailing byte) magic, or offsets that don't exactly account for every trailing byte)
is left untouched and copied as-is — the safe default when it's not is left untouched and copied as-is — the safe default when it's not
certain. certain.
## v7: table storage flags (databasev2 2)
The smallest version bump in the format's history: **no layout change at all.**
Both properties ride spare bits of the class descriptor's existing `flags`
u32, so a v7 class record is byte-identical in shape to a v6 one.
**What v7 adds**
- `flags` bit1 — `WO_CLASSF_VOLATILE`: the class is a `@table(durable: false)`.
Its writes are never staged to the WAL and replay skips its records.
- `flags` bit2 — `WO_CLASSF_RESIDENT_KEYS`: the class is a
`@table(resident: keys)`. Its id map, secondary indexes and unique shadows
are resident; its rows are read back from the log by offset.
- Both are spelled as the **non-default**, so a zero flags word means exactly
what every pre-v7 image meant: durable, every row resident. A class that is
not a `@table` must have both clear.
**Why bump at all, given nothing moved?** To stop an older runtime reading a
v7 image and silently treating a volatile table as durable — the one failure
mode where the program keeps running and quietly disagrees with its own source.
The loader would in fact also reject it, because the flags mask check
(`loader.c`) rejects unknown bits and has since v1; but a version refusal names
the real problem instead of blaming the flags.
**Refused by the loader, independently of the compiler:** bit1 and bit2 set
together. Rows that are neither logged nor resident have nowhere to live. `woc`
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.

View file

@ -40,7 +40,7 @@ half of the story ("moved here" / "borrowed here" / etc.).
| code | meaning | example message | | code | meaning | example message |
| --- | --- | --- | | --- | --- | --- |
| WO-E101 | generic syntax error: an unexpected token where the grammar expected something else, including running off the end of the file inside an unclosed block/type/interface body. Declaration-level recovery syncs to the next top-level keyword so one bad declaration yields one diagnostic, not a cascade. | `expected ')' or ',', got NEWLINE` | | WO-E101 | generic syntax error: an unexpected token where the grammar expected something else, including running off the end of the file inside an unclosed block/type/interface body. Declaration-level recovery syncs to the next top-level keyword so one bad declaration yields one diagnostic, not a cascade. | `expected ')' or ',', got NEWLINE` |
| WO-E102 | an invalid `@table(...)` configuration. Original causes: `name` given twice, an `index` with no columns, or an unknown argument key. **databasev2 2 (2026-08-26) added the storage arguments and five more causes under the same code:** `durable` given twice; `resident` given twice; a `resident` value other than `all`/`keys`; `resident: index`, which is the pre-review spelling and gets a message naming its replacement; and a *retired* argument word (`mode`, `store`, `ram`, `cold`, `tiered`, `paged`, `mmap`, `buffer` — vocabulary the design brainstorm explored and rejected), which gets a message stating the two real keys rather than a generic "unknown argument". A non-boolean after `durable:` is WO-E101 from the generic token expectation, not this code. | `` `cold` is not a @table argument — storage is declared with two keys: `durable: true\|false` and `resident: all\|keys` `` | | WO-E102 | an invalid `@table(...)` configuration. Original causes: `name` given twice, an `index` with no columns, or an unknown argument key. **databasev2 2 (2026-08-26) added the storage arguments and five more causes under the same code:** `durable` given twice; `resident` given twice; a `resident` value other than `all`/`keys`; `resident: index`, which is the pre-review spelling and gets a message naming its replacement; and a *retired* argument word (`mode`, `store`, `ram`, `cold`, `tiered`, `paged`, `mmap`, `buffer` — vocabulary the design brainstorm explored and rejected), which gets a message stating the two real keys rather than a generic "unknown argument". A non-boolean after `durable:` is WO-E101 from the generic token expectation, not this code. Also `durable: false` together with `resident: keys` — rows would be neither logged nor resident, checked after the whole argument list is known because it is a property of the combination; the loader refuses the same pair independently (`.wob` v7). | `` `cold` is not a @table argument — storage is declared with two keys: `durable: true\|false` and `resident: all\|keys` `` |
| WO-E103 | haxe-parity Task 2. `inline fn ...` — the haxe keyword verdict table's own reject half of the `inline` row (`const` values are the adopted half). The whole declaration is discarded by the usual top-level recovery, same as any other bad declaration. | `` `inline fn` is rejected — optimization is the compiler's job `` | | WO-E103 | haxe-parity Task 2. `inline fn ...` — the haxe keyword verdict table's own reject half of the `inline` row (`const` values are the adopted half). The whole declaration is discarded by the usual top-level recovery, same as any other bad declaration. | `` `inline fn` is rejected — optimization is the compiler's job `` |
| WO-E106 | iteration 15 (deps, 2026-08-18). A dependency fetch/shape failure, driver-level: missing `git` binary, clone/checkout failure, a locked commit missing from the remote, cache/lock drift (a moved `rev` — the message names both SHAs and points at `woc --update-deps`), a fetched dep that is not a writeonce project, or a dep declaring its own `[deps]` (transitive — refused flat-only). One code; the message names the dependency and the failing step. | `` dependency `niceframework`: lock drift — wo.lock pins <sha> but .wo-deps has <sha> (a moved `rev`?); run `woc --update-deps` or remove .wo-deps/niceframework `` | | WO-E106 | iteration 15 (deps, 2026-08-18). A dependency fetch/shape failure, driver-level: missing `git` binary, clone/checkout failure, a locked commit missing from the remote, cache/lock drift (a moved `rev` — the message names both SHAs and points at `woc --update-deps`), a fetched dep that is not a writeonce project, or a dep declaring its own `[deps]` (transitive — refused flat-only). One code; the message names the dependency and the failing step. | `` dependency `niceframework`: lock drift — wo.lock pins <sha> but .wo-deps has <sha> (a moved `rev`?); run `woc --update-deps` or remove .wo-deps/niceframework `` |
| WO-E107 | iteration 15 (deps). A `[deps]` name collides with a local top-level module directory of the same name — `use <name>` would be ambiguous, so the build refuses instead of silently picking one. | `` dependency `niceframework` collides with the local module directory `niceframework/` `` | | WO-E107 | iteration 15 (deps). A `[deps]` name collides with a local top-level module directory of the same name — `use <name>` would be ambiguous, so the build refuses instead of silently picking one. | `` dependency `niceframework` collides with the local module directory `niceframework/` `` |

View file

@ -177,8 +177,15 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
BAIL("class %u: truncated", (unsigned)i); BAIL("class %u: truncated", (unsigned)i);
if (name >= m->const_cnt || m->consts[name].tag != WOB_K_TEXT) if (name >= m->const_cnt || m->consts[name].tag != WOB_K_TEXT)
BAIL("class %u: bad name constant", (unsigned)i); BAIL("class %u: bad name constant", (unsigned)i);
if (flags & ~WO_CLASSF_GC) if (flags & ~WO_CLASSF_ALL)
BAIL("class %u: unknown flags", (unsigned)i); BAIL("class %u: unknown flags", (unsigned)i);
/* databasev2 2: rows that are neither logged nor resident would have
* nowhere to live. woc refuses this at compile time; the loader
* refuses it again because what the loader accepts, the interpreter
* trusts — this combination must never reach the engine. */
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);
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

@ -13,7 +13,17 @@
/* ---- 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 6u /* v6 (iteration 36): opcodes 42-46 (the Int #define WOB_VERSION 7u /* v7 (databasev2 2): two class flag bits —
* WO_CLASSF_VOLATILE (@table durable: false) and WO_CLASSF_RESIDENT_KEYS
* (@table resident: keys). No layout change: both ride spare bits of the
* class descriptor's existing `flags` u32, so the serialized shape is
* byte-identical to v6. The bump exists to stop an OLDER runtime reading a
* v7 image and silently treating a volatile table as durable — the loader
* would also reject the unknown flag bits, but a version refusal names the
* real problem. A v6 image is refused by the exact-match check rather than
* read with the bits clear, matching how v4-v6 each invalidated their
* predecessors.
* v6 (iteration 36): opcodes 42-46 (the Int
* bitwise set BAND/BOR/BXOR/SHL/SHR), trap kind WO_T_SHIFT. * bitwise set BAND/BOR/BXOR/SHL/SHR), trap kind WO_T_SHIFT.
* v5 (iteration 19): the two missing scalars. New * v5 (iteration 19): the two missing scalars. New
* constant tag WOB_K_FLOAT, field kinds WO_K_FLOAT/WO_K_BYTES (WO_K_MAX 5->7), * constant tag WOB_K_FLOAT, field kinds WO_K_FLOAT/WO_K_BYTES (WO_K_MAX 5->7),
@ -562,6 +572,12 @@ typedef struct wo_classdesc {
const uint32_t *idx_meta; const uint32_t *idx_meta;
} wo_classdesc; } wo_classdesc;
#define WO_CLASSF_GC 0x01u #define WO_CLASSF_GC 0x01u
/* databasev2 2. Both describe STORAGE, so both are meaningless on a class that
is not a @table and must be 0 there. Spelled as the non-default so a zero
flags word means today's behaviour: durable, every row resident. */
#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_ALL 0x07u
/* 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) {