The inspector says where a slot's value is stored

This commit is contained in:
Joseph Ferano 2026-09-25 07:27:25 +07:00
parent 85b9f35a66
commit 377ada8d27
7 changed files with 102 additions and 14 deletions

View File

@ -1721,8 +1721,13 @@ form is evaluated again plainly.
Both were daemon ops with nothing calling them. One command shows the backtrace Both were daemon ops with nothing calling them. One command shows the backtrace
with the selected frame's locals. with the selected frame's locals.
** TODO Hex, binary and an address on a primitive in the inspector ** DONE Hex, binary and an address on a primitive in the inspector
The last item of the Emacs batch besides the break buffer, and independent of it. 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.
** DONE A defclass is not on the definitions list as a type, and a sum's cases are not drawn ** DONE A defclass is not on the definitions list as a type, and a sum's cases are not drawn
CLOSED: [2026-09-25] CLOSED: [2026-09-25]

View File

@ -467,7 +467,9 @@ drew. It reaches an **option's payload** and a **union case's fields**, which
have offsets but no accessor form to write. In exchange it needs a **stopped** have offsets but no accessor form to write. In exchange it needs a **stopped**
program, and it is refused — by name, with the reason — once the program program, and it is refused — by name, with the reason — once the program
resumes or if the frame's body was redefined since the frame was entered. It resumes or if the frame's body was redefined since the frame was entered. It
cannot start from an expression at all. cannot start from an expression at all. Because it reads a place in the stopped
frame, the buffer also shows where that place is stored, as `at 0x…` under the
value.
**An address** — `M-x flan-inspect-address`. A number, `#x7f…` or decimal, of **An address** — `M-x flan-inspect-address`. A number, `#x7f…` or decimal, of
the kind a debugger, a valgrind report or a C shim's `printf` hands you. There the kind a debugger, a valgrind report or a C shim's `printf` hands you. There

View File

@ -426,6 +426,13 @@ an atom does not carry its type.")
Kept because it is the thing edit mode hands you: a Flan literal is what the Kept because it is the thing edit mode hands you: a Flan literal is what the
renderer writes, so the buffer you type into is the value's own spelling and renderer writes, so the buffer you type into is the value's own spelling and
not a second notation invented for editing it.") not a second notation invented for editing it.")
(defvar-local flan-inspect--addr nil
"Where the value on screen is stored, as a number, or nil.
The slot root answers with it, since it is reading a place in a stopped frame,
and so does the address root. An expression root's reply carries none: the
value it renders is the result of evaluating the expression, which is not
stored anywhere the program keeps.")
(defvar-local flan-inspect--at-stop nil (defvar-local flan-inspect--at-stop nil
"Which stop this buffer was drawn at, as the daemon numbered it. "Which stop this buffer was drawn at, as the daemon numbered it.
Nil where the reply did not say — only the slot root does, because only it Nil where the reply did not say — only the slot root does, because only it
@ -511,9 +518,10 @@ stated honestly — every Flan integer is rendered through i64."
('option (format "(some …)")) ('option (format "(some …)"))
(_ (plist-get node :text)))) (_ (plist-get node :text))))
(defun flan-inspect--render (root path node stack &optional declared) (defun flan-inspect--render (root path node stack &optional declared addr)
"Draw NODE, reached by ROOT walked by PATH, with STACK behind it. "Draw NODE, reached by ROOT walked by PATH, with STACK behind it.
DECLARED is the type the daemon named, when it named one." DECLARED is the type the daemon named, when it named one. ADDR is where the
value is stored, when the reply said."
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
(insert (propertize (flan-inspect--root-label root path) (insert (propertize (flan-inspect--root-label root path)
@ -537,6 +545,10 @@ DECLARED is the type the daemon named, when it named one."
;; someone opened the inspector on a number *for*, and it is long. ;; someone opened the inspector on a number *for*, and it is long.
(let ((detail (flan-inspect--detail node))) (let ((detail (flan-inspect--detail node)))
(when detail (insert (propertize (concat detail "\n") 'face 'shadow)))) (when detail (insert (propertize (concat detail "\n") 'face 'shadow))))
;; Where it lives, in the base an address is read in. An address root
;; names its address in the line above already.
(when (and addr (not (eq (car-safe root) :addr)))
(insert (propertize (format "at 0x%X\n" addr) 'face 'shadow)))
;; The stack made visible. CIDER keeps it and does not show it; here it is ;; The stack made visible. CIDER keeps it and does not show it; here it is
;; the difference between a value and *which* value, and the thing that was ;; the difference between a value and *which* value, and the thing that was
;; typed at the root is often several steps back by now. ;; typed at the root is often several steps back by now.
@ -644,6 +656,7 @@ and whether that may be followed is the answer"))
"flan: the program answered without a value for %s" "flan: the program answered without a value for %s"
(flan-inspect--root-label root path))) (flan-inspect--root-label root path)))
:type (plist-get r :type) :type (plist-get r :type)
:addr (plist-get r :addr)
;; The stop the read happened at, where the reply carried one. It ;; The stop the read happened at, where the reply carried one. It
;; travels with the value rather than being asked for separately, ;; travels with the value rather than being asked for separately,
;; because asked separately it would be a second question about a ;; because asked separately it would be a second question about a
@ -663,6 +676,7 @@ and whether that may be followed is the answer"))
(setq flan-inspect--node (flan-inspect-parse flan-inspect--rendered)) (setq flan-inspect--node (flan-inspect-parse flan-inspect--rendered))
(setq flan-inspect--type (plist-get answer :type)) (setq flan-inspect--type (plist-get answer :type))
(setq flan-inspect--at-stop (plist-get answer :at-stop)) (setq flan-inspect--at-stop (plist-get answer :at-stop))
(setq flan-inspect--addr (plist-get answer :addr))
(setq flan-inspect--stack stack) (setq flan-inspect--stack stack)
(setq flan-inspect--editing nil) (setq flan-inspect--editing nil)
;; Put back what editing turned off, here and not in the two commands ;; Put back what editing turned off, here and not in the two commands
@ -673,7 +687,7 @@ and whether that may be followed is the answer"))
(setq buffer-read-only t) (setq buffer-read-only t)
(setq-local truncate-lines t) (setq-local truncate-lines t)
(flan-inspect--render root path flan-inspect--node stack (flan-inspect--render root path flan-inspect--node stack
flan-inspect--type)) flan-inspect--type flan-inspect--addr))
(display-buffer buf) (display-buffer buf)
buf)) buf))

View File

@ -196,7 +196,20 @@
(let ((text (with-current-buffer (save-window-excursion (flan-inspect "flags")) (let ((text (with-current-buffer (save-window-excursion (flan-inspect "flags"))
(buffer-string)))) (buffer-string))))
(test-flan--check "a number opened on its own shows its bases" (test-flan--check "a number opened on its own shows its bases"
(string-match-p "0xFF 0b1111_1111" text)))) (string-match-p "0xFF 0b1111_1111" text))
(test-flan--check "an expression's value has no address line"
(not (string-match-p "^at 0x" text)))))
;; Where a slot's value is stored, which the slot root's reply carries: under
;; the value, in hex.
(let ((flan-inspect-request-function
(lambda (_) '(:status "ok" :type "i32" :value "7" :addr 140737488345360)))
(flan-inspect-buffer " *test-inspect*"))
(let ((text (with-current-buffer
(save-window-excursion (flan-inspect-slot 1 0 "n"))
(buffer-string))))
(test-flan--check "a slot's value says where it is stored"
(string-match-p "^at 0x7FFFFFFFD910$" text))))
(let ((flan-inspect-request-function (let ((flan-inspect-request-function
(lambda (_) '(:status "ok" :value "(Mask {.bits 255 .name \"all\"})"))) (lambda (_) '(:status "ok" :value "(Mask {.bits 255 .name \"all\"})")))

View File

@ -2376,13 +2376,26 @@ let inspect t ~frame ~slot ~path =
| Ok (c, label, ty) -> | Ok (c, label, ty) ->
(match run_render_thunk t ~tag:"i" ~c with (match run_render_thunk t ~tag:"i" ~c with
| Error m -> error m | Error m -> error m
| Ok v -> | Ok out ->
(* One value and nothing else, so the whole of what came back is (* The address the value is stored at, on a line of its own, and
it — minus the trailing newline the renderer does not write then the value. See [Session.render_slot]. *)
here, because there is no second line to separate it from. *) let addr, v =
match String.index_opt out '\n' with
| Some i ->
( Int64.of_string_opt (String.sub out 0 i),
String.sub out (i + 1) (String.length out - i - 1) )
| None -> (None, out)
in
ok ok
[ ":frame " ^ Wire.quote name; ":name " ^ Wire.quote label; ([ ":frame " ^ Wire.quote name; ":name " ^ Wire.quote label;
":type " ^ Wire.quote ty; ":value " ^ Wire.quote v; ":type " ^ Wire.quote ty; ":value " ^ Wire.quote v ]
@ (match addr with
(* Unsigned, as an address is: a user-space pointer is
positive as an i64 today, and printing it as the
unsigned number keeps that true if it ever is not. *)
| Some a -> [ Printf.sprintf ":addr %Lu" a ]
| None -> [])
@ [
(* Which stop this was read at, so that a write built from (* Which stop this was read at, so that a write built from
what is on the screen can name it and be refused if the what is on the screen can name it and be refused if the
program has been round the loop since. Nothing about the program has been round the loop since. Nothing about the
@ -2390,7 +2403,7 @@ let inspect t ~frame ~slot ~path =
moment at which it is true of what the reader is looking moment at which it is true of what the reader is looking
at, and an editor that asked for it separately would be at, and an editor that asked for it separately would be
asking a second time about a different instant. *) asking a second time about a different instant. *)
":at-stop " ^ string_of_int (Option.value ~default:0 (stop_gen t)) ])) ":at-stop " ^ string_of_int (Option.value ~default:0 (stop_gen t)) ])))
(* [(:op "set" :frame N :slot I :path (...) :edits (...) :at-stop G)] — the (* [(:op "set" :frame N :slot I :path (...) :edits (...) :at-stop G)] — the

View File

@ -1681,6 +1681,29 @@ let render_slot ?(origin = "<inspect>") t ~frame ~(fn : Tast.fn) ~slot ~path
(match Render.render c 0 v with (match Render.render c 0 v with
| exception Loc.Error { Loc.dmsg = why; _ } -> Error (name ^ path_text path ^ ": " ^ why) | exception Loc.Error { Loc.dmsg = why; _ } -> Error (name ^ path_text path ^ ": " ^ why)
| parts -> | 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
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
in
let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in let nullary n = { Tast.e = Tast.Call (n, []); ty = Types.Unit; loc } in
t.thunks <- t.thunks + 1; t.thunks <- t.thunks + 1;
let tname = Printf.sprintf "inspect/%d" t.thunks in let tname = Printf.sprintf "inspect/%d" t.thunks in

View File

@ -2367,6 +2367,24 @@ let () =
want "box" "(some)" "Point" "(Point {.x 4.5 .y 5.5})"; want "box" "(some)" "Point" "(Point {.x 4.5 .y 5.5})";
want "box" "(some \"x\")" "f32" "4.5"; want "box" "(some \"x\")" "f32" "4.5";
want "s" "(\"Shape.Rect.w\")" "i32" "3"; want "s" "(\"Shape.Rect.w\")" "i32" "3";
(* Where each value is stored. The struct and its first field share
an address and the second field is one f32 further on, so the
number is the layout's and not a label. *)
(match slot_of listing "mark" with
| None -> ()
| Some slot ->
let addr path =
match Wire.field (inspect ~path slot) "addr" with
| Some { Form.v = Form.Int a; _ } -> Some a
| _ -> None
in
(match addr "()", addr "(\"x\")", addr "(\"y\")" with
| Some m, Some x, Some y ->
if m <> x then
fail "mark is at %Ld and its first field at %Ld" m x;
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"));
(* Emacs prints an empty list as `nil' and has no other spelling for (* Emacs prints an empty list as `nil' and has no other spelling for
one, so a client in that language cannot send `()'. *) one, so a client in that language cannot send `()'. *)
(match slot_of listing "mark" with (match slot_of listing "mark" with