diff --git a/TODO.org b/TODO.org index 81a7cdc8..0cc26b67 100644 --- a/TODO.org +++ b/TODO.org @@ -1721,8 +1721,13 @@ form is evaluated again plainly. Both were daemon ops with nothing calling them. One command shows the backtrace with the selected frame's locals. -** TODO 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. +** 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. ** 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/MANUAL.md b/emacs/MANUAL.md index 444869b8..a20ab958 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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** 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 -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 the kind a debugger, a valgrind report or a C shim's `printf` hands you. There diff --git a/emacs/flan-inspect.el b/emacs/flan-inspect.el index be5c8be2..32ab9302 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -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 renderer writes, so the buffer you type into is the value's own spelling and 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 "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 @@ -511,9 +518,10 @@ stated honestly — every Flan integer is rendered through i64." ('option (format "(some …)")) (_ (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. -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)) (erase-buffer) (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. (let ((detail (flan-inspect--detail node))) (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 difference between a value and *which* value, and the thing that was ;; 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-inspect--root-label root path))) :type (plist-get r :type) + :addr (plist-get r :addr) ;; The stop the read happened at, where the reply carried one. It ;; travels with the value rather than being asked for separately, ;; 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--type (plist-get answer :type)) (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--editing nil) ;; 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-local truncate-lines t) (flan-inspect--render root path flan-inspect--node stack - flan-inspect--type)) + flan-inspect--type flan-inspect--addr)) (display-buffer buf) buf)) diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index 2ed8a8a4..017d3f73 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -196,7 +196,20 @@ (let ((text (with-current-buffer (save-window-excursion (flan-inspect "flags")) (buffer-string)))) (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 (lambda (_) '(:status "ok" :value "(Mask {.bits 255 .name \"all\"})"))) diff --git a/lib/dev.ml b/lib/dev.ml index 2628ee7f..3e0fdf6f 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -2376,13 +2376,26 @@ let inspect t ~frame ~slot ~path = | Ok (c, label, ty) -> (match run_render_thunk t ~tag:"i" ~c with | Error m -> error m - | Ok v -> - (* One value and nothing else, so the whole of what came back is - it — minus the trailing newline the renderer does not write - here, because there is no second line to separate it from. *) + | Ok out -> + (* The address the value is stored at, on a line of its own, and + then the value. See [Session.render_slot]. *) + 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 - [ ":frame " ^ Wire.quote name; ":name " ^ Wire.quote label; - ":type " ^ Wire.quote ty; ":value " ^ Wire.quote v; + ([ ":frame " ^ Wire.quote name; ":name " ^ Wire.quote label; + ":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 what is on the screen can name it and be refused if 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 at, and an editor that asked for it separately would be 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 diff --git a/lib/session.ml b/lib/session.ml index 4a8df7f1..e7522621 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -1681,6 +1681,29 @@ 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 + 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 t.thunks <- t.thunks + 1; let tname = Printf.sprintf "inspect/%d" t.thunks in diff --git a/test/test_dev.ml b/test/test_dev.ml index 192f338b..3136112d 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -2367,6 +2367,24 @@ let () = want "box" "(some)" "Point" "(Point {.x 4.5 .y 5.5})"; want "box" "(some \"x\")" "f32" "4.5"; 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 one, so a client in that language cannot send `()'. *) (match slot_of listing "mark" with