diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index d02879b..91ffca9 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -211,6 +211,11 @@ and expr_kind = | Binary of binop * expr * expr | Ctor of string * (string * expr) list | DbStub of Token.t list + (* `insert Class { field: expr, ... }` — the FIRST DB statement to leave + the stub behind (iteration 9, Task 3). Typed like a constructor + literal, returns the new row's id (Int), legal in statement and + expression position both. `select` stays a DbStub until Task 5. *) + | Insert of string * (string * expr) list (* haxe-parity Task 2: one `${expr}` interpolation site, produced only by the string-interpolation desugar (parser.ml) — never written directly by a parse rule the way every other expr_kind is. Its diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index b2530f2..9914646 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -222,6 +222,10 @@ let rec expr_str (e : Ast.expr) : string = Printf.sprintf "%s { %s }" name (String.concat ", " (List.map (fun (fname, fval) -> Printf.sprintf "%s: %s" fname (expr_str fval)) fields)) + | Ast.Insert (name, fields) -> + Printf.sprintf "INSERT %s { %s }" name + (String.concat ", " + (List.map (fun (fname, fval) -> Printf.sprintf "%s: %s" fname (expr_str fval)) fields)) | Ast.DbStub toks -> Printf.sprintf "DB_STUB(%s)" (dbstub_tokens_str toks) | Ast.Interp inner -> Printf.sprintf "INTERP(%s)" (expr_str inner) | Ast.ListLit items -> Printf.sprintf "[%s]" (String.concat ", " (List.map expr_str items)) diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 70255cc..519cc56 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -257,6 +257,7 @@ let b_map_val_at = 38 let b_multi_set = 39 let b_map_get_opt = 59 let b_text_copy = 60 +let b_db_insert = 61 (* json (runtime/src/json.c): encode takes the value's static kind as its second argument, decode the class id to build as its second. *) @@ -1054,6 +1055,7 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | Eq | Ne | Lt | Le | Gt | Ge | And | Or -> Some (Scalar "Bool") | Add | Sub | Mul | Div | Mod -> ( match ty_of_expr p f l with Some t -> Some t | None -> Some (Scalar "Int"))) | Ctor (cn, _) -> Some (Scalar cn) + | Insert _ -> Some (Scalar "Int") | Interp _ -> Some (Scalar "Text") | DbStub _ -> None | Switch (subject, arms) -> ( @@ -1654,6 +1656,7 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e put f (ins_abc op_neg dst b 0) | Binary (op, l, r) -> emit_binary p f v ~dst op l r | Ctor (cn, fields) -> emit_ctor p f v ~dst e cn fields + | Insert (cn, fields) -> emit_insert p f v ~dst e cn fields | Interp inner -> ( (* haxe-parity Task 2: the type-directed half of the interpolation desugar (parser.ml's own doc comment on Ast.Interp) — a Text @@ -2347,6 +2350,74 @@ and emit_ctor (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) (c ci.Types.fields); f.f_temp <- outer +(* iteration 9 Task 3: `insert Class { ... }` lowers to one DB_INSERT + builtin whose window is [class-id const, then one slot per DECLARED + field in declaration order] — the executor walks the class table's + kinds, so slot order must be the table's, not the literal's. A field + the literal omits gets its default (same emit_default_value the ctor + uses) or, for a `?` field, its kind's own nil (WO_NIL_SCALAR for a + nullable scalar, the zero word otherwise). The engine COPIES every + value at the row API, so after the builtin every freshly built + argument is still this frame's to drop — same reap as push/set. *) +and emit_insert (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) (cn : string) + (fields : (string * Ast.expr) list) : unit = + match class_of_name p cn with + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message:(Printf.sprintf "insert into `%s`, which is not a declared class" cn); + put f (ins_abx op_loadk dst (const_int p 0)) + | Some cid -> + let fcnt = Array.length p.p_classes.(cid).cr_fields in + let base = alloc_temps p f e.pos (fcnt + 1) in + put f (ins_abx op_loadk base (check_bx p f e.pos "constant" (const_int p cid))); + (* every field the literal names lands in ITS declared slot *) + List.iter + (fun ((fname : string), (fe : Ast.expr)) -> + match field_of p cid fname with + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message:(Printf.sprintf "`%s` has no field `%s`" cn fname) + | Some (idx, fty) -> + let save = f.f_temp in + emit_expr p f v ~dst:(base + 1 + idx) ~expected:fty fe; + f.f_temp <- save) + fields; + (* omitted fields: declared default, else the kind's own nil *) + let provided = List.map fst fields in + (match Types.StringMap.find_opt cn p.p_syms.Types.classes with + | None -> () + | Some (ci : Types.class_info) -> + List.iter + (fun (fname, fty, fdefault, _) -> + if not (List.mem fname provided) then + match field_of p cid fname with + | None -> () + | Some (idx, dfty) -> ( + match fdefault with + | Some d -> + let save = f.f_temp in + emit_default_value p f ~dst:(base + 1 + idx) ~fty:dfty ~pos:e.pos d; + f.f_temp <- save + | None -> + let nil_word = + if is_nullable_scalar p fty then const_int p nil_scalar_word + else const_int p 0 + in + put f (ins_abx op_loadk (base + 1 + idx) (check_bx p f e.pos "constant" nil_word)))) + ci.Types.fields); + sync_mask p f v e.id; + f.f_cur_line <- e.pos.line; + put f (ins_abc op_builtin dst base b_db_insert); + (* the engine copied: fresh argument values die here *) + List.iter + (fun ((fname : string), (fe : Ast.expr)) -> + match field_of p cid fname with + | None -> () + | Some (idx, _) -> + drop_fresh_owned ~keep:dst p f (base + 1 + idx) fe; + drop_fresh_text ~keep:dst p f (base + 1 + idx) fe) + fields + (* The default expressions the emitter can lower (haxe-parity Task 4): the literal shapes the sample's own typedefs use — Int (optionally negated), Text, Bool, `now()` (parse_default_expr's own recognized diff --git a/compiler/src/owner.ml b/compiler/src/owner.ml index c268bf3..fb6ef6b 100644 --- a/compiler/src/owner.ml +++ b/compiler/src/owner.ml @@ -560,6 +560,7 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option = | Binary (Concat, _, _) -> Some (Scalar "Text") | Binary _ -> None (* arithmetic/comparison: Copy either way *) | Ctor (cn, _) -> Some (Scalar cn) + | Insert _ -> Some (Scalar "Int") (* the new row's id — Copy, nothing to drop *) | Interp _ -> Some (Scalar "Text") (* an interpolation always produces Text *) | DbStub _ -> None | Switch (subject, arms) -> @@ -1148,6 +1149,14 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit = read_place_parts ctx e | Call (callee, args) -> analyze_call ctx e callee args | Ctor (cn, fields) -> analyze_ctor ctx cn fields + | Insert (_, fields) -> + (* iteration 9 Task 3: the engine copies every field value at the row + API (the two-worlds bulkhead), so an insert BORROWS its values — + no transfer, no E304, the source keeps what it had. Trap-capable + (unique violations arrive with Task 4), so the drop map is + recorded exactly like DbStub's. *) + List.iter (fun (_, fe) -> read_expr ctx fe) fields; + record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx)) | Unary (_, o) -> read_expr ctx o | Binary (_, a, b) -> read_expr ctx a; diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index c4b1695..b0376f7 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -1009,9 +1009,23 @@ and parse_switch_expr (st : state) : Ast.expr = done; { Ast.id; pos; kind = Ast.Switch (subject, List.rev !arms) } +and parse_insert_expr (st : state) : Ast.expr = + (* `insert` + a constructor literal, sharing parse_ctor_literal so the + field-list grammar (trailing commas, newlines) can never drift from the + ctor's. The literal's node is unwrapped into Insert — its id is reused, + which is safe because the Ctor node itself is discarded whole. *) + let pos = peek_pos st in + ignore (advance st) (* the `insert` trigger token *); + skip_newlines st; + let lit = parse_ctor_literal st in + (match lit.Ast.kind with + | Ast.Ctor (cn, fields) -> { lit with Ast.pos; kind = Ast.Insert (cn, fields) } + | _ -> lit (* unreachable: parse_ctor_literal only builds Ctor *)) + and parse_primary (st : state) : Ast.expr = match peek st with | k when is_select_trigger k -> parse_dbstub_expr st + | k when is_insert_trigger k -> parse_insert_expr st | Token.KwSwitch -> parse_switch_expr st | Token.Int n -> let pos = peek_pos st in @@ -1286,7 +1300,7 @@ and parse_stmt (st : state) : Ast.stmt = | k when is_insert_trigger k -> let pos = peek_pos st in let id = fresh_id st in - let e = parse_dbstub_expr st in + let e = parse_insert_expr st in end_of_stmt st; { Ast.s_id = id; s_pos = pos; s_kind = Ast.ExprStmt e } | Token.KwLet -> parse_let_stmt st @@ -1785,6 +1799,8 @@ let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : As { e with Ast.kind = Ast.Binary (op, subst_expr consts bound l, subst_expr consts bound r) } | Ast.Ctor (cn, fields) -> { e with Ast.kind = Ast.Ctor (cn, List.map (fun (n, v) -> (n, subst_expr consts bound v)) fields) } + | Ast.Insert (cn, fields) -> + { e with Ast.kind = Ast.Insert (cn, List.map (fun (n, v) -> (n, subst_expr consts bound v)) fields) } | Ast.Interp inner -> { e with Ast.kind = Ast.Interp (subst_expr consts bound inner) } | Ast.ListLit items -> { e with Ast.kind = Ast.ListLit (List.map (subst_expr consts bound) items) } | Ast.MapLit | Ast.NilLit -> e diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 3a5395f..f1f492b 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -1165,6 +1165,9 @@ let typecheck_program ~file ~(module_of : string -> string) needs `int_to_text` first) -- unlike the placeholders below, this is a fact, not a guess. *) Some (TScalar "Text") + | Insert _ -> + (* the new row's id — the one thing an insert produces *) + Some (TScalar "Int") | Unary _ | Binary _ | DbStub _ -> (* Not chased: the arithmetic-ladder `Binary` ops have no reliable per-node type in this pass at all (see above); `Unary`/`DbStub` @@ -1405,6 +1408,30 @@ let typecheck_program ~file ~(module_of : string -> string) (Diag.error ~code:unknown_type_code ~file ~line:e.pos.line ~col:e.pos.col ~message:(Printf.sprintf "unknown type `%s` in constructor" class_name) ()); { typ = TScalar "Int"; is_nil = false }) + | Insert (class_name, fields) -> + (* iteration 9 Task 3: typed exactly like a constructor literal — + same missing-field rule (defaults and `?` fields omittable), + same unknown-class diagnostic — but the VALUE is the new row's + id. The engine copies every field at the choke point, so field + values keep their owners (owner.ml's stores_by_copy). *) + (try + let cls = StringMap.find class_name syms.classes in + let provided = List.map (fun (n, _) -> n) fields in + let omittable (default : default_expr option) (fty : field_ty) : bool = + Option.is_some default || (match fty with Nullable _ -> true | _ -> false) + in + List.iter (fun (fname, fty, fdefault, _) -> + if not (List.mem fname provided) && not (omittable fdefault fty) then + Diag.Collector.add collector + (Diag.error ~code:incomplete_ctor_code ~file ~line:e.pos.line ~col:e.pos.col + ~message:(Printf.sprintf "missing field `%s` in insert of `%s`" fname class_name) ()) + ) cls.fields; + { typ = TScalar "Int"; is_nil = false } + with Not_found -> + Diag.Collector.add collector + (Diag.error ~code:unknown_type_code ~file ~line:e.pos.line ~col:e.pos.col + ~message:(Printf.sprintf "unknown type `%s` in insert" class_name) ()); + { typ = TScalar "Int"; is_nil = false }) | DbStub _ -> { typ = TVoid; is_nil = false } | Switch (subject, arms) -> typecheck_switch ~want_value:true env cenv subject arms | ListLit items -> @@ -2102,7 +2129,8 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e | Binary (_, l, r) -> walk_expr bound visit l; walk_expr bound visit r - | Ctor (_, fields) -> List.iter (fun (_, v) -> walk_expr bound visit v) fields + | Ctor (_, fields) | Insert (_, fields) -> + List.iter (fun (_, v) -> walk_expr bound visit v) fields | Interp inner -> walk_expr bound visit inner | ListLit items -> List.iter (walk_expr bound visit) items | MapLit | NilLit -> () diff --git a/compiler/test/golden/ast/db-stub.expected b/compiler/test/golden/ast/db-stub.expected index 17db105..3494df8 100644 --- a/compiler/test/golden/ast/db-stub.expected +++ b/compiler/test/golden/ast/db-stub.expected @@ -1,6 +1,6 @@ 1:1 METHOD sync() - 2:3 DB_STUB IDENT(insert) IDENT(Product) LBRACE IDENT(sku) COLON STR(A1) COMMA IDENT(price) COLON INT(10) RBRACE + 2:3 EXPR INSERT Product { sku: "A1", price: 10 } 3:3 DB_STUB IDENT(select) IDENT(Product) LBRACE IDENT(sku) EQEQ STR(A1) RBRACE 4:3 LET rows = DB_STUB(IDENT(select) IDENT(Product) LBRACE IDENT(price) GT INT(5) RBRACE) - 5:3 DB_STUB KW_INSERT IDENT(Product) LBRACE IDENT(sku) COLON STR(A2) RBRACE + 5:3 EXPR INSERT Product { sku: "A2" } 6:3 DB_STUB KW_SELECT IDENT(Product) LBRACE IDENT(sku) EQEQ STR(A2) RBRACE diff --git a/compiler/test/golden/owner/drops.wo b/compiler/test/golden/owner/drops.wo index f50111f..e487997 100644 --- a/compiler/test/golden/owner/drops.wo +++ b/compiler/test/golden/owner/drops.wo @@ -38,7 +38,7 @@ fn pick(take a: Item, take b: Item, flag: Bool) -> Int { } fn store(take r: Item) -> Int { - insert into rows values (1) + insert Row { n: 1 } return 0 } @@ -63,3 +63,7 @@ fn reinit_after_move(take a: Item) -> Int { a = Item { n: 7 } return 0 } + +class Row { + n: Int +} diff --git a/compiler/test/runner.ml b/compiler/test/runner.ml index 28635c7..0291c31 100644 --- a/compiler/test/runner.ml +++ b/compiler/test/runner.ml @@ -531,35 +531,35 @@ let () = | _ -> check "ctor literal: exactly one free fn" false let () = - (* The brief's stated asymmetry: `insert` is a statement-only trigger - (parser.ml's is_insert_trigger, checked only in parse_stmt) — a - bare `insert` reached from parse_primary is just an ordinary - identifier reference, exactly like self/me/on/service/policy's own - "recognized positionally, not a reserved word" rule (this task's - own keyword-discipline note). `select` (is_select_trigger) is - checked unconditionally *inside* parse_primary, so the same - position always builds a DbStub instead. Neither is an error on - its own — the difference shows up in which Ast.expr_kind comes - back. *) + (* Iteration 9 Task 3 retired the old asymmetry: `insert` is grammar-owned + in BOTH positions now — a typed Insert node validated like a ctor, + returning the id — while `select` stays the opaque DbStub until + Task 5. The old contract ("bare insert is a plain Ident") is gone + with the stub that motivated it. *) let prog, collector = - parse_str ~file:"insert-vs-select.wo" "fn f() {\n let a = insert\n let b = select\n}\n" + parse_str ~file:"insert-vs-select.wo" + "fn f() {\n let a = insert Product { sku: \"A1\" }\n let b = select\n}\n" in - check_eq "insert vs. select as bare expressions: no diagnostics" ~expected:0 + check_eq "typed insert + stub select: no diagnostics" ~expected:0 ~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int; - match prog.Ast.decls with + (match prog.Ast.decls with | [ Ast.Fn m ] -> ( match m.body with | [ { Ast.s_kind = Ast.Let { name = "a"; value = a_val; _ }; _ }; { Ast.s_kind = Ast.Let { name = "b"; value = b_val; _ }; _ }; ] -> - check "bare `insert` in expression position is a plain Ident" - (match a_val.Ast.kind with Ast.Ident "insert" -> true | _ -> false); + check "`insert` in expression position is a typed Insert node" + (match a_val.Ast.kind with Ast.Insert ("Product", [ ("sku", _) ]) -> true | _ -> false); check "bare `select` in expression position always becomes a DbStub" (match b_val.Ast.kind with Ast.DbStub _ -> true | _ -> false) | _ -> check "insert vs. select: exactly two `let` statements" false) - | _ -> check "insert vs. select: exactly one free fn" false + | _ -> check "insert vs. select: exactly one free fn" false); + (* and a bare `insert` with no literal is a parse error now, not an Ident *) + let _, c2 = parse_str ~file:"bare-insert.wo" "fn f() {\n let a = insert\n}\n" in + check "bare `insert` with no constructor literal is a diagnostic" + (List.length (Diag.Collector.diagnostics c2) > 0) let () = (* The no_brace guard (parser.ml's state.no_brace / looks_like_ctor): @@ -2580,7 +2580,12 @@ let validate_image (img : string) : string list = | 22 | 23 | 24 | 25 | 26 | 27 | 28 -> rchk pc a | 29 -> rchk pc a; - if c > 12 then fail (Printf.sprintf "method %d pc %d: builtin out of range" i pc) + (* the mirror's ceiling tracks wob.h's WO_B_MAX only for ids the + golden lowering suite actually emits; 61 = DB_INSERT (arity 1: + the class-id slot — field slots are runtime-validated, same as + the C loader) *) + if c > 12 && c <> 61 then + fail (Printf.sprintf "method %d pc %d: builtin out of range" i pc) else if c = 4 then begin if b > 5 then fail (Printf.sprintf "method %d pc %d: bad element kind" i pc) end @@ -2595,6 +2600,7 @@ let validate_image (img : string) : string list = | 1 | 2 | 3 | 7 | 8 -> 1 | 5 | 6 | 11 | 12 -> 2 | 10 -> 3 + | 61 -> 1 | _ -> 0 in if arity > 0 then begin @@ -2951,7 +2957,8 @@ let () = ( "text: concat, equality, words", "fn f(a: Text, b: Text) -> Int {\n let joined = a .. b\n\ \ if joined == a {\n return 1\n }\n return words(joined)\n}\n" ); - ("db stub statement", "fn f() -> Int {\n insert into rows values (1)\n return 0\n}\n"); + ( "db insert statement", + "class Row {\n n: Int\n}\n\nfn f() -> Int {\n insert Row { n: 1 }\n return 0\n}\n" ); ( "nested calls in arguments", "fn one() -> Int {\n return 1\n}\n\nfn add(a: Int, b: Int) -> Int {\n\ \ return a + b\n}\n\nfn f() -> Int {\n return add(add(one(), one()), one())\n}\n" ); diff --git a/database/src/CODE-LOGIC.md b/database/src/CODE-LOGIC.md index cab777f..ad5987e 100644 --- a/database/src/CODE-LOGIC.md +++ b/database/src/CODE-LOGIC.md @@ -43,6 +43,16 @@ tear). The crash battery in `runtime/test/test_wal.c` is the module's meaning proven: acked-over-a-pipe after commit, SIGKILL mid-stream, replay, zero acked-but-missing. +## db.c — statement executors (iteration 9, Task 3) + +One dispatcher, the builtin contract (0 ok, else WO_T_* + msg). The engine +handles ride `wo_rt.db` / `wo_rt.wal` as opaque pointers set by main.c — +NULL db traps WO_T_DB, NULL wal means RAM-only (the corpus's mode; WO_DATA +opts into durability). Insert's contract: RAM apply through the row API, +then stage + commit BEFORE returning — the builtin's return is the +acknowledgment, so a failed commit un-applies the row and traps WO_T_IO +rather than acknowledging what disk never got. + ## Verifying a change - `make -C runtime test` — `test_table` is this directory's suite (round diff --git a/database/src/db.c b/database/src/db.c new file mode 100644 index 0000000..abe8931 --- /dev/null +++ b/database/src/db.c @@ -0,0 +1,37 @@ +#include "db.h" + +#include "table.h" +#include "wal.h" + +int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { + uint32_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins); + wo_db *db = (wo_db *)vm->rt.db; + if (!db) { + *msg = "database engine not initialized"; + return WO_T_DB; + } + switch (C) { + case WO_B_DB_INSERT: { + uint32_t cid = (uint32_t)R[B]; + uint64_t id = wo_row_insert(db, cid, &R[B + 1], msg); + if (!id) return WO_T_DB; /* *msg already set (OOM / bad kind) */ + wo_wal *w = (wo_wal *)vm->rt.wal; + if (w) { + /* RAM applied, record staged, ONE commit before the ack (the + * builtin's return). A failed commit is a failed write: the + * row is removed again so RAM never claims what disk never + * acknowledged, and the statement traps. */ + if (wo_wal_append_insert(w, db, cid, id) != 0 || wo_wal_commit(w) != 0) { + wo_row_remove(db, cid, id); + *msg = "wal commit failed"; + return WO_T_IO; + } + } + R[A] = id; + return 0; + } + default: + *msg = "unknown db builtin"; + return WO_T_DB; + } +} diff --git a/database/src/db.h b/database/src/db.h new file mode 100644 index 0000000..95eb97d --- /dev/null +++ b/database/src/db.h @@ -0,0 +1,24 @@ +/* db.h — DB statement executors (iteration 9, Task 3+). + * + * The VM reaches the engine through one dispatcher with the same contract + * as every builtin family: 0 = ok, else a WO_T_* code with *msg set. The + * engine and WAL handles ride the runtime context as opaque pointers + * (obj.h's rt.db / rt.wal) — set by main.c at boot, NULL in test binaries + * that never touch DB statements (a DB builtin with rt.db == NULL traps + * WO_T_DB "engine not initialized"). + * + * Commit contract per statement (until iteration 8 brings ticks): the + * insert applies to RAM, stages its WAL record, and COMMITS before the + * builtin returns — the builtin returning IS the acknowledgment, so the + * ack-after-fsync doctrine holds at statement granularity. No WAL + * (rt.wal == NULL, no WO_DATA) means RAM-only: every test and every + * corpus fixture runs that way; durability is opt-in by pointing WO_DATA + * at a directory. */ +#ifndef WO_DB_H +#define WO_DB_H + +#include "vm.h" + +int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg); + +#endif /* WO_DB_H */ diff --git a/docs/plan/oop-vm/00-wob-format.md b/docs/plan/oop-vm/00-wob-format.md index 883ed83..c4e34be 100644 --- a/docs/plan/oop-vm/00-wob-format.md +++ b/docs/plan/oop-vm/00-wob-format.md @@ -61,6 +61,8 @@ The metadata exists for exactly one reason: `json.encode`/`json.decode` are runt - **text/containers** — len, byte_at, print_err, starts_with, ends_with, index_of, last_index_of, substr, trim, to_lower, char_of, parse_int, split, split_ws, join, slice, pop, shift, sort, reverse, remove, key_at, val_at, multi_set. Ids 16–39; `runtime/src/builtin.c`. - **the OS half** — fs.exists/list/stat/read_all/read_at/append, time.sleep/local/iso, env.get/stopping, net.listen/accept/read/write/close, proc.run. Ids 40–56; `runtime/src/sysio.c`. A member that returns a record takes that record's **class id as its last argument**, so the VM allocates what it fills without knowing any source type name. - **json** — encode (value + the value's static kind), decode (text + the class id to build). Ids 57–58; `runtime/src/json.c`. Decode yields the zero word on malformed input rather than trapping, which is what makes `json.decode(t) as T` a checked decode. +- **59 `map_get_opt`** (`m[k]`'s optional read), **60 `text_copy`** (Text's ownership-boundary copy — Task 1 of the executable plan). +- **database** — **61 `db_insert`** (iteration 9, Task 3): window is R[B] = class id, R[B+1..] = one slot per **declared** field in declaration order; result R[A] = the new row's id. The loader validates the class-id slot statically (variable window: the field slots are validated at runtime by the engine against the class table). Engine failure traps `WO_T_DB`; a failed WAL commit traps `WO_T_IO` after un-applying the row. `database/src/db.c`. **`?T` and nil.** A heap-shaped optional (`?Text`, `?Rec`, `?multi`, `?map`, `?@gc`) stores what `T` stores and spells nil as the **zero word** — every per-kind drop plan already ignores a zero slot, so `?T`'s field kind is `T`'s. A **nullable scalar** (`?Int`, `?Bool`, `?Timestamp`, `?Id`) cannot: `0` is a perfectly good `Int`, and real programs store it in a `?Int`. Its nil is therefore `WO_NIL_SCALAR` = −2^62 (not `INT64_MIN`: the compiler's own integers are 63-bit, so that value is not expressible on the emitting side). Such a field is marked `WOB_FIELD_NIL_SCALAR` in `field_class[i]`, which is how the runtime knows to write that word where it must produce absence itself — today only `json.decode` leaving a key absent, and `parse_int` on unparseable input. diff --git a/docs/plan/oop-vm/04-db-binding.md b/docs/plan/oop-vm/04-db-binding.md index 0dbcfbc..89dc330 100644 --- a/docs/plan/oop-vm/04-db-binding.md +++ b/docs/plan/oop-vm/04-db-binding.md @@ -107,9 +107,31 @@ intact record count and prefix end — the crash battery's verifier (`runtime/test/test_wal.c`: five rounds of insert/commit/ack-over-pipe with SIGKILL mid-stream; every acked row present and exact after replay). +## Insert (Task 3) — builtin 61, `database/src/db.c` + +`insert Class { field: expr, … }` is a typed expression (statement position +included): fields validate like a constructor literal (defaults and `?` +fields omittable — an omitted `?scalar` gets `WO_NIL_SCALAR`, other omitted +optionals the zero word, declared defaults their value), and the result is +the new row's id. Lowering emits builtin **61**: R[B] = class-id constant, +R[B+1..] = one slot per declared field in declaration order (the literal's +order is irrelevant — slots are the class table's). + +Execution: `wo_row_insert` (RAM, engine copies every value), then — when +durability is on — stage + **commit before the builtin returns**: the +builtin's return IS the acknowledgment, so ack-after-fsync holds at +statement granularity until iteration 8 brings tick-scoped group commit. A +failed commit un-applies the row and traps `WO_T_IO`; engine failures trap +`WO_T_DB`. Durability is opt-in: `WO_DATA=