diff --git a/emacs/flan-inspect.el b/emacs/flan-inspect.el index 74d1e48..44cb63c 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -230,7 +230,13 @@ reply without a daemon behind them, and so that this file names ('option "an option's payload: Flan has no accessor form that reaches it, so there is no expression to send") ('ptr - "a pointer: the renderer never follows one, and dereferencing a pointer on your behalf is not safe") + ;; And its address is not here either. `Render.render' (lib/render.ml) + ;; writes the bare word `' for every pointer, on purpose: it is the + ;; same renderer `print' uses, an address is not stable across runs, and + ;; test_acceptance.ml pins the current text for exactly that reason. + ;; Showing the address needs the renderer to write it, which is a decision + ;; about the language's printer rather than about this buffer. + "a pointer: the renderer never follows one, and dereferencing a pointer on your behalf is not safe. It does not write the address either — `print' uses the same renderer, and an address is not stable across runs") ('trunc "truncated: the walk stopped at its depth bound of 4. Inspect the field that holds it, which re-roots the walk") ('opaque @@ -250,6 +256,60 @@ reply without a daemon behind them, and so that this file names (let ((k (car child))) (if (integerp k) (format "%d." k) (format ":%s" k)))) +;;; What a leaf is, beyond its decimal + +;; A number is shown in decimal and nothing else, which is the wrong base for +;; about half the numbers anyone opens this buffer for: a colour is +;; `0x303030FF', a gesture is an OR of flags, a mask is read a bit at a time. +;; The decimal is still first — it is what the value *is* — and the other two +;; bases are beside it. +;; +;; Nothing is asked of the program for this. It is arithmetic on a number the +;; renderer already wrote, which is why it works on a stopped program and costs +;; no round trip. + +(defun flan-inspect--integer (node) + "NODE's value as an integer, or nil if it is not written as one. +Only a bare decimal integer counts. A float is not one: its bits are an IEEE +layout and reinterpreting them would be a different question from this, and one +the rendered text does not carry the width to answer." + (let ((text (plist-get node :text))) + (and (eq (plist-get node :kind) 'atom) + (stringp text) + (string-match-p "\\`-?[0-9]+\\'" text) + (string-to-number text)))) + +(defun flan-inspect--binary (n) + "N in base two, grouped in fours." + (let ((bits "") (n n)) + (if (zerop n) + "0" + (while (> n 0) + (setq bits (concat (number-to-string (logand n 1)) bits) + n (ash n -1))) + ;; Nibbles, because a mask is read in nibbles. Pad to a multiple of four + ;; so the groups line up with the hex beside them. + (let ((pad (% (- 4 (% (length bits) 4)) 4))) + (setq bits (concat (make-string pad ?0) bits))) + (let ((out nil) (i 0)) + (while (< i (length bits)) + (push (substring bits i (min (+ i 4) (length bits))) out) + (setq i (+ i 4))) + (string-join (nreverse out) "_"))))) + +(defun flan-inspect--detail (node) + "The other bases NODE reads in, as a string, or nil. +A negative number is shown as the 64-bit two's complement it is in memory, and +says so: the rendered value carries no width, so 64 is the only one that can be +stated honestly — every Flan integer is rendered through i64." + (let ((n (flan-inspect--integer node))) + (when n + (if (>= n 0) + (format "0x%X 0b%s" n (flan-inspect--binary n)) + (let ((two (logand n #xFFFFFFFFFFFFFFFF))) + (format "0x%X 0b%s (two's complement, 64-bit)" + two (flan-inspect--binary two))))))) + (defun flan-inspect--summary (node) "One line for NODE, as it appears beside its label." (pcase (plist-get node :kind) @@ -273,6 +333,10 @@ reply without a daemon behind them, and so that this file names ('ptr "a pointer — never followed\n") (_ (format "%s\n" (plist-get node :text)))) 'face 'font-lock-type-face)) + ;; The other bases, under the value rather than beside it: this is the line + ;; 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)))) ;; 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. @@ -298,7 +362,14 @@ reply without a daemon behind them, and so that this file names (start (point))) (insert (format " %s%s " label (make-string (- w (length label)) ?\s))) - (insert (flan-inspect--summary child) "\n") + (insert (flan-inspect--summary child)) + ;; A numeric field carries its other bases here, where the field + ;; list is read, rather than only when it is opened on its own — + ;; and a leaf cannot be opened on its own at all. + (let ((detail (flan-inspect--detail child))) + (when detail + (insert (propertize (concat " " detail) 'face 'shadow)))) + (insert "\n") (add-text-properties start (point) (list 'flan-inspect-step step diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index 89dab19..fbc7d49 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -136,6 +136,74 @@ (test-flan--check "a struct with fields does not refuse" (null (flan-inspect-refusal (flan-inspect-parse "(V {:x 1 :y 2})")))) +(test-flan--check "and a pointer says the address is not there either" + (string-match-p "does not write the address" + (flan-inspect-refusal + (flan-inspect-parse "")))) + + +;;; What a number reads as in the other two bases + +;; Arithmetic on text the renderer already wrote: nothing is asked of the +;; program, which is why these need no daemon and no fixture beyond the string. + +(message "\na number in the other bases") + +(dolist (case '(("255" "0xFF" "0b1111_1111") + ("0" "0x0" "0b0") + ("7" "0x7" "0b0111") + ;; sand.flan's world-edge colour, which is the case this is for: + ;; written 0x303030FF in the source and decimal everywhere else. + ("808464639" "0x303030FF" "0b0011_0000_0011_0000_0011_0000_1111_1111"))) + (let ((detail (flan-inspect--detail (flan-inspect-parse (nth 0 case))))) + (test-flan--check (format "%s is %s" (nth 0 case) (nth 1 case)) + (and detail (string-match-p (regexp-quote (nth 1 case)) detail))) + (test-flan--check (format "%s is %s" (nth 0 case) (nth 2 case)) + (and detail (string-match-p (regexp-quote (nth 2 case)) detail))))) + +;; A negative is the 64-bit two's complement it is in memory, and says so: the +;; rendered value carries no width, and every Flan integer comes back through +;; i64, so 64 is the one that can be stated honestly. +(let ((detail (flan-inspect--detail (flan-inspect-parse "-1")))) + (test-flan--check "a negative is two's complement, at a stated width" + (and detail + (string-match-p "0xFFFFFFFFFFFFFFFF" detail) + (string-match-p "64-bit" detail)))) + +;; The whole u64 range, which is past a fixnum and relies on bignums. +(test-flan--check "a u64 at the top of its range" + (string-match-p + "0xFFFFFFFFFFFFFFFF" + (or (flan-inspect--detail + (flan-inspect-parse "18446744073709551615")) ""))) + +;; Not everything that looks numeric is one. A float's bits are an IEEE layout +;; and the rendered text does not carry the width to reinterpret them, so it is +;; left alone rather than answered wrongly. +(dolist (text '("1.5" "-0.25" "true" ":green" "none" "..." "" "\"7\"")) + (test-flan--check (format "%s has no other base" text) + (null (flan-inspect--detail (flan-inspect-parse text))))) + +;; And it reaches the buffer, in both places it is drawn: the header of a value +;; opened on its own, and the field list where a leaf can only ever be seen. +(let ((flan-inspect-request-function + (lambda (_) '(:status "ok" :value "255"))) + (flan-inspect-buffer " *test-inspect*")) + (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)))) + +(let ((flan-inspect-request-function + (lambda (_) '(:status "ok" :value "(Mask {:bits 255 :name \"all\"})"))) + (flan-inspect-buffer " *test-inspect*")) + (let ((text (with-current-buffer (save-window-excursion (flan-inspect "m")) + (buffer-string)))) + (test-flan--check "and a numeric field shows them in the field list" + (string-match-p ":bits +255 +0xFF 0b1111_1111" text)) + (test-flan--check "while a string field is left alone" + (string-match-p ":name +\"all\"\n" text)))) + ;;; The inspector buffer