From 605eb97e757291c11b29ce67712e5546963988d0 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 26 Sep 2026 05:52:56 +0700
Subject: [PATCH] A dyn value's .field and [:key] read as get and assign as
put.
---
TODO.org | 8 +--
lib/check.ml | 73 +++++++++++++++++++++------
lib/emit.ml | 1 +
runtime/flan_dyn.c | 28 ++++++++--
runtime/flan_dyn.h | 3 ++
spec-syntax.md | 4 ++
test/programs/dyn-field-trap.flan | 18 +++++++
test/programs/dyn-field-trap.fln | 23 +++++++++
test/programs/dyn-fields.flan | 36 +++++++++++++
test/syntax/handwritten/dynfields.fln | 33 ++++++++++++
test/syntax/handwritten/dynfields.out | 5 ++
test/test_acceptance.ml | 57 +++++++++++++++++++++
test/test_dev.ml | 16 ++++++
test/test_dyn.ml | 2 +-
web/index.html | 12 +++--
15 files changed, 291 insertions(+), 28 deletions(-)
create mode 100644 test/programs/dyn-field-trap.flan
create mode 100644 test/programs/dyn-field-trap.fln
create mode 100644 test/programs/dyn-fields.flan
create mode 100644 test/syntax/handwritten/dynfields.fln
create mode 100644 test/syntax/handwritten/dynfields.out
diff --git a/TODO.org b/TODO.org
index cc934a8c..3552de2f 100644
--- a/TODO.org
+++ b/TODO.org
@@ -726,10 +726,10 @@ of !=.
* Checker
-** NEXT A dyn value takes .field and [:key]
-Decided 2026-09-26: on a dyn value, ~x.name~ / ~(.name x)~ reads ~(get x :name)~ and
-assigning it is ~(put x :name v)~ — a class slot or a map key; ~m[:k]~ indexes a dyn
-map as ~(get m :k)~, and assigning it puts.
+** DONE A dyn value takes .field and [:key]
+CLOSED: [2026-09-26]
+Assigning ~x.name~ or ~m[:k]~ is ~put~, not the stricter ~(set (get x :k) v)~: it adds
+a key a plain map or a class lacks. ~m[k]~ on a dyn map takes any key, as ~get~ does.
** DONE A slice from a C pointer, and a pointer cast
CLOSED: [2026-09-25]
diff --git a/lib/check.ml b/lib/check.ml
index a23f2e83..9508adf3 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -5945,12 +5945,36 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr =
let v = check ctx ~want:Types.Dyn v in
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_slot_set" [ target; k; v; here loc ])
+ (* (set (.name x) v) on a dyn is (put x :name v): a class slot's declared
+ type is checked by put, and a map takes a key it did not hold. Not
+ [flan_dyn_slot_set], which refuses a plain map — .name reads either, so
+ assigning it writes either. The target is checked once, here. *)
+ | Ast.Set (Ast.Pfield (target, name), v) ->
+ let t = check_target ctx target in
+ if t.Tast.ty = Types.Dyn then begin
+ refuse_const_change ctx loc t;
+ let k = dyn_kw ctx loc name in
+ let v = check ctx ~want:Types.Dyn v in
+ expect ctx loc ~want
+ (rt loc Types.Unit "flan_dyn_map_put" [ t; k; v; here loc ])
+ end else begin
+ let p, pty = field_place ~store:true ctx loc target t name in
+ let v = check ctx ~want:pty v in
+ expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
+ end
| Ast.Set (p, v) ->
let p, pty = check_place ctx loc p in
let v = check ctx ~want:pty v in
expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
+ (* On a dyn, (.name x) is (get x :name) — the same call, so a missing key is
+ nil and a value that is not a map traps with get's own sentence. *)
| Ast.Field (target, name) ->
- let target, sname = struct_target ctx target in
+ let t = check_target ctx target in
+ if t.Tast.ty = Types.Dyn then
+ expect ctx loc ~want
+ (rt loc Types.Dyn "flan_dyn_get" [ t; dyn_kw ctx loc name; here loc ])
+ else
+ let target, sname = struct_of ctx target t in
let s = Option.get (fields_named ctx.env sname) in
(match Tast.field_index s name with
| None ->
@@ -9567,9 +9591,9 @@ and fields_named env n : Tast.structure option =
(* The target of [.field] is a struct or an untagged union, or one level of
pointer to one. The auto-deref is inserted here as a real node, so no
- backend re-derives it. *)
-and struct_target ctx (target : Ast.expr) : Tast.expr * string =
- let t = check_target ctx target in
+ backend re-derives it. The target comes checked, because every caller
+ looks first for a dyn, whose [.name] is a map entry and not a field. *)
+and struct_of ctx (target : Ast.expr) (t : Tast.expr) : Tast.expr * string =
let has n = fields_named ctx.env n <> None in
match t.Tast.ty with
| Types.Named n when has n -> t, n
@@ -9704,15 +9728,19 @@ and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t =
| Some (ty, false) -> Tast.Pglobal name, ty
| None -> unknown_name ~setting:true ctx loc name)
| Ast.Pfield (target, name) ->
- let target, sname = struct_target ctx target in
- let s = Option.get (fields_named ctx.env sname) in
- (match Tast.field_index s name with
- | None ->
- Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname)
- "%s has no field %s" (tyname loc (Types.Named sname)) name
- | Some i ->
- if store then Option.iter (refuse_const_place ctx.env loc) (const_reached target);
- Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty)
+ let t = check_target ctx target in
+ if t.Tast.ty = Types.Dyn then begin
+ let x = match target.Ast.e with Ast.Var x -> x | _ -> "x" in
+ if Source.indented_at loc then
+ fail loc
+ "%s.%s is an entry of a dyn map, and has no address. Read it into \
+ a local: let v = %s.%s" x name x name
+ else
+ fail loc
+ "(.%s %s) is an entry of a dyn map, and has no address. Read it \
+ into a local: (let [v (.%s %s)] ...)" name x name x
+ end;
+ field_place ~store ctx loc target t name
| Ast.Pindex (target, idx) ->
let target = check_target ctx target in
(match target.Tast.ty with
@@ -9742,6 +9770,21 @@ and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t =
"a class slot (get inst :slot) is written with set and has no address. \
Read it into a local with let"
+(* The dyn keyword [:name], for a dyn's [.name]. *)
+and dyn_kw ctx loc name = check ctx ~want:Types.Dyn { Ast.e = Ast.Kw name; loc }
+
+(* A struct field as a place, over a target already checked. *)
+and field_place ~store ctx loc target t name =
+ let target, sname = struct_of ctx target t in
+ let s = Option.get (fields_named ctx.env sname) in
+ match Tast.field_index s name with
+ | None ->
+ Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname)
+ "%s has no field %s" (tyname loc (Types.Named sname)) name
+ | Some i ->
+ if store then Option.iter (refuse_const_place ctx.env loc) (const_reached target);
+ Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty
+
(* An index or a slice bound that is a literal is known now, so it is an error
now rather than a trap later. Only literals: a [defconst] is a global in the
typed IR, not a folded constant, so [(at arr size)] still traps at runtime —
@@ -11840,8 +11883,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
stored under the key. *)
if target.Tast.ty = Types.Dyn then
expect ctx loc ~want
- (rt loc Types.Dyn "flan_dyn_map_get"
- [ target; check ctx ~want:Types.Dyn k ])
+ (rt loc Types.Dyn "flan_dyn_get"
+ [ target; check ctx ~want:Types.Dyn k; here loc ])
else begin
let kt, vt = map_kv loc "get" target.Tast.ty in
let k = check ctx ~want:kt k in
diff --git a/lib/emit.ml b/lib/emit.ml
index ed5eb35c..2df56188 100644
--- a/lib/emit.ml
+++ b/lib/emit.ml
@@ -5007,6 +5007,7 @@ declare void @flan_dyn_class_def(i64, ptr, i64)
declare void @flan_dyn_class_hook(ptr)
declare i64 @flan_dyn_kw(ptr, i64)
declare i64 @flan_dyn_map_get(i64, i64)
+declare i64 @flan_dyn_get(i64, i64, ptr, i64)
declare void @flan_dyn_map_set(i64, i64, i64)
declare i64 @flan_dyn_map_contains(i64, i64)
; The ones that trap carry the site as ptr+len, the way the bounds and
diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c
index 2c9e048f..75cba6d5 100644
--- a/runtime/flan_dyn.c
+++ b/runtime/flan_dyn.c
@@ -1963,6 +1963,10 @@ flan_dyn flan_dyn_vec_new(void);
flan_dyn flan_dyn_map_new(void);
void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen);
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v);
+flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc,
+ int64_t loclen);
+void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
+ int64_t loclen);
/* The user hook, run on an instance the name-matching has just brought up to
* date. [inst], [added] and [gone] are rooted by the caller.
@@ -3037,8 +3041,12 @@ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i, const uint8_t *loc,
int64_t loclen) {
int64_t k;
flan_obj *o;
+ /* m[:k] on a map is (get m :k), whatever the key: get's rule, nil when
+ * absent. */
+ if (is_map(v)) return flan_dyn_get(v, i, loc, loclen);
if (!is_text(v) && !is_vec(v))
- trap2(loc, loclen, TYPE_TRAP, "at", "only a text or a vec is indexed", v, i);
+ trap2(loc, loclen, TYPE_TRAP, "at", "only a text, a vec or a map is indexed",
+ v, i);
k = need_index(loc, loclen, "at", v, i);
o = dyn_obj(v);
if (o->kind == OBJ_VIEW) {
@@ -3086,8 +3094,14 @@ void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x, const uint8_t *loc,
if (is_text(v))
trap2(loc, loclen, TYPE_TRAP, "set-at",
"a text is immutable — build another one", v, i);
+ /* Assigning m[:k] is (put m :k x), a class slot's type check with it. */
+ if (is_map(v)) {
+ flan_dyn_map_put(v, i, x, loc, loclen);
+ return;
+ }
if (!is_vec(v))
- trap2(loc, loclen, TYPE_TRAP, "set-at", "only a vec is assigned into", v, i);
+ trap2(loc, loclen, TYPE_TRAP, "set-at",
+ "only a vec or a map is assigned into", v, i);
k = need_index(loc, loclen, "set-at", v, i);
o = dyn_obj(v);
if (o->kind == OBJ_VIEW) {
@@ -3191,6 +3205,14 @@ flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k) {
return i < 0 ? flan_dyn_nil() : o->u.v.items[i * 2 + 1];
}
+/* A program's (get m k) and (.k m): the same, with the site a value that is
+ * not a map is refused at. */
+flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc,
+ int64_t loclen) {
+ if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "get", "only a map answers it", m, k);
+ return flan_dyn_map_get(m, k);
+}
+
flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k) {
flan_obj *o = want_map("has-key?", m, k);
return flan_dyn_from_bool(map_find(o, k) >= 0);
@@ -3333,7 +3355,7 @@ void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
int64_t loclen) {
flan_obj *o;
class_entry *e;
- if (!is_map(m)) trap2(NULL, 0, TYPE_TRAP, "put", "only a map answers it", m, k);
+ if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "put", "only a map answers it", m, k);
o = dyn_obj(m);
e = class_sync(o);
/* A map with no class, and a class with no typed slot, stop at the test. */
diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h
index 39d6173e..7e8538a6 100644
--- a/runtime/flan_dyn.h
+++ b/runtime/flan_dyn.h
@@ -244,6 +244,9 @@ void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen);
* to ask when nil might also be stored. [set] replaces the value of an equal
* key in place, so a key occurs once and insertion order is print order. */
flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k);
+/* [get]'s, and a dyn's [.field]: [flan_dyn_map_get] with a site. */
+flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc,
+ int64_t loclen);
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v);
/* [put]'s: [flan_dyn_map_set], with the site a typed class slot's refusal
* prints. */
diff --git a/spec-syntax.md b/spec-syntax.md
index 0416c89a..ca66d31e 100644
--- a/spec-syntax.md
+++ b/spec-syntax.md
@@ -172,6 +172,10 @@ Each item: the proposal, then the reason in one line.
one symbol. `test/programs/dev-rerun.flan:65` names a global
`.init-once.counter`; rename it. **Built**, without the rename: it prints and
reads back through the fallback, `defonce(.init-once.counter, i64, 7)`.
+- **On a dyn value, `x.name` is `(get x :name)` and `x.name = v` is
+ `(put x :name v)`**, for a class slot and a plain map's key alike; `m[:k]`
+ is `(get m :k)` and `m[:k] = v` puts. The paren spellings `(.name x)` and
+ `(at m :k)` mean the same. **Built.**
- **`and`, `or`, `not` are words**, since they are Flan's own names. **Built.**
- **Casts and type-taking builtins are calls:** `i32(x)`, `vec-new(u8)`,
`max-value(u8)`, `the([3 f32], [1 2 3.5])`. A pointer cast is the type
diff --git a/test/programs/dyn-field-trap.flan b/test/programs/dyn-field-trap.flan
new file mode 100644
index 00000000..499f3bf3
--- /dev/null
+++ b/test/programs/dyn-field-trap.flan
@@ -0,0 +1,18 @@
+;;;; A dyn's .field and [:key] trap with get's and put's own sentences, one
+;;;; per run. The argument chooses which; the test asserts the line numbers.
+(defclass State [paused bool step bool])
+
+(defn as-dyn [d dyn] dyn d)
+
+(defn main [args [string]] i32
+ (let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0)
+ s (State false false)
+ n (as-dyn 3)]
+ (println "before")
+ (cond
+ (= which 0) (println (.paused n))
+ (= which 1) (set (.paused s) 1)
+ (= which 2) (set (.paused n) true)
+ (= which 3) (set (at s :step) 2)
+ :else (println (at n :paused))))
+ 0)
diff --git a/test/programs/dyn-field-trap.fln b/test/programs/dyn-field-trap.fln
new file mode 100644
index 00000000..dd9c64ea
--- /dev/null
+++ b/test/programs/dyn-field-trap.fln
@@ -0,0 +1,23 @@
+;;;; A dyn's .field and [:key] trap with get's and put's own sentences, one
+;;;; per run. The argument chooses which; the test asserts the line numbers.
+defclass(State, [paused bool step bool])
+
+fn as-dyn(d) -> dyn = d
+
+fn main(args: [string]) -> i32
+ let which =
+ if length(args) > 1 then i32(bytes->i64(bytes-view(args[1]))) else 0
+ let s = State(false, false)
+ let n = as-dyn(3)
+ println("before")
+ if which == 0
+ println(n.paused)
+ elif which == 1
+ s.paused = 1
+ elif which == 2
+ n.paused = true
+ elif which == 3
+ s[:step] = 2
+ else
+ println(n[:paused])
+ 0
diff --git a/test/programs/dyn-fields.flan b/test/programs/dyn-fields.flan
new file mode 100644
index 00000000..e1c83ad5
--- /dev/null
+++ b/test/programs/dyn-fields.flan
@@ -0,0 +1,36 @@
+;;;; A dyn value's (.field x) and (at m :key): a read is get, a set is put.
+(defclass State [paused bool step bool])
+(defclass Pos [x y])
+(defclass Body [pos count items])
+
+(defonce state (State false false))
+
+(defn game-input [] ()
+ (when true
+ (set (.paused state) (not (.paused state)))))
+
+(defn pick [b dyn] dyn b)
+
+(defn main [] i32
+ (game-input)
+ (println (.paused state))
+ (game-input)
+ (println (.paused state))
+ (let [b (Body (Pos 1 2) 0 [10 20 30])]
+ (set (.count b) (+ (.count b) 1))
+ (++ (.count b))
+ (update (.count (pick b)) + 10)
+ (set (.x (.pos b)) 7)
+ (update (.y (.pos b)) * 10)
+ (set (at (.items b) 0) 11)
+ (update (at (.items b) 1) + 5)
+ (println (.count b) (.x (.pos b)) (.y (.pos b)) (at (.items b) 0) (at (.items b) 1))
+ (println (.missing b) (= (.missing b) (get b :missing))))
+ (let [m {:hp 3}]
+ (set (.hp m) (- (.hp m) 1))
+ (set (at m :mp) 9)
+ (update (at m :mp) + 1)
+ (set (.name m) "slime")
+ (set (at m 1) :one)
+ (println (at m :hp) (.mp m) (at m :gone) (at m 1) m))
+ 0)
diff --git a/test/syntax/handwritten/dynfields.fln b/test/syntax/handwritten/dynfields.fln
new file mode 100644
index 00000000..49d2956d
--- /dev/null
+++ b/test/syntax/handwritten/dynfields.fln
@@ -0,0 +1,33 @@
+;; A dyn value's .field and [:key]: a read is get, an assignment is put.
+
+defclass(State, [paused bool step bool])
+defclass(Pos, [x y])
+defclass(Body, [pos count items])
+
+once state = State(false, false)
+
+fn game-input() -> ()
+ if true
+ state.paused = not(state.paused)
+
+fn main() -> i32
+ game-input()
+ println(state.paused)
+ game-input()
+ println(state.paused)
+ let b = Body(Pos(1, 2), 0, [10 20 30])
+ b.count += 1
+ b.count += 1
+ b.pos.x = 7
+ b.pos.y *= 10
+ b.items[0] = 11
+ b.items[1] += 5
+ println(b.count, b.pos.x, b.pos.y, b.items[0], b.items[1])
+ println(b.missing)
+ let m = {:hp 3}
+ m.hp -= 1
+ m[:mp] = 9
+ m[:mp] += 1
+ m.name = "slime"
+ println(m[:hp], m.mp, m[:gone], m)
+ 0
diff --git a/test/syntax/handwritten/dynfields.out b/test/syntax/handwritten/dynfields.out
new file mode 100644
index 00000000..780925d7
--- /dev/null
+++ b/test/syntax/handwritten/dynfields.out
@@ -0,0 +1,5 @@
+true
+false
+2 7 20 11 25
+nil
+2 10 nil {:hp 2 :mp 10 :name "slime"}
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index f12aca6d..eec23df7 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -5701,6 +5701,63 @@ level "1"
slot_trap ();
slot_trap ~x86:true ();
+ (* A dyn's (.field x) and (at m :key) are get and put: a class slot, a
+ plain map's key, nil for one it lacks, and chained and compound forms
+ over both. The .fln spelling is test/syntax/handwritten/dynfields.fln. *)
+ let fields_out =
+ "true\nfalse\n12 7 20 11 25\nnil true\n\
+ 2 10 nil :one {:hp 2 :mp 10 :name \"slime\" 1 :one}\n"
+ in
+ outputs "dyn: .field and [:key]" "programs/dyn-fields.flan" fields_out;
+ outputs ~opt:"-O0" "dyn: .field and [:key], -O0" "programs/dyn-fields.flan"
+ fields_out;
+ outputs ~x86:true "dyn: .field and [:key], --x86" "programs/dyn-fields.flan"
+ fields_out;
+ outputs ~dev:true "dyn: .field and [:key], --dev" "programs/dyn-fields.flan"
+ fields_out;
+ (* Their refusals are get's, put's and at's own sentences, placed at the
+ access, in both syntaxes. *)
+ let field_trap ?x86 path rows =
+ let exe = compile ?x86 path in
+ List.iter
+ (fun (arg, want) ->
+ let code, text = run exe (Some arg) in
+ if code <> 134 || not (contains text want) then begin
+ incr failures;
+ Printf.printf
+ "FAIL dyn: a .field's refusal%s\n got: %S (exit %d)\n \
+ wanted: %S (exit 134)\n"
+ (match x86 with Some true -> ", --x86" | _ -> "")
+ text code want
+ end)
+ rows;
+ (try Sys.remove exe with Sys_error _ -> ())
+ in
+ let field_rows (l0, l1, l2, l3, l4) =
+ [ ("0", l0 ^ ": dyn get: int and keyword, and only a map answers it — \
+ (get 3 :paused)");
+ ("1", l1 ^ ": dyn put: the slot :paused of State is declared bool, \
+ and this is int — (put #State{:paused false :step false} \
+ :paused 1)");
+ ("2", l2 ^ ": dyn put: int and keyword, and only a map answers it — \
+ (put 3 :paused)");
+ ("3", l3 ^ ": dyn put: the slot :step of State is declared bool, and \
+ this is int");
+ ("4", l4 ^ ": dyn at: int and keyword, and only a text, a vec or a \
+ map is indexed — (at 3 :paused)") ]
+ in
+ let flan_rows =
+ field_rows ("dyn-field-trap.flan:13:28", "dyn-field-trap.flan:14:19",
+ "dyn-field-trap.flan:15:19", "dyn-field-trap.flan:16:19",
+ "dyn-field-trap.flan:17:22")
+ in
+ field_trap "programs/dyn-field-trap.flan" flan_rows;
+ field_trap ~x86:true "programs/dyn-field-trap.flan" flan_rows;
+ field_trap "programs/dyn-field-trap.fln"
+ (field_rows ("dyn-field-trap.fln:14:13", "dyn-field-trap.fln:16:5",
+ "dyn-field-trap.fln:18:5", "dyn-field-trap.fln:20:5",
+ "dyn-field-trap.fln:22:13"));
+
(* A numeric cast opening a dyn box — TODO.org, "A numeric cast opens a
dyn box". programs/dyn-cast.flan is one program because the three
behaviours are one story told in order: the same-kind casts print, the
diff --git a/test/test_dev.ml b/test/test_dev.ml
index e7dd4f1b..8ad6cb22 100644
--- a/test/test_dev.ml
+++ b/test/test_dev.ml
@@ -7418,6 +7418,22 @@ let () =
if read () <> "\"kept\"" then
fail "--%s: the global was not readable before any thunk ran: %S"
backend (read ());
+ (* A dyn's .field and [:key] are get and put in a thunk too. *)
+ List.iter
+ (fun (code, want) ->
+ let r =
+ request c
+ (Printf.sprintf
+ "(:op \"eval-expr\" :code %s \
+ :file \"programs/dev-dyn-global.flan\")"
+ (Wire.quote code))
+ in
+ if answer r <> want then
+ fail "--%s: %s answered %S (%s: %s), not %S" backend code
+ (answer r) (status r) (said r) want)
+ [ ("(.s config)", "\"kept\"");
+ ("(do (set (.n config) 5) (++ (.n config)) (.n config))", "6");
+ ("(do (set (at config :n) 1) (at config :n))", "1") ];
for cycle = 1 to 3 do
let r = churn () in
if status r <> "ok" then
diff --git a/test/test_dyn.ml b/test/test_dyn.ml
index 96875fce..7ab4675d 100644
--- a/test/test_dyn.ml
+++ b/test/test_dyn.ml
@@ -230,7 +230,7 @@ let () =
("atrange", "index 9 is out of bounds for text of length 2");
("atnegative", "index -1 is out of bounds");
("setattext", "a text is immutable");
- ("setatnotvec", "only a vec is assigned into");
+ ("setatnotvec", "only a vec or a map is assigned into");
("setatrange", "index 0 is out of bounds for vec of length 0");
("push", "only a vec is pushed to");
("needi64", "dyn i64: text");
diff --git a/web/index.html b/web/index.html
index 817f687b..4314a6c0 100644
--- a/web/index.html
+++ b/web/index.html
@@ -700,7 +700,9 @@ program was edited elsewhere would not be worth having.
{:a 1 :b "two"} is a dyn map and [1 2 3] is a dyn vector.
get, put, has-key?, at and
length read and write them, the same names the typed
-Map and Vec answer to. A keyword is a value here rather
+Map and Vec answer to. (.hp m) is
+(get m :hp) and (at m :hp) is too; set on
+either is put. A keyword is a value here rather
than only a way to name an enum member: keywords are interned, so comparing two is
comparing two pointers.
@@ -724,10 +726,10 @@ for anything that is not an instance. type-of answers any value's
kind as a keyword — :nil, :bool, :int,
:float, :text, :vec, :map or
:keyword — and an instance's class name, so a class cannot be named
-after one of those kinds. The slots are map keys: get
-reads one, and set writes one, as in
-(set (get s :pause) true). put writes one too, and is
-also how a key the class does not declare is added.
+after one of those kinds. The slots are map keys: (.pause s)
+reads one, and (set (.pause s) true) writes one, checking its
+type. get and put do the same, and put
+is also how a key the class does not declare is added.
Dispatch comes in the two styles and they are one mechanism.
defgeneric dispatches on the class of the first argument, which is