feat(woc): @table durable/resident arguments, defaults preserve behaviour
Task 1 of docs/superpowers/plans/2026-08-26-table-residency.md. - ast.ml: `table_cfg` gains `durable : bool` (default true) and `resident : residency` (ResAll | ResKeys, default ResAll) — both defaulting to the pre-existing behaviour, which is what lets every @table written before this compile byte-identically - parser.ml: `durable:` takes the existing KwTrue/KwFalse tokens; `resident:` takes the bare identifiers `all`/`keys`. Given-twice tracked by local seen flags rather than option fields, so "absent" and "explicitly the default" stay distinguishable without the AST carrying an option nobody reads - five new WO-E102 causes, all catalogued in the same commit: durable twice, resident twice, an unknown resident value, `resident: index` (the pre-review spelling, with a message naming its replacement), and a retired design word (mode/store/ram/cold/tiered/paged/mmap/buffer) which gets a message stating the two real keys instead of a generic "unknown argument" - dump.ml prints each property ONLY when it differs from its default. Printing unconditionally would have moved every pre-existing golden, which this iteration is not allowed to do - new golden compiler/test/golden/ast/table-residency.wo covers all four shapes, including a table declaring `resident: all` explicitly and correctly dumping nothing for it - verified, not assumed: `git diff --stat` over compiler/test/golden/ is EMPTY after a WOC_BLESS run, so all 30 pre-existing goldens are untouched. woc-test 557/0 (was 556), oop-e2e 116/0, employee 8/0; employee, db-bench, db-actor and porch all still typecheck - docs: language-surface's @table row now matches what the parser accepts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1ee7cce597
commit
37f7267115
8 changed files with 114 additions and 6 deletions
|
|
@ -518,9 +518,23 @@ type method_decl = {
|
|||
known keys; anything else inside `@table(...)` is a parse error
|
||||
(WO-E1xx), not a silent skip — unlike an unrecognized annotation
|
||||
*name*, which does skip silently (rt convention, see parser.ml). *)
|
||||
(* databasev2 2: what a table keeps in memory. `ResAll` is every row resident
|
||||
(the default, and what every table did before this existed); `ResKeys` keeps
|
||||
the id map, the secondary indexes and the unique shadows resident and reads
|
||||
rows back from the log by offset. Named `keys` and not `index` on review —
|
||||
`index:` is already an argument key, so the value would have collided. *)
|
||||
type residency = ResAll | ResKeys
|
||||
|
||||
type table_cfg = {
|
||||
table_name : string option;
|
||||
indexes : string list list;
|
||||
(* databasev2 2. Both DEFAULT to the pre-existing behaviour, which is what
|
||||
lets every `@table` written before this compile byte-identically:
|
||||
`durable = true` logs to the WAL as always, `resident = ResAll` keeps
|
||||
every row in a slab as always. dump.ml prints them only when they differ
|
||||
from these values, so no golden moves either. *)
|
||||
durable : bool;
|
||||
resident : residency;
|
||||
}
|
||||
|
||||
type class_decl = {
|
||||
|
|
|
|||
|
|
@ -362,7 +362,14 @@ let annotations_header (is_gc : bool) (table : Ast.table_cfg option) : string =
|
|||
let index_parts =
|
||||
List.map (fun cols -> Printf.sprintf "index=[%s]" (String.concat ", " cols)) t.indexes
|
||||
in
|
||||
let parts = name_part @ index_parts in
|
||||
(* databasev2 2: print these ONLY when they differ from the default.
|
||||
Printing them unconditionally would move every pre-existing golden,
|
||||
which is the one thing this iteration is not allowed to do. *)
|
||||
let durable_part = if t.durable then [] else [ "durable=false" ] in
|
||||
let resident_part =
|
||||
match t.resident with Ast.ResAll -> [] | Ast.ResKeys -> [ "resident=keys" ]
|
||||
in
|
||||
let parts = name_part @ index_parts @ durable_part @ resident_part in
|
||||
if parts = [] then " @table" else " @table(" ^ String.concat ", " parts ^ ")"
|
||||
in
|
||||
gc_part ^ table_part
|
||||
|
|
|
|||
|
|
@ -299,8 +299,20 @@ let skip_paren_args (st : state) : unit =
|
|||
done
|
||||
end
|
||||
|
||||
(* databasev2 2: the retired vocabulary. The brainstorm explored `ram`, `cold`,
|
||||
`tiered`, `paged`, `mmap` and `buffer` as `@table` modes and settled on two
|
||||
keys instead. Naming them here buys a message that says what to write, so a
|
||||
word from a rejected design does not turn into folklore in user code. *)
|
||||
let retired_table_words = [ "ram"; "cold"; "tiered"; "paged"; "mmap"; "buffer"; "mode"; "store" ]
|
||||
|
||||
let parse_table_cfg (st : state) : Ast.table_cfg =
|
||||
let cfg = ref { Ast.table_name = None; indexes = [] } in
|
||||
let cfg =
|
||||
ref { Ast.table_name = None; indexes = []; durable = true; resident = Ast.ResAll }
|
||||
in
|
||||
(* seen-flags, not `option` fields: both properties have a real default, so
|
||||
absence and "explicitly set to the default" must stay distinguishable for
|
||||
the given-twice check without making the AST carry an option nobody reads *)
|
||||
let saw_durable = ref false and saw_resident = ref false in
|
||||
if accept st Token.LParen then begin
|
||||
let continue_ = ref true in
|
||||
while !continue_ do
|
||||
|
|
@ -330,9 +342,49 @@ let parse_table_cfg (st : state) : Ast.table_cfg =
|
|||
if !cols = [] then
|
||||
fail st (peek_pos st) table_code "@table index needs at least one column";
|
||||
cfg := { !cfg with Ast.indexes = !cfg.Ast.indexes @ [ List.rev !cols ] }
|
||||
(* databasev2 2: durability, per table. Replaces the process-global
|
||||
WO_DATA all-or-nothing — a scratch table stops paying the fsync a
|
||||
precious one needs. *)
|
||||
| "durable" ->
|
||||
if !saw_durable then
|
||||
fail st (peek_pos st) table_code "@table(durable: ...) given twice";
|
||||
saw_durable := true;
|
||||
(match peek st with
|
||||
| Token.KwTrue ->
|
||||
ignore (advance st);
|
||||
cfg := { !cfg with Ast.durable = true }
|
||||
| Token.KwFalse ->
|
||||
ignore (advance st);
|
||||
cfg := { !cfg with Ast.durable = false }
|
||||
| _ -> unexpected st "`true` or `false` for @table durable")
|
||||
(* databasev2 2: residency, per table. `keys` is the 120-GB-on-32-GB
|
||||
case — indexes resident, rows read from the log by offset. *)
|
||||
| "resident" ->
|
||||
if !saw_resident then
|
||||
fail st (peek_pos st) table_code "@table(resident: ...) given twice";
|
||||
saw_resident := true;
|
||||
let v = expect_ident st "`all` or `keys` for @table resident" in
|
||||
(match v with
|
||||
| "all" -> cfg := { !cfg with Ast.resident = Ast.ResAll }
|
||||
| "keys" -> cfg := { !cfg with Ast.resident = Ast.ResKeys }
|
||||
| "index" ->
|
||||
fail st (peek_pos st) table_code
|
||||
"@table(resident: index) — renamed to `keys` (it collided with \
|
||||
the `index:` argument); write `resident: keys`"
|
||||
| other ->
|
||||
fail st (peek_pos st) table_code
|
||||
(Printf.sprintf
|
||||
"unknown @table resident value `%s` (supported: all, keys)" other))
|
||||
| other when List.mem other retired_table_words ->
|
||||
fail st (peek_pos st) table_code
|
||||
(Printf.sprintf
|
||||
"`%s` is not a @table argument — storage is declared with two \
|
||||
keys: `durable: true|false` and `resident: all|keys`" other)
|
||||
| other ->
|
||||
fail st (peek_pos st) table_code
|
||||
(Printf.sprintf "unknown @table argument `%s` (supported: name, index)" other));
|
||||
(Printf.sprintf
|
||||
"unknown @table argument `%s` (supported: name, index, durable, \
|
||||
resident)" other));
|
||||
skip_newlines st;
|
||||
if not (accept st Token.Comma) then begin
|
||||
skip_newlines st;
|
||||
|
|
|
|||
9
compiler/test/golden/ast/table-residency.expected
Normal file
9
compiler/test/golden/ast/table-residency.expected
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
6:1 CLASS Order @table(name="orders", index=[customer], resident=keys)
|
||||
7:3 FIELD customer: Text
|
||||
8:3 FIELD total: Int
|
||||
12:1 CLASS Session @table(name="sessions", durable=false)
|
||||
13:3 FIELD token: Text
|
||||
17:1 CLASS Chapter @table(name="chapters", index=[slug])
|
||||
18:3 FIELD slug: Text
|
||||
22:1 CLASS Scratch @table(name="scratch_big", durable=false)
|
||||
23:3 FIELD k: Text
|
||||
24
compiler/test/golden/ast/table-residency.wo
Normal file
24
compiler/test/golden/ast/table-residency.wo
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-- databasev2 2: the two storage arguments. `orders` is the 120-GB-on-32-GB
|
||||
-- shape (indexes resident, rows read from the log); `sessions` is scratch
|
||||
-- (never logged, gone on restart); `chapters` states neither and must dump
|
||||
-- exactly as it did before the arguments existed.
|
||||
@table(name: "orders", index: [customer], resident: keys)
|
||||
class Order {
|
||||
customer: Text
|
||||
total: Int
|
||||
}
|
||||
|
||||
@table(name: "sessions", durable: false)
|
||||
class Session {
|
||||
token: Text
|
||||
}
|
||||
|
||||
@table(name: "chapters", index: [slug])
|
||||
class Chapter {
|
||||
slug: Text
|
||||
}
|
||||
|
||||
@table(name: "scratch_big", durable: false, resident: all)
|
||||
class Scratch {
|
||||
k: Text
|
||||
}
|
||||
|
|
@ -535,7 +535,9 @@ let () =
|
|||
| [ Ast.Class c ] ->
|
||||
check "@table: name and index captured"
|
||||
(match c.table with
|
||||
| Some { Ast.table_name = Some "prices"; indexes = [ [ "sku"; "at" ] ] } -> true
|
||||
(* `; _` so databasev2 2's durable/resident fields do not have to be
|
||||
restated here — this check is about name and index capture only *)
|
||||
| Some { Ast.table_name = Some "prices"; indexes = [ [ "sku"; "at" ] ]; _ } -> true
|
||||
| _ -> false)
|
||||
| _ -> check "@table: exactly one class" false);
|
||||
let _, bad_collector = parse_str ~file:"bad-table.wo" "@table(shard_key: sku)\ntype T {\n id: Id\n}\n" in
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ main(args: multi Text) -> Int`.
|
|||
|
||||
| annotation | where | effect |
|
||||
| --- | --- | --- |
|
||||
| `@table(name: "…", index: [a], index: [b, c])` | on a `class`/`type` | the class IS a WAL-backed table |
|
||||
| `@table(name: "…", index: [a], index: [b, c], durable: true\|false, resident: all\|keys)` | on a `class`/`type` | the class IS a table. Every argument is optional. `durable` (default `true`) decides whether writes are WAL-logged at all — `false` is scratch storage, gone on restart. `resident` (default `all`) decides what is kept in memory — `keys` keeps the id map, secondary indexes and unique shadows resident and reads rows back from the log by offset, which is how a table larger than RAM works. Both defaults are exactly the pre-2026-08-26 behaviour. See [principle 7](../00-principles.md) |
|
||||
| `@unique` | on a field | uniqueness constraint |
|
||||
| `@gc` | on a class | **rejected** — GC-ness is inferred, never declared |
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ half of the story ("moved here" / "borrowed here" / etc.).
|
|||
| 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-E102 | an invalid `@table(...)` configuration: `name` given twice, an `index` with no columns, or an argument key other than `name`/`index`. | `@table(name: ...) given twice` |
|
||||
| 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-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-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/` `` |
|
||||
|
|
|
|||
Loading…
Reference in a new issue