From a0610cecd51a32681180ddc11224a59aee4c0956 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 15:06:49 +0700 Subject: [PATCH] A number in the inspector reads in the two bases it was written in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decimal is what the value is and stays first; hex and binary go beside it. It 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 — and reading those out of a decimal is arithmetic done by hand. In two places: under the header of a value opened on its own, and on each numeric row of a field list. The second is the one that matters, because a leaf cannot be stepped into, so the field list is the only place most numbers are ever seen. Nothing is asked of the program. It is arithmetic on text the renderer already wrote, so it works on a stopped program and costs no round trip. Binary is grouped in nibbles because a mask is read in nibbles. A negative is shown as the 64-bit two's complement it is in memory and says the width out loud: the rendered value carries none, and every Flan integer comes back through i64. A float is left alone rather than answered wrongly — its bits are an IEEE layout, reinterpreting them is a different question, and the rendered text does not carry the width to answer it. The pointer half of this is not done and the refusal now says why. Render.render 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 pins the current text for that reason. Showing one is a decision about the language's printer, not about this buffer. --- emacs/flan-inspect.el | 75 ++++++++++++++++++++++++++++++++++++++-- emacs/test-flan-cider.el | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) 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