feat(compiler): update-through-row + delete statement (9b cont.)
- `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 <row>` 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) <noreply@anthropic.com>
This commit is contained in:
parent
75b35fb456
commit
c8c34c118c
8 changed files with 111 additions and 0 deletions
|
|
@ -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 <row>` (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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
tests/corpus/run/db-update-delete/fixture.out
Normal file
5
tests/corpus/run/db-update-delete/fixture.out
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
after raise:
|
||||
asha 150
|
||||
bram 250
|
||||
after delete:
|
||||
bram
|
||||
19
tests/corpus/run/db-update-delete/fixture.wo
Normal file
19
tests/corpus/run/db-update-delete/fixture.wo
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- iteration 9b: update-through-row (e.f = v -> DB_UPDATE_FIELD) and the
|
||||
-- delete statement (delete <row> -> 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) }
|
||||
}
|
||||
Loading…
Reference in a new issue