From c8c34c118cca5d2b77b17de10f1b2ba331dca161 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 16 Aug 2026 05:24:58 +0200 Subject: [PATCH] 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) } +}