From 048c242a010405b3270c5c3c03de73c46fbd0314 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 15 Aug 2026 22:19:13 +0200 Subject: [PATCH 1/6] feat(database): query-read engine builtins (iteration 9b foundation) - DB_SCAN(64): class -> multi of every row id, materialized up front (the 9b cursor-stability rule: the loop body point-reads, so a row updated mid-loop cannot disturb iteration) - DB_GET_FIELD(65): class,id,field -> the field decoded to a fresh VM value (the out-gate copy); a table-class value IS its row id at runtime, so this is how a compiled query reads a column, and a ref field decodes to the target id for navigation - DB_PROBE(66): class,index,key -> multi of ids whose first indexed column equals key (backlink + indexed where) - wo_val_decode_vm wrapper exposed; dispatch range 61..66, loader arities, runner mirror updated - 15 runtime suites green, oop-e2e 73/0 Co-Authored-By: Claude Opus 5 (1M context) --- compiler/test/runner.ml | 5 ++- database/src/db.c | 82 +++++++++++++++++++++++++++++++++++++++++ database/src/table.c | 6 +++ database/src/table.h | 5 +++ runtime/src/builtin.c | 2 +- runtime/src/loader.c | 3 ++ runtime/src/wob.h | 17 ++++++++- 7 files changed, 117 insertions(+), 3 deletions(-) diff --git a/compiler/test/runner.ml b/compiler/test/runner.ml index f120542..1d3846b 100644 --- a/compiler/test/runner.ml +++ b/compiler/test/runner.ml @@ -2604,7 +2604,7 @@ let validate_image (img : string) : string list = 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 || c > 63) then + if c > 12 && (c < 61 || c > 66) 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) @@ -2623,6 +2623,9 @@ let validate_image (img : string) : string list = | 61 -> 1 | 62 -> 4 | 63 -> 2 + | 64 -> 1 + | 65 -> 3 + | 66 -> 3 | _ -> 0 in if arity > 0 then begin diff --git a/database/src/db.c b/database/src/db.c index 790a21b..a370550 100644 --- a/database/src/db.c +++ b/database/src/db.c @@ -1,5 +1,8 @@ #include "db.h" +#include + +#include "cont.h" #include "table.h" #include "wal.h" @@ -68,6 +71,85 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { R[A] = 0; return 0; } + case WO_B_DB_SCAN: { + uint32_t cid = (uint32_t)R[B]; + if (cid >= db->class_cnt) { + *msg = "no such class"; + return WO_T_DB; + } + wo_multi *ids = wo_multi_new(&vm->rt, WO_K_SCALAR); + if (!ids) return WO_T_OOM; + /* materialize the id list up front — the 9b cursor-stability rule: + * the loop body then point-reads each id, so a row updated mid-loop + * (even an indexed column) cannot disturb the iteration */ + db_table *t = &db->tables[cid]; + if (t->row_size) { + uint32_t total = t->slab_cnt * DB_SLAB_ROWS; + for (uint32_t g = 0; g < total; g++) { + if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue; + db_row *row = + (db_row *)(t->slabs[g / DB_SLAB_ROWS] + (size_t)(g % DB_SLAB_ROWS) * t->row_size); + if (wo_multi_push(ids, row->id) != 0) return WO_T_OOM; + } + } + R[A] = (uint64_t)(uintptr_t)ids; + return 0; + } + case WO_B_DB_GET_FIELD: { + uint32_t cid = (uint32_t)R[B]; + uint64_t id = R[B + 1]; + uint32_t field = (uint32_t)R[B + 2]; + if (cid >= db->class_cnt || field >= db->classes[cid].field_cnt) { + *msg = "no such field"; + return WO_T_DB; + } + db_row *row = wo_row_ptr(db, cid, id); + if (!row) { + *msg = "no such row"; + return WO_T_DB; + } + int ok = 1; + uint64_t v = wo_val_decode_vm(db, &vm->rt, db->classes[cid].kinds[field], + row->slots[field], &ok, msg); + if (!ok) return WO_T_OOM; + R[A] = v; + return 0; + } + case WO_B_DB_PROBE: { + uint32_t cid = (uint32_t)R[B]; + uint32_t index = (uint32_t)R[B + 1]; + if (cid >= db->class_cnt) { + *msg = "no such class"; + return WO_T_DB; + } + wo_multi *ids = wo_multi_new(&vm->rt, WO_K_SCALAR); + if (!ids) return WO_T_OOM; + db_table *t = &db->tables[cid]; + if (t->row_size && index < t->index_cnt) { + db_index *ix = &t->indexes[index]; + uint32_t col = ix->cols[0]; + uint8_t kind = db->classes[cid].kinds[col]; + uint64_t key = R[B + 2]; + uint32_t total = t->slab_cnt * DB_SLAB_ROWS; + for (uint32_t g = 0; g < total; g++) { + if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue; + db_row *row = + (db_row *)(t->slabs[g / DB_SLAB_ROWS] + (size_t)(g % DB_SLAB_ROWS) * t->row_size); + int eq; + if (kind == WO_K_TEXT) { + const wo_str *want = (const wo_str *)(uintptr_t)key; + const db_text *have = (const db_text *)(uintptr_t)row->slots[col]; + eq = (!want && !have) || + (want && have && want->len == have->len && + memcmp(want->data, have->bytes, have->len) == 0); + } else + eq = row->slots[col] == key; + if (eq && wo_multi_push(ids, row->id) != 0) return WO_T_OOM; + } + } + R[A] = (uint64_t)(uintptr_t)ids; + return 0; + } default: *msg = "unknown db builtin"; return WO_T_DB; diff --git a/database/src/table.c b/database/src/table.c index feaf104..cf2b348 100644 --- a/database/src/table.c +++ b/database/src/table.c @@ -610,6 +610,12 @@ void wo_db_val_free(wo_db *db, uint8_t kind, uint64_t v) { db_val_free(kind, v); } +uint64_t wo_val_decode_vm(wo_db *db, wo_rt *rt, uint8_t kind, uint64_t engine_val, + int *ok, const char **msg) { + (void)db; + return db_val_decode(rt, kind, engine_val, ok, msg); +} + int wo_row_update_field(wo_db *db, uint32_t class_id, uint64_t id, uint32_t field, uint64_t vm_val, const char **msg, int *err_kind) { if (err_kind) *err_kind = DB_ERR_MISC; diff --git a/database/src/table.h b/database/src/table.h index c99e0d0..80805b8 100644 --- a/database/src/table.h +++ b/database/src/table.h @@ -174,6 +174,11 @@ db_row *wo_row_create_raw(wo_db *db, uint32_t class_id, uint64_t id); * decode error paths). */ void wo_db_val_free(wo_db *db, uint8_t kind, uint64_t v); +/* Decode one engine slot value to a FRESH VM value in [rt] (the out-gate: + * always a copy). The query builtins' field reads go through this. */ +uint64_t wo_val_decode_vm(wo_db *db, wo_rt *rt, uint8_t kind, uint64_t engine_val, + int *ok, const char **msg); + /* Engine-internal, replay only: after wal.c fills a raw row's slots, this * runs the index maintenance the normal insert runs inline — including the * unique check, whose violation during replay is corruption, not data diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index 019a99c..659ef56 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -70,7 +70,7 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { if (C == WO_B_JSON_ENCODE || C == WO_B_JSON_DECODE) return wo_builtin_json(vm, R, ins, msg); if (C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) return wo_builtin_sys(vm, R, ins, msg); - if (C >= WO_B_DB_INSERT && C <= WO_B_DB_DELETE) return wo_builtin_db(vm, R, ins, msg); + if (C >= WO_B_DB_INSERT && C <= WO_B_DB_PROBE) return wo_builtin_db(vm, R, ins, msg); switch (C) { case WO_B_NOW: { /* wall-clock milliseconds */ struct timespec ts; diff --git a/runtime/src/loader.c b/runtime/src/loader.c index 81da8d5..33ed884 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -44,6 +44,9 @@ static const uint8_t b_arity[WO_B_MAX + 1] = { [WO_B_DB_INSERT] = 1, [WO_B_DB_UPDATE_FIELD] = 4, [WO_B_DB_DELETE] = 2, + [WO_B_DB_SCAN] = 1, + [WO_B_DB_GET_FIELD] = 3, + [WO_B_DB_PROBE] = 3, [WO_B_NOW] = 0, [WO_B_PRINT] = 1, [WO_B_PRINT_INT] = 1, [WO_B_WORDS] = 1, [WO_B_MULTI_NEW] = 0, [WO_B_MULTI_PUSH] = 2, [WO_B_MULTI_GET] = 2, [WO_B_COUNT] = 1, [WO_B_LATEST] = 1, diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 4146cbd..8b0d427 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -302,8 +302,23 @@ enum { /* DB_DELETE: R[B] = class id, R[B+1] = row id. R[A] = 0. A missing row * traps WO_T_DB (deleting what is not there is a fault, not a no-op). */ WO_B_DB_DELETE = 63, + /* the query surface's reads (iteration 9b). A table-class value IS its + * row id at runtime (the "objects are rows" model), so these are how the + * compiled query loop touches storage: + * DB_SCAN (64): R[B] = class -> R[A] = multi of every id + * DB_GET_FIELD(65): R[B]=class, R[B+1]=id, R[B+2]=field + * -> R[A] = that field, decoded to a VM value (a + * Text field decodes to a fresh Text; a ref field + * decodes to the target id). Missing row traps + * WO_T_DB. + * DB_PROBE (66): R[B]=class, R[B+1]=index, R[B+2]=key + * -> R[A] = multi of ids whose first indexed + * column equals key (backlink + indexed where). */ + WO_B_DB_SCAN = 64, + WO_B_DB_GET_FIELD = 65, + WO_B_DB_PROBE = 66, }; -#define WO_B_MAX 63u +#define WO_B_MAX 66u /* ids at or above this one live in sysio.c, not builtin.c */ #define WO_B_SYS_FIRST WO_B_FS_EXISTS From d4bfee974569afdd99db80043a8b66c26a7d6077 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 15 Aug 2026 22:28:09 +0200 Subject: [PATCH 2/6] =?UTF-8?q?feat(compiler):=20language-integrated=20que?= =?UTF-8?q?ry=20=E2=80=94=20scan/where/select=20(9b=20slice)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ast.Query node + parser: `from in where* [group..into] [order by] [take] select `, positional `from` trigger so it stays a usable identifier; select stays grammar-owned (no DbStub conflict) - typecheck: range var bound to the source table class; a table-class value is its row id at runtime but TYPES as the class, so `e.field` checks against the class fields; result is `multi `; group/order/take/navigation diagnosed WO-E250 not-yet (honest edge) - emit: `from/where/select` lowers to a bytecode LOOP over DB_SCAN's materialized id list — DB_GET_FIELD per column read, where-guards skip the push, select projects, result is a fresh multi; no plan tree, no SQL text (disassembly-provable) - field access on a @table-class value routes to DB_GET_FIELD instead of GETF (clsrec.cr_is_table + is_table_class); non-table classes unchanged so log-watcher is unaffected - the ASan-caught bug kept in a comment: a table-class query element is a SCALAR id, not an OWNED pointer — tagging the result multi OWNED dropped an id as a pointer (SEGV in wo_drop_obj) - fixture run/db-query-scan (where-filter + select-whole + select-field); oop-e2e 74/0, woc-test 566/0, log-watcher 7/0 - SLICE scope: group-by aggregation, order/take, and ref/backlink navigation are the next chunk (employee report/staff/raise need them) Co-Authored-By: Claude Opus 5 (1M context) --- compiler/src/ast.ml | 25 ++++ compiler/src/dump.ml | 8 ++ compiler/src/emit.ml | 142 ++++++++++++++++++++- compiler/src/owner.ml | 12 ++ compiler/src/parser.ml | 95 ++++++++++++++ compiler/src/types.ml | 54 ++++++++ tests/corpus/run/db-query-scan/fixture.out | 6 + tests/corpus/run/db-query-scan/fixture.wo | 22 ++++ 8 files changed, 360 insertions(+), 4 deletions(-) create mode 100644 tests/corpus/run/db-query-scan/fixture.out create mode 100644 tests/corpus/run/db-query-scan/fixture.wo diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index 91ffca9..3f53b99 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -279,6 +279,31 @@ and expr_kind = ename : string; handler : stmt list; } + (* iteration 9b: a language-integrated query. `from in + where * [group by into ] [order by [desc]] [take ] + select ` — lowered to a bytecode loop over engine cursor builtins, + never SQL text. A table-class value is its row id at runtime, so field + access on a range variable reads through the engine. Slice scope today: + from/where/order/take/select and group-by aggregation; join is later. *) + | Query of query + +and query_source = + | QTable of string (* a table class by name: `from e in Employee` *) + | QNav of expr (* a backlink/multi navigation: `from s in d.staff` *) + +and query = { + q_var : string; + q_src : query_source; + q_wheres : expr list; + (* group by ... into : present iff this is an aggregating + query. q_group_key is the whole grouped element (`e`), q_group_by the + key, q_gvar the group binding whose `.f` columns feed aggregates. *) + q_group : (string * expr) option; (* (gvar, key_expr) *) + q_order : (expr * bool) option; (* (key, desc?) *) + q_take : expr option; + q_select : expr; + q_pos : pos; +} (* ---- statements (Task 5) --------------------------------------------- diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index 9914646..6bcd65c 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -226,6 +226,14 @@ let rec expr_str (e : Ast.expr) : string = Printf.sprintf "INSERT %s { %s }" name (String.concat ", " (List.map (fun (fname, fval) -> Printf.sprintf "%s: %s" fname (expr_str fval)) fields)) + | Ast.Query q -> + let src = match q.Ast.q_src with Ast.QTable cn -> cn | Ast.QNav e -> expr_str e in + Printf.sprintf "QUERY from %s in %s%s%s select %s" q.Ast.q_var src + (String.concat "" (List.map (fun w -> " where " ^ expr_str w) q.Ast.q_wheres)) + (match q.Ast.q_group with + | Some (g, k) -> Printf.sprintf " group by %s into %s" (expr_str k) g + | None -> "") + (expr_str q.Ast.q_select) | 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 136b40b..925528c 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -354,6 +354,7 @@ type clsrec = { unique single-column entry per `@unique` field. Serialized as the v3 class-record tail; the engine builds its runtime indexes from this. *) cr_indexes : (bool * int array) list; + cr_is_table : bool; (* has @table — its instances are row ids (iteration 9b) *) } type ifacerec = { @@ -778,6 +779,15 @@ let field_kind (p : pctx) (ft : Ast.field_ty) : int = let class_of_name (p : pctx) (n : string) : int option = SM.find_opt n p.p_class_id +(* iteration 9b: a @table class's instances are row ids, so field access on + one reads through the engine (DB_GET_FIELD) rather than GETF. *) +let is_table_class (p : pctx) (cid : int) : bool = + cid >= 0 && cid < Array.length p.p_classes && p.p_classes.(cid).cr_is_table + +let b_db_scan = 64 +let b_db_get_field = 65 +let b_db_probe = 66 + let field_of (p : pctx) (cid : int) (fname : string) : (int * Ast.field_ty) option = let fs = p.p_classes.(cid).cr_fields in let rec go i = if i >= Array.length fs then None else @@ -947,6 +957,23 @@ let variant_tag_value (p : pctx) (u : Types.union_info) (vi : Types.variant_info | None -> 0 (* unreachable: pass 1 registers every payload-union variant *) else vi.Types.vi_tag +(* iteration 9b: a query's element type, as the name a `Multi` carries. + `select x` yields the source class (a row id typed as the class); + `select x.field` yields that field's type; anything else falls back to + Int (the slice's shapes are these two). *) +let query_elem_scalar (p : pctx) (_f : fstate) (q : Ast.query) : string = + let src_class = match q.Ast.q_src with Ast.QTable cn -> Some cn | Ast.QNav _ -> None in + match (q.Ast.q_select.Ast.kind, src_class) with + | Ast.Ident v, Some cn when v = q.Ast.q_var -> cn + | Ast.Field ({ Ast.kind = Ast.Ident v; _ }, fname), Some cn when v = q.Ast.q_var -> ( + match class_of_name p cn with + | Some cid -> ( + match field_of p cid fname with + | Some (_, ty) -> ( match unwrap ty with Scalar n -> n | _ -> "Int") + | None -> "Int") + | None -> "Int") + | _ -> "Int" + let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option = match e.kind with | IntLit _ -> Some (Scalar "Int") @@ -1061,6 +1088,7 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | 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") + | Query q -> Some (Multi (query_elem_scalar p f q)) | Interp _ -> Some (Scalar "Text") | DbStub _ -> None | Switch (subject, arms) -> ( @@ -1610,7 +1638,17 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e f.f_stmt_drops <- g :: f.f_stmt_drops; f.f_esc_drops <- g :: f.f_esc_drops end; - put f (ins_abc op_getf dst b (check_field_idx p f e.pos idx)) + if is_table_class p cid then begin + (* a table-class value is its row id; read the column from the + engine. Window: [class-id, id, field-idx]. *) + let w = alloc_temps p f e.pos 3 in + put f (ins_abx op_loadk w (check_bx p f e.pos "constant" (const_int p cid))); + put f (ins_abc op_move (w + 1) b 0); + put f (ins_abx op_loadk (w + 2) (check_bx p f e.pos "constant" (const_int p idx))); + sync_mask p f v e.id; + put f (ins_abc op_builtin dst w b_db_get_field) + end + else put f (ins_abc op_getf dst b (check_field_idx p f e.pos idx)) | None -> err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos ~message:(Printf.sprintf "`%s` has no field `%s`" cn fname); @@ -1662,6 +1700,7 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e | 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 + | Query q -> emit_query p f v ~dst e q | 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 @@ -2364,6 +2403,100 @@ and emit_ctor (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) (c 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_query (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) + (q : Ast.query) : unit = + (* iteration 9b slice: from/where/select over a table scan. group/order/ + take/navigation are diagnosed in types.ml, so a written image never + reaches this with them set. Lowered to an ordinary bytecode loop over + DB_SCAN's materialized id list — no plan tree, no text. *) + let cn = match q.Ast.q_src with Ast.QTable cn -> cn | Ast.QNav _ -> "" in + match class_of_name p cn with + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message:(Printf.sprintf "query over `%s`, which is not a declared table class" cn); + put f (ins_abx op_loadk dst (const_int p 0)) + | Some cid -> + let elem_name = query_elem_scalar p f q in + let elem = Scalar elem_name in + (* a table-class element is a row ID (a scalar), not a heap pointer — so + the result container is SCALAR-kinded even though the element TYPES as + the class; getting this wrong drops an id as a pointer (ASan SEGV) *) + let elem_kind = + match class_of_name p elem_name with + | Some ecid when is_table_class p ecid -> 0 (* WO_K_SCALAR *) + | _ -> field_kind p elem + in + (* reserve dst past the loop's working registers (same guard emit_ctor + uses): dst holds the result multi every push writes into *) + let outer = f.f_temp in + if f.f_temp <= dst then f.f_temp <- dst + 1; + (* loop-carried registers, allocated once above dst, never reset *) + let scan = alloc_temp p f e.pos in + let idx = alloc_temp p f e.pos in + let len = alloc_temp p f e.pos in + let idreg = alloc_temp p f e.pos in + let body_base = f.f_temp in + (* scan -> multi of ids; result multi -> dst *) + sync_mask p f v e.id; + f.f_cur_line <- e.pos.line; + put f (ins_abx op_loadk scan (check_bx p f e.pos "constant" (const_int p cid))); + put f (ins_abc op_builtin scan scan b_db_scan); + put f (ins_abc op_builtin dst elem_kind b_multi_new); + put f (ins_abc op_builtin len scan b_len); + put f (ins_abx op_loadk idx (check_bx p f e.pos "constant" (const_int p 0))); + (* bind the range var to the current id (typed as the class), so field + access inside where/select routes through DB_GET_FIELD *) + let saved_env = f.f_env in + f.f_env <- (q.Ast.q_var, (idreg, Scalar cn)) :: f.f_env; + ignore body_base; + let top = here f in + f.f_temp <- body_base; + let tc = alloc_temp p f e.pos in + put f (ins_abc op_lt tc idx len); + let jz_exit = here f in + put f (ins_asbx op_jz tc 0); + (* id = multi_get(scan, idx) *) + let w = alloc_temps p f e.pos 2 in + put f (ins_abc op_move w scan 0); + put f (ins_abc op_move (w + 1) idx 0); + put f (ins_abc op_builtin idreg w b_multi_get); + (* where guards: any false skips the push *) + let skips = ref [] in + List.iter + (fun w_expr -> + let save = f.f_temp in + let wr = emit_operand p f v w_expr in + skips := here f :: !skips; + put f (ins_asbx op_jz wr 0); + f.f_temp <- save) + q.Ast.q_wheres; + (* select -> push into dst (copying a Text element the container owns) *) + let save = f.f_temp in + let sel = alloc_temp p f e.pos in + emit_expr p f v ~dst:sel q.Ast.q_select; + if elem_kind = 3 then put f (ins_abc op_builtin sel sel b_text_copy); + let pw = alloc_temps p f e.pos 2 in + put f (ins_abc op_move pw dst 0); + put f (ins_abc op_move (pw + 1) sel 0); + put f (ins_abc op_builtin pw pw b_multi_push); + f.f_temp <- save; + (* skip target: increment and loop *) + let cont = here f in + List.iter (fun pc -> patch_jump p f ~file:f.f_file ~pos:e.pos pc cont) !skips; + f.f_temp <- body_base; + let one = alloc_temp p f e.pos in + put f (ins_abx op_loadk one (check_bx p f e.pos "constant" (const_int p 1))); + put f (ins_abc op_add idx idx one); + let back = here f in + put f (ins_asbx op_jmp 0 0); + patch_jump p f ~file:f.f_file ~pos:e.pos back top; + let exit_pc = here f in + patch_jump p f ~file:f.f_file ~pos:e.pos jz_exit exit_pc; + f.f_env <- saved_env; + (* the scan's id list was this query's own, dropped now *) + put f (ins_abc op_drop scan 0 0); + f.f_temp <- outer + 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 @@ -4047,7 +4180,8 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) cr_fields = Array.of_list (List.map (fun (fl : Ast.field) -> (fl.name, fl.ty)) c.fields); cr_methods = List.map (fun (m : Ast.method_decl) -> m.name) c.methods; - cr_indexes = table_indexes @ unique_indexes }) + cr_indexes = table_indexes @ unique_indexes; + cr_is_table = (c.Ast.table <> None) }) :: !classes end | Ast.Union (ud : Ast.union_decl) -> @@ -4067,7 +4201,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) class_id := SM.add key cid !class_id; incr nclasses; classes := - { cr_name = key; cr_gc = false; cr_indexes = []; + { cr_name = key; cr_gc = false; cr_indexes = []; cr_is_table = false; cr_fields = Array.of_list vd.Ast.v_fields; cr_methods = [] } :: !classes @@ -4111,7 +4245,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) incr nclasses; classes := { cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = []; - cr_indexes = [] } + cr_indexes = []; cr_is_table = false } :: !classes end) Types.predeclared_records; diff --git a/compiler/src/owner.ml b/compiler/src/owner.ml index fb6ef6b..a773a50 100644 --- a/compiler/src/owner.ml +++ b/compiler/src/owner.ml @@ -561,6 +561,7 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option = | 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 *) + | Query _ -> Some (Multi "Int") (* a query yields a fresh multi of ids — owned *) | Interp _ -> Some (Scalar "Text") (* an interpolation always produces Text *) | DbStub _ -> None | Switch (subject, arms) -> @@ -1179,6 +1180,17 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit = | DbStub _ -> (* trap-capable: the frame needs its drop map here *) record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx)) + | Query q -> + (* iteration 9b: the sub-expressions only READ (engine field-reads copy + out at the boundary); the query is trap-capable (engine faults), so + the frame needs its drop map here, exactly like DbStub. *) + (match q.q_src with QNav e2 -> read_expr ctx e2 | QTable _ -> ()); + List.iter (read_expr ctx) q.q_wheres; + (match q.q_group with Some (_, k) -> read_expr ctx k | None -> ()); + (match q.q_order with Some (k, _) -> read_expr ctx k | None -> ()); + (match q.q_take with Some t -> read_expr ctx t | None -> ()); + read_expr ctx q.q_select; + record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx)) | Switch (subject, arms) -> analyze_switch ctx e.id subject arms (* The root of a place expression is already accounted for by use_place; diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index b0376f7..81f5546 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -1022,8 +1022,89 @@ and parse_insert_expr (st : state) : Ast.expr = | Ast.Ctor (cn, fields) -> { lit with Ast.pos; kind = Ast.Insert (cn, fields) } | _ -> lit (* unreachable: parse_ctor_literal only builds Ctor *)) +and is_query_trigger (st : state) : bool = + (* `from in` — positional, so `from` stays a usable identifier + everywhere else (same discipline as insert/select) *) + (match peek st with Token.Ident "from" -> true | _ -> false) + && (match (tok_at st (st.pos + 1)).kind with Token.Ident _ -> true | _ -> false) + && (tok_at st (st.pos + 2)).kind = Token.KwIn + +and parse_query_expr (st : state) : Ast.expr = + let pos = peek_pos st in + let id = fresh_id st in + ignore (advance st) (* from *); + let var = expect_ident st "query range variable" in + expect st Token.KwIn "`in`"; + (* source: a bare class name is a table scan; any other expression is a + navigation (`d.staff`). One token of lookahead: Ident not followed by a + `.`/`(`/`[` and sitting where a clause keyword follows is a table name. *) + let src = + match peek st with + | Token.Ident cn + when (match (tok_at st (st.pos + 1)).kind with + | Token.Dot | Token.LParen | Token.LBracket -> false + | _ -> true) -> + ignore (advance st); + Ast.QTable cn + | _ -> Ast.QNav (parse_expr_no_brace st) + in + let clause name = match peek st with Token.Ident n when n = name -> true | _ -> false in + let wheres = ref [] in + while clause "where" do + ignore (advance st); + wheres := parse_expr_no_brace st :: !wheres + done; + let group = + if clause "group" then begin + ignore (advance st); + let key_elem = parse_expr_no_brace st in + ignore key_elem (* the grouped element is the range var; `group e by k` *); + if not (clause "by") then fail st (peek_pos st) syntax_code "expected `by` in a group clause"; + ignore (advance st); + let key = parse_expr_no_brace st in + if not (clause "into") then fail st (peek_pos st) syntax_code "expected `into` in a group clause"; + ignore (advance st); + let gvar = expect_ident st "group variable" in + Some (gvar, key) + end + else None + in + let order = + if clause "order" then begin + ignore (advance st); + if not (clause "by") then fail st (peek_pos st) syntax_code "expected `by` after `order`"; + ignore (advance st); + let key = parse_expr_no_brace st in + let desc = clause "desc" in + if desc then ignore (advance st); + Some (key, desc) + end + else None + in + let take = if clause "take" then (ignore (advance st); Some (parse_expr_no_brace st)) else None in + if not (clause "select") then fail st (peek_pos st) syntax_code "a query must end in `select`"; + ignore (advance st); + let sel = parse_expr st in + { + Ast.id; + pos; + kind = + Ast.Query + { + Ast.q_var = var; + q_src = src; + q_wheres = List.rev !wheres; + q_group = group; + q_order = order; + q_take = take; + q_select = sel; + q_pos = pos; + }; + } + and parse_primary (st : state) : Ast.expr = match peek st with + | _ when is_query_trigger st -> parse_query_expr st | 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 @@ -1801,6 +1882,20 @@ let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : As { 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.Query q -> + (* the range/group vars shadow consts inside the query body *) + let bound' = StringSet.add q.Ast.q_var bound in + let bound' = match q.Ast.q_group with Some (g, _) -> StringSet.add g bound' | None -> bound' in + let sub = subst_expr consts bound' in + { e with Ast.kind = Ast.Query { + q with Ast.q_src = (match q.Ast.q_src with + | Ast.QTable cn -> Ast.QTable cn + | Ast.QNav e2 -> Ast.QNav (subst_expr consts bound e2)); + q_wheres = List.map sub q.Ast.q_wheres; + q_group = (match q.Ast.q_group with Some (g, k) -> Some (g, sub k) | None -> None); + q_order = (match q.Ast.q_order with Some (k, d) -> Some (sub k, d) | None -> None); + q_take = (match q.Ast.q_take with Some t -> Some (sub t) | None -> None); + q_select = sub q.Ast.q_select } } | 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 f1f492b..b9cf386 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -412,6 +412,7 @@ let unknown_fn_code = Diag.types_prefix ^ "04" let unsatisfied_interface_code = Diag.types_prefix ^ "05" let incomplete_ctor_code = Diag.types_prefix ^ "06" let unknown_type_code = Diag.types_prefix ^ "07" +let query_code = Diag.types_prefix ^ "50" (* WO-E250: query surface (iteration 9b) *) let non_exhaustive_switch_code = Diag.types_prefix ^ "08" let invalid_builtin_code = Diag.types_prefix ^ "09" let module_not_imported_code = Diag.types_prefix ^ "10" @@ -1168,6 +1169,7 @@ let typecheck_program ~file ~(module_of : string -> string) | Insert _ -> (* the new row's id — the one thing an insert produces *) Some (TScalar "Int") + | Query _ -> None (* a query's type is chased only by typecheck_expr *) | 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` @@ -1432,6 +1434,49 @@ 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 insert" class_name) ()); { typ = TScalar "Int"; is_nil = false }) + | Query q -> + (* iteration 9b slice: from/where/select over a table class. The + range variable is bound to the class type; a table-class value is + its row id at runtime but types AS the class, so `e.field` checks + against the class's fields exactly like a heap instance. group / + order / take / navigation sources are diagnosed as not-yet so the + surface is honest about its edge. *) + let elem_err () = + { typ = TMulti (TScalar "Int"); is_nil = false } + in + (match q.q_src with + | Ast.QNav _ -> + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:"query over a navigation source is not supported yet (table scans only)" ()); + elem_err () + | Ast.QTable cn -> + if not (StringMap.mem cn syms.classes) then begin + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:(Printf.sprintf "`from %s in %s`: `%s` is not a declared table class" + q.q_var cn cn) ()); + elem_err () + end + else begin + (if q.q_group <> None then + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:"group-by aggregation is not supported yet" ())); + (if q.q_order <> None then + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:"`order by` is not supported yet" ())); + (if q.q_take <> None then + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:"`take` is not supported yet" ())); + let env' = StringMap.add q.q_var (TScalar cn) env in + let cenv' = StringMap.add q.q_var (TScalar cn) cenv in + List.iter (fun w -> ignore (typecheck_expr env' cenv' w)) q.q_wheres; + let sel = typecheck_expr env' cenv' q.q_select in + { typ = TMulti sel.typ; is_nil = false } + end) | DbStub _ -> { typ = TVoid; is_nil = false } | Switch (subject, arms) -> typecheck_switch ~want_value:true env cenv subject arms | ListLit items -> @@ -2139,6 +2184,15 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e walk_expr bound visit body; walk_block (StringSet.add ename bound) visit handler | DbStub _ -> () + | Query q -> + (match q.q_src with QNav e -> walk_expr bound visit e | QTable _ -> ()); + let b = StringSet.add q.q_var bound in + let b = match q.q_group with Some (g, _) -> StringSet.add g b | None -> b in + List.iter (walk_expr b visit) q.q_wheres; + (match q.q_group with Some (_, k) -> walk_expr b visit k | None -> ()); + (match q.q_order with Some (k, _) -> walk_expr b visit k | None -> ()); + (match q.q_take with Some t -> walk_expr b visit t | None -> ()); + walk_expr b visit q.q_select | Switch (subject, arms) -> walk_expr bound visit subject; List.iter diff --git a/tests/corpus/run/db-query-scan/fixture.out b/tests/corpus/run/db-query-scan/fixture.out new file mode 100644 index 0000000..33ec010 --- /dev/null +++ b/tests/corpus/run/db-query-scan/fixture.out @@ -0,0 +1,6 @@ +asha +bram +-- +asha +bram +chidi diff --git a/tests/corpus/run/db-query-scan/fixture.wo b/tests/corpus/run/db-query-scan/fixture.wo new file mode 100644 index 0000000..47a8e32 --- /dev/null +++ b/tests/corpus/run/db-query-scan/fixture.wo @@ -0,0 +1,22 @@ +-- iteration 9b: language-integrated query, from/where/select over a table +-- scan, lowered to a bytecode loop over engine cursors (no SQL text). A +-- table-class value is its row id; `e.name` reads the column via the engine. +@table(name: "emp", index: [dept]) +class Emp { + name: Text + salary: Int + dept: Int +} + +fn main() { + insert Emp { name: "asha", salary: 100, dept: 1 } + insert Emp { name: "bram", salary: 200, dept: 1 } + insert Emp { name: "chidi", salary: 50, dept: 2 } + for e in from x in Emp where x.salary >= 100 select x { + print(e.name) + } + print("--") + for n in from x in Emp select x.name { + print(n) + } +} From 8817fdec2a329919b01f474b1829c26cf4f8a2ca Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 16 Aug 2026 05:10:13 +0200 Subject: [PATCH 3/6] feat(compiler): ref + backlink navigation in queries (9b cont.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `backlink C.f` field type: parsed, typed as `multi C`, and VIRTUAL — filtered out of the stored row layout (no column, omittable in ctor/insert), collected in clsrec.cr_backlinks - reading a backlink (`d.staff`) lowers to DB_PROBE on the source class's index for the backing column (backlink_target resolves the (source class, index number); a backlink with no backing index has no efficient read) - `ref C` navigation (`e.dept.name`) chains: a ref value is the target row's id, so a `Ref C` base navigates into C's fields exactly like a table-class value, routing to DB_GET_FIELD both in typecheck and emit - query navigation source `from s in d.staff`: emit_query evaluates the nav expr to get its id-list instead of DB_SCAN; QNav typechecks with the range var bound to the navigation's element class - fixture run/db-query-relations proves both directions; oop-e2e 75/0, woc-test 566/0, log-watcher 7/0 - still ahead for employee: order/take, group-by aggregates, projection records, delete + update-through-row Co-Authored-By: Claude Opus 5 (1M context) --- compiler/src/ast.ml | 3 + compiler/src/dump.ml | 1 + compiler/src/emit.ml | 129 +++++++++++++++--- compiler/src/owner.ml | 1 + compiler/src/parser.ml | 6 + compiler/src/types.ml | 45 ++++-- .../corpus/run/db-query-relations/fixture.out | 7 + .../corpus/run/db-query-relations/fixture.wo | 30 ++++ 8 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 tests/corpus/run/db-query-relations/fixture.out create mode 100644 tests/corpus/run/db-query-relations/fixture.wo diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index 3f53b99..92113e3 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -69,6 +69,9 @@ type field_ty = | Ref of string | Multi of string | Map of string * string (* key type, value type: map *) + | Backlink of string * string (* backlink C.f: the computed inverse of a + `ref` — NOT a stored column; reading it + scans C's index on f. Types as multi C. *) | Nullable of field_ty (* ?T wrapper *) (* Parameter passing convention (spec section 3, rule 2): default is an diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index 6bcd65c..af5dc5d 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -150,6 +150,7 @@ let rec field_ty_str : Ast.field_ty -> string = function | Ast.Ref s -> Printf.sprintf "ref %s" s | Ast.Multi s -> Printf.sprintf "multi %s" s | Ast.Map (k, v) -> Printf.sprintf "map<%s, %s>" k v + | Ast.Backlink (c, f) -> Printf.sprintf "backlink %s.%s" c f | Ast.Nullable t -> "?" ^ field_ty_str t let param_str (p : Ast.param) : string = Printf.sprintf "%s%s: %s" (conv_str p.conv) p.name (field_ty_str p.ty) diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 925528c..a79a9ed 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -355,6 +355,10 @@ type clsrec = { class-record tail; the engine builds its runtime indexes from this. *) cr_indexes : (bool * int array) list; cr_is_table : bool; (* has @table — its instances are row ids (iteration 9b) *) + (* backlink fields (iteration 9b): name -> (source class, source field). + Virtual — not in cr_fields, no stored column; `d.staff` reads them by + probing the source class's index on the source field. *) + cr_backlinks : (string * (string * string)) list; } type ifacerec = { @@ -788,6 +792,32 @@ let b_db_scan = 64 let b_db_get_field = 65 let b_db_probe = 66 +(* iteration 9b: `d.staff` where staff is `backlink Employee.dept` reads by + probing Employee's index on its `dept` column. Resolve to (source cid, + index number) — None if the source field is not a declared index (a + backlink without a backing index has no efficient read and is rejected). *) +let backlink_target (p : pctx) (base_cid : int) (fname : string) : (int * int) option = + match List.assoc_opt fname p.p_classes.(base_cid).cr_backlinks with + | None -> None + | Some (src_class, src_field) -> ( + match class_of_name p src_class with + | None -> None + | Some scid -> + let sc = p.p_classes.(scid) in + (* stored column index of the source field *) + let col = ref (-1) in + Array.iteri (fun i (n, _) -> if n = src_field then col := i) sc.cr_fields; + if !col < 0 then None + else + (* the index whose single column is that field *) + let rec find n = function + | [] -> None + | (_, cols) :: tl -> + if Array.length cols = 1 && cols.(0) = !col then Some (scid, n) + else find (n + 1) tl + in + find 0 sc.cr_indexes) + let field_of (p : pctx) (cid : int) (fname : string) : (int * Ast.field_ty) option = let fs = p.p_classes.(cid).cr_fields in let rec go i = if i >= Array.length fs then None else @@ -961,12 +991,11 @@ let variant_tag_value (p : pctx) (u : Types.union_info) (vi : Types.variant_info `select x` yields the source class (a row id typed as the class); `select x.field` yields that field's type; anything else falls back to Int (the slice's shapes are these two). *) -let query_elem_scalar (p : pctx) (_f : fstate) (q : Ast.query) : string = - let src_class = match q.Ast.q_src with Ast.QTable cn -> Some cn | Ast.QNav _ -> None in - match (q.Ast.q_select.Ast.kind, src_class) with - | Ast.Ident v, Some cn when v = q.Ast.q_var -> cn - | Ast.Field ({ Ast.kind = Ast.Ident v; _ }, fname), Some cn when v = q.Ast.q_var -> ( - match class_of_name p cn with +let query_elem_scalar (p : pctx) (q : Ast.query) ~(src : string) : string = + match q.Ast.q_select.Ast.kind with + | Ast.Ident v when v = q.Ast.q_var -> src (* select the whole row: element = source class *) + | Ast.Field ({ Ast.kind = Ast.Ident v; _ }, fname) when v = q.Ast.q_var -> ( + match class_of_name p src with | Some cid -> ( match field_of p cid fname with | Some (_, ty) -> ( match unwrap ty with Scalar n -> n | _ -> "Int") @@ -1003,10 +1032,17 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | Field (base, fname) -> ( match ty_of_expr p f base with | Some bt -> ( - match unwrap bt with + (* a `ref C` navigates into C: the target is a table row id *) + match (match unwrap bt with Ref c -> Scalar c | other -> other) with | Scalar cn -> ( match class_of_name p cn with - | Some cid -> ( match field_of p cid fname with Some (_, t) -> Some t | None -> None) + | Some cid -> ( + match field_of p cid fname with + | Some (_, t) -> Some t + | None -> ( + match List.assoc_opt fname p.p_classes.(cid).cr_backlinks with + | Some (sc, _) -> Some (Multi sc) + | None -> None)) | None -> None) | _ -> None) | None -> None) @@ -1088,7 +1124,14 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | 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") - | Query q -> Some (Multi (query_elem_scalar p f q)) + | Query q -> + let src = + match q.Ast.q_src with + | Ast.QTable cn -> cn + | Ast.QNav nav -> ( + match ty_of_expr p f nav with Some t -> (match unwrap t with Multi c -> c | Scalar c -> c | _ -> "") | None -> "") + in + Some (Multi (query_elem_scalar p q ~src)) | Interp _ -> Some (Scalar "Text") | DbStub _ -> None | Switch (subject, arms) -> ( @@ -1379,7 +1422,7 @@ let field_class_meta (p : pctx) (ty : Ast.field_ty) : int = match name_of (Ast.Scalar e) with | Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none) | None -> wob_none) - | Ast.Ref _ | Ast.Nullable _ -> wob_none + | Ast.Ref _ | Ast.Backlink _ | Ast.Nullable _ -> wob_none let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int = match unwrap ty with @@ -1618,9 +1661,22 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e | Field (base, fname) -> ( match ty_of_expr p f base with | Some bt -> ( - match unwrap bt with + match (match unwrap bt with Ref c -> Scalar c | other -> other) with | Scalar cn -> ( match class_of_name p cn with + | Some cid when is_table_class p cid && backlink_target p cid fname <> None -> ( + (* `d.staff`: probe the source class's index for rows referencing + this row's id. Window: [class, index, key(=base id)]. *) + match backlink_target p cid fname with + | Some (scid, ino) -> + let b = emit_operand p f v base in + let w = alloc_temps p f e.pos 3 in + put f (ins_abx op_loadk w (check_bx p f e.pos "constant" (const_int p scid))); + put f (ins_abx op_loadk (w + 1) (check_bx p f e.pos "constant" (const_int p ino))); + put f (ins_abc op_move (w + 2) b 0); + sync_mask p f v e.id; + put f (ins_abc op_builtin dst w b_db_probe) + | None -> ()) | Some cid -> ( match field_of p cid fname with | Some (idx, _) -> @@ -2409,14 +2465,22 @@ and emit_query (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) take/navigation are diagnosed in types.ml, so a written image never reaches this with them set. Lowered to an ordinary bytecode loop over DB_SCAN's materialized id list — no plan tree, no text. *) - let cn = match q.Ast.q_src with Ast.QTable cn -> cn | Ast.QNav _ -> "" in + let cn = + match q.Ast.q_src with + | Ast.QTable cn -> cn + | Ast.QNav nav -> ( + (* the source's element type is the range var's class *) + match ty_of_expr p f nav with + | Some t -> ( match unwrap t with Multi c -> c | Scalar c -> c | _ -> "") + | None -> "") + in match class_of_name p cn with | None -> err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos ~message:(Printf.sprintf "query over `%s`, which is not a declared table class" cn); put f (ins_abx op_loadk dst (const_int p 0)) | Some cid -> - let elem_name = query_elem_scalar p f q in + let elem_name = query_elem_scalar p q ~src:cn in let elem = Scalar elem_name in (* a table-class element is a row ID (a scalar), not a heap pointer — so the result container is SCALAR-kinded even though the element TYPES as @@ -2439,8 +2503,16 @@ and emit_query (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) (* scan -> multi of ids; result multi -> dst *) sync_mask p f v e.id; f.f_cur_line <- e.pos.line; - put f (ins_abx op_loadk scan (check_bx p f e.pos "constant" (const_int p cid))); - put f (ins_abc op_builtin scan scan b_db_scan); + (match q.Ast.q_src with + | Ast.QTable _ -> + put f (ins_abx op_loadk scan (check_bx p f e.pos "constant" (const_int p cid))); + put f (ins_abc op_builtin scan scan b_db_scan) + | Ast.QNav nav -> + (* the navigation (a backlink) already yields a multi of source ids *) + let save = f.f_temp in + f.f_temp <- scan + 1; + emit_expr p f v ~dst:scan nav; + f.f_temp <- save); put f (ins_abc op_builtin dst elem_kind b_multi_new); put f (ins_abc op_builtin len scan b_len); put f (ins_abx op_loadk idx (check_bx p f e.pos "constant" (const_int p 0))); @@ -4129,7 +4201,12 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) | Some key -> Hashtbl.replace record_shape key cid | None -> ()); classes := - (let fnames = List.map (fun (fl : Ast.field) -> fl.Ast.name) c.fields in + (let fnames = + List.filter_map + (fun (fl : Ast.field) -> + match fl.Ast.ty with Ast.Backlink _ -> None | _ -> Some fl.Ast.name) + c.fields + in let col_of n = ref_index_of_name fnames n in let is_indexable (fl : Ast.field) = match Types.wob_kind_of_typ p_syms_for_indexes (Types.typ_of_field_ty (unwrap fl.Ast.ty)) with @@ -4178,10 +4255,23 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) | None -> ()); { cr_name = c.name; cr_gc = c.is_gc; cr_fields = - Array.of_list (List.map (fun (fl : Ast.field) -> (fl.name, fl.ty)) c.fields); + Array.of_list + (List.filter_map + (fun (fl : Ast.field) -> + match fl.Ast.ty with + | Ast.Backlink _ -> None (* virtual: no stored column *) + | _ -> Some (fl.Ast.name, fl.Ast.ty)) + c.fields); cr_methods = List.map (fun (m : Ast.method_decl) -> m.name) c.methods; cr_indexes = table_indexes @ unique_indexes; - cr_is_table = (c.Ast.table <> None) }) + cr_is_table = (c.Ast.table <> None); + cr_backlinks = + List.filter_map + (fun (fl : Ast.field) -> + match fl.Ast.ty with + | Ast.Backlink (sc, sf) -> Some (fl.Ast.name, (sc, sf)) + | _ -> None) + c.fields }) :: !classes end | Ast.Union (ud : Ast.union_decl) -> @@ -4202,6 +4292,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) incr nclasses; classes := { cr_name = key; cr_gc = false; cr_indexes = []; cr_is_table = false; + cr_backlinks = []; cr_fields = Array.of_list vd.Ast.v_fields; cr_methods = [] } :: !classes @@ -4245,7 +4336,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string) incr nclasses; classes := { cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = []; - cr_indexes = []; cr_is_table = false } + cr_indexes = []; cr_is_table = false; cr_backlinks = [] } :: !classes end) Types.predeclared_records; diff --git a/compiler/src/owner.ml b/compiler/src/owner.ml index a773a50..90c1cc4 100644 --- a/compiler/src/owner.ml +++ b/compiler/src/owner.ml @@ -439,6 +439,7 @@ let oclass_of (ctx : ctx) (ft : Ast.field_ty) : oclass = | Some u -> if u.Types.u_has_payload then Owned else Copy | None -> Copy (* unknown type: WO-E225 already reported by types.ml *)) | Ref _ -> Copy + | Backlink _ -> Copy (* a virtual collection of row ids read on demand *) | Multi _ | Map _ -> Owned | Nullable _ -> Copy (* unreachable: unwrapped above *) diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index 81f5546..24baf93 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -356,6 +356,12 @@ let parse_field_ty (st : state) : Ast.field_ty = | Token.Ident "multi" -> ignore (advance st); Ast.Multi (expect_ident st "multi target type") + | Token.Ident "backlink" -> + ignore (advance st); + let cls = expect_ident st "backlink source class" in + expect st Token.Dot "'.'"; + let fld = expect_ident st "backlink source field" in + Ast.Backlink (cls, fld) | Token.Ident "map" -> ignore (advance st); expect st Token.Lt "'<'"; diff --git a/compiler/src/types.ml b/compiler/src/types.ml index b9cf386..44faf2d 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -306,6 +306,7 @@ let rec has_recursive_structure (cls : class_info) : bool = | Ast.Ref name -> name = cls.name | Ast.Multi name -> name = cls.name (* multi Self *) | Ast.Map (k, v) -> k = cls.name || v = cls.name (* map<_, Self> / map *) + | Ast.Backlink _ -> false | Ast.Nullable inner -> has_recursive_structure_type inner cls.name ) cls.fields @@ -315,6 +316,7 @@ and has_recursive_structure_type (ty : Ast.field_ty) (cls_name : string) : bool | Ast.Ref name -> name = cls_name | Ast.Multi name -> name = cls_name | Ast.Map (k, v) -> k = cls_name || v = cls_name + | Ast.Backlink _ -> false (* a computed inverse holds no owned structure *) | Ast.Nullable inner -> has_recursive_structure_type inner cls_name (* @unique field -> persistent identity (plan's "When NOT to emit": a @@ -363,6 +365,7 @@ let rec typ_of_field_ty (ft : field_ty) : typ = | Ref name -> TRef name | Multi inner_name -> TMulti (TScalar inner_name) | Map (k_name, v_name) -> TMap (TScalar k_name, TScalar v_name) + | Backlink (c, _) -> TMulti (TScalar c) (* reads as a collection of C *) | Nullable inner -> TNullable (typ_of_field_ty inner) (* wob_kind_of_typ: maps internal typ to .wob field kind *) @@ -633,7 +636,7 @@ let rec scalar_name_of (ft : field_ty) : string option = match ft with | Scalar name -> Some name | Nullable inner -> scalar_name_of inner - | Ref _ | Multi _ | Map _ -> None + | Ref _ | Multi _ | Map _ | Backlink _ -> None (* Checked once per field declaration (not at every access/use site), so the diagnostic lands at the field's own declaration position and @@ -1203,7 +1206,7 @@ let typecheck_program ~file ~(module_of : string -> string) with Not_found -> { typ = TScalar "Int"; is_nil = false }) | Field (base, field_name) -> let base_res = typecheck_expr env cenv base in - (match base_res.typ with + (match (match base_res.typ with TRef c -> TScalar c | other -> other) with | TScalar class_name -> (* Only a *declared* class can be checked for a missing field. typecheck_expr falls back to `TScalar "Int"` for everything @@ -1396,7 +1399,8 @@ let typecheck_program ~file ~(module_of : string -> string) the zero word NEW already leaves there). Everything else stays WO-E206, classes and records alike. *) let omittable (default : default_expr option) (fty : field_ty) : bool = - Option.is_some default || (match fty with Nullable _ -> true | _ -> false) + Option.is_some default + || (match fty with Nullable _ | Backlink _ -> true | _ -> false) in List.iter (fun (fname, fty, fdefault, _) -> if not (List.mem fname provided) && not (omittable fdefault fty) then @@ -1420,7 +1424,8 @@ let typecheck_program ~file ~(module_of : string -> string) 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) + Option.is_some default + || (match fty with Nullable _ | Backlink _ -> true | _ -> false) in List.iter (fun (fname, fty, fdefault, _) -> if not (List.mem fname provided) && not (omittable fdefault fty) then @@ -1445,11 +1450,32 @@ let typecheck_program ~file ~(module_of : string -> string) { typ = TMulti (TScalar "Int"); is_nil = false } in (match q.q_src with - | Ast.QNav _ -> - Diag.Collector.add collector - (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col - ~message:"query over a navigation source is not supported yet (table scans only)" ()); - elem_err () + | Ast.QNav nav -> + (* `from s in d.staff`: the navigation yields `multi C`, so the + range var is a C. Reuse the QTable body by resolving C. *) + let nav_res = typecheck_expr env cenv nav in + let cn = + match nav_res.typ with + | TMulti (TScalar c) -> c + | _ -> "" + in + if not (StringMap.mem cn syms.classes) then begin + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:"query navigation source must be a `backlink`/`multi` of a table class" ()); + elem_err () + end + else begin + (if q.q_group <> None || q.q_order <> None || q.q_take <> None then + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col + ~message:"group/order/take on a navigation query are not supported yet" ())); + let env' = StringMap.add q.q_var (TScalar cn) env in + let cenv' = StringMap.add q.q_var (TScalar cn) cenv in + List.iter (fun w -> ignore (typecheck_expr env' cenv' w)) q.q_wheres; + let sel = typecheck_expr env' cenv' q.q_select in + { typ = TMulti sel.typ; is_nil = false } + end | Ast.QTable cn -> if not (StringMap.mem cn syms.classes) then begin Diag.Collector.add collector @@ -2452,6 +2478,7 @@ let rec field_ty_str (ft : field_ty) : string = | Ref s -> "ref " ^ s | Multi s -> "multi " ^ s | Map (k, v) -> "map<" ^ k ^ ", " ^ v ^ ">" + | Backlink (c, f) -> "backlink " ^ c ^ "." ^ f | Nullable t -> "?" ^ field_ty_str t let dump_symbols (syms : symbols) : string = diff --git a/tests/corpus/run/db-query-relations/fixture.out b/tests/corpus/run/db-query-relations/fixture.out new file mode 100644 index 0000000..66e1688 --- /dev/null +++ b/tests/corpus/run/db-query-relations/fixture.out @@ -0,0 +1,7 @@ +-- forward (e.dept.name): +eng +eng +ops +-- backward (d.staff): +asha +bram diff --git a/tests/corpus/run/db-query-relations/fixture.wo b/tests/corpus/run/db-query-relations/fixture.wo new file mode 100644 index 0000000..4aebd04 --- /dev/null +++ b/tests/corpus/run/db-query-relations/fixture.wo @@ -0,0 +1,30 @@ +-- iteration 9b: ref + backlink navigation. `e.dept.name` chains a ref to +-- its target row (a point read); `d.staff` reads a backlink by probing the +-- source class's index — both execute in the engine, no join written. +@table(name: "dept", index: [name]) +class Dept { + name: Text + staff: backlink Emp.dept +} +@table(name: "emp", index: [dept]) +class Emp { + name: Text + dept: ref Dept +} +fn main() { + let eng = insert Dept { name: "eng" } + let ops = insert Dept { name: "ops" } + insert Emp { name: "asha", dept: eng } + insert Emp { name: "bram", dept: eng } + insert Emp { name: "dora", dept: ops } + print("-- forward (e.dept.name):") + for e in from x in Emp select x { + print(e.dept.name) + } + print("-- backward (d.staff):") + for d in from x in Dept where x.name == "eng" select x { + for s in from y in d.staff select y { + print(s.name) + } + } +} From 75b35fb456305511d35cacba499e2528c18ead4a Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 16 Aug 2026 05:20:17 +0200 Subject: [PATCH 4/6] feat(compiler): query order-by + take (9b cont.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `order by [desc]` on whole-row queries: a selection sort over the result multi, re-reading the key per element via the range var (DB_GET_FIELD); O(n^2), KISS, no cost planner — the result sets are small by design - Text order keys use a new WO_B_STR_LT builtin (content compare, reusing the WO_B_SORT elem_cmp); scalar keys use the LT opcode. The bug this fixes: op_lt on two Text pointers compares ADDRESSES - `take N`: clamp to count, slice [0,N). `take` is the KwTake keyword, not an Ident — matched as the token - two bugs found + fixed while testing: multi-line query clauses (skip the separating newlines) and the key-kind read (must bind the range var BEFORE ty_of_expr of the order key, or a Text key silently uses op_lt); Index typechecks to the container's element type (`ds[0]`) - fixture run/db-query-order; oop-e2e 75/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0 - employee `seed`/`list`/`staff` modes now compile and run; report (group-by+projection), raise (update), drop (delete) remain Co-Authored-By: Claude Opus 5 (1M context) --- compiler/src/emit.ml | 141 ++++++++++++++++++++ compiler/src/parser.ml | 14 +- compiler/src/types.ml | 25 ++-- compiler/test/runner.ml | 3 +- runtime/src/builtin.c | 4 + runtime/src/loader.c | 1 + runtime/src/wob.h | 4 +- tests/corpus/run/db-query-order/fixture.out | 7 + tests/corpus/run/db-query-order/fixture.wo | 16 +++ 9 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 tests/corpus/run/db-query-order/fixture.out create mode 100644 tests/corpus/run/db-query-order/fixture.wo diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index a79a9ed..0c13528 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -788,6 +788,7 @@ let class_of_name (p : pctx) (n : string) : int option = SM.find_opt n p.p_class let is_table_class (p : pctx) (cid : int) : bool = cid >= 0 && cid < Array.length p.p_classes && p.p_classes.(cid).cr_is_table +let b_str_lt = 67 let b_db_scan = 64 let b_db_get_field = 65 let b_db_probe = 66 @@ -2567,6 +2568,146 @@ and emit_query (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) f.f_env <- saved_env; (* the scan's id list was this query's own, dropped now *) put f (ins_abc op_drop scan 0 0); + (* ---- order by (whole-row selection sort) ---------------------------- + Elements of dst are row ids; the key re-reads a field through the + range var. Selection sort is O(n^2) but the result sets here are + small and this is KISS by design (no cost planner). Only the + whole-row + field-key shape is supported; grouped/projection ordering + lands with group-by. *) + (match q.Ast.q_order with + | Some (key, desc) -> + f.f_temp <- body_base; + let n = alloc_temp p f e.pos in + put f (ins_abc op_builtin n dst b_count); + let i = alloc_temp p f e.pos in + let j = alloc_temp p f e.pos in + let best = alloc_temp p f e.pos in + let elem_j = alloc_temp p f e.pos in + let elem_b = alloc_temp p f e.pos in + let sort_scratch = f.f_temp in + put f (ins_abx op_loadk i (check_bx p f e.pos "constant" (const_int p 0))); + let oi = here f in (* outer: while i < n *) + let oc = alloc_temp p f e.pos in + put f (ins_abc op_lt oc i n); + let ojz = here f in + put f (ins_asbx op_jz oc 0); + put f (ins_abc op_move best i 0); + let oneA = alloc_temp p f e.pos in + put f (ins_abx op_loadk oneA (check_bx p f e.pos "constant" (const_int p 1))); + put f (ins_abc op_add j i oneA); + let ij = here f in (* inner: while j < n *) + let ic = alloc_temp p f e.pos in + put f (ins_abc op_lt ic j n); + let ijz = here f in + put f (ins_asbx op_jz ic 0); + (* elem_j = multi_get(dst,j); elem_b = multi_get(dst,best) *) + let gw = alloc_temps p f e.pos 2 in + put f (ins_abc op_move gw dst 0); + put f (ins_abc op_move (gw + 1) j 0); + put f (ins_abc op_builtin elem_j gw b_multi_get); + put f (ins_abc op_move (gw + 1) best 0); + put f (ins_abc op_builtin elem_b gw b_multi_get); + (* keys: bind range var to elem_j / elem_b, eval key expr *) + let saved_env2 = f.f_env in + f.f_temp <- sort_scratch; + f.f_env <- (q.Ast.q_var, (elem_j, Scalar cn)) :: saved_env2; + (* key kind must be read with the range var BOUND — else ty_of_expr of + `x.name` sees x unbound, returns None, and a Text key silently falls + to the pointer-comparing op_lt (the wrong-order bug) *) + let key_is_text = + match ty_of_expr p f key with Some t -> field_kind p t = 3 | None -> false + in + let kj = alloc_temp p f e.pos in + emit_expr p f v ~dst:kj key; + f.f_env <- (q.Ast.q_var, (elem_b, Scalar cn)) :: saved_env2; + let kb = alloc_temp p f e.pos in + emit_expr p f v ~dst:kb key; + f.f_env <- saved_env2; + (* cmp: for asc, kj < kb -> best=j; for desc, kj > kb (== kb < kj). *) + let cmp = alloc_temp p f e.pos in + let lt a b = + if key_is_text then begin + let save = f.f_temp in + let w = alloc_temps p f e.pos 2 in + put f (ins_abc op_move w a 0); + put f (ins_abc op_move (w + 1) b 0); + put f (ins_abc op_builtin cmp w b_str_lt); + f.f_temp <- save + end + else put f (ins_abc op_lt cmp a b) + in + if desc then lt kb kj else lt kj kb; + let cjz = here f in + put f (ins_asbx op_jz cmp 0); + put f (ins_abc op_move best j 0); + let after = here f in + patch_jump p f ~file:f.f_file ~pos:e.pos cjz after; + f.f_temp <- sort_scratch; + let oneB = alloc_temp p f e.pos in + put f (ins_abx op_loadk oneB (check_bx p f e.pos "constant" (const_int p 1))); + put f (ins_abc op_add j j oneB); + let iback = here f in + put f (ins_asbx op_jmp 0 0); + patch_jump p f ~file:f.f_file ~pos:e.pos iback ij; + let iexit = here f in + patch_jump p f ~file:f.f_file ~pos:e.pos ijz iexit; + (* swap dst[i], dst[best]: read both, multi_set both *) + f.f_temp <- sort_scratch; + let vi = alloc_temp p f e.pos in + let vb = alloc_temp p f e.pos in + let sw = alloc_temps p f e.pos 3 in + put f (ins_abc op_move sw dst 0); + put f (ins_abc op_move (sw + 1) i 0); + put f (ins_abc op_builtin vi sw b_multi_get); + put f (ins_abc op_move (sw + 1) best 0); + put f (ins_abc op_builtin vb sw b_multi_get); + (* dst[i] = vb *) + put f (ins_abc op_move sw dst 0); + put f (ins_abc op_move (sw + 1) i 0); + put f (ins_abc op_move (sw + 2) vb 0); + put f (ins_abc op_builtin sw sw b_multi_set); + (* dst[best] = vi *) + put f (ins_abc op_move sw dst 0); + put f (ins_abc op_move (sw + 1) best 0); + put f (ins_abc op_move (sw + 2) vi 0); + put f (ins_abc op_builtin sw sw b_multi_set); + f.f_temp <- sort_scratch; + let oneC = alloc_temp p f e.pos in + put f (ins_abx op_loadk oneC (check_bx p f e.pos "constant" (const_int p 1))); + put f (ins_abc op_add i i oneC); + let oback = here f in + put f (ins_asbx op_jmp 0 0); + patch_jump p f ~file:f.f_file ~pos:e.pos oback oi; + let oexit = here f in + patch_jump p f ~file:f.f_file ~pos:e.pos ojz oexit + | None -> ()); + (* ---- take N: slice dst to [0, N) --------------------------------- *) + (match q.Ast.q_take with + | Some tk -> + f.f_temp <- body_base; + let nreg = alloc_temp p f e.pos in + emit_expr p f v ~dst:nreg tk; + (* clamp N to count(dst) so slice never runs past the end *) + let cnt = alloc_temp p f e.pos in + put f (ins_abc op_builtin cnt dst b_count); + let over = alloc_temp p f e.pos in + put f (ins_abc op_lt over cnt nreg); (* count < N ? use count *) + let jz2 = here f in + put f (ins_asbx op_jz over 0); + put f (ins_abc op_move nreg cnt 0); + let aft = here f in + patch_jump p f ~file:f.f_file ~pos:e.pos jz2 aft; + let sw = alloc_temps p f e.pos 3 in + let zero = alloc_temp p f e.pos in + put f (ins_abx op_loadk zero (check_bx p f e.pos "constant" (const_int p 0))); + put f (ins_abc op_move sw dst 0); + put f (ins_abc op_move (sw + 1) zero 0); + put f (ins_abc op_move (sw + 2) nreg 0); + let sliced = alloc_temp p f e.pos in + put f (ins_abc op_builtin sliced sw b_slice); + put f (ins_abc op_drop dst 0 0); (* the pre-slice multi is discarded *) + put f (ins_abc op_move dst sliced 0) + | None -> ()); f.f_temp <- outer and emit_insert (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) (cn : string) diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index 24baf93..10c83a1 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -1054,7 +1054,12 @@ and parse_query_expr (st : state) : Ast.expr = Ast.QTable cn | _ -> Ast.QNav (parse_expr_no_brace st) in - let clause name = match peek st with Token.Ident n when n = name -> true | _ -> false in + (* clauses may sit on their own lines; skip the separating newlines when + looking for the next clause keyword (the query is one expression) *) + let clause name = + skip_newlines st; + match peek st with Token.Ident n when n = name -> true | _ -> false + in let wheres = ref [] in while clause "where" do ignore (advance st); @@ -1087,7 +1092,12 @@ and parse_query_expr (st : state) : Ast.expr = end else None in - let take = if clause "take" then (ignore (advance st); Some (parse_expr_no_brace st)) else None in + (* `take` is a reserved keyword (KwTake, the param convention), not an + Ident — so match the token, not the name *) + skip_newlines st; + let take = + if peek st = Token.KwTake then (ignore (advance st); Some (parse_expr_no_brace st)) else None + in if not (clause "select") then fail st (peek_pos st) syntax_code "a query must end in `select`"; ignore (advance st); let sel = parse_expr st in diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 44faf2d..17dab1a 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -1230,9 +1230,14 @@ let typecheck_program ~file ~(module_of : string -> string) { typ = TScalar "Int"; is_nil = false })) | _ -> { typ = TScalar "Int"; is_nil = false }) | Index (base, idx) -> - let _ = typecheck_expr env cenv base in + let base_res = typecheck_expr env cenv base in let _ = typecheck_expr env cenv idx in - { typ = TScalar "Int"; is_nil = false } + (* `xs[i]` yields the container's element type — a `multi C` indexed + is a C (iteration 9b: query results are indexed to pick a row) *) + (match base_res.typ with + | TMulti et -> { typ = et; is_nil = false } + | TMap (_, vt) -> { typ = vt; is_nil = false } + | _ -> { typ = TScalar "Int"; is_nil = false }) | Call (callee, args) -> List.iter (fun arg -> ignore (typecheck_expr env cenv arg)) args; (match callee.kind with @@ -1466,13 +1471,15 @@ let typecheck_program ~file ~(module_of : string -> string) elem_err () end else begin - (if q.q_group <> None || q.q_order <> None || q.q_take <> None then + (if q.q_group <> None then Diag.Collector.add collector (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col - ~message:"group/order/take on a navigation query are not supported yet" ())); + ~message:"group-by on a navigation query is not supported yet" ())); let env' = StringMap.add q.q_var (TScalar cn) env in let cenv' = StringMap.add q.q_var (TScalar cn) cenv in List.iter (fun w -> ignore (typecheck_expr env' cenv' w)) q.q_wheres; + (match q.q_order with Some (k, _) -> ignore (typecheck_expr env' cenv' k) | None -> ()); + (match q.q_take with Some t -> ignore (typecheck_expr env cenv t) | None -> ()); let sel = typecheck_expr env' cenv' q.q_select in { typ = TMulti sel.typ; is_nil = false } end @@ -1489,17 +1496,11 @@ let typecheck_program ~file ~(module_of : string -> string) Diag.Collector.add collector (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col ~message:"group-by aggregation is not supported yet" ())); - (if q.q_order <> None then - Diag.Collector.add collector - (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col - ~message:"`order by` is not supported yet" ())); - (if q.q_take <> None then - Diag.Collector.add collector - (Diag.error ~code:query_code ~file ~line:q.q_pos.line ~col:q.q_pos.col - ~message:"`take` is not supported yet" ())); let env' = StringMap.add q.q_var (TScalar cn) env in let cenv' = StringMap.add q.q_var (TScalar cn) cenv in List.iter (fun w -> ignore (typecheck_expr env' cenv' w)) q.q_wheres; + (match q.q_order with Some (k, _) -> ignore (typecheck_expr env' cenv' k) | None -> ()); + (match q.q_take with Some t -> ignore (typecheck_expr env cenv t) | None -> ()); let sel = typecheck_expr env' cenv' q.q_select in { typ = TMulti sel.typ; is_nil = false } end) diff --git a/compiler/test/runner.ml b/compiler/test/runner.ml index 1d3846b..8d644a5 100644 --- a/compiler/test/runner.ml +++ b/compiler/test/runner.ml @@ -2604,7 +2604,7 @@ let validate_image (img : string) : string list = 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 || c > 66) then + if c > 12 && (c < 61 || c > 67) 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) @@ -2626,6 +2626,7 @@ let validate_image (img : string) : string list = | 64 -> 1 | 65 -> 3 | 66 -> 3 + | 67 -> 2 | _ -> 0 in if arity > 0 then begin diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index 659ef56..c062bca 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -252,6 +252,10 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { } return 0; } + case WO_B_STR_LT: { + R[A] = elem_cmp(WO_K_TEXT, R[B], R[B + 1]) < 0 ? 1 : 0; + return 0; + } case WO_B_TEXT_COPY: { /* nil copies to nil: a `?Text` crosses this boundary * exactly like a Text does */ if (!R[B]) { diff --git a/runtime/src/loader.c b/runtime/src/loader.c index 33ed884..f475af8 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -47,6 +47,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = { [WO_B_DB_SCAN] = 1, [WO_B_DB_GET_FIELD] = 3, [WO_B_DB_PROBE] = 3, + [WO_B_STR_LT] = 2, [WO_B_NOW] = 0, [WO_B_PRINT] = 1, [WO_B_PRINT_INT] = 1, [WO_B_WORDS] = 1, [WO_B_MULTI_NEW] = 0, [WO_B_MULTI_PUSH] = 2, [WO_B_MULTI_GET] = 2, [WO_B_COUNT] = 1, [WO_B_LATEST] = 1, diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 8b0d427..aa31af3 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -317,8 +317,10 @@ enum { WO_B_DB_SCAN = 64, WO_B_DB_GET_FIELD = 65, WO_B_DB_PROBE = 66, + WO_B_STR_LT = 67, /* (a, b) text -> 1 if a < b by content, else 0 (query + * order-by on a Text key; scalars use the LT opcode) */ }; -#define WO_B_MAX 66u +#define WO_B_MAX 67u /* ids at or above this one live in sysio.c, not builtin.c */ #define WO_B_SYS_FIRST WO_B_FS_EXISTS diff --git a/tests/corpus/run/db-query-order/fixture.out b/tests/corpus/run/db-query-order/fixture.out new file mode 100644 index 0000000..bd92ed8 --- /dev/null +++ b/tests/corpus/run/db-query-order/fixture.out @@ -0,0 +1,7 @@ +salary desc: +bram +dora +asha +name asc take 2: +asha +bram diff --git a/tests/corpus/run/db-query-order/fixture.wo b/tests/corpus/run/db-query-order/fixture.wo new file mode 100644 index 0000000..9ea7610 --- /dev/null +++ b/tests/corpus/run/db-query-order/fixture.wo @@ -0,0 +1,16 @@ +-- iteration 9b: order by (whole-row selection sort, Text-aware) + take. +@table(name: "emp", index: [dept]) +class Emp { + name: Text + salary: Int + dept: Int +} +fn main() { + insert Emp { name: "asha", salary: 100, dept: 1 } + insert Emp { name: "bram", salary: 300, dept: 1 } + insert Emp { name: "dora", salary: 200, dept: 1 } + print("salary desc:") + for e in from x in Emp order by x.salary desc select x { print(e.name) } + print("name asc take 2:") + for e in from x in Emp order by x.name take 2 select x { print(e.name) } +} From c8c34c118cca5d2b77b17de10f1b2ba331dca161 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 16 Aug 2026 05:24:58 +0200 Subject: [PATCH 5/6] feat(compiler): update-through-row + delete statement (9b cont.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `e.field = v` where e is a table row lowers to DB_UPDATE_FIELD (class, id, field, value) — the row's indexes maintained at the choke point; heap-object field assignment still emits SETF unchanged - `delete ` expression: DB_DELETE(class, id), yields the id so it composes in `try delete x catch (e) nil` (restrict/trap surfaces catchably); Delete AST node threaded through type/owner/dump/emit - disassembly-caught bug fixed: in tail position dst == the builtin window's first reg, so moving the id into dst clobbered the class id — reserve dst past the window (the emit_ctor guard) - run/db-update-delete fixture; oop-e2e up, woc-test 566/0, log-watcher 7/0 - employee seed/list/staff/raise/drop now compile+run; only `report` (group-by aggregation + projection record) remains Co-Authored-By: Claude Opus 5 (1M context) --- compiler/src/ast.ml | 4 ++ compiler/src/dump.ml | 1 + compiler/src/emit.ml | 59 +++++++++++++++++++ compiler/src/owner.ml | 4 ++ compiler/src/parser.ml | 8 +++ compiler/src/types.ml | 11 ++++ tests/corpus/run/db-update-delete/fixture.out | 5 ++ tests/corpus/run/db-update-delete/fixture.wo | 19 ++++++ 8 files changed, 111 insertions(+) create mode 100644 tests/corpus/run/db-update-delete/fixture.out create mode 100644 tests/corpus/run/db-update-delete/fixture.wo diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index 92113e3..840f2f0 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -219,6 +219,10 @@ and expr_kind = 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 + (* `delete ` (iteration 9b): removes the row a table-class value + names; an expression yielding the deleted id (restrict/trap surfaces + through the engine like any DB fault, catchable). *) + | Delete of expr (* 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 af5dc5d..3cfe237 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -235,6 +235,7 @@ let rec expr_str (e : Ast.expr) : string = | Some (g, k) -> Printf.sprintf " group by %s into %s" (expr_str k) g | None -> "") (expr_str q.Ast.q_select) + | Ast.Delete t -> Printf.sprintf "DELETE %s" (expr_str t) | 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 0c13528..aacc2f0 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -789,6 +789,8 @@ let is_table_class (p : pctx) (cid : int) : bool = cid >= 0 && cid < Array.length p.p_classes && p.p_classes.(cid).cr_is_table let b_str_lt = 67 +let b_db_update_field = 62 +let b_db_delete = 63 let b_db_scan = 64 let b_db_get_field = 65 let b_db_probe = 66 @@ -1125,6 +1127,7 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option | 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") + | Delete _ -> Some (Scalar "Int") | Query q -> let src = match q.Ast.q_src with @@ -1757,6 +1760,41 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e | 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 + | Delete target -> ( + match ty_of_expr p f target with + | Some bt -> ( + match (match unwrap bt with Ref c -> Scalar c | o -> o) with + | Scalar cn -> ( + match class_of_name p cn with + | Some cid when is_table_class p cid -> + (* reserve dst past the window: in tail position dst == the first + window reg, and moving the id into dst would clobber the class + id — the disassembly-caught bug *) + let outer = f.f_temp in + if f.f_temp <= dst then f.f_temp <- dst + 1; + let w = alloc_temps p f e.pos 2 in + put f (ins_abx op_loadk w (check_bx p f e.pos "constant" (const_int p cid))); + let save = f.f_temp in + emit_expr p f v ~dst:(w + 1) target; + f.f_temp <- save; + (* keep the id so `delete x` can be used as an expression *) + put f (ins_abc op_move dst (w + 1) 0); + sync_mask p f v e.id; + f.f_cur_line <- e.pos.line; + put f (ins_abc op_builtin w w b_db_delete); + f.f_temp <- outer + | _ -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message:"`delete` target is not a table row"; + put f (ins_abx op_loadk dst (const_int p 0))) + | _ -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message:"`delete` target is not a table row"; + put f (ins_abx op_loadk dst (const_int p 0))) + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos + ~message:"cannot resolve the `delete` target's type"; + put f (ins_abx op_loadk dst (const_int p 0))) | Query q -> emit_query p f v ~dst e q | Interp inner -> ( (* haxe-parity Task 2: the type-directed half of the interpolation @@ -3646,6 +3684,27 @@ and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast | None -> err p ~code:cannot_lower_code ~file:f.f_file ~pos:target.pos ~message:(Printf.sprintf "assignment into `%s`, which is not a declared class" cn) + | Some cid when is_table_class p cid -> ( + (* iteration 9b: `e.salary = v` where e is a table row updates the + engine (DB_UPDATE_FIELD: class, id, field, value) — the row's + own indexes are maintained at the choke point *) + match field_of p cid fname with + | None -> + err p ~code:cannot_lower_code ~file:f.f_file ~pos:target.pos + ~message:(Printf.sprintf "`%s` has no field `%s`" cn fname) + | Some (idx, fty) -> + let w = alloc_temps p f target.pos 4 in + put f (ins_abx op_loadk w (check_bx p f target.pos "constant" (const_int p cid))); + let save = f.f_temp in + emit_expr p f v ~dst:(w + 1) base; + f.f_temp <- save; + put f (ins_abx op_loadk (w + 2) (check_bx p f target.pos "constant" (const_int p idx))); + let save = f.f_temp in + emit_expr p f v ~dst:(w + 3) ~expected:fty value; + f.f_temp <- save; + sync_mask p f v s.s_id; + f.f_cur_line <- s.s_pos.line; + put f (ins_abc op_builtin w w b_db_update_field)) | Some cid -> ( match field_of p cid fname with | None -> diff --git a/compiler/src/owner.ml b/compiler/src/owner.ml index 90c1cc4..c61e1db 100644 --- a/compiler/src/owner.ml +++ b/compiler/src/owner.ml @@ -563,6 +563,7 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option = | Ctor (cn, _) -> Some (Scalar cn) | Insert _ -> Some (Scalar "Int") (* the new row's id — Copy, nothing to drop *) | Query _ -> Some (Multi "Int") (* a query yields a fresh multi of ids — owned *) + | Delete _ -> Some (Scalar "Int") (* the deleted id — Copy *) | Interp _ -> Some (Scalar "Text") (* an interpolation always produces Text *) | DbStub _ -> None | Switch (subject, arms) -> @@ -1181,6 +1182,9 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit = | DbStub _ -> (* trap-capable: the frame needs its drop map here *) record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx)) + | Delete t -> + read_expr ctx t; + record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx)) | Query q -> (* iteration 9b: the sub-expressions only READ (engine field-reads copy out at the boundary); the query is trap-capable (engine faults), so diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index 10c83a1..ed64b95 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -1121,6 +1121,13 @@ and parse_query_expr (st : state) : Ast.expr = and parse_primary (st : state) : Ast.expr = match peek st with | _ when is_query_trigger st -> parse_query_expr st + | Token.Ident "delete" when (match (tok_at st (st.pos + 1)).kind with + | Token.Newline | Token.Semicolon | Token.Eof -> false | _ -> true) -> + let pos = peek_pos st in + let id = fresh_id st in + ignore (advance st); + let target = parse_expr st in + { Ast.id; pos; kind = Ast.Delete target } | 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 @@ -1898,6 +1905,7 @@ let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : As { 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.Delete t -> { e with Ast.kind = Ast.Delete (subst_expr consts bound t) } | Ast.Query q -> (* the range/group vars shadow consts inside the query body *) let bound' = StringSet.add q.Ast.q_var bound in diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 17dab1a..4b904ec 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -1173,6 +1173,7 @@ let typecheck_program ~file ~(module_of : string -> string) (* the new row's id — the one thing an insert produces *) Some (TScalar "Int") | Query _ -> None (* a query's type is chased only by typecheck_expr *) + | Delete _ -> 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` @@ -1444,6 +1445,15 @@ 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 insert" class_name) ()); { typ = TScalar "Int"; is_nil = false }) + | Delete target -> + let tr = typecheck_expr env cenv target in + (match (match tr.typ with TRef c -> TScalar c | o -> o) with + | TScalar cn when StringMap.mem cn syms.classes -> () + | _ -> + Diag.Collector.add collector + (Diag.error ~code:query_code ~file ~line:e.pos.line ~col:e.pos.col + ~message:"`delete` takes a table-row value" ())); + { typ = TScalar "Int"; is_nil = false } | Query q -> (* iteration 9b slice: from/where/select over a table class. The range variable is bound to the class type; a table-class value is @@ -2211,6 +2221,7 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e walk_expr bound visit body; walk_block (StringSet.add ename bound) visit handler | DbStub _ -> () + | Delete t -> walk_expr bound visit t | Query q -> (match q.q_src with QNav e -> walk_expr bound visit e | QTable _ -> ()); let b = StringSet.add q.q_var bound in diff --git a/tests/corpus/run/db-update-delete/fixture.out b/tests/corpus/run/db-update-delete/fixture.out new file mode 100644 index 0000000..48bc362 --- /dev/null +++ b/tests/corpus/run/db-update-delete/fixture.out @@ -0,0 +1,5 @@ +after raise: +asha 150 +bram 250 +after delete: +bram diff --git a/tests/corpus/run/db-update-delete/fixture.wo b/tests/corpus/run/db-update-delete/fixture.wo new file mode 100644 index 0000000..67d4c11 --- /dev/null +++ b/tests/corpus/run/db-update-delete/fixture.wo @@ -0,0 +1,19 @@ +-- iteration 9b: update-through-row (e.f = v -> DB_UPDATE_FIELD) and the +-- delete statement (delete -> DB_DELETE), both via the choke point. +@table(name: "emp", index: [dept]) +class Emp { + name: Text + salary: Int + dept: Int +} +fn main() { + insert Emp { name: "asha", salary: 100, dept: 1 } + insert Emp { name: "bram", salary: 200, dept: 1 } + for e in from x in Emp select x { e.salary = e.salary + 50 } + print("after raise:") + for e in from x in Emp order by x.name select x { print("${e.name} ${e.salary}") } + let gone = from x in Emp where x.name == "asha" take 1 select x + delete gone[0] + print("after delete:") + for e in from x in Emp select x { print(e.name) } +} From 2677c1457e14575327a9bcaef27fd291b035f1f1 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 16 Aug 2026 18:37:23 +0200 Subject: [PATCH 6/6] feat: FK restrict on delete + employee sample runs; group-by parked (9b) - FK restrict: deleting a row a non-nullable `ref` still points at traps WO_T_FK (11), catchable. The compiler now records a `ref` field's target class in the class-table field_class metadata; the engine (wo_row_has_referrers) scans referencing scalar columns before a delete. Correctness-first full scan; the backlink-index optimization is recorded for later - docs/examples/employee now COMPILES AND RUNS all six modes against a WAL-durable database: seed (+@unique trap across restart), report (per-dept aggregates + payroll), staff (unique probe + backlink + ref nav), raise (update-through-row), drop (FK restrict), and persistence via replay - group-by SYNTAX parked to a future iteration (user decision): the report mode is hand-rolled from the shipped primitives meanwhile (same numbers). "table relations and FK" is complete - scripts/employee-accept.sh (8 checks) + a `just employee` module; manifest parser tolerates iteration 9c's [share]/[[share.clients]] sections so `woc .` builds the sample on this branch - fixtures trap/db-fk-restrict (code 11) + run/db-fk-restrict-catch; oop-e2e 79/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0, employee-accept 8/0 - 9b story + status board updated Co-Authored-By: Claude Opus 5 (1M context) --- compiler/bin/main.ml | 19 +++- compiler/src/emit.ml | 3 +- database/src/db.c | 6 + database/src/table.c | 22 ++++ database/src/table.h | 8 ++ docs/00-status.md | 2 +- docs/examples/employee/justfile | 14 +++ docs/examples/employee/main.wo | 37 ++++-- docs/examples/employee/target/employee | Bin 0 -> 98980 bytes .../09b-table-relations-query.md | 29 +++++ justfile | 5 + runtime/src/wob.h | 3 + scripts/employee-accept.sh | 106 ++++++++++++++++++ .../run/db-fk-restrict-catch/fixture.out | 2 + .../run/db-fk-restrict-catch/fixture.wo | 23 ++++ tests/corpus/trap/db-fk-restrict/fixture.trap | 1 + tests/corpus/trap/db-fk-restrict/fixture.wo | 18 +++ 17 files changed, 282 insertions(+), 16 deletions(-) create mode 100644 docs/examples/employee/justfile create mode 100755 docs/examples/employee/target/employee create mode 100755 scripts/employee-accept.sh create mode 100644 tests/corpus/run/db-fk-restrict-catch/fixture.out create mode 100644 tests/corpus/run/db-fk-restrict-catch/fixture.wo create mode 100644 tests/corpus/trap/db-fk-restrict/fixture.trap create mode 100644 tests/corpus/trap/db-fk-restrict/fixture.wo diff --git a/compiler/bin/main.ml b/compiler/bin/main.ml index a8f1276..a3240bb 100644 --- a/compiler/bin/main.ml +++ b/compiler/bin/main.ml @@ -600,10 +600,23 @@ let manifest_parse (path : string) : (string * string) list = if line = "" || (String.length line >= 1 && line.[0] = '#') then () else if line.[0] = '[' then begin if line.[String.length line - 1] <> ']' then fail !lineno "malformed section header"; - section := String.sub line 1 (String.length line - 2); - if !section <> "runtime" && !section <> "build" then - fail !lineno (Printf.sprintf "unknown section [%s] (runtime and build exist)" !section) + (* accept `[[table.array]]` headers too (iteration 9c's + [[share.clients]]) by trimming the doubled brackets *) + let inner = String.sub line 1 (String.length line - 2) in + let inner = + if String.length inner >= 2 && inner.[0] = '[' && inner.[String.length inner - 1] = ']' + then String.sub inner 1 (String.length inner - 2) + else inner + in + section := inner; + if !section <> "runtime" && !section <> "build" && !section <> "share" + && !section <> "share.clients" + then + fail !lineno + (Printf.sprintf "unknown section [%s] (runtime and build exist)" !section) end + else if !section = "share" || !section = "share.clients" then + () (* iteration 9c manifest keys — parsed by the attach feature, ignored here *) else match String.index_opt line '=' with | None -> fail !lineno "expected `key = \"value\"`" diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index aacc2f0..5dcf173 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -1426,7 +1426,8 @@ let field_class_meta (p : pctx) (ty : Ast.field_ty) : int = match name_of (Ast.Scalar e) with | Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none) | None -> wob_none) - | Ast.Ref _ | Ast.Backlink _ | Ast.Nullable _ -> wob_none + | Ast.Ref n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none) + | Ast.Backlink _ | Ast.Nullable _ -> wob_none let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int = match unwrap ty with diff --git a/database/src/db.c b/database/src/db.c index a370550..3f40d88 100644 --- a/database/src/db.c +++ b/database/src/db.c @@ -57,6 +57,12 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { case WO_B_DB_DELETE: { uint32_t cid = (uint32_t)R[B]; uint64_t id = R[B + 1]; + /* FK restrict: refuse if another row still references this one + (iteration 9b) — nothing is removed, the statement traps */ + if (wo_row_has_referrers(db, cid, id)) { + *msg = "row is still referenced (restrict)"; + return WO_T_FK; + } if (wo_row_remove(db, cid, id) != 0) { *msg = "no such row"; return WO_T_DB; diff --git a/database/src/table.c b/database/src/table.c index cf2b348..a8dc324 100644 --- a/database/src/table.c +++ b/database/src/table.c @@ -703,6 +703,28 @@ int wo_row_update_field(wo_db *db, uint32_t class_id, uint64_t id, uint32_t fiel return 0; } +int wo_row_has_referrers(wo_db *db, uint32_t class_id, uint64_t id) { + if (!id) return 0; + for (uint32_t c = 0; c < db->class_cnt; c++) { + const wo_classdesc *cd = &db->classes[c]; + db_table *t = &db->tables[c]; + if (!t->row_size || !cd->field_class) continue; + for (uint32_t fld = 0; fld < cd->field_cnt; fld++) { + /* a scalar column whose recorded field_class is our target is a + `ref` to it (WOB_NONE / JSON_RAW / NIL_SCALAR are not class ids) */ + if (cd->kinds[fld] != WO_K_SCALAR || cd->field_class[fld] != class_id) continue; + uint32_t total = t->slab_cnt * DB_SLAB_ROWS; + for (uint32_t g = 0; g < total; g++) { + if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue; + db_row *r = (db_row *)(t->slabs[g / DB_SLAB_ROWS] + + (size_t)(g % DB_SLAB_ROWS) * t->row_size); + if (r->slots[fld] == id) return 1; + } + } + } + return 0; +} + int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id) { if (class_id >= db->class_cnt) return -1; db_table *t = &db->tables[class_id]; diff --git a/database/src/table.h b/database/src/table.h index 80805b8..cf0dd62 100644 --- a/database/src/table.h +++ b/database/src/table.h @@ -150,6 +150,14 @@ int wo_row_read(wo_db *db, wo_rt *rt, uint32_t class_id, uint64_t id, * it. 0 ok, -1 no such row. */ int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id); +/* iteration 9b FK restrict: 1 if some row in some class holds a non-nullable + * `ref` to [class_id] equal to [id] — i.e. deleting this row would dangle a + * reference. The compiler records a ref field's target class in the class + * table's field_class metadata; this scans those columns. Correctness-first + * (a full scan of referencing tables); the backlink index is the later + * optimization the spec records. */ +int wo_row_has_referrers(wo_db *db, uint32_t class_id, uint64_t id); + /* Update one field in place (iteration 9 Task 5): encode the VM value, * swap it into the slot, keep every index containing that column honest — * remove-old/add-new with the unique re-check running BEFORE anything diff --git a/docs/00-status.md b/docs/00-status.md index 0cfd7ae..c2c0b95 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -116,7 +116,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⏸ off the workload's path (no `@gc`) | | 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ | | 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | 🔄 engine complete (storage/WAL/indexes/insert-update-delete); reads land with 9b | -| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ spec + plan ready (2026-08-15) | +| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | 🔄 query surface + relations + FK done (branch query-surface); group-by parked | | 9c | [Cross-program tables](stories/language-runtime-database/09c-cross-program-tables.md) | 🔄 channel done (branch ipc-attach); manifest+binding pending | | 9d | [Keypair attach auth](stories/language-runtime-database/09d-keypair-attach-auth.md) | 🔄 crypto+handshake done (branch keypair-auth); manifest pending | | 9e | [Durability, throughput, scale](stories/language-runtime-database/09e-durability-throughput-scale.md) | ⬜ needs a spec first | diff --git a/docs/examples/employee/justfile b/docs/examples/employee/justfile new file mode 100644 index 0000000..6a7c168 --- /dev/null +++ b/docs/examples/employee/justfile @@ -0,0 +1,14 @@ +# docs/examples/employee — the database track's acceptance workload. +# `just employee::` from the repo root, or plain `just ` here. +ROOT := source_directory() / "../../.." + +default: accept + +# build the standalone binary from wo.toml (needs woc-build + wovm-build once) +build: + {{ROOT}}/compiler/_build/default/bin/woc . + @ls -la target/employee + +# the acceptance: compile + every mode against a WAL-durable database +accept: + {{ROOT}}/scripts/employee-accept.sh diff --git a/docs/examples/employee/main.wo b/docs/examples/employee/main.wo index 9b6975b..cedd19d 100644 --- a/docs/examples/employee/main.wo +++ b/docs/examples/employee/main.wo @@ -48,19 +48,34 @@ fn seed() -> Int { return 0; } --- One GROUP BY after another: group-and-reduce lowers to a single hash pass --- (no group objects), avg/min/max are ?Int because an empty group is data. +-- Per-department aggregates. The group-by SYNTAX +-- from e in Employee group e by e.dept into g order by avg(g.salary) desc +-- select { dept: g.key.name, headcount: count(g), avg_salary: avg(g.salary), ... } +-- is PARKED for a future iteration (compile-time group-and-reduce + projection +-- records). Until it lands, the same report is hand-rolled from the primitives +-- that DO exist — a scan of departments, a backlink scan of each one's staff, +-- and plain scalar accumulation. Same numbers, more lines; the group-by +-- version is the ergonomic upgrade, not a new capability. fn report() -> Int { - let rows = from e in Employee - group e by e.dept into g - order by avg(g.salary) desc - select { dept: g.key.name, headcount: count(g), - avg_salary: avg(g.salary), min_salary: min(g.salary), - max_salary: max(g.salary) }; - for r in rows { - print("DEPT ${r.dept} headcount=${r.headcount} avg=${r.avg_salary} min=${r.min_salary} max=${r.max_salary}"); + let payroll = 0; + for d in from x in Department order by x.name select x { + let headcount = 0; + let total = 0; + let smin = -1; + let smax = -1; + for e in from s in d.staff select s { + headcount = headcount + 1; + total = total + e.salary; + payroll = payroll + e.salary; + if smin == -1 or e.salary < smin { smin = e.salary; } + if smax == -1 or e.salary > smax { smax = e.salary; } + } + if headcount == 0 { + print("DEPT ${d.name} headcount=0 avg=nil min=nil max=nil"); + } else { + print("DEPT ${d.name} headcount=${headcount} avg=${total / headcount} min=${smin} max=${smax}"); + } } - let payroll = sum(from e in Employee select e.salary); print("PAYROLL ${payroll}"); return 0; } diff --git a/docs/examples/employee/target/employee b/docs/examples/employee/target/employee new file mode 100755 index 0000000000000000000000000000000000000000..88cf7e3062b0cdc2a549c7f4de6cba2d9194d540 GIT binary patch literal 98980 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 literal 0 HcmV?d00001 diff --git a/docs/stories/language-runtime-database/09b-table-relations-query.md b/docs/stories/language-runtime-database/09b-table-relations-query.md index 6603518..dd2f722 100644 --- a/docs/stories/language-runtime-database/09b-table-relations-query.md +++ b/docs/stories/language-runtime-database/09b-table-relations-query.md @@ -132,6 +132,35 @@ validation (`WO-E102`), the Rust runtime already ships secondary indexes and id rather than a pointer — so the relational vocabulary partly exists and this iteration makes it mean something in the C stack. +## Query surface landed (2026-08-16, branch `query-surface`) + +The compiler-checked query surface runs end to end, proven by +`docs/examples/employee` (8-check acceptance, `scripts/employee-accept.sh`): + +- **Queries**: `from in where* [order by [desc]] [take n] + select `, lowered to bytecode loops over engine cursor builtins + (DB_SCAN / DB_GET_FIELD / DB_PROBE) — no SQL text, disassembly-provable. +- **Relations**: `ref C` forward navigation (`e.dept.name`, a point read), + `backlink C.f` reverse navigation (`d.staff`, an index probe); backlink + fields are virtual (no stored column). +- **Mutation**: update-through-row (`e.salary = v` → DB_UPDATE_FIELD), + `delete `, and **FK restrict** — deleting a row a `ref` still points at + traps `WO_T_FK` (the compiler records the ref target in the class table's + field_class metadata; the engine scans referencing columns). +- **`@unique`** violations trap and are catchable; everything is WAL-durable + and survives a process restart (proven in the acceptance). + +**PARKED to a future iteration (2026-08-16, user decision):** **group-by +aggregation** — the `group … by … into g … select { count(g), avg(g.salary), +… }` syntax, which needs projection-record synthesis (anonymous record types), +aggregate clause-functions, and two-phase hash aggregation. The employee +sample's `report` mode is hand-rolled from the shipped primitives meanwhile +(a scan of departments × a backlink scan of each one's staff × scalar +accumulation) — same numbers, and the group-by version is the ergonomic +upgrade, not a new capability. The relational vocabulary these queries used +(`ref`/`backlink`/`@unique`/restrict) is the "table relations and FK" half, +now complete. + ## Proposed Solution - ~~Brainstorm a spec first~~ — **done 2026-08-15**; the spec settles all diff --git a/justfile b/justfile index 47a8455..5f587ff 100644 --- a/justfile +++ b/justfile @@ -50,6 +50,11 @@ wovm-test: # pass; the corpus below gates the individual behaviors underneath it. mod log-watcher "docs/examples/log-watcher" +# the database track's acceptance workload (iteration 9/9b): @table storage, +# ref/backlink relations + FK restrict, and the compiler-checked query surface +# (scan/where/select/order/take, update, delete). `just employee` runs it. +mod employee "docs/examples/employee" + # conformance harness (plan 3): walks tests/corpus/{run,compile-fail,trap}, # exact outcome per fixture kind — see docs/plan/oop-vm/02-corpus.md. # Fails loudly (and names the recipe to run) if woc or wovm isn't built. diff --git a/runtime/src/wob.h b/runtime/src/wob.h index aa31af3..4d797b6 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -125,6 +125,9 @@ enum { raised by the engine at the row choke point, catchable like any trap (the employee sample's SEED-DUP line) */ WO_T_UNIQUE = 10, + /* iteration 9b: deleting a row still referenced by a `ref` traps here + (restrict) — the employee sample's DROP-of-a-department-with-staff */ + WO_T_FK = 11, }; /* ---- opcodes (spec section 5; semantics in the format doc) ---- */ diff --git a/scripts/employee-accept.sh b/scripts/employee-accept.sh new file mode 100755 index 0000000..e679c47 --- /dev/null +++ b/scripts/employee-accept.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# scripts/employee-accept.sh — the database track's acceptance workload. +# +# docs/examples/employee must compile via `woc ` and run all its modes +# against a real WAL-durable database: insert + @unique trap, per-department +# aggregates, ref/backlink navigation, update-through-row, FK restrict on +# delete, and persistence across a process restart. This is iteration 9/9b's +# acceptance the way log-watcher is iterations 1-7's. +# +# Group-by SYNTAX is parked (a future iteration); report is hand-rolled from +# the primitives, so the numbers below exercise the shipped query surface. + +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WOC="$ROOT/compiler/_build/default/bin/woc" +WOVM="$ROOT/runtime/wovm" +SAMPLE="$ROOT/docs/examples/employee" + +pass=0 +fail=0 +ok() { echo "ok $1"; pass=$((pass + 1)); } +bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); } + +if [[ ! -x "$WOC" || ! -x "$WOVM" ]]; then + echo "employee-accept: build woc and wovm first (just woc-build; just wovm-build)" >&2 + exit 1 +fi + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/emp-accept.XXXXXX")" +DATA="$WORK/data" +mkdir -p "$DATA" +IMG="$WORK/employee.wob" +cleanup() { [[ -n "${EMP_ACCEPT_KEEP:-}" ]] && echo "kept $WORK" || rm -rf "$WORK"; } +trap cleanup EXIT + +# ---- 1. compile ------------------------------------------------------ +if "$WOC" --emit "$SAMPLE" -o "$IMG" >"$WORK/compile.out" 2>&1; then + ok "compile ($(stat -c%s "$IMG") bytes)" +else + bad "compile" "$(head -1 "$WORK/compile.out")" + echo; printf 'employee-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"; exit 1 +fi + +run() { WO_DATA="$DATA" "$WOVM" "$IMG" "$@"; } + +# ---- 2. seed (insert + WAL) ------------------------------------------ +if run seed 2>&1 | grep -q "^SEEDED 3 departments, 6 employees"; then + ok "seed (insert, WAL-durable)" +else + bad "seed" "no SEEDED line" +fi + +# ---- 3. seed again: @unique trap, caught, across a process boundary -- +out="$(run seed 2>&1)"; rc=$? +if [[ "$out" == *"SEED-DUP"* && $rc -eq 3 ]]; then + ok "unique violation caught on re-seed (persisted via replay)" +else + bad "unique re-seed" "got rc=$rc: $(printf '%s' "$out" | tr '\n' '|' | cut -c1-100)" +fi + +# ---- 4. report: per-department aggregates + payroll ------------------ +rep="$(run report 2>&1)" +if [[ "$rep" == *"DEPT Engineering headcount=3 avg=8200000 min=7300000 max=9200000"* \ + && "$rep" == *"DEPT Operations headcount=2 avg=6150000 min=5900000 max=6400000"* \ + && "$rep" == *"PAYROLL 45700000"* ]]; then + ok "report (aggregates + payroll)" +else + bad "report" "$(printf '%s' "$rep" | tr '\n' '|' | cut -c1-160)" +fi + +# ---- 5. staff: index probe + backlink + ref navigation -------------- +st="$(run staff Engineering 2>&1)" +if [[ "$st" == *"STAFF Asha 9200000 (Engineering)"* \ + && "$st" == *"STAFF Chidi 7300000 (Engineering)"* ]]; then + ok "staff (unique probe + backlink scan + ref nav)" +else + bad "staff" "$(printf '%s' "$st" | tr '\n' '|' | cut -c1-160)" +fi + +# ---- 6. raise: update-through-row, reflected in a re-report ---------- +run raise Operations 5 >/dev/null 2>&1 +if run report 2>&1 | grep -q "^DEPT Operations headcount=2 avg=6457500"; then + ok "raise (update-through-row, durable)" +else + bad "raise" "operations average did not move to 6457500" +fi + +# ---- 7. drop: FK restrict (Engineering still has staff) -------------- +out="$(run drop Engineering 2>&1)"; rc=$? +if [[ "$out" == *"restricted"* && $rc -eq 4 ]]; then + ok "drop restricted by FK (department has staff)" +else + bad "drop restrict" "got rc=$rc: $(printf '%s' "$out" | tr '\n' '|' | cut -c1-100)" +fi + +# ---- 8. persistence: a fresh process still sees every acked write ---- +if run report 2>&1 | grep -q "^PAYROLL 46315000"; then + ok "persistence (replay: raised payroll survives restart)" +else + bad "persistence" "payroll after restart not 46315000" +fi + +echo +printf 'employee-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail" +[[ $fail -eq 0 ]] diff --git a/tests/corpus/run/db-fk-restrict-catch/fixture.out b/tests/corpus/run/db-fk-restrict-catch/fixture.out new file mode 100644 index 0000000..9be7799 --- /dev/null +++ b/tests/corpus/run/db-fk-restrict-catch/fixture.out @@ -0,0 +1,2 @@ +eng restricted +ops deleted diff --git a/tests/corpus/run/db-fk-restrict-catch/fixture.wo b/tests/corpus/run/db-fk-restrict-catch/fixture.wo new file mode 100644 index 0000000..16fe671 --- /dev/null +++ b/tests/corpus/run/db-fk-restrict-catch/fixture.wo @@ -0,0 +1,23 @@ +-- the restrict trap is catchable; a free (unreferenced) row deletes. +@table(name: "dept", index: [name]) +class Dept { + name: Text + staff: backlink Emp.dept +} +@table(name: "emp", index: [dept]) +class Emp { + name: Text + dept: ref Dept +} +fn main() { + let eng = insert Dept { name: "eng" } + let ops = insert Dept { name: "ops" } + insert Emp { name: "asha", dept: eng } + let de = from x in Dept where x.name == "eng" take 1 select x + let r1 = try delete de[0] catch (e) nil + if r1 == nil { print("eng restricted") } + let dop = from x in Dept where x.name == "ops" take 1 select x + let r2 = try delete dop[0] catch (e) nil + if r2 == nil { print("ops restricted (WRONG)") } + print("ops deleted") +} diff --git a/tests/corpus/trap/db-fk-restrict/fixture.trap b/tests/corpus/trap/db-fk-restrict/fixture.trap new file mode 100644 index 0000000..9d60796 --- /dev/null +++ b/tests/corpus/trap/db-fk-restrict/fixture.trap @@ -0,0 +1 @@ +11 \ No newline at end of file diff --git a/tests/corpus/trap/db-fk-restrict/fixture.wo b/tests/corpus/trap/db-fk-restrict/fixture.wo new file mode 100644 index 0000000..517ccb4 --- /dev/null +++ b/tests/corpus/trap/db-fk-restrict/fixture.wo @@ -0,0 +1,18 @@ +-- iteration 9b: deleting a row a `ref` still points at traps WO_T_FK +-- (restrict). Uncaught here: the trap code (11) is the assertion. +@table(name: "dept", index: [name]) +class Dept { + name: Text + staff: backlink Emp.dept +} +@table(name: "emp", index: [dept]) +class Emp { + name: Text + dept: ref Dept +} +fn main() { + let d = insert Dept { name: "eng" } + insert Emp { name: "asha", dept: d } + let ds = from x in Dept where x.name == "eng" take 1 select x + delete ds[0] +}