feat: map iteration (for k, v in m) + multi element writes

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) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-14 16:46:53 +02:00
parent e5c5952cdc
commit ea77fc9d5c
9 changed files with 164 additions and 29 deletions

View file

@ -318,6 +318,11 @@ and stmt_kind =
}
| For of {
var : string;
(* `for k, v in m` over a `map<K, V>`: 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;
}

View file

@ -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) ]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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) {