diff --git a/TODO.org b/TODO.org index 0cc26b67..920de488 100644 --- a/TODO.org +++ b/TODO.org @@ -1724,10 +1724,11 @@ with the selected frame's locals. ** DONE Hex, binary and an address on a primitive in the inspector CLOSED: [2026-09-25] Hex and binary were already drawn under every integer. The slot root's reply -now carries =:addr=, the address of the place it read (a field or element down -a path included), and the inspector shows it as =at 0x…=. An expression root has -no address, because its value is not stored anywhere. A stack address is not -offered to =flan-inspect-address=, since the registry does not follow one. +now carries =:addr=, the address of the place it read, a field, element or +option payload down a path included, and the inspector shows it as =at 0x…=. A +data case's field and an expression root's value are not places and carry none, +rather than the address of a copy. A stack address is not offered to +=flan-inspect-address=, since the registry does not follow one. ** DONE A defclass is not on the definitions list as a type, and a sum's cases are not drawn CLOSED: [2026-09-25] diff --git a/emacs/flan-cnr.el b/emacs/flan-cnr.el index c132f264..1d83ac5d 100644 --- a/emacs/flan-cnr.el +++ b/emacs/flan-cnr.el @@ -778,6 +778,7 @@ drawn from." (get-text-property pos 'flan-cnr-abort) (get-text-property pos 'flan-cnr-frame) (get-text-property pos 'flan-cnr-hidden) + (get-text-property pos 'flan-cnr-loc) (get-text-property pos 'flan-cnr-inspect))) (defun flan-cnr-tab () diff --git a/lib/session.ml b/lib/session.ml index e7522621..ed0d7860 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -1681,28 +1681,33 @@ let render_slot ?(origin = "") t ~frame ~(fn : Tast.fn) ~slot ~path (match Render.render c 0 v with | exception Loc.Error { Loc.dmsg = why; _ } -> Error (name ^ path_text path ^ ": " ^ why) | parts -> - (* Where the value lives, first and on a line of its own: [v] is - a place in the stopped frame, so its address is the storage - the listing is reading. The caller splits it off at the first - newline; a rendering has none, because [Render] quotes a + (* Where the value lives, first and on a line of its own, when [v] + is a place in the stopped frame: its address is then the + storage the listing is reading. A data case's field is not a + place — [AddrOf] on it would answer the address of a copy — so + it has no address line. The caller splits the line off at the + first newline; a rendering has none, because [Render] quotes a string's. *) - let addr = - { Tast.e = - Tast.Prim - (Tast.Cast (Types.Int Types.I64), - [ { Tast.e = Tast.Prim (Tast.AddrOf, [ v ]); - ty = Types.Ptr v.Tast.ty; loc } ]); - ty = Types.Int Types.I64; loc } - in - let newline = - { Tast.e = - Tast.Prim - (Tast.Bytes, [ { Tast.e = Tast.Str "\n"; ty = Types.String; loc } ]); - ty = Types.Slice (Types.Int Types.U8); loc } - in let parts = - dev_emitter.Render.ei64 addr :: dev_emitter.Render.ebytes newline - :: parts + if not (Emit.addr_is_place v) then parts + else + let addr = + { Tast.e = + Tast.Prim + (Tast.Cast (Types.Int Types.I64), + [ { Tast.e = Tast.Prim (Tast.AddrOf, [ v ]); + ty = Types.Ptr v.Tast.ty; loc } ]); + ty = Types.Int Types.I64; loc } + in + let newline = + { Tast.e = + Tast.Prim + (Tast.Bytes, + [ { Tast.e = Tast.Str "\n"; ty = Types.String; loc } ]); + ty = Types.Slice (Types.Int Types.U8); loc } + in + dev_emitter.Render.ei64 addr :: dev_emitter.Render.ebytes newline + :: parts in let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in t.thunks <- t.thunks + 1; diff --git a/test/programs/dev-watch.flan b/test/programs/dev-watch.flan index d2caf46f..5c65e201 100644 --- a/test/programs/dev-watch.flan +++ b/test/programs/dev-watch.flan @@ -41,6 +41,10 @@ (watch "pos" (Pos {.x 3 .y 1.5})) (watch "row" [1 2 3]) (watch "t2" (* ticks 2)) + ;; A dyn value renders through the dyn printer into the slot, and a string + ;; through the form is quoted. + (watch "d" {:a 1}) + (watch "s" "x") ;; A hot inner loop, and the value *varies* across it — which is what makes ;; the row a test of the accumulator rather than of the plumbing. A slot that ;; only kept [last] would report 21 and no range; n, min and max are each diff --git a/test/test_dev.ml b/test/test_dev.ml index 3136112d..ce3b4a55 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -1368,7 +1368,9 @@ let () = (* The site, on LLVM: an arith trap publishes its loc around the hook exactly as a bounds trap does. *) (match Wire.string_field (ask "(:op \"break\")") "site" with - | Some site when contains_sub site "dev-break.flan:" -> () + (* Absolute, because the break buffer opens the file it names and + an editor is not in this process's working directory. *) + | Some site when contains_sub site "dev-break.flan:" && site.[0] = '/' -> () | Some site -> fail "the arith site points at %s" site | None -> fail "a division by zero carries no :site"); let r = ask "(:op \"restart\" :name \"use-zero\")" in @@ -2385,6 +2387,29 @@ let () = if Int64.sub y x <> 4L then fail "mark.y is %Ld bytes past mark.x, not 4" (Int64.sub y x) | _ -> fail "inspect on a slot answered with no :addr")); + (let addr name path = + match slot_of listing name with + | None -> None + | Some slot -> + (match Wire.field (inspect ~path slot) "addr" with + | Some { Form.v = Form.Int a; _ } -> Some a + | _ -> None) + in + (* An element is its index's worth of elements past the first. *) + (match addr "xs" "()", addr "xs" "(1)" with + | Some a, Some b when Int64.sub b a = 4L -> () + | a, b -> + fail "xs[1] is not one i32 past xs: %s, %s" + (Option.fold ~none:"none" ~some:Int64.to_string a) + (Option.fold ~none:"none" ~some:Int64.to_string b)); + (* An option's payload is inside the option's own storage. *) + (match addr "box" "()", addr "box" "(some)" with + | Some a, Some b when b > a && Int64.sub b a < 16L -> () + | _ -> fail "box's payload is not inside box's storage"); + (* A data case's field has no accessor place to take the address + of, and says nothing rather than a copy's address. *) + if addr "s" "(\"Shape.Rect.w\")" <> None then + fail "a data case's field answered with an address"); (* Emacs prints an empty list as `nil' and has no other spelling for one, so a client in that language cannot send `()'. *) (match slot_of listing "mark" with @@ -3921,6 +3946,14 @@ let () = | Some v when int_of_string_opt v <> None -> () | Some v -> fail "watch rendered a computed i64 as %s" v | None -> fail "the (watch ...) form never wrote a scalar"); + (match List.assoc_opt "d" t with + | Some "{ :a 1}" -> () + | Some v -> fail "watch rendered a dyn map as %s" v + | None -> fail "the (watch ...) form never wrote a dyn value"); + (match List.assoc_opt "s" t with + | Some "\"x\"" -> () + | Some v -> fail "the (watch ...) form rendered a string as %s" v + | None -> fail "the (watch ...) form never wrote a string"); (* The accumulator, which is the other half of the watch and the half a scalar row cannot stand in for. [loop-cells] samples "cell" eight times per step at 0, 3, ... 21, so the row has to show a