From b6d7d4c940e2e17878b35778b096688dae5f8be9 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Fri, 14 Aug 2026 16:46:53 +0200 Subject: [PATCH] feat: map iteration (for k, v in m) + multi element writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit log-watcher parse errors 7 -> 3 (only `as` left); corpus 71/0, woc 565/0. - wob.h/builtin.c/loader.c: WO_B_MULTI_SET — `m[i] = v` for a multi, dropping the element it replaces (the mirror of map_set, which the format doc's sugar rule already had; the "no element write" gap is closed) - ast/parser: `for k, v in m` — the second name binds the value for that key - types.ml/owner.ml: the two cursors take the map's key and value types; both are borrows of what the map owns, so neither is dropped per iteration - emit.ml: map form lowers to len + key_at/val_at over slot indexes (insertion order, cont.h's parallel arrays), same loop skeleton as the multi form Co-Authored-By: Claude Opus 5 (1M context) --- compiler/src/ast.ml | 5 +++ compiler/src/dump.ml | 3 +- compiler/src/emit.ml | 92 ++++++++++++++++++++++++++++++++++++++---- compiler/src/owner.ml | 27 +++++++++---- compiler/src/parser.ml | 16 ++++++-- compiler/src/types.ml | 29 +++++++++---- runtime/src/builtin.c | 14 +++++++ runtime/src/loader.c | 2 +- runtime/src/wob.h | 5 ++- 9 files changed, 164 insertions(+), 29 deletions(-) diff --git a/compiler/src/ast.ml b/compiler/src/ast.ml index 8564ad6..7f1cd9f 100644 --- a/compiler/src/ast.ml +++ b/compiler/src/ast.ml @@ -318,6 +318,11 @@ and stmt_kind = } | For of { var : string; + (* `for k, v in m` over a `map`: the second name binds the value + for that key. None is the one-name form, over a `multi`. Map + enumeration is slot-ordered (runtime/src/cont.h's parallel arrays), + which is insertion order. *) + var2 : string option; iter : expr; body : stmt list; } diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index 5012ded..87e0bfe 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -273,7 +273,8 @@ let rec dump_stmt (s : Ast.stmt) : string list = (header :: indent_block then_body) @ else_lines | Ast.While { cond; body } -> Printf.sprintf "%s WHILE %s" (pos_str s.Ast.s_pos) (expr_str cond) :: indent_block body - | Ast.For { var; iter; body } -> + | Ast.For { var; var2; iter; body } -> + let var = match var2 with Some v2 -> var ^ ", " ^ v2 | None -> var in Printf.sprintf "%s FOR %s IN %s" (pos_str s.Ast.s_pos) var (expr_str iter) :: indent_block body | Ast.Return None -> [ Printf.sprintf "%s RETURN" (pos_str s.Ast.s_pos) ] | Ast.Return (Some e) -> [ Printf.sprintf "%s RETURN %s" (pos_str s.Ast.s_pos) (expr_str e) ] diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index cd7df77..8697ee1 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -254,6 +254,7 @@ let b_reverse = 35 let b_map_remove = 36 let b_map_key_at = 37 let b_map_val_at = 38 +let b_multi_set = 39 let ins_abc op a b c = op lor (a lsl 8) lor (b lsl 16) lor (c lsl 24) let ins_abx op a bx = op lor (a lsl 8) lor (bx lsl 16) @@ -2605,7 +2606,7 @@ and emit_stmt (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit = | Return opt -> emit_return p f v s opt | If { cond; then_body; else_body } -> emit_if p f v s cond then_body else_body | While { cond; body } -> emit_while p f v s cond body - | For { var; iter; body } -> emit_for p f v s var iter body + | For { var; var2; iter; body } -> emit_for p f v s var var2 iter body | Break -> emit_break p f v s | Continue -> emit_continue p f v s | DoWhile { body; cond } -> emit_do_while p f v s body cond @@ -2735,10 +2736,20 @@ and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast put f (ins_abc op_builtin sink w b_map_set); release_guards f guards | Multi _ -> - err p ~code:cannot_lower_code ~file:f.f_file ~pos:target.pos - ~message: - "element assignment into a `multi` — the v1 instruction set has push/get but no \ - element write" + (* `m[i] = x` is the multi_set builtin — container, index, value in + three consecutive registers, exactly the map_set shape above. + The element it replaces is the container's, so the VM drops it. *) + let w = alloc_temps p f s.s_pos 3 in + emit_expr p f v ~dst:w base; + Hashtbl.replace f.f_node target.id w; + emit_expr p f v ~dst:(w + 1) idx; + emit_expr p f v ~dst:(w + 2) value; + let sink = alloc_temp p f s.s_pos in + f.f_cur_line <- s.s_pos.line; + let guards = residual_guards p f v s.s_id None in + acquire_guards f guards; + put f (ins_abc op_builtin sink w b_multi_set); + release_guards f guards | _ -> err p ~code:cannot_lower_code ~file:f.f_file ~pos:target.pos ~message:"element assignment into a value that is neither a `multi` nor a `map`") @@ -2928,9 +2939,76 @@ and emit_while (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (cond : Ast.ex because BUILTIN's argument window is consecutive. A `map` has no key-enumeration builtin in v1, so iterating one is WO-E403 rather than invented bytecode. *) -and emit_for (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (var : string) (iter : Ast.expr) - (body : Ast.stmt list) : unit = +and emit_for (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (var : string) + (var2 : string option) (iter : Ast.expr) (body : Ast.stmt list) : unit = match ty_of_expr p f iter with + | Some t + when (match (unwrap t, var2) with Map _, Some _ -> true | _ -> false) -> + (* `for k, v in m` — slot-ordered enumeration over the map's parallel + arrays: `len` bounds it, `key_at`/`val_at` read slot i. Both cursors + are borrows of what the map owns, so nothing is dropped per + iteration (owner.ml declares them l_holds = false). *) + let kt, vt = match unwrap t with Map (k, v') -> (Scalar k, Scalar v') | _ -> (Scalar "Int", Scalar "Int") in + let v2 = match var2 with Some n -> n | None -> "_" in + let saved_locals = f.f_nlocals in + let saved_env = f.f_env in + let saved_decls = f.f_declared in + let div0 = f.f_div in + let rc = alloc_local p f s.s_pos in + let ri = alloc_local p f s.s_pos in + let rn = alloc_local p f s.s_pos in + let rk = alloc_local p f s.s_pos in + let rv = alloc_local p f s.s_pos in + f.f_temp <- f.f_nlocals; + emit_expr p f v ~dst:rc iter; + f.f_cur_line <- s.s_pos.line; + put f (ins_abc op_builtin rn rc b_len); + put f (ins_abx op_loadk ri (check_bx p f s.s_pos "constant" (const_int p 0))); + f.f_env <- (v2, (rv, vt)) :: (var, (rk, kt)) :: f.f_env; + Hashtbl.replace f.f_decl s.s_id rk; + f.f_declared <- s.s_id :: f.f_declared; + let top = here f in + let entry_owned = f.f_owned and entry_gc = f.f_gc in + f.f_temp <- f.f_nlocals; + let tc = alloc_temp p f s.s_pos in + put f (ins_abc op_lt tc ri rn); + let jz = here f in + put f (ins_asbx op_jz tc 0); + (* the builtin window is (container, index) in two consecutive slots *) + let w = alloc_temps p f s.s_pos 2 in + put f (ins_abc op_move w rc 0); + put f (ins_abc op_move (w + 1) ri 0); + put f (ins_abc op_builtin rk w b_map_key_at); + put f (ins_abc op_builtin rv w b_map_val_at); + let lf = { lf_node = s.s_id; lf_breaks = []; lf_continues = [] } in + f.f_loops <- lf :: f.f_loops; + List.iter (emit_stmt p f v) body; + f.f_loops <- List.tl f.f_loops; + emit_scope_drops p f v ~node:s.s_id ~label:"FOR"; + emit_rc p f v ~node:s.s_id ~acquire:false ~groups:(declared_since f saved_decls) (); + let continue_target = here f in + List.iter (fun pc -> patch_jump p f ~file:f.f_file ~pos:s.s_pos pc continue_target) lf.lf_continues; + f.f_temp <- f.f_nlocals; + f.f_cur_line <- s.s_pos.line; + let one = alloc_temp p f s.s_pos in + put f (ins_abx op_loadk one (check_bx p f s.s_pos "constant" (const_int p 1))); + put f (ins_abc op_add ri ri one); + let back = here f in + put f (ins_asbx op_jmp 0 0); + patch_jump p f ~file:f.f_file ~pos:s.s_pos back top; + let exit_pc = here f in + patch_jump p f ~file:f.f_file ~pos:s.s_pos jz exit_pc; + List.iter (fun pc -> patch_jump p f ~file:f.f_file ~pos:s.s_pos pc exit_pc) lf.lf_breaks; + if f.f_div then begin + f.f_owned <- entry_owned; + f.f_gc <- entry_gc + end + else mask_meet f entry_owned entry_gc; + f.f_div <- div0; + f.f_nlocals <- saved_locals; + f.f_env <- saved_env; + f.f_declared <- saved_decls; + f.f_temp <- saved_locals | Some t when (match unwrap t with Multi _ -> true | _ -> false) -> let elem = match unwrap t with Multi e -> Scalar e | other -> other in let saved_locals = f.f_nlocals in diff --git a/compiler/src/owner.ml b/compiler/src/owner.ml index aaf80a6..df58b53 100644 --- a/compiler/src/owner.ml +++ b/compiler/src/owner.ml @@ -1505,11 +1505,23 @@ and analyze_stmt (ctx : ctx) (s : Ast.stmt) : unit = read_expr ctx cond; analyze_block ctx ~node:s.s_id ~pos:s.s_pos ~label:"WHILE" body); ctx.loop_stack <- List.tl ctx.loop_stack - | For { var; iter; body } -> + | For { var; var2; iter; body } -> read_expr ctx iter; let src = place_of iter in - let item_ty = - match expr_ty ctx iter with Some t -> ( match elem_ty t with Some e -> e | None -> t) | None -> Scalar "Int" + let iter_ty = expr_ty ctx iter in + (* `for k, v in m` binds the map's key and value types; the one-name + form binds the element type (elem_ty). Both cursors are BORROWS of + what the container owns, so neither is ever dropped by the body. *) + let key_ty, item_ty = + match (iter_ty, var2) with + | Some (Map (k, v)), Some _ -> (Some (Scalar k), Scalar v) + | Some (Nullable (Map (k, v))), Some _ -> (Some (Scalar k), Scalar v) + | Some t, _ -> (None, ( match elem_ty t with Some e -> e | None -> t)) + | None, _ -> (None, Scalar "Int") + in + let cursor (n : string) (t : Ast.field_ty) : local = + { l_name = n; l_ty = t; l_class = oclass_of ctx t; l_node = s.s_id; l_pos = s.s_pos; + l_holds = false; l_src = src; l_bkind = AShared; l_state = Borrowed s.s_pos } in ctx.loop_stack <- s.s_id :: ctx.loop_stack; fixpoint ctx @@ -1517,10 +1529,11 @@ and analyze_stmt (ctx : ctx) (s : Ast.stmt) : unit = push_scope ctx ~node:s.s_id ~pos:s.s_pos ~label:"FOR"; (* the cursor borrows an element of the iterable for the whole body: moving the container out from under it is E302 *) - declare ctx - { l_name = var; l_ty = item_ty; l_class = oclass_of ctx item_ty; l_node = s.s_id; - l_pos = s.s_pos; l_holds = false; l_src = src; l_bkind = AShared; - l_state = Borrowed s.s_pos }; + (match (key_ty, var2) with + | Some kt, Some v2 -> + declare ctx (cursor var kt); + declare ctx (cursor v2 item_ty) + | _ -> declare ctx (cursor var item_ty)); List.iter (analyze_stmt ctx) body; pop_scope ctx); ctx.loop_stack <- List.tl ctx.loop_stack diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index 25ea599..1368220 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -1202,10 +1202,14 @@ and parse_for_stmt (st : state) : Ast.stmt = ignore (advance st); (* 'for' *) let var = expect_ident st "loop variable name" in + (* `for k, v in m` — the map form (see Ast.For.var2) *) + let var2 = + if accept st Token.Comma then Some (expect_ident st "second loop variable name") else None + in expect st Token.KwIn "`in`"; let iter = parse_expr_no_brace st in let body = parse_block st in - { Ast.s_id = id; s_pos = pos; s_kind = Ast.For { var; iter; body } } + { Ast.s_id = id; s_pos = pos; s_kind = Ast.For { var; var2; iter; body } } and parse_return_stmt (st : state) : Ast.stmt = let pos = peek_pos st in @@ -1820,9 +1824,13 @@ and subst_stmt (consts : Ast.expr StringMap.t) (bound : StringSet.t) (s : Ast.st } | Ast.While { cond; body } -> { s with Ast.s_kind = Ast.While { cond = e cond; body = subst_block consts bound body } } - | Ast.For { var; iter; body } -> - let bound' = StringSet.add var bound in - { s with Ast.s_kind = Ast.For { var; iter = e iter; body = subst_block consts bound' body } } + | Ast.For { var; var2; iter; body } -> + let bound' = + match var2 with + | Some v2 -> StringSet.add v2 (StringSet.add var bound) + | None -> StringSet.add var bound + in + { s with Ast.s_kind = Ast.For { var; var2; iter = e iter; body = subst_block consts bound' body } } | Ast.Return opt -> { s with Ast.s_kind = Ast.Return (Option.map e opt) } | Ast.ExprStmt ex -> { s with Ast.s_kind = Ast.ExprStmt (e ex) } | Ast.Break | Ast.Continue -> s diff --git a/compiler/src/types.ml b/compiler/src/types.ml index f665ff2..69e52ca 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -1686,14 +1686,22 @@ let typecheck_program ~file ~(module_of : string -> string) | While { cond; body } -> let _ = typecheck_expr env cenv cond in List.fold_left typecheck_stmt (env, cenv) body - | For { var; iter; body } -> + | For { var; var2; iter; body } -> let iter_res = typecheck_expr env cenv iter in - let env_body = StringMap.add var iter_res.typ env in - let cenv_body = - match confident_typ cenv iter with - | Some (TMulti inner_t) -> StringMap.add var inner_t cenv - | _ -> StringMap.remove var cenv + (* `for k, v in m`: the names take the map's key and value types. + The one-name form over a `multi` keeps the element type. *) + let bind (env0 : typ StringMap.t) (t : typ option) : typ StringMap.t = + match (t, var2) with + | Some (TMap (kt, vt)), Some v2 -> StringMap.add v2 vt (StringMap.add var kt env0) + | Some (TMulti it), None -> StringMap.add var it env0 + | Some other, None -> StringMap.add var other env0 + | _ -> ( + match var2 with + | Some v2 -> StringMap.remove v2 (StringMap.remove var env0) + | None -> StringMap.remove var env0) in + let env_body = bind env (Some iter_res.typ) in + let cenv_body = bind cenv (confident_typ cenv iter) in List.fold_left typecheck_stmt (env_body, cenv_body) body | Return opt_e -> (match opt_e with Some e -> let _ = typecheck_expr env cenv e in () | None -> ()); @@ -1878,9 +1886,14 @@ and walk_stmt (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (s : s walk_expr bound visit cond; walk_block bound visit body; bound - | For { var; iter; body } -> + | For { var; var2; iter; body } -> walk_expr bound visit iter; - walk_block (StringSet.add var bound) visit body; + let bound' = + match var2 with + | Some v2 -> StringSet.add v2 (StringSet.add var bound) + | None -> StringSet.add var bound + in + walk_block bound' visit body; bound | Return (Some e) -> walk_expr bound visit e; diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index a184216..45f8f9d 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -582,6 +582,20 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { R[A] = C == WO_B_MAP_KEY_AT ? m->keys[i] : m->vals[i]; return 0; } + case WO_B_MULTI_SET: { /* `m[i] = v`: the replaced element was the + * container's, so it dies here */ + wo_multi *m = native_check(R[B], WO_CLS_MULTI, msg); + if (!m) return WO_T_BOUNDS; + uint64_t i = R[B + 1]; + if (i >= m->len) { + *msg = "multi index out of range"; + return WO_T_BOUNDS; + } + if (m->items[i] != R[B + 2]) wo_drop_kind(rt, m->elem_kind, m->items[i]); + m->items[i] = R[B + 2]; + R[A] = 0; + return 0; + } default: /* unreachable: loader validated the id */ *msg = "unknown builtin"; return WO_T_EXPLICIT; diff --git a/runtime/src/loader.c b/runtime/src/loader.c index c705b5f..ef9a1b7 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -50,7 +50,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = { [WO_B_SPLIT] = 2, [WO_B_SPLIT_WS] = 1, [WO_B_JOIN] = 2, [WO_B_SLICE] = 3, [WO_B_POP] = 1, [WO_B_SHIFT] = 1, [WO_B_SORT] = 1, [WO_B_REVERSE] = 1, [WO_B_MAP_REMOVE] = 2, - [WO_B_MAP_KEY_AT] = 2, [WO_B_MAP_VAL_AT] = 2, + [WO_B_MAP_KEY_AT] = 2, [WO_B_MAP_VAL_AT] = 2, [WO_B_MULTI_SET] = 3, }; static int vtab_cmp(const void *a, const void *b) { diff --git a/runtime/src/wob.h b/runtime/src/wob.h index e804148..461423f 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -205,8 +205,11 @@ enum { WO_B_MAP_REMOVE = 36, /* (map, key) -> 1/0; drops the removed key and value */ WO_B_MAP_KEY_AT = 37, /* (map, i) -> key at slot i (insertion order) */ WO_B_MAP_VAL_AT = 38, /* (map, i) -> value at slot i */ + WO_B_MULTI_SET = 39, /* (multi, i, v) -> 0; in-place element write, + * dropping the element it replaces. `m[i] = v` + * for a multi, the mirror of map_set. */ }; -#define WO_B_MAX 38u +#define WO_B_MAX 39u /* ---- instruction encode/decode: op:8 A:8 then B:8 C:8 or Bx:16 ---- */ static inline uint32_t wo_ins_abc(uint8_t op, uint8_t a, uint8_t b, uint8_t c) {